diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..48ad8303 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2008-2010 Marco Bonetti + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation + files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, + copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following + conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..d2777813 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include MANIFEST.in +include LICENSE +recursive-include rosetta/locale * +recursive-include rosetta/templates * diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..e69de29b diff --git a/rosetta/__init__.py b/rosetta/__init__.py new file mode 100644 index 00000000..faa4a9a9 --- /dev/null +++ b/rosetta/__init__.py @@ -0,0 +1,12 @@ +VERSION = (0, 6, 1) + +def get_version(svn=False, limit=3): + "Returns the version as a human-format string." + v = '.'.join([str(i) for i in VERSION[:limit]]) + if svn and limit >= 3: + from django.utils.version import get_svn_revision + import os + svn_rev = get_svn_revision(os.path.dirname(__file__)) + if svn_rev: + v = '%s.%s' % (v, svn_rev) + return v diff --git a/rosetta/conf/__init__.py b/rosetta/conf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rosetta/conf/settings.py b/rosetta/conf/settings.py new file mode 100644 index 00000000..101ea5d9 --- /dev/null +++ b/rosetta/conf/settings.py @@ -0,0 +1,46 @@ +from django.conf import settings + +# Number of messages to display per page. +MESSAGES_PER_PAGE = getattr(settings,'ROSETTA_MESSAGES_PER_PAGE',10) + + +# Enable Google translation suggestions +ENABLE_TRANSLATION_SUGGESTIONS = getattr(settings,'ROSETTA_ENABLE_TRANSLATION_SUGGESTIONS',True) + +# Displays this language beside the original MSGID in the admin +MAIN_LANGUAGE = getattr(settings,'ROSETTA_MAIN_LANGUAGE', None) + +# Change these if the source language in your PO files isn't English +MESSAGES_SOURCE_LANGUAGE_CODE = getattr(settings,'ROSETTA_MESSAGES_SOURCE_LANGUAGE_CODE', 'en') +MESSAGES_SOURCE_LANGUAGE_NAME = getattr(settings,'ROSETTA_MESSAGES_SOURCE_LANGUAGE_NAME', 'English') + + +""" +When running WSGI daemon mode, using mod_wsgi 2.0c5 or later, this setting +controls whether the contents of the gettext catalog files should be +automatically reloaded by the WSGI processes each time they are modified. + +Notes: + + * The WSGI daemon process must have write permissions on the WSGI script file + (as defined by the WSGIScriptAlias directive.) + * WSGIScriptReloading must be set to On (it is by default) + * For performance reasons, this setting should be disabled in production environments + * When a common rosetta installation is shared among different Django projects, + each one running in its own distinct WSGI virtual host, you can activate + auto-reloading in individual projects by enabling this setting in the project's + own configuration file, i.e. in the project's settings.py + +Refs: + + * http://code.google.com/p/modwsgi/wiki/ReloadingSourceCode + * http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIReloadMechanism + +""" +WSGI_AUTO_RELOAD = getattr(settings,'ROSETTA_WSGI_AUTO_RELOAD', False) +UWSGI_AUTO_RELOAD = getattr(settings,'ROSETTA_UWSGI_AUTO_RELOAD', False) + + +# Exclude applications defined in this list from being translated +EXCLUDED_APPLICATIONS = getattr(settings,'ROSETTA_EXCLUDED_APPLICATIONS', ()) + diff --git a/rosetta/locale/cs/LC_MESSAGES/django.mo b/rosetta/locale/cs/LC_MESSAGES/django.mo new file mode 100644 index 00000000..ebb19e6e Binary files /dev/null and b/rosetta/locale/cs/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/cs/LC_MESSAGES/django.po b/rosetta/locale/cs/LC_MESSAGES/django.po new file mode 100644 index 00000000..f4686c55 --- /dev/null +++ b/rosetta/locale/cs/LC_MESSAGES/django.po @@ -0,0 +1,175 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-09-12 12:16\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Výběr jazyka" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Domů" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "Aplikace" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Hotovo" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Textů" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Přeloženo" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Zastaralé" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Soubor" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Nic k překladu!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"V souboru settings nejsou specifikovány žádné jazyky nebo ještě nebyl " +"spuštěn proces překladu katalogů." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Podívejte se prosím do Django dokumentace " +"I18N , kde naleznete podklady pro nastavení Vašeho vícejazyčného projektu." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Vybrat jiný soubor" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Stáhnout tento katalog" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Hotovo: %(percent_translated)s %" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "Soubor je pouze pro čtení: pro dokončení úprav si soubor stáhněte!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "Některé z Vámi přeložených textových položek nebyly uloženy: důvodem je" +", že Váš kolega Vám provedl pod rukami nějaké úpravy :-)." + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Překlad do %(rosetta_i18n_lang_name)s " + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Zobrazit:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Pouze nepřeložené" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Pouze přeložené" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Pouze Fuzzy" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "Vše" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Hledat" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Hledat" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Výskyt(y)" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "navrhnout" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s více" +msgstr[1] "%(more_count)s více" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Uložit a přeložit další část" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Přejít na stránku:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Zobrazeno:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s text" +msgstr[1] "%(hits)s/%(message_number)s textů" + +#~ msgid "English" +#~ msgstr "Anglicky" + +#~ msgid "Both" +#~ msgstr "Oba" diff --git a/rosetta/locale/de/LC_MESSAGES/django.mo b/rosetta/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 00000000..d7416120 Binary files /dev/null and b/rosetta/locale/de/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/de/LC_MESSAGES/django.po b/rosetta/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..32a7a1a1 --- /dev/null +++ b/rosetta/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,179 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-06-16 10:11+0100\n" +"Last-Translator: Martin Mahner \n" +"Language-Team: patrick lauber \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: SWITZERLAND\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Sprachwahl" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Home" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Fortschritt" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Texte" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Übersetzt" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "Unscharf" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Veraltet" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Datei" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Nichts zu übersetzen!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Sie haben keine Sprachen in der Einstellungsdatei definiert oder bis jetzt " +"noch keine Übersetzungskataloge generiert." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Unter Django's I18N documentation finden " +"Sie eine Anleitung, wie sie Internationalisierung in Ihrem Projekt " +"einrichten." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Wähle eine andere Datei" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Laden sie diesen Katalog herunter" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Fortschritt: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"Datei ist schreibgeschützt: Laden Sie die Datei herunter wenn sie mit dem " +"Editieren fertig sind!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Übersetze in %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Anzeige:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Nur unübersetzte" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Nur übersetzte" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Nur unscharfe" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "Alle" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Suchen" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Los" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "Original" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Gefunden in" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "vorschlagen" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s mehr" +msgstr[1] "%(more_count)s mehr" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Speichern und nächsten Block übersetzen" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Zur Seite:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Anzeigen:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s Text" +msgstr[1] "%(hits)s/%(message_number)s Texte" + +#~ msgid "English" +#~ msgstr "Englisch" + +#~ msgid "Both" +#~ msgstr "Beide" diff --git a/rosetta/locale/es/LC_MESSAGES/django.mo b/rosetta/locale/es/LC_MESSAGES/django.mo new file mode 100644 index 00000000..28b4091d Binary files /dev/null and b/rosetta/locale/es/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/es/LC_MESSAGES/django.po b/rosetta/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..bd1fdeb7 --- /dev/null +++ b/rosetta/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,176 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-11-30 12:12\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Selección de idioma" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Inicio" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Progreso" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Mensajes" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Traducido" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "Revisar" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Obsoleto" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Archivo" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "¡Nada que traducir!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"No has especificado ningún idioma en tu archivo de settings o no has " +"generado todavía un batch para traducción de catálogos." + +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Por favor visita la Documentación de Django " +"sobre I18N para obtener una guía sobre cómo añadir internacionalización " +"a tu proyecto." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Selecciona otro archivo" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Descarga este catálogo" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Progreso: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"El archivo está en modo lectura: ¡Descarga el archivo cuando termines de " +"editarlo!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Traducir al %(rosetta_i18n_lang_name)s " + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Pantalla:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Sin traducir sólo" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Traducidos sólo" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Vellosos sólo" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Búsqueda" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Ir" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Ocurrencia(s)" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "sugerir" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s más" +msgstr[1] "%(more_count)s más" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Guardar y traducir siguiente bloque" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Ir a la página:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Mostrando:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s mensaje" +msgstr[1] "%(hits)s/%(message_number)s mensajes" + +#~ msgid "English" +#~ msgstr "Inglés" + +#~ msgid "Both" +#~ msgstr "Ambos" diff --git a/rosetta/locale/fr/LC_MESSAGES/django.mo b/rosetta/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..b4a11a5a Binary files /dev/null and b/rosetta/locale/fr/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/fr/LC_MESSAGES/django.po b/rosetta/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..1728efa8 --- /dev/null +++ b/rosetta/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,179 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-09-22 11:02\n" +"Last-Translator: Admin Admin \n" +"Language-Team: French \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Sélection de la langue" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Accueil" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "Application" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Progression" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Messages" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Traduits" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "Flous" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Obsolètes" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Fichier" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Rien à Traduire!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Vous n'avez spécifié aucune langue dans votre fichier de configuration, ou " +"n'avez pas encore généré le catalogue initial de traductions pour votre " +"projet." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Veuillez vous référer à la documentation sur " +"l'internationalisation de Django pour un guide sur comment activer et " +"configurer la traduction de votre projet." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Choisir un autre fichier" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Télécharger ce catalogue" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Progression: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"Fichier en seule lecture: télécharger le fichier à la fin de l'édition!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "Certains parmi vos derniers éléments n'ont pas pu être sauvés: ceci " +"arrive généralement quand le catalogue de traductions à été modifié après qu'il ait " +"été chargé." + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Traduire en %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Afficher:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Non-traduits uniquement" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Traduits uniquement" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Flous uniquement" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "Tous" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Recherche" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Go" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "Original" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Occurrences(s)" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "suggérer" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s de plus" +msgstr[1] "%(more_count)s de plus" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Sauver et traduire le prochain bloc" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Passer à la page:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Affichés:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s message" +msgstr[1] " %(hits)s/%(message_number)s messages" + +#~ msgid "English" +#~ msgstr "Anglais" + +#~ msgid "Both" +#~ msgstr "Les deux" diff --git a/rosetta/locale/hu/LC_MESSAGES/django.mo b/rosetta/locale/hu/LC_MESSAGES/django.mo new file mode 100644 index 00000000..d7df015f Binary files /dev/null and b/rosetta/locale/hu/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/hu/LC_MESSAGES/django.po b/rosetta/locale/hu/LC_MESSAGES/django.po new file mode 100644 index 00000000..ddc1964a --- /dev/null +++ b/rosetta/locale/hu/LC_MESSAGES/django.po @@ -0,0 +1,170 @@ +# Copyright (C) 2008 +# Gergely Kontra , 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-10-09 19:08+0100\n" +"Last-Translator: Gergely Kontra \n" +"Language-Team: nomail \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Nyelvválasztás" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Főoldal" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Állapot" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Üzenet" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Lefordítva" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Elavult" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Fájl" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Nincs mit fordítani!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Nincs beállítva egyetlen nyelv sem van nem hozott létre egy fordítási " +"katalóguskoteget." + +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Projekted többnyelvűvé alakításához lásd a Django I18N dokumentációt!" + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Másik fájl választása" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Katalógus letöltése" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "%(percent_translated)s% kész" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "A fájl írásvédett: töltse le a katalógust, ha végzett!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Fordítsa %(rosetta_i18n_lang_name)s nyelvre" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Szűrés:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Fordításra vár" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Lefordított" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Keresés" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Keress" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Előfordulások" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "ajánl" + +#: templates/rosetta/pofile.html:93 +#, fuzzy, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "még %(more_count)s" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Mentés" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Ugorj erre az oldalra:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Szűrés:" + +#: templates/rosetta/pofile.html:124 +#, fuzzy, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s üzenet" + +#~ msgid "English" +#~ msgstr "Angol" + +#~ msgid "Both" +#~ msgstr "Minden" diff --git a/rosetta/locale/it/LC_MESSAGES/django.mo b/rosetta/locale/it/LC_MESSAGES/django.mo new file mode 100644 index 00000000..b731208d Binary files /dev/null and b/rosetta/locale/it/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/it/LC_MESSAGES/django.po b/rosetta/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..61dc9087 --- /dev/null +++ b/rosetta/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,174 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"Last-Translator: Marco Bonetti \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Selezione della lingua" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Inizio" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "Applicazione" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Progressione" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Messaggio" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Tradotto" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Obsoleto" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "File" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Nulla da tradurre!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Non avete specificato nessuna lingua nelle vostre impostazioni, o non avete " +"ancore generato un catalogo iniziale da tradurre." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Vogliate riferirvi alla documentazione " +"sull'internazionalizzazione di Django per una giuda su come impostare le " +"traduzioni per il vostro progetto." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Scegliere un altro file" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Scaricare il catalogo" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Progressione: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "file in solo lettura: scaricare il file alla fine dell'edizione!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "Alcuni elementi non sono stati salvati: questo capita solitamente quando " +"il catalogo delle traduzioni è stato modificato dopo che voi l'abbiate caricato." + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Tradurre verso il %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Mostra:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Solo non tradotti" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Solo tradotti" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Solo fuzzy" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "Tutti" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Cercare" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Via" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "Originale" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Occorrenze" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "suggerire" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "un altro" +msgstr[1] "ancora %(more_count)s" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Salvare e tradurre il prossimo blocco" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Passare alla pagina:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Mostrato:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s messaggio" +msgstr[1] "%(hits)s/%(message_number)s messaggi" + +#~ msgid "English" +#~ msgstr "Inglese" + +#~ msgid "Both" +#~ msgstr "Entrambi" diff --git a/rosetta/locale/nl/LC_MESSAGES/django.mo b/rosetta/locale/nl/LC_MESSAGES/django.mo new file mode 100644 index 00000000..6f143373 Binary files /dev/null and b/rosetta/locale/nl/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/nl/LC_MESSAGES/django.po b/rosetta/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 00000000..110aff31 --- /dev/null +++ b/rosetta/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,176 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2008 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the rosetta package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-06-05 17:06+0200\n" +"Last-Translator: Joost Cassee \n" +"Language-Team: Joost Cassee \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Taalselectie" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Voorpagina" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Voortgang" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Berichten" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Vertaald" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Niet meer gebruikt" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Bestand" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Niets te vertalen!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"U heeft geen talen gespecificeerd in het bestand settings.py of nog geen " +"catalogus gegenereerd." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Zie Django's I18N documentatie (Engels) " +"voor meer informatie over internationalisatie." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Selecteer een ander bestand" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Download deze catalogus" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Voortgang: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"Dit is bestand is alleen-lezen; download dit bestand wanneer u klaar bent " +"met bewerken!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Vertalen in het %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Beeld:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Alleen onvertaald" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Alleen vertaald" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Zoeken" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Ga" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Gevonden in" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "" +msgstr[1] "" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Opslaan en volgende blok vertalen" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Snel naar pagina:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Tonen:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s bericht" +msgstr[1] "%(hits)s/%(message_number)s berichten" + +#~ msgid "English" +#~ msgstr "Engels" + +#~ msgid "Both" +#~ msgstr "Beide" diff --git a/rosetta/locale/pl/LC_MESSAGES/django.mo b/rosetta/locale/pl/LC_MESSAGES/django.mo new file mode 100644 index 00000000..e6ea59a9 Binary files /dev/null and b/rosetta/locale/pl/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/pl/LC_MESSAGES/django.po b/rosetta/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..1a7c4d2f --- /dev/null +++ b/rosetta/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,175 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"PO-Revision-Date: 2008-09-12 12:16\n" +"Last-Translator: \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Wybór języka" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Początek" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Postęp" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Komunikaty" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Przetłumaczone" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Przestarzałe" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Plik" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Nie ma nic do przetłumaczenia!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Nie podałeś żadnych języków w plikach ustawień, lub nie wygenerowałeś " +"jeszcze katalogów do tłumaczeń." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Sprawdź pomoc dotyczącą internacjonalizacji " +"Django I18N , aby znaleźć dokumentację na temat internacjonalizacji " +"twojego projektu." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Wybierz inny plik" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Pobierz ten katalog" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Postęp: %(percent_translated)s %" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "Plik jest tylko do odczytu: pobierz go kiedy skończysz edytować" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Przetłumacz na %(rosetta_i18n_lang_name)s " + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Wyświetl:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Tylko nieprzetłumaczone" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Tylko przetłumaczone" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Szukaj" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Idź" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Wystąpienie(a)" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "sugeruj" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s więcej" +msgstr[1] "%(more_count)s więcej" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Zapisz i tłumacz następny blok" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Przejdź do strony:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Wyświetlanie:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s komunikat" +msgstr[1] "%(hits)s/%(message_number)s komunikatów" + +#~ msgid "English" +#~ msgstr "Angielski" + +#~ msgid "Both" +#~ msgstr "Oba" diff --git a/rosetta/locale/ru/LC_MESSAGES/django.mo b/rosetta/locale/ru/LC_MESSAGES/django.mo new file mode 100644 index 00000000..45627281 Binary files /dev/null and b/rosetta/locale/ru/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/ru/LC_MESSAGES/django.po b/rosetta/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..53c3311c --- /dev/null +++ b/rosetta/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,168 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"Last-Translator: Nazar Leush \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Выбор языка" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Начало" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Обработано" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Сообщений" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "С переводом" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Устаревших" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Файл" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" + +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Выбрать другой файл" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Скачать этот каталог" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Обработано: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"Файл только для чтения: загрузите файл после завершения редактирования!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Перевод на %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Отображать:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Только без перевода" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Только с переводом" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Поиск" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Вперёд" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Встречается в" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "" +msgstr[1] "" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Сохранить и перевести следующий блок" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Перейти к странице:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Отображение:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s сообщение" +msgstr[1] "%(hits)s/%(message_number)s сообщений" + +#~ msgid "English" +#~ msgstr "Английский" + +#~ msgid "Both" +#~ msgstr "Все" diff --git a/rosetta/locale/tr/LC_MESSAGES/django.mo b/rosetta/locale/tr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..7bed8c0a Binary files /dev/null and b/rosetta/locale/tr/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/tr/LC_MESSAGES/django.po b/rosetta/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 00000000..8204401e --- /dev/null +++ b/rosetta/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,184 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-11-11 15:45+0200\n" +"PO-Revision-Date: 2008-06-16 10:11+0100\n" +"Last-Translator: RECEP KIRMIZI \n" +"Language-Team: \n" +"Language: Turkish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: \n" +"X-Poedit-Country: \n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Dil seçimi" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Ana Sayfa" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "Uygulama" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "İlerleme" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Mesaj" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Çevrildi" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "Belirsiz" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Kullanımda olmayan" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Dosya" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "Çevrilecek birşey yok!" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" +"Settings dosyanızda hiçbir dil belirtmediniz, ya da toplu olarak çeviri " +"kataloglarını oluşturmadınız." + +# python-format +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" +"Projenizdeki çoklu dilde geliştirebilmek için lütfen Django'nun Django'nun I18N dökümantasyonuna başvurun." + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Başka bir dosya alın" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Bu kataloğu bilgisayarınıza indirin" + +#: templates/rosetta/pofile.html:21 +#, fuzzy, python-format +msgid "Progress: %(percent_translated)s%%" +msgstr "İlerleme: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "" +"Dosya salt-okunur: İşlemleriniz bittikten sonra bilgisayarınıza indirin" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" +"Çeviri bloğunuzdaki bazı öğeler kayıt edilemedi: bu genellikle üzerinde " +"çalıştığınız dosyanın sunucu tarafında değişmesi sonucundaortaya çıkar." + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Şuna çevir %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Göster:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Sadece çevirilmemiş" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Sadece çevirilmiş" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "Sadece belirsiz" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "Hepsi" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Arama" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Git" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "Orijinal" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "öneri" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "%(more_count)s daha" +msgstr[1] "%(more_count)s daha" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Kaydet ve sonraki bloğu çevir" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Sayfaya geç:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Gösteriliyor:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s mesaj" +msgstr[1] "%(hits)s/%(message_number)s mesajlar" + +#~ msgid "Oluşum(lar)" +#~ msgstr "Gefunden in" + +#~ msgid "English" +#~ msgstr "İngilizce" + +#~ msgid "Both" +#~ msgstr "İkiside" diff --git a/rosetta/locale/uk/LC_MESSAGES/django.mo b/rosetta/locale/uk/LC_MESSAGES/django.mo new file mode 100644 index 00000000..58627ed6 Binary files /dev/null and b/rosetta/locale/uk/LC_MESSAGES/django.mo differ diff --git a/rosetta/locale/uk/LC_MESSAGES/django.po b/rosetta/locale/uk/LC_MESSAGES/django.po new file mode 100644 index 00000000..f3864d40 --- /dev/null +++ b/rosetta/locale/uk/LC_MESSAGES/django.po @@ -0,0 +1,167 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-07-04 13:42+0200\n" +"Last-Translator: Nazar Leush \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: templates/rosetta/languages.html:4 templates/rosetta/languages.html.py:6 +msgid "Language selection" +msgstr "Вибір мови" + +#: templates/rosetta/languages.html:6 templates/rosetta/pofile.html:18 +msgid "Home" +msgstr "Домівка" + +#: templates/rosetta/languages.html:17 +msgid "Application" +msgstr "" + +#: templates/rosetta/languages.html:18 +msgid "Progress" +msgstr "Завершено" + +#: templates/rosetta/languages.html:19 +msgid "Messages" +msgstr "Повідомлень" + +#: templates/rosetta/languages.html:20 +msgid "Translated" +msgstr "Перекладено" + +#: templates/rosetta/languages.html:21 templates/rosetta/pofile.html:57 +msgid "Fuzzy" +msgstr "" + +#: templates/rosetta/languages.html:22 +msgid "Obsolete" +msgstr "Застарілих" + +#: templates/rosetta/languages.html:23 +msgid "File" +msgstr "Файл" + +#: templates/rosetta/languages.html:44 +msgid "Nothing to translate!" +msgstr "" + +#: templates/rosetta/languages.html:45 +msgid "" +"You haven't specified any languages in your settings file, or haven't yet " +"generated a batch of translation catalogs." +msgstr "" + +#: templates/rosetta/languages.html:46 +#, python-format +msgid "" +"Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project." +msgstr "" + +#: templates/rosetta/pofile.html:8 +msgid "Pick another file" +msgstr "Вибрати інший файл" + +#: templates/rosetta/pofile.html:9 +msgid "Download this catalog" +msgstr "Завантажити цей каталог" + +#: templates/rosetta/pofile.html:21 +msgid "Progress: %(percent_translated)s%" +msgstr "Завершено: %(percent_translated)s%" + +#: templates/rosetta/pofile.html:23 +msgid "File is read-only: download the file when done editing!" +msgstr "Файл тільки для читання: завантажте файл після завершення редагування!" + +#: templates/rosetta/pofile.html:24 +msgid "" +"Some items in your last translation block couldn't be saved: this usually " +"happens when the catalog file changes on disk after you last loaded it." +msgstr "" + +#: templates/rosetta/pofile.html:28 +#, python-format +msgid "Translate into %(rosetta_i18n_lang_name)s" +msgstr "Переклад на %(rosetta_i18n_lang_name)s" + +#: templates/rosetta/pofile.html:31 +msgid "Display:" +msgstr "Відображати:" + +#: templates/rosetta/pofile.html:32 +msgid "Untranslated only" +msgstr "Тільки неперекладені" + +#: templates/rosetta/pofile.html:33 +msgid "Translated only" +msgstr "Тільки перекладені" + +#: templates/rosetta/pofile.html:34 +msgid "Fuzzy only" +msgstr "" + +#: templates/rosetta/pofile.html:35 +msgid "All" +msgstr "" + +#: templates/rosetta/pofile.html:42 +msgid "Search" +msgstr "Пошук" + +#: templates/rosetta/pofile.html:44 +msgid "Go" +msgstr "Вперед" + +#: templates/rosetta/pofile.html:54 +msgid "Original" +msgstr "" + +#: templates/rosetta/pofile.html:58 +msgid "Occurrences(s)" +msgstr "Зустрічається в" + +#: templates/rosetta/pofile.html:82 +msgid "suggest" +msgstr "" + +#: templates/rosetta/pofile.html:93 +#, python-format +msgid "%(more_count)s more" +msgid_plural "%(more_count)s more" +msgstr[0] "" +msgstr[1] "" + +#: templates/rosetta/pofile.html:105 +msgid "Save and translate next block" +msgstr "Записати і перекласти наступний блок" + +#: templates/rosetta/pofile.html:109 +msgid "Skip to page:" +msgstr "Перейти до сторінки:" + +#: templates/rosetta/pofile.html:122 +msgid "Displaying:" +msgstr "Відображення:" + +#: templates/rosetta/pofile.html:124 +#, python-format +msgid "%(hits)s/%(message_number)s message" +msgid_plural "%(hits)s/%(message_number)s messages" +msgstr[0] "%(hits)s/%(message_number)s повідомлення" +msgstr[1] "%(hits)s/%(message_number)s повідомлень" + +#~ msgid "English" +#~ msgstr "Англійська" + +#~ msgid "Both" +#~ msgstr "Всі" diff --git a/rosetta/locale/xx/LC_MESSAGES/django.po b/rosetta/locale/xx/LC_MESSAGES/django.po new file mode 100644 index 00000000..3ae3af87 --- /dev/null +++ b/rosetta/locale/xx/LC_MESSAGES/django.po @@ -0,0 +1,37 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-10-21 12:21+0200\n" +"PO-Revision-Date: 2008-09-22 11:02\n" +"Last-Translator: Admin Admin \n" +"Language-Team: French \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + + +msgid "String 1" +msgstr "" + +msgid "String 2" +msgstr "" + +#. Translators: This is a text of the base template +#: templates/base.html:43 +msgid "String 3 with comment" +msgstr "" + +msgctxt "Context hint" +msgid "String 4" +msgstr "" + + + + diff --git a/rosetta/models.py b/rosetta/models.py new file mode 100644 index 00000000..ca200a05 --- /dev/null +++ b/rosetta/models.py @@ -0,0 +1,2 @@ +from django.db import models +# Create your models here. diff --git a/rosetta/polib.py b/rosetta/polib.py new file mode 100644 index 00000000..0003030b --- /dev/null +++ b/rosetta/polib.py @@ -0,0 +1,1548 @@ +# -*- coding: utf-8 -*- +# +# License: MIT (see LICENSE file provided) +# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: + +""" +**polib** allows you to manipulate, create, modify gettext files (pot, po and +mo files). You can load existing files, iterate through it's entries, add, +modify entries, comments or metadata, etc. or create new po files from scratch. + +**polib** provides a simple and pythonic API via the :func:`~polib.pofile` and +:func:`~polib.mofile` convenience functions. +""" + +__author__ = 'David Jean Louis ' +__version__ = '0.6.2' +__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry', + 'detect_encoding', 'escape', 'unescape', 'detect_encoding',] + +import array +import codecs +import os +import re +import struct +import sys +import textwrap +import types + + +# the default encoding to use when autodetect_encoding is disabled +default_encoding = 'utf-8' + +# _pofile_or_mofile {{{ + +def _pofile_or_mofile(f, type, **kwargs): + """ + Internal function used by :func:`polib.pofile` and :func:`polib.mofile` to + honor the DRY concept. + """ + # get the file encoding + if kwargs.get('autodetect_encoding', True): + enc = detect_encoding(f, type == 'mofile') + else: + enc = kwargs.get('encoding', default_encoding) + + # parse the file + kls = type == 'pofile' and _POFileParser or _MOFileParser + parser = kls( + f, + encoding=enc, + check_for_duplicates=kwargs.get('check_for_duplicates', False) + ) + instance = parser.parse() + instance.wrapwidth = kwargs.get('wrapwidth', 78) + return instance + +# }}} +# function pofile() {{{ + +def pofile(pofile, **kwargs): + """ + Convenience function that parses the po or pot file ``pofile`` and returns + a :class:`~polib.POFile` instance. + + Arguments: + + ``pofile`` + string, full or relative path to the po/pot file or its content (data). + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was passed + to xgettext (optional, default: ``78``). + + ``autodetect_encoding`` + boolean, if set to ``False`` the function will not try to detect the + po file encoding and will use either the value of the ``encoding`` + argument or the ``default_encoding`` (optional, default: ``True``). + + ``encoding`` + string, the encoding to use (e.g. "utf-8"), only relevant if + ``autodetect_encoding`` is set to ``False``. + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + return _pofile_or_mofile(pofile, 'pofile', **kwargs) + +# }}} +# function mofile() {{{ + +def mofile(mofile, **kwargs): + """ + Convenience function that parses the mo file ``mofile`` and returns a + :class:`~polib.MOFile` instance. + + Arguments: + + ``mofile`` + string, full or relative path to the mo file or its content (data). + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was passed + to xgettext to generate the po file that was used to format the mo file + (optional, default: ``78``). + + ``autodetect_encoding`` + boolean, if set to ``False`` the function will not try to detect the + mo file encoding (optional, default: ``True``). + + ``encoding`` + string, the encoding to use, only relevant if ``autodetect_encoding`` + is set to ``False``. + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + return _pofile_or_mofile(mofile, 'mofile', **kwargs) + +# }}} +# function detect_encoding() {{{ + +def detect_encoding(file, binary_mode=False): + """ + Try to detect the encoding used by the ``file``. The ``file`` argument can + be a PO or MO file path or a string containing the contents of the file. + If the encoding cannot be detected, the function will return the value of + ``default_encoding``. + + Arguments: + + ``file`` + string, full or relative path to the po/mo file or its content. + + ``binary_mode`` + boolean, set this to True if ``file`` is a mo file. + """ + rx = re.compile(r'"?Content-Type:.+? charset=([\w_\-:\.]+)') + + def charset_exists(charset): + """Check whether ``charset`` is valid or not.""" + try: + codecs.lookup(charset) + except LookupError: + return False + return True + + if not os.path.exists(file): + match = rx.search(file) + if match: + enc = match.group(1).strip() + if charset_exists(enc): + return enc + else: + if binary_mode: + mode = 'rb' + else: + mode = 'r' + f = open(file, mode) + for l in f.readlines(): + match = rx.search(l) + if match: + f.close() + enc = match.group(1).strip() + if charset_exists(enc): + return enc + f.close() + return default_encoding + +# }}} +# function escape() {{{ + +def escape(st): + """ + Escapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in + the given string ``st`` and returns it. + """ + return st.replace('\\', r'\\')\ + .replace('\t', r'\t')\ + .replace('\r', r'\r')\ + .replace('\n', r'\n')\ + .replace('\"', r'\"') + +# }}} +# function unescape() {{{ + +def unescape(st): + """ + Unescapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in + the given string ``st`` and returns it. + """ + def unescape_repl(m): + m = m.group(1) + if m == 'n': + return '\n' + if m == 't': + return '\t' + if m == 'r': + return '\r' + if m == '\\': + return '\\' + return m # handles escaped double quote + return re.sub(r'\\(\\|n|t|r|")', unescape_repl, st) + +# }}} +# class _BaseFile {{{ + +class _BaseFile(list): + """ + Common base class for the :class:`~polib.POFile` and :class:`~polib.MOFile` + classes. This class should **not** be instanciated directly. + """ + + def __init__(self, *args, **kwargs): + """ + Constructor, accepts the following keyword arguments: + + ``pofile`` + string, the path to the po or mo file, or its content as a string. + + ``wrapwidth`` + integer, the wrap width, only useful when the ``-w`` option was + passed to xgettext (optional, default: ``78``). + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file, (optional, default: ``False``). + """ + list.__init__(self) + # the opened file handle + pofile = kwargs.get('pofile', None) + if pofile and os.path.exists(pofile): + self.fpath = pofile + else: + self.fpath = kwargs.get('fpath') + # the width at which lines should be wrapped + self.wrapwidth = kwargs.get('wrapwidth', 78) + # the file encoding + self.encoding = kwargs.get('encoding', default_encoding) + # whether to check for duplicate entries or not + self.check_for_duplicates = kwargs.get('check_for_duplicates', False) + # header + self.header = '' + # both po and mo files have metadata + self.metadata = {} + self.metadata_is_fuzzy = 0 + + def __unicode__(self): + """ + Returns the unicode representation of the file. + """ + ret = [] + entries = [self.metadata_as_entry()] + \ + [e for e in self if not e.obsolete] + for entry in entries: + ret.append(entry.__unicode__(self.wrapwidth)) + for entry in self.obsolete_entries(): + ret.append(entry.__unicode__(self.wrapwidth)) + ret = '\n'.join(ret) + + if type(ret) != types.UnicodeType: + return unicode(ret, self.encoding) + return ret + + def __str__(self): + """ + Returns the string representation of the file. + """ + return unicode(self).encode(self.encoding) + + def __contains__(self, entry): + """ + Overriden ``list`` method to implement the membership test (in and + not in). + The method considers that an entry is in the file if it finds an entry + that has the same msgid (the test is **case sensitive**). + + Argument: + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + return self.find(entry.msgid, by='msgid') is not None + + def __eq__(self, other): + return unicode(self) == unicode(other) + + def append(self, entry): + """ + Overriden method to check for duplicates entries, if a user tries to + add an entry that is already in the file, the method will raise a + ``ValueError`` exception. + + Argument: + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + if self.check_for_duplicates and entry in self: + raise ValueError('Entry "%s" already exists' % entry.msgid) + super(_BaseFile, self).append(entry) + + def insert(self, index, entry): + """ + Overriden method to check for duplicates entries, if a user tries to + add an entry that is already in the file, the method will raise a + ``ValueError`` exception. + + Arguments: + + ``index`` + index at which the entry should be inserted. + + ``entry`` + an instance of :class:`~polib._BaseEntry`. + """ + if self.check_for_duplicates and entry in self: + raise ValueError('Entry "%s" already exists' % entry.msgid) + super(_BaseFile, self).insert(index, entry) + + def metadata_as_entry(self): + """ + Returns the file metadata as a :class:`~polib.POFile` instance. + """ + e = POEntry(msgid='') + mdata = self.ordered_metadata() + if mdata: + strs = [] + for name, value in mdata: + # Strip whitespace off each line in a multi-line entry + strs.append('%s: %s' % (name, value)) + e.msgstr = '\n'.join(strs) + '\n' + if self.metadata_is_fuzzy: + e.flags.append('fuzzy') + return e + + def save(self, fpath=None, repr_method='__str__'): + """ + Saves the po file to ``fpath``. + If it is an existing file and no ``fpath`` is provided, then the + existing file is rewritten with the modified data. + + Keyword arguments: + + ``fpath`` + string, full or relative path to the file. + + ``repr_method`` + string, the method to use for output. + """ + if self.fpath is None and fpath is None: + raise IOError('You must provide a file path to save() method') + contents = getattr(self, repr_method)() + if fpath is None: + fpath = self.fpath + if repr_method == 'to_binary': + fhandle = open(fpath, 'wb') + else: + fhandle = codecs.open(fpath, 'w', self.encoding) + if type(contents) != types.UnicodeType: + contents = contents.decode(self.encoding) + fhandle.write(contents) + fhandle.close() + # set the file path if not set + if self.fpath is None and fpath: + self.fpath = fpath + + def find(self, st, by='msgid', include_obsolete_entries=False, + msgctxt=False): + """ + Find the entry which msgid (or property identified by the ``by`` + argument) matches the string ``st``. + + Keyword arguments: + + ``st`` + string, the string to search for. + + ``by`` + string, the property to use for comparison (default: ``msgid``). + + ``include_obsolete_entries`` + boolean, whether to also search in entries that are obsolete. + + ``msgctxt`` + string, allows to specify a specific message context for the + search. + """ + if include_obsolete_entries: + entries = self[:] + else: + entries = [e for e in self if not e.obsolete] + for e in entries: + if getattr(e, by) == st: + if msgctxt and e.msgctxt != msgctxt: + continue + return e + return None + + def ordered_metadata(self): + """ + Convenience method that returns an ordered version of the metadata + dictionnary. The return value is list of tuples (metadata name, + metadata_value). + """ + # copy the dict first + metadata = self.metadata.copy() + data_order = [ + 'Project-Id-Version', + 'Report-Msgid-Bugs-To', + 'POT-Creation-Date', + 'PO-Revision-Date', + 'Last-Translator', + 'Language-Team', + 'MIME-Version', + 'Content-Type', + 'Content-Transfer-Encoding' + ] + ordered_data = [] + for data in data_order: + try: + value = metadata.pop(data) + ordered_data.append((data, value)) + except KeyError: + pass + # the rest of the metadata will be alphabetically ordered since there + # are no specs for this AFAIK + keys = metadata.keys() + keys.sort() + for data in keys: + value = metadata[data] + ordered_data.append((data, value)) + return ordered_data + + def to_binary(self): + """ + Return the binary representation of the file. + """ + offsets = [] + entries = self.translated_entries() + # the keys are sorted in the .mo file + def cmp(_self, other): + # msgfmt compares entries with msgctxt if it exists + self_msgid = _self.msgctxt and _self.msgctxt or _self.msgid + other_msgid = other.msgctxt and other.msgctxt or other.msgid + if self_msgid > other_msgid: + return 1 + elif self_msgid < other_msgid: + return -1 + else: + return 0 + # add metadata entry + entries.sort(cmp) + mentry = self.metadata_as_entry() + #mentry.msgstr = mentry.msgstr.replace('\\n', '').lstrip() + entries = [mentry] + entries + entries_len = len(entries) + ids, strs = '', '' + for e in entries: + # For each string, we need size and file offset. Each string is + # NUL terminated; the NUL does not count into the size. + msgid = '' + if e.msgctxt: + # Contexts are stored by storing the concatenation of the + # context, a byte, and the original string + msgid = self._encode(e.msgctxt + '\4') + if e.msgid_plural: + indexes = e.msgstr_plural.keys() + indexes.sort() + msgstr = [] + for index in indexes: + msgstr.append(e.msgstr_plural[index]) + msgid += self._encode(e.msgid + '\0' + e.msgid_plural) + msgstr = self._encode('\0'.join(msgstr)) + else: + msgid += self._encode(e.msgid) + msgstr = self._encode(e.msgstr) + offsets.append((len(ids), len(msgid), len(strs), len(msgstr))) + ids += msgid + '\0' + strs += msgstr + '\0' + + # The header is 7 32-bit unsigned integers. + keystart = 7*4+16*entries_len + # and the values start after the keys + valuestart = keystart + len(ids) + koffsets = [] + voffsets = [] + # The string table first has the list of keys, then the list of values. + # Each entry has first the size of the string, then the file offset. + for o1, l1, o2, l2 in offsets: + koffsets += [l1, o1+keystart] + voffsets += [l2, o2+valuestart] + offsets = koffsets + voffsets + # check endianness for magic number + if struct.pack('@h', 1) == struct.pack('' % (self.__class__.__name__, id(self)) + + def __unicode__(self, wrapwidth=78): + """ + Returns the unicode representation of the entry. + """ + if self.obsolete: + delflag = '#~ ' + else: + delflag = '' + ret = [] + # write the msgctxt if any + if self.msgctxt is not None: + ret += self._str_field("msgctxt", delflag, "", self.msgctxt, wrapwidth) + # write the msgid + ret += self._str_field("msgid", delflag, "", self.msgid, wrapwidth) + # write the msgid_plural if any + if self.msgid_plural: + ret += self._str_field("msgid_plural", delflag, "", self.msgid_plural, wrapwidth) + if self.msgstr_plural: + # write the msgstr_plural if any + msgstrs = self.msgstr_plural + keys = list(msgstrs) + keys.sort() + for index in keys: + msgstr = msgstrs[index] + plural_index = '[%s]' % index + ret += self._str_field("msgstr", delflag, plural_index, msgstr, wrapwidth) + else: + # otherwise write the msgstr + ret += self._str_field("msgstr", delflag, "", self.msgstr, wrapwidth) + ret.append('') + ret = '\n'.join(ret) + + if type(ret) != types.UnicodeType: + return unicode(ret, self.encoding) + return ret + + def __str__(self): + """ + Returns the string representation of the entry. + """ + return unicode(self).encode(self.encoding) + + def __eq__(self, other): + return unicode(self) == unicode(other) + + def _str_field(self, fieldname, delflag, plural_index, field, wrapwidth=78): + lines = field.splitlines(True) + if len(lines) > 1: + lines = ['']+lines # start with initial empty line + else: + if wrapwidth > 0 and len(field) > wrapwidth-(len(fieldname)+2): + # Wrap the line but take field name into account + lines = ['']+ wrap(field, + wrapwidth-(len(fieldname)+2), + drop_whitespace=False, + break_long_words=False) + else: + lines = [field] # needed for the empty string case + #lines = [field] # needed for the empty string case + if fieldname.startswith('previous_'): + # quick and dirty trick to get the real field name + fieldname = fieldname[9:] + + ret = ['%s%s%s "%s"' % (delflag, fieldname, plural_index, + escape(lines.pop(0)))] + for mstr in lines: + ret.append('%s"%s"' % (delflag, escape(mstr))) + return ret + +# }}} +# class POEntry {{{ + +class POEntry(_BaseEntry): + """ + Represents a po file entry. + """ + + def __init__(self, *args, **kwargs): + """ + TODO: document keyword arguments. + """ + _BaseEntry.__init__(self, *args, **kwargs) + self.comment = kwargs.get('comment', '') + self.tcomment = kwargs.get('tcomment', '') + self.occurrences = kwargs.get('occurrences', []) + self.flags = kwargs.get('flags', []) + self.previous_msgctxt = kwargs.get('previous_msgctxt', None) + self.previous_msgid = kwargs.get('previous_msgid', None) + self.previous_msgid_plural = kwargs.get('previous_msgid_plural', None) + + def __unicode__(self, wrapwidth=78): + """ + Returns the unicode representation of the entry. + """ + if self.obsolete: + return _BaseEntry.__unicode__(self, wrapwidth) + ret = [] + # comment first, if any (with text wrapping as xgettext does) + if self.comment != '': + for comment in self.comment.split('\n'): + if wrapwidth > 0 and len(comment) > wrapwidth-3: + ret += wrap(comment, wrapwidth, + initial_indent='#. ', + subsequent_indent='#. ', + break_long_words=False) + else: + ret.append('#. %s' % comment) + # translator comment, if any (with text wrapping as xgettext does) + if self.tcomment != '': + for tcomment in self.tcomment.split('\n'): + if wrapwidth > 0 and len(tcomment) > wrapwidth-2: + ret += wrap(tcomment, wrapwidth, + initial_indent='# ', + subsequent_indent='# ', + break_long_words=False) + else: + ret.append('# %s' % tcomment) + # occurrences (with text wrapping as xgettext does) + if self.occurrences: + filelist = [] + for fpath, lineno in self.occurrences: + if lineno: + filelist.append('%s:%s' % (fpath, lineno)) + else: + filelist.append(fpath) + filestr = ' '.join(filelist) + if wrapwidth > 0 and len(filestr)+3 > wrapwidth: + # XXX textwrap split words that contain hyphen, this is not + # what we want for filenames, so the dirty hack is to + # temporally replace hyphens with a char that a file cannot + # contain, like "*" + lines = wrap(filestr.replace('-', '*'), + wrapwidth, + initial_indent='#: ', + subsequent_indent='#: ', + break_long_words=False) + # end of the replace hack + for line in lines: + ret.append(line.replace('*', '-')) + else: + ret.append('#: '+filestr) + # flags + if self.flags: + flags = [] + for flag in self.flags: + flags.append(flag) + ret.append('#, %s' % ', '.join(flags)) + + # previous context and previous msgid/msgid_plural + if self.previous_msgctxt: + ret += self._str_field("previous_msgctxt", "#| ", "", + self.previous_msgctxt, wrapwidth) + if self.previous_msgid: + ret += self._str_field("previous_msgid", "#| ", "", + self.previous_msgid, wrapwidth) + if self.previous_msgid_plural: + ret += self._str_field("previous_msgid_plural", "#| ", "", + self.previous_msgid_plural, wrapwidth) + + ret.append(_BaseEntry.__unicode__(self, wrapwidth)) + ret = '\n'.join(ret) + + if type(ret) != types.UnicodeType: + return unicode(ret, self.encoding) + return ret + + def __cmp__(self, other): + """ + Called by comparison operations if rich comparison is not defined. + """ + def compare_occurrences(a, b): + """ + Compare an entry occurrence with another one. + """ + if a[0] != b[0]: + return a[0] < b[0] + if a[1] != b[1]: + return a[1] < b[1] + return 0 + + # First: Obsolete test + if self.obsolete != other.obsolete: + if self.obsolete: + return -1 + else: + return 1 + # Work on a copy to protect original + occ1 = self.occurrences[:] + occ2 = other.occurrences[:] + # Sorting using compare method + occ1.sort(compare_occurrences) + occ2.sort(compare_occurrences) + # Comparing sorted occurrences + pos = 0 + for entry1 in occ1: + try: + entry2 = occ2[pos] + except IndexError: + return 1 + pos = pos + 1 + if entry1[0] != entry2[0]: + if entry1[0] > entry2[0]: + return 1 + else: + return -1 + if entry1[1] != entry2[1]: + if entry1[1] > entry2[1]: + return 1 + else: + return -1 + # Finally: Compare message ID + if self.msgid > other.msgid: return 1 + else: return -1 + + def translated(self): + """ + Returns ``True`` if the entry has been translated or ``False`` + otherwise. + """ + if self.obsolete or 'fuzzy' in self.flags: + return False + if self.msgstr != '': + return True + if self.msgstr_plural: + for pos in self.msgstr_plural: + if self.msgstr_plural[pos] == '': + return False + return True + return False + + def merge(self, other): + """ + Merge the current entry with the given pot entry. + """ + self.msgid = other.msgid + self.msgctxt = other.msgctxt + self.occurrences = other.occurrences + self.comment = other.comment + fuzzy = 'fuzzy' in self.flags + self.flags = other.flags[:] # clone flags + if fuzzy: + self.flags.append('fuzzy') + self.msgid_plural = other.msgid_plural + self.obsolete = other.obsolete + self.previous_msgctxt = other.previous_msgctxt + self.previous_msgid = other.previous_msgid + self.previous_msgid_plural = other.previous_msgid_plural + if other.msgstr_plural: + for pos in other.msgstr_plural: + try: + # keep existing translation at pos if any + self.msgstr_plural[pos] + except KeyError: + self.msgstr_plural[pos] = '' + +# }}} +# class MOEntry {{{ + +class MOEntry(_BaseEntry): + """ + Represents a mo file entry. + """ + pass + +# }}} +# class _POFileParser {{{ + +class _POFileParser(object): + """ + A finite state machine to parse efficiently and correctly po + file format. + """ + + def __init__(self, pofile, *args, **kwargs): + """ + Constructor. + + Keyword arguments: + + ``pofile`` + string, path to the po file or its content + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + enc = kwargs.get('encoding', default_encoding) + if os.path.exists(pofile): + try: + self.fhandle = codecs.open(pofile, 'rU', enc) + except LookupError: + enc = default_encoding + self.fhandle = codecs.open(pofile, 'rU', enc) + else: + self.fhandle = pofile.splitlines() + + self.instance = POFile( + pofile=pofile, + encoding=enc, + check_for_duplicates=kwargs.get('check_for_duplicates', False) + ) + self.transitions = {} + self.current_entry = POEntry() + self.current_state = 'ST' + self.current_token = None + # two memo flags used in handlers + self.msgstr_index = 0 + self.entry_obsolete = 0 + # Configure the state machine, by adding transitions. + # Signification of symbols: + # * ST: Beginning of the file (start) + # * HE: Header + # * TC: a translation comment + # * GC: a generated comment + # * OC: a file/line occurence + # * FL: a flags line + # * CT: a message context + # * PC: a previous msgctxt + # * PM: a previous msgid + # * PP: a previous msgid_plural + # * MI: a msgid + # * MP: a msgid plural + # * MS: a msgstr + # * MX: a msgstr plural + # * MC: a msgid or msgstr continuation line + all = ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'PC', 'PM', 'PP', 'TC', + 'MS', 'MP', 'MX', 'MI'] + + self.add('TC', ['ST', 'HE'], 'HE') + self.add('TC', ['GC', 'OC', 'FL', 'TC', 'PC', 'PM', 'PP', 'MS', + 'MP', 'MX', 'MI'], 'TC') + self.add('GC', all, 'GC') + self.add('OC', all, 'OC') + self.add('FL', all, 'FL') + self.add('PC', all, 'PC') + self.add('PM', all, 'PM') + self.add('PP', all, 'PP') + self.add('CT', ['ST', 'HE', 'GC', 'OC', 'FL', 'TC', 'PC', 'PM', + 'PP', 'MS', 'MX'], 'CT') + self.add('MI', ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'TC', 'PC', + 'PM', 'PP', 'MS', 'MX'], 'MI') + self.add('MP', ['TC', 'GC', 'PC', 'PM', 'PP', 'MI'], 'MP') + self.add('MS', ['MI', 'MP', 'TC'], 'MS') + self.add('MX', ['MI', 'MX', 'MP', 'TC'], 'MX') + self.add('MC', ['CT', 'MI', 'MP', 'MS', 'MX', 'PM', 'PP', 'PC'], 'MC') + + def parse(self): + """ + Run the state machine, parse the file line by line and call process() + with the current matched symbol. + """ + i, lastlen = 1, 0 + for line in self.fhandle: + line = line.strip() + if line == '': + i = i+1 + continue + if line[:3] == '#~ ': + line = line[3:] + self.entry_obsolete = 1 + else: + self.entry_obsolete = 0 + self.current_token = line + if line[:2] == '#:': + # we are on a occurrences line + self.process('OC', i) + elif line[:9] == 'msgctxt "': + # we are on a msgctxt + self.process('CT', i) + elif line[:7] == 'msgid "': + # we are on a msgid + self.process('MI', i) + elif line[:8] == 'msgstr "': + # we are on a msgstr + self.process('MS', i) + elif line[:1] == '"' or line[:4] == '#| "': + # we are on a continuation line or some metadata + self.process('MC', i) + elif line[:14] == 'msgid_plural "': + # we are on a msgid plural + self.process('MP', i) + elif line[:7] == 'msgstr[': + # we are on a msgstr plural + self.process('MX', i) + elif line[:3] == '#, ': + # we are on a flags line + self.process('FL', i) + elif line[:2] == '# ' or line == '#': + if line == '#': line = line + ' ' + # we are on a translator comment line + self.process('TC', i) + elif line[:2] == '#.': + # we are on a generated comment line + self.process('GC', i) + elif line[:15] == '#| msgid_plural': + # we are on a previous msgid_plural + self.process('PP', i) + elif line[:8] == '#| msgid': + self.process('PM', i) + # we are on a previous msgid + elif line[:10] == '#| msgctxt': + # we are on a previous msgctxt + self.process('PC', i) + i = i+1 + + if self.current_entry: + # since entries are added when another entry is found, we must add + # the last entry here (only if there are lines) + self.instance.append(self.current_entry) + # before returning the instance, check if there's metadata and if + # so extract it in a dict + firstentry = self.instance[0] + if firstentry.msgid == '': # metadata found + # remove the entry + firstentry = self.instance.pop(0) + self.instance.metadata_is_fuzzy = firstentry.flags + key = None + for msg in firstentry.msgstr.splitlines(): + try: + key, val = msg.split(':', 1) + self.instance.metadata[key] = val.strip() + except: + if key is not None: + self.instance.metadata[key] += '\n'+ msg.strip() + # close opened file + if isinstance(self.fhandle, file): + self.fhandle.close() + return self.instance + + def add(self, symbol, states, next_state): + """ + Add a transition to the state machine. + + Keywords arguments: + + ``symbol`` + string, the matched token (two chars symbol). + + ``states`` + list, a list of states (two chars symbols). + + ``next_state`` + the next state the fsm will have after the action. + """ + for state in states: + action = getattr(self, 'handle_%s' % next_state.lower()) + self.transitions[(symbol, state)] = (action, next_state) + + def process(self, symbol, linenum): + """ + Process the transition corresponding to the current state and the + symbol provided. + + Keywords arguments: + + ``symbol`` + string, the matched token (two chars symbol). + + ``linenum`` + integer, the current line number of the parsed file. + """ + try: + (action, state) = self.transitions[(symbol, self.current_state)] + if action(): + self.current_state = state + except Exception, exc: + raise IOError('Syntax error in po file (line %s)' % linenum) + + # state handlers + + def handle_he(self): + """Handle a header comment.""" + if self.instance.header != '': + self.instance.header += '\n' + self.instance.header += self.current_token[2:] + return 1 + + def handle_tc(self): + """Handle a translator comment.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + if self.current_entry.tcomment != '': + self.current_entry.tcomment += '\n' + self.current_entry.tcomment += self.current_token[2:] + return True + + def handle_gc(self): + """Handle a generated comment.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + if self.current_entry.comment != '': + self.current_entry.comment += '\n' + self.current_entry.comment += self.current_token[3:] + return True + + def handle_oc(self): + """Handle a file:num occurence.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + occurrences = self.current_token[3:].split() + for occurrence in occurrences: + if occurrence != '': + try: + fil, line = occurrence.split(':') + if not line.isdigit(): + fil = fil + line + line = '' + self.current_entry.occurrences.append((fil, line)) + except: + self.current_entry.occurrences.append((occurrence, '')) + return True + + def handle_fl(self): + """Handle a flags line.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.flags += self.current_token[3:].split(', ') + return True + + def handle_pp(self): + """Handle a previous msgid_plural line.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.previous_msgid_plural = \ + unescape(self.current_token[17:-1]) + return True + + def handle_pm(self): + """Handle a previous msgid line.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.previous_msgid = \ + unescape(self.current_token[10:-1]) + return True + + def handle_pc(self): + """Handle a previous msgctxt line.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.previous_msgctxt = \ + unescape(self.current_token[12:-1]) + return True + + def handle_ct(self): + """Handle a msgctxt.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.msgctxt = unescape(self.current_token[9:-1]) + return True + + def handle_mi(self): + """Handle a msgid.""" + if self.current_state in ['MC', 'MS', 'MX']: + self.instance.append(self.current_entry) + self.current_entry = POEntry() + self.current_entry.obsolete = self.entry_obsolete + self.current_entry.msgid = unescape(self.current_token[7:-1]) + return True + + def handle_mp(self): + """Handle a msgid plural.""" + self.current_entry.msgid_plural = unescape(self.current_token[14:-1]) + return True + + def handle_ms(self): + """Handle a msgstr.""" + self.current_entry.msgstr = unescape(self.current_token[8:-1]) + return True + + def handle_mx(self): + """Handle a msgstr plural.""" + index, value = self.current_token[7], self.current_token[11:-1] + self.current_entry.msgstr_plural[index] = unescape(value) + self.msgstr_index = index + return True + + def handle_mc(self): + """Handle a msgid or msgstr continuation line.""" + token = unescape(self.current_token[1:-1]) + if self.current_state == 'CT': + typ = 'msgctxt' + self.current_entry.msgctxt += token + elif self.current_state == 'MI': + typ = 'msgid' + self.current_entry.msgid += token + elif self.current_state == 'MP': + typ = 'msgid_plural' + self.current_entry.msgid_plural += token + elif self.current_state == 'MS': + typ = 'msgstr' + self.current_entry.msgstr += token + elif self.current_state == 'MX': + typ = 'msgstr[%s]' % self.msgstr_index + self.current_entry.msgstr_plural[self.msgstr_index] += token + elif self.current_state == 'PP': + typ = 'previous_msgid_plural' + token = token[3:] + self.current_entry.previous_msgid_plural += token + elif self.current_state == 'PM': + typ = 'previous_msgid' + token = token[3:] + self.current_entry.previous_msgid += token + elif self.current_state == 'PC': + typ = 'previous_msgctxt' + token = token[3:] + self.current_entry.previous_msgctxt += token + # don't change the current state + return False + +# }}} +# class _MOFileParser {{{ + +class _MOFileParser(object): + """ + A class to parse binary mo files. + """ + + def __init__(self, mofile, *args, **kwargs): + """ + Constructor. + + Keyword arguments: + + ``mofile`` + string, path to the mo file or its content + + ``encoding`` + string, the encoding to use, defaults to ``default_encoding`` + global variable (optional). + + ``check_for_duplicates`` + whether to check for duplicate entries when adding entries to the + file (optional, default: ``False``). + """ + self.fhandle = open(mofile, 'rb') + self.instance = MOFile( + fpath=mofile, + encoding=kwargs.get('encoding', default_encoding), + check_for_duplicates=kwargs.get('check_for_duplicates', False) + ) + + def parse(self): + """ + Build the instance with the file handle provided in the + constructor. + """ + # parse magic number + magic_number = self._readbinary(' 1: + entry = MOEntry( + msgid=msgid_tokens[0], + msgid_plural=msgid_tokens[1], + msgstr_plural=dict((k,v) for k,v in \ + enumerate(msgstr.split('\0'))) + ) + else: + entry = MOEntry(msgid=msgid, msgstr=msgstr) + self.instance.append(entry) + # close opened file + self.fhandle.close() + return self.instance + + def _readbinary(self, fmt, numbytes): + """ + Private method that unpack n bytes of data using format . + It returns a tuple or a mixed value if the tuple length is 1. + """ + bytes = self.fhandle.read(numbytes) + tup = struct.unpack(fmt, bytes) + if len(tup) == 1: + return tup[0] + return tup + +# }}} +# class TextWrapper {{{ + +class TextWrapper(textwrap.TextWrapper): + """ + Subclass of textwrap.TextWrapper that backport the + drop_whitespace option. + """ + def __init__(self, *args, **kwargs): + drop_whitespace = kwargs.pop('drop_whitespace', True) + textwrap.TextWrapper.__init__(self, *args, **kwargs) + self.drop_whitespace = drop_whitespace + + def _wrap_chunks(self, chunks): + """_wrap_chunks(chunks : [string]) -> [string] + + Wrap a sequence of text chunks and return a list of lines of + length 'self.width' or less. (If 'break_long_words' is false, + some lines may be longer than this.) Chunks correspond roughly + to words and the whitespace between them: each chunk is + indivisible (modulo 'break_long_words'), but a line break can + come between any two chunks. Chunks should not have internal + whitespace; ie. a chunk is either all whitespace or a "word". + Whitespace chunks will be removed from the beginning and end of + lines, but apart from that whitespace is preserved. + """ + lines = [] + if self.width <= 0: + raise ValueError("invalid width %r (must be > 0)" % self.width) + + # Arrange in reverse order so items can be efficiently popped + # from a stack of chucks. + chunks.reverse() + + while chunks: + + # Start the list of chunks that will make up the current line. + # cur_len is just the length of all the chunks in cur_line. + cur_line = [] + cur_len = 0 + + # Figure out which static string will prefix this line. + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + # Maximum width for this line. + width = self.width - len(indent) + + # First chunk on line is whitespace -- drop it, unless this + # is the very beginning of the text (ie. no lines started yet). + if self.drop_whitespace and chunks[-1].strip() == '' and lines: + del chunks[-1] + + while chunks: + l = len(chunks[-1]) + + # Can at least squeeze this chunk onto the current line. + if cur_len + l <= width: + cur_line.append(chunks.pop()) + cur_len += l + + # Nope, this line is full. + else: + break + + # The current line is full, and the next chunk is too big to + # fit on *any* line (not just this one). + if chunks and len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + + # If the last chunk on this line is all whitespace, drop it. + if self.drop_whitespace and cur_line and cur_line[-1].strip() == '': + del cur_line[-1] + + # Convert current line back to a string and store it in list + # of all lines (return value). + if cur_line: + lines.append(indent + ''.join(cur_line)) + + return lines + +# }}} +# function wrap() {{{ + +def wrap(text, width=70, **kwargs): + """ + Wrap a single paragraph of text, returning a list of wrapped lines. + """ + if sys.version_info < (2, 6): + return TextWrapper(width=width, **kwargs).wrap(text) + return textwrap.wrap(text, width=width, **kwargs) + +#}}} diff --git a/rosetta/poutil.py b/rosetta/poutil.py new file mode 100644 index 00000000..2c9ce852 --- /dev/null +++ b/rosetta/poutil.py @@ -0,0 +1,123 @@ +import os, django +from django.conf import settings +from rosetta.conf import settings as rosetta_settings +from django.core.cache import cache + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + +def find_pos(lang, project_apps = True, django_apps = False, third_party_apps = False): + """ + scans a couple possible repositories of gettext catalogs for the given + language code + + """ + + paths = [] + + # project/locale + parts = settings.SETTINGS_MODULE.split('.') + project = __import__(parts[0], {}, {}, []) + abs_project_path = os.path.normpath(os.path.abspath(os.path.dirname(project.__file__))) + if project_apps: + paths.append(os.path.abspath(os.path.join(os.path.dirname(project.__file__), 'locale'))) + + # django/locale + if django_apps: + django_paths = cache.get('rosetta_django_paths') + if django_paths is None: + django_paths = [] + for root,dirnames,filename in os.walk(os.path.abspath(os.path.dirname(django.__file__))): + if 'locale' in dirnames: + django_paths.append(os.path.join(root , 'locale')) + continue + cache.set('rosetta_django_paths', django_paths, 60*60) + paths = paths + django_paths + + + # settings + for localepath in settings.LOCALE_PATHS: + if os.path.isdir(localepath): + paths.append(localepath) + + # project/app/locale + for appname in settings.INSTALLED_APPS: + + if rosetta_settings.EXCLUDED_APPLICATIONS and appname in rosetta_settings.EXCLUDED_APPLICATIONS: + continue + + p = appname.rfind('.') + if p >= 0: + app = getattr(__import__(appname[:p], {}, {}, [appname[p+1:]]), appname[p+1:]) + else: + app = __import__(appname, {}, {}, []) + + apppath = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(app.__file__), 'locale'))) + + + # django apps + if 'contrib' in apppath and 'django' in apppath and not django_apps: + continue + + # third party external + if not third_party_apps and abs_project_path not in apppath: + continue + + # local apps + if not project_apps and abs_project_path in apppath: + continue + + + if os.path.isdir(apppath): + paths.append(apppath) + + + + + ret = set() + langs = (lang,) + if u'-' in lang: + _l,_c = map(lambda x:x.lower(),lang.split(u'-')) + langs += (u'%s_%s' %(_l, _c), u'%s_%s' %(_l, _c.upper()), ) + elif u'_' in lang: + _l,_c = map(lambda x:x.lower(),lang.split(u'_')) + langs += (u'%s-%s' %(_l, _c), u'%s-%s' %(_l, _c.upper()), ) + + paths = map(os.path.normpath, paths) + for path in paths: + for lang_ in langs: + dirname = os.path.join(path, lang_, 'LC_MESSAGES') + for fn in ('django.po','djangojs.po',): + filename = os.path.join(dirname, fn) + if os.path.isfile(filename): + ret.add(os.path.abspath(filename)) + return list(ret) + +def pagination_range(first,last,current): + r = [] + + r.append(first) + if first + 1 < last: r.append(first+1) + + if current -2 > first and current -2 < last: r.append(current-2) + if current -1 > first and current -1 < last: r.append(current-1) + if current > first and current < last: r.append(current) + if current + 1 < last and current+1 > first: r.append(current+1) + if current + 2 < last and current+2 > first: r.append(current+2) + + if last-1 > first: r.append(last-1) + r.append(last) + + r = list(set(r)) + r.sort() + prev = 10000 + for e in r[:]: + if prev + 1 < e: + try: + r.insert(r.index(e), '...') + except ValueError: + pass + prev = e + return r diff --git a/rosetta/signals.py b/rosetta/signals.py new file mode 100644 index 00000000..04c9b576 --- /dev/null +++ b/rosetta/signals.py @@ -0,0 +1,8 @@ +from django import dispatch +entry_changed = dispatch.Signal( + providing_args=["user", "old_msgstr", "old_fuzzy", "pofile", "language_code",] +) + +post_save = dispatch.Signal( + providing_args=["language_code","request",] +) diff --git a/rosetta/templates/rosetta/base.html b/rosetta/templates/rosetta/base.html new file mode 100644 index 00000000..e3abf2a4 --- /dev/null +++ b/rosetta/templates/rosetta/base.html @@ -0,0 +1,40 @@ + + + + {% block pagetitle %}Rosetta{% endblock %} + + + + + + + + + + +
+ + +
+ {% block main %}{% endblock %} +
+ +
+ + diff --git a/rosetta/templates/rosetta/css/rosetta.css b/rosetta/templates/rosetta/css/rosetta.css new file mode 100644 index 00000000..c98b92e4 --- /dev/null +++ b/rosetta/templates/rosetta/css/rosetta.css @@ -0,0 +1,31 @@ +#user-tools p span { margin-left:2em; } +table{width:100%;} +td.translation label {font-size: 95%;} +td.translation textarea, td.original { font-size:110%;} +td.translation,td.original {width:30%;} +td.original .plural-container { position:relative} +td.original .context,td.original .message {display:block;} +td.original .context { margin-top: 2em; color: #bbb; font-size:90%; } +td.translation textarea{ width:98.5%; min-height:25px; margin:2px 0; } +.rtl td.translation textarea { direction: rtl;} +.plural span {display:block; margin:2px 0; position:absolute} +td.location code,td.location a { font-size:95%; display:block;} +td.location code.hide { display:none;} +.submit-row {margin-bottom:0;} +li.nobubble,li.nobubble:hover { background-image:none;} +.object-tools li.active, .object-tools li.active a { color:yellow;} +p.paginator input { float:right;} +p.paginator span.space { margin: 0 0.5em;} +.paginator {text-align:left; border:none; background:transparent; } +.paginator strong { margin-left:1em;} +th.r,td.r {text-align:right;} +th.c,td.c {text-align: center;} +td.original code {font-size:90%; padding: 0 1px; } +tr.row2 td.original code {background-color:#FFB2A5; padding: 0 0.3em;} +tr.row1 td.original code {background-color:#FFB2A5;} +.alert { font-weight:bold;padding:4px 5px 4px 25px; margin-left:1em; color: red; background:transparent url({{ADMIN_MEDIA_PREFIX}}img/admin/icon_alert.gif) 5px .3em no-repeat; } +p.errornote { margin-top:0.4em;} +#footer { clear:both; color:#999; font-size:9px; margin:0 6px; text-align:left;} +#changelist table tbody td:first-child, #changelist table thead th:first-child {text-align: left;} +td.hint {color: #777;} +div.module {margin-bottom: 20px;} diff --git a/rosetta/templates/rosetta/js/rosetta.js b/rosetta/templates/rosetta/js/rosetta.js new file mode 100644 index 00000000..08e3ae22 --- /dev/null +++ b/rosetta/templates/rosetta/js/rosetta.js @@ -0,0 +1,63 @@ +google.setOnLoadCallback(function() { + $('.location a').show().toggle(function() { + $('.hide', $(this).parent()).show(); + }, function() { + $('.hide', $(this).parent()).hide(); + }); +{% if ENABLE_TRANSLATION_SUGGESTIONS %} + $('a.suggest').click(function() { + var a=$(this), + str=a.html(), + orig=$('.original .message', + a.parents('tr')).html(), + trans=$('textarea',a.parent()); + orig = unescape(orig).replace(//g,'\n').replace(//g,'').replace(/<\/code>/g,'').replace(/>/g,'>').replace(/</g,'<'); + a.attr('class','suggesting').html('...'); + google.language.translate(orig, '{{MESSAGES_SOURCE_LANGUAGE_CODE}}', '{{rosetta_i18n_lang_code}}', function(result) { + if (!result.error) { + trans.val(unescape(result.translation).replace(/'/g,'\'').replace(/"/g,'"').replace(/%\s+(\([^\)]+\))\s*s/g,' %$1s ')); + a.hide(); + } else { + a.hide().before($(''+result.error.message+'')); + } + }); + return false; + }); +{% endif %} + $('td.plural').each(function(i) { + var td = $(this), trY = parseInt(td.closest('tr').offset().top); + $('textarea', $(this).closest('tr')).each(function(j) { + var textareaY= parseInt($(this).offset().top) - trY; + $($('.part',td).get(j)).css('top',textareaY + 'px'); + }); + }); + + $('.translation textarea').blur(function() { + if($(this).val()) { + $('.alert', $(this).parents('tr')).remove(); + var RX = /%(?:\([^\s\)]*\))?[sdf]/g, + origs=$('.original', $(this).parents('tr')).html().match(RX), + trads=$(this).val().match(RX), + error = $('Unmatched variables'); + if (origs && trads) { + for (var i = trads.length; i--;){ + var key = trads[i]; + if (-1 == $.inArray(key, origs)) { + $(this).before(error) + return false; + } + } + return true; + } else { + if (!(origs === null && trads === null)) { + $(this).before(error); + return false; + } + } + return true; + } + }); + + $('.translation textarea').eq(0).focus(); + +}); diff --git a/rosetta/templates/rosetta/languages.html b/rosetta/templates/rosetta/languages.html new file mode 100644 index 00000000..d8c0cf9e --- /dev/null +++ b/rosetta/templates/rosetta/languages.html @@ -0,0 +1,61 @@ +{% extends "rosetta/base.html" %} +{% load i18n %} + +{% block pagetitle %}{{block.super}} - {% trans "Language selection" %}{% endblock %} + +{% block breadcumbs %}{% trans "Home" %} › {% trans "Language selection" %}{% endblock %} + +{% block main %} +

 

