Repository: python-babel/babel

Issue:
Crash with double-slashes in /etc/localtime on Python 3.9
## Overview Description

Importing `babel.localtime` crashes on Python 3.9 if `/etc/localtime` contains double-slashes.


## Steps to Reproduce

```none
ls -lh /etc/localtime 
lrwxrwxrwx 1 root root 24 Mar  2 03:25 /etc/localtime -> /usr/share/zoneinfo//UTC
```

Note the double-slash, `//UTC`.

```none
# python
Python 3.9.16 (main, Mar 12 2023, 19:18:41) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import babel.localtime
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.9/site-packages/babel/localtime/__init__.py", line 41, in <module>
    LOCALTZ = get_localzone()
  File "/usr/local/lib/python3.9/site-packages/babel/localtime/__init__.py", line 37, in get_localzone
    return _get_localzone()
  File "/usr/local/lib/python3.9/site-packages/babel/localtime/_unix.py", line 49, in _get_localzone
    tzinfo = _get_tzinfo(zone_name)
  File "/usr/local/lib/python3.9/site-packages/babel/localtime/_helpers.py", line 21, in _get_tzinfo
    return zoneinfo.ZoneInfo(tzenv)
  File "/usr/local/lib/python3.9/zoneinfo/_tzpath.py", line 67, in find_tzfile
    _validate_tzfile_path(key)
  File "/usr/local/lib/python3.9/zoneinfo/_tzpath.py", line 81, in _validate_tzfile_path
    raise ValueError(
ValueError: ZoneInfo keys may not be absolute paths, got: /UTC
>>> import babel
>>> babel.__version__
'2.12.1'
```

## Actual Results

Crash.

## Expected Results

No crash.

## Reproducibility

Crashes for me on Python 3.9 with `babel` `2.12.1`. Works on Python 3.8.

