Merge branch 'develop' into refactor_remote_operation_to_download_file

This commit is contained in:
masensio 2013-12-11 09:38:13 +01:00
commit 6daaf70be8
24 changed files with 21 additions and 3878 deletions

View file

@ -28,7 +28,7 @@
<string name="prefs_feedback">Rückmeldungen</string>
<string name="prefs_imprint">Impressum</string>
<string name="recommend_subject">Probiere %1$s auf Deinem Smartphone!</string>
<string name="recommend_text">Ich möchte Dich zum Benutzen von %1$s auf Deinem Smartphone einladen!\nLade es hier herunter: %2$s</string>
<string name="recommend_text">Ich möchte Dich zu %1$s für Dein Smartphone einladen!\nLade es hier herunter: %2$s</string>
<string name="auth_check_server">Überprüfe den Server</string>
<string name="auth_host_url">Adresse des Servers</string>
<string name="auth_username">Benutzername</string>
@ -94,7 +94,7 @@
<string name="sync_fail_in_favourites_content">Inhalte von %1$d konnte nicht synchronisiert werden (%2$d Konflikte)</string>
<string name="sync_foreign_files_forgotten_ticker">Einige lokale Dateien wurden vergessen</string>
<string name="sync_foreign_files_forgotten_content">%1$d Dateien aus dem Verzeichnis %2$s konnten nicht kopiert werden nach</string>
<string name="sync_foreign_files_forgotten_explanation">\"Mit Version 1.3.16 werden Dateien die von diesem Gerät aus hochgeladen werden in den lokalen Ordner %1$s kopiert um Datenverlust zu vermeiden, wenn eine einzelne Datei mit mehreren Accounts synchronisiert wird.\n\nInfolge dieser Änderung wurden alle Dateien, die mit vorherigen Versionen dieser App hochgeladen wurden, in den Ordner %2$s verschoben. Jedoch ist während der Account-Synchronisation ein Fehler aufgetreten, der das Abschließen dieses Vorgangs verhindert. Du kannst die Datei(en) entweder wie sie sind belassen und den Link zu %3$s entfernen oder die Datei(en) in den %1$s Ordner verschieben und den Link zu %4$s beibehalten.\n\nUnten befindet sich eine Liste der lokalen Datei(en) und der mit ihnen verbundenen Remote-Datei(en) in %5$s.</string>
<string name="sync_foreign_files_forgotten_explanation">\"Mit Version 1.3.16 werden Dateien die von diesem Gerät aus hochgeladen werden in den lokalen Ordner %1$s kopiert, um Datenverlust zu vermeiden, wenn eine einzelne Datei mit mehreren Accounts synchronisiert wird.\n\nInfolge dieser Änderung wurden alle Dateien, die mit vorherigen Versionen dieser App hochgeladen wurden, in den Ordner %2$s verschoben. Jedoch ist während der Account-Synchronisation ein Fehler aufgetreten, der das Abschließen dieses Vorgangs verhindert. Du kannst die Datei(en) entweder wie sie sind belassen und den Link zu %3$s entfernen, oder die Datei(en) in den %1$s Ordner verschieben, und den Link zu %4$s beibehalten.\n\nUnten befindet sich eine Liste der lokalen Datei(en) und der mit ihnen verbundenen Remote-Datei(en) in %5$s.</string>
<string name="sync_current_folder_was_removed">Das Verzeichnis %1$s existiert nicht mehr</string>
<string name="foreign_files_move">Verschiebe alle</string>
<string name="foreign_files_success">Alle Dateien wurden verschoben</string>
@ -152,9 +152,9 @@
<string name="auth_oauth_error">Autorisierung nicht erfolgreich</string>
<string name="auth_oauth_error_access_denied">Zugriff durch den Autorisierungsserver abgelehnt</string>
<string name="auth_wtf_reenter_URL">Unerwarteter Zustand; bitte gib die URL des Servers nochmals ein</string>
<string name="auth_expired_oauth_token_toast">Ihre Autorisierung ist abgelaufen. Bitte Autorisierung nochmals durchführen</string>
<string name="auth_expired_oauth_token_toast">Deine Autorisierung ist abgelaufen. Bitte Autorisierung nochmals durchführen</string>
<string name="auth_expired_basic_auth_toast">Bitte gib dein aktuelles Passwort ein</string>
<string name="auth_expired_saml_sso_token_toast">Ihre Sitzung ist abgelaufen. Bitte Anmeldung nochmals durchführen</string>
<string name="auth_expired_saml_sso_token_toast">Deine Sitzung ist abgelaufen. Bitte Anmeldung nochmals durchführen</string>
<string name="auth_connecting_auth_server">Verbinde mit dem Authentifizierung-Server…</string>
<string name="auth_unsupported_auth_method">Der Server unterstützt diese Authentifizierung-Methode nicht</string>
<string name="auth_unsupported_multiaccount">%1$s unterstützt nicht mehrere Benutzerkonten</string>