+ + + {% if has_pos %} + + {% for lid,language,pos in languages %} + {% if pos %} + +
+

{{language}}

+ + + + + + + + + + + + + + {% for app,path,po in pos %} + + + + {% with po.untranslated_entries|length as len_untranslated_entries %} + + {% endwith %} + + + + + + {% endfor %} + +
{% trans "Application" %}{% trans "Progress"%}{% trans "Messages" %}{% trans "Translated" %}{% trans "Fuzzy"%}{% trans "Obsolete"%}{% trans "File" %}
{{ app|title }}{{po.percent_translated|floatformat:2}}%{{po.translated_entries|length|add:len_untranslated_entries}}{{po.translated_entries|length}}{{po.fuzzy_entries|length}}{{po.obsolete_entries|length}}{{ path }}
+
+ {% endif %} + {% endfor %} + {% else %} +

{% trans "Nothing to translate!" %}

+

{% trans "You haven't specified any languages in your settings file, or haven't yet generated a batch of translation catalogs." %}

+

{% blocktrans with "http://docs.djangoproject.com/en/dev/topics/i18n/#topics-i18n" as i18n_doc_link %}Please refer to Django's I18N documentation for a guide on how to set up internationalization for your project.{% endblocktrans %}

+ {% endif %} +{% endblock %} diff --git a/rosetta/templates/rosetta/pofile.html b/rosetta/templates/rosetta/pofile.html new file mode 100644 index 00000000..1e8bbd04 --- /dev/null +++ b/rosetta/templates/rosetta/pofile.html @@ -0,0 +1,152 @@ +{% extends "rosetta/base.html" %} +{% load rosetta i18n %} + +{% block header %} + {{block.super}} + +{% endblock %} + +{% block pagetitle %}{{block.super}} - {{MESSAGES_SOURCE_LANGUAGE_NAME}} - {{rosetta_i18n_lang_name}} ({{ rosetta_i18n_pofile.percent_translated|floatformat:0 }}%){% endblock %} + +{% block breadcumbs %} +
+ {% trans "Home" %} › + {{ rosetta_i18n_lang_name }} › + {{ rosetta_i18n_app|title }} › + {% blocktrans with rosetta_i18n_pofile.percent_translated|floatformat:2 as percent_translated %}Progress: {{ percent_translated }}%{% endblocktrans %} +
+ {% if not rosetta_i18n_write %}

