--- /dev/null
+[main]
+host = https://www.transifex.com
+
+[owncloud.android]
+file_filter = res/values-<lang>/strings.xml
+source_lang = en
+
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="hello">Hallo Welt, ownCloudStartBildschirm!</string>
+ <string name="app_name">ownCloud</string>
+ <string name="main_password">Passwort:</string>
+ <string name="main_login">Benutzername</string>
+ <string name="main_button_login">Anmelden</string>
+ <string name="main_welcome">Herzlich willkommen bei ihrer ownCloud</string>
+ <string name="main_files">Dateien</string>
+ <string name="main_music">Musik</string>
+ <string name="main_contacts">Kontakte</string>
+ <string name="main_calendar">Kalender</string>
+ <string name="main_bookmarks">Lesezeichen</string>
+ <string name="main_settings">Einstellungen</string>
+ <string name="main_tit_accsetup">Konto einrichten</string>
+ <string name="main_wrn_accsetup">Auf Ihrem Gerät sind keine Konten eingerichtet. Bitte erstellen Sie ein Konto, um diese App nutzen zu können.</string>
+ <string name="actionbar_sync">Konto synchronisieren</string>
+ <string name="actionbar_upload">Datei hochladen</string>
+ <string name="actionbar_mkdir">Verzeichnis erstellen</string>
+ <string name="actionbar_search">Suche</string>
+ <string name="actionbar_settings">Einstellungen</string>
+ <string name="prefs_category_general">Allgemein</string>
+ <string name="prefs_category_trackmydevice">Gerät verfolgen</string>
+ <string name="prefs_add_session">Neue Sitzung hinzufügen</string>
+ <string name="prefs_create_img_thumbnails">Bildvorschauen erstellen</string>
+ <string name="prefs_select_oc_account">Konto auswählen</string>
+ <string name="prefs_summary_select_oc_account">Bitte wählen, welches Konto von der App verwendet werden soll</string>
+ <string name="prefs_trackmydevice">Gerät verfolgen</string>
+ <string name="prefs_trackmydevice_summary_off">Geräteverfolgung in ownCloud aktivieren</string>
+ <string name="prefs_trackmydevice_summary_on">Ihre ownCloud verfolgt dieses Gerät</string>
+ <string name="prefs_trackmydevice_interval">Aktualisierungsintervall</string>
+ <string name="prefs_trackmydevice_interval_summary">Alle %1$s Minuten aktualisieren</string>
+ <string name="prefs_accounts">Konten</string>
+ <string name="auth_host_url">ownCloud URL</string>
+ <string name="auth_username">Benutzername</string>
+ <string name="auth_password">Passwort</string>
+ <string name="new_session_uri_error">Falsche URL gegeben</string>
+ <string name="new_session_session_name_error">Nicht gültiger Sitzungsname</string>
+ <string name="sync_string_files">Dateien</string>
+ <string name="uploader_no_file_selected">Sie haben keine Datei für das Hochladen ausgewählt</string>
+ <string name="setup_hint_username">Benutzername</string>
+ <string name="setup_hint_password">Passwort</string>
+ <string name="setup_hint_address">Internetadresse</string>
+ <string name="setup_hint_show_password">Passwort anzeigen?</string>
+ <string name="setup_title">Mit ihrer ownCloud verbinden</string>
+ <string name="setup_btn_connect">Verbinden</string>
+ <string name="uploader_btn_upload_text">Hochladen</string>
+ <string name="uploader_wrn_no_account_title">kein Konto gefunden</string>
+ <string name="uploader_wrn_no_account_text">Es sind keine ownCloud Konten auf ihrem Gerät eingerichtet. Bitte richten Sie zuerst ein Konto ein.</string>
+ <string name="uploader_wrn_no_account_setup_btn_text">Einrichten</string>
+ <string name="uploader_wrn_no_account_quit_btn_text">Beenden</string>
+ <string name="uploader_info_uploading">Hochladen</string>
+ <string name="uploader_btn_create_dir_text">Verzeichnis für hochgeladene Dateien erstellen</string>
+ <string name="filedetails_select_file">Klicken Sie auf eine Datei, um mehr Informationen zu bekommen.</string>
+ <string name="filedetails_size">Größe:</string>
+ <string name="filedetails_type">Art:</string>
+ <string name="filedetails_created">Erstellt:</string>
+ <string name="filedetails_modified">Geändert:</string>
+ <string name="filedetails_download">Herunterladen</string>
+ <string name="filedetails_open">Öffnen</string>
+ <string name="common_yes">Ja</string>
+ <string name="common_no">Nein</string>
+ <string name="common_ok">OK</string>
+ <string name="common_cancel">Abbrechen</string>
+ <string name="common_save_exit">Speichern & Schließen</string>
+ <string name="common_error">Fehler</string>
+ <string name="uploader_info_dirname">Verzeichnisname</string>
+ <string name="uploader_upload_succeed">Hochladen erfolgreich beendet</string>
+ <string name="uploader_upload_failed">Hochladen fehlgeschlagen</string>
+ <string name="uploader_files_uploaded_suffix">Dateien hochgeladen</string>
+ <string name="downloader_download_failed">Das Herunterladen konnte nicht erfolgreich beendet werden</string>
+ <string name="common_choose_account">Konto auswählen</string>
+ <string name="sync_string_contacts">Kontakte</string>
+ <string name="use_ssl">Sichere Verbindung nutzen</string>
+ <string name="location_no_provider">ownCloud kann Ihr Gerät nicht verfolgen. Bitte überprüfen Sie Ihre Standorteinstellungen</string>
+ <string name="pincode_enter_pin_code">Bitte geben Sie Ihre PIN ein</string>
+ <string name="pincode_enter_new_pin_code">Bitte geben Sie Ihre neue PIN ein</string>
+ <string name="pincode_configure_your_pin">Bitte geben Sie Ihre PIN ein</string>
+ <string name="pincode_reenter_your_pincode">Bitte geben Sie Ihre PIN erneut ein.</string>
+ <string name="pincode_remove_your_pincode">App PIN entfernen</string>
+ <string name="pincode_mismatch">Die PINs stimmen nicht überein</string>
+ <string name="pincode_wrong">Falsche PIN</string>
+ <string name="pincode_removed">App PIN wurde entfernt</string>
+ <string name="pincode_stored">App PIN wurde gespeichert</string>
+ <string-array name="prefs_trackmydevice_intervall_keys">
+ <item>15 Minuten</item>
+ <item>30 Minuten</item>
+ <item>60 Minuten</item>
+ </string-array>
+ <string-array name="prefs_trackmydevice_intervall_values">
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ </string-array>
+ <string name="auth_trying_to_login">Versuche sich anzumelden</string>
+ <string name="auth_no_net_conn_title">Keine Netzwerkverbindung</string>
+ <string name="auth_no_net_conn_message">Es konnte keine Netzwerkverbindung gefunden werden, bitte überprüfen Sie Ihre Internetverbindung</string>
+ <string name="auth_connect_anyway">Trotzdem verbinden</string>
+ <string name="auth_nossl_plain_ok_title">Sichere Verbindung nicht verfügbar.</string>
+ <string name="auth_nossl_plain_ok_message">Die App konnte keine sichere Verbindung zum Server herstellen. Eine nicht sichere Verbindung ist nichts desto trotz verfügbar. Wollen Sie fortfahren oder abbrechen.</string>
+ <string name="auth_connection_established">Verbindung hergestellt</string>
+ <string name="auth_testing_connection">Verbindung testen.</string>
+ <string name="auth_not_configured_title">falsch konfigurierte ownCloud</string>
+ <string name="auth_not_configured_message">Es scheint als wäre Ihre ownCloud Installation nicht richtig konfiguriert. Bitte kontaktieren Sie ihren Administrator, um weitere Details zu erhalten.</string>
+ <string name="auth_unknown_error_title">Ein unbekannter Fehler ist aufgetreten</string>
+ <string name="auth_unknown_error_message">Ein unbekannter Fehler ist aufgetreten. Bitte kontaktieren Sie den Administrator mit den Log Dateien Ihres Gerätes.</string>
+ <string name="auth_unknow_host_title">Es konnte keine Verbindung hergestellt werden</string>
+ <string name="auth_unknow_host_message">Es konnte keine Verbindung zum Host hergestellt werden. Bitte überprüfen Sie Hostname und Server Verfügbarkeit und versuchen Sie es erneut.</string>
+ <string name="auth_incorrect_path_title">ownCloud Installation nicht gefunden</string>
+ <string name="auth_incorrect_path_message">Die App konnte ownCloud unter dem gegebenen Pfad nicht finden. Bitte überprüfen Sie den Pfad und versuchen Sie es erneut.</string>
+ <string name="auth_secure_connection">Sichere Verbindung hergestellt</string>
+ <string name="auth_unknow_error">Unbekannter Fehler aufgetreten</string>
+ <string name="auth_login_details">Anmeldedetails</string>
+ <string name="crashlog_message">Die App ist abgestürzt. Möchten Sie einen Bericht senden?</string>
+ <string name="crashlog_send_report">Bericht senden</string>
+ <string name="crashlog_dont_send_report">Keinen Bericht senden</string>
+ <string name="extensions_avail_title">Erweiterung verfügbar!</string>
+ <string name="extensions_avail_message">Es scheint, dass ihre ownCloud Erweiterungen unterstützt. Wollen Sie verfügbare Erweiterungen für Android sehen?</string>
+ <string name="fd_keep_in_sync">Datei aktuell halten</string>
+ <string name="common_share">Teilen</string>
+ <string name="common_rename">Umbenennen</string>
+ <string name="common_remove">Löschen</string>
+ <string name="confirmation_alert_prefix">Wollen Sie wirklich</string>
+ <string name="confirmation_alert_suffix"></string>
+ <string name="remove_success_msg">Erfolgreich gelöscht</string>
+ <string name="remove_fail_msg">Löschen konnte nicht beendet werden</string>
+ <string name="filedisplay_unexpected_bad_get_content">Unerwartetes Problem; Bitte versuchen Sie die Datei in einer anderen App zu öffnen</string>
+</resources>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="hello">Hola Mundo, ¡PantallaPrincipalOwnCloud!</string>
+ <string name="app_name">ownCloud</string>
+ <string name="main_password">Contraseña:</string>
+ <string name="main_login">Nombre de usuario:</string>
+ <string name="main_button_login">Inicio de sesión</string>
+ <string name="main_welcome">Bienvenido a tu ownCloud</string>
+ <string name="main_files">Archivos</string>
+ <string name="main_music">Música</string>
+ <string name="main_contacts">Contactos</string>
+ <string name="main_calendar">Calendario</string>
+ <string name="main_bookmarks">Marcadores</string>
+ <string name="main_settings">Ajustes</string>
+ <string name="main_tit_accsetup">Configuración de cuenta</string>
+ <string name="main_wrn_accsetup">No hay cuentas de ownCloud en tu dispositivo. Para usar esta aplicación, necesitas crear una.</string>
+ <string name="actionbar_sync">Sincronización de cuenta</string>
+ <string name="actionbar_upload">Subir archivo</string>
+ <string name="actionbar_mkdir">Crear directorio</string>
+ <string name="actionbar_search">Buscar</string>
+ <string name="actionbar_settings">Ajustes</string>
+ <string name="prefs_category_general">General</string>
+ <string name="prefs_category_trackmydevice">Rastreo de dispositivo</string>
+ <string name="prefs_add_session">Agregar nueva sesión</string>
+ <string name="prefs_create_img_thumbnails">Crear miniaturas de imagen</string>
+ <string name="prefs_select_oc_account">Seleccionar una cuenta</string>
+ <string name="prefs_summary_select_oc_account">Elige, cuál de tus cuentas la aplicación debería usar.</string>
+ <string name="prefs_trackmydevice">Rastreo de dispositivo</string>
+ <string name="prefs_trackmydevice_summary_off">Habilitar ownCloud para rastrear la localización de tu dispositivo</string>
+ <string name="prefs_trackmydevice_summary_on">Tu ownCloud realiza un seguimiento de este dispositivo</string>
+ <string name="prefs_trackmydevice_interval">Intervalo de actualización</string>
+ <string name="prefs_trackmydevice_interval_summary">Actualizar cada %1$s minutos</string>
+ <string name="prefs_accounts">Cuentas</string>
+ <string name="auth_host_url">URL de ownCloud</string>
+ <string name="auth_username">Nombre de usuario</string>
+ <string name="auth_password">Contraseña</string>
+ <string name="new_session_uri_error">La URL dada es incorrecta</string>
+ <string name="new_session_session_name_error">Nombre de sesión incorrecto</string>
+ <string name="sync_string_files">Archivos</string>
+ <string name="uploader_no_file_selected">No se seleccionaron archivos para enviar</string>
+ <string name="setup_hint_username">Nombre de usuario</string>
+ <string name="setup_hint_password">Contraseña</string>
+ <string name="setup_hint_address">Dirección web</string>
+ <string name="setup_hint_show_password">¿Mostrar contraseña?</string>
+ <string name="setup_title">Conectar a tu ownCloud</string>
+ <string name="setup_btn_connect">Conectar</string>
+ <string name="uploader_btn_upload_text">Subir</string>
+ <string name="uploader_wrn_no_account_title">No se encontraron cuentas</string>
+ <string name="uploader_wrn_no_account_text">No hay cuentas de ownCloud en tu dispositivo. Por favor configura una cuenta primero.</string>
+ <string name="uploader_wrn_no_account_setup_btn_text">Configuración</string>
+ <string name="uploader_wrn_no_account_quit_btn_text">Salir</string>
+ <string name="uploader_info_uploading">Enviando</string>
+ <string name="uploader_btn_create_dir_text">Crear directorio para envío</string>
+ <string name="filedetails_select_file">Pulsa sobre un archivo para mostrar información adicional.</string>
+ <string name="filedetails_size">Tamaño:</string>
+ <string name="filedetails_type">Tipo:</string>
+ <string name="filedetails_created">Creado:</string>
+ <string name="filedetails_modified">Modificado:</string>
+ <string name="filedetails_download">Descargar</string>
+ <string name="filedetails_open">Abrir</string>
+ <string name="common_yes">Sí</string>
+ <string name="common_no">No</string>
+ <string name="common_ok">Aceptar</string>
+ <string name="common_cancel">Cancelar</string>
+ <string name="common_save_exit">Guardar & Salir</string>
+ <string name="common_error">Error</string>
+ <string name="uploader_info_dirname">Nombre de directorio</string>
+ <string name="uploader_upload_succeed">Enviado correctamente</string>
+ <string name="uploader_upload_failed">Falló el envío:</string>
+ <string name="uploader_files_uploaded_suffix"> archivos subidos</string>
+ <string name="downloader_download_failed">La descarga no pudo completarse</string>
+ <string name="common_choose_account">Elige una cuenta</string>
+ <string name="sync_string_contacts">Contactos</string>
+ <string name="use_ssl">Usar conexión segura</string>
+ <string name="location_no_provider">ownCloud no puede rastear tu dispositivo. Por favor chequea tu configuración de localización</string>
+ <string name="pincode_enter_pin_code">Por favor, inserta tu PIN de aplicación</string>
+ <string name="pincode_enter_new_pin_code">Por favor, inserta tu nuevo PIN de aplicación</string>
+ <string name="pincode_configure_your_pin">Ingrese PIN de aplicación ownCloud</string>
+ <string name="pincode_reenter_your_pincode">Reingrese PIN de aplicación ownCloud, por favor</string>
+ <string name="pincode_remove_your_pincode">Borrar tu PIN de aplicación ownCloud</string>
+ <string name="pincode_mismatch">Los PIN de aplicación ownCloud no son iguales</string>
+ <string name="pincode_wrong">PIN de aplicación ownCloud incorrecto</string>
+ <string name="pincode_removed">PIN de aplicación ownCloud borrado</string>
+ <string name="pincode_stored">PIN de aplicación ownCloud almacenado</string>
+ <string-array name="prefs_trackmydevice_intervall_keys">
+ <item>15 minutos</item>
+ <item>30 minutos</item>
+ <item>60 minutos</item>
+ </string-array>
+ <string-array name="prefs_trackmydevice_intervall_values">
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ </string-array>
+ <string name="auth_trying_to_login">Intentado iniciar sesión...</string>
+ <string name="auth_no_net_conn_title">Sin conexión de red</string>
+ <string name="auth_no_net_conn_message">No se ha detectado una conexión de red, chequea tu conexión a internet e intenta nuevamente.</string>
+ <string name="auth_connect_anyway">Conectar de todos modos</string>
+ <string name="auth_nossl_plain_ok_title">Conexión segura no disponible.</string>
+ <string name="auth_nossl_plain_ok_message">La aplicación no pudo establecer una conexión segura al servidor. Aunque no haya una conexión segura disponible, puedes continuar o cancelar.</string>
+ <string name="auth_connection_established">Conexión establecida</string>
+ <string name="auth_testing_connection">Probando conexión...</string>
+ <string name="auth_not_configured_title">Configuración de ownCloud en formato incorrecto</string>
+ <string name="auth_not_configured_message">Parece que tu instancia de ownCloud no está correctamente configurada. Contacta a tu administrador para más detalles.</string>
+ <string name="auth_unknown_error_title">Ocurrió un error desconocido</string>
+ <string name="auth_unknown_error_message">Ocurrió un error desconocido. Por favor, contacta a los autores e incluye los registros de tu dispositivo.</string>
+ <string name="auth_unknow_host_title">No se pudo establecer la conexión</string>
+ <string name="auth_unknow_host_message">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.</string>
+ <string name="auth_incorrect_path_title">Instancia de ownCloud no encontrada</string>
+ <string name="auth_incorrect_path_message">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.</string>
+ <string name="auth_secure_connection">Conexión segura establecida</string>
+ <string name="auth_unknow_error">¡Ocurrió un error desconocido!</string>
+ <string name="auth_login_details">Detalles de inicio de sesión</string>
+ <string name="crashlog_message">La aplicación finalizó inesperadamente. ¿Desea enviar un reporte de error?</string>
+ <string name="crashlog_send_report">Enviar reporte</string>
+ <string name="crashlog_dont_send_report">No enviar reporte</string>
+ <string name="extensions_avail_title">¡Extensiones disponibles!</string>
+ <string name="extensions_avail_message">Parece que su instancia de ownCloud soporta extensiones avanzadas. ¿Desea ver las extensiones disponibles para android?</string>
+ <string name="fd_keep_in_sync">Mantener el archivo actualizado</string>
+ <string name="common_share">Compartir</string>
+ <string name="common_rename">Renombrar</string>
+ <string name="common_remove">Borrar</string>
+ <string name="confirmation_alert_prefix">¿Está seguro que quiere</string>
+ <string name="confirmation_alert_suffix"></string>
+ <string name="remove_success_msg">Borrado correctamente</string>
+ <string name="remove_fail_msg">El borrado no pudo ser completado</string>
+ <string name="filedisplay_unexpected_bad_get_content">Problema inesperado; por favor, prueba otra app para seleccionar el archivo</string>
+</resources>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="app_name">ownCloud</string>
+ <string name="main_password">Password:</string>
+ <string name="main_welcome">Benvenuto nel tuo ownCloud</string>
+ <string name="main_files">File</string>
+ <string name="main_music">Musica</string>
+ <string name="main_contacts">Contatti</string>
+ <string name="main_calendar">Calendario</string>
+ <string name="main_bookmarks">Segnalibri</string>
+ <string name="main_settings">Impostazioni</string>
+ <string name="actionbar_upload">Carica un file</string>
+ <string name="actionbar_mkdir">Crea cartella</string>
+ <string name="actionbar_search">Cerca</string>
+ <string name="actionbar_settings">Impostazioni</string>
+ <string name="prefs_add_session">Aggiungi nuova sessione</string>
+ <string name="prefs_select_oc_account">Scegli un account</string>
+ <string name="prefs_summary_select_oc_account">Scegli, quale dei tuoi account la app deve usare</string>
+ <string name="prefs_trackmydevice">Localizzazione del dispositivo</string>
+ <string name="prefs_trackmydevice_interval">Intervallo di aggiornamento</string>
+ <string name="prefs_trackmydevice_interval_summary">Aggiorna ogni %1$s minuti</string>
+ <string name="auth_username">Nome utente</string>
+ <string name="auth_password">Password</string>
+ <string name="sync_string_files">File</string>
+ <string name="setup_hint_password">Password</string>
+ <string name="setup_hint_address">Indirizzo web</string>
+ <string name="setup_hint_show_password">Mostra password?</string>
+ <string name="setup_title">Connetti al tuo ownCloud</string>
+ <string name="setup_btn_connect">Connetti</string>
+ <string name="uploader_btn_upload_text">Carica</string>
+ <string name="uploader_wrn_no_account_text">Non ci sono account ownCloud sul tuo dispositivo. Per favore prima imposta un account.</string>
+ <string name="uploader_wrn_no_account_quit_btn_text">Esci</string>
+ <string name="filedetails_size">Dimensione:</string>
+ <string name="filedetails_type">Tipo:</string>
+ <string name="filedetails_created">Creato:</string>
+ <string name="filedetails_modified">Modificato:</string>
+ <string name="filedetails_open">Apri</string>
+ <string name="common_yes">Sì</string>
+ <string name="common_no">No</string>
+ <string name="common_ok">OK</string>
+ <string name="common_error">Errore</string>
+ <string name="uploader_info_dirname">Nome cartella</string>
+ <string name="common_choose_account">Scegli account</string>
+ <string name="sync_string_contacts">Contatti</string>
+ <string name="use_ssl">Usa connessione sicura</string>
+ <string-array name="prefs_trackmydevice_intervall_keys">
+ <item>15 Minuti</item>
+ <item>30 Minuti</item>
+ <item>60 Minuti</item>
+ </string-array>
+ <string-array name="prefs_trackmydevice_intervall_values">
+ <item>15</item>
+ <item>30</item>
+ <item>60</item>
+ </string-array>
+ <string name="auth_no_net_conn_title">Nessuna connessione a internet</string>
+ <string name="auth_no_net_conn_message">Non sono state rilevate connessioni alla rete, controlla la tua connessione Internet e prova ancora.</string>
+ <string name="auth_unknown_error_title">Errore sconosciuto</string>
+ <string name="auth_unknow_host_title">Impossibile stabilire una connessione</string>
+ <string name="auth_unknow_host_message">Impossibile stabilire una connessione</string>
+ <string name="auth_unknow_error">Errore sconosciuto!</string>
+ <string name="common_share">Condividi</string>
+ <string name="common_rename">Rinomina</string>
+ <string name="common_remove">Rimuovi</string>
+ <string name="confirmation_alert_prefix">Vuoi davvero </string>
+ <string name="confirmation_alert_suffix"></string>
+ <string name="remove_success_msg">Rimozione effettuata con successo</string>
+ <string name="remove_fail_msg">La rimozione non può essere completata</string>
+</resources>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources>
+ <string name="app_name">ownCloud</string>
+ <string name="main_password">Hasło:</string>
+ <string name="main_login">Nazwa użytkownika:</string>
+ <string name="main_button_login">Login</string>
+ <string name="main_welcome">Witaj w ownCloud</string>
+ <string name="main_settings">Ustawienia</string>
+ <string name="prefs_category_general">Ogólne</string>
+ <string name="auth_username">Nazwa użytkownika</string>
+ <string name="auth_password">Hasło</string>
+ <string name="setup_hint_username">Nazwa użytkownika</string>
+ <string name="setup_hint_password">Hasło</string>
+ <string name="setup_hint_show_password">Pokazać hasło?</string>
+ <string name="setup_btn_connect">Połącz</string>
+ <string name="uploader_wrn_no_account_quit_btn_text">Wyjdź</string>
+ <string name="filedetails_size">Rozmiar:</string>
+ <string name="filedetails_type">Typ:</string>
+ <string name="filedetails_created">Utworzony:</string>
+ <string name="filedetails_modified">Zmodyfikowany:</string>
+ <string name="filedetails_download">Pobierz</string>
+ <string name="filedetails_open">Otwórz</string>
+ <string name="common_yes">Tak</string>
+ <string name="common_no">Nie</string>
+ <string name="common_ok">OK</string>
+ <string name="common_cancel">Anuluj</string>
+ <string name="common_save_exit">Zapisz i wyjdź</string>
+ <string name="common_error">Błąd</string>
+ <string name="uploader_info_dirname">Nazwa katalogu</string>
+ <string name="common_choose_account">Wybierz konto</string>
+ <string name="pincode_wrong">ownCloud</string>
+ <string name="auth_trying_to_login">Logowanie...</string>
+ <string name="auth_no_net_conn_title">Brak połączenia sieciowego</string>
+ <string name="auth_connect_anyway">Połącz mimo wszystko</string>
+ <string name="auth_connection_established">Połączenie nawiązane</string>
+ <string name="auth_testing_connection">Testowanie połączenia...</string>
+ <string name="auth_unknow_host_title">Nie można nawiązać połączenia</string>
+ <string name="crashlog_send_report">Wyślij raport</string>
+ <string name="crashlog_dont_send_report">Nie wysyłaj</string>
+ <string name="common_rename">Zmień nazwę</string>
+ <string name="common_remove">Usuń</string>
+ <string name="confirmation_alert_prefix">Czy na pewno chcesz </string>
+</resources>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+<?xml version='1.0' encoding='UTF-8'?>
+<resources/>
--- /dev/null
+.tx
+*pyc
+*pyo
+*~
+*egg-info*
--- /dev/null
+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
--- /dev/null
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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.
+
--- /dev/null
+include tx
+
+# Docs
+include LICENSE README.rst
+recursive-include docs *
+
--- /dev/null
+
+=============================
+ 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/
+
--- /dev/null
+#!/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',),
+)
--- /dev/null
+# -*- 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)
--- /dev/null
+# -*- 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')
+
--- /dev/null
+#!/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:])
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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 '<lang>' 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 <project_slug>"\
+ ".<resource_slug> 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 <project_slug>"\
+ ".<resource_slug> 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 <project_slug>"\
+ ".<resource_slug> 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 <lang> 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()
--- /dev/null
+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)
--- /dev/null
+# -*- 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."""
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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))
--- /dev/null
+# -*- 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 <lang_code>\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 <path>"
+ 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 <file>\n\n"\
+ " To set a single translation file:\n $ tx set -r project.resource -l de <file>\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 <file>\n\n"\
+ " To set a remote release/resource/project:\n"\
+ " $ tx set --auto-remote <transifex-url>\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 []
--- /dev/null
+# -*- 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
--- /dev/null
+# -*- 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<sep>%(proj)s.%(res)s<sep><lang>.%(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("<sep>", 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('<lang>', 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)
--- /dev/null
+# 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/',
+}
+
+
--- /dev/null
+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 <lang> expression
+ """
+ # Force expr to be a valid regex expr (escaped) but keep <lang> intact
+ expr_re = re.escape(os.path.join(root_path, file_filter))
+ expr_re = expr_re.replace("\\<lang\\>", '<lang>').replace(
+ '<lang>', '([^%(sep)s]+)' % { 'sep': re.escape(os.path.sep)})
+
+ return "^%s$" % expr_re
+
+
+TX_URLS = {
+ 'resource': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\w|-)+)/resource/(?P<resource>(\w|-)+)/?$',
+ 'release': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\w|-)+)/r/(?P<release>(\w|-)+)/?$',
+ 'project': '(?P<hostname>https?://(\w|\.|:|-)+)/projects/p/(?P<project>(\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
--- /dev/null
+# -*- 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