View file

@ -24,7 +24,10 @@
<string name="prefs_log_summary_history">Honek gordetako erregistroak bistaratzen ditu.</string>
<string name="prefs_log_delete_history_button">Ezabatu historia</string>
<string name="prefs_help">Laguntza</string>
<string name="prefs_recommend">Lagun bati aholkatu</string>
<string name="prefs_imprint">Imprint</string>
<string name="recommend_subject">Probatu %1$s zure telefono adimentsuan!</string>
<string name="recommend_text">Nik %1$s zure telefono adimentsuan erabitzera gonbidatu nahi zaitut!\nDeskargatu hemen: %2$s</string>
<string name="auth_check_server">Egiaztatu zerbitzaria</string>
<string name="auth_host_url">Zerbitzariaren helbidea</string>
<string name="auth_username">Erabiltzaile izena</string>
@ -90,6 +93,8 @@
<string name="sync_fail_in_favourites_content">%1$d fitxategien edukiak ezin dira sinkronizatu (%2$d gatazka)</string>
<string name="sync_foreign_files_forgotten_ticker">Bertako fitxategi batzuk ahaztu dira</string>
<string name="sync_foreign_files_forgotten_content">%2$s karpetako %1$d fitxategi ezin dira dira kopiatu</string>
<string name="sync_foreign_files_forgotten_explanation">1.3.16 bertsioan, gailu honetatik igotzen diren fitxategiak bertako %1$s karpetara mugitzen dira datu galera ekiditzeko fitxategi bat kontu ezberdinekin sinkronizatzen denean.\n\nAldaketa hau dela eta, programa honen aurreko bertsioetan igotako fitxategi guztiak %2$s karpetara kopiatu dira. Hala ere, errore batek hau burutzea ekidin du kontuaren sinkronizazioa egiten ari zen bitartean. Orain fitxategiak dauden bezala utz ditzakezu eta %3$s rako lotura ezabatu, edo fitxategiak %1$s karpetara mugi ditzakezu eta %4$srako lotura mantendu.\n\nBehean bertako fitxategien zerrenda eta %5$s era lotuta zeuden urruneko fitxategiena.</string>
<string name="sync_current_folder_was_removed">%1$s karpeta dagoeneko ez da existitzen</string>
<string name="foreign_files_move">Mugitu denak</string>
<string name="foreign_files_success">Fitxategi guztiak mugitu dira</string>
<string name="foreign_files_fail">Fitxategi batzuk ezin dira mugitu</string>
@ -115,6 +120,7 @@
<string name="media_err_unsupported">Onartzen ez de euskarri kodeka</string>
<string name="media_err_io">Euskarri fitxategia ezin da bihurtu</string>
<string name="media_err_malformed">Euskarri fitxategia ezin da kodetu</string>
<string name="media_err_timeout">Erreproduzitzen saiatzean denbora iraungitu da</string>
<string name="media_err_invalid_progressive_playback">Euskarri fitxategia ezin da jariotu</string>
<string name="media_err_unknown">Euskarri fitxategia ezin erreproduzitu stock euskarri erreproduzigailuarekin</string>
<string name="media_err_security_ex">Segurtasun errorea %1$s erreproduzitzen saiatzean</string>
@ -129,12 +135,15 @@
<string name="auth_connection_established">Konexioa ezarri da</string>
<string name="auth_testing_connection">Konexioa probatzen...</string>
<string name="auth_not_configured_title">gaizki egindako server konfigurazioa</string>
<string name="auth_account_not_new">Erabiltzaile eta zerbitzari hauendako dagoeneko kontu bat existitzen da gailu honetan</string>
<string name="auth_account_not_the_same">Sartutako erabiltzaileak ez du bat egiten kontu honetako erabiltzailearekin</string>
<string name="auth_unknown_error_title">Errore ezezagun bat gertatu da</string>
<string name="auth_unknown_host_title">Ezin izan da hostalaria aurkitu</string>
<string name="auth_incorrect_path_title">ez da serveren instalaziorik aurkitu</string>
<string name="auth_timeout_title">Zerbitzariak denbora asko hartu du erantzuteko</string>
<string name="auth_incorrect_address_title">Gaizki sortutako URLa</string>
<string name="auth_ssl_general_error_title">SSL abiaratzeak huts egin du</string>
<string name="auth_ssl_unverified_server_title">Ezin izan da SSL zerbitzariaren identitaea egiaztatu</string>
<string name="auth_bad_oc_version_title">server zerbitzari bertsio ezezaguna</string>
<string name="auth_wrong_connection_title">Ezin izan da konexioa egin</string>
<string name="auth_secure_connection">Konexio segurua ezarri da</string>
@ -142,7 +151,12 @@
<string name="auth_oauth_error">Baimena ez da lortu</string>
<string name="auth_oauth_error_access_denied">Sarrera autorizazio zerbitzariak ukatua</string>
<string name="auth_wtf_reenter_URL">Egoera esperogabea, mesedez idatzi berriz zerbitzari URLa</string>
<string name="auth_expired_oauth_token_toast">Zure baimena iraungitu da.\nMesedez, baimendu berriz</string>
<string name="auth_expired_basic_auth_toast">Mesedez, sartu oraingo pasahitza</string>
<string name="auth_expired_saml_sso_token_toast">Zure saioa iraungitu da. Mesdez konektatu berriro</string>
<string name="auth_connecting_auth_server">Konektatzen autentikazio zerbitzarira...</string>
<string name="auth_unsupported_auth_method">Zerbitzariak ez du autentikazio metodo hau onartzen</string>
<string name="auth_unsupported_multiaccount">%1$s ez du kontu anitzak onartzen</string>
<string name="fd_keep_in_sync">Mantendu fitxategia eguneratuta</string>
<string name="common_rename">Berrizendatu</string>
<string name="common_remove">Ezabatu</string>
@ -160,9 +174,11 @@
<string name="sync_file_fail_msg">Urruneko fitxategia ezin izan da arakatu</string>
<string name="sync_file_nothing_to_do_msg">Fitxategi edukiak dagoeneko sinkronizaturik</string>
<string name="create_dir_fail_msg">Karpeta ezin da sortu</string>
<string name="filename_forbidden_characters">Debekatutako karaktereak: / \\ &lt; &gt; : \" | ? *</string>
<string name="wait_a_moment">Itxaron momentu bat</string>
<string name="filedisplay_unexpected_bad_get_content">Ezusteko arazoa; mesedez, saiatu beste app batekin fitxategia hautatzeko</string>
<string name="filedisplay_no_file_selected">Ez da fitxategirik hautatu</string>
<string name="oauth_check_onoff">Saioa hasi oAuth2-rekin</string>
<string name="oauth_login_connection">Konektatzen oAuth2 zerbitzarira...</string>
<string name="ssl_validator_header">Lekuaren identitatea ezin da egiaztatu</string>
<string name="ssl_validator_reason_cert_not_trusted">- Zerbitzariaren ziurtagiria ez da fidagarria</string>
@ -202,6 +218,7 @@
<string name="preview_image_description">Irudi aurreikuspena</string>
<string name="preview_image_error_unknown_format">Ezin da irudi hau erakutsi</string>
<string name="error__upload__local_file_not_copied">%1$s ezin da %2$s bertako karpetara kopiatu</string>
<string name="actionbar_failed_instant_upload">UnekoIgoerak huts egin du</string>
<string name="failed_upload_headline_text">Uneko igoerek huts egin dute</string>
<string name="failed_upload_headline_hint">Huts egindako igoeren laburpena</string>
<string name="failed_upload_all_cb">Hautatu dena</string>

