Repository: Ouranosinc/xclim

Issue:
Reading YAML file code seems need add encoding setting
* xclim version: 0.29.0
* Python version: 3.9.7
* Operating System: Windows 10(zh-cn)

### Description
<!--Describe what you were trying to get done.
Tell us what happened, what went wrong, and what you expected to happen.-->
Maybe Reading YAML file part need add parameter `encoding='UTF8'`.

### What I Did
```
import xclim
```

### What I Received
```
UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 3097: illegal multibyte sequence
---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_3096/3692564148.py in <module>
----> 1 import xclim

~\miniconda3\envs\climpred\lib\site-packages\xclim\__init__.py in <module>
     25 # Virtual modules creation:
     26 with path("xclim.data", "icclim.yml") as f:
---> 27     build_indicator_module_from_yaml(f.with_suffix(""), mode="raise")
     28 with path("xclim.data", "anuclim.yml") as f:
     29     build_indicator_module_from_yaml(f.with_suffix(""), mode="raise")

~\miniconda3\envs\climpred\lib\site-packages\xclim\core\indicator.py in build_indicator_module_from_yaml(filename, name, base, doc, indices, translations, mode, realm, keywords, references, notes)
   1352     # Read YAML file
   1353     with ymlpath.open() as f: #encoding='UTF8'
-> 1354         yml = safe_load(f)
   1355 
   1356     # Load values from top-level in yml.

~\miniconda3\envs\climpred\lib\site-packages\yaml\__init__.py in safe_load(stream)
    160     to be safe for untrusted input.
    161     """
--> 162     return load(stream, SafeLoader)
    163 
    164 def safe_load_all(stream):

~\miniconda3\envs\climpred\lib\site-packages\yaml\__init__.py in load(stream, Loader)
    110         Loader = FullLoader
    111 
--> 112     loader = Loader(stream)
    113     try:
    114         return loader.get_single_data()

~\miniconda3\envs\climpred\lib\site-packages\yaml\loader.py in __init__(self, stream)
     32 
     33     def __init__(self, stream):
---> 34         Reader.__init__(self, stream)
     35         Scanner.__init__(self)
     36         Parser.__init__(self)

~\miniconda3\envs\climpred\lib\site-packages\yaml\reader.py in __init__(self, stream)
     83             self.eof = False
     84             self.raw_buffer = None
---> 85             self.determine_encoding()
     86 
     87     def peek(self, index=0):

~\miniconda3\envs\climpred\lib\site-packages\yaml\reader.py in determine_encoding(self)
    122     def determine_encoding(self):
    123         while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2):
--> 124             self.update_raw()
    125         if isinstance(self.raw_buffer, bytes):
    126             if self.raw_buffer.startswith(codecs.BOM_UTF16_LE):

~\miniconda3\envs\climpred\lib\site-packages\yaml\reader.py in update_raw(self, size)
    176 
    177     def update_raw(self, size=4096):
--> 178         data = self.stream.read(size)
    179         if self.raw_buffer is None:
    180             self.raw_buffer = data

UnicodeDecodeError: 'gbk' codec can't decode byte 0xa6 in position 3097: illegal multibyte sequence
```

### What I Change

I think it is system coding fault, so I try add to parameter `encoding='UTF8'`, it seems OK. But I am not sure, Because I don't know the whole code very well.

```python
...
    # Read YAML file
    with ymlpath.open(encoding='UTF8') as f: 
        yml = safe_load(f)
...
```



Discussion:

Patch:
diff --git a/HISTORY.rst b/HISTORY.rst
index 13b838e3..581e8cde 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -44,6 +44,7 @@ Bug fixes
 * Fix bug in ``missing_wmo``, where a period would be considered valid if all months met WMO criteria, but complete months in a year were missing. Now if any month does not meet criteria or is absent, the period will be considered missing.
 * Fix bootstrapping with dask arrays. Dask does not support using ``loc`` with multiple indexes to set new values so a workaround was necessary.
 * Fix bootstrapping when the bootstrapped year must be converted to a 366_day calendar.
