Repository: timvink/mkdocs-git-revision-date-localized-plugin

Issue:
locale can be longer than just 2 letter code
For this assertion:

https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/blob/708128b1358d45da4aaa4629f4e1ca7df76a3236/mkdocs_git_revision_date_localized_plugin/plugin.py#L117

...and the way `locale` is used:

https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/blob/708128b1358d45da4aaa4629f4e1ca7df76a3236/mkdocs_git_revision_date_localized_plugin/util.py#L9
https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/blob/708128b1358d45da4aaa4629f4e1ca7df76a3236/mkdocs_git_revision_date_localized_plugin/util.py#L72
https://github.com/timvink/mkdocs-git-revision-date-localized-plugin/blob/9747e59c37d5969c5a99631677effe73f9c779c3/mkdocs_git_revision_date_localized_plugin/js/timeago_mkdocs_material.js#L9

...and the document of Babel and timeago:

- [for Babel](https://babel.pocoo.org/en/latest/locale.html)
- [for timeago/timeago.js](https://timeago.org/)

Instead of just 2 letter code, `locale` also support longer format like `en_US`, or even:
- [RFC3066](https://datatracker.ietf.org/doc/html/rfc3066.html) for Babel (`date`, `datetime`...),
- [`en_short`](https://github.com/hustcc/timeago.js/blob/master/src/lang/en_short.ts) .etc for `timeago`.

This assertion should be lifted, and document of this plugin may be changed accordingly, from ISO 639-1 to the document of Babel and timeago.

---

Change locale to `locale: { default: en, babel: en_US, timeago: en_short }` or:

``` yaml
locale: en
locale_babel: en_US
locale_timeago: en_short
```

... may also be considered.

Discussion:
@timvink: Thanks for reporting this. I agree we should support this, using a 2-letter locale is an unnecessary simplification.

I'm thinking of writing a kind of validation function, that depending on the `type` validates the `locale` input. I would like to throw a helpful error message when a user has invalid input.

Separate locale input is also something to be considered. Great for developers that want to use multiple time formats in a theme, but also additional cognitive overhead for regular users. 

Will give it some thought next time I work on this plugin. In the meantime I could remove the assertion if that helps your use-case? @Crystal-RainSlide: For me, I can just use JavaScript to change the strings afterwards, and I don't need to specify the sub-language tag right now, thus I'm not in a hurry.

The need is focused on `pt_BR`, `zh_Hant` / `zh_HK` / `zh_TW` and so on, as `pt` defaults to `pt_PT`, and `zh` defaults to `zh_Hans` / `zh_CN`.@EDIflyer: Agree would be good to change - I was trying using `en-gb` at first to get non-US date format but then realised only `en` was allowed 😉@tooilxui: It is recommended to remove this restriction first. This will be very helpful for Chinese-speaking countries. For us, it is the same as zh but there are more than three differences.@jonaharagon: > Will give it some thought next time I work on this plugin. In the meantime I could remove the assertion if that helps your use-case?

@timvink This would help my use-case, my zh-Hant mkdocs-material site builds are now broken after #143 was fixed.@tooilxui: > > Will give it some thought next time I work on this plugin. In the meantime I could remove the assertion if that helps your use-case?
> 
> @timvink This would help my use-case, my zh-Hant mkdocs-material site builds are now broken after #143 was fixed.

@jonaharagon agree with you

/cc @timvink @LorhanSohaky: Any update? I'm trying to use pt-br

Solution patch:
diff --git a/docs/howto/specify-locale.md b/docs/howto/specify-locale.md
index cfeb46d..1123898 100644
--- a/docs/howto/specify-locale.md
+++ b/docs/howto/specify-locale.md
@@ -1,6 +1,6 @@
 # Specify a locale
 
-`locale` is a two letter [ISO639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code that `git-revision-date-localized` uses to display dates in your preferred language.
+`locale` is aa two letter [ISO639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code (f.e. `en`) or [5-letter language code with added territory/region/country](https://www.mkdocs.org/user-guide/localizing-your-theme/#supported-locales) (`en_US`) that `git-revision-date-localized` uses to display dates in your preferred language.
 
 For example:
 
@@ -68,6 +68,7 @@ plugins:
 If no `locale` is specified anywhere, the fallback is English with the US date format (`en`).
 
 !!! info "Supported locales"
+
     - When used in combination with `type: date` or `type: datetime`, translation is done using [babel](https://github.com/python-babel/babel) which supports [these locales](http://www.unicode.org/cldr/charts/latest/supplemental/territory_language_information.html)
 
     - When used in combination with `type: timeago` then [timeago.js](https://github.com/hustcc/timeago.js) is added to your website, which supports [these locales](https://github.com/hustcc/timeago.js/tree/master/src/lang). If you specify a locale not supported by timeago.js, the fallback is English (`en`)
diff --git a/docs/options.md b/docs/options.md
index 1f00e52..c0835d6 100644
--- a/docs/options.md
+++ b/docs/options.md
@@ -53,17 +53,19 @@ Default is `UTC`. Specify a time zone database name ([reference](https://en.wiki
 
 ## `locale`
 
-Default is `None`. Specify a two letter [ISO639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code to display dates in your preferred language. Note this plugin supports many different ways to [specify the locale](howto/specify-locale.md), but if not specified anywhere the fallback is English (`en`).
+Default is `None`. Specify a two letter [ISO639](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code (f.e. `en`) or [5-letter language code with added territory/region/country](https://www.mkdocs.org/user-guide/localizing-your-theme/#supported-locales) (`en_US`) to display dates in your preferred language. Note this plugin supports many different ways to [specify the locale](howto/specify-locale.md), but if not specified _anywhere_ the fallback will be English (`en`). `locale` is used to translate timestamps to dates when `type: date` or `type: datetime` (using [babel](https://github.com/python-babel/babel)) as well as to translate datetimes to a relative timeago string when `type: timeago` (using [timeago.js](https://github.com/hustcc/timeago.js)).
 
 Example outputs:
 
 ```yaml
-April 27, 2021                # `locale: en` with `type: date` (default)
-April 27, 2021 13:11:28       # `locale: en` with `type: datetime`
-2 weeks ago                   # `locale: en` with `type: timeago`
-27 de marzo de 2021           # `locale: es` with `type: date`
-27 de marzo de 2021 13:57:28  # `locale: es` with `type: datetime`
-hace 2 semanas                # `locale: es` with `type: timeago`
+# `locale: en`
+April 27, 2021                # with `type: date` (default)
+April 27, 2021 13:11:28       # with `type: datetime`
+2 weeks ago                   # with `type: timeago`
+# `locale: es`
+27 de marzo de 2021           # with `type: date`
+27 de marzo de 2021 13:57:28  # with `type: datetime`
+hace 2 semanas                # with `type: timeago`
 ```
 
 === ":octicons-file-code-16: mkdocs.yml"
@@ -71,9 +73,13 @@ hace 2 semanas                # `locale: es` with `type: timeago`
   ```yaml
   plugins:
     - git-revision-date-localized:
-        locale: en
+        locale: en_US
   ```
 
+!!! note when using `type: timeago`
+
+  When using `type: timeago` then [timeago.js](https://github.com/hustcc/timeago.js) is added to your website, which supports [these locales](https://github.com/hustcc/timeago.js/tree/master/src/lang). If you specify a locale not supported by timeago.js, the fallback is English (`en`). It might happen that your specific locale is supported by babel (used by date formats) but not by timeago. In that case open an issue with this plugin.
+
 
 ## `fallback_to_build_date`
 
diff --git a/src/mkdocs_git_revision_date_localized_plugin/dates.py b/src/mkdocs_git_revision_date_localized_plugin/dates.py
index 4eafe7e..3fd4dd6 100644
--- a/src/mkdocs_git_revision_date_localized_plugin/dates.py
+++ b/src/mkdocs_git_revision_date_localized_plugin/dates.py
@@ -1,4 +1,4 @@
-from babel.dates import format_date, get_timezone
+from babel.dates import format_date, get_timezone, format_datetime
 
 from datetime import datetime, timezone
 from typing import Any, Dict
@@ -36,5 +36,56 @@ def get_date_formats(
         "iso_date": loc_revision_date.strftime("%Y-%m-%d"),
         "iso_datetime": loc_revision_date.strftime("%Y-%m-%d %H:%M:%S"),
         "timeago": '<span class="timeago" datetime="%s" locale="%s"></span>' % (loc_revision_date.isoformat(), locale),
-        "custom": loc_revision_date.strftime(custom_format),
+        "custom": format_datetime(loc_revision_date, format=strftime_to_babel_format(custom_format), locale=locale),
     }
+
+
+def strftime_to_babel_format(fmt: str) -> str:
+    """
+    Convert strftime format string to Babel format pattern.
+    
+    Args:
+        fmt (str): strftime format string
+        
+    Returns:
+        str: Babel format pattern
+    """
+    # Dictionary mapping strftime directives to Babel format patterns
+    mapping = {
+        '%a': 'EEE',     # Weekday abbreviated
+        '%A': 'EEEE',    # Weekday full
+        '%b': 'MMM',     # Month abbreviated
+        '%B': 'MMMM',    # Month full
+        '%c': '',        # Locale's date and time (not directly mappable)
+        '%d': 'dd',      # Day of month zero-padded
+        '%-d': 'd',      # Day of month
+        '%e': 'd',       # Day of month space-padded
+        '%f': 'SSSSSS',  # Microsecond
+        '%H': 'HH',      # Hour 24h zero-padded
+        '%-H': 'H',      # Hour 24h
+        '%I': 'hh',      # Hour 12h zero-padded
+        '%-I': 'h',      # Hour 12h
+        '%j': 'DDD',     # Day of year
+        '%m': 'MM',      # Month zero-padded
+        '%-m': 'M',      # Month
+        '%M': 'mm',      # Minute zero-padded
+        '%-M': 'm',      # Minute
+        '%p': 'a',       # AM/PM
+        '%S': 'ss',      # Second zero-padded
+        '%-S': 's',      # Second
+        '%w': 'e',       # Weekday as number
+        '%W': 'w',       # Week of year
+        '%x': '',        # Locale's date (not directly mappable)
+        '%X': '',        # Locale's time (not directly mappable)
+        '%y': 'yy',      # Year without century
+        '%Y': 'yyyy',    # Year with century
+        '%z': 'Z',       # UTC offset
+        '%Z': 'z',       # Timezone name
+        '%%': '%'        # Literal %
+    }
+    
+    result = fmt
+    for strftime_code, babel_code in mapping.items():
+        result = result.replace(strftime_code, babel_code)
+    
+    return result
\ No newline at end of file
diff --git a/src/mkdocs_git_revision_date_localized_plugin/plugin.py b/src/mkdocs_git_revision_date_localized_plugin/plugin.py
index a33f96b..49747da 100644
--- a/src/mkdocs_git_revision_date_localized_plugin/plugin.py
+++ b/src/mkdocs_git_revision_date_localized_plugin/plugin.py
@@ -81,7 +81,7 @@ class GitRevisionDateLocalizedPlugin(BasePlugin):
         # Get locale from plugin configuration
         plugin_locale = self.config.get("locale", None)
 
-        # theme locale
+        # Get locale from theme configuration
         if "theme" in config and "language" in config.get("theme"):
             custom_theme = config.get("theme")
             theme_locale = (
@@ -96,7 +96,6 @@ class GitRevisionDateLocalizedPlugin(BasePlugin):
                 custom_theme.locale if Version(mkdocs_version) >= Version("1.6.0") else custom_theme._vars.get("locale")
             )
             logging.debug("Locale '%s' extracted from the custom theme: '%s'" % (theme_locale, custom_theme.name))
-
         else:
             theme_locale = None
             logging.debug("No locale found in theme configuration (or no custom theme set)")
@@ -116,9 +115,6 @@ class GitRevisionDateLocalizedPlugin(BasePlugin):
 
         # Validate locale
         locale_set = str(locale_set)
-        assert len(locale_set) == 2, (
-            "locale must be a 2 letter code, see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"
-        )
 
         # set locale also in plugin configuration
         self.config["locale"] = locale_set
@@ -173,7 +169,6 @@ class GitRevisionDateLocalizedPlugin(BasePlugin):
         # First prio is use mkdocs-static-i18n locale if set
         try:
             locale = page.file.locale
-
         except AttributeError:
             locale = None
 
@@ -183,18 +178,12 @@ class GitRevisionDateLocalizedPlugin(BasePlugin):
                 locale = page.meta["locale"]
 
         # Finally, if no page locale set, we take the locale determined on_config()
+        # (fourth prio is plugin configuration)
+        # (firth prio is theme configuration)
+        # (sixth prio is fallback to English)
         if not locale:
             locale = self.config.get("locale")
 
-        # MkDocs supports 2-letter and 5-letter locales
-        # https://www.mkdocs.org/user-guide/localizing-your-theme/#supported-locales
-        # We need the 2 letter variant
-        if len(locale) == 5:
-            locale = locale[:2]
-        assert len(locale) == 2, (
-            "locale must be a 2 letter code, see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes"
-        )
-
         # Retrieve git commit timestamp
         # Except for generated pages (f.e. by mkdocs-gen-files plugin)
         if getattr(page.file, "generated_by", None):


Test patch:
diff --git a/tests/test_dates.py b/tests/test_dates.py
index bbe0e94..c98750c 100644
--- a/tests/test_dates.py
+++ b/tests/test_dates.py
@@ -1,7 +1,7 @@
 import pytest
 from datetime import datetime, timezone
 from babel.dates import get_timezone
-
+from babel.core import UnknownLocaleError
 from mkdocs_git_revision_date_localized_plugin.dates import get_date_formats
 
 
@@ -18,6 +18,37 @@ def test_get_dates():
     }
     assert get_date_formats(0) == expected_output
 
+    # Test with 4-letter locale
+    new_expected_output = expected_output.copy()
+    new_expected_output["timeago"] = '<span class="timeago" datetime="1970-01-01T00:00:00+00:00" locale="en_US"></span>'
+    assert get_date_formats(0, locale="en_US") == new_expected_output
+    
+    # Test with different locale
+    expected_output = {
+        "date": "1 janvier 1970",
+        "datetime": "1 janvier 1970 00:00:00",
+        "iso_date": "1970-01-01",
+        "iso_datetime": "1970-01-01 00:00:00",
+        "timeago": '<span class="timeago" datetime="1970-01-01T00:00:00+00:00" locale="fr"></span>',
+        "custom": "01. janvier 1970"
+    }
+    assert get_date_formats(0, locale="fr") == expected_output
+
+    # Test with pt_BR locale
+    expected_output = {
+        "date": "1 de janeiro de 1970",
+        "datetime": "1 de janeiro de 1970 00:00:00",
+        "iso_date": "1970-01-01",
+        "iso_datetime": "1970-01-01 00:00:00",
+        "timeago": '<span class="timeago" datetime="1970-01-01T00:00:00+00:00" locale="pt_BR"></span>',
+        "custom": "01. janeiro 1970"
+    }
+    assert get_date_formats(0, locale="pt_BR") == expected_output
+
+    # Test with non-existing locale
+    with pytest.raises(UnknownLocaleError):
+        get_date_formats(0, locale="abcd")
+
     # Test with custom arguments
     expected_output = {
         "date": "January 1, 1970",