diff --git a/AUTHORS b/AUTHORS
index 4632c66a62e83..aef3caa812f25 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -766,6 +766,7 @@ answer newbie questions, and generally made Django that much better:
Rob Hudson
Rob Nguyen
Robin Munn
+ Rodrigo Gadea
Rodrigo Pinheiro Marques de Araújo
Romain Garrigues
Ronny Haryanto
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py
index 830ba25408b41..006d04ce29390 100644
--- a/django/conf/global_settings.py
+++ b/django/conf/global_settings.py
@@ -151,6 +151,11 @@ def gettext_noop(s):
USE_I18N = True
LOCALE_PATHS = []
+# If you set this to True, Django will issue a warning when merging a message
+# file for translations with different plural forms than the main ones of the
+# locale.
+PLURAL_FORMS_CONSISTENCY = False
+
# Settings for language cookie
LANGUAGE_COOKIE_NAME = 'django_language'
LANGUAGE_COOKIE_AGE = None
diff --git a/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po b/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
index 2b335c932556d..f5e1edcc53770 100644
--- a/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
+++ b/django/contrib/admin/locale/en/LC_MESSAGES/djangojs.po
@@ -12,6 +12,7 @@ msgstr ""
"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"
#: contrib/admin/static/admin/js/SelectFilter2.js:47
#, javascript-format
diff --git a/django/contrib/admin/locale/kn/LC_MESSAGES/django.mo b/django/contrib/admin/locale/kn/LC_MESSAGES/django.mo
index 3740da20869e1..51248ece22333 100644
Binary files a/django/contrib/admin/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/admin/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/admin/locale/kn/LC_MESSAGES/django.po b/django/contrib/admin/locale/kn/LC_MESSAGES/django.po
index 3ae96cfa6791b..0e16ea62a5eac 100644
--- a/django/contrib/admin/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/admin/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#, python-format
msgid "Successfully deleted %(count)d %(items)s."
@@ -221,11 +221,13 @@ msgstr "ದತ್ತಸಂಚಯದ ದೋಷ"
msgid "%(count)s %(name)s was changed successfully."
msgid_plural "%(count)s %(name)s were changed successfully."
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(total_count)s selected"
msgid_plural "All %(total_count)s selected"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "0 of %(cnt)s selected"
@@ -511,6 +513,7 @@ msgstr ""
msgid "%(counter)s result"
msgid_plural "%(counter)s results"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(full_result_count)s total"
diff --git a/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.mo b/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.mo
index 988728ce948e9..ab3a19dab2af1 100644
Binary files a/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.mo and b/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.mo differ
diff --git a/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.po b/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.po
index 90363b7a2cf94..71e3e880eb439 100644
--- a/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.po
+++ b/django/contrib/admin/locale/kn/LC_MESSAGES/djangojs.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#, javascript-format
msgid "Available %s"
@@ -68,6 +68,7 @@ msgstr ""
msgid "%(sel)s of %(cnt)s selected"
msgid_plural "%(sel)s of %(cnt)s selected"
msgstr[0] ""
+msgstr[1] ""
msgid ""
"You have unsaved changes on individual editable fields. If you run an "
@@ -92,11 +93,13 @@ msgstr ""
msgid "Note: You are %s hour ahead of server time."
msgid_plural "Note: You are %s hours ahead of server time."
msgstr[0] ""
+msgstr[1] ""
#, javascript-format
msgid "Note: You are %s hour behind server time."
msgid_plural "Note: You are %s hours behind server time."
msgstr[0] ""
+msgstr[1] ""
msgid "Now"
msgstr "ಈಗ"
diff --git a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po
index ebbbd3abd9470..75ec91bd425a4 100644
--- a/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/admindocs/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-08 17:27+0200\n"
+"POT-Creation-Date: 2020-01-05 22:32-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,288 +12,201 @@ msgstr ""
"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"
-#: contrib/admindocs/apps.py:7
msgid "Administrative Documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:6
-#: contrib/admindocs/templates/admin_doc/index.html:6
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:6
-#: contrib/admindocs/templates/admin_doc/model_detail.html:14
-#: contrib/admindocs/templates/admin_doc/model_index.html:8
-#: contrib/admindocs/templates/admin_doc/template_detail.html:6
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:7
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:7
-#: contrib/admindocs/templates/admin_doc/view_detail.html:6
-#: contrib/admindocs/templates/admin_doc/view_index.html:7
msgid "Home"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:7
-#: contrib/admindocs/templates/admin_doc/index.html:7
-#: contrib/admindocs/templates/admin_doc/index.html:10
-#: contrib/admindocs/templates/admin_doc/index.html:14
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:7
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:14
-#: contrib/admindocs/templates/admin_doc/model_detail.html:15
-#: contrib/admindocs/templates/admin_doc/model_index.html:9
-#: contrib/admindocs/templates/admin_doc/template_detail.html:7
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:8
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:8
-#: contrib/admindocs/templates/admin_doc/view_detail.html:7
-#: contrib/admindocs/templates/admin_doc/view_index.html:8
msgid "Documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:8
-#: contrib/admindocs/templates/admin_doc/index.html:29
msgid "Bookmarklets"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:11
msgid "Documentation bookmarklets"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:15
msgid ""
"To install bookmarklets, drag the link to your bookmarks toolbar, or right-"
"click the link and add it to your bookmarks. Now you can select the "
"bookmarklet from any page in the site."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:22
msgid "Documentation for this page"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/bookmarklets.html:23
msgid ""
"Jumps you from any page to the documentation for the view that generates "
"that page."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:17
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:9
msgid "Tags"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:18
msgid "List of all the template tags and their functions."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:20
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:9
msgid "Filters"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:21
msgid ""
"Filters are actions which can be applied to variables in a template to alter "
"the output."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:23
-#: contrib/admindocs/templates/admin_doc/model_detail.html:16
-#: contrib/admindocs/templates/admin_doc/model_index.html:10
-#: contrib/admindocs/templates/admin_doc/model_index.html:14
msgid "Models"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:24
msgid ""
"Models are descriptions of all the objects in the system and their "
"associated fields. Each model has a list of fields which can be accessed as "
"template variables"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:26
-#: contrib/admindocs/templates/admin_doc/view_detail.html:8
-#: contrib/admindocs/templates/admin_doc/view_index.html:9
-#: contrib/admindocs/templates/admin_doc/view_index.html:12
msgid "Views"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:27
msgid ""
"Each page on the public site is generated by a view. The view defines which "
"template is used to generate the page and which objects are available to "
"that template."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/index.html:30
msgid "Tools for your browser to quickly access admin functionality."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:10
msgid "Please install docutils"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:17
#, python-format
msgid ""
"The admin documentation system requires Python's docutils library."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/missing_docutils.html:19
#, python-format
msgid ""
"Please ask your administrators to install docutils."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:21
#, python-format
msgid "Model: %(name)s"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:30
msgid "Fields"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:35
msgid "Field"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:36
msgid "Type"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:37
-#: contrib/admindocs/templates/admin_doc/model_detail.html:60
msgid "Description"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:53
msgid "Methods with arguments"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:58
msgid "Method"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:59
msgid "Arguments"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_detail.html:76
msgid "Back to Model documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_index.html:18
msgid "Model documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/model_index.html:43
msgid "Model groups"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_detail.html:8
msgid "Templates"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_detail.html:13
#, python-format
msgid "Template: %(name)s"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_detail.html:16
#, python-format
msgid "Template: %(name)s
"
msgstr ""
#. Translators: Search is not a verb here, it qualifies path (a search path)
-#: contrib/admindocs/templates/admin_doc/template_detail.html:19
#, python-format
msgid "Search path for template %(name)s
:"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_detail.html:22
msgid "(does not exist)"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_detail.html:26
msgid "Back to Documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:12
msgid "Template filters"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:16
msgid "Template filter documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:22
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:43
msgid "Built-in filters"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_filter_index.html:23
#, python-format
msgid ""
"To use these filters, put %(code)s
in your template before "
"using the filter."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:12
msgid "Template tags"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:16
msgid "Template tag documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:22
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:43
msgid "Built-in tags"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/template_tag_index.html:23
#, python-format
msgid ""
"To use these tags, put %(code)s
in your template before using "
"the tag."
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_detail.html:12
#, python-format
msgid "View: %(name)s"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_detail.html:23
msgid "Context:"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_detail.html:28
msgid "Templates:"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_detail.html:32
msgid "Back to View documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:16
msgid "View documentation"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:22
msgid "Jump to namespace"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:27
msgid "Empty namespace"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:40
#, python-format
msgid "Views by namespace %(name)s"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:42
msgid "Views by empty namespace"
msgstr ""
-#: contrib/admindocs/templates/admin_doc/view_index.html:49
#, python-format
msgid ""
"\n"
@@ -301,59 +214,42 @@ msgid ""
"code>.\n"
msgstr ""
-#: contrib/admindocs/views.py:71 contrib/admindocs/views.py:72
-#: contrib/admindocs/views.py:74
msgid "tag:"
msgstr ""
-#: contrib/admindocs/views.py:102 contrib/admindocs/views.py:103
-#: contrib/admindocs/views.py:105
msgid "filter:"
msgstr ""
-#: contrib/admindocs/views.py:161 contrib/admindocs/views.py:162
-#: contrib/admindocs/views.py:164
msgid "view:"
msgstr ""
-#: contrib/admindocs/views.py:191
#, python-format
msgid "App %(app_label)r not found"
msgstr ""
-#: contrib/admindocs/views.py:195
#, python-format
msgid "Model %(model_name)r not found in app %(app_label)r"
msgstr ""
-#: contrib/admindocs/views.py:200 contrib/admindocs/views.py:201
-#: contrib/admindocs/views.py:216 contrib/admindocs/views.py:239
-#: contrib/admindocs/views.py:244 contrib/admindocs/views.py:259
-#: contrib/admindocs/views.py:300 contrib/admindocs/views.py:305
msgid "model:"
msgstr ""
-#: contrib/admindocs/views.py:212
#, python-format
msgid "the related `%(app_label)s.%(data_type)s` object"
msgstr ""
-#: contrib/admindocs/views.py:232 contrib/admindocs/views.py:292
#, python-format
msgid "related `%(app_label)s.%(object_name)s` objects"
msgstr ""
-#: contrib/admindocs/views.py:239 contrib/admindocs/views.py:300
#, python-format
msgid "all %s"
msgstr ""
-#: contrib/admindocs/views.py:244 contrib/admindocs/views.py:305
#, python-format
msgid "number of %s"
msgstr ""
-#: contrib/admindocs/views.py:398
#, python-format
msgid "%s does not appear to be a urlpattern object"
msgstr ""
diff --git a/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.mo b/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.mo
index 1c112d5364747..6864131dc714d 100644
Binary files a/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.po b/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.po
index e1a900df0d40f..7d6743620c107 100644
--- a/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/admindocs/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Administrative Documentation"
msgstr ""
diff --git a/django/contrib/auth/locale/kn/LC_MESSAGES/django.mo b/django/contrib/auth/locale/kn/LC_MESSAGES/django.mo
index be5a6deede908..005a31be43f76 100644
Binary files a/django/contrib/auth/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/auth/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/auth/locale/kn/LC_MESSAGES/django.po b/django/contrib/auth/locale/kn/LC_MESSAGES/django.po
index f1b7174d08994..e1784d2a3c73a 100644
--- a/django/contrib/auth/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/auth/locale/kn/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Personal info"
msgstr "ವೈಯುಕ್ತಿಕ ಮಾಹಿತಿ"
@@ -221,11 +221,13 @@ msgid_plural ""
"This password is too short. It must contain at least %(min_length)d "
"characters."
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "Your password must contain at least %(min_length)d character."
msgid_plural "Your password must contain at least %(min_length)d characters."
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "The password is too similar to the %(verbose_name)s."
diff --git a/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po b/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po
index cee02371410a4..bf55304e4d55f 100644
--- a/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/contenttypes/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-08 17:27+0200\n"
+"POT-Creation-Date: 2019-12-31 17:33-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,6 +12,7 @@ msgstr ""
"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"
#: contrib/contenttypes/apps.py:16
msgid "Content Types"
diff --git a/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.mo b/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.mo
index 55454596ee3e5..760e3f8d5c188 100644
Binary files a/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.po b/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.po
index 6c5c82121ebee..e2f139688cd9f 100644
--- a/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/contenttypes/locale/ka/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Content Types"
msgstr "კონტენტის ტიპები"
diff --git a/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.mo b/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.mo
index 00951f1cb484e..8295af4955470 100644
Binary files a/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.po b/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.po
index 10e5ae6adcfe7..d4bde118f9145 100644
--- a/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/contenttypes/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Content Types"
msgstr ""
diff --git a/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po b/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po
index 8fe158ead1536..2bfdf8bd726bb 100644
--- a/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/flatpages/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-08 17:27+0200\n"
+"POT-Creation-Date: 2019-12-31 17:33-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,6 +12,7 @@ msgstr ""
"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"
#: contrib/flatpages/admin.py:12
msgid "Advanced options"
diff --git a/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.mo b/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.mo
index 091b0afc48bba..73040091050c6 100644
Binary files a/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.mo and b/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.po b/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.po
index 0412ace95b8a4..c3c56c7bc8e03 100644
--- a/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.po
+++ b/django/contrib/flatpages/locale/fa/LC_MESSAGES/django.po
@@ -18,7 +18,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fa\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Advanced options"
msgstr "گزینههای پیشرفته"
diff --git a/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.mo b/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.mo
index 9b20adf6c18ac..76f49ae9ed7bb 100644
Binary files a/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.po b/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.po
index 12dcb14058b43..3bf4bcc1403fa 100644
--- a/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/flatpages/locale/ka/LC_MESSAGES/django.po
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Advanced options"
msgstr "დამატებითი პარამეტრები"
diff --git a/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.mo b/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.mo
index ed64a6b861787..a06136827ed6e 100644
Binary files a/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.po b/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.po
index 9d86fb9fefa58..83198cc083818 100644
--- a/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/flatpages/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Advanced options"
msgstr ""
diff --git a/django/contrib/gis/locale/en/LC_MESSAGES/django.po b/django/contrib/gis/locale/en/LC_MESSAGES/django.po
index aed3a2f9ddb15..9859184a8c27b 100644
--- a/django/contrib/gis/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-08 17:27+0200\n"
+"POT-Creation-Date: 2019-12-31 17:33-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,6 +12,7 @@ msgstr ""
"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"
#: contrib/gis/apps.py:8
msgid "GIS"
diff --git a/django/contrib/gis/locale/ka/LC_MESSAGES/django.mo b/django/contrib/gis/locale/ka/LC_MESSAGES/django.mo
index b684ee95ae592..9ea3da3bb570b 100644
Binary files a/django/contrib/gis/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/gis/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/gis/locale/ka/LC_MESSAGES/django.po b/django/contrib/gis/locale/ka/LC_MESSAGES/django.po
index 52f95b8b277c5..31bb23c8d5b5c 100644
--- a/django/contrib/gis/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/ka/LC_MESSAGES/django.po
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "GIS"
msgstr ""
diff --git a/django/contrib/gis/locale/kk/LC_MESSAGES/django.mo b/django/contrib/gis/locale/kk/LC_MESSAGES/django.mo
index c7503014981e9..426ba82bd94b5 100644
Binary files a/django/contrib/gis/locale/kk/LC_MESSAGES/django.mo and b/django/contrib/gis/locale/kk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/gis/locale/kk/LC_MESSAGES/django.po b/django/contrib/gis/locale/kk/LC_MESSAGES/django.po
index cd5e947cfdf53..2666802e787ba 100644
--- a/django/contrib/gis/locale/kk/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/kk/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kk\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "GIS"
msgstr ""
diff --git a/django/contrib/gis/locale/kn/LC_MESSAGES/django.mo b/django/contrib/gis/locale/kn/LC_MESSAGES/django.mo
index be4f674140bee..a381f5b9b7153 100644
Binary files a/django/contrib/gis/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/gis/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/gis/locale/kn/LC_MESSAGES/django.po b/django/contrib/gis/locale/kn/LC_MESSAGES/django.po
index b2b29a8f0f385..0ec1b161b4770 100644
--- a/django/contrib/gis/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/kn/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "GIS"
msgstr ""
diff --git a/django/contrib/gis/locale/sk/LC_MESSAGES/django.mo b/django/contrib/gis/locale/sk/LC_MESSAGES/django.mo
index 7b3b58569b1e8..8d2bf2fd08418 100644
Binary files a/django/contrib/gis/locale/sk/LC_MESSAGES/django.mo and b/django/contrib/gis/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/gis/locale/sk/LC_MESSAGES/django.po b/django/contrib/gis/locale/sk/LC_MESSAGES/django.po
index 9ad9a85c8d34a..731d3262bf28a 100644
--- a/django/contrib/gis/locale/sk/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/sk/LC_MESSAGES/django.po
@@ -17,7 +17,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sk\n"
-"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
msgid "GIS"
msgstr "GIS"
diff --git a/django/contrib/gis/locale/uk/LC_MESSAGES/django.mo b/django/contrib/gis/locale/uk/LC_MESSAGES/django.mo
index 2437a6326bee3..56964d54a35dc 100644
Binary files a/django/contrib/gis/locale/uk/LC_MESSAGES/django.mo and b/django/contrib/gis/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/gis/locale/uk/LC_MESSAGES/django.po b/django/contrib/gis/locale/uk/LC_MESSAGES/django.po
index 1d6a3c05fd87f..8c1919eb23f7c 100644
--- a/django/contrib/gis/locale/uk/LC_MESSAGES/django.po
+++ b/django/contrib/gis/locale/uk/LC_MESSAGES/django.po
@@ -21,8 +21,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
msgid "GIS"
msgstr "ГІС"
diff --git a/django/contrib/humanize/locale/kn/LC_MESSAGES/django.mo b/django/contrib/humanize/locale/kn/LC_MESSAGES/django.mo
index b75cf46d95c92..9359e87e5a5c1 100644
Binary files a/django/contrib/humanize/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/humanize/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/humanize/locale/kn/LC_MESSAGES/django.po b/django/contrib/humanize/locale/kn/LC_MESSAGES/django.po
index b38dd8cc7609c..bdae525203cfc 100644
--- a/django/contrib/humanize/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/humanize/locale/kn/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Humanize"
msgstr ""
@@ -35,111 +35,133 @@ msgstr ""
msgid "%(value).1f million"
msgid_plural "%(value).1f million"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s million"
msgid_plural "%(value)s million"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f billion"
msgid_plural "%(value).1f billion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s billion"
msgid_plural "%(value)s billion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f trillion"
msgid_plural "%(value).1f trillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s trillion"
msgid_plural "%(value)s trillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f quadrillion"
msgid_plural "%(value).1f quadrillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s quadrillion"
msgid_plural "%(value)s quadrillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f quintillion"
msgid_plural "%(value).1f quintillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s quintillion"
msgid_plural "%(value)s quintillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f sextillion"
msgid_plural "%(value).1f sextillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s sextillion"
msgid_plural "%(value)s sextillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f septillion"
msgid_plural "%(value).1f septillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s septillion"
msgid_plural "%(value)s septillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f octillion"
msgid_plural "%(value).1f octillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s octillion"
msgid_plural "%(value)s octillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f nonillion"
msgid_plural "%(value).1f nonillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s nonillion"
msgid_plural "%(value)s nonillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f decillion"
msgid_plural "%(value).1f decillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s decillion"
msgid_plural "%(value)s decillion"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value).1f googol"
msgid_plural "%(value).1f googol"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgid "%(value)s googol"
msgid_plural "%(value)s googol"
msgstr[0] ""
+msgstr[1] ""
msgid "one"
msgstr ""
@@ -191,6 +213,7 @@ msgstr ""
msgid "a second ago"
msgid_plural "%(count)s seconds ago"
msgstr[0] ""
+msgstr[1] ""
#. Translators: please keep a non-breaking space (U+00A0)
#. between count and time unit.
@@ -198,6 +221,7 @@ msgstr[0] ""
msgid "a minute ago"
msgid_plural "%(count)s minutes ago"
msgstr[0] ""
+msgstr[1] ""
#. Translators: please keep a non-breaking space (U+00A0)
#. between count and time unit.
@@ -205,6 +229,7 @@ msgstr[0] ""
msgid "an hour ago"
msgid_plural "%(count)s hours ago"
msgstr[0] ""
+msgstr[1] ""
#, python-format
msgctxt "naturaltime"
@@ -217,6 +242,7 @@ msgstr ""
msgid "a second from now"
msgid_plural "%(count)s seconds from now"
msgstr[0] ""
+msgstr[1] ""
#. Translators: please keep a non-breaking space (U+00A0)
#. between count and time unit.
@@ -224,6 +250,7 @@ msgstr[0] ""
msgid "a minute from now"
msgid_plural "%(count)s minutes from now"
msgstr[0] ""
+msgstr[1] ""
#. Translators: please keep a non-breaking space (U+00A0)
#. between count and time unit.
@@ -231,3 +258,4 @@ msgstr[0] ""
msgid "an hour from now"
msgid_plural "%(count)s hours from now"
msgstr[0] ""
+msgstr[1] ""
diff --git a/django/contrib/postgres/locale/en/LC_MESSAGES/django.mo b/django/contrib/postgres/locale/en/LC_MESSAGES/django.mo
index 08a7b68596a8a..d7c1287a9422a 100644
Binary files a/django/contrib/postgres/locale/en/LC_MESSAGES/django.mo and b/django/contrib/postgres/locale/en/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/postgres/locale/en_GB/LC_MESSAGES/django.mo b/django/contrib/postgres/locale/en_GB/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..71cbdf3e9d8d5
Binary files /dev/null and b/django/contrib/postgres/locale/en_GB/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/postgres/locale/km/LC_MESSAGES/django.mo b/django/contrib/postgres/locale/km/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..314bedb17d5ca
Binary files /dev/null and b/django/contrib/postgres/locale/km/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/postgres/locale/ml/LC_MESSAGES/django.mo b/django/contrib/postgres/locale/ml/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..71cbdf3e9d8d5
Binary files /dev/null and b/django/contrib/postgres/locale/ml/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/en/LC_MESSAGES/django.po b/django/contrib/redirects/locale/en/LC_MESSAGES/django.po
index 5da48fb866892..996110b550527 100644
--- a/django/contrib/redirects/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-09-08 17:27+0200\n"
+"POT-Creation-Date: 2019-12-31 17:33-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,6 +12,7 @@ msgstr ""
"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"
#: contrib/redirects/apps.py:7
msgid "Redirects"
diff --git a/django/contrib/redirects/locale/fa/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/fa/LC_MESSAGES/django.mo
index 2969ccc727130..98acfd82a78b5 100644
Binary files a/django/contrib/redirects/locale/fa/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/fa/LC_MESSAGES/django.po b/django/contrib/redirects/locale/fa/LC_MESSAGES/django.po
index 10625cca863f1..c87ab16b7930a 100644
--- a/django/contrib/redirects/locale/fa/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/fa/LC_MESSAGES/django.po
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fa\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Redirects"
msgstr "باز-ارسالها"
diff --git a/django/contrib/redirects/locale/he/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/he/LC_MESSAGES/django.mo
index effbc33efc4c9..5cf696117fdd6 100644
Binary files a/django/contrib/redirects/locale/he/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/he/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/he/LC_MESSAGES/django.po b/django/contrib/redirects/locale/he/LC_MESSAGES/django.po
index 4d32ce1e8d9bb..6b5e0fba288a3 100644
--- a/django/contrib/redirects/locale/he/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/he/LC_MESSAGES/django.po
@@ -15,7 +15,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: he\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % "
+"1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
msgid "Redirects"
msgstr "הפניות"
diff --git a/django/contrib/redirects/locale/ka/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/ka/LC_MESSAGES/django.mo
index 475955eefe145..f9b02abd4afa2 100644
Binary files a/django/contrib/redirects/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/ka/LC_MESSAGES/django.po b/django/contrib/redirects/locale/ka/LC_MESSAGES/django.po
index a168d070f0e47..bf76c7a2c05a9 100644
--- a/django/contrib/redirects/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/ka/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Redirects"
msgstr "გადამისამართებები"
diff --git a/django/contrib/redirects/locale/kk/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/kk/LC_MESSAGES/django.mo
index fb9d36f3abb89..dbe7765429bd4 100644
Binary files a/django/contrib/redirects/locale/kk/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/kk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/kk/LC_MESSAGES/django.po b/django/contrib/redirects/locale/kk/LC_MESSAGES/django.po
index 91d2a9c4aea16..8c6af97d57060 100644
--- a/django/contrib/redirects/locale/kk/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/kk/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kk\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Redirects"
msgstr "Қайта бағыттаулар"
diff --git a/django/contrib/redirects/locale/kn/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/kn/LC_MESSAGES/django.mo
index a497d71c4134e..da36d536174a2 100644
Binary files a/django/contrib/redirects/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/kn/LC_MESSAGES/django.po b/django/contrib/redirects/locale/kn/LC_MESSAGES/django.po
index 214edc4e54f8d..40609952cd1d4 100644
--- a/django/contrib/redirects/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Redirects"
msgstr ""
diff --git a/django/contrib/redirects/locale/lt/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/lt/LC_MESSAGES/django.mo
index 294ce4c532e3d..d2a24806dd1df 100644
Binary files a/django/contrib/redirects/locale/lt/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/lt/LC_MESSAGES/django.po b/django/contrib/redirects/locale/lt/LC_MESSAGES/django.po
index 2d79101c35d7a..811c24887839e 100644
--- a/django/contrib/redirects/locale/lt/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/lt/LC_MESSAGES/django.po
@@ -16,8 +16,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lt\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n"
-"%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < "
+"11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? "
+"1 : n % 1 != 0 ? 2: 3);\n"
msgid "Redirects"
msgstr "Nukreipimai"
diff --git a/django/contrib/redirects/locale/sk/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/sk/LC_MESSAGES/django.mo
index 92ad19f45af40..a562e68080a4d 100644
Binary files a/django/contrib/redirects/locale/sk/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/sk/LC_MESSAGES/django.po b/django/contrib/redirects/locale/sk/LC_MESSAGES/django.po
index 0517b05e6107c..f657303dc1f97 100644
--- a/django/contrib/redirects/locale/sk/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/sk/LC_MESSAGES/django.po
@@ -16,7 +16,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sk\n"
-"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
msgid "Redirects"
msgstr "Presmerovania"
diff --git a/django/contrib/redirects/locale/uk/LC_MESSAGES/django.mo b/django/contrib/redirects/locale/uk/LC_MESSAGES/django.mo
index ca77d244d8eed..dffe8adf857ec 100644
Binary files a/django/contrib/redirects/locale/uk/LC_MESSAGES/django.mo and b/django/contrib/redirects/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/redirects/locale/uk/LC_MESSAGES/django.po b/django/contrib/redirects/locale/uk/LC_MESSAGES/django.po
index 8851897584862..bbb1653db8547 100644
--- a/django/contrib/redirects/locale/uk/LC_MESSAGES/django.po
+++ b/django/contrib/redirects/locale/uk/LC_MESSAGES/django.po
@@ -18,8 +18,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
msgid "Redirects"
msgstr "Перенаправлення"
diff --git a/django/contrib/sessions/locale/en/LC_MESSAGES/django.po b/django/contrib/sessions/locale/en/LC_MESSAGES/django.po
index 2ce18723404f0..3e2771486a2da 100644
--- a/django/contrib/sessions/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-17 11:07+0100\n"
+"POT-Creation-Date: 2020-01-05 23:23-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,27 +12,22 @@ msgstr ""
"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"
-#: contrib/sessions/apps.py:8
msgid "Sessions"
msgstr ""
-#: contrib/sessions/models.py:44
msgid "session key"
msgstr ""
-#: contrib/sessions/models.py:46
msgid "session data"
msgstr ""
-#: contrib/sessions/models.py:47
msgid "expire date"
msgstr ""
-#: contrib/sessions/models.py:52
msgid "session"
msgstr ""
-#: contrib/sessions/models.py:53
msgid "sessions"
msgstr ""
diff --git a/django/contrib/sessions/locale/fa/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/fa/LC_MESSAGES/django.mo
index c2fb6622b418c..7313c7240a655 100644
Binary files a/django/contrib/sessions/locale/fa/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/fa/LC_MESSAGES/django.po b/django/contrib/sessions/locale/fa/LC_MESSAGES/django.po
index 5e53fab5c6229..276d2de3c64de 100644
--- a/django/contrib/sessions/locale/fa/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/fa/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fa\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sessions"
msgstr "نشستها"
diff --git a/django/contrib/sessions/locale/he/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/he/LC_MESSAGES/django.mo
index 1cce90494d9b2..002850414f8e7 100644
Binary files a/django/contrib/sessions/locale/he/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/he/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/he/LC_MESSAGES/django.po b/django/contrib/sessions/locale/he/LC_MESSAGES/django.po
index 85206d42b6c95..e8bd93468bee8 100644
--- a/django/contrib/sessions/locale/he/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/he/LC_MESSAGES/django.po
@@ -15,7 +15,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: he\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % "
+"1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
msgid "Sessions"
msgstr "התחברויות"
diff --git a/django/contrib/sessions/locale/ka/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/ka/LC_MESSAGES/django.mo
index be4e5d87e082f..3fa111db93303 100644
Binary files a/django/contrib/sessions/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/ka/LC_MESSAGES/django.po b/django/contrib/sessions/locale/ka/LC_MESSAGES/django.po
index b42fbf1b368d7..bcdcb5f802f30 100644
--- a/django/contrib/sessions/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/ka/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Sessions"
msgstr ""
diff --git a/django/contrib/sessions/locale/kk/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/kk/LC_MESSAGES/django.mo
index f3b0c1b2389c3..3b8262aa03063 100644
Binary files a/django/contrib/sessions/locale/kk/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/kk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/kk/LC_MESSAGES/django.po b/django/contrib/sessions/locale/kk/LC_MESSAGES/django.po
index 9fd740b9b0f9b..5777dc2621d9d 100644
--- a/django/contrib/sessions/locale/kk/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/kk/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kk\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Sessions"
msgstr "Сессиялар"
diff --git a/django/contrib/sessions/locale/kn/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/kn/LC_MESSAGES/django.mo
index 1240807f06b7d..f818c57f1fa12 100644
Binary files a/django/contrib/sessions/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/kn/LC_MESSAGES/django.po b/django/contrib/sessions/locale/kn/LC_MESSAGES/django.po
index cf9e7bf4aa69b..7f369494c17a3 100644
--- a/django/contrib/sessions/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sessions"
msgstr ""
diff --git a/django/contrib/sessions/locale/lt/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/lt/LC_MESSAGES/django.mo
index bfd37fbd0e5a6..f365ad36b2312 100644
Binary files a/django/contrib/sessions/locale/lt/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/lt/LC_MESSAGES/django.po b/django/contrib/sessions/locale/lt/LC_MESSAGES/django.po
index 5307182d5621e..0051e3000a24a 100644
--- a/django/contrib/sessions/locale/lt/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/lt/LC_MESSAGES/django.po
@@ -16,8 +16,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lt\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n"
-"%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < "
+"11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? "
+"1 : n % 1 != 0 ? 2: 3);\n"
msgid "Sessions"
msgstr "Sesijos"
diff --git a/django/contrib/sessions/locale/sk/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/sk/LC_MESSAGES/django.mo
index 675f761319080..681667a41400b 100644
Binary files a/django/contrib/sessions/locale/sk/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/sk/LC_MESSAGES/django.po b/django/contrib/sessions/locale/sk/LC_MESSAGES/django.po
index 84c591aad37db..ca01c84e68c75 100644
--- a/django/contrib/sessions/locale/sk/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/sk/LC_MESSAGES/django.po
@@ -15,7 +15,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sk\n"
-"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
msgid "Sessions"
msgstr "Relácie"
diff --git a/django/contrib/sessions/locale/uk/LC_MESSAGES/django.mo b/django/contrib/sessions/locale/uk/LC_MESSAGES/django.mo
index a2def308b9fd1..839c316b0ec12 100644
Binary files a/django/contrib/sessions/locale/uk/LC_MESSAGES/django.mo and b/django/contrib/sessions/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sessions/locale/uk/LC_MESSAGES/django.po b/django/contrib/sessions/locale/uk/LC_MESSAGES/django.po
index 6a5aef7b40c45..befabf0653993 100644
--- a/django/contrib/sessions/locale/uk/LC_MESSAGES/django.po
+++ b/django/contrib/sessions/locale/uk/LC_MESSAGES/django.po
@@ -16,8 +16,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
msgid "Sessions"
msgstr "Сесії"
diff --git a/django/contrib/sites/locale/en/LC_MESSAGES/django.po b/django/contrib/sites/locale/en/LC_MESSAGES/django.po
index 3b1884c4af547..c4d19ce06ff2d 100644
--- a/django/contrib/sites/locale/en/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/en/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Django\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2015-01-17 11:07+0100\n"
+"POT-Creation-Date: 2020-01-06 01:01-0500\n"
"PO-Revision-Date: 2010-05-13 15:35+0200\n"
"Last-Translator: Django team\n"
"Language-Team: English \n"
@@ -12,27 +12,22 @@ msgstr ""
"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"
-#: contrib/sites/apps.py:11
msgid "Sites"
msgstr ""
-#: contrib/sites/models.py:30
msgid "The domain name cannot contain any spaces or tabs."
msgstr ""
-#: contrib/sites/models.py:81
msgid "domain name"
msgstr ""
-#: contrib/sites/models.py:83
msgid "display name"
msgstr ""
-#: contrib/sites/models.py:88
msgid "site"
msgstr ""
-#: contrib/sites/models.py:89
msgid "sites"
msgstr ""
diff --git a/django/contrib/sites/locale/fa/LC_MESSAGES/django.mo b/django/contrib/sites/locale/fa/LC_MESSAGES/django.mo
index 7bc004f3b11eb..bddae167b59e4 100644
Binary files a/django/contrib/sites/locale/fa/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/fa/LC_MESSAGES/django.po b/django/contrib/sites/locale/fa/LC_MESSAGES/django.po
index 4aab2231eec52..ba91492439ed2 100644
--- a/django/contrib/sites/locale/fa/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/fa/LC_MESSAGES/django.po
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fa\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sites"
msgstr "وبگاهها"
diff --git a/django/contrib/sites/locale/he/LC_MESSAGES/django.mo b/django/contrib/sites/locale/he/LC_MESSAGES/django.mo
index 3cdf5dc68c031..8d5cb380d75ed 100644
Binary files a/django/contrib/sites/locale/he/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/he/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/he/LC_MESSAGES/django.po b/django/contrib/sites/locale/he/LC_MESSAGES/django.po
index 6bc277defc9a5..62de695f3dc9e 100644
--- a/django/contrib/sites/locale/he/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/he/LC_MESSAGES/django.po
@@ -15,7 +15,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: he\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n == 2 && n % "
+"1 == 0) ? 1: (n % 10 == 0 && n % 1 == 0 && n > 10) ? 2 : 3;\n"
msgid "Sites"
msgstr "אתרים"
diff --git a/django/contrib/sites/locale/ka/LC_MESSAGES/django.mo b/django/contrib/sites/locale/ka/LC_MESSAGES/django.mo
index c7eb889d0737c..a08a7a8cf4a89 100644
Binary files a/django/contrib/sites/locale/ka/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/ka/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/ka/LC_MESSAGES/django.po b/django/contrib/sites/locale/ka/LC_MESSAGES/django.po
index 4a2ac8673a841..5d05019a821e9 100644
--- a/django/contrib/sites/locale/ka/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/ka/LC_MESSAGES/django.po
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ka\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Sites"
msgstr "საიტები"
diff --git a/django/contrib/sites/locale/kk/LC_MESSAGES/django.mo b/django/contrib/sites/locale/kk/LC_MESSAGES/django.mo
index edfaf6dfd1d97..433c0828298d1 100644
Binary files a/django/contrib/sites/locale/kk/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/kk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/kk/LC_MESSAGES/django.po b/django/contrib/sites/locale/kk/LC_MESSAGES/django.po
index b986095fbc04b..23c0f0024abea 100644
--- a/django/contrib/sites/locale/kk/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/kk/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kk\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
msgid "Sites"
msgstr "Сайттар"
diff --git a/django/contrib/sites/locale/kn/LC_MESSAGES/django.mo b/django/contrib/sites/locale/kn/LC_MESSAGES/django.mo
index 234ed90f9d04b..9607c2d4b72e7 100644
Binary files a/django/contrib/sites/locale/kn/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/kn/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/kn/LC_MESSAGES/django.po b/django/contrib/sites/locale/kn/LC_MESSAGES/django.po
index 1f9b3b984f905..2d81d8f5a042e 100644
--- a/django/contrib/sites/locale/kn/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/kn/LC_MESSAGES/django.po
@@ -15,7 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: kn\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
msgid "Sites"
msgstr ""
diff --git a/django/contrib/sites/locale/lt/LC_MESSAGES/django.mo b/django/contrib/sites/locale/lt/LC_MESSAGES/django.mo
index 8cdaf71ebfd0f..9d42bf0f865b9 100644
Binary files a/django/contrib/sites/locale/lt/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/lt/LC_MESSAGES/django.po b/django/contrib/sites/locale/lt/LC_MESSAGES/django.po
index f919c3b19c946..afcb5ac6a9b7b 100644
--- a/django/contrib/sites/locale/lt/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/lt/LC_MESSAGES/django.po
@@ -18,8 +18,9 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lt\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n"
-"%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < "
+"11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? "
+"1 : n % 1 != 0 ? 2: 3);\n"
msgid "Sites"
msgstr "Tinklalapiai"
diff --git a/django/contrib/sites/locale/sk/LC_MESSAGES/django.mo b/django/contrib/sites/locale/sk/LC_MESSAGES/django.mo
index fbcf69a0d4814..2706b4717b455 100644
Binary files a/django/contrib/sites/locale/sk/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/sk/LC_MESSAGES/django.po b/django/contrib/sites/locale/sk/LC_MESSAGES/django.po
index 9b3f3182fbda5..0f48ec98a1534 100644
--- a/django/contrib/sites/locale/sk/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/sk/LC_MESSAGES/django.po
@@ -16,7 +16,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sk\n"
-"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
msgid "Sites"
msgstr "Sídla"
diff --git a/django/contrib/sites/locale/uk/LC_MESSAGES/django.mo b/django/contrib/sites/locale/uk/LC_MESSAGES/django.mo
index 8ffb1b948f77e..7ae4795b761de 100644
Binary files a/django/contrib/sites/locale/uk/LC_MESSAGES/django.mo and b/django/contrib/sites/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/django/contrib/sites/locale/uk/LC_MESSAGES/django.po b/django/contrib/sites/locale/uk/LC_MESSAGES/django.po
index 232940a7a5aa4..f98454c9d4cbb 100644
--- a/django/contrib/sites/locale/uk/LC_MESSAGES/django.po
+++ b/django/contrib/sites/locale/uk/LC_MESSAGES/django.po
@@ -17,8 +17,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != "
+"11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % "
+"100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || "
+"(n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
msgid "Sites"
msgstr "Сайти"
diff --git a/django/core/checks/translation.py b/django/core/checks/translation.py
index 8457a6b89d862..2790a4c253364 100644
--- a/django/core/checks/translation.py
+++ b/django/core/checks/translation.py
@@ -1,8 +1,13 @@
+import re
+import warnings
+
from django.conf import settings
-from django.utils.translation import get_supported_language_variant
-from django.utils.translation.trans_real import language_code_re
+from django.utils.translation.trans_real import (
+ activate, deactivate, get_language, get_supported_language_variant,
+ language_code_re, reset_translations_cache,
+)
-from . import Error, Tags, register
+from . import Error, Tags, Warning, register
E001 = Error(
'You have provided an invalid value for the LANGUAGE_CODE setting: {!r}.',
@@ -25,6 +30,11 @@
id='translation.E004',
)
+W005 = Warning(
+ 'Inconsistent plural forms across catalogs for language {!r}.',
+ id='translation.W005',
+)
+
@register(Tags.translation)
def check_setting_language_code(app_configs, **kwargs):
@@ -60,5 +70,36 @@ def check_language_settings_consistent(app_configs, **kwargs):
get_supported_language_variant(settings.LANGUAGE_CODE)
except LookupError:
return [E004]
- else:
- return []
+ return []
+
+
+@register(Tags.translation)
+def check_plural_forms_consistency(app_configs, **kwargs):
+ """
+ Warns if plural forms are not consistent for languages in the LANGUAGES setting.
+ """
+ if settings.USE_I18N and settings.PLURAL_FORMS_CONSISTENCY:
+ with warnings.catch_warnings(record=True) as ws:
+ warnings.simplefilter("always")
+ saved_locale = get_language()
+ try:
+ warns = []
+ deactivate()
+ reset_translations_cache()
+ if settings.LANGUAGES:
+ for lang, _ in settings.LANGUAGES:
+ activate(lang)
+ else:
+ activate(settings.LANGUAGE_CODE)
+ if len(ws) > 0:
+ for warn in ws:
+ m = re.search(r'(?<=Locale: )(.*)(?=\n)', str(warn.message))
+ if m:
+ tag = m.group(0)
+ warns.append(Warning(W005.msg.format(tag), id=W005.id))
+ return warns
+ finally:
+ reset_translations_cache()
+ if saved_locale:
+ activate(saved_locale)
+ return []
diff --git a/django/core/management/commands/makemessages.py b/django/core/management/commands/makemessages.py
index 1b6bacc02e66b..a52c7afc0e2c9 100644
--- a/django/core/management/commands/makemessages.py
+++ b/django/core/management/commands/makemessages.py
@@ -19,6 +19,7 @@
from django.utils.regex_helper import _lazy_re_compile
from django.utils.text import get_text_list
from django.utils.translation import templatize
+from django.utils.translation.plural_forms import PluralForms
plural_forms_re = _lazy_re_compile(r'^(?P"Plural-Forms.+?\\n")\s*$', re.MULTILINE | re.DOTALL)
STATUS_OK = 0
@@ -205,12 +206,14 @@ class Command(BaseCommand):
translatable_file_class = TranslatableFile
build_file_class = BuildFile
+ plural_forms_class = PluralForms
requires_system_checks = False
msgmerge_options = ['-q', '--previous']
msguniq_options = ['--to-code=utf-8']
msgattrib_options = ['--no-obsolete']
+ msgcat_options = ['--use-first', '--to-code=UTF-8']
xgettext_options = ['--from-code=UTF-8', '--add-comments=Translators']
def add_arguments(self, parser):
@@ -279,6 +282,21 @@ def add_arguments(self, parser):
'--keep-pot', action='store_true',
help="Keep .pot file after making messages. Useful when debugging.",
)
+ parser.add_argument(
+ '--collect-bundled', '-cb',
+ choices=('default-path', 'all-bundled'), nargs='?',
+ const='default-path',
+ help="Collects Django's bundled message files into LOCALE_ROOT."
+ )
+ parser.add_argument(
+ '--update-plural-forms', '-upf', action='store', nargs='?',
+ const='interactive',
+ help=(
+ "Checks or aligns all project or application message files' "
+ "plural forms for the locale(s) with the main plural forms "
+ "in a domain (see --domain)."
+ )
+ )
def handle(self, *args, **options):
locale = options['locale']
@@ -288,6 +306,8 @@ def handle(self, *args, **options):
process_all = options['all']
extensions = options['extensions']
self.symlinks = options['symlinks']
+ self.collect_bundled = options['collect_bundled']
+ self.update_pfs = options['update_plural_forms']
ignore_patterns = options['ignore_patterns']
if options['use_default_ignore_patterns']:
@@ -299,11 +319,13 @@ def handle(self, *args, **options):
self.msgmerge_options = self.msgmerge_options[:] + ['--no-wrap']
self.msguniq_options = self.msguniq_options[:] + ['--no-wrap']
self.msgattrib_options = self.msgattrib_options[:] + ['--no-wrap']
+ self.msgcat_options = self.msgcat_options[:] + ['--no-wrap']
self.xgettext_options = self.xgettext_options[:] + ['--no-wrap']
if options['no_location']:
self.msgmerge_options = self.msgmerge_options[:] + ['--no-location']
self.msguniq_options = self.msguniq_options[:] + ['--no-location']
self.msgattrib_options = self.msgattrib_options[:] + ['--no-location']
+ self.msgcat_options = self.msgcat_options[:] + ['--no-location']
self.xgettext_options = self.xgettext_options[:] + ['--no-location']
if options['add_location']:
if self.gettext_version < (0, 19):
@@ -315,6 +337,7 @@ def handle(self, *args, **options):
self.msgmerge_options = self.msgmerge_options[:] + [arg_add_location]
self.msguniq_options = self.msguniq_options[:] + [arg_add_location]
self.msgattrib_options = self.msgattrib_options[:] + [arg_add_location]
+ self.msgcat_options = self.msgcat_options[:] + [arg_add_location]
self.xgettext_options = self.xgettext_options[:] + [arg_add_location]
self.no_obsolete = options['no_obsolete']
@@ -335,6 +358,20 @@ def handle(self, *args, **options):
% (os.path.basename(sys.argv[0]), sys.argv[1])
)
+ if (self.collect_bundled) and self.settings_available and not hasattr(settings, 'LOCALE_ROOT'):
+ raise CommandError(
+ "currently makemessages only supports collecting bundled message files "
+ "with the LOCALE_ROOT setting defined."
+ )
+
+ if self.update_pfs and self.update_pfs not in ['interactive', 'copy']:
+ update_pfs_regex = r'(\d,)*\d'
+ if not re.fullmatch(update_pfs_regex, self.update_pfs):
+ raise CommandError(
+ "currently the --update-plural-forms option only supports "
+ "'interactive', 'copy' or comma-separated digits as a parameter."
+ )
+
if self.verbosity > 1:
self.stdout.write(
'examining files with the extensions: %s\n'
@@ -346,6 +383,7 @@ def handle(self, *args, **options):
self.default_locale_path = None
if os.path.isdir(os.path.join('conf', 'locale')):
self.locale_paths = [os.path.abspath(os.path.join('conf', 'locale'))]
+ self.locale_paths.extend(self.contrib_apps_locale_dirs)
self.default_locale_path = self.locale_paths[0]
self.invoked_for_django = True
else:
@@ -355,16 +393,14 @@ def handle(self, *args, **options):
if os.path.isdir('locale'):
self.locale_paths.append(os.path.abspath('locale'))
if self.locale_paths:
- self.default_locale_path = self.locale_paths[0]
+ if self.settings_available and hasattr(settings, 'LOCALE_ROOT'):
+ self.default_locale_path = settings.LOCALE_ROOT
+ else:
+ self.default_locale_path = self.locale_paths[0]
os.makedirs(self.default_locale_path, exist_ok=True)
# Build locale list
- looks_like_locale = re.compile(r'[a-z]{2}')
- locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % self.default_locale_path))
- all_locales = [
- lang_code for lang_code in map(os.path.basename, locale_dirs)
- if looks_like_locale.match(lang_code)
- ]
+ all_locales = self.locale_list_from_path(self.default_locale_path)
# Account for excluded locales
if process_all:
@@ -376,17 +412,46 @@ def handle(self, *args, **options):
if locales:
check_programs('msguniq', 'msgmerge', 'msgattrib')
+ if self.collect_bundled:
+ check_programs('msgcat')
+
+ if self.update_pfs:
+ check_programs('msgfmt')
+
check_programs('xgettext')
try:
potfiles = self.build_potfiles()
+ if self.collect_bundled:
+ main_po_ph = os.path.join(
+ self.django_dir, 'conf', 'locale', '%s', 'LC_MESSAGES', 'django.po'
+ )
+ locales_with_catalogs = [
+ lang_code for lang_code in
+ self.locale_list_from_path(self.django_conf_locale_dir)
+ if os.path.exists(main_po_ph % lang_code)
+ ]
+ locales_to_collect = (
+ set(locales_with_catalogs + self.locale_list_from_path(settings.LOCALE_ROOT))
+ if self.collect_bundled == 'all-bundled' else locales
+ )
+ for locale in locales_to_collect:
+ if self.verbosity > 0:
+ self.stdout.write(
+ "collecting bundled messages for locale %s\n" % locale
+ )
+ self.collect_bundled_messages(locale)
+
# Build po files for each selected locale
for locale in locales:
if self.verbosity > 0:
self.stdout.write("processing locale %s\n" % locale)
- for potfile in potfiles:
- self.write_po_file(potfile, locale)
+ if self.update_pfs:
+ self.update_plural_forms(locale)
+ else:
+ for potfile in potfiles:
+ self.write_po_file(potfile, locale)
finally:
if not self.keep_pot:
self.remove_potfiles()
@@ -415,11 +480,56 @@ def settings_available(self):
return False
return True
+ @cached_property
+ def django_dir(self):
+ return os.path.normpath(os.path.join(os.path.dirname(django.__file__)))
+
+ @cached_property
+ def django_conf_locale_dir(self):
+ return os.path.join(self.django_dir, 'conf', 'locale')
+
+ @cached_property
+ def contrib_apps_locale_dirs(self):
+ return glob.glob(self.django_dir + '/contrib/*/locale')
+
+ def locale_list_from_path(self, path):
+ looks_like_locale = re.compile(r'[a-z]{2}')
+ locale_dirs = filter(os.path.isdir, glob.glob('%s/*' % path))
+ return [
+ lang_code for lang_code in map(os.path.basename, locale_dirs)
+ if looks_like_locale.match(lang_code)
+ ]
+
+ def get_main_po(self, locale, domain):
+ """
+ Returns a string with the path to main po for the locale and domain if
+ exists. If it doesn't exists for a language variant, return the main
+ language main po.
+ """
+ if self.settings_available and hasattr(settings, 'LOCALE_ROOT'):
+ main_po_dir = os.path.join(settings.LOCALE_ROOT, '%s', 'LC_MESSAGES')
+ else:
+ main_po_dir = os.path.join(
+ self.django_dir, 'conf', 'locale', '%s', 'LC_MESSAGES'
+ )
+ main_po_ph = os.path.join(main_po_dir, '%s.po')
+ if os.path.exists(main_po_ph % (locale, domain)):
+ return main_po_ph % (locale, domain)
+ else:
+ if "_" in locale:
+ # Use the main language instead of the variant
+ lang_locale = locale.split("_")[0]
+ if os.path.exists(main_po_ph % (lang_locale, domain)):
+ return main_po_ph % (lang_locale, domain)
+ return None
+
def build_potfiles(self):
"""
Build pot files and apply msguniq to them.
"""
file_list = self.find_files(".")
+ if self.collect_bundled:
+ file_list.extend(self.find_files(self.django_dir))
self.remove_potfiles()
self.process_files(file_list)
potfiles = []
@@ -475,6 +585,10 @@ def find_files(self, root):
else:
locale_dir = None
for path in self.locale_paths:
+ if self.collect_bundled:
+ if self.django_dir < os.path.abspath(path):
+ locale_dir = settings.LOCALE_ROOT
+ break
if os.path.abspath(dirpath).startswith(os.path.dirname(path)):
locale_dir = path
break
@@ -630,21 +744,98 @@ def write_po_file(self, potfile, locale):
elif self.verbosity > 0:
self.stdout.write(errors)
- def copy_plural_forms(self, msgs, locale):
+ def collect_bundled_messages(self, locale):
+ """
+ Extract all the literals and collects all the translation strings in
+ message files bundled with Django, for a `locale` and creates or
+ updates the corresponding message file in LOCALE_ROOT.
+
+ Use msgcat GNU gettext utility.
+ """
+ django_main_po = [
+ os.path.join(self.django_dir, 'conf', 'locale', locale,
+ 'LC_MESSAGES', '%s.po')
+ ]
+ django_contrib_pos = [
+ os.path.join(dirname, locale, 'LC_MESSAGES', '%s.po')
+ for dirname in self.contrib_apps_locale_dirs
+ ]
+
+ django_pos = [
+ po % self.domain for po in django_main_po + django_contrib_pos
+ if os.path.exists(po % self.domain)
+ ]
+ if len(django_pos) > 0:
+ args = ['msgcat'] + self.msgcat_options + django_pos
+ msgs, errors, status = popen_wrapper(args)
+ if errors:
+ if status != STATUS_OK:
+ raise CommandError(
+ "errors happened while running msgcat\n%s" % errors)
+ elif self.verbosity > 0:
+ self.stdout.write(errors)
+ msgs = normalize_eols(msgs)
+ if not msgs:
+ # msgcat will not output the header if there is only a null
+ # msgid in the file, keep the header for the plural forms
+ with open(django_pos[0], encoding='utf-8') as fp:
+ msgs = fp.read()
+ else:
+ # No catalogs were bundled for the locale (adding a new locale)
+ potfile = os.path.join(settings.LOCALE_ROOT, '%s.pot' % self.domain)
+ with open(potfile, encoding='utf-8') as fp:
+ msgs = fp.read()
+
+ translators_re = r'(?<=# Translators:\n)(.*?)(?=msgid)'
+ m = re.search(translators_re, msgs, flags=re.DOTALL)
+ if m:
+ new_translators_strs = [
+ "# Contributors to this catalog are listed in:",
+ ]
+ new_translators_strs += [
+ "# " + django_po for django_po in django_pos
+ ]
+ new_translators_strs = '\n'.join(new_translators_strs) + '\n'
+ i, j = m.span()
+ msgs = msgs[:i] + new_translators_strs + msgs[j:]
+
+ basedir = os.path.join(settings.LOCALE_ROOT, locale, 'LC_MESSAGES')
+ os.makedirs(basedir, exist_ok=True)
+ pofile = os.path.join(basedir, '%s.po' % self.domain)
+
+ if os.path.exists(pofile):
+ tempfile = NamedTemporaryFile(mode='w+b')
+ tempfile.file.write(bytes(msgs, encoding='utf-8'))
+ tempfile.file.flush()
+ args = ['msgcat'] + self.msgcat_options + [pofile, tempfile.name]
+ msgs, errors, status = popen_wrapper(args)
+ if errors:
+ if status != STATUS_OK:
+ raise CommandError(
+ "errors happened while running msgcat\n%s" % errors)
+ elif self.verbosity > 0:
+ self.stdout.write(errors)
+ msgs = normalize_eols(msgs)
+ tempfile.close()
+
+ with open(pofile, 'w', encoding='utf-8') as fp:
+ fp.write(msgs)
+
+ def copy_plural_forms(self, msgs, locale, update=False):
"""
- Copy plural forms header contents from a Django catalog of locale to
+ Copy plural forms header contents from a main catalog of a locale to
the msgs string, inserting it at the right place. msgs should be the
- contents of a newly created .po file.
+ contents of a newly created .po file or the contents of an already
+ created .po if update is True.
"""
- django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__)))
if self.domain == 'djangojs':
domains = ('djangojs', 'django')
else:
domains = ('django',)
for domain in domains:
- django_po = os.path.join(django_dir, 'conf', 'locale', locale, 'LC_MESSAGES', '%s.po' % domain)
- if os.path.exists(django_po):
- with open(django_po, encoding='utf-8') as fp:
+ main_po = self.get_main_po(locale, domain) or self.get_main_po(locale, 'django')
+ if main_po:
+ with open(main_po, encoding='utf-8') as fp:
m = plural_forms_re.search(fp.read())
if m:
plural_form_line = m.group('value')
@@ -652,11 +843,249 @@ def copy_plural_forms(self, msgs, locale):
self.stdout.write("copying plural forms: %s\n" % plural_form_line)
lines = []
found = False
- for line in msgs.splitlines():
- if not found and (not line or plural_forms_re.search(line)):
- line = plural_form_line
- found = True
- lines.append(line)
- msgs = '\n'.join(lines)
- break
+ if not update:
+ for line in msgs.splitlines():
+ if not found and (not line or plural_forms_re.search(line)):
+ line = plural_form_line + '\n'
+ found = True
+ lines.append(line)
+ msgs = '\n'.join(lines)
+ break
+ else:
+ o = plural_forms_re.search(msgs)
+ if o:
+ plural_form_to_update = o.group('value')
+ msgs = msgs.replace(
+ plural_form_to_update, plural_form_line)
+ return msgs
+
+ def update_plural_forms(self, locale):
+ """
+ Examines the user's .po files of a locale for a divergence in the
+ plural forms with the main forms. If one is found (mismatch) and no
+ action or form map is given in the argument, prompts the user to map
+ the main form to the user's form and outputs a compliant .po file.
+ """
+ # For the 'djangojs' domain, fallback to 'django' if not exists
+ main_po = (self.get_main_po(locale, self.domain) or
+ self.get_main_po(locale, 'django'))
+ if not main_po:
+ raise CommandError(
+ "unable to find the main .po file for '%s' (or fallback), if "
+ "you're using settings.LOCALE_ROOT, make sure you have already "
+ "run `makemessages --collect-base-catalogs --locale=%s`." %
+ (locale, locale)
+ )
+
+ self.check_po_header_validity(main_po)
+ with open(main_po, encoding='utf-8') as fp:
+ m = plural_forms_re.search(fp.read())
+ if m:
+ try:
+ main_plural_forms = self.plural_forms_class(m.group('value'))
+ except ValueError:
+ raise CommandError(
+ 'plural forms in %s seems incomplete or unset - unable to parse.' % main_po
+ )
+
+ locale_paths_to_check = set(self.locale_paths)
+ if self.settings_available and hasattr(settings, 'LOCALE_ROOT'):
+ locale_paths_to_check = locale_paths_to_check - set([settings.LOCALE_ROOT])
+ for locale_path in locale_paths_to_check:
+ user_po = os.path.join(locale_path, locale, 'LC_MESSAGES',
+ '%s.po' % self.domain)
+ if os.path.exists(user_po):
+ self.check_po_header_validity(user_po)
+ with open(user_po, encoding='utf-8') as fp:
+ o = plural_forms_re.search(fp.read())
+ if o:
+ try:
+ user_plural_forms = self.plural_forms_class(o.group('value'))
+ except ValueError:
+ raise CommandError(
+ 'plural forms in %s seems incomplete or unset - unable to parse.' % user_po
+ )
+
+ if user_plural_forms == main_plural_forms:
+ if self.verbosity > 0:
+ self.stdout.write(
+ "Plural forms in %s for %s are compliant "
+ "with the main forms." % (locale_path, locale)
+ )
+ else:
+ if self.verbosity > 0:
+ self.stdout.write(
+ ":: Aligning plural forms in %s for %s "
+ "with the main form ::" % (locale_path, locale)
+ )
+ if self.update_pfs == 'interactive':
+ form_map = []
+ overwrite = None # will be set by user input
+ else:
+ form_map = [int(d) for d in self.update_pfs.split(",")]
+ overwrite = 'y'
+ if main_plural_forms.nplurals < user_plural_forms.nplurals:
+ if self.verbosity > 0:
+ self.stdout.write(
+ "WARNING: Main form plurals are less than "
+ "user's, trimming will occur."
+ )
+ m_forms = main_plural_forms.forms
+ u_forms = user_plural_forms.forms
+ if not form_map:
+ if self.verbosity > 0:
+ self.stdout.write("Main Plural Forms:")
+ for f_number in m_forms:
+ self.stdout.write(
+ "%s: %s" % (f_number, m_forms[f_number]))
+ self.stdout.write("User Plural Forms:")
+ for f_number in u_forms:
+ self.stdout.write(
+ "%s: %s" % (f_number, u_forms[f_number]))
+ for form_number in m_forms:
+ input_message = "Main form %s: %s maps to [%s] in User form: " % (
+ form_number, m_forms[form_number],
+ ("|").join(map(str, range(len(u_forms))))
+ )
+ user_input = int(self.get_user_input(input_message))
+ while user_input not in u_forms:
+ self.stdout.write("Form not available in User catalog.")
+ user_input = int(self.get_user_input(input_message))
+ form_map.append(user_input)
+ msgs = self.remap_plural_forms(user_po, form_map)
+ msgs = self.copy_plural_forms('\n'.join(msgs), locale, update=True)
+ overwrite_msg = "Overwrite user's catalog? [y/n]: "
+ if not overwrite:
+ overwrite = self.get_user_input(overwrite_msg)
+ while overwrite not in ['y', 'yes', 'n', 'no']:
+ self.stdout.write("Please answer 'y'/'yes' or 'n'/'no'.")
+ overwrite = self.get_user_input(overwrite_msg)
+ filename = user_po if overwrite in ['y', 'yes'] else user_po + '.new'
+ with open(filename, 'w', encoding="utf-8") as fp:
+ msgs = normalize_eols(msgs)
+ fp.write(msgs)
+ if self.verbosity > 0:
+ self.stdout.write(
+ "Remapped plural forms have been written to %s." % filename
+ )
+ else:
+ copy_pf, overwrite = ('y', 'y') if self.update_pfs == 'copy' else (None, None)
+ copy_pf_msg = (
+ "Catalog in %s does not contain plural forms while the main catalog "
+ "has, copy plural forms from the main one? (no remapping will be "
+ "done) [y/n]:" % user_po
+
+ )
+ if not copy_pf:
+ copy_pf = self.get_user_input(copy_pf_msg)
+ while copy_pf not in ['y', 'yes', 'n', 'no']:
+ self.stdout.write("Please answer 'y'/'yes' or 'n'/'no'.")
+ copy_pf = self.get_user_input(copy_pf_msg)
+ overwrite_msg = "Overwrite user's catalog? [y/n]: "
+ if not overwrite:
+ overwrite = self.get_user_input(overwrite_msg)
+ while overwrite not in ['y', 'yes', 'n', 'no']:
+ self.stdout.write("Please answer 'y'/'yes' or 'n'/'no'.")
+ overwrite = self.get_user_input(overwrite_msg)
+ filename = user_po if overwrite in ['y', 'yes'] else user_po + '.new'
+ with open(user_po, encoding='utf-8') as fp:
+ msgs = fp.read()
+ msgs = self.copy_plural_forms(msgs, locale)
+ msgs = normalize_eols(msgs)
+ with open(filename, 'w', encoding='utf-8') as fp:
+ fp.write(msgs)
+ if self.verbosity > 0:
+ self.stdout.write(
+ "Plural forms have been written to %s." % filename
+ )
+
+ def remap_plural_forms(self, pofile, form_map):
+ """
+ Performs the remapping of the .po file with the given form map.
+ """
+ with open(pofile, encoding='utf-8') as fp:
+ msgs = fp.read()
+ msgs = msgs.splitlines()
+ groups, groups_msgs, groups_forms, groups_new_msgs = [], [], [], []
+ group_start, group_end = None, None
+ # Find the groups of pluralized msgs in msgs
+ for i, line in enumerate(msgs):
+ if line.startswith("msgstr[0]"):
+ group_start = i
+ elif line == "" and group_start:
+ group_end = i
+ if i == len(msgs) - 1 and group_start:
+ # Last empty line is stripped by splitlines()
+ group_end = i + 1
+ if group_start and group_end:
+ groups.append([group_start, group_end])
+ group_start, group_end = None, None
+ # Collect all msgs in groups
+ for group in groups:
+ groups_msgs.append(msgs[group[0]:group[1]])
+ # Process the msgs in groups
+ for group_index, group_msgs in enumerate(groups_msgs):
+ group_forms = {}
+ for k, msg in enumerate(group_msgs):
+ m = re.search(r'(?<=msgstr\[)\d(?=\])', msg)
+ if m:
+ group_number = int(m.group(0))
+ group_forms[group_number] = [msg]
+ else:
+ group_forms[group_number].append(msg)
+ groups_forms.append(group_forms)
+
+ new_msgs = []
+ for main_form, user_form in enumerate(form_map):
+ try:
+ msgs_to_append = groups_forms[group_index][user_form]
+ msgs_to_append[0] = "msgstr[%d]" % main_form + msgs_to_append[0][9:]
+ except KeyError:
+ raise CommandError(
+ "The provided form map is not compatible with the main "
+ "and user plural forms."
+ )
+ new_msgs.extend(msgs_to_append)
+ groups_new_msgs.append(new_msgs)
+ # It's needed to go again through msgs because replacing msgs may
+ # change groups locations and the index
+ i, group_number = 0, 0
+ group_start, group_end = None, None
+ while i < len(msgs):
+ line = msgs[i]
+ if line.startswith("msgstr[0]"):
+ group_start = i
+ elif line == "" and group_start:
+ group_end = i
+ if i == len(msgs) - 1 and group_start:
+ group_end = i + 1
+ if group_start and group_end:
+ del msgs[group_start:group_end]
+ msgs[group_start:group_start] = groups_new_msgs[group_number]
+ i += len(groups_new_msgs[group_number]) - (group_end - group_start) - 1
+ group_number += 1
+ group_start, group_end = None, None
+ i += 1
+
return msgs
+
+ def get_user_input(self, message):
+ """
+ Wraps input() to make it testable.
+ """
+ return(input(message))
+
+ def check_po_header_validity(self, pofile):
+ """
+ Check the validity the header (including plural forms) of a .po file
+ using msgfmt.
+ """
+ args = ['msgfmt', '--check-header', pofile]
+ msgs, errors, status = popen_wrapper(args)
+ if errors:
+ if status != STATUS_OK:
+ raise CommandError(
+ "errors happened while checking po validity\n%s" % errors)
+ elif self.verbosity > 0:
+ self.stdout.write(errors)
+ return True
diff --git a/django/utils/translation/plural_forms.py b/django/utils/translation/plural_forms.py
new file mode 100644
index 0000000000000..5115fa54d8c7c
--- /dev/null
+++ b/django/utils/translation/plural_forms.py
@@ -0,0 +1,64 @@
+import re
+
+
+class PluralForms:
+ """
+ Represent Plural Forms, constructed from an already msgfmt-validated string.
+ """
+ PLACEHOLDER_STRING = '"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\\n"'
+
+ def __init__(self, raw_plural_form):
+ self.full_string = raw_plural_form
+ self.forms = {}
+ self.nplurals = None
+ if self.full_string != self.PLACEHOLDER_STRING:
+ pf = re.sub(r'"\n"|"|\\n', "", raw_plural_form.strip(), re.MULTILINE)
+ m = re.search(r'(?<=nplurals=)\d', pf)
+ o = re.search(r'(?<=plural=)(.*?)(?=;|$)', pf)
+ if m and o:
+ self.nplurals = int(m.group(0))
+ self.forms_string = o.group(0)
+ else:
+ # In some cases, msgfmt may consider valid forms without nplurals
+ # or plurals (bug)
+ raise ValueError(
+ "Unable to find 'nplurals' and/or 'plural' in the init string "
+ "(%s)." % raw_plural_form
+ )
+ forms = re.split(r'\s?:\s?', self.forms_string)
+ forms = [self.trim_enclosing_parentheses(f) for f in forms]
+ if len(forms) == 1 and self.nplurals == 1:
+ self.forms[0] = forms[0]
+ elif len(forms) == 1 and self.nplurals == 2:
+ self.forms[0] = "SINGULAR"
+ self.forms[1] = forms[0]
+ else:
+ for form in forms:
+ p = re.split(r'\s?\?\s?', form)
+ if len(p) == 1:
+ self.forms[int(p[0])] = "OTHER"
+ else:
+ self.forms[int(p[1])] = p[0]
+
+ def __eq__(self, other):
+ if other and self.nplurals == other.nplurals and self.forms == other.forms:
+ return True
+ else:
+ return False
+
+ def trim_enclosing_parentheses(self, form_string):
+ """
+ Trim all-forms enclosing parenthensis (if any).
+ """
+ counter = 0
+ for char in form_string:
+ if char == '(':
+ counter += 1
+ elif char == ')':
+ counter -= 1
+ if counter < 0:
+ return form_string[:counter]
+ elif counter > 0:
+ return form_string[counter:]
+ else:
+ return form_string
diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py
index d8526753605b4..f95ae73136465 100644
--- a/django/utils/translation/trans_real.py
+++ b/django/utils/translation/trans_real.py
@@ -18,6 +18,7 @@
from django.utils.safestring import SafeData, mark_safe
from . import to_language, to_locale
+from .plural_forms import PluralForms
# Translations are cached in a dictionary for every language.
# The active translations are stored by threadid to make them thread local.
@@ -99,8 +100,15 @@ def __init__(self, language, domain=None, localedirs=None):
self._add_local_translations()
if self.__language == settings.LANGUAGE_CODE and self.domain == 'django' and self._catalog is None:
- # default lang should have at least one translation file available.
- raise OSError('No translation files found for default language %s.' % settings.LANGUAGE_CODE)
+ if not hasattr(settings, 'LOCALE_ROOT'):
+ # default lang should have at least one translation file available if not using LOCALE_ROOT.
+ raise OSError('No translation files found for default language %s.' % settings.LANGUAGE_CODE)
+ else:
+ msg = ('LOCALE_ROOT has been set and no translation files found for default language %s. '
+ 'Make sure that the bundled message files have been collected with '
+ '`makemessages --collect-bundled` and/or they have been compiled with '
+ '`compilemessages`.' % settings.LANGUAGE_CODE)
+ warnings.warn(msg, RuntimeWarning)
self._add_fallback(localedirs)
if self._catalog is None:
# No catalogs found for this language, set an empty catalog.
@@ -111,23 +119,28 @@ def __repr__(self):
def _new_gnu_trans(self, localedir, use_null_fallback=True):
"""
- Return a mergeable gettext.GNUTranslations instance.
+ Return an annotated mergeable gettext.GNUTranslations instance.
A convenience wrapper. By default gettext uses 'fallback=False'.
Using param `use_null_fallback` to avoid confusion with any other
- references to 'fallback'.
+ references to 'fallback'. Also annotates the localedir in the object.
"""
- return gettext_module.translation(
+ annotated_gnu_trans = gettext_module.translation(
domain=self.domain,
localedir=localedir,
languages=[self.__locale],
fallback=use_null_fallback,
)
+ annotated_gnu_trans.localedir = localedir
+ return annotated_gnu_trans
def _init_translation_catalog(self):
"""Create a base catalog using global django translations."""
- settingsfile = sys.modules[settings.__module__].__file__
- localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
+ if not hasattr(settings, 'LOCALE_ROOT'):
+ settingsfile = sys.modules[settings.__module__].__file__
+ localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
+ else:
+ localedir = settings.LOCALE_ROOT
translation = self._new_gnu_trans(localedir)
self.merge(translation)
@@ -142,9 +155,13 @@ def _add_installed_apps_translations(self):
"gettext calls at import time.")
for app_config in app_configs:
localedir = os.path.join(app_config.path, 'locale')
- if os.path.exists(localedir):
- translation = self._new_gnu_trans(localedir)
- self.merge(translation)
+ if (hasattr(settings, 'LOCALE_ROOT') and
+ os.path.join('django', 'contrib') in os.path.dirname(localedir)):
+ continue
+ else:
+ if os.path.exists(localedir):
+ translation = self._new_gnu_trans(localedir)
+ self.merge(translation)
def _add_local_translations(self):
"""Merge translations defined in LOCALE_PATHS."""
@@ -177,7 +194,32 @@ def merge(self, other):
self._info = other._info.copy()
self._catalog = other._catalog.copy()
else:
+ if settings.PLURAL_FORMS_CONSISTENCY:
+ if 'plural-forms' in self.info():
+ current_plural_forms = PluralForms(self.info()['plural-forms'])
+ if 'plural-forms' not in other.info():
+ other._info['MISSING PLURAL FORMS'] = "MISSING PLURAL FORMS"
+ other_plural_forms = None
+ else:
+ other_plural_forms = PluralForms(other.info()['plural-forms'])
+ if current_plural_forms != other_plural_forms:
+ from pprint import pformat
+ msg = (
+ "\nPosible inconsistencies and undesired behavior detected "
+ "due to different plural forms in message file.\n"
+ "Locale: %s\n"
+ "Unconsistent message file localedir: %s\n"
+ "Unconsistent message file info: \n%s\n"
+ "MAIN PLURAL FORM: \n%s\n"
+ "See https://docs.djangoproject.com/en/dev/topics/i18n/translation/#plural-forms"
+ % (self.__language,
+ other.localedir,
+ pformat(other.info(), indent=4),
+ pformat(self.info()['plural-forms'], indent=4), )
+ )
+ warnings.warn(msg, RuntimeWarning)
self._catalog.update(other._catalog)
+
if other._fallback:
self.add_fallback(other._fallback)
@@ -200,6 +242,15 @@ def translation(language):
return _translations[language]
+def reset_translations_cache():
+ """
+ Clears the translations cache dict.
+ """
+ global _translations
+ if _translations:
+ _translations.clear()
+
+
def activate(language):
"""
Fetch the translation object for a given language and install it as the
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index a080b5bdf51c6..8215bcf762763 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -457,6 +457,8 @@ configured:
:setting:`OPTIONS ` must be a string but got: ``{value}``
(``{type}``).
+.. _translation-checks:
+
Translation
-----------
@@ -472,6 +474,12 @@ The following checks are performed on your translation configuration:
:setting:`LANGUAGE_CODE` setting that is not in the :setting:`LANGUAGES`
setting.
+The following checks are performed on your translations catalogs for the
+languages in the :setting:`LANGUAGES` setting:
+
+* **translation.W005**: Inconsistent plural forms across catalogs for
+ language ```` (unmerged catalogs).
+
URLs
----
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index cbb4a2d8eda44..30e2cac6195af 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -629,7 +629,8 @@ the :ref:`i18n documentation ` for details.
This command doesn't require configured settings. However, when settings aren't
configured, the command can't ignore the :setting:`MEDIA_ROOT` and
-:setting:`STATIC_ROOT` directories or include :setting:`LOCALE_PATHS`.
+:setting:`STATIC_ROOT` directories, include :setting:`LOCALE_PATHS` or collect
+to :setting:`LOCALE_ROOT`.
.. django-admin-option:: --all, -a
@@ -727,6 +728,75 @@ Prevents deleting the temporary ``.pot`` files generated before creating the
``.po`` file. This is useful for debugging errors which may prevent the final
language files from being created.
+.. django-admin-option:: --collect-bundled [{default-path, all-bundled}] -cb
+
+Pulls out all strings marked for translation in Django for a domain (defaults
+to 'django'), collects all catalogs (message files) bundled for the locale(s)
+and merges them into the corresponding main po file(s) in
+:setting:`LOCALE_ROOT` (required setting).
+
+Defaults to the locales defined in the default locale path that the command
+considers (``default-path``), use ``all-bundled`` for collecting all locales
+bundled with Django.
+
+Specific locale(s) to collect can be set with the ``--locale`` option (or
+ignored with ``--exclude``). If the specified locale does not exists, an
+untranslated message file will be created for it.
+
+Example usage::
+
+ django-admin makemessages --collect-bundled --locale=en --locale=pt
+ django-admin makemessages --collect-bundled all-bundled --exclude=fr
+ django-admin makemessages --collect-bundled --domain djangojs
+ django-admin makemessages -cb -l xx_yy -d djangojs
+
+.. django-admin-option:: --update-plural-forms [{interactive, copy, form_map}] -cpf
+
+Checks or aligns all project or application message files' plural forms for the
+locale(s) (detected or specified) in a domain (defaults to ``django``) with the
+main plural forms (see :ref:`plural-forms`).
+
+If a divergence is found and the option is set to ``interactive`` (default),
+the user will be prompted to map the main plural forms to the ones found in
+the message file. With the form map provided, the message file will be
+reorganized accordingly and the plural forms will be updated.
+
+In the case of an increase in the number of plurals, it will fill
+those with the previously chosen forms so you avoid having translation results
+in the fallback language or the original string - this will produce the same
+results as before though you may want to update those later for a
+better expression of the language if it corresponds.
+
+In a decrease of the forms, it will reorganize and trim the exceeding ones.
+
+When only the equation differs, only reorganization will occur.
+
+If no plural forms is found in the message file, the user will be prompted to
+copy them from the main po file. No remapping will be done, it is assumed that
+is already aligned. If it is not the case, use a plural form with the number of
+plurals matching the message file's and then re-run the command to perform form
+mapping.
+
+Specific locale(s) to consider can be set with the ``--locale`` option (or
+ignored with ``--exclude``).
+
+Automation can be done only for a specific locale by using the values ``copy``
+for only copying the plural forms or the ``form_map`` in comma-separated digits
+- i.e. ``0,1,1,1`` - for perform form mapping.
+
+Example usage::
+
+ django-admin makemessages --update-plural-forms
+ django-admin makemessages --update-plural-forms copy --locale=en
+ django-admin makemessages -upf 0,1,1,1 -l he -d djangojs
+
+.. note::
+
+ Be aware that while this tool can handle most of the regular cases,
+ it may be an unlikely situation where a linear mapping is not adequate.
+ It is recommended not to overwrite and check the results if the form
+ map is not "obvious".
+
.. seealso::
See :ref:`customizing-makemessages` for instructions on how to customize
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt
index 51bbba35f0fd8..e7c96803301c2 100644
--- a/docs/ref/settings.txt
+++ b/docs/ref/settings.txt
@@ -1954,6 +1954,57 @@ Example::
Django will look within each of these paths for the ``/LC_MESSAGES``
directories containing the actual translation files.
+.. setting:: LOCALE_ROOT
+
+``LOCALE_ROOT``
+----------------
+
+.. versionadded:: 3.1
+
+Default: ``None``
+
+Absolute filesystem path to the directory that will hold the main po files of
+translations.
+
+Example::
+
+ LOCALE_ROOT = BASEDIR / 'locale'
+
+Django will use the files contained in ``/LC_MESSAGES`` under this
+directory as the main Django po file of a translation for a domain.
+
+.. note::
+
+ Using this setting requires compiled message file(s) for your locale(s), you may use
+ :option:`django-admin makemessages --collect-bundled`
+ for collecting the bundled ones with Django (for both ``django`` and ``djangojs`` domains)
+ and then
+ :djadmin:`django-admin compilemessages ` to provide them.
+
+.. note::
+
+ This setting will make Django not take into account the bundled message files,
+ is up to the user to run
+ :option:`django-admin makemessages --collect-bundled`
+ after an upgrade to collect any new messages.
+
+.. setting:: PLURAL_FORMS_CONSISTENCY
+
+``PLURAL_FORMS_CONSISTENCY``
+----------------------------
+
+.. versionadded:: 3.1
+
+Default: ``False``
+
+When ``True``, Django will issue a warning when merging any message file for
+translations that contains different plural forms than the main Django po file,
+both at
+:ref:`system check level ` (``translation.W005``)
+and run-time.
+
+See also :ref:`plural-forms`.
+
.. setting:: LOGGING
``LOGGING``
@@ -3578,8 +3629,10 @@ Globalization (``i18n``/``l10n``)
* :setting:`LANGUAGES`
* :setting:`LANGUAGES_BIDI`
* :setting:`LOCALE_PATHS`
+* :setting:`LOCALE_ROOT`
* :setting:`MONTH_DAY_FORMAT`
* :setting:`NUMBER_GROUPING`
+* :setting:`PLURAL_FORMS_CONSISTENCY`
* :setting:`SHORT_DATE_FORMAT`
* :setting:`SHORT_DATETIME_FORMAT`
* :setting:`THOUSAND_SEPARATOR`
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index 29e5e223140b6..8eee2631b8ab3 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -27,6 +27,41 @@ officially support the latest release of each series.
What's new in Django 3.1
========================
+Plural forms consistency and customization
+------------------------------------------
+
+Django does not support multiple plural forms in message files. As all
+translation files are merged, only the plural forms in the main Django po
+file is considered.
+
+This led to inconsistencies and undesired results in users' translations
+mostly when Django updated the plural forms of a locale and the users
+did not re-organize their message files to be in line with the new plural
+forms (:ref:`more details `).
+
+To avoid this undesired result, the :setting:`PLURAL_FORMS_CONSISTENCY`
+setting has been introduced.
+
+When enabled (disabled by default), Django will issue a warning when merging
+any message file that contains different plural forms than the main Django po
+file, both at a
+:ref:`system check level ` (``translation.W005``)
+and run-time.
+
+As fixing this issue can be automated in most scenarios, the tool used for
+ensuring plural forms consistency in all Django bundled message files is
+provided in the
+:option:`django-admin makemessages --update-plural-forms`.
+option.
+
+Also, customization of the plural forms was not possible previously without
+modifying Django source files.
+
+The :setting:`LOCALE_ROOT` setting and the
+:option:`django-admin makemessages --collect-bundled`
+option has been introduced for enabling the feature.
+
+
Minor features
--------------
diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist
index 6a36a117073f9..9fa8993587fcc 100644
--- a/docs/spelling_wordlist
+++ b/docs/spelling_wordlist
@@ -728,6 +728,7 @@ unlocalize
unlocalized
unmaintained
unmanaged
+unmerged
unordered
unparseable
unparsed
diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt
index 241dcea660b5f..fcf4e89fd9f7e 100644
--- a/docs/topics/i18n/translation.txt
+++ b/docs/topics/i18n/translation.txt
@@ -198,10 +198,10 @@ plural translation string and the number of objects.
This function is useful when you need your Django application to be localizable
to languages where the number and complexity of `plural forms
-`_ is
-greater than the two forms used in English ('object' for the singular and
-'objects' for all the cases where ``count`` is different from one, irrespective
-of its value.)
+`_
+(and :ref:`see below `) greater than the two forms used in English
+('object' for the singular and 'objects' for all the cases where ``count`` is
+different from one, irrespective of its value.)
For example::
@@ -279,14 +279,52 @@ In a case like this, consider something like the following::
a format specification for argument 'name', as in 'msgstr[0]', doesn't exist in 'msgid'
-.. note:: Plural form and po files
+.. _plural-forms:
- Django does not support custom plural equations in po files. As all
- translation catalogs are merged, only the plural form for the main Django po
- file (in ``django/conf/locale//LC_MESSAGES/django.po``) is
- considered. Plural forms in all other po files are ignored. Therefore, you
- should not use different plural equations in your project or application po
- files.
+Plural Forms
+~~~~~~~~~~~~
+
+Django does not support multiple plural forms in message files. As all
+translation files are merged, only the plural form in the main Django po
+file (located by default in ``django/conf/locale//LC_MESSAGES/django.po``,
+customized with the :setting:`LOCALE_ROOT` setting) is considered.
+
+To prevent inconsistencies and undesired results in translations, Django
+provides the :setting:`PLURAL_FORMS_CONSISTENCY` setting (disabled by default).
+
+If enabled, Django will issue a warning when merging any message file that
+contains different plural forms than the main Django po file, both at a
+:ref:`system check level ` (``translation.W005``) and
+run-time.
+
+This issue may arise mostly in two situations:
+
+* when the main plural forms for a language is updated in Django and your po
+ files were created with a previous one, or
+
+* when including third-party translations with different plural forms.
+
+If you had created your message file with a previous version of Django, the
+standard may have changed or a bug has been fixed in the release.
+
+For aligning with the new version of the standard (or addressing the bug),
+you may use
+:option:`django-admin makemessages --update-plural-forms`
+to update your message files so they are aligned with the main plural forms
+and avoid unexpected behavior.
+
+Customization of the plural forms for a language is done via the
+:setting:`LOCALE_ROOT` setting.
+
+Once the setting is defined, Django will use the files under this directory as
+the main Django po files for translations.
+
+You may use :option:`django-admin makemessages --collect-bundled`
+for collecting the bundled ones with Django and then customize them.
+
+.. versionchanged:: 3.1
+
+ Handling plural forms as described above was added.
.. _contextual-markers:
diff --git a/tests/.coveragerc b/tests/.coveragerc
index e519f06259498..9e204434b6b28 100644
--- a/tests/.coveragerc
+++ b/tests/.coveragerc
@@ -9,6 +9,5 @@ ignore_errors = True
omit =
*/django/conf/locale/*
*/tests/*
-
[html]
directory = coverage_html
diff --git a/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.mo b/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..11f853d6f6f46
Binary files /dev/null and b/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.mo differ
diff --git a/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.po b/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..e4eb778b3a851
--- /dev/null
+++ b/tests/check_framework/locale_dir/cs/LC_MESSAGES/django.po
@@ -0,0 +1,17 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-12-13 09:47+0200\n"
+"PO-Revision-Date: 2010-05-13 15:35+0200\n"
+"Last-Translator: Django team\n"
+"Language-Team: English \n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "Hello"
+msgstr ""
diff --git a/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.mo b/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..fe513c297214d
Binary files /dev/null and b/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.mo differ
diff --git a/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.po b/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..c9169b7982a33
--- /dev/null
+++ b/tests/check_framework/locale_dir/fr/LC_MESSAGES/django.po
@@ -0,0 +1,14 @@
+# This file is distributed under the same license as the Django package.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-29 09:09+0000\n"
+"Last-Translator: Claude Paroz \n"
+"Language-Team: French (http://www.transifex.com/django/django/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/tests/check_framework/test_translation.py b/tests/check_framework/test_translation.py
index 8747a52cda075..5a93e47242f26 100644
--- a/tests/check_framework/test_translation.py
+++ b/tests/check_framework/test_translation.py
@@ -1,10 +1,15 @@
-from django.core.checks import Error
+import os
+
+from django.core.checks import Error, Warning
from django.core.checks.translation import (
- check_language_settings_consistent, check_setting_language_code,
- check_setting_languages, check_setting_languages_bidi,
+ check_language_settings_consistent, check_plural_forms_consistency,
+ check_setting_language_code, check_setting_languages,
+ check_setting_languages_bidi,
)
from django.test import SimpleTestCase, override_settings
+here = os.path.dirname(os.path.abspath(__file__))
+
class TranslationCheckTests(SimpleTestCase):
@@ -108,3 +113,42 @@ def test_valid_variant_consistent_language_settings(self):
for tag in tests:
with self.subTest(tag), self.settings(LANGUAGE_CODE=tag):
self.assertEqual(check_language_settings_consistent(None), [])
+
+ def test_inconsistent_plural_forms_in_languages(self):
+ languages = [('cs', 'Czech'), ('fr', 'French'), ('sk', 'Slovak')]
+ msg = 'Inconsistent plural forms across catalogs for language {!r}.'
+ with self.settings(
+ PLURAL_FORMS_CONSISTENCY=True,
+ LANGUAGE_CODE='cs',
+ LANGUAGES=languages,
+ LOCALE_PATHS=[os.path.join(here, 'locale_dir'), ]):
+ expected_warnings = [
+ Warning(msg.format(lang), id='translation.W005') for lang in ['cs', 'fr']
+ ]
+ received_warnings = check_plural_forms_consistency(None)
+ for warn in received_warnings:
+ self.assertIn(warn, expected_warnings)
+ expected_warnings.remove(warn)
+ received_warnings.remove(warn)
+ self.assertEqual(expected_warnings, received_warnings, [])
+
+ def test_inconsistent_plural_forms_in_language_code(self):
+ msg = 'Inconsistent plural forms across catalogs for language {!r}.'
+ with self.settings(
+ PLURAL_FORMS_CONSISTENCY=True,
+ LANGUAGE_CODE='cs',
+ LANGUAGES=None,
+ LOCALE_PATHS=[os.path.join(here, 'locale_dir'), ]):
+ expected_warnings = [Warning(msg.format('cs'), id='translation.W005'), ]
+ received_warnings = check_plural_forms_consistency(None)
+ self.assertEqual(expected_warnings, received_warnings)
+
+ def test_inconsistent_plural_forms_in_languages_disabled_setting(self):
+ languages = [('cs', 'Czech'), ('fr', 'French'), ('sk', 'Slovak')]
+ with self.settings(
+ PLURAL_FORMS_CONSISTENCY=False,
+ LANGUAGE_CODE='cs',
+ LANGUAGES=languages,
+ LOCALE_PATHS=[os.path.join(here, 'locale_dir'), ]):
+ received_warnings = check_plural_forms_consistency(None)
+ self.assertEqual(received_warnings, [])
diff --git a/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..74ff78f9b66a7
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..1fbb8888dcbea
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,30 @@
+# This file is distributed under the same license as the Django package.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"Language-Team: Czech (http://www.transifex.com/django/django/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+
+#, python-format
+msgid ""
+"Ensure that there are no more than %(max)s digit before the decimal point."
+msgid_plural ""
+"Ensure that there are no more than %(max)s digits before the decimal point."
+msgstr[0] ""
+"Ujistěte se, že hodnota neobsahuje více než %(max)s místo před desetinnou "
+"čárkou (tečkou)."
+msgstr[1] ""
+"Ujistěte se, že hodnota neobsahuje více než %(max)s místa před desetinnou "
+"čárkou (tečkou)."
+
+#, python-format
+msgid "Ensure that there are no more than %(max)s digit in total."
+msgid_plural "Ensure that there are no more than %(max)s digits in total."
+msgstr[0] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslici."
+msgstr[1] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslice."
diff --git a/tests/i18n/commands/app_with_locale/locale/en/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..61b22e32ee774
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,18 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: English (http://www.transifex.com/django/django/language/"
+"en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+
+msgid "Oh, what a string!!"
+msgstr ""
diff --git a/tests/i18n/commands/app_with_locale/locale/es_XX/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/es_XX/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..2240292d657ac
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/es_XX/LC_MESSAGES/django.po
@@ -0,0 +1,16 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django/language/"
+"es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+
diff --git a/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..cb6a8f611f33a
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..dcd8fa024ddba
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/lt/LC_MESSAGES/django.po
@@ -0,0 +1,28 @@
+# This file is distributed under the same license as the Django package.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Lithuanian (http://www.transifex.com/django/django/language/"
+"lt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: lt\n"
+"Plural-Forms: nplurals=4; plural=(n % 10 == 1 && (n % 100 > 19 || n % 100 < "
+"11) ? 0 : (n % 10 >= 2 && n % 10 <=9) && (n % 100 > 19 || n % 100 < 11) ? "
+"1 : n % 1 != 0 ? 2: 3);\n"
+
+msgid "Password change"
+msgstr "Slaptažodžio keitimas"
+
+#, python-format
+msgid "%(counter)s result"
+msgid_plural "%(counter)s results"
+msgstr[0] "%(counter)s rezultatas"
+msgstr[1] "%(counter)s rezultatai"
+msgstr[2] "%(counter)s rezultatai"
+msgstr[3] "%(counter)s rezultatai"
diff --git a/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..ec20271eeef74
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..a9cc6b45ce940
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/mk/LC_MESSAGES/django.po
@@ -0,0 +1,22 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# dekomote , 2015
+# Jannis Leidel , 2011
+# Vasil Vangelovski , 2016-2017
+# Vasil Vangelovski , 2013-2015
+# Vasil Vangelovski , 2011-2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Macedonian (http://www.transifex.com/django/django/language/"
+"mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"
\ No newline at end of file
diff --git a/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..7e5dd9943b160
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.po
index ae23e448a37c4..2eb59503cb9dc 100644
--- a/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.po
+++ b/tests/i18n/commands/app_with_locale/locale/ru/LC_MESSAGES/django.po
@@ -16,8 +16,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
#
msgid "Lenin"
diff --git a/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..a5decce96e6e4
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..06e31e5b286dc
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,30 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel , 2011
+# Juraj Bubniak , 2012-2013
+# Marian Andre , 2013,2015,2017-2018
+# Martin Kosír, 2011
+# Martin Tóth , 2017
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovak (http://www.transifex.com/django/django/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
+
+#, python-format
+msgid "Please submit %d or fewer forms."
+msgid_plural "Please submit %d or fewer forms."
+msgstr[0] "Prosím odošlite %d alebo menej formulárov. -0-"
+msgstr[1] "Prosím odošlite %d alebo menej formulárov. -1-"
+msgstr[2] "Prosím odošlite %d alebo menej formulárov. -2-"
+msgstr[3] "Prosím odošlite %d alebo menej formulárov. -3-"
\ No newline at end of file
diff --git a/tests/i18n/commands/app_with_locale/locale/sk_XX/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/sk_XX/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..0801df56f7c3c
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/sk_XX/LC_MESSAGES/django.po
@@ -0,0 +1,19 @@
+# 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.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2020-01-03 05:13+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
diff --git a/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..6ad4e390ec752
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..40c6d50ea1a7e
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/sl/LC_MESSAGES/django.po
@@ -0,0 +1,31 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# iElectric , 2011-2012
+# Jannis Leidel , 2011
+# Jure Cuhalev , 2012-2013
+# Marko Zabreznik , 2016
+# Primož Verdnik , 2017
+# zejn , 2013,2016-2017
+# zejn , 2011-2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovenian (http://www.transifex.com/django/django/language/"
+"sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sl\n"
+"Plural-Forms: plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
+
+msgid "Afrikaans"
+msgstr "Afrikanščina"
+
+msgid "Arabic"
+msgstr "Arabščina"
diff --git a/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.mo b/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..57a9e19a5b76b
Binary files /dev/null and b/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.po b/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..a9a297b0ff032
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/tt/LC_MESSAGES/django.po
@@ -0,0 +1,23 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Azat Khasanshin , 2011
+# v_ildar , 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Tatar (http://www.transifex.com/django/django/language/tt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tt\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#, python-format
+msgid "%d day"
+msgid_plural "%d days"
+msgstr[0] ""
diff --git a/tests/i18n/commands/app_with_locale/locale/xxx/LC_MESSAGES/djangojs.po b/tests/i18n/commands/app_with_locale/locale/xxx/LC_MESSAGES/djangojs.po
new file mode 100644
index 0000000000000..1da4fead7f63a
--- /dev/null
+++ b/tests/i18n/commands/app_with_locale/locale/xxx/LC_MESSAGES/djangojs.po
@@ -0,0 +1,14 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Language-Team: XXX\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: xxx\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/tests/i18n/commands/django_dir/conf/global_settings.py b/tests/i18n/commands/django_dir/conf/global_settings.py
new file mode 100644
index 0000000000000..5170baf296828
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/global_settings.py
@@ -0,0 +1,15 @@
+"""
+Mocked file based on default settings to trigger xgettext extraction
+"""
+
+
+# This is defined here as a do-nothing function because we can't import
+# django.utils.translation -- that module depends on the settings.
+def gettext_noop(s):
+ return s
+
+
+# Languages we provide translations for, out of the box.
+LANGUAGES = [
+ ('lv', gettext_noop('Love')),
+]
diff --git a/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..47cddf1774457
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..ca569c59dbdba
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,18 @@
+# This file is distributed under the same license as the Django package.
+# Translators:
+# Translator #1
+# Translator #2
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"Language-Team: Czech (http://www.transifex.com/django/django/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
+"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
+
+msgid "A string"
+msgstr "translated to cs"
diff --git a/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..4bf35741a0c66
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..e5340004c4768
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,19 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django/language/"
+"es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Afrikaans"
+msgstr "Africano"
diff --git a/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..1b839200b9295
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..0fd6966870df7
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/lt/LC_MESSAGES/django.po
@@ -0,0 +1,18 @@
+# This file is distributed under the same license as the Django package.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Lithuanian (http://www.transifex.com/django/django/language/"
+"lt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: lt\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "A string"
+msgstr "translated to lt"
diff --git a/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..6adfcce106c3d
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..92b9b383894b3
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/mk/LC_MESSAGES/django.po
@@ -0,0 +1,25 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# dekomote , 2015
+# Jannis Leidel , 2011
+# Vasil Vangelovski , 2016-2017
+# Vasil Vangelovski , 2013-2015
+# Vasil Vangelovski , 2011-2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Macedonian (http://www.transifex.com/django/django/language/"
+"mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"
+
+msgid "A string"
+msgstr "translated to mk"
diff --git a/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..7007a3d4e50dd
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..6a96e2aa1339e
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,21 @@
+# 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.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-03-30 12:51+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
diff --git a/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..63b975834c407
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..5f9593a3665f5
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/sl/LC_MESSAGES/django.po
@@ -0,0 +1,25 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# iElectric , 2011-2012
+# Jannis Leidel , 2011
+# Jure Cuhalev , 2012-2013
+# Marko Zabreznik , 2016
+# Primož Verdnik , 2017
+# zejn , 2013,2016-2017
+# zejn , 2011-2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovenian (http://www.transifex.com/django/django/language/"
+"sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sl\n"
+"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
+"%100==4 ? 2 : 3);\n"
diff --git a/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..ae0373e2e176c
Binary files /dev/null and b/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..dad8137eab350
--- /dev/null
+++ b/tests/i18n/commands/django_dir/conf/locale/tt/LC_MESSAGES/django.po
@@ -0,0 +1,18 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Azat Khasanshin , 2011
+# v_ildar , 2014
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Tatar (http://www.transifex.com/django/django/language/tt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tt\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.mo b/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..d1239e3d8259b
Binary files /dev/null and b/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.po b/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..458f91cbeb284
--- /dev/null
+++ b/tests/i18n/commands/django_dir/contrib/admin/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,19 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django/language/"
+"es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Arabic"
+msgstr "Árabe"
diff --git a/tests/i18n/commands/locale/en/LC_MESSAGES/django.po b/tests/i18n/commands/locale/en/LC_MESSAGES/django.po
index ddb831b24b187..e6724f1f9b276 100644
--- a/tests/i18n/commands/locale/en/LC_MESSAGES/django.po
+++ b/tests/i18n/commands/locale/en/LC_MESSAGES/django.po
@@ -16,6 +16,7 @@ msgstr ""
"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"
#. Translators: This comment should be extracted
#: __init__.py:4
diff --git a/tests/i18n/commands/locale/fr/LC_MESSAGES/django.mo b/tests/i18n/commands/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..93a58ad34a2f0
Binary files /dev/null and b/tests/i18n/commands/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/locale/hr/LC_MESSAGES/django.mo b/tests/i18n/commands/locale/hr/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..b8d616fa2c745
Binary files /dev/null and b/tests/i18n/commands/locale/hr/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/locale/ko/LC_MESSAGES/django.mo b/tests/i18n/commands/locale/ko/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..cca2e5dfe95a3
Binary files /dev/null and b/tests/i18n/commands/locale/ko/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/locale/ru/LC_MESSAGES/django.mo b/tests/i18n/commands/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..7e5dd9943b160
Binary files /dev/null and b/tests/i18n/commands/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/commands/locale/ru/LC_MESSAGES/django.po b/tests/i18n/commands/locale/ru/LC_MESSAGES/django.po
index ae23e448a37c4..2eb59503cb9dc 100644
--- a/tests/i18n/commands/locale/ru/LC_MESSAGES/django.po
+++ b/tests/i18n/commands/locale/ru/LC_MESSAGES/django.po
@@ -16,8 +16,9 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n"
+"%100>=11 && n%100<=14)? 2 : 3);\n"
#
msgid "Lenin"
diff --git a/tests/i18n/commands/locale_root/es/LC_MESSAGES/django.po b/tests/i18n/commands/locale_root/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..350a2782b7972
--- /dev/null
+++ b/tests/i18n/commands/locale_root/es/LC_MESSAGES/django.po
@@ -0,0 +1,19 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django/language/"
+"es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Asturian"
+msgstr "Asturiano"
diff --git a/tests/i18n/commands/locale_root/mk/LC_MESSAGES/django.po b/tests/i18n/commands/locale_root/mk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..2b3104ff31d5b
--- /dev/null
+++ b/tests/i18n/commands/locale_root/mk/LC_MESSAGES/django.po
@@ -0,0 +1,16 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Macedonian (http://www.transifex.com/django/django/language/"
+"mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=3; plural=(? 0 : 1;\n"
diff --git a/tests/i18n/commands/locale_root/sk/LC_MESSAGES/django.po b/tests/i18n/commands/locale_root/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..93a6f5af5e461
--- /dev/null
+++ b/tests/i18n/commands/locale_root/sk/LC_MESSAGES/django.po
@@ -0,0 +1,21 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel , 2011
+# Juraj Bubniak , 2012-2013
+# Marian Andre , 2013,2015,2017-2018
+# Martin Kosír, 2011
+# Martin Tóth , 2017
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovak (http://www.transifex.com/django/django/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/tests/i18n/commands/locale_root/sl/LC_MESSAGES/django.po b/tests/i18n/commands/locale_root/sl/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..696f0c9bd85eb
--- /dev/null
+++ b/tests/i18n/commands/locale_root/sl/LC_MESSAGES/django.po
@@ -0,0 +1,30 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# iElectric , 2011-2012
+# Jannis Leidel , 2011
+# Jure Cuhalev , 2012-2013
+# Marko Zabreznik , 2016
+# Primož Verdnik , 2017
+# zejn , 2013,2016-2017
+# zejn , 2011-2013
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovenian (http://www.transifex.com/django/django/language/"
+"sl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sl\n"
+"Plural-Forms: nplurals=4;\n"
+
+msgid "Afrikaans"
+msgstr "Afrikanščina"
+
+msgid "Arabic"
+msgstr "Arabščina"
diff --git a/tests/i18n/contenttypes/locale/en/LC_MESSAGES/django.po b/tests/i18n/contenttypes/locale/en/LC_MESSAGES/django.po
index 2529ce6dfbcd7..dbdb6034d3e48 100644
--- a/tests/i18n/contenttypes/locale/en/LC_MESSAGES/django.po
+++ b/tests/i18n/contenttypes/locale/en/LC_MESSAGES/django.po
@@ -16,6 +16,7 @@ msgstr ""
"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"
#: models.py:6
msgid "Anything"
@@ -23,4 +24,4 @@ msgstr ""
#: models.py:15
msgid "Company"
-msgstr "Company"
\ No newline at end of file
+msgstr "Company"
diff --git a/tests/i18n/locale_root/cs/LC_MESSAGES/django.mo b/tests/i18n/locale_root/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..a640f358ca149
Binary files /dev/null and b/tests/i18n/locale_root/cs/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/locale_root/cs/LC_MESSAGES/django.po b/tests/i18n/locale_root/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..0f0662ddfcf56
--- /dev/null
+++ b/tests/i18n/locale_root/cs/LC_MESSAGES/django.po
@@ -0,0 +1,23 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel , 2011
+# Jan Papež , 2012
+# Jirka Vejrazka , 2011
+# Tomáš Ehrlich , 2015
+# Vláďa Macek , 2012-2014
+# Vláďa Macek , 2015-2019
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-18 11:37+0000\n"
+"Last-Translator: Vláďa Macek \n"
+"Language-Team: Czech (http://www.transifex.com/django/django/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
+"<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
diff --git a/tests/i18n/locale_root/es/LC_MESSAGES/django.mo b/tests/i18n/locale_root/es/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..2814f78ad5fc8
Binary files /dev/null and b/tests/i18n/locale_root/es/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/locale_root/es/LC_MESSAGES/django.po b/tests/i18n/locale_root/es/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..ba01a93d316ae
--- /dev/null
+++ b/tests/i18n/locale_root/es/LC_MESSAGES/django.po
@@ -0,0 +1,19 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Spanish (http://www.transifex.com/django/django/language/"
+"es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Do you like icecream?"
+msgstr "¿Os gusta el helado?"
diff --git a/tests/i18n/other/locale/cs/LC_MESSAGES/django.mo b/tests/i18n/other/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..891619034f175
Binary files /dev/null and b/tests/i18n/other/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/other/locale/cs/LC_MESSAGES/django.po b/tests/i18n/other/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..f9e9b3d180ee8
--- /dev/null
+++ b/tests/i18n/other/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,26 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel , 2011
+# Jan Papež , 2012
+# Jirka Vejrazka , 2011
+# Tomáš Ehrlich , 2015
+# Vláďa Macek , 2012-2014
+# Vláďa Macek , 2015-2019
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-18 11:37+0000\n"
+"Last-Translator: Vláďa Macek \n"
+"Language-Team: Czech (http://www.transifex.com/django/django/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
+
+msgid "Afrikaans"
+msgstr "afrikánsky"
diff --git a/tests/i18n/other/locale/lt/LC_MESSAGES/django.mo b/tests/i18n/other/locale/lt/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..f5f04b613df8d
Binary files /dev/null and b/tests/i18n/other/locale/lt/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/other/locale/lt/LC_MESSAGES/django.po b/tests/i18n/other/locale/lt/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..e74e3c86dd5ad
--- /dev/null
+++ b/tests/i18n/other/locale/lt/LC_MESSAGES/django.po
@@ -0,0 +1,18 @@
+# This file is distributed under the same license as the Django package.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Lithuanian (http://www.transifex.com/django/django/language/"
+"lt/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: lt\n"
+
+msgid "Afrikaans"
+msgstr "Afrikiečių"
diff --git a/tests/i18n/other/locale/sk/LC_MESSAGES/django.mo b/tests/i18n/other/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 0000000000000..62edf93facd63
Binary files /dev/null and b/tests/i18n/other/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/other/locale/sk/LC_MESSAGES/django.po b/tests/i18n/other/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 0000000000000..eb96dd0588650
--- /dev/null
+++ b/tests/i18n/other/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,25 @@
+# This file is distributed under the same license as the Django package.
+#
+# Translators:
+# Jannis Leidel , 2011
+# Juraj Bubniak , 2012-2013
+# Marian Andre , 2013,2015,2017-2018
+# Martin Kosír, 2011
+# Martin Tóth , 2017
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-09-27 22:40+0200\n"
+"PO-Revision-Date: 2019-11-05 00:38+0000\n"
+"Last-Translator: Ramiro Morales\n"
+"Language-Team: Slovak (http://www.transifex.com/django/django/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n == 1 ? 0 : n % 1 == 0 && n "
+">= 2 && n <= 4 ? 1 : n % 1 != 0 ? 2: 3);\n"
+
+msgid "a test string"
+msgstr "translated into sk"
\ No newline at end of file
diff --git a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
index 9a14a80ceb8f9..0bbf0e4e8526d 100644
--- a/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
+++ b/tests/i18n/patterns/locale/en/LC_MESSAGES/django.po
@@ -15,6 +15,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: urls/default.py:11
msgid "^translated/$"
diff --git a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo
index ebc475caa20d4..a57181b098fbb 100644
Binary files a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo and b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po
index 734f139b5c77c..fca21f824731a 100644
--- a/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po
+++ b/tests/i18n/sampleproject/locale/fr/LC_MESSAGES/django.po
@@ -3,6 +3,7 @@ msgstr ""
"Report-Msgid-Bugs-To: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: templates/percents.html:3
#, python-format
diff --git a/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.mo b/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.mo
index 454fef0c7a779..dec34a957e27d 100644
Binary files a/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.mo and b/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.po b/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.po
index 72013ab5798f9..55c796ac50ff0 100644
--- a/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.po
+++ b/tests/i18n/territorial_fallback/locale/de/LC_MESSAGES/django.po
@@ -16,6 +16,7 @@ msgstr ""
"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"
#. Translators: This comment should be extracted
#: __init__.py:1
diff --git a/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.mo b/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.mo
index 4d014cb1bf6dc..b0b70e0a3eee8 100644
Binary files a/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.mo and b/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.mo differ
diff --git a/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.po b/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.po
index e28fcd3405174..6df9f4e94ee5a 100644
--- a/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.po
+++ b/tests/i18n/territorial_fallback/locale/de_DE/LC_MESSAGES/django.po
@@ -16,6 +16,7 @@ msgstr ""
"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"
#. Translators: This comment should be extracted
#: __init__.py:1
diff --git a/tests/i18n/test_extraction.py b/tests/i18n/test_extraction.py
index 709a6133f58a5..808c91813f0e4 100644
--- a/tests/i18n/test_extraction.py
+++ b/tests/i18n/test_extraction.py
@@ -515,6 +515,7 @@ def test_symlink(self):
class CopyPluralFormsExtractorTests(ExtractorTests):
PO_FILE_ES = 'locale/es/LC_MESSAGES/django.po'
+ PO_FILE_RU = 'locale/ru/LC_MESSAGES/django.po'
def test_copy_plural_forms(self):
management.call_command('makemessages', locale=[LOCALE], verbosity=0)
@@ -547,6 +548,289 @@ def test_translate_and_plural_blocktranslate_collision(self):
self.assertMsgIdPlural('Plural for a `translate` and `blocktranslate` collision case', po_contents)
+class CollectBundledTests(ExtractorTests):
+
+ PO_FILE = 'locale_root/%s/LC_MESSAGES/django.po'
+ BUNDLED_LOCALES = ['cs', 'es', 'lt', 'mk', 'ru', 'sl', 'tt']
+
+ def test_single_locale(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ out = StringIO()
+ management.call_command(
+ 'makemessages', locale=['es'], collect_bundled='default-path',
+ stdout=out, verbosity=1
+ )
+ with open(self.PO_FILE % 'es', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = """
+ msgid "Afrikaans"
+ msgstr "Africano"
+ msgid "Arabic"
+ msgstr "Árabe"
+ msgid "Asturian"
+ msgstr "Asturiano"
+ """
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ def test_translators_string(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ management.call_command(
+ 'makemessages', locale=['cs'], collect_bundled='default-path',
+ verbosity=0
+ )
+ with open(self.PO_FILE % 'cs', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = "Contributors to this catalog are listed in"
+ self.assertIn(should_contain, po_contents)
+
+ def test_all_bundled(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ management.call_command('makemessages', collect_bundled='all-bundled', verbosity=0)
+ for locale in self.BUNDLED_LOCALES:
+ self.assertTrue(os.path.exists(self.PO_FILE % locale))
+
+ def test_new_locale(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ management.call_command(
+ 'makemessages', locale=['xxx'], collect_bundled='default-path',
+ verbosity=0
+ )
+ should_contain = """
+ "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
+ msgid "Love"
+ """
+ with open(self.PO_FILE % 'xxx', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ def test_no_locale_root_setting(self):
+ msg = ("currently makemessages only supports collecting bundled message files "
+ "with the LOCALE_ROOT setting defined.")
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command('makemessages', collect_bundled='default-path', verbosity=0)
+
+
+class UpdatePluralFormsTests(ExtractorTests):
+
+ PO_FILE_NEW = 'app_with_locale/locale/%s/LC_MESSAGES/django.po.new'
+ PO_FILE = 'app_with_locale/locale/%s/LC_MESSAGES/django.po'
+
+ def test_remapping_interactive(self):
+ """
+ Test the correct functioning of form remapping by --update-plural-forms.
+ """
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ with mock.patch('django.core.management.commands.makemessages.Command.get_user_input',
+ side_effect=['0', '1', '1', '9', '1', 'm', 'n']):
+ out = StringIO()
+ management.call_command(
+ 'makemessages', locale=['cs'], update_plural_forms='interactive',
+ stdout=out, verbosity=1
+ )
+ with open(self.PO_FILE_NEW % 'cs', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = """
+ "Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n "
+ "<= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n"
+
+ #, python-format
+ msgid ""
+ "Ensure that there are no more than %(max)s digit before the decimal point."
+ msgid_plural ""
+ "Ensure that there are no more than %(max)s digits before the decimal point."
+ msgstr[0] ""
+ "Ujistěte se, že hodnota neobsahuje více než %(max)s místo před desetinnou "
+ "čárkou (tečkou)."
+ msgstr[1] ""
+ "Ujistěte se, že hodnota neobsahuje více než %(max)s místa před desetinnou "
+ "čárkou (tečkou)."
+ msgstr[2] ""
+ "Ujistěte se, že hodnota neobsahuje více než %(max)s místa před desetinnou "
+ "čárkou (tečkou)."
+ msgstr[3] ""
+ "Ujistěte se, že hodnota neobsahuje více než %(max)s místa před desetinnou "
+ "čárkou (tečkou)."
+
+ #, python-format
+ msgid "Ensure that there are no more than %(max)s digit in total."
+ msgid_plural "Ensure that there are no more than %(max)s digits in total."
+ msgstr[0] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslici."
+ msgstr[1] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslice."
+ msgstr[2] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslice."
+ msgstr[3] "Ujistěte se, že pole neobsahuje celkem více než %(max)s číslice."
+ """
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ def test_trimming(self):
+ """
+ Test the correct functioning of trimming in --update-plural-forms ('trimming' is when the
+ main form has less plural forms than the user's forms.
+ """
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ with mock.patch('django.core.management.commands.makemessages.Command.get_user_input',
+ side_effect=['0', '1', 'n']):
+ out = StringIO()
+ management.call_command(
+ 'makemessages', locale=['lt'], update_plural_forms='interactive',
+ stdout=out, verbosity=1
+ )
+ with open(self.PO_FILE_NEW % 'lt', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = """
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
+ msgid "Password change"
+ msgstr "Slaptažodžio keitimas"
+ #, python-format
+ msgid "%(counter)s result"
+ msgid_plural "%(counter)s results"
+ msgstr[0] "%(counter)s rezultatas"
+ msgstr[1] "%(counter)s rezultatai"
+ """
+
+ should_not_contain = """msgstr[2] "%(counter)s rezultatai"
+ msgstr[3] "%(counter)s rezultatai"""
+
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ for line in should_not_contain.splitlines():
+ self.assertNotIn(line.strip(), po_contents)
+
+ def test_automation_remapping(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ management.call_command('makemessages', locale=['tt'], update_plural_forms='0,0', verbosity=0)
+ with open(self.PO_FILE % 'tt', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = """
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
+ #, python-format
+ msgid "%d day"
+ msgid_plural "%d days"
+ msgstr[0] ""
+ msgstr[1] ""
+ """
+
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ def test_automation_bad_form_map(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ msg = ("currently the --update-plural-forms option only supports "
+ "'interactive', 'copy' or comma-separated digits as a parameter.")
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command('makemessages', locale=['tt'], update_plural_forms='0,b', verbosity=0)
+
+ def test_copy_interactive(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.get_user_input',
+ side_effect=['u', 'y', 'm', 'n']):
+ out = StringIO()
+ management.call_command(
+ 'makemessages', locale=['es_XX'], update_plural_forms='interactive',
+ stdout=out, verbosity=1
+ )
+ with open(self.PO_FILE_NEW % 'es_XX', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = "Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+ self.assertIn(should_contain, po_contents)
+
+ def test_automation_copy(self):
+ management.call_command('makemessages', locale=['en'], update_plural_forms='copy', verbosity=0)
+ with open(self.PO_FILE % 'en', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = "Plural-Forms: nplurals=2; plural=(n != 1);\\n"
+ self.assertIn(should_contain, po_contents)
+
+ def test_locale_root(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ management.call_command('makemessages', locale=['sk'], update_plural_forms='2,3', verbosity=0)
+ with open(self.PO_FILE % 'sk', encoding='utf-8') as fp:
+ po_contents = fp.read()
+ should_contain = """
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
+ msgid "Please submit %d or fewer forms."
+ msgid_plural "Please submit %d or fewer forms."
+ msgstr[0] "Prosím odošlite %d alebo menej formulárov. -2-"
+ msgstr[1] "Prosím odošlite %d alebo menej formulárov. -3-"
+ """
+
+ should_not_contain = """msgstr[2] "Prosím odošlite %d alebo menej formulárov. -2-"
+ msgstr[3] "Prosím odošlite %d alebo menej formulárov. -3-" """
+
+ for line in should_contain.splitlines():
+ self.assertIn(line.strip(), po_contents)
+
+ for line in should_not_contain.splitlines():
+ self.assertNotIn(line.strip(), po_contents)
+
+ def test_msgfmt_check_main_form(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ msg = "invalid plural"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command(
+ 'makemessages', locale=['mk'], update_plural_forms='interactive', verbosity=0
+ )
+
+ def test_msgfmt_check_user_form(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ msg = "invalid nplurals value"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command('makemessages', locale=['mk'], update_plural_forms='interactive', verbosity=0)
+
+ def test_plural_forms_check_main_form(self):
+ """
+ In some cases, msgfmt --check deems the plural forms as valid if
+ they not contain either 'nplurals' or 'plural' (bug in msgfmt).
+ """
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ msg = "unable to parse"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command(
+ 'makemessages', locale=['sl'], update_plural_forms='interactive', verbosity=0
+ )
+
+ def test_plural_forms_check_user_form(self):
+ with mock.patch('django.core.management.commands.makemessages.Command.django_dir',
+ os.path.join(self.test_dir, 'django_dir')):
+ msg = "unable to parse"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command('makemessages', locale=['sl'], update_plural_forms='interactive', verbosity=0)
+
+ def test_incompatible_form_map(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ msg = "The provided form map is not compatible"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command('makemessages', locale=['sk'], update_plural_forms='4,5', verbosity=0)
+
+ def test_no_main_po(self):
+ with override_settings(LOCALE_ROOT=os.path.join(self.test_dir, 'locale_root')):
+ msg = "unable to find the main .po file"
+ with self.assertRaisesMessage(CommandError, msg):
+ management.call_command(
+ 'makemessages', locale=['xxx'], domain='djangojs', update_plural_forms='4,5',
+ verbosity=0
+ )
+
+
class NoWrapExtractorTests(ExtractorTests):
def test_no_wrap_enabled(self):
diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py
index ac813b3439f50..4ab275d0bf765 100644
--- a/tests/i18n/tests.py
+++ b/tests/i18n/tests.py
@@ -1,6 +1,7 @@
import datetime
import decimal
import gettext as gettext_module
+import glob
import os
import pickle
import re
@@ -12,6 +13,7 @@
from asgiref.local import Local
+import django
from django import forms
from django.apps import AppConfig
from django.conf import settings
@@ -1780,6 +1782,98 @@ def test_failure_finding_default_mo_files(self):
with self.assertRaises(OSError):
activate('en')
+ def test_failure_finding_mo_files_with_locale_root(self):
+ """
+ Only a warning is raised if no .mo files are found when
+ setting.LOCALE_ROOT is set.
+ """
+ with override_settings(LOCALE_ROOT=os.path.join(here, 'locale_root/')):
+ trans_real._translations = {}
+ msg = (
+ 'LOCALE_ROOT has been set and no translation files found for '
+ 'default language'
+ )
+ with self.assertWarnsMessage(RuntimeWarning, msg):
+ activate('en')
+
+
+class LocaleRootTests(SimpleTestCase):
+ """
+ Test the functioning of the LOCALE_ROOT setting.
+ """
+ @override_settings(
+ USE_I18N=True,
+ LANGUAGE_CODE='es',
+ LOCALE_ROOT=os.path.join(here, 'locale_root/')
+ )
+ def test_correct_functioning(self):
+ self.assertEqual(gettext("Do you like icecream?"), "¿Os gusta el helado?")
+ self.assertEqual(gettext("password"), "password")
+
+
+class CatalogMergingTests(SimpleTestCase):
+ """
+ Test the functioning of catalog merging in translations
+ """
+
+ @override_settings(
+ PLURAL_FORMS_CONSISTENCY=True
+ )
+ def test_django_bundled_catalogs_consistency(self):
+ app_configs = []
+ django_dir = os.path.normpath(os.path.join(os.path.dirname(django.__file__)))
+ contrib_apps_dirs = glob.glob(django_dir + '/contrib/*')
+ for contrib_app_dir in contrib_apps_dirs:
+ app_configs.append(
+ AppConfig(contrib_app_dir, AppModuleStub(__path__=[contrib_app_dir]))
+ )
+ with mock.patch('django.apps.apps.get_app_configs', return_value=app_configs):
+ for lang in LANG_INFO:
+ activate(lang)
+ # If it gets up to here, no warning have been issued and all catalogs
+ # have been merged correctly (any warning will make this test fail).
+
+ @override_settings(
+ PLURAL_FORMS_CONSISTENCY=True,
+ LOCALE_PATHS=extended_locale_paths
+ )
+ def test_catalog_with_plural_forms_merged(self):
+ activate('sk')
+ self.assertEqual(gettext("a test string"), "translated into sk")
+
+ @override_settings(
+ PLURAL_FORMS_CONSISTENCY=True,
+ LANGUAGE_CODE='es',
+ LOCALE_ROOT=os.path.join(here, 'locale_root/'),
+ LOCALE_PATHS=extended_locale_paths
+ )
+ def test_catalog_with_different_plural_forms(self):
+ msg = 'Posible inconsistencies and undesired behavior detected'
+ with self.assertWarnsMessage(RuntimeWarning, msg):
+ activate('cs')
+
+ @override_settings(
+ PLURAL_FORMS_CONSISTENCY=True,
+ LANGUAGE_CODE='es',
+ LOCALE_PATHS=extended_locale_paths
+ )
+ def test_catalog_without_plural_forms(self):
+ msg = 'Posible inconsistencies and undesired behavior detected'
+ with self.assertWarnsMessage(RuntimeWarning, msg):
+ activate('lt')
+
+ @override_settings(
+ PLURAL_FORMS_CONSISTENCY=False,
+ LANGUAGE_CODE='es',
+ LOCALE_PATHS=extended_locale_paths
+ )
+ def test_inconsistent_catalog_merging(self):
+ trans_real.reset_translations_cache()
+ activate('cs')
+ activate('lt')
+ # If it gets up to here, no warning have been issued and all catalogs
+ # have been merged correctly (any warning will make this test fail).
+
class NonDjangoLanguageTests(SimpleTestCase):
"""
diff --git a/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.mo b/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.mo
index e755e5baeade0..73bd6f9e54429 100644
Binary files a/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.mo and b/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.po b/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.po
index de1b96611ce05..efb396f901849 100644
--- a/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.po
+++ b/tests/urlpatterns_reverse/translations/locale/fr/LC_MESSAGES/django.po
@@ -14,7 +14,7 @@ msgstr ""
"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"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
msgid "^foo/$"
msgstr "^foo-fr/$"
diff --git a/tests/view_tests/locale/nl/LC_MESSAGES/django.mo b/tests/view_tests/locale/nl/LC_MESSAGES/django.mo
index ef12f2ade80a2..4e8770621b18c 100644
Binary files a/tests/view_tests/locale/nl/LC_MESSAGES/django.mo and b/tests/view_tests/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/tests/view_tests/locale/nl/LC_MESSAGES/django.po b/tests/view_tests/locale/nl/LC_MESSAGES/django.po
index 4e5f7e2fb598b..7db917a079dc2 100644
--- a/tests/view_tests/locale/nl/LC_MESSAGES/django.po
+++ b/tests/view_tests/locale/nl/LC_MESSAGES/django.po
@@ -15,6 +15,7 @@ msgstr ""
"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"
#: urls.py:78
msgid "^translated/$"