+* Virtual modules and translations now use 'UTF-8' by default when reading yaml or json file, instead of a machine-dependent encoding.
 
 Internal Changes
 ~~~~~~~~~~~~~~~~
diff --git a/xclim/core/indicator.py b/xclim/core/indicator.py
index d42f4210..315517bc 100644
--- a/xclim/core/indicator.py
+++ b/xclim/core/indicator.py
@@ -1289,6 +1289,7 @@ def build_indicator_module_from_yaml(
     keywords: Optional[str] = None,
     references: Optional[str] = None,
     notes: Optional[str] = None,
+    encoding: str = "UTF8",
 ) -> ModuleType:
     """Build or extend an indicator module from a YAML file.
 
@@ -1324,6 +1325,9 @@ def build_indicator_module_from_yaml(
       Other indicator attributes that would apply to all indicators in this module.
       Values given here are overridden by the ones given in individual definition, but
       they override the ones given at top-level in the YAMl file.
+    encoding: str
+      The encoding used to open the `.yaml` and `.json` files.
+      It defaults to UTF-8, overriding python's mechanism which is machine dependent.
 
     Returns
     -------
@@ -1356,7 +1360,7 @@ def build_indicator_module_from_yaml(
         ymlpath = filepath
 
     # Read YAML file
-    with ymlpath.open() as f:
+    with ymlpath.open(encoding=encoding) as f:
         yml = safe_load(f)
 
     # Load values from top-level in yml.
@@ -1377,7 +1381,9 @@ def build_indicator_module_from_yaml(
             translations = {}
             for locfile in filepath.parent.glob(filepath.stem + ".*.json"):
                 locale = locfile.suffixes[0][1:]
-                translations[locale] = read_locale_file(locfile, module=module_name)
+                translations[locale] = read_locale_file(
+                    locfile, module=module_name, encoding=encoding
+                )
 
     # Module-wide default values for some attributes
     defkwargs = {
diff --git a/xclim/core/locales.py b/xclim/core/locales.py
index 39a89393..dd571341 100644
--- a/xclim/core/locales.py
+++ b/xclim/core/locales.py
@@ -231,12 +231,21 @@ class UnavailableLocaleError(ValueError):
         )
 
 
-def read_locale_file(filename, module=None):
+def read_locale_file(filename, module=None, encoding="UTF8"):
     """Read a locale file (*.json) and return its dictionary.
 
-    If module is a string, this module name is added to all identifiers translated in this file.
+    Parameters
+    ----------
+    filename: PathLike
+      The file to read.
+    module: string, optional
+      If module is a string, this module name is added to all identifiers translated in this file.
+      Defaults to None, and no module name is added (as if the indicator was an official xclim indicator).
+    encoding : string
+      The encoding to use when reading the file.
+      Defaults to UTF-8, overriding python's default mechanism which is machine dependent.
     """
-    with open(filename, "r", encoding="utf-8") as f:
+    with open(filename, "r", encoding=encoding) as f:
         locdict = json.load(f)
 
     if module is not None:


Test patch:
diff --git a/xclim/testing/tests/test_modules.py b/xclim/testing/tests/test_modules.py
index 39667ba4..55d8c073 100644
--- a/xclim/testing/tests/test_modules.py
+++ b/xclim/testing/tests/test_modules.py
@@ -85,3 +85,24 @@ def test_custom_indices():
 
     # Check that missing was not modified even with injecting `freq`.
     assert ex1.RX5day.missing == indicators.atmos.max_n_day_precipitation_amount.missing
+
+
+# It's not really slow, but this is an unstable test (when it fails) and we might not want to execute it on all builds
+@pytest.mark.slow
+def test_encoding():
+    import sys
+
+    import _locale
+
+    # remove xclim
+    del sys.modules["xclim"]
+
+    # patch so that the default encoding is not UTF-8
+    old = _locale.nl_langinfo
+    _locale.nl_langinfo = lambda x: "GBK"
+
+    try:
+        import xclim  # noqa
+    finally:
+        # Put the correct function back
+        _locale.nl_langinfo = old