diff --git a/ckan/i18n/check_po_files.py b/ckan/i18n/check_po_files.py index 2d366c6b9ae..16352254e34 100755 --- a/ckan/i18n/check_po_files.py +++ b/ckan/i18n/check_po_files.py @@ -10,9 +10,11 @@ pip install polib ''' +import polib import re import paste.script.command + def simple_conv_specs(s): '''Return the simple Python string conversion specifiers in the string s. @@ -24,27 +26,6 @@ def simple_conv_specs(s): simple_conv_specs_re = re.compile('\%\w') return simple_conv_specs_re.findall(s) -def test_simple_conv_specs(): - assert simple_conv_specs("Authorization function not found: %s") == ( - ['%s']) - assert simple_conv_specs("Problem purging revision %s: %s") == ( - ['%s', '%s']) - assert simple_conv_specs( - "Cannot create new entity of this type: %s %s") == ['%s', '%s'] - assert simple_conv_specs("Could not read parameters: %r") == ['%r'] - assert simple_conv_specs("User %r not authorized to edit %r") == ( - ['%r', '%r']) - assert simple_conv_specs( - "Please update your profile and add your email " - "address and your full name. " - "%s uses your email address if you need to reset your password.") == ( - ['%s', '%s']) - assert simple_conv_specs( - "You can use %sMarkdown formatting%s here.") == ['%s', '%s'] - assert simple_conv_specs( - "Name must be a maximum of %i characters long") == ['%i'] - assert simple_conv_specs("Blah blah %s blah %(key)s blah %i") == ( - ['%s', '%i']) def mapping_keys(s): '''Return a sorted list of the mapping keys in the string s. @@ -57,20 +38,6 @@ def mapping_keys(s): mapping_keys_re = re.compile('\%\([^\)]*\)\w') return sorted(mapping_keys_re.findall(s)) -def test_mapping_keys(): - assert mapping_keys( - "You have requested your password on %(site_title)s to be reset.\n" - "\n" - "Please click the following link to confirm this request:\n" - "\n" - " %(reset_link)s\n") == ['%(reset_link)s', '%(site_title)s'] - assert mapping_keys( - "The input field %(name)s was not expected.") == ['%(name)s'] - assert mapping_keys( - "[1:You searched for \"%(query)s\". ]%(number_of_results)s " - "datasets found.") == ['%(number_of_results)s', '%(query)s'] - assert mapping_keys("Blah blah %s blah %(key)s blah %i") == ( - ['%(key)s']), mapping_keys("Blah blah %s blah %(key)s blah %i") def replacement_fields(s): '''Return a sorted list of the Python replacement fields in the string s. @@ -83,11 +50,6 @@ def replacement_fields(s): repl_fields_re = re.compile('\{[^\}]*\}') return sorted(repl_fields_re.findall(s)) -def test_replacement_fields(): - assert replacement_fields( - "{actor} added the tag {object} to the dataset {target}") == ( - ['{actor}', '{object}', '{target}']) - assert replacement_fields("{actor} updated their profile") == ['{actor}'] class CheckPoFiles(paste.script.command.Command): @@ -97,19 +59,38 @@ class CheckPoFiles(paste.script.command.Command): parser = paste.script.command.Command.standard_parser(verbose=True) def command(self): - import polib - test_simple_conv_specs() - test_mapping_keys() - test_replacement_fields() for path in self.args: print u'Checking file {}'.format(path) - po = polib.pofile(path) - for entry in po.translated_entries(): - if not entry.msgstr: - continue - for function in (simple_conv_specs, mapping_keys, - replacement_fields): - if not function(entry.msgid) == function(entry.msgstr): - print " Format specifiers don't match:" - print u' {0} -> {1}'.format(entry.msgid, entry.msgstr).encode('latin7', 'ignore') + errors = check_po_file(path) + if errors: + for msgid, msgstr in errors: + print 'Format specifiers don\'t match:' + print u' {0} -> {1}'.format(entry.msgid, entry.msgstr).encode('latin7', 'ignore') + + +def check_po_file(path): + errors = [] + + def check_translation(validator, msgid, msgstr): + if not validator(msgid) == validator(msgstr): + errors.append((msgid, msgstr)) + + po = polib.pofile(path) + for entry in po.translated_entries(): + if entry.msgid_plural and entry.msgstr_plural: + for function in (simple_conv_specs, mapping_keys, + replacement_fields): + for key, msgstr in entry.msgstr_plural.iteritems(): + if key == '0': + check_translation(function, entry.msgid, + entry.msgstr_plural[key]) + else: + check_translation(function, entry.msgid_plural, + entry.msgstr_plural[key]) + elif entry.msgstr: + for function in (simple_conv_specs, mapping_keys, + replacement_fields): + check_translation(function, entry.msgid, entry.msgstr) + + return errors diff --git a/ckan/lib/email_notifications.py b/ckan/lib/email_notifications.py index 7f24b978285..a6e7d8dab62 100644 --- a/ckan/lib/email_notifications.py +++ b/ckan/lib/email_notifications.py @@ -98,7 +98,7 @@ def _notifications_for_activities(activities, user_dict): # certain types of activity to be sent in their own individual emails, # etc. subject = ungettext( - "1 new activity from {site_title}", + "{n} new activity from {site_title}", "{n} new activities from {site_title}", len(activities)).format( site_title=pylons.config.get('ckan.site_title'), diff --git a/ckan/templates/activity_streams/activity_stream_email_notifications.text b/ckan/templates/activity_streams/activity_stream_email_notifications.text index 36851ad8487..951f3abde48 100644 --- a/ckan/templates/activity_streams/activity_stream_email_notifications.text +++ b/ckan/templates/activity_streams/activity_stream_email_notifications.text @@ -1,4 +1,4 @@ -{% set num = activities|length %}{{ ungettext("You have 1 new activity on your {site_title} dashboard", "You have {num} new activities on your {site_title} dashboard", num).format(site_title=g.site_title, num=num) }} {{ _('To view your dashboard, click on this link:') }} +{% set num = activities|length %}{{ ungettext("You have {num} new activity on your {site_title} dashboard", "You have {num} new activities on your {site_title} dashboard", num).format(site_title=g.site_title, num=num) }} {{ _('To view your dashboard, click on this link:') }} {{ g.site_url + '/dashboard' }} diff --git a/ckan/tests/i18n/test_check_po_files.py b/ckan/tests/i18n/test_check_po_files.py new file mode 100644 index 00000000000..4e226940ee8 --- /dev/null +++ b/ckan/tests/i18n/test_check_po_files.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +import nose + +from ckan.i18n.check_po_files import (check_po_file, + simple_conv_specs, + mapping_keys, + replacement_fields) + +eq_ = nose.tools.eq_ + + +PO_OK = ''' +#: ckan/lib/formatters.py:57 +msgid "November" +msgstr "Noiembrie" + +#: ckan/lib/formatters.py:61 +msgid "December" +msgstr "Decembrie" +''' + +PO_WRONG = ''' +#: ckan/templates/snippets/search_result_text.html:15 +msgid "{number} dataset found for {query}" +msgstr "צביר נתונים אחד נמצא עבור {query}" +''' + +PO_PLURALS_OK = ''' +#: ckan/lib/formatters.py:114 +msgid "{hours} hour ago" +msgid_plural "{hours} hours ago" +msgstr[0] "Fa {hours} hora" +msgstr[1] "Fa {hours} hores" +''' + +PO_WRONG_PLURALS = ''' +#: ckan/lib/formatters.py:114 +msgid "{hours} hour ago" +msgid_plural "{hours} hours ago" +msgstr[0] "o oră în urmă" +msgstr[1] "cîteva ore în urmă" +msgstr[2] "{hours} ore în urmă" +''' + + +class TestCheckPoFiles(object): + + def test_basic(self): + + errors = check_po_file(PO_OK) + + eq_(errors, []) + + def test_wrong(self): + + errors = check_po_file(PO_WRONG) + + eq_(len(errors), 1) + + eq_(errors[0][0], '{number} dataset found for {query}') + + def test_plurals_ok(self): + + errors = check_po_file(PO_PLURALS_OK) + + eq_(errors, []) + + def test_wrong_plurals(self): + + errors = check_po_file(PO_WRONG_PLURALS) + + eq_(len(errors), 2) + + for error in errors: + assert error[0] in ('{hours} hour ago', '{hours} hours ago') + + +class TestValidators(object): + + def test_simple_conv_specs(self): + eq_(simple_conv_specs("Authorization function not found: %s"), + (['%s'])) + eq_(simple_conv_specs("Problem purging revision %s: %s"), + (['%s', '%s'])) + eq_(simple_conv_specs("Cannot create new entity of this type: %s %s"), + ['%s', '%s']) + eq_(simple_conv_specs("Could not read parameters: %r"), ['%r']) + eq_(simple_conv_specs("User %r not authorized to edit %r"), + (['%r', '%r'])) + eq_(simple_conv_specs( + "Please update your profile and add your email " + "address and your full name. " + "%s uses your email address if you need to reset your password."), + (['%s', '%s'])) + eq_(simple_conv_specs("You can use %sMarkdown formatting%s here."), + ['%s', '%s']) + eq_(simple_conv_specs("Name must be a maximum of %i characters long"), + ['%i']) + eq_(simple_conv_specs("Blah blah %s blah %(key)s blah %i"), + (['%s', '%i'])) + + def test_replacement_fields(self): + eq_(replacement_fields( + "{actor} added the tag {object} to the dataset {target}"), + (['{actor}', '{object}', '{target}'])) + eq_(replacement_fields("{actor} updated their profile"), ['{actor}']) + + def test_mapping_keys(self): + eq_(mapping_keys( + "You have requested your password on %(site_title)s to be reset.\n" + "\n" + "Please click the following link to confirm this request:\n" + "\n" + " %(reset_link)s\n"), + ['%(reset_link)s', '%(site_title)s']) + eq_(mapping_keys( + "The input field %(name)s was not expected."), + ['%(name)s']) + eq_(mapping_keys( + "[1:You searched for \"%(query)s\". ]%(number_of_results)s " + "datasets found."), + ['%(number_of_results)s', '%(query)s']) + eq_(mapping_keys("Blah blah %s blah %(key)s blah %i"), + (['%(key)s']), mapping_keys("Blah blah %s blah %(key)s blah %i")) diff --git a/ckan/tests/legacy/test_coding_standards.py b/ckan/tests/legacy/test_coding_standards.py index 36fe5fb22fc..f008762b9aa 100644 --- a/ckan/tests/legacy/test_coding_standards.py +++ b/ckan/tests/legacy/test_coding_standards.py @@ -379,7 +379,6 @@ class TestPep8(object): 'ckan/controllers/admin.py', 'ckan/controllers/revision.py', 'ckan/exceptions.py', - 'ckan/i18n/check_po_files.py', 'ckan/include/rcssmin.py', 'ckan/include/rjsmin.py', 'ckan/lib/activity_streams.py',