From: Bartek Przybylski Date: Sat, 21 Jul 2012 20:10:36 +0000 (+0200) Subject: adding translations and transifex third party tool X-Git-Tag: oc-android-1.4.3~256 X-Git-Url: http://git.linex4red.de/pub/Android/ownCloud.git/commitdiff_plain/adf87805bb2ffb1f297867dcee8dc21264209496?ds=inline adding translations and transifex third party tool --- diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..ff4649ac --- /dev/null +++ b/.tx/config @@ -0,0 +1,7 @@ +[main] +host = https://www.transifex.com + +[owncloud.android] +file_filter = res/values-/strings.xml +source_lang = en + diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-af/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ar/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ca/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-da/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml new file mode 100644 index 00000000..92a5b1a2 --- /dev/null +++ b/res/values-de/strings.xml @@ -0,0 +1,128 @@ + + + Hallo Welt, ownCloudStartBildschirm! + ownCloud + Passwort: + Benutzername + Anmelden + Herzlich willkommen bei ihrer ownCloud + Dateien + Musik + Kontakte + Kalender + Lesezeichen + Einstellungen + Konto einrichten + Auf Ihrem Gerät sind keine Konten eingerichtet. Bitte erstellen Sie ein Konto, um diese App nutzen zu können. + Konto synchronisieren + Datei hochladen + Verzeichnis erstellen + Suche + Einstellungen + Allgemein + Gerät verfolgen + Neue Sitzung hinzufügen + Bildvorschauen erstellen + Konto auswählen + Bitte wählen, welches Konto von der App verwendet werden soll + Gerät verfolgen + Geräteverfolgung in ownCloud aktivieren + Ihre ownCloud verfolgt dieses Gerät + Aktualisierungsintervall + Alle %1$s Minuten aktualisieren + Konten + ownCloud URL + Benutzername + Passwort + Falsche URL gegeben + Nicht gültiger Sitzungsname + Dateien + Sie haben keine Datei für das Hochladen ausgewählt + Benutzername + Passwort + Internetadresse + Passwort anzeigen? + Mit ihrer ownCloud verbinden + Verbinden + Hochladen + kein Konto gefunden + Es sind keine ownCloud Konten auf ihrem Gerät eingerichtet. Bitte richten Sie zuerst ein Konto ein. + Einrichten + Beenden + Hochladen + Verzeichnis für hochgeladene Dateien erstellen + Klicken Sie auf eine Datei, um mehr Informationen zu bekommen. + Größe: + Art: + Erstellt: + Geändert: + Herunterladen + Öffnen + Ja + Nein + OK + Abbrechen + Speichern & Schließen + Fehler + Verzeichnisname + Hochladen erfolgreich beendet + Hochladen fehlgeschlagen + Dateien hochgeladen + Das Herunterladen konnte nicht erfolgreich beendet werden + Konto auswählen + Kontakte + Sichere Verbindung nutzen + ownCloud kann Ihr Gerät nicht verfolgen. Bitte überprüfen Sie Ihre Standorteinstellungen + Bitte geben Sie Ihre PIN ein + Bitte geben Sie Ihre neue PIN ein + Bitte geben Sie Ihre PIN ein + Bitte geben Sie Ihre PIN erneut ein. + App PIN entfernen + Die PINs stimmen nicht überein + Falsche PIN + App PIN wurde entfernt + App PIN wurde gespeichert + + 15 Minuten + 30 Minuten + 60 Minuten + + + 15 + 30 + 60 + + Versuche sich anzumelden + Keine Netzwerkverbindung + Es konnte keine Netzwerkverbindung gefunden werden, bitte überprüfen Sie Ihre Internetverbindung + Trotzdem verbinden + Sichere Verbindung nicht verfügbar. + Die App konnte keine sichere Verbindung zum Server herstellen. Eine nicht sichere Verbindung ist nichts desto trotz verfügbar. Wollen Sie fortfahren oder abbrechen. + Verbindung hergestellt + Verbindung testen. + falsch konfigurierte ownCloud + Es scheint als wäre Ihre ownCloud Installation nicht richtig konfiguriert. Bitte kontaktieren Sie ihren Administrator, um weitere Details zu erhalten. + Ein unbekannter Fehler ist aufgetreten + Ein unbekannter Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator mit den Log Dateien Ihres Gerätes. + Es konnte keine Verbindung hergestellt werden + Es konnte keine Verbindung zum Host hergestellt werden. Bitte überprüfen Sie Hostname und Server Verfügbarkeit und versuchen Sie es erneut. + ownCloud Installation nicht gefunden + Die App konnte ownCloud unter dem gegebenen Pfad nicht finden. Bitte überprüfen Sie den Pfad und versuchen Sie es erneut. + Sichere Verbindung hergestellt + Unbekannter Fehler aufgetreten + Anmeldedetails + Die App ist abgestürzt. Möchten Sie einen Bericht senden? + Bericht senden + Keinen Bericht senden + Erweiterung verfügbar! + Es scheint, dass ihre ownCloud Erweiterungen unterstützt. Wollen Sie verfügbare Erweiterungen für Android sehen? + Datei aktuell halten + Teilen + Umbenennen + Löschen + Wollen Sie wirklich + + Erfolgreich gelöscht + Löschen konnte nicht beendet werden + Unerwartetes Problem; Bitte versuchen Sie die Datei in einer anderen App zu öffnen + diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-el/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-eo/strings.xml b/res/values-eo/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-eo/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml new file mode 100644 index 00000000..51c0a12c --- /dev/null +++ b/res/values-es/strings.xml @@ -0,0 +1,128 @@ + + + Hola Mundo, ¡PantallaPrincipalOwnCloud! + ownCloud + Contraseña: + Nombre de usuario: + Inicio de sesión + Bienvenido a tu ownCloud + Archivos + Música + Contactos + Calendario + Marcadores + Ajustes + Configuración de cuenta + No hay cuentas de ownCloud en tu dispositivo. Para usar esta aplicación, necesitas crear una. + Sincronización de cuenta + Subir archivo + Crear directorio + Buscar + Ajustes + General + Rastreo de dispositivo + Agregar nueva sesión + Crear miniaturas de imagen + Seleccionar una cuenta + Elige, cuál de tus cuentas la aplicación debería usar. + Rastreo de dispositivo + Habilitar ownCloud para rastrear la localización de tu dispositivo + Tu ownCloud realiza un seguimiento de este dispositivo + Intervalo de actualización + Actualizar cada %1$s minutos + Cuentas + URL de ownCloud + Nombre de usuario + Contraseña + La URL dada es incorrecta + Nombre de sesión incorrecto + Archivos + No se seleccionaron archivos para enviar + Nombre de usuario + Contraseña + Dirección web + ¿Mostrar contraseña? + Conectar a tu ownCloud + Conectar + Subir + No se encontraron cuentas + No hay cuentas de ownCloud en tu dispositivo. Por favor configura una cuenta primero. + Configuración + Salir + Enviando + Crear directorio para envío + Pulsa sobre un archivo para mostrar información adicional. + Tamaño: + Tipo: + Creado: + Modificado: + Descargar + Abrir + Sí + No + Aceptar + Cancelar + Guardar & Salir + Error + Nombre de directorio + Enviado correctamente + Falló el envío: + archivos subidos + La descarga no pudo completarse + Elige una cuenta + Contactos + Usar conexión segura + ownCloud no puede rastear tu dispositivo. Por favor chequea tu configuración de localización + Por favor, inserta tu PIN de aplicación + Por favor, inserta tu nuevo PIN de aplicación + Ingrese PIN de aplicación ownCloud + Reingrese PIN de aplicación ownCloud, por favor + Borrar tu PIN de aplicación ownCloud + Los PIN de aplicación ownCloud no son iguales + PIN de aplicación ownCloud incorrecto + PIN de aplicación ownCloud borrado + PIN de aplicación ownCloud almacenado + + 15 minutos + 30 minutos + 60 minutos + + + 15 + 30 + 60 + + Intentado iniciar sesión... + Sin conexión de red + No se ha detectado una conexión de red, chequea tu conexión a internet e intenta nuevamente. + Conectar de todos modos + Conexión segura no disponible. + La aplicación no pudo establecer una conexión segura al servidor. Aunque no haya una conexión segura disponible, puedes continuar o cancelar. + Conexión establecida + Probando conexión... + Configuración de ownCloud en formato incorrecto + Parece que tu instancia de ownCloud no está correctamente configurada. Contacta a tu administrador para más detalles. + Ocurrió un error desconocido + Ocurrió un error desconocido. Por favor, contacta a los autores e incluye los registros de tu dispositivo. + No se pudo establecer la conexión + No se pudo establecer la conexión hacia el servidor. Por favor chequea el nombre del host y la disponibilidad del servidor e intenta nuevamente. + Instancia de ownCloud no encontrada + La aplicación no pudo encontrar la instancia de ownCloud en la ruta de acceso dada. Por favor chequea tu ruta de acceso e intenta nuevamente. + Conexión segura establecida + ¡Ocurrió un error desconocido! + Detalles de inicio de sesión + La aplicación finalizó inesperadamente. ¿Desea enviar un reporte de error? + Enviar reporte + No enviar reporte + ¡Extensiones disponibles! + Parece que su instancia de ownCloud soporta extensiones avanzadas. ¿Desea ver las extensiones disponibles para android? + Mantener el archivo actualizado + Compartir + Renombrar + Borrar + ¿Está seguro que quiere + + Borrado correctamente + El borrado no pudo ser completado + Problema inesperado; por favor, prueba otra app para seleccionar el archivo + diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-eu/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-fa/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-fr/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-gl/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-he/strings.xml b/res/values-he/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-he/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-hr/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-hy/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-ia/strings.xml b/res/values-ia/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ia/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-id/strings.xml b/res/values-id/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-id/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml new file mode 100644 index 00000000..bbdf0097 --- /dev/null +++ b/res/values-it/strings.xml @@ -0,0 +1,69 @@ + + + ownCloud + Password: + Benvenuto nel tuo ownCloud + File + Musica + Contatti + Calendario + Segnalibri + Impostazioni + Carica un file + Crea cartella + Cerca + Impostazioni + Aggiungi nuova sessione + Scegli un account + Scegli, quale dei tuoi account la app deve usare + Localizzazione del dispositivo + Intervallo di aggiornamento + Aggiorna ogni %1$s minuti + Nome utente + Password + File + Password + Indirizzo web + Mostra password? + Connetti al tuo ownCloud + Connetti + Carica + Non ci sono account ownCloud sul tuo dispositivo. Per favore prima imposta un account. + Esci + Dimensione: + Tipo: + Creato: + Modificato: + Apri + Sì + No + OK + Errore + Nome cartella + Scegli account + Contatti + Usa connessione sicura + + 15 Minuti + 30 Minuti + 60 Minuti + + + 15 + 30 + 60 + + Nessuna connessione a internet + Non sono state rilevate connessioni alla rete, controlla la tua connessione Internet e prova ancora. + Errore sconosciuto + Impossibile stabilire una connessione + Impossibile stabilire una connessione + Errore sconosciuto! + Condividi + Rinomina + Rimuovi + Vuoi davvero + + Rimozione effettuata con successo + La rimozione non può essere completata + diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ko/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-lb/strings.xml b/res/values-lb/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-lb/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-mk/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-nl/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml new file mode 100644 index 00000000..0c9c3343 --- /dev/null +++ b/res/values-pl/strings.xml @@ -0,0 +1,43 @@ + + + ownCloud + Hasło: + Nazwa użytkownika: + Login + Witaj w ownCloud + Ustawienia + Ogólne + Nazwa użytkownika + Hasło + Nazwa użytkownika + Hasło + Pokazać hasło? + Połącz + Wyjdź + Rozmiar: + Typ: + Utworzony: + Zmodyfikowany: + Pobierz + Otwórz + Tak + Nie + OK + Anuluj + Zapisz i wyjdź + Błąd + Nazwa katalogu + Wybierz konto + ownCloud + Logowanie... + Brak połączenia sieciowego + Połącz mimo wszystko + Połączenie nawiązane + Testowanie połączenia... + Nie można nawiązać połączenia + Wyślij raport + Nie wysyłaj + Zmień nazwę + Usuń + Czy na pewno chcesz + diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ro/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-ru/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-sl/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-so/strings.xml b/res/values-so/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-so/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-sr/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-sv/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-tr/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-uk/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml new file mode 100644 index 00000000..c757504a --- /dev/null +++ b/res/values-vi/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/third_party/transifex-client/.gitignore b/third_party/transifex-client/.gitignore new file mode 100644 index 00000000..72bd50ca --- /dev/null +++ b/third_party/transifex-client/.gitignore @@ -0,0 +1,5 @@ +.tx +*pyc +*pyo +*~ +*egg-info* diff --git a/third_party/transifex-client/DEVELOPMENT.rst b/third_party/transifex-client/DEVELOPMENT.rst new file mode 100644 index 00000000..992e518f --- /dev/null +++ b/third_party/transifex-client/DEVELOPMENT.rst @@ -0,0 +1,21 @@ +Releasing +========= + +To create a new release: + +1. Update local rep and update the version in ``setup.py``:: + + $ hg pull -u + $ vim setup.py + +2. Test:: + + $ python setup.py clean sdist + $ cd dist + $ tar zxf ... + $ cd transifex-client + ...test + +3. Package and upload on PyPI:: + + $ python setup.py clean sdist bdist_egg upload diff --git a/third_party/transifex-client/LICENSE b/third_party/transifex-client/LICENSE new file mode 100644 index 00000000..add7251c --- /dev/null +++ b/third_party/transifex-client/LICENSE @@ -0,0 +1,344 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software + interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Program does not specify a +version number of this License, you may choose any version ever +published by the Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. + diff --git a/third_party/transifex-client/MANIFEST.in b/third_party/transifex-client/MANIFEST.in new file mode 100644 index 00000000..83126ac6 --- /dev/null +++ b/third_party/transifex-client/MANIFEST.in @@ -0,0 +1,6 @@ +include tx + +# Docs +include LICENSE README.rst +recursive-include docs * + diff --git a/third_party/transifex-client/README.rst b/third_party/transifex-client/README.rst new file mode 100644 index 00000000..bb887503 --- /dev/null +++ b/third_party/transifex-client/README.rst @@ -0,0 +1,30 @@ + +============================= + Transifex Command-Line Tool +============================= + +The Transifex Command-line Client is a command line tool that enables +you to easily manage your translations within a project without the need +of an elaborate UI system. + +You can use the command line client to easily create new resources, map +locale files to translations and synchronize your Transifex project with +your local repository and vice verca. Translators and localization +managers can also use it to handle large volumes of translation files +easily and without much hassle. + +Check the full documentation at +http://help.transifex.com/user-guide/client/ + + +Installing +========== + +You can install the latest version of transifex-client running ``pip +install transifex-client`` or ``easy_install transifex-client`` +You can also install the `in-development version`_ of transifex-client +with ``pip install transifex-client==dev`` or ``easy_install +transifex-client==dev``. + +.. _in-development version: http://code.transifex.com/transifex-client/ + diff --git a/third_party/transifex-client/setup.py b/third_party/transifex-client/setup.py new file mode 100755 index 00000000..626303d5 --- /dev/null +++ b/third_party/transifex-client/setup.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import glob +from codecs import BOM + +from setuptools import setup, find_packages +from setuptools.command.build_py import build_py as _build_py + +from txclib import get_version + +readme_file = open(u'README.rst') +long_description = readme_file.read() +readme_file.close() +if long_description.startswith(BOM): + long_description = long_description.lstrip(BOM) +long_description = long_description.decode('utf-8') + +package_data = { + '': ['LICENSE', 'README.rst'], +} + +scripts = ['tx'] + +install_requires = [] +try: + import json +except ImportError: + install_requires.append('simplejson') + +setup( + name="transifex-client", + version=get_version(), + scripts=scripts, + description="A command line interface for Transifex", + long_description=long_description, + author="Transifex", + author_email="info@transifex.com", + url="https://www.transifex.com", + license="GPLv2", + dependency_links = [ + ], + setup_requires = [ + ], + install_requires = install_requires, + tests_require = ["mock", ], + data_files=[ + ], + test_suite="tests", + zip_safe=False, + packages=['txclib', ], + include_package_data=True, + package_data = package_data, + keywords = ('translation', 'localization', 'internationalization',), +) diff --git a/third_party/transifex-client/tests/__init__.py b/third_party/transifex-client/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/third_party/transifex-client/tests/test_processors.py b/third_party/transifex-client/tests/test_processors.py new file mode 100644 index 00000000..dd7d7d95 --- /dev/null +++ b/third_party/transifex-client/tests/test_processors.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- + +""" +Unit tests for processor functions. +""" + +import unittest +from urlparse import urlparse +from txclib.processors import hostname_tld_migration, hostname_ssl_migration + + +class TestHostname(unittest.TestCase): + """Test for hostname processors.""" + + def test_tld_migration_needed(self): + """ + Test the tld migration of Transifex, when needed. + """ + hostnames = [ + 'http://transifex.net', 'http://www.transifex.net', + 'https://fedora.transifex.net', + ] + for h in hostnames: + hostname = hostname_tld_migration(h) + self.assertTrue(hostname.endswith('com')) + orig_hostname = 'http://www.transifex.net/path/' + hostname = hostname_tld_migration(orig_hostname) + self.assertEqual(hostname, orig_hostname.replace('net', 'com', 1)) + + def test_tld_migration_needed(self): + """ + Test that unneeded tld migrations are detected correctly. + """ + hostnames = [ + 'https://www.transifex.com', 'http://fedora.transifex.com', + 'http://www.example.net/path/' + ] + for h in hostnames: + hostname = hostname_tld_migration(h) + self.assertEqual(hostname, h) + + def test_no_scheme_specified(self): + """ + Test that, if no scheme has been specified, the https one will be used. + """ + hostname = '//transifex.net' + hostname = hostname_ssl_migration(hostname) + self.assertTrue(hostname.startswith('https://')) + + def test_http_replacement(self): + """Test the replacement of http with https.""" + hostnames = [ + 'http://transifex.com', 'http://transifex.net/http/', + 'http://www.transifex.com/path/' + ] + for h in hostnames: + hostname = hostname_ssl_migration(h) + self.assertEqual(hostname[:8], 'https://') + self.assertEqual(hostname[7:], h[6:]) + + def test_no_http_replacement_needed(self): + """Test that http will not be replaces with https, when not needed.""" + for h in ['http://example.com', 'http://example.com/http/']: + hostname = hostname_ssl_migration(h) + self.assertEqual(hostname, hostname) diff --git a/third_party/transifex-client/tests/test_project.py b/third_party/transifex-client/tests/test_project.py new file mode 100644 index 00000000..3b421214 --- /dev/null +++ b/third_party/transifex-client/tests/test_project.py @@ -0,0 +1,531 @@ +# -*- coding: utf-8 -*- + +from __future__ import with_statement +import unittest +import contextlib +import itertools +try: + import json +except ImportError: + import simplejson as json +from mock import Mock, patch + +from txclib.project import Project +from txclib.config import Flipdict + + +class TestProject(unittest.TestCase): + + def test_extract_fields(self): + """Test the functions that extract a field from a stats object.""" + stats = { + 'completed': '80%', + 'last_update': '00:00', + 'foo': 'bar', + } + self.assertEqual( + stats['completed'], '%s%%' % Project._extract_completed(stats) + ) + self.assertEqual(stats['last_update'], Project._extract_updated(stats)) + + def test_specifying_resources(self): + """Test the various ways to specify resources in a project.""" + p = Project(init=False) + resources = [ + 'proj1.res1', + 'proj2.res2', + 'transifex.txn', + 'transifex.txo', + ] + with patch.object(p, 'get_resource_list') as mock: + mock.return_value = resources + cmd_args = [ + 'proj1.res1', '*1*', 'transifex*', '*r*', + '*o', 'transifex.tx?', 'transifex.txn', + ] + results = [ + ['proj1.res1', ], + ['proj1.res1', ], + ['transifex.txn', 'transifex.txo', ], + ['proj1.res1', 'proj2.res2', 'transifex.txn', 'transifex.txo', ], + ['transifex.txo', ], + ['transifex.txn', 'transifex.txo', ], + ['transifex.txn', ], + [], + ] + + for i, arg in enumerate(cmd_args): + resources = [arg] + self.assertEqual(p.get_chosen_resources(resources), results[i]) + + # wrong argument + resources = ['*trasnifex*', ] + self.assertRaises(Exception, p.get_chosen_resources, resources) + + +class TestProjectMinimumPercent(unittest.TestCase): + """Test the minimum-perc option.""" + + def setUp(self): + super(TestProjectMinimumPercent, self).setUp() + self.p = Project(init=False) + self.p.minimum_perc = None + self.p.resource = "resource" + + def test_cmd_option(self): + """Test command-line option.""" + self.p.minimum_perc = 20 + results = itertools.cycle([80, 90 ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertFalse(self.p._satisfies_min_translated({'completed': '12%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '20%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '30%'})) + + def test_global_only(self): + """Test only global option.""" + results = itertools.cycle([80, None ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertFalse(self.p._satisfies_min_translated({'completed': '70%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'})) + + def test_local_lower_than_global(self): + """Test the case where the local option is lower than the global.""" + results = itertools.cycle([80, 70 ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'})) + + def test_local_higher_than_global(self): + """Test the case where the local option is lower than the global.""" + results = itertools.cycle([60, 70 ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'})) + + def test_local_only(self): + """Test the case where the local option is lower than the global.""" + results = itertools.cycle([None, 70 ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertFalse(self.p._satisfies_min_translated({'completed': '60%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '70%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '80%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'})) + + def test_no_option(self): + """"Test the case there is nothing defined.""" + results = itertools.cycle([None, None ]) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "get_resource_option") as mock: + mock.side_effect = side_effect + self.assertTrue(self.p._satisfies_min_translated({'completed': '0%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '10%'})) + self.assertTrue(self.p._satisfies_min_translated({'completed': '90%'})) + + +class TestProjectFilters(unittest.TestCase): + """Test filters used to decide whether to push/pull a translation or not.""" + + def setUp(self): + super(TestProjectFilters, self).setUp() + self.p = Project(init=False) + self.p.minimum_perc = None + self.p.resource = "resource" + self.stats = { + 'en': { + 'completed': '100%', 'last_update': '2011-11-01 15:00:00', + }, 'el': { + 'completed': '60%', 'last_update': '2011-11-01 15:00:00', + }, 'pt': { + 'completed': '70%', 'last_update': '2011-11-01 15:00:00', + }, + } + self.langs = self.stats.keys() + + def test_add_translation(self): + """Test filters for adding translations. + + We do not test here for minimum percentages. + """ + with patch.object(self.p, "get_resource_option") as mock: + mock.return_value = None + should_add = self.p._should_add_translation + for force in [True, False]: + for lang in self.langs: + self.assertTrue(should_add(lang, self.stats, force)) + + # unknown language + self.assertFalse(should_add('es', self.stats)) + + def test_update_translation(self): + """Test filters for updating a translation. + + We do not test here for minimum percentages. + """ + with patch.object(self.p, "get_resource_option") as mock: + mock.return_value = None + + should_update = self.p._should_update_translation + force = True + for lang in self.langs: + self.assertTrue(should_update(lang, self.stats, 'foo', force)) + + force = False # reminder + local_file = 'foo' + + # unknown language + self.assertFalse(should_update('es', self.stats, local_file)) + + # no local file + with patch.object(self.p, "_get_time_of_local_file") as time_mock: + time_mock.return_value = None + with patch.object(self.p, "get_full_path") as path_mock: + path_mock.return_value = "foo" + for lang in self.langs: + self.assertTrue( + should_update(lang, self.stats, local_file) + ) + + # older local files + local_times = [self.p._generate_timestamp('2011-11-01 14:00:59')] + results = itertools.cycle(local_times) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "_get_time_of_local_file") as time_mock: + time_mock.side_effect = side_effect + with patch.object(self.p, "get_full_path") as path_mock: + path_mock.return_value = "foo" + for lang in self.langs: + self.assertTrue( + should_update(lang, self.stats, local_file) + ) + + # newer local files + local_times = [self.p._generate_timestamp('2011-11-01 15:01:59')] + results = itertools.cycle(local_times) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "_get_time_of_local_file") as time_mock: + time_mock.side_effect = side_effect + with patch.object(self.p, "get_full_path") as path_mock: + path_mock.return_value = "foo" + for lang in self.langs: + self.assertFalse( + should_update(lang, self.stats, local_file) + ) + + def test_push_translation(self): + """Test filters for pushing a translation file.""" + with patch.object(self.p, "get_resource_option") as mock: + mock.return_value = None + + local_file = 'foo' + should_push = self.p._should_push_translation + force = True + for lang in self.langs: + self.assertTrue(should_push(lang, self.stats, local_file, force)) + + force = False # reminder + + # unknown language + self.assertTrue(should_push('es', self.stats, local_file)) + + # older local files + local_times = [self.p._generate_timestamp('2011-11-01 14:00:59')] + results = itertools.cycle(local_times) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "_get_time_of_local_file") as time_mock: + time_mock.side_effect = side_effect + with patch.object(self.p, "get_full_path") as path_mock: + path_mock.return_value = "foo" + for lang in self.langs: + self.assertFalse( + should_push(lang, self.stats, local_file) + ) + + # newer local files + local_times = [self.p._generate_timestamp('2011-11-01 15:01:59')] + results = itertools.cycle(local_times) + def side_effect(*args): + return results.next() + + with patch.object(self.p, "_get_time_of_local_file") as time_mock: + time_mock.side_effect = side_effect + with patch.object(self.p, "get_full_path") as path_mock: + path_mock.return_value = "foo" + for lang in self.langs: + self.assertTrue( + should_push(lang, self.stats, local_file) + ) + + +class TestProjectPull(unittest.TestCase): + """Test bits & pieces of the pull method.""" + + def setUp(self): + super(TestProjectPull, self).setUp() + self.p = Project(init=False) + self.p.minimum_perc = None + self.p.resource = "resource" + self.p.host = 'foo' + self.p.project_slug = 'foo' + self.p.resource_slug = 'foo' + self.stats = { + 'en': { + 'completed': '100%', 'last_update': '2011-11-01 15:00:00', + }, 'el': { + 'completed': '60%', 'last_update': '2011-11-01 15:00:00', + }, 'pt': { + 'completed': '70%', 'last_update': '2011-11-01 15:00:00', + }, + } + self.langs = self.stats.keys() + self.files = dict(zip(self.langs, itertools.repeat(None))) + self.details = {'available_languages': []} + for lang in self.langs: + self.details['available_languages'].append({'code': lang}) + self.slang = 'en' + self.lang_map = Flipdict() + + def test_new_translations(self): + """Test finding new transaltions to add.""" + with patch.object(self.p, 'do_url_request') as resource_mock: + resource_mock.return_value = json.dumps(self.details) + files_keys = self.langs + new_trans = self.p._new_translations_to_add + for force in [True, False]: + res = new_trans( + self.files, self.slang, self.lang_map, self.stats, force + ) + self.assertEquals(res, set([])) + + with patch.object(self.p, '_should_add_translation') as filter_mock: + filter_mock.return_value = True + for force in [True, False]: + res = new_trans( + {'el': None}, self.slang, self.lang_map, self.stats, force + ) + self.assertEquals(res, set(['pt'])) + for force in [True, False]: + res = new_trans( + {}, self.slang, self.lang_map, self.stats, force + ) + self.assertEquals(res, set(['el', 'pt'])) + + files = {} + files['pt_PT'] = None + lang_map = {'pt': 'pt_PT'} + for force in [True, False]: + res = new_trans( + files, self.slang, lang_map, self.stats, force + ) + self.assertEquals(res, set(['el'])) + + def test_languages_to_pull_empty_initial_list(self): + """Test determining the languages to pull, when the initial + list is empty. + """ + languages = [] + force = False + + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['el', 'en', 'pt'])) + self.assertFalse(new) + + del self.files['el'] + self.files['el-gr'] = None + self.lang_map['el'] = 'el-gr' + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['el', 'en', 'pt'])) + self.assertFalse(new) + + def test_languages_to_pull_with_initial_list(self): + """Test determining the languages to pull, then there is a + language selection from the user. + """ + languages = ['el', 'en'] + self.lang_map['el'] = 'el-gr' + del self.files['el'] + self.files['el-gr'] = None + force = False + + with patch.object(self.p, '_should_add_translation') as mock: + mock.return_value = True + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['en', 'el-gr', ])) + self.assertFalse(new) + + mock.return_value = False + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['en', 'el-gr', ])) + self.assertFalse(new) + + del self.files['el-gr'] + mock.return_value = True + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['en', ])) + self.assertEquals(new, set(['el', ])) + + mock.return_value = False + res = self.p._languages_to_pull( + languages, self.files, self.lang_map, self.stats, force + ) + existing = res[0] + new = res[1] + self.assertEquals(existing, set(['en', ])) + self.assertEquals(new, set([])) + + def test_in_combination_with_force_option(self): + """Test the minumum-perc option along with -f.""" + with patch.object(self.p, 'get_resource_option') as mock: + mock.return_value = 70 + + res = self.p._should_download('de', self.stats, None, False) + self.assertEquals(res, False) + res = self.p._should_download('el', self.stats, None, False) + self.assertEquals(res, False) + res = self.p._should_download('el', self.stats, None, True) + self.assertEquals(res, False) + res = self.p._should_download('en', self.stats, None, False) + self.assertEquals(res, True) + res = self.p._should_download('en', self.stats, None, True) + self.assertEquals(res, True) + + with patch.object(self.p, '_remote_is_newer') as local_file_mock: + local_file_mock = False + res = self.p._should_download('pt', self.stats, None, False) + self.assertEquals(res, True) + res = self.p._should_download('pt', self.stats, None, True) + self.assertEquals(res, True) + + +class TestFormats(unittest.TestCase): + """Tests for the supported formats.""" + + def setUp(self): + self.p = Project(init=False) + + def test_extensions(self): + """Test returning the correct extension for a format.""" + sample_formats = { + 'PO': {'file-extensions': '.po, .pot'}, + 'QT': {'file-extensions': '.ts'}, + } + extensions = ['.po', '.ts', '', ] + with patch.object(self.p, "do_url_request") as mock: + mock.return_value = json.dumps(sample_formats) + for (type_, ext) in zip(['PO', 'QT', 'NONE', ], extensions): + extension = self.p._extension_for(type_) + self.assertEquals(extension, ext) + + +class TestOptions(unittest.TestCase): + """Test the methods related to parsing the configuration file.""" + + def setUp(self): + self.p = Project(init=False) + + def test_get_option(self): + """Test _get_option method.""" + with contextlib.nested( + patch.object(self.p, 'get_resource_option'), + patch.object(self.p, 'config', create=True) + ) as (rmock, cmock): + rmock.return_value = 'resource' + cmock.has_option.return_value = 'main' + cmock.get.return_value = 'main' + self.assertEqual(self.p._get_option(None, None), 'resource') + rmock.return_value = None + cmock.has_option.return_value = 'main' + cmock.get.return_value = 'main' + self.assertEqual(self.p._get_option(None, None), 'main') + cmock.has_option.return_value = None + self.assertIs(self.p._get_option(None, None), None) + + +class TestConfigurationOptions(unittest.TestCase): + """Test the various configuration options.""" + + def test_i18n_type(self): + p = Project(init=False) + type_string = 'type' + i18n_type = 'PO' + with patch.object(p, 'config', create=True) as config_mock: + p.set_i18n_type([], i18n_type) + calls = config_mock.method_calls + self.assertEquals('set', calls[0][0]) + self.assertEquals('main', calls[0][1][0]) + p.set_i18n_type(['transifex.txo'], 'PO') + calls = config_mock.method_calls + self.assertEquals('set', calls[0][0]) + p.set_i18n_type(['transifex.txo', 'transifex.txn'], 'PO') + calls = config_mock.method_calls + self.assertEquals('set', calls[0][0]) + self.assertEquals('set', calls[1][0]) + + +class TestStats(unittest.TestCase): + """Test the access to the stats objects.""" + + def setUp(self): + self.stats = Mock() + self.stats.__getitem__ = Mock() + self.stats.__getitem__.return_value = '12%' + + def test_field_used_per_mode(self): + """Test the fields used for each mode.""" + Project._extract_completed(self.stats, 'translate') + self.stats.__getitem__.assert_called_with('completed') + Project._extract_completed(self.stats, 'reviewed') + self.stats.__getitem__.assert_called_with('reviewed_percentage') + diff --git a/third_party/transifex-client/tx b/third_party/transifex-client/tx new file mode 100755 index 00000000..dfb4a4c1 --- /dev/null +++ b/third_party/transifex-client/tx @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from optparse import OptionParser, OptionValueError +import os +import sys + +from txclib import utils +from txclib import get_version +from txclib.log import set_log_level, logger + +reload(sys) # WTF? Otherwise setdefaultencoding doesn't work + +# This block ensures that ^C interrupts are handled quietly. +try: + import signal + + def exithandler(signum,frame): + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + sys.exit(1) + + signal.signal(signal.SIGINT, exithandler) + signal.signal(signal.SIGTERM, exithandler) + if hasattr(signal, 'SIGPIPE'): + signal.signal(signal.SIGPIPE, signal.SIG_DFL) + +except KeyboardInterrupt: + sys.exit(1) + +# When we open file with f = codecs.open we specifi FROM what encoding to read +# This sets the encoding for the strings which are created with f.read() +sys.setdefaultencoding('utf-8') + + +def main(argv): + """ + Here we parse the flags (short, long) and we instantiate the classes. + """ + usage = "usage: %prog [options] command [cmd_options]" + description = "This is the Transifex command line client which"\ + " allows you to manage your translations locally and sync"\ + " them with the master Transifex server.\nIf you'd like to"\ + " check the available commands issue `%prog help` or if you"\ + " just want help with a specific command issue `%prog help"\ + " command`" + + parser = OptionParser( + usage=usage, version=get_version(), description=description + ) + parser.disable_interspersed_args() + parser.add_option( + "-d", "--debug", action="store_true", dest="debug", + default=False, help=("enable debug messages") + ) + parser.add_option( + "-q", "--quiet", action="store_true", dest="quiet", + default=False, help="don't print status messages to stdout" + ) + parser.add_option( + "-r", "--root", action="store", dest="root_dir", type="string", + default=None, help="change root directory (default is cwd)" + ) + parser.add_option( + "--traceback", action="store_true", dest="trace", default=False, + help="print full traceback on exceptions" + ) + parser.add_option( + "--disable-colors", action="store_true", dest="color_disable", + default=(os.name == 'nt' or not sys.stdout.isatty()), + help="disable colors in the output of commands" + ) + (options, args) = parser.parse_args() + + if len(args) < 1: + parser.error("No command was given") + + utils.DISABLE_COLORS = options.color_disable + + # set log level + if options.quiet: + set_log_level('WARNING') + elif options.debug: + set_log_level('DEBUG') + + # find .tx + path_to_tx = options.root_dir or utils.find_dot_tx() + + + cmd = args[0] + try: + utils.exec_command(cmd, args[1:], path_to_tx) + except utils.UnknownCommandError: + logger.error("tx: Command %s not found" % cmd) + except SystemExit: + sys.exit() + except: + import traceback + if options.trace: + traceback.print_exc() + else: + formatted_lines = traceback.format_exc().splitlines() + logger.error(formatted_lines[-1]) + sys.exit(1) + +# Run baby :) ... run +if __name__ == "__main__": + # sys.argv[0] is the name of the script that we’re running. + main(sys.argv[1:]) diff --git a/third_party/transifex-client/txclib/__init__.py b/third_party/transifex-client/txclib/__init__.py new file mode 100644 index 00000000..814773c8 --- /dev/null +++ b/third_party/transifex-client/txclib/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +""" +Copyright (C) 2010 by Indifex (www.indifex.com), see AUTHORS. +License: BSD, see LICENSE for details. + +For further information visit http://code.indifex.com/transifex-client +""" + + +VERSION = (0, 9, 0, 'devel') + +def get_version(): + version = '%s.%s' % (VERSION[0], VERSION[1]) + if VERSION[2]: + version = '%s.%s' % (version, VERSION[2]) + if VERSION[3] != 'final': + version = '%s %s' % (version, VERSION[3]) + return version diff --git a/third_party/transifex-client/txclib/commands.py b/third_party/transifex-client/txclib/commands.py new file mode 100644 index 00000000..282287fc --- /dev/null +++ b/third_party/transifex-client/txclib/commands.py @@ -0,0 +1,576 @@ +# -*- coding: utf-8 -*- +""" +In this file we have all the top level commands for the transifex client. +Since we're using a way to automatically list them and execute them, when +adding code to this file you must take care of the following: + * Added functions must begin with 'cmd_' followed by the actual name of the + command being used in the command line (eg cmd_init) + * The description for each function that we display to the user is read from + the func_doc attribute which reads the doc string. So, when adding + docstring to a new function make sure you add an oneliner which is + descriptive and is meant to be seen by the user. + * When including libraries, it's best if you include modules instead of + functions because that way our function resolution will work faster and the + chances of overlapping are minimal + * All functions should use the OptionParser and should have a usage and + descripition field. +""" +import os +import re, shutil +import sys +from optparse import OptionParser, OptionGroup +import ConfigParser + + +from txclib import utils, project +from txclib.utils import parse_json, compile_json, relpath +from txclib.config import OrderedRawConfigParser +from txclib.exceptions import UnInitializedError +from txclib.parsers import delete_parser, help_parser, parse_csv_option, \ + status_parser, pull_parser, set_parser, push_parser, init_parser +from txclib.log import logger + + +def cmd_init(argv, path_to_tx): + "Initialize a new transifex project." + parser = init_parser() + (options, args) = parser.parse_args(argv) + if len(args) > 1: + parser.error("Too many arguments were provided. Aborting...") + if args: + path_to_tx = args[0] + else: + path_to_tx = os.getcwd() + + if os.path.isdir(os.path.join(path_to_tx,".tx")): + logger.info("tx: There is already a tx folder!") + reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ") + while (reinit != 'y' and reinit != 'Y' and reinit != 'N' and reinit != 'n' and reinit != ''): + reinit = raw_input("Do you want to delete it and reinit the project? [y/N]: ") + if not reinit or reinit in ['N', 'n', 'NO', 'no', 'No']: + return + # Clean the old settings + # FIXME: take a backup + else: + rm_dir = os.path.join(path_to_tx, ".tx") + shutil.rmtree(rm_dir) + + logger.info("Creating .tx folder...") + os.mkdir(os.path.join(path_to_tx,".tx")) + + # Handle the credentials through transifexrc + home = os.path.expanduser("~") + txrc = os.path.join(home, ".transifexrc") + config = OrderedRawConfigParser() + + default_transifex = "https://www.transifex.com" + transifex_host = options.host or raw_input("Transifex instance [%s]: " % default_transifex) + + if not transifex_host: + transifex_host = default_transifex + if not transifex_host.startswith(('http://', 'https://')): + transifex_host = 'https://' + transifex_host + + config_file = os.path.join(path_to_tx, ".tx", "config") + if not os.path.exists(config_file): + # The path to the config file (.tx/config) + logger.info("Creating skeleton...") + config = OrderedRawConfigParser() + config.add_section('main') + config.set('main', 'host', transifex_host) + # Touch the file if it doesn't exist + logger.info("Creating config file...") + fh = open(config_file, 'w') + config.write(fh) + fh.close() + + prj = project.Project(path_to_tx) + prj.getset_host_credentials(transifex_host, user=options.user, + password=options.password) + prj.save() + logger.info("Done.") + + +def cmd_set(argv, path_to_tx): + "Add local or remote files under transifex" + parser = set_parser() + (options, args) = parser.parse_args(argv) + + # Implement options/args checks + # TODO !!!!!!! + if options.local: + try: + expression = args[0] + except IndexError: + parser.error("Please specify an expression.") + if not options.resource: + parser.error("Please specify a resource") + if not options.source_language: + parser.error("Please specify a source language.") + if not '' in expression: + parser.error("The expression you have provided is not valid.") + if not utils.valid_slug(options.resource): + parser.error("Invalid resource slug. The format is "\ + ". and the valid characters include [_-\w].") + _auto_local(path_to_tx, options.resource, + source_language=options.source_language, + expression = expression, source_file=options.source_file, + execute=options.execute, regex=False) + if options.execute: + _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx) + _set_mode(options.resource, options.mode, path_to_tx) + _set_type(options.resource, options.i18n_type, path_to_tx) + return + + if options.remote: + try: + url = args[0] + except IndexError: + parser.error("Please specify an remote url") + _auto_remote(path_to_tx, url) + _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx) + _set_mode(options.resource, options.mode, path_to_tx) + return + + if options.is_source: + resource = options.resource + if not resource: + parser.error("You must specify a resource name with the" + " -r|--resource flag.") + + lang = options.language + if not lang: + parser.error("Please specify a source language.") + + if len(args) != 1: + parser.error("Please specify a file.") + + if not utils.valid_slug(resource): + parser.error("Invalid resource slug. The format is "\ + ". and the valid characters include [_-\w].") + + file = args[0] + # Calculate relative path + path_to_file = relpath(file, path_to_tx) + _set_source_file(path_to_tx, resource, options.language, path_to_file) + elif options.resource or options.language: + resource = options.resource + lang = options.language + + if len(args) != 1: + parser.error("Please specify a file") + + # Calculate relative path + path_to_file = relpath(args[0], path_to_tx) + + try: + _go_to_dir(path_to_tx) + except UnInitializedError, e: + utils.logger.error(e) + return + + if not utils.valid_slug(resource): + parser.error("Invalid resource slug. The format is "\ + ". and the valid characters include [_-\w].") + _set_translation(path_to_tx, resource, lang, path_to_file) + + _set_mode(options.resource, options.mode, path_to_tx) + _set_type(options.resource, options.i18n_type, path_to_tx) + _set_minimum_perc(options.resource, options.minimum_perc, path_to_tx) + + logger.info("Done.") + return + + +def _auto_local(path_to_tx, resource, source_language, expression, execute=False, + source_file=None, regex=False): + """Auto configure local project.""" + # The path everything will be relative to + curpath = os.path.abspath(os.curdir) + + # Force expr to be a valid regex expr (escaped) but keep intact + expr_re = utils.regex_from_filefilter(expression, curpath) + expr_rec = re.compile(expr_re) + + if not execute: + logger.info("Only printing the commands which will be run if the " + "--execute switch is specified.") + + # First, let's construct a dictionary of all matching files. + # Note: Only the last matching file of a language will be stored. + translation_files = {} + for root, dirs, files in os.walk(curpath): + for f in files: + f_path = os.path.abspath(os.path.join(root, f)) + match = expr_rec.match(f_path) + if match: + lang = match.group(1) + f_path = os.path.abspath(f_path) + if lang == source_language and not source_file: + source_file = f_path + else: + translation_files[lang] = f_path + + if not source_file: + raise Exception("Could not find a source language file. Please run" + " set --source manually and then re-run this command or provide" + " the source file with the -s flag.") + if execute: + logger.info("Updating source for resource %s ( %s -> %s )." % (resource, + source_language, relpath(source_file, path_to_tx))) + _set_source_file(path_to_tx, resource, source_language, + relpath(source_file, path_to_tx)) + else: + logger.info('\ntx set --source -r %(res)s -l %(lang)s %(file)s\n' % { + 'res': resource, + 'lang': source_language, + 'file': relpath(source_file, curpath)}) + + prj = project.Project(path_to_tx) + root_dir = os.path.abspath(path_to_tx) + + if execute: + try: + prj.config.get("%s" % resource, "source_file") + except ConfigParser.NoSectionError: + raise Exception("No resource with slug \"%s\" was found.\nRun 'tx set --auto" + "-local -r %s \"expression\"' to do the initial configuration." % resource) + + # Now let's handle the translation files. + if execute: + logger.info("Updating file expression for resource %s ( %s )." % (resource, + expression)) + # Eval file_filter relative to root dir + file_filter = relpath(os.path.join(curpath, expression), + path_to_tx) + prj.config.set("%s" % resource, "file_filter", file_filter) + else: + for (lang, f_path) in sorted(translation_files.items()): + logger.info('tx set -r %(res)s -l %(lang)s %(file)s' % { + 'res': resource, + 'lang': lang, + 'file': relpath(f_path, curpath)}) + + if execute: + prj.save() + + +def _auto_remote(path_to_tx, url): + """ + Initialize a remote release/project/resource to the current directory. + """ + logger.info("Auto configuring local project from remote URL...") + + type, vars = utils.parse_tx_url(url) + prj = project.Project(path_to_tx) + username, password = prj.getset_host_credentials(vars['hostname']) + + if type == 'project': + logger.info("Getting details for project %s" % vars['project']) + proj_info = utils.get_details('project_details', + username, password, + hostname = vars['hostname'], project = vars['project']) + resources = [ '.'.join([vars['project'], r['slug']]) for r in proj_info['resources'] ] + logger.info("%s resources found. Configuring..." % len(resources)) + elif type == 'release': + logger.info("Getting details for release %s" % vars['release']) + rel_info = utils.get_details('release_details', + username, password, hostname = vars['hostname'], + project = vars['project'], release = vars['release']) + resources = [] + for r in rel_info['resources']: + if r.has_key('project'): + resources.append('.'.join([r['project']['slug'], r['slug']])) + else: + resources.append('.'.join([vars['project'], r['slug']])) + logger.info("%s resources found. Configuring..." % len(resources)) + elif type == 'resource': + logger.info("Getting details for resource %s" % vars['resource']) + resources = [ '.'.join([vars['project'], vars['resource']]) ] + else: + raise("Url '%s' is not recognized." % url) + + for resource in resources: + logger.info("Configuring resource %s." % resource) + proj, res = resource.split('.') + res_info = utils.get_details('resource_details', + username, password, hostname = vars['hostname'], + project = proj, resource=res) + try: + source_lang = res_info['source_language_code'] + i18n_type = res_info['i18n_type'] + except KeyError: + raise Exception("Remote server seems to be running an unsupported version" + " of Transifex. Either update your server software of fallback" + " to a previous version of transifex-client.") + prj.set_remote_resource( + resource=resource, + host = vars['hostname'], + source_lang = source_lang, + i18n_type = i18n_type) + + prj.save() + + +def cmd_push(argv, path_to_tx): + "Push local files to remote server" + parser = push_parser() + (options, args) = parser.parse_args(argv) + force_creation = options.force_creation + languages = parse_csv_option(options.languages) + resources = parse_csv_option(options.resources) + skip = options.skip_errors + prj = project.Project(path_to_tx) + if not (options.push_source or options.push_translations): + parser.error("You need to specify at least one of the -s|--source," + " -t|--translations flags with the push command.") + + prj.push( + force=force_creation, resources=resources, languages=languages, + skip=skip, source=options.push_source, + translations=options.push_translations, + no_interactive=options.no_interactive + ) + logger.info("Done.") + + +def cmd_pull(argv, path_to_tx): + "Pull files from remote server to local repository" + parser = pull_parser() + (options, args) = parser.parse_args(argv) + if options.fetchall and options.languages: + parser.error("You can't user a language filter along with the"\ + " -a|--all option") + languages = parse_csv_option(options.languages) + resources = parse_csv_option(options.resources) + skip = options.skip_errors + minimum_perc = options.minimum_perc or None + + try: + _go_to_dir(path_to_tx) + except UnInitializedError, e: + utils.logger.error(e) + return + + # instantiate the project.Project + prj = project.Project(path_to_tx) + prj.pull( + languages=languages, resources=resources, overwrite=options.overwrite, + fetchall=options.fetchall, fetchsource=options.fetchsource, + force=options.force, skip=skip, minimum_perc=minimum_perc, + mode=options.mode + ) + logger.info("Done.") + + +def _set_source_file(path_to_tx, resource, lang, path_to_file): + """Reusable method to set source file.""" + proj, res = resource.split('.') + if not proj or not res: + raise Exception("\"%s.%s\" is not a valid resource identifier. It should" + " be in the following format project_slug.resource_slug." % + (proj, res)) + if not lang: + raise Exception("You haven't specified a source language.") + + try: + _go_to_dir(path_to_tx) + except UnInitializedError, e: + utils.logger.error(e) + return + + if not os.path.exists(path_to_file): + raise Exception("tx: File ( %s ) does not exist." % + os.path.join(path_to_tx, path_to_file)) + + # instantiate the project.Project + prj = project.Project(path_to_tx) + root_dir = os.path.abspath(path_to_tx) + + if root_dir not in os.path.normpath(os.path.abspath(path_to_file)): + raise Exception("File must be under the project root directory.") + + logger.info("Setting source file for resource %s.%s ( %s -> %s )." % ( + proj, res, lang, path_to_file)) + + path_to_file = relpath(path_to_file, root_dir) + + prj = project.Project(path_to_tx) + + # FIXME: Check also if the path to source file already exists. + try: + try: + prj.config.get("%s.%s" % (proj, res), "source_file") + except ConfigParser.NoSectionError: + prj.config.add_section("%s.%s" % (proj, res)) + except ConfigParser.NoOptionError: + pass + finally: + prj.config.set("%s.%s" % (proj, res), "source_file", + path_to_file) + prj.config.set("%s.%s" % (proj, res), "source_lang", + lang) + + prj.save() + + +def _set_translation(path_to_tx, resource, lang, path_to_file): + """Reusable method to set translation file.""" + + proj, res = resource.split('.') + if not project or not resource: + raise Exception("\"%s\" is not a valid resource identifier. It should" + " be in the following format project_slug.resource_slug." % + resource) + + try: + _go_to_dir(path_to_tx) + except UnInitializedError, e: + utils.logger.error(e) + return + + # Warn the user if the file doesn't exist + if not os.path.exists(path_to_file): + logger.info("Warning: File '%s' doesn't exist." % path_to_file) + + # instantiate the project.Project + prj = project.Project(path_to_tx) + root_dir = os.path.abspath(path_to_tx) + + if root_dir not in os.path.normpath(os.path.abspath(path_to_file)): + raise Exception("File must be under the project root directory.") + + if lang == prj.config.get("%s.%s" % (proj, res), "source_lang"): + raise Exception("tx: You cannot set translation file for the source language." + " Source languages contain the strings which will be translated!") + + logger.info("Updating translations for resource %s ( %s -> %s )." % (resource, + lang, path_to_file)) + path_to_file = relpath(path_to_file, root_dir) + prj.config.set("%s.%s" % (proj, res), "trans.%s" % lang, + path_to_file) + + prj.save() + + +def cmd_status(argv, path_to_tx): + "Print status of current project" + parser = status_parser() + (options, args) = parser.parse_args(argv) + resources = parse_csv_option(options.resources) + prj = project.Project(path_to_tx) + resources = prj.get_chosen_resources(resources) + resources_num = len(resources) + for idx, res in enumerate(resources): + p, r = res.split('.') + logger.info("%s -> %s (%s of %s)" % (p, r, idx + 1, resources_num)) + logger.info("Translation Files:") + slang = prj.get_resource_option(res, 'source_lang') + sfile = prj.get_resource_option(res, 'source_file') or "N/A" + lang_map = prj.get_resource_lang_mapping(res) + logger.info(" - %s: %s (%s)" % (utils.color_text(slang, "RED"), + sfile, utils.color_text("source", "YELLOW"))) + files = prj.get_resource_files(res) + fkeys = files.keys() + fkeys.sort() + for lang in fkeys: + local_lang = lang + if lang in lang_map.values(): + local_lang = lang_map.flip[lang] + logger.info(" - %s: %s" % (utils.color_text(local_lang, "RED"), + files[lang])) + logger.info("") + + +def cmd_help(argv, path_to_tx): + """List all available commands""" + parser = help_parser() + (options, args) = parser.parse_args(argv) + if len(args) > 1: + parser.error("Multiple arguments received. Exiting...") + + # Get all commands + fns = utils.discover_commands() + + # Print help for specific command + if len(args) == 1: + try: + fns[argv[0]](['--help'], path_to_tx) + except KeyError: + utils.logger.error("Command %s not found" % argv[0]) + # or print summary of all commands + + # the code below will only be executed if the KeyError exception is thrown + # becuase in all other cases the function called with --help will exit + # instead of return here + keys = fns.keys() + keys.sort() + + logger.info("Transifex command line client.\n") + logger.info("Available commands are:") + for key in keys: + logger.info(" %-15s\t%s" % (key, fns[key].func_doc)) + logger.info("\nFor more information run %s command --help" % sys.argv[0]) + + +def cmd_delete(argv, path_to_tx): + "Delete an accessible resource or translation in a remote server." + parser = delete_parser() + (options, args) = parser.parse_args(argv) + languages = parse_csv_option(options.languages) + resources = parse_csv_option(options.resources) + skip = options.skip_errors + force = options.force_delete + prj = project.Project(path_to_tx) + prj.delete(resources, languages, skip, force) + logger.info("Done.") + + +def _go_to_dir(path): + """Change the current working directory to the directory specified as + argument. + + Args: + path: The path to chdor to. + Raises: + UnInitializedError, in case the directory has not been initialized. + """ + if path is None: + raise UnInitializedError( + "Directory has not been initialzied. " + "Did you forget to run 'tx init' first?" + ) + os.chdir(path) + + +def _set_minimum_perc(resource, value, path_to_tx): + """Set the minimum percentage in the .tx/config file.""" + args = (resource, 'minimum_perc', value, path_to_tx, 'set_min_perc') + _set_project_option(*args) + + +def _set_mode(resource, value, path_to_tx): + """Set the mode in the .tx/config file.""" + args = (resource, 'mode', value, path_to_tx, 'set_default_mode') + _set_project_option(*args) + + +def _set_type(resource, value, path_to_tx): + """Set the i18n type in the .tx/config file.""" + args = (resource, 'type', value, path_to_tx, 'set_i18n_type') + _set_project_option(*args) + + +def _set_project_option(resource, name, value, path_to_tx, func_name): + """Save the option to the project config file.""" + if value is None: + return + if not resource: + logger.debug("Setting the %s for all resources." % name) + resources = [] + else: + logger.debug("Setting the %s for resource %s." % (name, resource)) + resources = [resource, ] + prj = project.Project(path_to_tx) + getattr(prj, func_name)(resources, value) + prj.save() diff --git a/third_party/transifex-client/txclib/config.py b/third_party/transifex-client/txclib/config.py new file mode 100644 index 00000000..a9d055f5 --- /dev/null +++ b/third_party/transifex-client/txclib/config.py @@ -0,0 +1,115 @@ +import ConfigParser + + +class OrderedRawConfigParser( ConfigParser.RawConfigParser ): + """ + Overload standard Class ConfigParser.RawConfigParser + """ + def write(self, fp): + """Write an .ini-format representation of the configuration state.""" + if self._defaults: + fp.write("[%s]\n" % DEFAULTSECT) + for key in sorted( self._defaults ): + fp.write( "%s = %s\n" % (key, str( self._defaults[ key ] + ).replace('\n', '\n\t')) ) + fp.write("\n") + for section in self._sections: + fp.write("[%s]\n" % section) + for key in sorted( self._sections[section] ): + if key != "__name__": + fp.write("%s = %s\n" % + (key, str( self._sections[section][ key ] + ).replace('\n', '\n\t'))) + fp.write("\n") + + optionxform = str + + +_NOTFOUND = object() + + +class Flipdict(dict): + """An injective (one-to-one) python dict. Ensures that each key maps + to a unique value, and each value maps back to that same key. + + Code mostly taken from here: + http://code.activestate.com/recipes/576968-flipdict-python-dict-that-also-maintains-a-one-to-/ + """ + + def __init__(self, *args, **kw): + self._flip = dict.__new__(self.__class__) + setattr(self._flip, "_flip", self) + for key, val in dict(*args, **kw).iteritems(): + self[key] = val + + @property + def flip(self): + """The inverse mapping.""" + return self._flip + + def __repr__(self): + return "%s(%r)" % (self.__class__.__name__, dict(self)) + + __str__ = __repr__ + + def copy(self): + return self.__class__(self) + + @classmethod + def fromkeys(cls, keys, value=None): + return cls(dict.fromkeys(keys, value)) + + def __setitem__(self, key, val): + k = self._flip.get(val, _NOTFOUND) + if not (k is _NOTFOUND or k==key): + raise KeyError('(key,val) would erase mapping for value %r' % val) + + v = self.get(key, _NOTFOUND) + if v is not _NOTFOUND: + dict.__delitem__(self._flip, v) + + dict.__setitem__(self, key, val) + dict.__setitem__(self._flip, val, key) + + def setdefault(self, key, default = None): + # Copied from python's UserDict.DictMixin code. + try: + return self[key] + except KeyError: + self[key] = default + return default + + def update(self, other = None, **kwargs): + # Copied from python's UserDict.DictMixin code. + # Make progressively weaker assumptions about "other" + if other is None: + pass + elif hasattr(other, 'iteritems'): # iteritems saves memory and lookups + for k, v in other.iteritems(): + self[k] = v + elif hasattr(other, 'keys'): + for k in other.keys(): + self[k] = other[k] + else: + for k, v in other: + self[k] = v + if kwargs: + self.update(kwargs) + + def __delitem__(self, key): + val = dict.pop(self, key) + dict.__delitem__(self._flip, val) + + def pop(self, key, *args): + val = dict.pop(self, key, *args) + dict.__delitem__(self._flip, val) + return val + + def popitem(self): + key, val = dict.popitem(self) + dict.__delitem__(self._flip, val) + return key, val + + def clear(self): + dict.clear(self) + dict.clear(self._flip) diff --git a/third_party/transifex-client/txclib/exceptions.py b/third_party/transifex-client/txclib/exceptions.py new file mode 100644 index 00000000..8766a018 --- /dev/null +++ b/third_party/transifex-client/txclib/exceptions.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +""" +Exception classes for the tx client. +""" + + +class UnInitializedError(Exception): + """The project directory has not been initialized.""" + + +class UnknownCommandError(Exception): + """The provided command is not supported.""" diff --git a/third_party/transifex-client/txclib/http_utils.py b/third_party/transifex-client/txclib/http_utils.py new file mode 100644 index 00000000..3243149c --- /dev/null +++ b/third_party/transifex-client/txclib/http_utils.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- + +""" +HTTP-related utility functions. +""" + +from __future__ import with_statement +import gzip +try: + import cStringIO as StringIO +except ImportError: + import StringIO + + +def _gzip_decode(gzip_data): + """ + Unzip gzipped data and return them. + + :param gzip_data: Gzipped data. + :returns: The actual data. + """ + try: + gzip_data = StringIO.StringIO(gzip_data) + gzip_file = gzip.GzipFile(fileobj=gzip_data) + data = gzip_file.read() + return data + finally: + gzip_data.close() + + +def http_response(response): + """ + Return the response of a HTTP request. + + If the response has been gzipped, gunzip it first. + + :param response: The raw response of a HTTP request. + :returns: A response suitable to be used by clients. + """ + metadata = response.info() + data = response.read() + response.close() + if metadata.get('content-encoding') == 'gzip': + return _gzip_decode(data) + else: + return data diff --git a/third_party/transifex-client/txclib/log.py b/third_party/transifex-client/txclib/log.py new file mode 100644 index 00000000..9baf3220 --- /dev/null +++ b/third_party/transifex-client/txclib/log.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +""" +Add logging capabilities to tx-client. +""" + +import sys +import logging + +_logger = logging.getLogger('txclib') +_logger.setLevel(logging.INFO) + +_formatter = logging.Formatter('%(message)s') + +_error_handler = logging.StreamHandler(sys.stderr) +_error_handler.setLevel(logging.ERROR) +_error_handler.setFormatter(_formatter) +_logger.addHandler(_error_handler) + +_msg_handler = logging.StreamHandler(sys.stdout) +_msg_handler.setLevel(logging.DEBUG) +_msg_handler.setFormatter(_formatter) +_msg_filter = logging.Filter() +_msg_filter.filter = lambda r: r.levelno < logging.ERROR +_msg_handler.addFilter(_msg_filter) +_logger.addHandler(_msg_handler) + +logger = _logger + + +def set_log_level(level): + """Set the level for the logger. + + Args: + level: A string among DEBUG, INFO, WARNING, ERROR, CRITICAL. + """ + logger.setLevel(getattr(logging, level)) diff --git a/third_party/transifex-client/txclib/parsers.py b/third_party/transifex-client/txclib/parsers.py new file mode 100644 index 00000000..fd3237d2 --- /dev/null +++ b/third_party/transifex-client/txclib/parsers.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- + +from optparse import OptionParser, OptionGroup + + +class EpilogParser(OptionParser): + def format_epilog(self, formatter): + return self.epilog + + +def delete_parser(): + """Return the command-line parser for the delete command.""" + usage = "usage: %prog [tx_options] delete OPTION [OPTIONS]" + description = ( + "This command deletes translations for a resource in the remote server." + ) + epilog = ( + "\nExamples:\n" + " To delete a translation:\n " + "$ tx delete -r project.resource -l \n\n" + " To delete a resource:\n $ tx delete -r project.resource\n" + ) + parser = EpilogParser(usage=usage, description=description, epilog=epilog) + parser.add_option( + "-r", "--resource", action="store", dest="resources", default=None, + help="Specify the resource you want to delete (defaults to all)" + ) + parser.add_option( + "-l","--language", action="store", dest="languages", + default=None, help="Specify the translation you want to delete" + ) + parser.add_option( + "--skip", action="store_true", dest="skip_errors", default=False, + help="Don't stop on errors." + ) + parser.add_option( + "-f","--force", action="store_true", dest="force_delete", + default=False, help="Delete an entity forcefully." + ) + return parser + + +def help_parser(): + """Return the command-line parser for the help command.""" + usage="usage: %prog help command" + description="Lists all available commands in the transifex command"\ + " client. If a command is specified, the help page of the specific"\ + " command is displayed instead." + + parser = OptionParser(usage=usage, description=description) + return parser + + +def init_parser(): + """Return the command-line parser for the init command.""" + usage="usage: %prog [tx_options] init " + description="This command initializes a new project for use with"\ + " transifex. It is recommended to execute this command in the"\ + " top level directory of your project so that you can include"\ + " all files under it in transifex. If no path is provided, the"\ + " current working dir will be used." + parser = OptionParser(usage=usage, description=description) + parser.add_option("--host", action="store", dest="host", + default=None, help="Specify a default Transifex host.") + parser.add_option("--user", action="store", dest="user", + default=None, help="Specify username for Transifex server.") + parser.add_option("--pass", action="store", dest="password", + default=None, help="Specify password for Transifex server.") + return parser + + +def pull_parser(): + """Return the command-line parser for the pull command.""" + usage="usage: %prog [tx_options] pull [options]" + description="This command pulls all outstanding changes from the remote"\ + " Transifex server to the local repository. By default, only the"\ + " files that are watched by Transifex will be updated but if you"\ + " want to fetch the translations for new languages as well, use the"\ + " -a|--all option. (Note: new translations are saved in the .tx folder"\ + " and require the user to manually rename them and add then in "\ + " transifex using the set_translation command)." + parser = OptionParser(usage=usage,description=description) + parser.add_option("-l","--language", action="store", dest="languages", + default=[], help="Specify which translations you want to pull" + " (defaults to all)") + parser.add_option("-r","--resource", action="store", dest="resources", + default=[], help="Specify the resource for which you want to pull" + " the translations (defaults to all)") + parser.add_option("-a","--all", action="store_true", dest="fetchall", + default=False, help="Fetch all translation files from server (even new" + " ones)") + parser.add_option("-s","--source", action="store_true", dest="fetchsource", + default=False, help="Force the fetching of the source file (default:" + " False)") + parser.add_option("-f","--force", action="store_true", dest="force", + default=False, help="Force download of translations files.") + parser.add_option("--skip", action="store_true", dest="skip_errors", + default=False, help="Don't stop on errors. Useful when pushing many" + " files concurrently.") + parser.add_option("--disable-overwrite", action="store_false", + dest="overwrite", default=True, + help="By default transifex will fetch new translations files and"\ + " replace existing ones. Use this flag if you want to disable"\ + " this feature") + parser.add_option("--minimum-perc", action="store", type="int", + dest="minimum_perc", default=0, + help="Specify the minimum acceptable percentage of a translation " + "in order to download it.") + parser.add_option( + "--mode", action="store", dest="mode", help=( + "Specify the mode of the translation file to pull (e.g. " + "'reviewed'). See http://bit.ly/txcmod1 for available values." + ) + ) + return parser + + +def push_parser(): + """Return the command-line parser for the push command.""" + usage="usage: %prog [tx_options] push [options]" + description="This command pushes all local files that have been added to"\ + " Transifex to the remote server. All new translations are merged"\ + " with existing ones and if a language doesn't exists then it gets"\ + " created. If you want to push the source file as well (either"\ + " because this is your first time running the client or because"\ + " you just have updated with new entries), use the -f|--force option."\ + " By default, this command will push all files which are watched by"\ + " Transifex but you can filter this per resource or/and language." + parser = OptionParser(usage=usage, description=description) + parser.add_option("-l","--language", action="store", dest="languages", + default=None, help="Specify which translations you want to push" + " (defaults to all)") + parser.add_option("-r","--resource", action="store", dest="resources", + default=None, help="Specify the resource for which you want to push" + " the translations (defaults to all)") + parser.add_option("-f","--force", action="store_true", dest="force_creation", + default=False, help="Push source files without checking modification" + " times.") + parser.add_option("--skip", action="store_true", dest="skip_errors", + default=False, help="Don't stop on errors. Useful when pushing many" + " files concurrently.") + parser.add_option("-s", "--source", action="store_true", dest="push_source", + default=False, help="Push the source file to the server.") + + parser.add_option("-t", "--translations", action="store_true", dest="push_translations", + default=False, help="Push the translation files to the server") + parser.add_option("--no-interactive", action="store_true", dest="no_interactive", + default=False, help="Don't require user input when forcing a push.") + return parser + + +def set_parser(): + """Return the command-line parser for the set command.""" + usage="usage: %prog [tx_options] set [options] [args]" + description="This command can be used to create a mapping between files"\ + " and projects either using local files or using files from a remote"\ + " Transifex server." + epilog="\nExamples:\n"\ + " To set the source file:\n $ tx set -r project.resource --source -l en \n\n"\ + " To set a single translation file:\n $ tx set -r project.resource -l de \n\n"\ + " To automatically detect and assign the source files and translations:\n"\ + " $ tx set --auto-local -r project.resource 'expr' --source-lang en\n\n"\ + " To set a specific file as a source and auto detect translations:\n"\ + " $ tx set --auto-local -r project.resource 'expr' --source-lang en"\ + " --source-file \n\n"\ + " To set a remote release/resource/project:\n"\ + " $ tx set --auto-remote \n" + parser = EpilogParser(usage=usage, description=description, epilog=epilog) + parser.add_option("--auto-local", action="store_true", dest="local", + default=False, help="Used when auto configuring local project.") + parser.add_option("--auto-remote", action="store_true", dest="remote", + default=False, help="Used when adding remote files from Transifex" + " server.") + parser.add_option("-r","--resource", action="store", dest="resource", + default=None, help="Specify the slug of the resource that you're" + " setting up (This must be in the following format:" + " `project_slug.resource_slug`).") + parser.add_option( + "--source", action="store_true", dest="is_source", default=False, + help=( + "Specify that the given file is a source file " + "[doesn't work with the --auto-* commands]." + ) + ) + parser.add_option("-l","--language", action="store", dest="language", + default=None, help="Specify which translations you want to pull" + " [doesn't work with the --auto-* commands].") + parser.add_option("-t", "--type", action="store", dest="i18n_type", + help=( + "Specify the i18n type of the resource(s). This is only needed, if " + "the resource(s) does not exist yet in Transifex. For a list of " + "available i18n types, see " + "http://help.transifex.com/features/formats.html" + ) + ) + parser.add_option("--minimum-perc", action="store", dest="minimum_perc", + help=( + "Specify the minimum acceptable percentage of a translation " + "in order to download it." + ) + ) + parser.add_option( + "--mode", action="store", dest="mode", help=( + "Specify the mode of the translation file to pull (e.g. " + "'reviewed'). See http://help.transifex.com/features/client/" + "index.html#defining-the-mode-of-the-translated-file for the" + "available values." + ) + ) + group = OptionGroup(parser, "Extended options", "These options can only be" + " used with the --auto-local command.") + group.add_option("-s","--source-language", action="store", + dest="source_language", + default=None, help="Specify the source language of a resource" + " [requires --auto-local].") + group.add_option("-f","--source-file", action="store", dest="source_file", + default=None, help="Specify the source file of a resource [requires" + " --auto-local].") + group.add_option("--execute", action="store_true", dest="execute", + default=False, help="Execute commands [requires --auto-local].") + parser.add_option_group(group) + return parser + + +def status_parser(): + """Return the command-line parser for the status command.""" + usage="usage: %prog [tx_options] status [options]" + description="Prints the status of the current project by reading the"\ + " data in the configuration file." + parser = OptionParser(usage=usage,description=description) + parser.add_option("-r","--resource", action="store", dest="resources", + default=[], help="Specify resources") + return parser + + +def parse_csv_option(option): + """Return a list out of the comma-separated option or an empty list.""" + if option: + return option.split(',') + else: + return [] diff --git a/third_party/transifex-client/txclib/processors.py b/third_party/transifex-client/txclib/processors.py new file mode 100644 index 00000000..dc3a73f9 --- /dev/null +++ b/third_party/transifex-client/txclib/processors.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- + +""" +Module for API-related calls. +""" + +import urlparse + + +def hostname_tld_migration(hostname): + """ + Migrate transifex.net to transifex.com. + + :param hostname: The hostname to migrate (if needed). + :returns: A hostname with the transifex.com domain (if needed). + """ + parts = urlparse.urlparse(hostname) + if parts.hostname.endswith('transifex.net'): + hostname = hostname.replace('transifex.net', 'transifex.com', 1) + return hostname + + +def hostname_ssl_migration(hostname): + """ + Migrate Transifex hostnames to use HTTPS. + + :param hostname: The hostname to migrate (if needed). + :returns: A https hostname (if needed). + """ + parts = urlparse.urlparse(hostname) + is_transifex = ( + parts.hostname[-14:-3] == '.transifex.' or + parts.hostname == 'transifex.net' or + parts.hostname == 'transifex.com' + ) + is_https = parts.scheme == 'https' + if is_transifex and not is_https: + if not parts.scheme: + hostname = 'https:' + hostname + else: + hostname = hostname.replace(parts.scheme, 'https', 1) + return hostname + + +def visit_hostname(hostname): + """ + Have a chance to visit a hostname before actually using it. + + :param hostname: The original hostname. + :returns: The hostname with the necessary changes. + """ + for processor in [hostname_ssl_migration, hostname_tld_migration, ]: + hostname = processor(hostname) + return hostname diff --git a/third_party/transifex-client/txclib/project.py b/third_party/transifex-client/txclib/project.py new file mode 100644 index 00000000..88bb46bf --- /dev/null +++ b/third_party/transifex-client/txclib/project.py @@ -0,0 +1,1233 @@ +# -*- coding: utf-8 -*- +import base64 +import copy +import getpass +import os +import re +import fnmatch +import urllib2 +import datetime, time +import ConfigParser + +from txclib.web import * +from txclib.utils import * +from txclib.urls import API_URLS +from txclib.config import OrderedRawConfigParser, Flipdict +from txclib.log import logger +from txclib.http_utils import http_response +from txclib.processors import visit_hostname + + +class ProjectNotInit(Exception): + pass + + +class Project(object): + """ + Represents an association between the local and remote project instances. + """ + + def __init__(self, path_to_tx=None, init=True): + """ + Initialize the Project attributes. + """ + if init: + self._init(path_to_tx) + + def _init(self, path_to_tx=None): + instructions = "Run 'tx init' to initialize your project first!" + try: + self.root = self._get_tx_dir_path(path_to_tx) + self.config_file = self._get_config_file_path(self.root) + self.config = self._read_config_file(self.config_file) + self.txrc_file = self._get_transifex_file() + self.txrc = self._get_transifex_config(self.txrc_file) + except ProjectNotInit, e: + logger.error('\n'.join([unicode(e), instructions])) + raise + + def _get_config_file_path(self, root_path): + """Check the .tx/config file exists.""" + config_file = os.path.join(root_path, ".tx", "config") + logger.debug("Config file is %s" % config_file) + if not os.path.exists(config_file): + msg = "Cannot find the config file (.tx/config)!" + raise ProjectNotInit(msg) + return config_file + + def _get_tx_dir_path(self, path_to_tx): + """Check the .tx directory exists.""" + root_path = path_to_tx or find_dot_tx() + logger.debug("Path to tx is %s." % root_path) + if not root_path: + msg = "Cannot find any .tx directory!" + raise ProjectNotInit(msg) + return root_path + + def _read_config_file(self, config_file): + """Parse the config file and return its contents.""" + config = OrderedRawConfigParser() + try: + config.read(config_file) + except Exception, err: + msg = "Cannot open/parse .tx/config file: %s" % err + raise ProjectNotInit(msg) + return config + + def _get_transifex_config(self, txrc_file): + """Read the configuration from the .transifexrc file.""" + txrc = OrderedRawConfigParser() + try: + txrc.read(txrc_file) + except Exception, e: + msg = "Cannot read global configuration file: %s" % e + raise ProjectNotInit(msg) + self._migrate_txrc_file(txrc) + return txrc + + def _migrate_txrc_file(self, txrc): + """Migrate the txrc file, if needed.""" + for section in txrc.sections(): + orig_hostname = txrc.get(section, 'hostname') + hostname = visit_hostname(orig_hostname) + if hostname != orig_hostname: + msg = "Hostname %s should be changed to %s." + logger.info(msg % (orig_hostname, hostname)) + if (sys.stdin.isatty() and sys.stdout.isatty() and + confirm('Change it now? ', default=True)): + txrc.set(section, 'hostname', hostname) + msg = 'Hostname changed' + logger.info(msg) + else: + hostname = orig_hostname + self._save_txrc_file(txrc) + return txrc + + def _get_transifex_file(self, directory=None): + """Fetch the path of the .transifexrc file. + + It is in the home directory ofthe user by default. + """ + if directory is None: + directory = os.path.expanduser('~') + txrc_file = os.path.join(directory, ".transifexrc") + logger.debug(".transifexrc file is at %s" % directory) + if not os.path.exists(txrc_file): + msg = "No authentication data found." + logger.info(msg) + mask = os.umask(077) + open(txrc_file, 'w').close() + os.umask(mask) + return txrc_file + + def validate_config(self): + """ + To ensure the json structure is correctly formed. + """ + pass + + def getset_host_credentials(self, host, user=None, password=None): + """ + Read .transifexrc and report user,pass for a specific host else ask the + user for input. + """ + try: + username = self.txrc.get(host, 'username') + passwd = self.txrc.get(host, 'password') + except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): + logger.info("No entry found for host %s. Creating..." % host) + username = user or raw_input("Please enter your transifex username: ") + while (not username): + username = raw_input("Please enter your transifex username: ") + passwd = password + while (not passwd): + passwd = getpass.getpass() + + logger.info("Updating %s file..." % self.txrc_file) + self.txrc.add_section(host) + self.txrc.set(host, 'username', username) + self.txrc.set(host, 'password', passwd) + self.txrc.set(host, 'token', '') + self.txrc.set(host, 'hostname', host) + + return username, passwd + + def set_remote_resource(self, resource, source_lang, i18n_type, host, + file_filter="translations%(proj)s.%(res)s.%(extension)s"): + """ + Method to handle the add/conf of a remote resource. + """ + if not self.config.has_section(resource): + self.config.add_section(resource) + + p_slug, r_slug = resource.split('.') + file_filter = file_filter.replace("", r"%s" % os.path.sep) + self.url_info = { + 'host': host, + 'project': p_slug, + 'resource': r_slug + } + extension = self._extension_for(i18n_type)[1:] + + self.config.set(resource, 'source_lang', source_lang) + self.config.set( + resource, 'file_filter', + file_filter % {'proj': p_slug, 'res': r_slug, 'extension': extension} + ) + if host != self.config.get('main', 'host'): + self.config.set(resource, 'host', host) + + def get_resource_host(self, resource): + """ + Returns the host that the resource is configured to use. If there is no + such option we return the default one + """ + if self.config.has_option(resource, 'host'): + return self.config.get(resource, 'host') + return self.config.get('main', 'host') + + def get_resource_lang_mapping(self, resource): + """ + Get language mappings for a specific resource. + """ + lang_map = Flipdict() + try: + args = self.config.get("main", "lang_map") + for arg in args.replace(' ', '').split(','): + k,v = arg.split(":") + lang_map.update({k:v}) + except ConfigParser.NoOptionError: + pass + except (ValueError, KeyError): + raise Exception("Your lang map configuration is not correct.") + + if self.config.has_section(resource): + res_lang_map = Flipdict() + try: + args = self.config.get(resource, "lang_map") + for arg in args.replace(' ', '').split(','): + k,v = arg.split(":") + res_lang_map.update({k:v}) + except ConfigParser.NoOptionError: + pass + except (ValueError, KeyError): + raise Exception("Your lang map configuration is not correct.") + + # merge the lang maps and return result + lang_map.update(res_lang_map) + + return lang_map + + + def get_resource_files(self, resource): + """ + Get a dict for all files assigned to a resource. First we calculate the + files matching the file expression and then we apply all translation + excpetions. The resulting dict will be in this format: + + { 'en': 'path/foo/en/bar.po', 'de': 'path/foo/de/bar.po', 'es': 'path/exceptions/es.po'} + + NOTE: All paths are relative to the root of the project + """ + tr_files = {} + if self.config.has_section(resource): + try: + file_filter = self.config.get(resource, "file_filter") + except ConfigParser.NoOptionError: + file_filter = "$^" + source_lang = self.config.get(resource, "source_lang") + source_file = self.get_resource_option(resource, 'source_file') or None + expr_re = regex_from_filefilter(file_filter, self.root) + expr_rec = re.compile(expr_re) + for root, dirs, files in os.walk(self.root): + for f in files: + f_path = os.path.abspath(os.path.join(root, f)) + match = expr_rec.match(f_path) + if match: + lang = match.group(1) + if lang != source_lang: + f_path = relpath(f_path, self.root) + if f_path != source_file: + tr_files.update({lang: f_path}) + + for (name, value) in self.config.items(resource): + if name.startswith("trans."): + lang = name.split('.')[1] + # delete language which has same file + if value in tr_files.values(): + keys = [] + for k, v in tr_files.iteritems(): + if v == value: + keys.append(k) + if len(keys) == 1: + del tr_files[keys[0]] + else: + raise Exception("Your configuration seems wrong."\ + " You have multiple languages pointing to"\ + " the same file.") + # Add language with correct file + tr_files.update({lang:value}) + + return tr_files + + return None + + def get_resource_option(self, resource, option): + """ + Return the requested option for a specific resource + + If there is no such option, we return None + """ + + if self.config.has_section(resource): + if self.config.has_option(resource, option): + return self.config.get(resource, option) + return None + + def get_resource_list(self, project=None): + """ + Parse config file and return tuples with the following format + + [ (project_slug, resource_slug), (..., ...)] + """ + + resource_list= [] + for r in self.config.sections(): + if r == 'main': + continue + p_slug, r_slug = r.split('.', 1) + if project and p_slug != project: + continue + resource_list.append(r) + + return resource_list + + def save(self): + """ + Store the config dictionary in the .tx/config file of the project. + """ + self._save_tx_config() + self._save_txrc_file() + + def _save_tx_config(self, config=None): + """Save the local config file.""" + if config is None: + config = self.config + fh = open(self.config_file,"w") + config.write(fh) + fh.close() + + def _save_txrc_file(self, txrc=None): + """Save the .transifexrc file.""" + if txrc is None: + txrc = self.txrc + mask = os.umask(077) + fh = open(self.txrc_file, 'w') + txrc.write(fh) + fh.close() + os.umask(mask) + + def get_full_path(self, relpath): + if relpath[0] == "/": + return relpath + else: + return os.path.join(self.root, relpath) + + def pull(self, languages=[], resources=[], overwrite=True, fetchall=False, + fetchsource=False, force=False, skip=False, minimum_perc=0, mode=None): + """Pull all translations file from transifex server.""" + self.minimum_perc = minimum_perc + resource_list = self.get_chosen_resources(resources) + + if mode == 'reviewed': + url = 'pull_reviewed_file' + elif mode == 'translator': + url = 'pull_translator_file' + elif mode == 'developer': + url = 'pull_developer_file' + else: + url = 'pull_file' + + for resource in resource_list: + logger.debug("Handling resource %s" % resource) + self.resource = resource + project_slug, resource_slug = resource.split('.') + files = self.get_resource_files(resource) + slang = self.get_resource_option(resource, 'source_lang') + sfile = self.get_resource_option(resource, 'source_file') + lang_map = self.get_resource_lang_mapping(resource) + host = self.get_resource_host(resource) + logger.debug("Language mapping is: %s" % lang_map) + if mode is None: + mode = self._get_option(resource, 'mode') + self.url_info = { + 'host': host, + 'project': project_slug, + 'resource': resource_slug + } + logger.debug("URL data are: %s" % self.url_info) + + stats = self._get_stats_for_resource() + + + try: + file_filter = self.config.get(resource, 'file_filter') + except ConfigParser.NoOptionError: + file_filter = None + + # Pull source file + pull_languages = set([]) + new_translations = set([]) + + if fetchall: + new_translations = self._new_translations_to_add( + files, slang, lang_map, stats, force + ) + if new_translations: + msg = "New translations found for the following languages: %s" + logger.info(msg % ', '.join(new_translations)) + + existing, new = self._languages_to_pull( + languages, files, lang_map, stats, force + ) + pull_languages |= existing + new_translations |= new + logger.debug("Adding to new translations: %s" % new) + + if fetchsource: + if sfile and slang not in pull_languages: + pull_languages.add(slang) + elif slang not in new_translations: + new_translations.add(slang) + + if pull_languages: + logger.debug("Pulling languages for: %s" % pull_languages) + msg = "Pulling translations for resource %s (source: %s)" + logger.info(msg % (resource, sfile)) + + for lang in pull_languages: + local_lang = lang + if lang in lang_map.values(): + remote_lang = lang_map.flip[lang] + else: + remote_lang = lang + if languages and lang not in pull_languages: + logger.debug("Skipping language %s" % lang) + continue + if lang != slang: + local_file = files.get(lang, None) or files[lang_map[lang]] + else: + local_file = sfile + logger.debug("Using file %s" % local_file) + + kwargs = { + 'lang': remote_lang, + 'stats': stats, + 'local_file': local_file, + 'force': force, + 'mode': mode, + } + if not self._should_update_translation(**kwargs): + msg = "Skipping '%s' translation (file: %s)." + logger.info( + msg % (color_text(remote_lang, "RED"), local_file) + ) + continue + + if not overwrite: + local_file = ("%s.new" % local_file) + logger.warning( + " -> %s: %s" % (color_text(remote_lang, "RED"), local_file) + ) + try: + r = self.do_url_request(url, language=remote_lang) + except Exception,e: + if not skip: + raise e + else: + logger.error(e) + continue + base_dir = os.path.split(local_file)[0] + mkdir_p(base_dir) + fd = open(local_file, 'wb') + fd.write(r) + fd.close() + + if new_translations: + msg = "Pulling new translations for resource %s (source: %s)" + logger.info(msg % (resource, sfile)) + for lang in new_translations: + if lang in lang_map.keys(): + local_lang = lang_map[lang] + else: + local_lang = lang + remote_lang = lang + if file_filter: + local_file = relpath(os.path.join(self.root, + file_filter.replace('', local_lang)), os.curdir) + else: + trans_dir = os.path.join(self.root, ".tx", resource) + if not os.path.exists(trans_dir): + os.mkdir(trans_dir) + local_file = relpath(os.path.join(trans_dir, '%s_translation' % + local_lang, os.curdir)) + + if lang != slang: + satisfies_min = self._satisfies_min_translated( + stats[remote_lang], mode + ) + if not satisfies_min: + msg = "Skipping language %s due to used options." + logger.info(msg % lang) + continue + logger.warning( + " -> %s: %s" % (color_text(remote_lang, "RED"), local_file) + ) + r = self.do_url_request(url, language=remote_lang) + + base_dir = os.path.split(local_file)[0] + mkdir_p(base_dir) + fd = open(local_file, 'wb') + fd.write(r) + fd.close() + + def push(self, source=False, translations=False, force=False, resources=[], languages=[], + skip=False, no_interactive=False): + """ + Push all the resources + """ + resource_list = self.get_chosen_resources(resources) + self.skip = skip + self.force = force + for resource in resource_list: + push_languages = [] + project_slug, resource_slug = resource.split('.') + files = self.get_resource_files(resource) + slang = self.get_resource_option(resource, 'source_lang') + sfile = self.get_resource_option(resource, 'source_file') + lang_map = self.get_resource_lang_mapping(resource) + host = self.get_resource_host(resource) + logger.debug("Language mapping is: %s" % lang_map) + logger.debug("Using host %s" % host) + self.url_info = { + 'host': host, + 'project': project_slug, + 'resource': resource_slug + } + + logger.info("Pushing translations for resource %s:" % resource) + + stats = self._get_stats_for_resource() + + if force and not no_interactive: + answer = raw_input("Warning: By using --force, the uploaded" + " files will overwrite remote translations, even if they" + " are newer than your uploaded files.\nAre you sure you" + " want to continue? [y/N] ") + + if not answer in ["", 'Y', 'y', "yes", 'YES']: + return + + if source: + if sfile == None: + logger.error("You don't seem to have a proper source file" + " mapping for resource %s. Try without the --source" + " option or set a source file first and then try again." % + resource) + continue + # Push source file + try: + logger.warning("Pushing source file (%s)" % sfile) + if not self._resource_exists(stats): + logger.info("Resource does not exist. Creating...") + fileinfo = "%s;%s" % (resource_slug, slang) + filename = self.get_full_path(sfile) + self._create_resource(resource, project_slug, fileinfo, filename) + self.do_url_request( + 'push_source', multipart=True, method="PUT", + files=[( + "%s;%s" % (resource_slug, slang) + , self.get_full_path(sfile) + )], + ) + except Exception, e: + if not skip: + raise + else: + logger.error(e) + else: + try: + self.do_url_request('resource_details') + except Exception, e: + code = getattr(e, 'code', None) + if code == 404: + msg = "Resource %s doesn't exist on the server." + logger.error(msg % resource) + continue + + if translations: + # Check if given language codes exist + if not languages: + push_languages = files.keys() + else: + push_languages = [] + f_langs = files.keys() + for l in languages: + if l in lang_map.keys(): + l = lang_map[l] + push_languages.append(l) + if l not in f_langs: + msg = "Warning: No mapping found for language code '%s'." + logger.error(msg % color_text(l,"RED")) + logger.debug("Languages to push are %s" % push_languages) + + # Push translation files one by one + for lang in push_languages: + local_lang = lang + if lang in lang_map.values(): + remote_lang = lang_map.flip[lang] + else: + remote_lang = lang + + local_file = files[local_lang] + + kwargs = { + 'lang': remote_lang, + 'stats': stats, + 'local_file': local_file, + 'force': force, + } + if not self._should_push_translation(**kwargs): + msg = "Skipping '%s' translation (file: %s)." + logger.info(msg % (color_text(lang, "RED"), local_file)) + continue + + msg = "Pushing '%s' translations (file: %s)" + logger.warning( + msg % (color_text(remote_lang, "RED"), local_file) + ) + try: + self.do_url_request( + 'push_translation', multipart=True, method='PUT', + files=[( + "%s;%s" % (resource_slug, remote_lang), + self.get_full_path(local_file) + )], language=remote_lang + ) + logger.debug("Translation %s pushed." % remote_lang) + except Exception, e: + if not skip: + raise e + else: + logger.error(e) + + def delete(self, resources=[], languages=[], skip=False, force=False): + """Delete translations.""" + resource_list = self.get_chosen_resources(resources) + self.skip = skip + self.force = force + + if not languages: + delete_func = self._delete_resource + else: + delete_func = self._delete_translations + + for resource in resource_list: + project_slug, resource_slug = resource.split('.') + host = self.get_resource_host(resource) + self.url_info = { + 'host': host, + 'project': project_slug, + 'resource': resource_slug + } + logger.debug("URL data are: %s" % self.url_info) + project_details = parse_json( + self.do_url_request('project_details', project=self) + ) + teams = project_details['teams'] + stats = self._get_stats_for_resource() + delete_func(project_details, resource, stats, languages) + + def _delete_resource(self, project_details, resource, stats, *args): + """Delete a resource from Transifex.""" + project_slug, resource_slug = resource.split('.') + project_resource_slugs = [ + r['slug'] for r in project_details['resources'] + ] + logger.info("Deleting resource %s:" % resource) + if resource_slug not in project_resource_slugs: + if not self.skip: + msg = "Skipping: %s : Resource does not exist." + logger.info(msg % resource) + return + if not self.force: + slang = self.get_resource_option(resource, 'source_lang') + for language in stats: + if language == slang: + continue + if int(stats[language]['translated_entities']) > 0: + msg = ( + "Skipping: %s : Unable to delete resource because it " + "has a not empty %s translation.\nPlease use -f or " + "--force option to delete this resource." + ) + logger.info(msg % (resource, language)) + return + try: + self.do_url_request('delete_resource', method="DELETE") + self.config.remove_section(resource) + self.save() + msg = "Deleted resource %s of project %s." + logger.info(msg % (resource_slug, project_slug)) + except Exception, e: + msg = "Unable to delete resource %s of project %s." + logger.error(msg % (resource_slug, project_slug)) + if not self.skip: + raise + + def _delete_translations(self, project_details, resource, stats, languages): + """Delete the specified translations for the specified resource.""" + logger.info("Deleting translations from resource %s:" % resource) + for language in languages: + self._delete_translation(project_details, resource, stats, language) + + def _delete_translation(self, project_details, resource, stats, language): + """Delete a specific translation from the specified resource.""" + project_slug, resource_slug = resource.split('.') + if language not in stats: + if not self.skip: + msg = "Skipping %s: Translation does not exist." + logger.warning(msg % (language)) + return + if not self.force: + teams = project_details['teams'] + if language in teams: + msg = ( + "Skipping %s: Unable to delete translation because it is " + "associated with a team.\nPlease use -f or --force option " + "to delete this translation." + ) + logger.warning(msg % language) + return + if int(stats[language]['translated_entities']) > 0: + msg = ( + "Skipping %s: Unable to delete translation because it " + "is not empty.\nPlease use -f or --force option to delete " + "this translation." + ) + logger.warning(msg % language) + return + try: + self.do_url_request( + 'delete_translation', language=language, method="DELETE" + ) + msg = "Deleted language %s from resource %s of project %s." + logger.info(msg % (language, resource_slug, project_slug)) + except Exception, e: + msg = "Unable to delete translation %s" + logger.error(msg % language) + if not self.skip: + raise + + def do_url_request(self, api_call, multipart=False, data=None, + files=[], encoding=None, method="GET", **kwargs): + """ + Issues a url request. + """ + # Read the credentials from the config file (.transifexrc) + host = self.url_info['host'] + try: + username = self.txrc.get(host, 'username') + passwd = self.txrc.get(host, 'password') + token = self.txrc.get(host, 'token') + hostname = self.txrc.get(host, 'hostname') + except ConfigParser.NoSectionError: + raise Exception("No user credentials found for host %s. Edit" + " ~/.transifexrc and add the appropriate info in there." % + host) + + # Create the Url + kwargs['hostname'] = hostname + kwargs.update(self.url_info) + url = (API_URLS[api_call] % kwargs).encode('UTF-8') + logger.debug(url) + + opener = None + headers = None + req = None + + if multipart: + opener = urllib2.build_opener(MultipartPostHandler) + for info,filename in files: + data = { "resource" : info.split(';')[0], + "language" : info.split(';')[1], + "uploaded_file" : open(filename,'rb') } + + urllib2.install_opener(opener) + req = RequestWithMethod(url=url, data=data, method=method) + else: + req = RequestWithMethod(url=url, data=data, method=method) + if encoding: + req.add_header("Content-Type",encoding) + + base64string = base64.encodestring('%s:%s' % (username, passwd))[:-1] + authheader = "Basic %s" % base64string + req.add_header("Authorization", authheader) + req.add_header("Accept-Encoding", "gzip,deflate") + req.add_header("User-Agent", user_agent_identifier()) + + try: + response = urllib2.urlopen(req, timeout=300) + return http_response(response) + except urllib2.HTTPError, e: + if e.code in [401, 403, 404]: + raise e + elif 200 <= e.code < 300: + return None + else: + # For other requests, we should print the message as well + raise Exception("Remote server replied: %s" % e.read()) + except urllib2.URLError, e: + error = e.args[0] + raise Exception("Remote server replied: %s" % error[1]) + + + def _should_update_translation(self, lang, stats, local_file, force=False, + mode=None): + """Whether a translation should be udpated from Transifex. + + We use the following criteria for that: + - If user requested to force the download. + - If language exists in Transifex. + - If the local file is older than the Transifex's file. + - If the user requested a x% completion. + + Args: + lang: The language code to check. + stats: The (global) statistics object. + local_file: The local translation file. + force: A boolean flag. + mode: The mode for the translation. + Returns: + True or False. + """ + return self._should_download(lang, stats, local_file, force) + + def _should_add_translation(self, lang, stats, force=False, mode=None): + """Whether a translation should be added from Transifex. + + We use the following criteria for that: + - If user requested to force the download. + - If language exists in Transifex. + - If the user requested a x% completion. + + Args: + lang: The language code to check. + stats: The (global) statistics object. + force: A boolean flag. + mode: The mode for the translation. + Returns: + True or False. + """ + return self._should_download(lang, stats, None, force) + + def _should_download(self, lang, stats, local_file=None, force=False, + mode=None): + """Return whether a translation should be downloaded. + + If local_file is None, skip the timestamps check (the file does + not exist locally). + """ + try: + lang_stats = stats[lang] + except KeyError, e: + logger.debug("No lang %s in statistics" % lang) + return False + + satisfies_min = self._satisfies_min_translated(lang_stats, mode) + if not satisfies_min: + return False + + if force: + logger.debug("Downloading translation due to -f") + return True + + if local_file is not None: + remote_update = self._extract_updated(lang_stats) + if not self._remote_is_newer(remote_update, local_file): + logger.debug("Local is newer than remote for lang %s" % lang) + return False + return True + + def _should_push_translation(self, lang, stats, local_file, force=False): + """Return whether a local translation file should be + pushed to Trasnifex. + + We use the following criteria for that: + - If user requested to force the upload. + - If language exists in Transifex. + - If local file is younger than the remote file. + + Args: + lang: The language code to check. + stats: The (global) statistics object. + local_file: The local translation file. + force: A boolean flag. + Returns: + True or False. + """ + if force: + logger.debug("Push translation due to -f.") + return True + try: + lang_stats = stats[lang] + except KeyError, e: + logger.debug("Language %s does not exist in Transifex." % lang) + return True + if local_file is not None: + remote_update = self._extract_updated(lang_stats) + if self._remote_is_newer(remote_update, local_file): + msg = "Remote translation is newer than local file for lang %s" + logger.debug(msg % lang) + return False + return True + + def _generate_timestamp(self, update_datetime): + """Generate a UNIX timestamp from the argument. + + Args: + update_datetime: The datetime in the format used by Transifex. + Returns: + A float, representing the timestamp that corresponds to the + argument. + """ + time_format = "%Y-%m-%d %H:%M:%S" + return time.mktime( + datetime.datetime( + *time.strptime(update_datetime, time_format)[0:5] + ).utctimetuple() + ) + + def _get_time_of_local_file(self, path): + """Get the modified time of the path_. + + Args: + path: The path we want the mtime for. + Returns: + The time as a timestamp or None, if the file does not exist + """ + if not os.path.exists(path): + return None + return time.mktime(time.gmtime(os.path.getmtime(path))) + + def _satisfies_min_translated(self, stats, mode=None): + """Check whether a translation fulfills the filter used for + minimum translated percentage. + + Args: + perc: The current translation percentage. + Returns: + True or False + """ + cur = self._extract_completed(stats, mode) + option_name = 'minimum_perc' + if self.minimum_perc is not None: + minimum_percent = self.minimum_perc + else: + global_minimum = int( + self.get_resource_option('main', option_name) or 0 + ) + resource_minimum = int( + self.get_resource_option( + self.resource, option_name + ) or global_minimum + ) + minimum_percent = resource_minimum + return cur >= minimum_percent + + def _remote_is_newer(self, remote_updated, local_file): + """Check whether the remote translation is newer that the local file. + + Args: + remote_updated: The date and time the translation was last + updated remotely. + local_file: The local file. + Returns: + True or False. + """ + if remote_updated is None: + logger.debug("No remote time") + return False + remote_time = self._generate_timestamp(remote_updated) + local_time = self._get_time_of_local_file( + self.get_full_path(local_file) + ) + logger.debug( + "Remote time is %s and local %s" % (remote_time, local_time) + ) + if local_time is not None and remote_time < local_time: + return False + return True + + @classmethod + def _extract_completed(cls, stats, mode=None): + """Extract the information for the translated percentage from the stats. + + Args: + stats: The stats object for a language as returned by Transifex. + mode: The mode of translations requested. + Returns: + The percentage of translation as integer. + """ + if mode == 'reviewed': + key = 'reviewed_percentage' + else: + key = 'completed' + try: + return int(stats[key][:-1]) + except KeyError, e: + return 0 + + @classmethod + def _extract_updated(cls, stats): + """Extract the information for the last update of a translation. + + Args: + stats: The stats object for a language as returned by Transifex. + Returns: + The last update field. + """ + try: + return stats['last_update'] + except KeyError, e: + return None + + def _new_translations_to_add(self, files, slang, lang_map, + stats, force=False): + """Return a list of translations which are new to the + local installation. + """ + new_translations = [] + timestamp = time.time() + langs = stats.keys() + logger.debug("Available languages are: %s" % langs) + + for lang in langs: + lang_exists = lang in files.keys() + lang_is_source = lang == slang + mapped_lang_exists = ( + lang in lang_map and lang_map[lang] in files.keys() + ) + if lang_exists or lang_is_source or mapped_lang_exists: + continue + if self._should_add_translation(lang, stats, force): + new_translations.append(lang) + return set(new_translations) + + def _get_stats_for_resource(self): + """Get the statistics information for a resource.""" + try: + r = self.do_url_request('resource_stats') + logger.debug("Statistics response is %s" % r) + stats = parse_json(r) + except urllib2.HTTPError, e: + logger.debug("Resource not found: %s" % e) + stats = {} + except Exception,e: + logger.debug("Network error: %s" % e) + raise + return stats + + def get_chosen_resources(self, resources): + """Get the resources the user selected. + + Support wildcards in the resources specified by the user. + + Args: + resources: A list of resources as specified in command-line or + an empty list. + Returns: + A list of resources. + """ + configured_resources = self.get_resource_list() + if not resources: + return configured_resources + + selected_resources = [] + for resource in resources: + found = False + for full_name in configured_resources: + if fnmatch.fnmatch(full_name, resource): + selected_resources.append(full_name) + found = True + if not found: + msg = "Specified resource '%s' does not exist." + raise Exception(msg % resource) + logger.debug("Operating on resources: %s" % selected_resources) + return selected_resources + + def _languages_to_pull(self, languages, files, lang_map, stats, force): + """Get a set of langauges to pull. + + Args: + languages: A list of languages the user selected in cmd. + files: A dictionary of current local translation files. + Returns: + A tuple of a set of existing languages and new translations. + """ + if not languages: + pull_languages = set([]) + pull_languages |= set(files.keys()) + mapped_files = [] + for lang in pull_languages: + if lang in lang_map.flip: + mapped_files.append(lang_map.flip[lang]) + pull_languages -= set(lang_map.flip.keys()) + pull_languages |= set(mapped_files) + return (pull_languages, set([])) + else: + pull_languages = [] + new_translations = [] + f_langs = files.keys() + for l in languages: + if l not in f_langs and not (l in lang_map and lang_map[l] in f_langs): + if self._should_add_translation(l, stats, force): + new_translations.append(l) + else: + if l in lang_map.keys(): + l = lang_map[l] + pull_languages.append(l) + return (set(pull_languages), set(new_translations)) + + def _extension_for(self, i18n_type): + """Return the extension used for the specified type.""" + try: + res = parse_json(self.do_url_request('formats')) + return res[i18n_type]['file-extensions'].split(',')[0] + except Exception,e: + logger.error(e) + return '' + + def _resource_exists(self, stats): + """Check if resource exists. + + Args: + stats: The statistics dict as returned by Tx. + Returns: + True, if the resource exists in the server. + """ + return bool(stats) + + def _create_resource(self, resource, pslug, fileinfo, filename, **kwargs): + """Create a resource. + + Args: + resource: The full resource name. + pslug: The slug of the project. + fileinfo: The information of the resource. + filename: The name of the file. + Raises: + URLError, in case of a problem. + """ + multipart = True + method = "POST" + api_call = 'create_resource' + + host = self.url_info['host'] + try: + username = self.txrc.get(host, 'username') + passwd = self.txrc.get(host, 'password') + token = self.txrc.get(host, 'token') + hostname = self.txrc.get(host, 'hostname') + except ConfigParser.NoSectionError: + raise Exception("No user credentials found for host %s. Edit" + " ~/.transifexrc and add the appropriate info in there." % + host) + + # Create the Url + kwargs['hostname'] = hostname + kwargs.update(self.url_info) + kwargs['project'] = pslug + url = (API_URLS[api_call] % kwargs).encode('UTF-8') + + opener = None + headers = None + req = None + + i18n_type = self._get_option(resource, 'type') + if i18n_type is None: + logger.error( + "Please define the resource type in .tx/config (eg. type = PO)." + " More info: http://bit.ly/txcl-rt" + ) + + opener = urllib2.build_opener(MultipartPostHandler) + data = { + "slug": fileinfo.split(';')[0], + "name": fileinfo.split(';')[0], + "uploaded_file": open(filename,'rb'), + "i18n_type": i18n_type + } + urllib2.install_opener(opener) + req = RequestWithMethod(url=url, data=data, method=method) + + base64string = base64.encodestring('%s:%s' % (username, passwd))[:-1] + authheader = "Basic %s" % base64string + req.add_header("Authorization", authheader) + + try: + fh = urllib2.urlopen(req) + except urllib2.HTTPError, e: + if e.code in [401, 403, 404]: + raise e + else: + # For other requests, we should print the message as well + raise Exception("Remote server replied: %s" % e.read()) + except urllib2.URLError, e: + error = e.args[0] + raise Exception("Remote server replied: %s" % error[1]) + + raw = fh.read() + fh.close() + return raw + + def _get_option(self, resource, option): + """Get the value for the option in the config file. + + If the option is not in the resource section, look for it in + the project. + + Args: + resource: The resource name. + option: The option the value of which we are interested in. + Returns: + The option value or None, if it does not exist. + """ + value = self.get_resource_option(resource, option) + if value is None: + if self.config.has_option('main', option): + return self.config.get('main', option) + return value + + def set_i18n_type(self, resources, i18n_type): + """Set the type for the specified resources.""" + self._set_resource_option(resources, key='type', value=i18n_type) + + def set_min_perc(self, resources, perc): + """Set the minimum percentage for the resources.""" + self._set_resource_option(resources, key='minimum_perc', value=perc) + + def set_default_mode(self, resources, mode): + """Set the default mode for the specified resources.""" + self._set_resource_option(resources, key='mode', value=mode) + + def _set_resource_option(self, resources, key, value): + """Set options in the config file. + + If resources is empty. set the option globally. + """ + if not resources: + self.config.set('main', key, value) + return + for r in resources: + self.config.set(r, key, value) diff --git a/third_party/transifex-client/txclib/urls.py b/third_party/transifex-client/txclib/urls.py new file mode 100644 index 00000000..0bb74fde --- /dev/null +++ b/third_party/transifex-client/txclib/urls.py @@ -0,0 +1,21 @@ +# These are the Transifex API urls + +API_URLS = { + 'get_resources': '%(hostname)s/api/2/project/%(project)s/resources/', + 'project_details': '%(hostname)s/api/2/project/%(project)s/?details', + 'resource_details': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/', + 'release_details': '%(hostname)s/api/2/project/%(project)s/release/%(release)s/', + 'pull_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file', + 'pull_reviewed_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=reviewed', + 'pull_translator_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=translated', + 'pull_developer_file': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/?file&mode=default', + 'resource_stats': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/stats/', + 'create_resource': '%(hostname)s/api/2/project/%(project)s/resources/', + 'push_source': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/content/', + 'push_translation': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/', + 'delete_translation': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/translation/%(language)s/', + 'formats': '%(hostname)s/api/2/formats/', + 'delete_resource': '%(hostname)s/api/2/project/%(project)s/resource/%(resource)s/', +} + + diff --git a/third_party/transifex-client/txclib/utils.py b/third_party/transifex-client/txclib/utils.py new file mode 100644 index 00000000..318bee91 --- /dev/null +++ b/third_party/transifex-client/txclib/utils.py @@ -0,0 +1,259 @@ +import os, sys, re, errno +try: + from json import loads as parse_json, dumps as compile_json +except ImportError: + from simplejson import loads as parse_json, dumps as compile_json +import urllib2 # This should go and instead use do_url_request everywhere + +from urls import API_URLS +from txclib.log import logger +from txclib.exceptions import UnknownCommandError + + +def find_dot_tx(path = os.path.curdir, previous = None): + """ + Return the path where .tx folder is found. + + The 'path' should be a DIRECTORY. + This process is functioning recursively from the current directory to each + one of the ancestors dirs. + """ + path = os.path.abspath(path) + if path == previous: + return None + joined = os.path.join(path, ".tx") + if os.path.isdir(joined): + return path + else: + return find_dot_tx(os.path.dirname(path), path) + + +################################################# +# Parse file filter expressions and create regex + +def regex_from_filefilter(file_filter, root_path = os.path.curdir): + """ + Create proper regex from expression + """ + # Force expr to be a valid regex expr (escaped) but keep intact + expr_re = re.escape(os.path.join(root_path, file_filter)) + expr_re = expr_re.replace("\\", '').replace( + '', '([^%(sep)s]+)' % { 'sep': re.escape(os.path.sep)}) + + return "^%s$" % expr_re + + +TX_URLS = { + 'resource': '(?Phttps?://(\w|\.|:|-)+)/projects/p/(?P(\w|-)+)/resource/(?P(\w|-)+)/?$', + 'release': '(?Phttps?://(\w|\.|:|-)+)/projects/p/(?P(\w|-)+)/r/(?P(\w|-)+)/?$', + 'project': '(?Phttps?://(\w|\.|:|-)+)/projects/p/(?P(\w|-)+)/?$', +} + + +def parse_tx_url(url): + """ + Try to match given url to any of the valid url patterns specified in + TX_URLS. If not match is found, we raise exception + """ + for type in TX_URLS.keys(): + pattern = TX_URLS[type] + m = re.match(pattern, url) + if m: + return type, m.groupdict() + + raise Exception("tx: Malformed url given. Please refer to our docs: http://bit.ly/txautor") + + +def get_details(api_call, username, password, *args, **kwargs): + """ + Get the tx project info through the API. + + This function can also be used to check the existence of a project. + """ + import base64 + url = (API_URLS[api_call] % (kwargs)).encode('UTF-8') + + req = urllib2.Request(url=url) + base64string = base64.encodestring('%s:%s' % (username, password))[:-1] + authheader = "Basic %s" % base64string + req.add_header("Authorization", authheader) + + try: + fh = urllib2.urlopen(req) + raw = fh.read() + fh.close() + remote_project = parse_json(raw) + except urllib2.HTTPError, e: + if e.code in [401, 403, 404]: + raise e + else: + # For other requests, we should print the message as well + raise Exception("Remote server replied: %s" % e.read()) + except urllib2.URLError, e: + error = e.args[0] + raise Exception("Remote server replied: %s" % error[1]) + + return remote_project + + +def valid_slug(slug): + """ + Check if a slug contains only valid characters. + + Valid chars include [-_\w] + """ + try: + a, b = slug.split('.') + except ValueError: + return False + else: + if re.match("^[A-Za-z0-9_-]*$", a) and re.match("^[A-Za-z0-9_-]*$", b): + return True + return False + + +def discover_commands(): + """ + Inspect commands.py and find all available commands + """ + import inspect + from txclib import commands + + command_table = {} + fns = inspect.getmembers(commands, inspect.isfunction) + + for name, fn in fns: + if name.startswith("cmd_"): + command_table.update({ + name.split("cmd_")[1]:fn + }) + + return command_table + + +def exec_command(command, *args, **kwargs): + """ + Execute given command + """ + commands = discover_commands() + try: + cmd_fn = commands[command] + except KeyError: + raise UnknownCommandError + cmd_fn(*args,**kwargs) + + +def mkdir_p(path): + try: + if path: + os.makedirs(path) + except OSError, exc: # Python >2.5 + if exc.errno == errno.EEXIST: + pass + else: + raise + + +def confirm(prompt='Continue?', default=True): + """ + Prompt the user for a Yes/No answer. + + Args: + prompt: The text displayed to the user ([Y/n] will be appended) + default: If the default value will be yes or no + """ + valid_yes = ['Y', 'y', 'Yes', 'yes', ] + valid_no = ['N', 'n', 'No', 'no', ] + if default: + prompt = prompt + '[Y/n]' + valid_yes.append('') + else: + prompt = prompt + '[y/N]' + valid_no.append('') + + ans = raw_input(prompt) + while (ans not in valid_yes and ans not in valid_no): + ans = raw_input(prompt) + + return ans in valid_yes + + +# Stuff for command line colored output + +COLORS = [ + 'BLACK', 'RED', 'GREEN', 'YELLOW', + 'BLUE', 'MAGENTA', 'CYAN', 'WHITE' +] + +DISABLE_COLORS = False + + +def color_text(text, color_name, bold=False): + """ + This command can be used to colorify command line output. If the shell + doesn't support this or the --disable-colors options has been set, it just + returns the plain text. + + Usage: + print "%s" % color_text("This text is red", "RED") + """ + if color_name in COLORS and not DISABLE_COLORS: + return '\033[%s;%sm%s\033[0m' % ( + int(bold), COLORS.index(color_name) + 30, text) + else: + return text + + +############################################## +# relpath implementation taken from Python 2.7 + +if not hasattr(os.path, 'relpath'): + if os.path is sys.modules.get('ntpath'): + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + + if not path: + raise ValueError("no path specified") + start_list = os.path.abspath(start).split(os.path.sep) + path_list = os.path.abspath(path).split(os.path.sep) + if start_list[0].lower() != path_list[0].lower(): + unc_path, rest = os.path.splitunc(path) + unc_start, rest = os.path.splitunc(start) + if bool(unc_path) ^ bool(unc_start): + raise ValueError("Cannot mix UNC and non-UNC paths (%s and %s)" + % (path, start)) + else: + raise ValueError("path is on drive %s, start on drive %s" + % (path_list[0], start_list[0])) + # Work out how much of the filepath is shared by start and path. + for i in range(min(len(start_list), len(path_list))): + if start_list[i].lower() != path_list[i].lower(): + break + else: + i += 1 + + rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return os.path.curdir + return os.path.join(*rel_list) + + else: + # default to posixpath definition + def relpath(path, start=os.path.curdir): + """Return a relative version of a path""" + + if not path: + raise ValueError("no path specified") + + start_list = os.path.abspath(start).split(os.path.sep) + path_list = os.path.abspath(path).split(os.path.sep) + + # Work out how much of the filepath is shared by start and path. + i = len(os.path.commonprefix([start_list, path_list])) + + rel_list = [os.path.pardir] * (len(start_list)-i) + path_list[i:] + if not rel_list: + return os.path.curdir + return os.path.join(*rel_list) +else: + from os.path import relpath diff --git a/third_party/transifex-client/txclib/web.py b/third_party/transifex-client/txclib/web.py new file mode 100644 index 00000000..a3cb3f00 --- /dev/null +++ b/third_party/transifex-client/txclib/web.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +import urllib2 +import itertools, mimetools, mimetypes +import platform +from txclib import get_version + +# Helper class to enable urllib2 to handle PUT/DELETE requests as well +class RequestWithMethod(urllib2.Request): + """Workaround for using DELETE with urllib2""" + def __init__(self, url, method, data=None, headers={}, + origin_req_host=None, unverifiable=False): + self._method = method + urllib2.Request.__init__(self, url, data=data, headers=headers, + origin_req_host=None, unverifiable=False) + + def get_method(self): + return self._method + +import urllib +import os, stat +from cStringIO import StringIO + +class Callable: + def __init__(self, anycallable): + self.__call__ = anycallable + +# Controls how sequences are uncoded. If true, elements may be given multiple +# values by assigning a sequence. +doseq = 1 + +class MultipartPostHandler(urllib2.BaseHandler): + handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first + + def http_request(self, request): + data = request.get_data() + if data is not None and type(data) != str: + v_files = [] + v_vars = [] + try: + for(key, value) in data.items(): + if type(value) == file: + v_files.append((key, value)) + else: + v_vars.append((key, value)) + except TypeError: + systype, value, traceback = sys.exc_info() + raise TypeError, "not a valid non-string sequence or mapping object", traceback + + if len(v_files) == 0: + data = urllib.urlencode(v_vars, doseq) + else: + boundary, data = self.multipart_encode(v_vars, v_files) + + contenttype = 'multipart/form-data; boundary=%s' % boundary + if(request.has_header('Content-Type') + and request.get_header('Content-Type').find('multipart/form-data') != 0): + print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') + request.add_unredirected_header('Content-Type', contenttype) + + request.add_data(data) + + return request + + def multipart_encode(vars, files, boundary = None, buf = None): + if boundary is None: + boundary = mimetools.choose_boundary() + if buf is None: + buf = StringIO() + for(key, value) in vars: + buf.write('--%s\r\n' % boundary) + buf.write('Content-Disposition: form-data; name="%s"' % key) + buf.write('\r\n\r\n' + value + '\r\n') + for(key, fd) in files: + file_size = os.fstat(fd.fileno())[stat.ST_SIZE] + filename = fd.name.split('/')[-1] + contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + buf.write('--%s\r\n' % boundary) + buf.write('Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename)) + buf.write('Content-Type: %s\r\n' % contenttype) + # buffer += 'Content-Length: %s\r\n' % file_size + fd.seek(0) + buf.write('\r\n' + fd.read() + '\r\n') + buf.write('--' + boundary + '--\r\n\r\n') + buf = buf.getvalue() + return boundary, buf + multipart_encode = Callable(multipart_encode) + + https_request = http_request + + +def user_agent_identifier(): + """Return the user agent for the client.""" + client_info = (get_version(), platform.system(), platform.machine()) + return "txclient/%s (%s %s)" % client_info