Browse Source

Add useful properties to Locale

- Update get_display_name() to always return a string
- Add documentation and unit tests
Andy Babic 2 years ago
parent
commit
f680f188f3

+ 1 - 0
CHANGELOG.txt

@@ -20,6 +20,7 @@ Changelog
  * Ensure that changed or cleared selection from choosers will dispatch a DOM `change` event (George Sakkis)
  * Add the ability to disable model indexing by setting `search_fields = []` (Daniel Kirkham)
  * Enhance `wagtail.search.utils.parse_query_string` to allow inner single quotes for key/value parsing (Aman Pandey)
+ * Add helpful properties to `Locale` for more convenient usage within templates (Andy Babic)
  * Fix: Ensure `label_format` on StructBlock gracefully handles missing variables (Aadi jindal)
  * Fix: Adopt a no-JavaScript and more accessible solution for the 'Reset to default' switch to Gravatar when editing user profile (Loveth Omokaro)
  * Fix: Ensure `Site.get_site_root_paths` works on cache backends that do not preserve Python objects (Jaap Roes)

+ 14 - 16
docs/advanced_topics/i18n.md

@@ -355,7 +355,7 @@ languages.
 
 If you're not convinced that you need this, have a look at [https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings](https://www.w3.org/International/questions/qa-site-conneg#yyyshortcomings) for some rationale.
 
-(basic_example)=
+(i18n_basic_example)=
 
 ##### Basic example
 
@@ -371,15 +371,13 @@ otherwise skip to the next section that has a more complicated example which tak
 this into account.
 
 ```html+django
-
 {# make sure these are at the top of the file #}
-{% load i18n wagtailcore_tags %}
+{% load wagtailcore_tags %}
 
 {% if page %}
     {% for translation in page.get_translations.live %}
-        {% get_language_info for translation.locale.language_code as lang %}
-        <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
-            {{ lang.name_local }}
+        <a href="{% pageurl translation %}" rel="alternate" hreflang="{{ translation.locale.language_code }}">
+            {{ translation.locale.language_name_local }}
         </a>
     {% endfor %}
 {% endif %}
@@ -404,26 +402,26 @@ If this is part of a shared base template it may be used in situations where no
 This `for` block iterates through all published translations of the current page.
 
 ```html+django
-{% get_language_info for translation.locale.language_code as lang %}
+<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ translation.locale.language_code }}">
+    {{ translation.locale.language_name_local }}
+</a>
 ```
 
-This is a Django built-in tag that gets info about the language of the translation.
+This adds a link to the translation. We use `{{ translation.locale.language_name_local }}` to display
+the name of the locale in its own language. We also add `rel` and `hreflang` attributes to the `<a>` tag for SEO.
+`translation.locale` is an instance of the [Locale model](locale_model_ref).
+
+Alternatively, a built-in tag from Django that gets info about the language of the translation.
 For more information, see [get_language_info() in the Django docs](https://docs.djangoproject.com/en/stable/topics/i18n/translation/#django.utils.translation.get_language_info).
 
 ```html+django
-<a href="{% pageurl translation %}" rel="alternate" hreflang="{{ language_code }}">
-    {{ lang.name_local }}
-</a>
+{% get_language_info for translation.locale.language_code as lang %}
 ```
 
-This adds a link to the translation. We use `{{ lang.name_local }}` to display
-the name of the locale in its own language. We also add `rel` and `hreflang`
-attributes to the `<a>` tag for SEO.
-
 ##### Handling locales that share content
 
 Rather than iterating over pages, this example iterates over all of the configured
-languages and finds the page for each one. This works better than the [Basic example](basic_example)
+languages and finds the page for each one. This works better than the [Basic example](i18n_basic_example)
 above on sites that have extra Django `LANGUAGES` that share the same Wagtail content.
 
 For this example to work, you firstly need to add Django's

+ 14 - 0
docs/reference/pages/model_reference.md

@@ -392,6 +392,8 @@ The {meth}`~wagtail.models.Site.find_for_request` function returns the Site obje
     .. automethod:: get_site_root_paths
 ```
 
+(locale_model_ref)=
+
 ## `Locale`
 
 The `Locale` model defines the set of languages and/or locales that can be used on a site.
@@ -425,6 +427,18 @@ database queries making them unable to be edited or viewed.
 
     .. automethod:: get_active
 