View file

@ -1,5 +0,0 @@
.tx
*pyc
*pyo
*~
*egg-info*

View file

@ -1,21 +0,0 @@
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

View file

@ -1,343 +0,0 @@
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
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.

View file

@ -1,6 +0,0 @@
include tx
# Docs
include LICENSE README.rst
recursive-include docs *

View file

@ -1,30 +0,0 @@
=============================
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/

View file

@ -1,56 +0,0 @@
#!/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',),
)

View file

@ -1,65 +0,0 @@
# -*- 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)

View file

@ -1,531 +0,0 @@
# -*- 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')

View file

@ -1,109 +0,0 @@
#!/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 were running.
main(sys.argv[1:])

View file

@ -1,19 +0,0 @@
# -*- 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

View file

@ -1,576 +0,0 @@
# -*- 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()

View file

@ -1,115 +0,0 @@
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)

View file

@ -1,13 +0,0 @@
# -*- 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."""

View file

@ -1,46 +0,0 @@
# -*- 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

View file

@ -1,37 +0,0 @@
# -*- 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))

View file

@ -1,241 +0,0 @@
# -*- 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 []

View file

@ -1,54 +0,0 @@
# -*- 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

File diff suppressed because it is too large Load diff

View file

@ -1,21 +0,0 @@
# 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/',
}

View file

@ -1,259 +0,0 @@
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

View file

@ -1,94 +0,0 @@
# -*- 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