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',