+    .. autoattribute:: language_name
+
+    .. autoattribute:: language_name_local
+
+    .. autoattribute:: language_name_localized
+
+    .. autoattribute:: is_default
+
+    .. autoattribute:: is_active
+
+    .. autoattribute:: is_bidi
+
     .. automethod:: get_display_name
 ```
 

+ 1 - 0
docs/releases/5.0.md

@@ -32,6 +32,7 @@ Support for adding custom validation logic to StreamField blocks has been formal
  * Ensure that changed or cleared selection from choosers will dispatch a DOM `change` event (George Sakkis)
  * Add the ability to [disable model indexing](wagtailsearch_disable_indexing) by setting `search_fields = []` (Daniel Kirkham)
  * Enhance `wagtail.search.utils.parse_query_string` to allow inner single quotes for key/value parsing (Aman Pandey)
+ * Add helpful properties to [`Locale`](locale_model_ref) for more convenient usage within templates, see [](i18n_basic_example) (Andy Babic)
 
 ### Bug fixes
 

+ 89 - 3
wagtail/models/i18n.py

@@ -1,4 +1,5 @@
 import uuid
+from typing import Dict
 
 from django.apps import apps
 from django.conf import settings
@@ -81,11 +82,96 @@ class Locale(models.Model):
     def language_code_is_valid(self):
         return self.language_code in get_content_languages()
 
-    def get_display_name(self):
-        return get_content_languages().get(self.language_code)
+    def get_display_name(self) -> str:
+        try:
+            return get_content_languages()[self.language_code]
+        except KeyError:
+            pass
+        try:
+            return self.language_name
+        except KeyError:
+            pass
+
+        return self.language_code
 
     def __str__(self):
-        return force_str(self.get_display_name() or self.language_code)
+        return force_str(self.get_display_name())
+
+    def _get_language_info(self) -> Dict[str, str]:
+        return translation.get_language_info(self.language_code)
+
+    @property
+    def language_info(self):
+        return translation.get_language_info(self.language_code)
+
+    @property
+    def language_name(self):
+        """
+        Uses data from ``django.conf.locale`` to return the language name in
+        English. For example, if the object's ``language_code`` were ``"fr"``,
+        the return value would be ``"French"``.
+
+        Raises ``KeyError`` if ``django.conf.locale`` has no information
+        for the object's ``language_code`` value.
+        """
+        return self.language_info["name"]
+
+    @property
+    def language_name_local(self):
+        """
+        Uses data from ``django.conf.locale`` to return the language name in
+        the language itself. For example, if the ``language_code`` were
+        ``"fr"`` (French), the return value would be ``"français"``.
+
+        Raises ``KeyError`` if ``django.conf.locale`` has no information
+        for the object's ``language_code`` value.
+        """
+        return self.language_info["name_local"]
+
+    @property
+    def language_name_localized(self):
+        """
+        Uses data from ``django.conf.locale`` to return the language name in
+        the currently active language. For example, if ``language_code`` were
+        ``"fr"`` (French), and the active language were ``"da"`` (Danish), the
+        return value would be ``"Fransk"``.
+
+        Raises ``KeyError`` if ``django.conf.locale`` has no information
+        for the object's ``language_code`` value.
+
+        """
+        return translation.gettext(self.language_name)
+
+    @property
+    def is_bidi(self) -> bool:
+        """
+        Returns a boolean indicating whether the language is bi-directional.
+        """
+        return self.language_code in settings.LANGUAGES_BIDI
+
+    @property
+    def is_default(self) -> bool:
+        """
+        Returns a boolean indicating whether this object is the default locale.
+        """
+        try:
+            return self.language_code == get_supported_content_language_variant(
+                settings.LANGUAGE_CODE
+            )
+        except LookupError:
+            return False
+
+    @property
+    def is_active(self) -> bool:
+        """
+        Returns a boolean indicating whether this object is the currently active locale.
+        """
+        try:
+            return self.language_code == get_supported_content_language_variant(
+                translation.get_language()
+            )
+        except LookupError:
+            return self.is_default
 
 
 class TranslatableMixin(models.Model):

+ 111 - 17
wagtail/tests/test_locale_model.py

@@ -36,27 +36,121 @@ class TestLocaleModel(TestCase):
         with translation.override("fr"):
             self.assertEqual(Locale.get_active().language_code, "fr")
 
-    def test_get_display_name(self):
-        locale = Locale.objects.get(language_code="en")
-        self.assertEqual(locale.get_display_name(), "English")
-
-    def test_get_display_name_for_unconfigured_language(self):
-        # This language is not in LANGUAGES so it should just return the language code
-        locale = Locale.objects.create(language_code="foo")
-        self.assertIsNone(locale.get_display_name())
+    def test_language_name(self):
+        for language_code, expected_result in (
+            ("en", "English"),
+            ("fr", "French"),
+            ("zh-hans", "Simplified Chinese"),
+        ):
+            with self.subTest(language_code):
+                locale = Locale(language_code=language_code)
+                self.assertEqual(locale.language_name, expected_result)
+
+    def test_language_name_for_unrecognised_language(self):
+        locale = Locale(language_code="foo")
+        with self.assertRaises(KeyError):
+            locale.language_name
+
+    def test_language_name_local(self):
+        for language_code, expected_result in (
+            ("en", "English"),
+            ("fr", "français"),
+            ("zh-hans", "简体中文"),
+        ):
+            with self.subTest(language_code):
+                locale = Locale(language_code=language_code)
+                self.assertEqual(locale.language_name_local, expected_result)
+
+    def test_language_name_local_for_unrecognised_language(self):
+        locale = Locale(language_code="foo")
+        with self.assertRaises(KeyError):
+            locale.language_name_local
+
+    def test_language_name_localized_reflects_active_language(self):
+        for language_code in (
+            "fr",  # French
+            "zh-hans",  # Simplified Chinese
+            "ca",  # Catalan
+            "de",  # German
+        ):
+            with self.subTest(language_code):
+                locale = Locale(language_code=language_code)
+                with translation.override("en"):
+                    self.assertEqual(
+                        locale.language_name_localized, locale.language_name
+                    )
+                with translation.override(language_code):
+                    # NB: Casing can differ between these, hence the lower()
+                    self.assertEqual(
+                        locale.language_name_localized.lower(),
+                        locale.language_name_local.lower(),
+                    )
+
+    def test_language_name_localized_for_unconfigured_language(self):
+        locale = Locale(language_code="zh-hans")
+        self.assertEqual(locale.language_name_localized, "Simplified Chinese")
+        with translation.override("zh-hans"):
+            self.assertEqual(locale.language_name_localized, locale.language_name_local)
+
+    def test_language_name_localized_for_unrecognised_language(self):
+        locale = Locale(language_code="foo")
+        with self.assertRaises(KeyError):
+            locale.language_name_localized
+
+    def test_is_bidi(self):
+        for language_code, expected_result in (
+            ("en", False),
+            ("ar", True),
+            ("he", True),
+            ("fr", False),
+            ("foo", False),
+        ):
+            with self.subTest(language_code):
+                locale = Locale(language_code=language_code)
+                self.assertIs(locale.is_bidi, expected_result)
+
+    def test_is_default(self):
+        for language_code, expected_result in (
+            (settings.LANGUAGE_CODE, True),  # default
+            ("zh-hans", False),  # alternative
+            ("foo", False),  # invalid
+        ):
+            with self.subTest(language_code):
+                locale = Locale(language_code=language_code)
+                self.assertIs(locale.is_default, expected_result)
+
+    def test_is_active(self):
+        for locale_language, active_language, expected_result in (
+            (settings.LANGUAGE_CODE, settings.LANGUAGE_CODE, True),
+            (settings.LANGUAGE_CODE, "fr", False),
+            ("zh-hans", settings.LANGUAGE_CODE, False),
+            ("en", "en-gb", True),
+            ("foo", settings.LANGUAGE_CODE, False),
+        ):
+            with self.subTest(f"locale={locale_language} active={active_language}"):
+                with translation.override(active_language):
+                    locale = Locale(language_code=locale_language)
+                    self.assertEqual(locale.is_active, expected_result)
 
-    def test_str(self):
-        locale = Locale.objects.get(language_code="en")
-        self.assertEqual(str(locale), "English")
-
-    def test_str_for_unconfigured_language(self):
-        # This language is not in LANGUAGES so it should just return the language code
-        locale = Locale.objects.create(language_code="foo")
-        self.assertEqual(str(locale), "foo")
+    def test_get_display_name(self):
+        for language_code, expected_result in (
+            ("en", "English"),  # configured
+            ("zh-hans", "Simplified Chinese"),  # not configured but valid
+            ("foo", "foo"),  # not configured or valid
+        ):
+            locale = Locale(language_code=language_code)
+            with self.subTest(language_code):
+                self.assertEqual(locale.get_display_name(), expected_result)
+
+    def test_str_reflects_get_display(self):
+        for language_code in ("en", "zh-hans", "foo"):
+            locale = Locale(language_code=language_code)
+            with self.subTest(language_code):
+                self.assertEqual(str(locale), locale.get_display_name())
 
     @override_settings(LANGUAGES=[("en", _("English")), ("fr", _("French"))])
     def test_str_when_languages_uses_gettext(self):
-        locale = Locale.objects.get(language_code="en")
+        locale = Locale(language_code="en")
         self.assertIsInstance(locale.__str__(), str)
 
     @override_settings(LANGUAGE_CODE="fr")