{% trans "File is read-only: download the file when done editing!" %}

{% endif %} + {% if rosetta_last_save_error %}

{% trans "Some items in your last translation block couldn't be saved: this usually happens when the catalog file changes on disk after you last loaded it." %}

{% endif %} +{% endblock %} + +{% block main %} +

{% blocktrans %}Translate into {{rosetta_i18n_lang_name}}{% endblocktrans %}

+ + +
+
+ +
+ +
+ + + + {% rosetta_csrf_token %} + + {% if main_language %}{% endif %} + + + + + + + {% for message in messages %} + + {% if message.msgid_plural %} + + + {% else %} + + {% if main_language %}{% endif %} + + {% endif %} + + + + {% endfor %} + +
{% trans "Original" %}{{ main_language }}{{ rosetta_i18n_lang_name }}{% trans "Fuzzy" %}{% trans "Occurrences(s)" %}
+
+ {{message.msgid|format_message|linebreaksbr}} + {{message.msgid_plural|format_message|linebreaksbr}} +
+ + {% if message.msgctxt %} + {% trans "Context hint" %}: {{message.msgctxt}} + {% else %} + {% if message.comment %} + {% trans "Context hint" %}: {{message.comment}} + {% endif %} + {% endif %} + +
+ {% for k, msgstr in message.msgstr_plural.items|dictsort:"0" %} + + + {% endfor %} + + {{ message.msgid|format_message|linebreaksbr }} + {% if message.msgctxt %} + {% trans "Contextual hint" %}: "{{message.msgctxt}}" + {% else %} + {% if message.comment %} + {% trans "Context hint" %}: {{message.comment}} + {% endif %} + {% endif %} + {{ message.main_lang|format_message|linebreaksbr }} + + {% if ENABLE_TRANSLATION_SUGGESTIONS %}{% trans "suggest" %}{% endif %} + + + + {% for fn,lineno in message.occurrences %} + {{ fn }}:{{lineno}} + {% endfor %} + {% if message.occurrences|length|gt:"3" %} + … ({% blocktrans count message.occurrences|length|minus:"3" as more_count %}{{more_count}} more{% plural %}{{more_count}} more{% endblocktrans %}) + {% endif %} +
+
+