The problem is that `babel.localtime._unix._get_localzone` [has naive path handling](https://github.com/python-babel/babel/blob/134c792df3b6aa84de3e74e81a875493c95dcc2e/babel/localtime/_unix.py#L48).

## Additional Information

I'm not sure whether `babel` is to blame here, but since this is working on Python 3.8 it would be nice to make `babel` more robust here. As far as I can tell, [double slashes are valid in POSIX and should be treated as single slashes](https://unix.stackexchange.com/a/1919/30497).

Hints from issue comments:
@alexmaragko: Using babel version v2.11.0 with Python 3.9 does not give the error, so this becomes an issue in the latest versions.@mdklatt: I just encountered this with Babel `2.12.1` running with Python 3.9 inside an Ubuntu 22.04 (Jammy) container. The problem is definitely the double slash in the `/etc/localtime` link. I don't know why the Ubuntu installer creates an irregular link like that, but it is indeed valid.

It looks like the problem is in [localtime/_unix.py](https://github.com/python-babel/babel/blob/master/babel/localtime/_unix.py). The `os.readlink()` function is used to get the target of `/etc/localtime`. This does not normalize the path, so the time zone ID is being parsed incorrectly as `/UTC` instead of `UTC`.


``` python

def _get_localzone(_root: str = '/') -> datetime.tzinfo:
    """Tries to find the local timezone configuration.
    This method prefers finding the timezone name and passing that to
    zoneinfo or pytz, over passing in the localtime file, as in the later
    case the zoneinfo name is unknown.
    The parameter _root makes the function look for files like /etc/localtime
    beneath the _root directory. This is primarily used by the tests.
    In normal usage you call the function without parameters.
    """
    # <snip>

    # This is actually a pretty reliable way to test for the local time
    # zone on operating systems like OS X.  On OS X especially this is the
    # only one that actually works.
    try:
        link_dst = os.readlink('/etc/localtime')
    except OSError:
        pass

    # <snip>
```@mdklatt: I think the solution to this is to replace `os.readlink()` with `pathlib.Path().resolve()`, which will normalize the path and remove the double slashes. I created a fork, but coming up with a test for this is tricky.@natescherer: Confirmed this is also a problem with Python 3.10 on Ubuntu.@mdklatt: I submitted https://github.com/python-babel/babel/pull/1006 for this back in June. The PR could not be automatically merged because some unrelated broken tests were failing. It looks like it was manually [merged](https://github.com/python-babel/babel/commit/deebeb22681b099785c783992811d10b1f8862f4) by a maintainer, but I'm not sure what the current status is.@mdklatt: The broken unit tests have been fixed upstream. I rebased my fixes onto `master` and submitted https://github.com/python-babel/babel/pull/1035. This should pass all automated checks, which will hopefully streamline the merge process and speed up the resolution of this issue.@mdklatt: For anybody that needs a temporary workaround, I was able to fix this in an Ubuntu Docker image by recreating the `/etc/localtime` link to remove the double slashes:

``` shell
rm -f /etc/localtime
ln -s /usr/share/zoneinfo/Etc/UTC /etc/localtime
```@igormcoelho: I'm having this issue with Ubuntu 22.04, python 3.10 and babel 2.13.

```
Traceback (most recent call last):
  File "/usr/local/bin/sphinx-build", line 5, in <module>
    from sphinx.cmd.build import main
  File "/usr/local/lib/python3.10/dist-packages/sphinx/cmd/build.py", line 25, in <module>
    from sphinx.application import Sphinx
  File "/usr/local/lib/python3.10/dist-packages/sphinx/application.py", line 32, in <module>
    from sphinx.config import Config
  File "/usr/local/lib/python3.10/dist-packages/sphinx/config.py", line 22, in <module>
    from sphinx.util.i18n import format_date
  File "/usr/local/lib/python3.10/dist-packages/sphinx/util/i18n.py", line 17, in <module>
    import babel.dates
  File "/usr/local/lib/python3.10/dist-packages/babel/dates.py", line 34, in <module>
    from babel import localtime
  File "/usr/local/lib/python3.10/dist-packages/babel/localtime/__init__.py", line 41, in <module>
    LOCALTZ = get_localzone()
  File "/usr/local/lib/python3.10/dist-packages/babel/localtime/__init__.py", line 37, in get_localzone
    return _get_localzone()
  File "/usr/local/lib/python3.10/dist-packages/babel/localtime/_unix.py", line 49, in _get_localzone
    tzinfo = _get_tzinfo(zone_name)
  File "/usr/local/lib/python3.10/dist-packages/babel/localtime/_helpers.py", line 21, in _get_tzinfo
    return zoneinfo.ZoneInfo(tzenv)
  File "/usr/lib/python3.10/zoneinfo/_tzpath.py", line 67, in find_tzfile
    _validate_tzfile_path(key)
  File "/usr/lib/python3.10/zoneinfo/_tzpath.py", line 81, in _validate_tzfile_path
    raise ValueError(
ValueError: ZoneInfo keys may not be absolute paths, got: /UTC
```

The strange thing is that my /etc/localtime does not have any slash... but it is quite strange anyway:

```
$ cat /etc/localtime 
TZif2UTCTZif2UTC
UTC0
```

Don't know how to fix it.

[EDIT] Sorry! Now I noticed that the double slash is on the filepath itself...

 ```
$ ls -la /etc/localtime
/etc/localtime -> /usr/share/zoneinfo//UTC
```
So this solution indeed fixed my error:

```
$ rm -f /etc/localtime
$ ln -s /usr/share/zoneinfo/UTC /etc/localtime
```@falkephi: Because other people might hit the same problem:

In an Ubuntu 22.04 container I hat to execute this in addition to the above mentioned fixes:

```bash
sed -i "s/\///g" /etc/timezone
```

Solution patch:
diff --git a/babel/core.py b/babel/core.py
index a2e1e1d..a3ebb40 100644
--- a/babel/core.py
+++ b/babel/core.py
@@ -269,7 +269,7 @@ class Locale:
     @classmethod
     def parse(
         cls,
-        identifier: str | Locale | None,
+        identifier: Locale | str | None,
         sep: str = '_',
         resolve_likely_subtags: bool = True,
     ) -> Locale:
@@ -286,8 +286,8 @@ class Locale:
         Locale('de', territory='DE')
 
         If the `identifier` parameter is neither of these, such as `None`
-        e.g. because a default locale identifier could not be determined,
-        a `TypeError` is raised:
+        or an empty string, e.g. because a default locale identifier
+        could not be determined, a `TypeError` is raised:
 
         >>> Locale.parse(None)
         Traceback (most recent call last):
@@ -325,10 +325,23 @@ class Locale:
         :raise `UnknownLocaleError`: if no locale data is available for the
                                      requested locale
         :raise `TypeError`: if the identifier is not a string or a `Locale`
+        :raise `ValueError`: if the identifier is not a valid string
         """
         if isinstance(identifier, Locale):
             return identifier
-        elif not isinstance(identifier, str):
+
+        if not identifier:
+            msg = (
+                f"Empty locale identifier value: {identifier!r}\n\n"
+                f"If you didn't explicitly pass an empty value to a Babel function, "
+                f"this could be caused by there being no suitable locale environment "
+                f"variables for the API you tried to use.",
+            )
+            if isinstance(identifier, str):
+                raise ValueError(msg)  # `parse_locale` would raise a ValueError, so let's do that here
+            raise TypeError(msg)
+
+        if not isinstance(identifier, str):
             raise TypeError(f"Unexpected value for identifier: {identifier!r}")
 
         parts = parse_locale(identifier, sep=sep)
@@ -1235,6 +1248,8 @@ def parse_locale(
     :raise `ValueError`: if the string does not appear to be a valid locale
                          identifier
     """
+    if not identifier:
+        raise ValueError("empty locale identifier")
     identifier, _, modifier = identifier.partition('@')
     if '.' in identifier:
         # this is probably the charset/encoding, which we don't care about
diff --git a/babel/dates.py b/babel/dates.py
index 2e48185..8c82d72 100644
--- a/babel/dates.py
+++ b/babel/dates.py
@@ -252,8 +252,11 @@ def get_timezone(zone: str | datetime.tzinfo | None = None) -> datetime.tzinfo:
     raise LookupError(f"Unknown timezone {zone}") from exc
 
 
-def get_period_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
-                     context: _Context = 'stand-alone', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
+def get_period_names(
+    width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+    context: _Context = 'stand-alone',
+    locale: Locale | str | None = None,
+) -> LocaleDataDict:
     """Return the names for day periods (AM/PM) used by the locale.
 
     >>> get_period_names(locale='en_US')['am']
@@ -261,13 +264,16 @@ def get_period_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
 
     :param width: the width to use, one of "abbreviated", "narrow", or "wide"
     :param context: the context, either "format" or "stand-alone"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).day_periods[context][width]
+    return Locale.parse(locale or LC_TIME).day_periods[context][width]
 
 
-def get_day_names(width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide',
-                  context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
+def get_day_names(
+    width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wide',
+    context: _Context = 'format',
+    locale: Locale | str | None = None,
+) -> LocaleDataDict:
     """Return the day names used by the locale for the specified format.
 
     >>> get_day_names('wide', locale='en_US')[1]
@@ -281,13 +287,16 @@ def get_day_names(width: Literal['abbreviated', 'narrow', 'short', 'wide'] = 'wi
 
     :param width: the width to use, one of "wide", "abbreviated", "short" or "narrow"
     :param context: the context, either "format" or "stand-alone"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).days[context][width]
+    return Locale.parse(locale or LC_TIME).days[context][width]
 
 
-def get_month_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
-                    context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
+def get_month_names(
+    width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+    context: _Context = 'format',
+    locale: Locale | str | None = None,
+) -> LocaleDataDict:
     """Return the month names used by the locale for the specified format.
 
     >>> get_month_names('wide', locale='en_US')[1]
@@ -299,13 +308,16 @@ def get_month_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
 
     :param width: the width to use, one of "wide", "abbreviated", or "narrow"
     :param context: the context, either "format" or "stand-alone"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).months[context][width]
+    return Locale.parse(locale or LC_TIME).months[context][width]
 
 
-def get_quarter_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
-                      context: _Context = 'format', locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
+def get_quarter_names(
+    width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+    context: _Context = 'format',
+    locale: Locale | str | None = None,
+) -> LocaleDataDict:
     """Return the quarter names used by the locale for the specified format.
 
     >>> get_quarter_names('wide', locale='en_US')[1]
@@ -317,13 +329,15 @@ def get_quarter_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
 
     :param width: the width to use, one of "wide", "abbreviated", or "narrow"
     :param context: the context, either "format" or "stand-alone"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).quarters[context][width]
+    return Locale.parse(locale or LC_TIME).quarters[context][width]
 
 
-def get_era_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
-                  locale: Locale | str | None = LC_TIME) -> LocaleDataDict:
+def get_era_names(
+    width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
+    locale: Locale | str | None = None,
+) -> LocaleDataDict:
     """Return the era names used by the locale for the specified format.
 
     >>> get_era_names('wide', locale='en_US')[1]
@@ -332,12 +346,15 @@ def get_era_names(width: Literal['abbreviated', 'narrow', 'wide'] = 'wide',
     u'n. Chr.'
 
     :param width: the width to use, either "wide", "abbreviated", or "narrow"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).eras[width]
+    return Locale.parse(locale or LC_TIME).eras[width]
 
 
-def get_date_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
+def get_date_format(
+    format: _PredefinedTimeFormat = 'medium',
+    locale: Locale | str | None = None,
+) -> DateTimePattern:
     """Return the date formatting patterns used by the locale for the specified
     format.
 
@@ -348,12 +365,15 @@ def get_date_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | s
 
     :param format: the format to use, one of "full", "long", "medium", or
                    "short"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).date_formats[format]
+    return Locale.parse(locale or LC_TIME).date_formats[format]
 
 
-def get_datetime_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
+def get_datetime_format(
+    format: _PredefinedTimeFormat = 'medium',
+    locale: Locale | str | None = None,
+) -> DateTimePattern:
     """Return the datetime formatting patterns used by the locale for the
     specified format.
 
@@ -362,15 +382,18 @@ def get_datetime_format(format: _PredefinedTimeFormat = 'medium', locale: Locale
 
     :param format: the format to use, one of "full", "long", "medium", or
                    "short"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    patterns = Locale.parse(locale).datetime_formats
+    patterns = Locale.parse(locale or LC_TIME).datetime_formats
     if format not in patterns:
         format = None
     return patterns[format]
 
 
-def get_time_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | str | None = LC_TIME) -> DateTimePattern:
+def get_time_format(
+    format: _PredefinedTimeFormat = 'medium',
+    locale: Locale | str | None = None,
+) -> DateTimePattern:
     """Return the time formatting patterns used by the locale for the specified
     format.
 
@@ -381,15 +404,15 @@ def get_time_format(format: _PredefinedTimeFormat = 'medium', locale: Locale | s
 
     :param format: the format to use, one of "full", "long", "medium", or
                    "short"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     """
-    return Locale.parse(locale).time_formats[format]
+    return Locale.parse(locale or LC_TIME).time_formats[format]
 
 
 def get_timezone_gmt(
     datetime: _Instant = None,
     width: Literal['long', 'short', 'iso8601', 'iso8601_short'] = 'long',
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
     return_z: bool = False,
 ) -> str:
     """Return the timezone associated with the given `datetime` object formatted
@@ -423,12 +446,12 @@ def get_timezone_gmt(
     :param datetime: the ``datetime`` object; if `None`, the current date and
                      time in UTC is used
     :param width: either "long" or "short" or "iso8601" or "iso8601_short"
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     :param return_z: True or False; Function returns indicator "Z"
                      when local time offset is 0
     """
     datetime = _ensure_datetime_tzinfo(_get_datetime(datetime))
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
 
     offset = datetime.tzinfo.utcoffset(datetime)
     seconds = offset.days * 24 * 60 * 60 + offset.seconds
@@ -448,7 +471,7 @@ def get_timezone_gmt(
 
 def get_timezone_location(
     dt_or_tzinfo: _DtOrTzinfo = None,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
     return_city: bool = False,
 ) -> str:
     """Return a representation of the given timezone using "location format".
@@ -479,13 +502,13 @@ def get_timezone_location(
     :param dt_or_tzinfo: the ``datetime`` or ``tzinfo`` object that determines
                          the timezone; if `None`, the current date and time in
                          UTC is assumed
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     :param return_city: True or False, if True then return exemplar city (location)
                         for the time zone
     :return: the localized timezone name using location format
 
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
 
     zone = _get_tz_name(dt_or_tzinfo)
 
@@ -530,7 +553,7 @@ def get_timezone_name(
     dt_or_tzinfo: _DtOrTzinfo = None,
     width: Literal['long', 'short'] = 'long',
     uncommon: bool = False,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
     zone_variant: Literal['generic', 'daylight', 'standard'] | None = None,
     return_zone: bool = False,
 ) -> str:
@@ -600,12 +623,12 @@ def get_timezone_name(
                            ``'generic'`` variation is assumed.  The following
                            values are valid: ``'generic'``, ``'daylight'`` and
                            ``'standard'``.
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     :param return_zone: True or False. If true then function
                         returns long time zone ID
     """
     dt, tzinfo = _get_dt_and_tzinfo(dt_or_tzinfo)
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
 
     zone = _get_tz_name(dt_or_tzinfo)
 
@@ -651,7 +674,7 @@ def get_timezone_name(
 def format_date(
     date: datetime.date | None = None,
     format: _PredefinedTimeFormat | str = 'medium',
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     """Return a date formatted according to the given pattern.
 
@@ -672,14 +695,14 @@ def format_date(
                  date is used
     :param format: one of "full", "long", "medium", or "short", or a custom
                    date/time pattern
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     """
     if date is None:
         date = datetime.date.today()
     elif isinstance(date, datetime.datetime):
         date = date.date()
 
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
     if format in ('full', 'long', 'medium', 'short'):
         format = get_date_format(format, locale=locale)
     pattern = parse_pattern(format)
@@ -690,7 +713,7 @@ def format_datetime(
     datetime: _Instant = None,
     format: _PredefinedTimeFormat | str = 'medium',
     tzinfo: datetime.tzinfo | None = None,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     r"""Return a date formatted according to the given pattern.
 
@@ -713,11 +736,11 @@ def format_datetime(
     :param format: one of "full", "long", "medium", or "short", or a custom
                    date/time pattern
     :param tzinfo: the timezone to apply to the time for display
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     """
     datetime = _ensure_datetime_tzinfo(_get_datetime(datetime), tzinfo)
 
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
     if format in ('full', 'long', 'medium', 'short'):
         return get_datetime_format(format, locale=locale) \
             .replace("'", "") \
@@ -731,7 +754,8 @@ def format_datetime(
 def format_time(
     time: datetime.time | datetime.datetime | float | None = None,
     format: _PredefinedTimeFormat | str = 'medium',
-    tzinfo: datetime.tzinfo | None = None, locale: Locale | str | None = LC_TIME,
+    tzinfo: datetime.tzinfo | None = None,
+    locale: Locale | str | None = None,
 ) -> str:
     r"""Return a time formatted according to the given pattern.
 
@@ -786,7 +810,7 @@ def format_time(
     :param format: one of "full", "long", "medium", or "short", or a custom
                    date/time pattern
     :param tzinfo: the time-zone to apply to the time for display
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     """
 
     # get reference date for if we need to find the right timezone variant
@@ -795,7 +819,7 @@ def format_time(
 
     time = _get_time(time, tzinfo)
 
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
     if format in ('full', 'long', 'medium', 'short'):
         format = get_time_format(format, locale=locale)
     return parse_pattern(format).apply(time, locale, reference_date=ref_date)
@@ -806,7 +830,7 @@ def format_skeleton(
     datetime: _Instant = None,
     tzinfo: datetime.tzinfo | None = None,
     fuzzy: bool = True,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     r"""Return a time and/or date formatted according to the given pattern.
 
@@ -842,9 +866,9 @@ def format_skeleton(
     :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
                   close enough to it. If there is no close match, a `KeyError`
                   is thrown.
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
     if fuzzy and skeleton not in locale.datetime_skeletons:
         skeleton = match_skeleton(skeleton, locale.datetime_skeletons)
     format = locale.datetime_skeletons[skeleton]
@@ -868,7 +892,7 @@ def format_timedelta(
     threshold: float = .85,
     add_direction: bool = False,
     format: Literal['narrow', 'short', 'medium', 'long'] = 'long',
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     """Return a time delta according to the rules of the given locale.
 
@@ -923,7 +947,7 @@ def format_timedelta(
     :param format: the format, can be "narrow", "short" or "long". (
                    "medium" is deprecated, currently converted to "long" to
                    maintain compatibility)
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     """
     if format not in ('narrow', 'short', 'medium', 'long'):
         raise TypeError('Format must be one of "narrow", "short" or "long"')
@@ -939,7 +963,7 @@ def format_timedelta(
         seconds = int((delta.days * 86400) + delta.seconds)
     else:
         seconds = delta
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
     date_fields = locale._data["date_fields"]
     unit_patterns = locale._data["unit_patterns"]
 
@@ -988,7 +1012,7 @@ def _format_fallback_interval(
     end: _Instant,
     skeleton: str | None,
     tzinfo: datetime.tzinfo | None,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale,
 ) -> str:
     if skeleton in locale.datetime_skeletons:  # Use the given skeleton
         format = lambda dt: format_skeleton(skeleton, dt, tzinfo, locale=locale)
@@ -1018,7 +1042,7 @@ def format_interval(
     skeleton: str | None = None,
     tzinfo: datetime.tzinfo | None = None,
     fuzzy: bool = True,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     """
     Format an interval between two instants according to the locale's rules.
@@ -1058,10 +1082,10 @@ def format_interval(
     :param tzinfo: tzinfo to use (if none is already attached)
     :param fuzzy: If the skeleton is not found, allow choosing a skeleton that's
                   close enough to it.
-    :param locale: A locale object or identifier.
+    :param locale: A locale object or identifier. Defaults to the system time locale.
     :return: Formatted interval
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
 
     # NB: The quote comments below are from the algorithm description in
     #     https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats
@@ -1121,7 +1145,7 @@ def get_period_id(
     time: _Instant,
     tzinfo: datetime.tzinfo | None = None,
     type: Literal['selection'] | None = None,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
 ) -> str:
     """
     Get the day period ID for a given time.
@@ -1143,12 +1167,12 @@ def get_period_id(
     :param type: The period type to use. Either "selection" or None.
                  The selection type is used for selecting among phrases such as
                  “Your email arrived yesterday evening” or “Your email arrived last night”.
-    :param locale: the `Locale` object, or a locale string
+    :param locale: the `Locale` object, or a locale string. Defaults to the system time locale.
     :return: period ID. Something is always returned -- even if it's just "am" or "pm".
     """
     time = _get_time(time, tzinfo)
     seconds_past_midnight = int(time.hour * 60 * 60 + time.minute * 60 + time.second)
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_TIME)
 
     # The LDML rules state that the rules may not overlap, so iterating in arbitrary
     # order should be alright, though `at` periods should be preferred.
@@ -1199,7 +1223,7 @@ class ParseError(ValueError):
 
 def parse_date(
     string: str,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
     format: _PredefinedTimeFormat | str = 'medium',
 ) -> datetime.date:
     """Parse a date from a string.
@@ -1228,6 +1252,7 @@ def parse_date(
 
     :param string: the string containing the date
     :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     :param format: the format to use, either an explicit date format,
                    or one of "full", "long", "medium", or "short"
                    (see ``get_time_format``)
@@ -1275,7 +1300,7 @@ def parse_date(
 
 def parse_time(
     string: str,
-    locale: Locale | str | None = LC_TIME,
+    locale: Locale | str | None = None,
     format: _PredefinedTimeFormat | str = 'medium',
 ) -> datetime.time:
     """Parse a time from a string.
@@ -1292,7 +1317,7 @@ def parse_time(
     datetime.time(15, 30)
 
     :param string: the string containing the time
-    :param locale: a `Locale` object or a locale identifier
+    :param locale: a `Locale` object or a locale identifier. Defaults to the system time locale.
     :param format: the format to use, either an explicit time format,
                    or one of "full", "long", "medium", or "short"
                    (see ``get_time_format``)
diff --git a/babel/lists.py b/babel/lists.py
index 6c34cb0..f1d3790 100644
--- a/babel/lists.py
+++ b/babel/lists.py
@@ -15,6 +15,7 @@
 """
 from __future__ import annotations
 
+import warnings
 from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
@@ -23,13 +24,26 @@ from babel.core import Locale, default_locale
 if TYPE_CHECKING:
     from typing_extensions import Literal
 
-DEFAULT_LOCALE = default_locale()
+
+
+_DEFAULT_LOCALE = default_locale()  # TODO(3.0): Remove this.
+
+
+def __getattr__(name):
+    if name == "DEFAULT_LOCALE":
+        warnings.warn(
+            "The babel.lists.DEFAULT_LOCALE constant is deprecated and will be removed.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return _DEFAULT_LOCALE
+    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
 
 
 def format_list(
     lst: Sequence[str],
     style: Literal['standard', 'standard-short', 'or', 'or-short', 'unit', 'unit-short', 'unit-narrow'] = 'standard',
-    locale: Locale | str | None = DEFAULT_LOCALE,
+    locale: Locale | str | None = None,
 ) -> str:
     """
     Format the items in `lst` as a list.
@@ -74,9 +88,9 @@ def format_list(
 
     :param lst: a sequence of items to format in to a list
     :param style: the style to format the list with. See above for description.
-    :param locale: the locale
+    :param locale: the locale. Defaults to the system locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or _DEFAULT_LOCALE)
     if not lst:
         return ''
     if len(lst) == 1:
diff --git a/babel/localtime/_unix.py b/babel/localtime/_unix.py
index eb81beb..782a7d2 100644
--- a/babel/localtime/_unix.py
+++ b/babel/localtime/_unix.py
@@ -45,7 +45,13 @@ def _get_localzone(_root: str = '/') -> datetime.tzinfo:
     else:
         pos = link_dst.find('/zoneinfo/')
         if pos >= 0:
-            zone_name = link_dst[pos + 10:]
+            # On occasion, the `/etc/localtime` symlink has a double slash, e.g.
+            # "/usr/share/zoneinfo//UTC", which would make `zoneinfo.ZoneInfo`
+            # complain (no absolute paths allowed), and we'd end up returning
+            # `None` (as a fix for #1092).
+            # Instead, let's just "fix" the double slash symlink by stripping
+            # leading slashes before passing the assumed zone name forward.
+            zone_name = link_dst[pos + 10:].lstrip("/")
             tzinfo = _get_tzinfo(zone_name)
             if tzinfo is not None:
                 return tzinfo
diff --git a/babel/messages/catalog.py b/babel/messages/catalog.py
index cff22f9..1036ab8 100644
--- a/babel/messages/catalog.py
+++ b/babel/messages/catalog.py
@@ -279,7 +279,7 @@ class Catalog:
 
     def __init__(
         self,
-        locale: str | Locale | None = None,
+        locale: Locale | str | None = None,
         domain: str | None = None,
         header_comment: str | None = DEFAULT_HEADER,
         project: str | None = None,
diff --git a/babel/messages/plurals.py b/babel/messages/plurals.py
index 01c38f4..cb1ea5b 100644
--- a/babel/messages/plurals.py
+++ b/babel/messages/plurals.py
@@ -220,11 +220,13 @@ class _PluralTuple(tuple):
         return self.plural_forms
 
 
-def get_plural(locale: str | None = LC_CTYPE) -> _PluralTuple:
+def get_plural(locale: Locale | str | None = None) -> _PluralTuple:
     """A tuple with the information catalogs need to perform proper
     pluralization.  The first item of the tuple is the number of plural
     forms, the second the plural expression.
 
+    :param locale: the `Locale` object or locale identifier. Defaults to the system character type locale.
+
     >>> get_plural(locale='en')
     (2, '(n != 1)')
     >>> get_plural(locale='ga')
@@ -246,7 +248,7 @@ def get_plural(locale: str | None = LC_CTYPE) -> _PluralTuple:
     >>> str(tup)
     'nplurals=1; plural=0;'
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_CTYPE)
     try:
         tup = PLURALS[str(locale)]
     except KeyError:
diff --git a/babel/messages/pofile.py b/babel/messages/pofile.py
index 7217076..a292c8a 100644
--- a/babel/messages/pofile.py
+++ b/babel/messages/pofile.py
@@ -380,7 +380,7 @@ class PoFileParser:
 
 def read_po(
     fileobj: IO[AnyStr] | Iterable[AnyStr],
-    locale: str | Locale | None = None,
+    locale: Locale | str | None = None,
     domain: str | None = None,
     ignore_obsolete: bool = False,
     charset: str | None = None,
diff --git a/babel/numbers.py b/babel/numbers.py
index 624e8d6..aed9aa4 100644
--- a/babel/numbers.py
+++ b/babel/numbers.py
@@ -108,7 +108,7 @@ def normalize_currency(currency: str, locale: Locale | str | None = None) -> str
 def get_currency_name(
     currency: str,
     count: float | decimal.Decimal | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
 ) -> str:
     """Return the name used by the locale for the specified currency.
 
@@ -120,9 +120,9 @@ def get_currency_name(
     :param currency: the currency code.
     :param count: the optional count.  If provided the currency name
                   will be pluralized to that number if possible.
-    :param locale: the `Locale` object or locale identifier.
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     """
-    loc = Locale.parse(locale)
+    loc = Locale.parse(locale or LC_NUMERIC)
     if count is not None:
         try:
             plural_form = loc.plural_form(count)
@@ -138,16 +138,16 @@ def get_currency_name(
     return loc.currencies.get(currency, currency)
 
 
-def get_currency_symbol(currency: str, locale: Locale | str | None = LC_NUMERIC) -> str:
+def get_currency_symbol(currency: str, locale: Locale | str | None = None) -> str:
     """Return the symbol used by the locale for the specified currency.
 
     >>> get_currency_symbol('USD', locale='en_US')
     u'$'
 
     :param currency: the currency code.
-    :param locale: the `Locale` object or locale identifier.
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     """
-    return Locale.parse(locale).currency_symbols.get(currency, currency)
+    return Locale.parse(locale or LC_NUMERIC).currency_symbols.get(currency, currency)
 
 
 def get_currency_precision(currency: str) -> int:
@@ -167,7 +167,7 @@ def get_currency_precision(currency: str) -> int:
 def get_currency_unit_pattern(
     currency: str,
     count: float | decimal.Decimal | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
 ) -> str:
     """
     Return the unit pattern used for long display of a currency value
@@ -184,9 +184,9 @@ def get_currency_unit_pattern(
     :param currency: the currency code.
     :param count: the optional count.  If provided the unit
                   pattern for that number will be returned.
-    :param locale: the `Locale` object or locale identifier.
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     """
-    loc = Locale.parse(locale)
+    loc = Locale.parse(locale or LC_NUMERIC)
     if count is not None:
         plural_form = loc.plural_form(count)
         try:
@@ -325,16 +325,15 @@ def _get_numbering_system(locale: Locale, numbering_system: Literal["default"] |
 
 
 def _get_number_symbols(
-    locale: Locale | str | None,
+    locale: Locale,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> LocaleDataDict:
-    parsed_locale = Locale.parse(locale)
-    numbering_system = _get_numbering_system(parsed_locale, numbering_system)
+    numbering_system = _get_numbering_system(locale, numbering_system)
     try:
-        return parsed_locale.number_symbols[numbering_system]
+        return locale.number_symbols[numbering_system]
     except KeyError as error:
-        raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {parsed_locale}.") from error
+        raise UnsupportedNumberingSystemError(f"Unknown numbering system {numbering_system} for Locale {locale}.") from error
 
 
 class UnsupportedNumberingSystemError(Exception):
@@ -343,7 +342,7 @@ class UnsupportedNumberingSystemError(Exception):
 
 
 def get_decimal_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -356,16 +355,17 @@ def get_decimal_symbol(
     >>> get_decimal_symbol('ar_EG', numbering_system='latn')
     u'.'
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('decimal', '.')
 
 
 def get_plus_sign_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -378,16 +378,17 @@ def get_plus_sign_symbol(
     >>> get_plus_sign_symbol('ar_EG', numbering_system='latn')
     u'\u200e+'
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('plusSign', '+')
 
 
 def get_minus_sign_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -400,16 +401,17 @@ def get_minus_sign_symbol(
     >>> get_minus_sign_symbol('ar_EG', numbering_system='latn')
     u'\u200e-'
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('minusSign', '-')
 
 
 def get_exponential_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -422,16 +424,17 @@ def get_exponential_symbol(
     >>> get_exponential_symbol('ar_EG', numbering_system='latn')
     u'E'
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('exponential', 'E')
 
 
 def get_group_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -444,16 +447,17 @@ def get_group_symbol(
     >>> get_group_symbol('ar_EG', numbering_system='latn')
     u','
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('group', ',')
 
 
 def get_infinity_symbol(
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -466,15 +470,16 @@ def get_infinity_symbol(
     >>> get_infinity_symbol('ar_EG', numbering_system='latn')
     u'∞'
 
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for fetching the symbol. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: if the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
     return _get_number_symbols(locale, numbering_system=numbering_system).get('infinity', '∞')
 
 
-def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = LC_NUMERIC) -> str:
+def format_number(number: float | decimal.Decimal | str, locale: Locale | str | None = None) -> str:
     """Return the given number formatted for a specific locale.
 
     >>> format_number(1099, locale='en_US')  # doctest: +SKIP
@@ -487,7 +492,7 @@ def format_number(number: float | decimal.Decimal | str, locale: Locale | str |
        Use babel.numbers.format_decimal() instead.
 
     :param number: the number to format
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
 
 
     """
@@ -518,7 +523,7 @@ def get_decimal_quantum(precision: int | decimal.Decimal) -> decimal.Decimal:
 def format_decimal(
     number: float | decimal.Decimal | str,
     format: str | NumberPattern | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     decimal_quantization: bool = True,
     group_separator: bool = True,
     *,
@@ -562,7 +567,7 @@ def format_decimal(
 
     :param number: the number to format
     :param format:
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
     :param group_separator: Boolean to switch group separator on/off in a locale's
@@ -571,7 +576,7 @@ def format_decimal(
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     if format is None:
         format = locale.decimal_formats[format]
     pattern = parse_pattern(format)
@@ -583,7 +588,7 @@ def format_compact_decimal(
     number: float | decimal.Decimal | str,
     *,
     format_type: Literal["short", "long"] = "short",
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     fraction_digits: int = 0,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -606,13 +611,13 @@ def format_compact_decimal(
 
     :param number: the number to format
     :param format_type: Compact format to use ("short" or "long")
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     compact_format = locale.compact_decimal_formats[format_type]
     number, format = _get_compact_format(number, compact_format, locale, fraction_digits)
     # Did not find a format, fall back.
@@ -670,7 +675,7 @@ def format_currency(
     number: float | decimal.Decimal | str,
     currency: str,
     format: str | NumberPattern | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     currency_digits: bool = True,
     format_type: Literal["name", "standard", "accounting"] = "standard",
     decimal_quantization: bool = True,
@@ -758,7 +763,7 @@ def format_currency(
     :param number: the number to format
     :param currency: the currency code
     :param format: the format string to use
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param currency_digits: use the currency's natural number of decimal digits
     :param format_type: the currency format type to use
     :param decimal_quantization: Truncate and round high-precision numbers to
@@ -769,12 +774,20 @@ def format_currency(
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
+    locale = Locale.parse(locale or LC_NUMERIC)
+
     if format_type == 'name':
-        return _format_currency_long_name(number, currency, format=format,
-                                          locale=locale, currency_digits=currency_digits,
-                                          decimal_quantization=decimal_quantization, group_separator=group_separator,
-                                          numbering_system=numbering_system)
-    locale = Locale.parse(locale)
+        return _format_currency_long_name(
+            number,
+            currency,
+            locale=locale,
+            format=format,
+            currency_digits=currency_digits,
+            decimal_quantization=decimal_quantization,
+            group_separator=group_separator,
+            numbering_system=numbering_system,
+        )
+
     if format:
         pattern = parse_pattern(format)
     else:
@@ -791,18 +804,17 @@ def format_currency(
 def _format_currency_long_name(
     number: float | decimal.Decimal | str,
     currency: str,
-    format: str | NumberPattern | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
-    currency_digits: bool = True,
-    format_type: Literal["name", "standard", "accounting"] = "standard",
-    decimal_quantization: bool = True,
-    group_separator: bool = True,
     *,
-    numbering_system: Literal["default"] | str = "latn",
+    locale: Locale,
+    format: str | NumberPattern | None,
+    currency_digits: bool,
+    decimal_quantization: bool,
+    group_separator: bool,
+    numbering_system: Literal["default"] | str,
 ) -> str:
     # Algorithm described here:
     # https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies
-    locale = Locale.parse(locale)
+
     # Step 1.
     # There are no examples of items with explicit count (0 or 1) in current
     # locale data. So there is no point implementing that.
@@ -835,7 +847,7 @@ def format_compact_currency(
     currency: str,
     *,
     format_type: Literal["short"] = "short",
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     fraction_digits: int = 0,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -851,13 +863,13 @@ def format_compact_currency(
     :param number: the number to format
     :param currency: the currency code
     :param format_type: the compact format type to use. Defaults to "short".
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param fraction_digits: Number of digits after the decimal point to use. Defaults to `0`.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     try:
         compact_format = locale.compact_currency_formats[format_type]
     except KeyError as error:
@@ -885,7 +897,7 @@ def format_compact_currency(
 def format_percent(
     number: float | decimal.Decimal | str,
     format: str | NumberPattern | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     decimal_quantization: bool = True,
     group_separator: bool = True,
     *,
@@ -924,7 +936,7 @@ def format_percent(
 
     :param number: the percent number to format
     :param format:
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
     :param group_separator: Boolean to switch group separator on/off in a locale's
@@ -933,7 +945,7 @@ def format_percent(
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     if not format:
         format = locale.percent_formats[None]
     pattern = parse_pattern(format)
@@ -944,12 +956,12 @@ def format_percent(
 
 
 def format_scientific(
-        number: float | decimal.Decimal | str,
-        format: str | NumberPattern | None = None,
-        locale: Locale | str | None = LC_NUMERIC,
-        decimal_quantization: bool = True,
-        *,
-        numbering_system: Literal["default"] | str = "latn",
+    number: float | decimal.Decimal | str,
+    format: str | NumberPattern | None = None,
+    locale: Locale | str | None = None,
+    decimal_quantization: bool = True,
+    *,
+    numbering_system: Literal["default"] | str = "latn",
 ) -> str:
     """Return value formatted in scientific notation for a specific locale.
 
@@ -974,14 +986,14 @@ def format_scientific(
 
     :param number: the number to format
     :param format:
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param decimal_quantization: Truncate and round high-precision numbers to
                                  the format pattern. Defaults to `True`.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     if not format:
         format = locale.scientific_formats[None]
     pattern = parse_pattern(format)
@@ -1009,7 +1021,7 @@ SPACE_CHARS_RE = re.compile('|'.join(SPACE_CHARS))
 
 def parse_number(
     string: str,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> int:
@@ -1028,7 +1040,7 @@ def parse_number(
     NumberFormatError: '1.099,98' is not a valid number
 
     :param string: the string to parse
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :return: the parsed number
@@ -1053,7 +1065,7 @@ def parse_number(
 
 def parse_decimal(
     string: str,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     strict: bool = False,
     *,
     numbering_system: Literal["default"] | str = "latn",
@@ -1090,7 +1102,7 @@ def parse_decimal(
     NumberFormatError: '0.00' is not a properly formatted decimal number. Did you mean '0'?
 
     :param string: the string to parse
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param strict: controls whether numbers formatted in a weird way are
                    accepted or rejected
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
@@ -1099,7 +1111,7 @@ def parse_decimal(
                               decimal number
     :raise UnsupportedNumberingSystemError: if the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     group_symbol = get_group_symbol(locale, numbering_system=numbering_system)
     decimal_symbol = get_decimal_symbol(locale, numbering_system=numbering_system)
 
diff --git a/babel/support.py b/babel/support.py
index 7dcda5c..a4484ce 100644
--- a/babel/support.py
+++ b/babel/support.py
@@ -642,7 +642,7 @@ class Translations(NullTranslations, gettext.GNUTranslations):
     def load(
         cls,
         dirname: str | os.PathLike[str] | None = None,
-        locales: Iterable[str | Locale] | str | Locale | None = None,
+        locales: Iterable[str | Locale] | Locale | str | None = None,
         domain: str | None = None,
     ) -> NullTranslations:
         """Load translations from the given directory.
@@ -709,7 +709,7 @@ class Translations(NullTranslations, gettext.GNUTranslations):
 
 
 def _locales_to_names(
-    locales: Iterable[str | Locale] | str | Locale | None,
+    locales: Iterable[str | Locale] | Locale | str | None,
 ) -> list[str] | None:
     """Normalize a `locales` argument to a list of locale names.
 
diff --git a/babel/units.py b/babel/units.py
index 8dae694..1ff71eb 100644
--- a/babel/units.py
+++ b/babel/units.py
@@ -18,7 +18,7 @@ class UnknownUnitError(ValueError):
 def get_unit_name(
     measurement_unit: str,
     length: Literal['short', 'long', 'narrow'] = 'long',
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
 ) -> str | None:
     """
     Get the display name for a measurement unit in the given locale.
@@ -38,17 +38,17 @@ def get_unit_name(
                              https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml
 
     :param length: "short", "long" or "narrow"
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :return: The unit display name, or None.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     unit = _find_unit_pattern(measurement_unit, locale=locale)
     if not unit:
         raise UnknownUnitError(unit=measurement_unit, locale=locale)
     return locale.unit_display_names.get(unit, {}).get(length)
 
 
-def _find_unit_pattern(unit_id: str, locale: Locale | str | None = LC_NUMERIC) -> str | None:
+def _find_unit_pattern(unit_id: str, locale: Locale | str | None = None) -> str | None:
     """
     Expand a unit into a qualified form.
 
@@ -65,7 +65,7 @@ def _find_unit_pattern(unit_id: str, locale: Locale | str | None = LC_NUMERIC) -
     :param unit_id: the code of a measurement unit.
     :return: A key to the `unit_patterns` mapping, or None.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
     unit_patterns = locale._data["unit_patterns"]
     if unit_id in unit_patterns:
         return unit_id
@@ -80,7 +80,7 @@ def format_unit(
     measurement_unit: str,
     length: Literal['short', 'long', 'narrow'] = 'long',
     format: str | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str:
@@ -130,12 +130,12 @@ def format_unit(
                              https://unicode.org/repos/cldr/tags/latest/common/validity/unit.xml
     :param length: "short", "long" or "narrow"
     :param format: An optional format, as accepted by `format_decimal`.
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
 
     q_unit = _find_unit_pattern(measurement_unit, locale=locale)
     if not q_unit:
@@ -161,7 +161,7 @@ def format_unit(
 def _find_compound_unit(
     numerator_unit: str,
     denominator_unit: str,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
 ) -> str | None:
     """
     Find a predefined compound unit pattern.
@@ -182,11 +182,11 @@ def _find_compound_unit(
 
     :param numerator_unit: The numerator unit's identifier
     :param denominator_unit: The denominator unit's identifier
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :return: A key to the `unit_patterns` mapping, or None.
     :rtype: str|None
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
 
     # Qualify the numerator and denominator units.  This will turn possibly partial
     # units like "kilometer" or "hour" into actual units like "length-kilometer" and
@@ -217,7 +217,7 @@ def format_compound_unit(
     denominator_unit: str | None = None,
     length: Literal["short", "long", "narrow"] = "long",
     format: str | None = None,
-    locale: Locale | str | None = LC_NUMERIC,
+    locale: Locale | str | None = None,
     *,
     numbering_system: Literal["default"] | str = "latn",
 ) -> str | None:
@@ -265,13 +265,13 @@ def format_compound_unit(
     :param denominator_unit: The denominator unit. See `format_unit`.
     :param length: The formatting length. "short", "long" or "narrow"
     :param format: An optional format, as accepted by `format_decimal`.
-    :param locale: the `Locale` object or locale identifier
+    :param locale: the `Locale` object or locale identifier. Defaults to the system numeric locale.
     :param numbering_system: The numbering system used for formatting number symbols. Defaults to "latn".
                              The special value "default" will use the default numbering system of the locale.
     :return: A formatted compound value.
     :raise `UnsupportedNumberingSystemError`: If the numbering system is not supported by the locale.
     """
-    locale = Locale.parse(locale)
+    locale = Locale.parse(locale or LC_NUMERIC)
 
     # Look for a specific compound unit first...
 


Test patch:
diff --git a/tests/messages/test_pofile.py b/tests/messages/test_pofile.py
index ed9da78..ab7c7f4 100644
--- a/tests/messages/test_pofile.py
+++ b/tests/messages/test_pofile.py
@@ -1088,9 +1088,8 @@ def test_issue_1134(case: str, abort_invalid: bool):
 
     if abort_invalid:
         # Catalog not created, aborted with PoFileError
-        with pytest.raises(pofile.PoFileError) as excinfo:
+        with pytest.raises(pofile.PoFileError, match="missing msgstr for msgid 'foo' on 0"):
             pofile.read_po(buf, abort_invalid=True)
-        assert str(excinfo.value) == "missing msgstr for msgid 'foo' on 0"
     else:
         # Catalog is created with warning, no abort
         output = pofile.read_po(buf)
diff --git a/tests/test_core.py b/tests/test_core.py
index 21debf6..8aa21b4 100644
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -296,10 +296,8 @@ def test_parse_locale():
     assert core.parse_locale('zh_Hans_CN') == ('zh', 'CN', 'Hans', None)
     assert core.parse_locale('zh-CN', sep='-') == ('zh', 'CN', None, None)
 
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError, match="'not_a_LOCALE_String' is not a valid locale identifier"):
         core.parse_locale('not_a_LOCALE_String')
-    assert (excinfo.value.args[0] ==
-            "'not_a_LOCALE_String' is not a valid locale identifier")
 
     assert core.parse_locale('it_IT@euro') == ('it', 'IT', None, None, 'euro')
     assert core.parse_locale('it_IT@something') == ('it', 'IT', None, None, 'something')
@@ -308,6 +306,9 @@ def test_parse_locale():
     assert (core.parse_locale('de_DE.iso885915@euro') ==
             ('de', 'DE', None, None, 'euro'))
 
+    with pytest.raises(ValueError, match="empty"):
+        core.parse_locale("")
+
 
 @pytest.mark.parametrize('filename', [
     'babel/global.dat',
@@ -375,3 +376,12 @@ def test_language_alt_official_not_used():
     locale = Locale('mus')
     assert locale.get_display_name() == 'Mvskoke'
     assert locale.get_display_name(Locale('en')) == 'Muscogee'
+
+
+def test_locale_parse_empty():
+    with pytest.raises(ValueError, match="Empty"):
+        Locale.parse("")
+    with pytest.raises(TypeError, match="Empty"):
+        Locale.parse(None)
+    with pytest.raises(TypeError, match="Empty"):
+        Locale.parse(False)  # weird...!
diff --git a/tests/test_lists.py b/tests/test_lists.py
index 46ca10d..ef522f2 100644
--- a/tests/test_lists.py
+++ b/tests/test_lists.py
@@ -30,3 +30,8 @@ def test_issue_1098():
         # Translation verified using Google Translate. It would add more spacing, but the glyphs are correct.
         "1英尺5英寸"
     )
+
+
+def test_lists_default_locale_deprecation():
+    with pytest.warns(DeprecationWarning):
+        _ = lists.DEFAULT_LOCALE
diff --git a/tests/test_localtime.py b/tests/test_localtime.py
index 723ffa0..a7eaba8 100644
--- a/tests/test_localtime.py
+++ b/tests/test_localtime.py
@@ -1,4 +1,6 @@
+import os
 import sys
+from unittest.mock import Mock
 
 import pytest
 
@@ -27,3 +29,16 @@ def test_issue_1092_with_pytz(monkeypatch):
     monkeypatch.setenv("TZ", "/UTC")  # Malformed timezone name.
     with pytest.raises(LookupError):
         get_localzone()
+
+
+@pytest.mark.skipif(
+    sys.platform == "win32",
+    reason="Issue 990 is not applicable on Windows",
+)
+def test_issue_990(monkeypatch):
+    monkeypatch.setenv("TZ", "")
+    fake_readlink = Mock(return_value="/usr/share/zoneinfo////UTC")  # Double slash, oops!
+    monkeypatch.setattr(os, "readlink", fake_readlink)
+    from babel.localtime._unix import _get_localzone
+    assert _get_localzone() is not None
+    fake_readlink.assert_called_with("/etc/localtime")
diff --git a/tests/test_numbers.py b/tests/test_numbers.py
index 106b83e..f3e2dec 100644
--- a/tests/test_numbers.py
+++ b/tests/test_numbers.py
@@ -244,9 +244,8 @@ def test_list_currencies():
     assert isinstance(list_currencies(locale='fr'), set)
     assert list_currencies('fr').issuperset(['BAD', 'BAM', 'KRO'])
 
-    with pytest.raises(ValueError) as excinfo:
+    with pytest.raises(ValueError, match="expected only letters, got 'yo!'"):
         list_currencies('yo!')
-    assert excinfo.value.args[0] == "expected only letters, got 'yo!'"
 
     assert list_currencies(locale='pa_Arab') == {'PKR', 'INR', 'EUR'}
 
@@ -256,9 +255,8 @@ def test_list_currencies():
 def test_validate_currency():
     validate_currency('EUR')
 
-    with pytest.raises(UnknownCurrencyError) as excinfo:
+    with pytest.raises(UnknownCurrencyError, match="Unknown currency 'FUU'."):
         validate_currency('FUU')
-    assert excinfo.value.args[0] == "Unknown currency 'FUU'."
 
 
 def test_is_currency():
@@ -486,6 +484,19 @@ def test_format_currency():
             == 'US$0,00')          # other
 
 
+def test_format_currency_with_none_locale_with_default(monkeypatch):
+    """Test that the default locale is used when locale is None."""
+    monkeypatch.setattr(numbers, "LC_NUMERIC", "fi_FI")
+    assert numbers.format_currency(0, "USD", locale=None) == "0,00\xa0$"
+
+
+def test_format_currency_with_none_locale(monkeypatch):
+    """Test that the API raises the "Empty locale identifier" error when locale is None, and the default is too."""
+    monkeypatch.setattr(numbers, "LC_NUMERIC", None)  # Pretend we couldn't find any locale when importing the module
+    with pytest.raises(TypeError, match="Empty"):
+        numbers.format_currency(0, "USD", locale=None)
+
+
 def test_format_currency_format_type():
     assert (numbers.format_currency(1099.98, 'USD', locale='en_US',
                                     format_type="standard")
@@ -501,10 +512,8 @@ def test_format_currency_format_type():
                                     format_type="accounting")
             == '$0.00')
 
-    with pytest.raises(numbers.UnknownCurrencyFormatError) as excinfo:
-        numbers.format_currency(1099.98, 'USD', locale='en_US',
-                                format_type='unknown')
-    assert excinfo.value.args[0] == "'unknown' is not a known currency format type"
+    with pytest.raises(numbers.UnknownCurrencyFormatError, match="'unknown' is not a known currency format type"):
+        numbers.format_currency(1099.98, 'USD', locale='en_US', format_type='unknown')
 
     assert (numbers.format_currency(1099.98, 'JPY', locale='en_US')
             == '\xa51,100')
@@ -744,9 +753,8 @@ def test_parse_number():
     assert numbers.parse_number('1.099', locale='de_DE') == 1099
     assert numbers.parse_number('1٬099', locale='ar_EG', numbering_system="default") == 1099
 
-    with pytest.raises(numbers.NumberFormatError) as excinfo:
+    with pytest.raises(numbers.NumberFormatError, match="'1.099,98' is not a valid number"):
         numbers.parse_number('1.099,98', locale='de')
-    assert excinfo.value.args[0] == "'1.099,98' is not a valid number"
 
     with pytest.raises(numbers.UnsupportedNumberingSystemError):
         numbers.parse_number('1.099,98', locale='en', numbering_system="unsupported")
@@ -765,9 +773,8 @@ def test_parse_decimal():
             == decimal.Decimal('1099.98'))
     assert numbers.parse_decimal('1.099,98', locale='de') == decimal.Decimal('1099.98')
 
-    with pytest.raises(numbers.NumberFormatError) as excinfo:
+    with pytest.raises(numbers.NumberFormatError, match="'2,109,998' is not a valid decimal number"):
         numbers.parse_decimal('2,109,998', locale='de')
-    assert excinfo.value.args[0] == "'2,109,998' is not a valid decimal number"
 
 
 @pytest.mark.parametrize('string', [
diff --git a/tests/test_support.py b/tests/test_support.py
index a102ae6..6514721 100644
--- a/tests/test_support.py
+++ b/tests/test_support.py
@@ -299,11 +299,9 @@ class LazyProxyTestCase(unittest.TestCase):
             raise AttributeError('message')
 
         proxy = support.LazyProxy(raise_attribute_error)
-        with pytest.raises(AttributeError) as exception:
+        with pytest.raises(AttributeError, match='message'):
             _ = proxy.value
 
-        assert str(exception.value) == 'message'
-
 
 class TestFormat:
     def test_format_datetime(self, timezone_getter):