+ {% if query %} + + {% endif %} + + + + {% if needs_pagination %} + {% trans "Skip to page:" %} + {% for i in page_range %} + {% ifequal i '...' %} + {{ i }} + {% else %} + {% ifequal i page %} + {{i}} + {% else %} + {{i}} + {% endifequal %} + {% endifequal %} + {% endfor %} + {% else %} + {% trans "Displaying:" %} + {% endif %} + + {% with paginator.object_list|length as hits %} + {% blocktrans count rosetta_i18n_pofile|length as message_number %}{{hits}}/{{message_number}} message{% plural %}{{hits}}/{{message_number}} messages{% endblocktrans %} + {% endwith %} + + +

+
+
+
+{% endblock %} diff --git a/rosetta/templatetags/__init__.py b/rosetta/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/rosetta/templatetags/rosetta.py b/rosetta/templatetags/rosetta.py new file mode 100644 index 00000000..5a699360 --- /dev/null +++ b/rosetta/templatetags/rosetta.py @@ -0,0 +1,76 @@ +from django import template +from django.utils.safestring import mark_safe +from django.utils.html import escape +import re +from django.template import Node +from django.utils.encoding import smart_str, smart_unicode + +register = template.Library() +rx = re.compile(r'(%(\([^\s\)]*\))?[sd])') + +def format_message(message): + return mark_safe(rx.sub('\\1', escape(message).replace(r'\n','
\n'))) +format_message=register.filter(format_message) + + +def lines_count(message): + return 1 + sum([len(line)/50 for line in message.split('\n')]) +lines_count=register.filter(lines_count) + +def mult(a,b): + return int(a)*int(b) +mult=register.filter(mult) + +def minus(a,b): + try: + return int(a) - int(b) + except: + return 0 +minus=register.filter(minus) + + +def gt(a,b): + try: + return int(a) > int(b) + except: + return False +gt=register.filter(gt) + + +def do_incr(parser, token): + args = token.split_contents() + if len(args) < 2: + raise TemplateSyntaxError("'incr' tag requires at least one argument") + name = args[1] + if not hasattr(parser, '_namedIncrNodes'): + parser._namedIncrNodes = {} + if not name in parser._namedIncrNodes: + parser._namedIncrNodes[name] = IncrNode(0) + return parser._namedIncrNodes[name] +do_incr = register.tag('increment', do_incr) + + +class IncrNode(template.Node): + def __init__(self, init_val=0): + self.val = init_val + + def render(self, context): + self.val += 1 + return smart_unicode(self.val) + + +def is_fuzzy(message): + return message and hasattr(message, 'flags') and 'fuzzy' in message.flags +is_fuzzy = register.filter(is_fuzzy) + +class RosettaCsrfTokenPlaceholder(Node): + def render(self, context): + return mark_safe(u"") + +def rosetta_csrf_token(parser, token): + try: + from django.template.defaulttags import csrf_token + return csrf_token(parser,token) + except ImportError: + return RosettaCsrfTokenPlaceholder() +rosetta_csrf_token=register.tag(rosetta_csrf_token) diff --git a/rosetta/tests/__init__.py b/rosetta/tests/__init__.py new file mode 100644 index 00000000..f680d9ce --- /dev/null +++ b/rosetta/tests/__init__.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- +from django.conf import settings +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.dispatch import receiver +from django.test import TestCase +from django.test.client import Client +from rosetta.conf import settings as rosetta_settings +from rosetta.signals import entry_changed, post_save +import os, shutil, django + + +class RosettaTestCase(TestCase): + urls = 'rosetta.tests.urls' + + + def __init__(self, *args,**kwargs): + super(RosettaTestCase,self).__init__(*args,**kwargs) + self.curdir = os.path.dirname(__file__) + self.dest_file = os.path.normpath(os.path.join(self.curdir, '../locale/xx/LC_MESSAGES/django.po')) + self.django_version_major, self.django_version_minor = django.VERSION[0],django.VERSION[1] + + + def setUp(self): + user = User.objects.create_user('test_admin', 'test@test.com', 'test_password') + user2 = User.objects.create_user('test_admin2', 'test@test2.com', 'test_password') + user3 = User.objects.create_user('test_admin3', 'test@test2.com', 'test_password') + + user.is_superuser, user2.is_superuser, user3.is_superuser = True,True, True + user.is_staff, user2.is_staff, user3.is_staff = True,True, False + + user.save() + user2.save() + user3.save() + + self.client2 = Client() + + self.client.login(username='test_admin',password='test_password') + self.client2.login(username='test_admin2',password='test_password') + + settings.LANGUAGES = (('xx','dummy language'),) + + + + def test_1_ListLoading(self): + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue(os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in r.content) + + + def test_2_PickFile(self): + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) +'?rosetta') + r = self.client.get(reverse('rosetta-home')) + + self.assertTrue('dummy language' in r.content) + + def test_3_DownloadZIP(self): + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) +'?rosetta') + r = self.client.get(reverse('rosetta-home')) + r = self.client.get(reverse('rosetta-download-file' ) +'?rosetta') + self.assertTrue ('content-type' in r._headers.keys() ) + self.assertTrue ('application/x-zip' in r._headers.get('content-type')) + + def test_4_DoChanges(self): + + # copy the template file + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.template')), self.dest_file) + + # Load the template file + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home') + '?filter=untranslated') + r = self.client.get(reverse('rosetta-home')) + + # make sure both strings are untranslated + self.assertTrue('dummy language' in r.content) + self.assertTrue('String 1' in r.content) + self.assertTrue('String 2' in r.content) + self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # post a translation + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) + + # reload all untranslated strings + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) +'?rosetta') + r = self.client.get(reverse('rosetta-home') + '?filter=untranslated') + r = self.client.get(reverse('rosetta-home')) + + # the translated string no longer is up for translation + self.assertTrue('String 1' in r.content) + self.assertTrue('String 2' not in r.content) + + # display only translated strings + r = self.client.get(reverse('rosetta-home') + '?filter=translated') + r = self.client.get(reverse('rosetta-home')) + + # The tranlsation was persisted + self.assertTrue('String 1' not in r.content) + self.assertTrue('String 2' in r.content) + self.assertTrue('Hello, world' in r.content) + + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + + def test_5_TestIssue67(self): + # testcase for issue 67: http://code.google.com/p/django-rosetta/issues/detail?id=67 + # copy the template file + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.issue67.template')), self.dest_file) + + # Make sure the plurals string is valid + f_ = open(self.dest_file,'rb') + content = f_.read() + f_.close() + self.assertTrue(u'Hello, world' not in content) + self.assertTrue(u'|| n%100>=20) ? 1 : 2)' in content) + del(content) + + # Load the template file + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) +'?rosetta') + r = self.client.get(reverse('rosetta-home') + '?filter=untranslated') + r = self.client.get(reverse('rosetta-home')) + + # make sure all strings are untranslated + self.assertTrue('dummy language' in r.content) + self.assertTrue('String 1' in r.content) + self.assertTrue('String 2' in r.content) + self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # post a translation + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) + + # Make sure the plurals string is still valid + f_ = open(self.dest_file,'rb') + content = f_.read() + f_.close() + self.assertTrue(u'Hello, world' in content) + self.assertTrue(u'|| n%100>=20) ? 1 : 2)' in content) + self.assertTrue(u'or n%100>=20) ? 1 : 2)' not in content) + del(content) + + shutil.move(self.dest_file + '.orig', self.dest_file) + + + def test_6_ExcludedApps(self): + + rosetta_settings.EXCLUDED_APPLICATIONS = ('rosetta',) + + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue('rosetta/locale/xx/LC_MESSAGES/django.po' not in r.content) + + rosetta_settings.EXCLUDED_APPLICATIONS = () + + r = self.client.get(reverse('rosetta-pick-file') +'?rosetta') + self.assertTrue('rosetta/locale/xx/LC_MESSAGES/django.po' in r.content) + + def test_7_selfInApplist(self): + self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue('rosetta/locale/xx/LC_MESSAGES/django.po' in r.content) + + self.client.get(reverse('rosetta-pick-file') + '?filter=project') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue('rosetta/locale/xx/LC_MESSAGES/django.po' not in r.content) + + + def test_8_hideObsoletes(self): + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + + # not in listing + for p in range(1,5): + r = self.client.get(reverse('rosetta-home') + '?page=%d'%p) + self.assertTrue('dummy language' in r.content) + self.assertTrue('Les deux' not in r.content) + + r = self.client.get(reverse('rosetta-home') + '?query=Les%20Deux') + self.assertTrue('dummy language' in r.content) + self.assertTrue('Les deux' not in r.content) + + + def test_9_concurrency(self): + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.template')), self.dest_file) + + r = self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client2.get(reverse('rosetta-pick-file') +'?filter=third-party') + + self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) ) + self.client2.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + + # Load the template file + r = self.client.get(reverse('rosetta-home') + '?filter=untranslated') + r = self.client.get(reverse('rosetta-home')) + r2 = self.client2.get(reverse('rosetta-home') + '?filter=untranslated') + r2 = self.client2.get(reverse('rosetta-home')) + + self.assertTrue('String 1' in r.content) + self.assertTrue('String 1' in r2.content) + self.assertTrue('m_08e4e11e2243d764fc45a5a4fba5d0f2' in r.content) + + + r = self.client.post(reverse('rosetta-home'), dict(m_08e4e11e2243d764fc45a5a4fba5d0f2='Hello, world', _next='_next')) + r2 = self.client2.get(reverse('rosetta-home')) + + # Client 2 reloads the home, forces a reload of the catalog, + # the untranslated string1 is now translated + self.assertTrue('String 1' not in r2.content) + self.assertTrue('String 2' in r2.content) + + + r = self.client.get(reverse('rosetta-home') + '?filter=untranslated') + r = self.client.get(reverse('rosetta-home')) + r2 = self.client2.get(reverse('rosetta-home') + '?filter=untranslated') + r2 = self.client2.get(reverse('rosetta-home')) + + + self.assertTrue('String 2' in r2.content and 'm_e48f149a8b2e8baa81b816c0edf93890' in r2.content) + self.assertTrue('String 2' in r.content and 'm_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # client 2 posts! + r2 = self.client2.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client two!', _next='_next')) + self.assertTrue('save-conflict' not in r2.content) + + + # uh-oh here comes client 1 + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client one!', _next='_next')) + # An error message is displayed + self.assertTrue('save-conflict' in r.content) + + # Both clients show all strings, error messages are gone + r = self.client.get(reverse('rosetta-home') +'?filter=translated') + self.assertTrue('save-conflict' not in r.content) + r2 = self.client2.get(reverse('rosetta-home') +'?filter=translated') + self.assertTrue('save-conflict' not in r2.content) + r = self.client.get(reverse('rosetta-home')) + self.assertTrue('save-conflict' not in r.content) + r2 = self.client2.get(reverse('rosetta-home')) + self.assertTrue('save-conflict' not in r2.content) + + # Both have client's two version + self.assertTrue('Hello, world, from client two!' in r.content) + self.assertTrue('Hello, world, from client two!' in r2.content) + self.assertTrue('save-conflict' not in r2.content) + self.assertTrue('save-conflict' not in r.content) + + + + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + + def test_10_issue_79_num_entries(self): + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.issue79.template')), self.dest_file) + + self.client.get(reverse('rosetta-pick-file') +'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + + self.assertTrue('1' in r.content) + self.assertTrue('0.00%' in r.content) + self.assertTrue('1' in r.content) + + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + def test_11_issue_80_tab_indexes(self): + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home')) + self.assertTrue('tabindex="3"' in r.content) + + + def test_12_issue_82_staff_user(self): + self.client3 = Client() + self.client3.login(username='test_admin3',password='test_password') + + + r = self.client3.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() ) +'?rosetta') + r = self.client3.get(reverse('rosetta-home')) + self.assertTrue(not r.content) + + + def test_13_catalog_filters(self): + settings.LANGUAGES = (('fr','French'),('xx','Dummy Language'),) + + + + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue(os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in r.content) + self.assertTrue(('contrib') not in r.content) + + self.client.get(reverse('rosetta-pick-file')+'?filter=django') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue(os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') not in r.content) + + if self.django_version_major >=1 and self.django_version_minor >=3: + self.assertTrue(('contrib') in r.content) + + self.client.get(reverse('rosetta-pick-file')+'?filter=all') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue(os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') in r.content) + + if self.django_version_major >=1 and self.django_version_minor >=3: + self.assertTrue(('contrib') in r.content) + + self.client.get(reverse('rosetta-pick-file')+'?filter=project') + r = self.client.get(reverse('rosetta-pick-file')) + self.assertTrue(os.path.normpath('rosetta/locale/xx/LC_MESSAGES/django.po') not in r.content) + + if self.django_version_major >=1 and self.django_version_minor >=3: + self.assertTrue(('contrib') not in r.content) + + + def test_14_issue_99_context_and_comments(self): + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home')) + self.assertTrue('This is a text of the base template' in r.content) + self.assertTrue('Context hint' in r.content) + + + def test_14_issue_87_entry_changed_signal(self): + # copy the template file + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.template')), self.dest_file) + + + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home')) + + @receiver(entry_changed) + def test_receiver(sender, **kwargs): + self.test_old_msgstr = kwargs.get('old_msgstr') + self.test_new_msgstr = sender.msgstr + self.test_msg_id = sender.msgid + + + self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # post a translation + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) + + self.assertTrue(self.test_old_msgstr == '') + self.assertTrue(self.test_new_msgstr == 'Hello, world') + self.assertTrue(self.test_msg_id == 'String 2') + + del(self.test_old_msgstr, self.test_new_msgstr, self.test_msg_id) + + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + def test_15_issue_101_post_save_signal(self): + # copy the template file + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.template')), self.dest_file) + + + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home')) + + @receiver(post_save) + def test_receiver(sender, **kwargs): + self.test_sig_lang = kwargs.get('language_code') + + self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # post a translation + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) + + self.assertTrue(self.test_sig_lang == 'xx') + del(self.test_sig_lang) + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + + def test_16_issue_103_post_save_signal_has_request(self): + # copy the template file + shutil.copy(self.dest_file, self.dest_file + '.orig') + shutil.copy(os.path.normpath(os.path.join(self.curdir,'./django.po.template')), self.dest_file) + + + self.client.get(reverse('rosetta-pick-file')+'?filter=third-party') + r = self.client.get(reverse('rosetta-language-selection', args=('xx',0,), kwargs=dict() )) + r = self.client.get(reverse('rosetta-home')) + + @receiver(post_save) + def test_receiver(sender, **kwargs): + self.test_16_has_request = 'request' in kwargs + + self.assertTrue('m_e48f149a8b2e8baa81b816c0edf93890' in r.content) + + # post a translation + r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world', _next='_next')) + + self.assertTrue(self.test_16_has_request) + del(self.test_16_has_request) + # reset the original file + shutil.move(self.dest_file+'.orig', self.dest_file) + + diff --git a/rosetta/tests/django.po.issue67.template b/rosetta/tests/django.po.issue67.template new file mode 100644 index 00000000..e55a084f --- /dev/null +++ b/rosetta/tests/django.po.issue67.template @@ -0,0 +1,28 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-10-21 12:21+0200\n" +"PO-Revision-Date: 2008-09-22 11:02\n" +"Last-Translator: Admin Admin \n" +"Language-Team: French \n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + + +msgid "String 1" +msgstr "" + +msgid "String 2" +msgstr "" + + diff --git a/rosetta/tests/django.po.issue79.template b/rosetta/tests/django.po.issue79.template new file mode 100644 index 00000000..3e7bb14f --- /dev/null +++ b/rosetta/tests/django.po.issue79.template @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-10-21 12:21+0200\n" +"PO-Revision-Date: 2008-09-22 11:02\n" +"Last-Translator: Admin Admin \n" +"Language-Team: French \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + + +msgid "String 1" +msgstr "" + +#~ msgid "String 2" +#~ msgstr "" + + + diff --git a/rosetta/tests/django.po.template b/rosetta/tests/django.po.template new file mode 100644 index 00000000..3ae3af87 --- /dev/null +++ b/rosetta/tests/django.po.template @@ -0,0 +1,37 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: Rosetta\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2009-10-21 12:21+0200\n" +"PO-Revision-Date: 2008-09-22 11:02\n" +"Last-Translator: Admin Admin \n" +"Language-Team: French \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Translated-Using: django-rosetta 0.4.RC2\n" + + +msgid "String 1" +msgstr "" + +msgid "String 2" +msgstr "" + +#. Translators: This is a text of the base template +#: templates/base.html:43 +msgid "String 3 with comment" +msgstr "" + +msgctxt "Context hint" +msgid "String 4" +msgstr "" + + + + diff --git a/rosetta/tests/urls.py b/rosetta/tests/urls.py new file mode 100644 index 00000000..0f9c1991 --- /dev/null +++ b/rosetta/tests/urls.py @@ -0,0 +1,5 @@ +from django.conf.urls.defaults import * +urlpatterns = patterns('', + url(r'^rosetta/',include('rosetta.urls')), + url(r'^admin/$','rosetta.tests.views.dummy', name='dummy-login') +) diff --git a/rosetta/tests/views.py b/rosetta/tests/views.py new file mode 100644 index 00000000..9bc5b730 --- /dev/null +++ b/rosetta/tests/views.py @@ -0,0 +1,3 @@ +def dummy(request): + pass + diff --git a/rosetta/urls.py b/rosetta/urls.py new file mode 100644 index 00000000..07314e09 --- /dev/null +++ b/rosetta/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * +urlpatterns = patterns('rosetta.views', + url(r'^$', 'home', name='rosetta-home'), + url(r'^pick/$', 'list_languages', name='rosetta-pick-file'), + url(r'^download/$', 'download_file', name='rosetta-download-file'), + url(r'^select/(?P[\w\-]+)/(?P\d+)/$','lang_sel', name='rosetta-language-selection'), +) diff --git a/rosetta/views.py b/rosetta/views.py new file mode 100644 index 00000000..5b3960f5 --- /dev/null +++ b/rosetta/views.py @@ -0,0 +1,366 @@ +from django.conf import settings +from django.contrib.auth.decorators import user_passes_test +from django.core.paginator import Paginator +from django.core.urlresolvers import reverse +from django.http import Http404, HttpResponseRedirect, HttpResponse +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.utils.encoding import smart_unicode, iri_to_uri +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.cache import never_cache +from rosetta.conf import settings as rosetta_settings +from rosetta.polib import pofile +from rosetta.poutil import find_pos, pagination_range +from rosetta.signals import entry_changed, post_save +import re, rosetta, datetime, unicodedata, hashlib, os + + +def home(request): + """ + Displays a list of messages to be translated + """ + + def fix_nls(in_,out_): + """Fixes submitted translations by filtering carriage returns and pairing + newlines at the begging and end of the translated string with the original + """ + if 0 == len(in_) or 0 == len(out_): + return out_ + + if "\r" in out_ and "\r" not in in_: + out_=out_.replace("\r",'') + + if "\n" == in_[0] and "\n" != out_[0]: + out_ = "\n" + out_ + elif "\n" != in_[0] and "\n" == out_[0]: + out_ = out_.lstrip() + if "\n" == in_[-1] and "\n" != out_[-1]: + out_ = out_ + "\n" + elif "\n" != in_[-1] and "\n" == out_[-1]: + out_ = out_.rstrip() + return out_ + + version = rosetta.get_version(True) + if 'rosetta_i18n_fn' in request.session: + rosetta_i18n_fn=request.session.get('rosetta_i18n_fn') + rosetta_i18n_app = get_app_name(rosetta_i18n_fn) + rosetta_i18n_lang_code = request.session['rosetta_i18n_lang_code'] + rosetta_i18n_lang_bidi = rosetta_i18n_lang_code.split('-')[0] in settings.LANGUAGES_BIDI + rosetta_i18n_write = request.session.get('rosetta_i18n_write', True) + if rosetta_i18n_write: + rosetta_i18n_pofile = pofile(rosetta_i18n_fn) + for entry in rosetta_i18n_pofile: + entry.md5hash = hashlib.md5(entry.msgid.encode("utf8")+entry.msgstr.encode("utf8")).hexdigest() + + else: + rosetta_i18n_pofile = request.session.get('rosetta_i18n_pofile') + + + if 'filter' in request.GET: + if request.GET.get('filter') in ('untranslated', 'translated', 'fuzzy', 'all'): + filter_ = request.GET.get('filter') + request.session['rosetta_i18n_filter'] = filter_ + return HttpResponseRedirect(reverse('rosetta-home')) + + rosetta_i18n_filter = request.session.get('rosetta_i18n_filter', 'all') + + if '_next' in request.POST: + rx = re.compile(r'^m_([0-9a-f]+)') + rx_plural = re.compile(r'^m_([0-9a-f]+)_([0-9]+)') + file_change = False + for key, value in request.POST.items(): + md5hash = None + plural_id = None + + if rx_plural.match(key): + md5hash = str(rx_plural.match(key).groups()[0]) + # polib parses .po files into unicode strings, but + # doesn't bother to convert plural indexes to int, + # so we need unicode here. + plural_id = unicode(rx_plural.match(key).groups()[1]) + + elif rx.match(key): + md5hash = str(rx.match(key).groups()[0]) + + + if md5hash is not None: + entry = rosetta_i18n_pofile.find(md5hash, 'md5hash') + # If someone did a makemessage, some entries might + # have been removed, so we need to check. + if entry: + old_msgstr = entry.msgstr + + + if plural_id is not None: + plural_string = fix_nls(entry.msgstr_plural[plural_id], value) + entry.msgstr_plural[plural_id] = plural_string + else: + entry.msgstr = fix_nls(entry.msgid, value) + + is_fuzzy = bool(request.POST.get('f_%s' % md5hash, False)) + old_fuzzy = 'fuzzy' in entry.flags + + if old_fuzzy and not is_fuzzy: + entry.flags.remove('fuzzy') + elif not old_fuzzy and is_fuzzy: + entry.flags.append('fuzzy') + + file_change = True + + if old_msgstr != value or old_fuzzy != is_fuzzy: + entry_changed.send(sender=entry, + user=request.user, + old_msgstr = old_msgstr, + old_fuzzy = old_fuzzy, + pofile = rosetta_i18n_fn, + language_code = rosetta_i18n_lang_code, + ) + + else: + request.session['rosetta_last_save_error'] = True + + + + if file_change and rosetta_i18n_write: + + try: + rosetta_i18n_pofile.metadata['Last-Translator'] = unicodedata.normalize('NFKD', u"%s %s <%s>" %(request.user.first_name,request.user.last_name,request.user.email)).encode('ascii', 'ignore') + rosetta_i18n_pofile.metadata['X-Translated-Using'] = u"django-rosetta %s" % rosetta.get_version(False) + rosetta_i18n_pofile.metadata['PO-Revision-Date'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M%z') + except UnicodeDecodeError: + pass + try: + rosetta_i18n_pofile.save() + rosetta_i18n_pofile.save_as_mofile(rosetta_i18n_fn.replace('.po','.mo')) + + post_save.send(sender=None,language_code=rosetta_i18n_lang_code,request=request) + + + # Try auto-reloading via the WSGI daemon mode reload mechanism + if rosetta_settings.WSGI_AUTO_RELOAD and \ + request.environ.has_key('mod_wsgi.process_group') and \ + request.environ.get('mod_wsgi.process_group',None) and \ + request.environ.has_key('SCRIPT_FILENAME') and \ + int(request.environ.get('mod_wsgi.script_reloading', '0')): + try: + os.utime(request.environ.get('SCRIPT_FILENAME'), None) + except OSError: + pass + # Try auto-reloading via uwsgi daemon reload mechanism + if rosetta_settings.UWSGI_AUTO_RELOAD: + try: + import uwsgi + # pretty easy right? + uwsgi.reload() + except: + # we may not be running under uwsgi :P + pass + + except: + request.session['rosetta_i18n_write'] = False + + request.session['rosetta_i18n_pofile']=rosetta_i18n_pofile + + # Retain query arguments + query_arg = '' + if 'query' in request.REQUEST: + query_arg = '?query=%s' %request.REQUEST.get('query') + if 'page' in request.GET: + if query_arg: + query_arg = query_arg + '&' + else: + query_arg = '?' + query_arg = query_arg + 'page=%d' % int(request.GET.get('page')) + + + return HttpResponseRedirect(reverse('rosetta-home') + iri_to_uri(query_arg)) + + + rosetta_i18n_lang_name = _(request.session.get('rosetta_i18n_lang_name')) + rosetta_i18n_lang_code = request.session.get('rosetta_i18n_lang_code') + + if 'query' in request.REQUEST and request.REQUEST.get('query','').strip(): + query=request.REQUEST.get('query').strip() + rx=re.compile(re.escape(query), re.IGNORECASE) + paginator = Paginator([e for e in rosetta_i18n_pofile if not e.obsolete and rx.search(smart_unicode(e.msgstr)+smart_unicode(e.msgid)+u''.join([o[0] for o in e.occurrences]))], rosetta_settings.MESSAGES_PER_PAGE) + else: + if rosetta_i18n_filter == 'untranslated': + paginator = Paginator(rosetta_i18n_pofile.untranslated_entries(), rosetta_settings.MESSAGES_PER_PAGE) + elif rosetta_i18n_filter == 'translated': + paginator = Paginator(rosetta_i18n_pofile.translated_entries(), rosetta_settings.MESSAGES_PER_PAGE) + elif rosetta_i18n_filter == 'fuzzy': + paginator = Paginator([e for e in rosetta_i18n_pofile.fuzzy_entries() if not e.obsolete], rosetta_settings.MESSAGES_PER_PAGE) + else: + paginator = Paginator([e for e in rosetta_i18n_pofile if not e.obsolete], rosetta_settings.MESSAGES_PER_PAGE) + + if 'page' in request.GET and int(request.GET.get('page')) <= paginator.num_pages and int(request.GET.get('page')) > 0: + page = int(request.GET.get('page')) + else: + page = 1 + messages = paginator.page(page).object_list + if rosetta_settings.MAIN_LANGUAGE and rosetta_settings.MAIN_LANGUAGE != rosetta_i18n_lang_code: + + main_language = None + for language in settings.LANGUAGES: + if language[0] == rosetta_settings.MAIN_LANGUAGE: + main_language = _(language[1]) + break + + fl = ("/%s/" % rosetta_settings.MAIN_LANGUAGE).join(rosetta_i18n_fn.split("/%s/" % rosetta_i18n_lang_code)) + po = pofile(fl) + + main_messages = [] + for message in messages: + message.main_lang = po.find(message.msgid).msgstr + + needs_pagination = paginator.num_pages > 1 + if needs_pagination: + if paginator.num_pages >= 10: + page_range = pagination_range(1, paginator.num_pages, page) + else: + page_range = range(1,1+paginator.num_pages) + ADMIN_MEDIA_PREFIX = settings.ADMIN_MEDIA_PREFIX + ENABLE_TRANSLATION_SUGGESTIONS = rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS + + MESSAGES_SOURCE_LANGUAGE_NAME = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_NAME + MESSAGES_SOURCE_LANGUAGE_CODE = rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE + + if 'rosetta_last_save_error' in request.session: + del(request.session['rosetta_last_save_error']) + rosetta_last_save_error = True + + + return render_to_response('rosetta/pofile.html', locals(), context_instance=RequestContext(request)) + + + else: + return list_languages(request) +home=user_passes_test(lambda user:can_translate(user),settings.LOGIN_URL)(home) +home=never_cache(home) + + +def download_file(request): + import zipfile, os + from StringIO import StringIO + # original filename + rosetta_i18n_fn=request.session.get('rosetta_i18n_fn', None) + # in-session modified catalog + rosetta_i18n_pofile = request.session.get('rosetta_i18n_pofile', None) + # language code + rosetta_i18n_lang_code = request.session.get('rosetta_i18n_lang_code', None) + + if not rosetta_i18n_lang_code or not rosetta_i18n_pofile or not rosetta_i18n_fn: + return HttpResponseRedirect(reverse('rosetta-home')) + try: + if len(rosetta_i18n_fn.split('/')) >= 5: + offered_fn = '_'.join(rosetta_i18n_fn.split('/')[-5:]) + else: + offered_fn = rosetta_i18n_fn.split('/')[-1] + po_fn = str(rosetta_i18n_fn.split('/')[-1]) + mo_fn = str(po_fn.replace('.po','.mo')) # not so smart, huh + zipdata = StringIO() + zipf = zipfile.ZipFile(zipdata, mode="w") + zipf.writestr(po_fn, unicode(rosetta_i18n_pofile).encode("utf8")) + zipf.writestr(mo_fn, rosetta_i18n_pofile.to_binary()) + zipf.close() + zipdata.seek(0) + + response = HttpResponse(zipdata.read()) + response['Content-Disposition'] = 'attachment; filename=%s.%s.zip' %(offered_fn,rosetta_i18n_lang_code) + response['Content-Type'] = 'application/x-zip' + return response + except Exception, e: + + return HttpResponseRedirect(reverse('rosetta-home')) + +download_file=user_passes_test(lambda user:can_translate(user),settings.LOGIN_URL)(download_file) +download_file=never_cache(download_file) + + + +def list_languages(request): + """ + Lists the languages for the current project, the gettext catalog files + that can be translated and their translation progress + """ + languages = [] + + if 'filter' in request.GET: + if request.GET.get('filter') in ('project', 'third-party', 'django', 'all'): + filter_ = request.GET.get('filter') + request.session['rosetta_i18n_catalog_filter'] = filter_ + return HttpResponseRedirect(reverse('rosetta-pick-file')) + + rosetta_i18n_catalog_filter = request.session.get('rosetta_i18n_catalog_filter', 'project') + + third_party_apps = rosetta_i18n_catalog_filter in ('all', 'third-party') + django_apps = rosetta_i18n_catalog_filter in ('all', 'django') + project_apps = rosetta_i18n_catalog_filter in ('all', 'project') + + has_pos = False + for language in settings.LANGUAGES: + pos = find_pos(language[0], project_apps=project_apps,django_apps=django_apps,third_party_apps=third_party_apps) + has_pos = has_pos or len(pos) + languages.append( + (language[0], + _(language[1]), + [(get_app_name(l), os.path.realpath(l), pofile(l)) for l in pos], + ) + ) + ADMIN_MEDIA_PREFIX = settings.ADMIN_MEDIA_PREFIX + version = rosetta.get_version(True) + return render_to_response('rosetta/languages.html', locals(), context_instance=RequestContext(request)) +list_languages=user_passes_test(lambda user:can_translate(user),settings.LOGIN_URL)(list_languages) +list_languages=never_cache(list_languages) + +def get_app_name(path): + app = path.split("/locale")[0].split("/")[-1] + return app + +def lang_sel(request,langid,idx): + """ + Selects a file to be translated + """ + if langid not in [l[0] for l in settings.LANGUAGES]: + raise Http404 + else: + + rosetta_i18n_catalog_filter = request.session.get('rosetta_i18n_catalog_filter', 'project') + + third_party_apps = rosetta_i18n_catalog_filter in ('all', 'third-party') + django_apps = rosetta_i18n_catalog_filter in ('all', 'django') + project_apps = rosetta_i18n_catalog_filter in ('all', 'project') + + file_ = find_pos(langid, project_apps=project_apps,django_apps=django_apps,third_party_apps=third_party_apps)[int(idx)] + + request.session['rosetta_i18n_lang_code'] = langid + request.session['rosetta_i18n_lang_name'] = unicode([l[1] for l in settings.LANGUAGES if l[0] == langid][0]) + request.session['rosetta_i18n_fn'] = file_ + po = pofile(file_) + for entry in po: + entry.md5hash = hashlib.md5(entry.msgid.encode("utf8")+entry.msgstr.encode("utf8")).hexdigest() + + + request.session['rosetta_i18n_pofile'] = po + try: + os.utime(file_,None) + request.session['rosetta_i18n_write'] = True + except OSError: + request.session['rosetta_i18n_write'] = False + + return HttpResponseRedirect(reverse('rosetta-home')) +lang_sel=user_passes_test(lambda user:can_translate(user),settings.LOGIN_URL)(lang_sel) +lang_sel=never_cache(lang_sel) + +def can_translate(user): + if not user.is_authenticated(): + return False + elif user.is_superuser and user.is_staff: + return True + else: + try: + from django.contrib.auth.models import Group + translators = Group.objects.get(name='translators') + return translators in user.groups.all() + except Group.DoesNotExist: + return False diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..f905c527 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +from setuptools import setup, find_packages +setup( + name='django-rosetta', + version=__import__('rosetta').get_version(limit=3), + description='A Django application that eases the translation of Django projects', + author='Marco Bonetti', + author_email='mbonetti@gmail.com', + url='http://code.google.com/p/django-rosetta/', + license='MIT', + packages=find_packages(), + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Software Development :: Localization', + 'Topic :: Software Development :: Internationalization', + 'Framework :: Django', + ], + include_package_data=True, + zip_safe=False, + install_requires=['setuptools'] +)