Repository: mkdocs/mkdocs
Pull request #2981 (2022-09-18 17:29:40)

Update admonition styles in MkDocs theme
* Apply admonition styles also to `<details>` tags

* Remove extraneous margin at the bottom of admonition blocks

* Increase contrast of default admonitions
  ![mkdocs-colors](https://user-images.githubusercontent.com/371383/190920492-c375eff4-317a-4a07-aa30-0915740351b1.gif)

* Add some admonition styles (only for use on MkDocs' site for now)
  ![mkdocs-colors-2](https://user-images.githubusercontent.com/371383/190920487-c2e5bf72-4441-49a1-a64e-654a8c8c4073.png)



Patch:
diff --git a/docs/css/extra.css b/docs/css/extra.css
index 75040dcd..ba566c91 100644
--- a/docs/css/extra.css
+++ b/docs/css/extra.css
@@ -23,6 +23,22 @@ code.no-highlight {
 div.admonition.block>.admonition-title {
     display: none;
 }
+div.admonition.new {
+    color: #15654a;
+    background-color: #e4f7f1;
+    border-color: #bcf1e8;
+}
+
+.admonition.new, details.new {
+    color: #15654a;
+    background-color: #edfff9;
+    border-color: #bcf1e8;
+}
+.admonition.example, details.example {
+    color: #353579;
+    background-color: #f0f1ff;
+    border-color: #d8dcf0;
+}
 
 /* Definition List styles */
 
diff --git a/docs/dev-guide/plugins.md b/docs/dev-guide/plugins.md
index 77c29b97..86e1544f 100644
--- a/docs/dev-guide/plugins.md
+++ b/docs/dev-guide/plugins.md
@@ -140,7 +140,7 @@ There are three kinds of events: [Global Events], [Page Events] and
 [Template Events].
 
 <details class="card">
-  <summary class="card-header">
+  <summary>
     See a diagram with relations between all the plugin events
   </summary>
   <div class="card-body">
diff --git a/docs/user-guide/choosing-your-theme.md b/docs/user-guide/choosing-your-theme.md
index 107600d6..df2431f5 100644
--- a/docs/user-guide/choosing-your-theme.md
+++ b/docs/user-guide/choosing-your-theme.md
@@ -109,6 +109,8 @@ supports the following options:
     * `fa`: Persian (Farsi)
     * `it`: Italian
     * `tr_TR`: Turkish (Turkey)
+    * `ru`: Russian
+    * `uk`: Ukrainian
 
     See the guide on [localizing your theme] for more information.
 
@@ -190,6 +192,8 @@ theme supports the following options:
     * `fa`: Persian (Farsi)
     * `it`: Italian
     * `tr_TR`: Turkish (Turkey)
+    * `ru`: Russian
+    * `uk`: Ukrainian
 
     See the guide on [localizing your theme] for more information.
 
diff --git a/docs/user-guide/configuration.md b/docs/user-guide/configuration.md
index 8d4c2660..f32a26a2 100644
--- a/docs/user-guide/configuration.md
+++ b/docs/user-guide/configuration.md
@@ -480,6 +480,8 @@ creates links that point directly to the target *file* rather than the target
 Determines how warnings are handled. Set to `true` to halt processing when a
 warning is raised. Set to `false` to print a warning and continue processing.
 
+This is also available as a command line flag: `--strict`.
+
 **default**: `false`
 
 ### dev_addr
@@ -577,6 +579,56 @@ This alternative syntax is required if you intend to override some options via
 
 **default**: `[]` (an empty list).
 
+### hooks
+
+NEW: **New in version 1.4.**
+
+A list of paths to Python scripts (relative to `mkdocs.yml`) that are loaded and used as [plugin](#plugins) instances.
+
+For example:
+
+```yaml
+hooks:
+    - my_hooks.py
+```
+
+Then the file *my_hooks.py* can contain any [plugin event handlers](../dev-guide/plugins.md#events) (without `self`), e.g.:
+
+```python
+def on_page_markdown(markdown, **kwargs):
+    return markdown.replace('a', 'z')
+```
+
+Advanced example that produces warnings based on the Markdown content (and warnings are fatal in [strict](#strict) mode):
+
+```python
+import logging, re
+import mkdocs.plugins
+
+log = logging.getLogger('mkdocs')
+
+@mkdocs.plugins.event_priority(-50)
+def on_page_markdown(markdown, page, **kwargs):
+    path = page.file.src_uri
+    for m in re.finditer(r'\bhttp://[^) ]+', markdown):
+        log.warning(f"Documentation file '{path}' contains a non-HTTPS link: {m[0]}")
+```
+
+This does not enable any new abilities compared to [plugins][], it only simplifies one-off usages, as these don't need to be *installed* like plugins do.
+
+Note that for `mkdocs serve` the hook module will *not* be reloaded on each build.
+
+You might have seen this feature in the [mkdocs-simple-hooks plugin](https://github.com/aklajnert/mkdocs-simple-hooks). If using standard method names, it can be directly replaced, e.g.:
+
+```diff
+-plugins:
+-  - mkdocs-simple-hooks:
+-      hooks:
+-        on_page_markdown: 'my_hooks:on_page_markdown'
++hooks:
++  - my_hooks.py
+```
+
 ### plugins
 
 A list of plugins (with optional configuration settings) to use when building
diff --git a/mkdocs/config/config_options.py b/mkdocs/config/config_options.py
index c955b674..c04a3338 100644
--- a/mkdocs/config/config_options.py
+++ b/mkdocs/config/config_options.py
@@ -1,5 +1,6 @@
 from __future__ import annotations
 
+import functools
 import ipaddress
 import os
 import string
@@ -798,10 +799,10 @@ class Plugins(OptionallyRequired):
     def run_validation(self, value):
         if not isinstance(value, (list, tuple, dict)):
             raise ValidationError('Invalid Plugins configuration. Expected a list or dict.')
-        plgins = plugins.PluginCollection()
+        self.plugins = plugins.PluginCollection()
         if isinstance(value, dict):
             for name, cfg in value.items():
-                plgins[name] = self.load_plugin(name, cfg)
+                self.plugins[name] = self.load_plugin(name, cfg)
         else:
             for item in value:
                 if isinstance(item, dict):
@@ -811,8 +812,8 @@ class Plugins(OptionallyRequired):
                     item = name
                 else:
                     cfg = {}
-                plgins[item] = self.load_plugin(item, cfg)
-        return plgins
+                self.plugins[item] = self.load_plugin(item, cfg)
+        return self.plugins
 
     def load_plugin(self, name, config):
         if not isinstance(name, str):
@@ -846,3 +847,34 @@ class Plugins(OptionallyRequired):
         if errors_message:
             raise ValidationError(errors_message)
         return plugin
+
+
+class Hooks(ListOfItems):
+    """A list of Python scripts to be treated as instances of plugins."""
+
+    def __init__(self, plugins_key: str):
+        super().__init__(File(exists=True), default=[])
+        self.plugins_key = plugins_key
+
+    def run_validation(self, value):
+        paths = super().run_validation(value)
+        hooks = {}
+        for name, path in zip(value, paths):
+            hooks[name] = self._load_hook(name, path)
+        return hooks
+
+    @functools.lru_cache(maxsize=None)
+    def _load_hook(self, name, path):
+        import importlib.util
+
+        spec = importlib.util.spec_from_file_location(name, path)
+        if spec is None:
+            raise ValidationError(f"Cannot import path '{path}' as a Python module")
+        module = importlib.util.module_from_spec(spec)
+        spec.loader.exec_module(module)
+        return module
+
+    def post_validation(self, config, key_name):
+        plugins = config[self.plugins_key]
+        for name, hook in config[key_name].items():
+            plugins[name] = hook
diff --git a/mkdocs/config/defaults.py b/mkdocs/config/defaults.py
index cf4b4073..4c36a6eb 100644
--- a/mkdocs/config/defaults.py
+++ b/mkdocs/config/defaults.py
@@ -119,5 +119,9 @@ class _MkDocsConfig:
     A key value pair should be the string name (as the key) and a dict of config
     options (as the value)."""
 
+    hooks = config_options.Hooks('plugins')
+    """A list of filenames that will be imported as Python modules and used as
+    an instance of a plugin each."""
+
     watch = config_options.ListOfPaths(default=[])
     """A list of extra paths to watch while running `mkdocs serve`."""
diff --git a/mkdocs/contrib/search/__init__.py b/mkdocs/contrib/search/__init__.py
index 1be6c440..ebff2682 100644
--- a/mkdocs/contrib/search/__init__.py
+++ b/mkdocs/contrib/search/__init__.py
@@ -18,8 +18,10 @@ class LangOption(config_options.OptionallyRequired):
     """Validate Language(s) provided in config are known languages."""
 
     def get_lunr_supported_lang(self, lang):
+        fallback = {'uk': 'ru'}
         for lang_part in lang.split("_"):
             lang_part = lang_part.lower()
+            lang_part = fallback.get(lang_part, lang_part)
             if os.path.isfile(os.path.join(base_path, 'lunr-language', f'lunr.{lang_part}.js')):
                 return lang_part
 
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index 0793466d..25d63f7a 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -442,12 +442,6 @@ class PluginCollection(OrderedDict):
         )
 
     def __setitem__(self, key: str, value: BasePlugin, **kwargs) -> None:
-        if not isinstance(value, BasePlugin):
-            raise TypeError(
-                f'{self.__module__}.{self.__name__} only accepts values which'
-                f' are instances of {BasePlugin.__module__}.{BasePlugin.__name__}'
-                ' subclasses'
-            )
         super().__setitem__(key, value, **kwargs)
         # Register all of the event methods defined for this Plugin.
         for event_name in (x for x in dir(value) if x.startswith('on_')):
diff --git a/mkdocs/themes/mkdocs/css/base.css b/mkdocs/themes/mkdocs/css/base.css
index 25e17a63..3a4c08f4 100644
--- a/mkdocs/themes/mkdocs/css/base.css
+++ b/mkdocs/themes/mkdocs/css/base.css
@@ -215,7 +215,7 @@ h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink, h4:hover .head
 
 
 
-.admonition {
+.admonition, details {
     padding: 15px;
     margin-bottom: 20px;
     border: 1px solid transparent;
@@ -223,29 +223,33 @@ h1:hover .headerlink, h2:hover .headerlink, h3:hover .headerlink, h4:hover .head
     text-align: left;
 }
 
-.admonition.note { /* csslint allow: adjoining-classes */
-    color: #3a87ad;
-    background-color: #d9edf7;
+.admonition.note, details.note { /* csslint allow: adjoining-classes */
+    color: #2e6b89;
+    background-color: #e2f0f7;
     border-color: #bce8f1;
 }
 
-.admonition.warning { /* csslint allow: adjoining-classes */
-    color: #c09853;
-    background-color: #fcf8e3;
+.admonition.warning, details.warning { /* csslint allow: adjoining-classes */
+    color: #7a6032;
+    background-color: #fffae5;
     border-color: #fbeed5;
 }
 
-.admonition.danger { /* csslint allow: adjoining-classes */
-    color: #b94a48;
-    background-color: #f2dede;
+.admonition.danger, details.danger { /* csslint allow: adjoining-classes */
+    color: #7f3130;
+    background-color: #fde3e3;
     border-color: #eed3d7;
 }
 
-.admonition-title {
+.admonition-title, summary {
     font-weight: bold;
     text-align: left;
 }
 
+.admonition>p:last-child, details>p:last-child {
+    margin-bottom: 0;
+}
+
 @media (max-width: 991.98px) {
     .navbar-collapse.show { /* csslint allow: adjoining-classes */
         overflow-y: auto;
diff --git a/mkdocs/themes/mkdocs/locales/ru/LC_MESSAGES/messages.po b/mkdocs/themes/mkdocs/locales/ru/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..c275758a
--- /dev/null
+++ b/mkdocs/themes/mkdocs/locales/ru/LC_MESSAGES/messages.po
@@ -0,0 +1,102 @@
+# Russian translations for MkDocs.
+# Copyright (C) 2022 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.2\n"
+"Report-Msgid-Bugs-To: https://github.com/mkdocs/mkdocs/issues\n"
+"POT-Creation-Date: 2022-09-13 23:23+0300\n"
+"PO-Revision-Date: 2022-09-13 23:23+0300\n"
+"Last-Translator: Alexander Krasnikov <alexander@krasnikov.net>\n"
+"Language: ru\n"
+"Language-Team: ru <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.10.3\n"
+
+#: mkdocs/themes/mkdocs/404.html:8
+msgid "Page not found"
+msgstr "Страница не найдена"
+
+#: mkdocs/themes/mkdocs/base.html:116
+#: mkdocs/themes/mkdocs/keyboard-modal.html:31
+#: mkdocs/themes/mkdocs/search-modal.html:5
+msgid "Search"
+msgstr "Поиск"
+
+#: mkdocs/themes/mkdocs/base.html:126
+msgid "Previous"
+msgstr "Назад"
+
+#: mkdocs/themes/mkdocs/base.html:131
+msgid "Next"
+msgstr "Вперед"
+
+#: mkdocs/themes/mkdocs/base.html:142 mkdocs/themes/mkdocs/base.html:144
+#: mkdocs/themes/mkdocs/base.html:146 mkdocs/themes/mkdocs/base.html:148
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "Редактировать в %(repo_name)s"
+
+#: mkdocs/themes/mkdocs/base.html:150
+msgid "Edit"
+msgstr "Редактировать"
+
+#: mkdocs/themes/mkdocs/base.html:190
+#, python-format
+msgid "Documentation built with %(mkdocs_link)s."
+msgstr "Документация создана с помощью %(mkdocs_link)s."
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:5
+msgid "Keyboard Shortcuts"
+msgstr "Сочетания клавиш"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:6
+#: mkdocs/themes/mkdocs/search-modal.html:6
+msgid "Close"
+msgstr "Закрыть"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:12
+msgid "Keys"
+msgstr "Клавиши"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:13
+msgid "Action"
+msgstr "Действие"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:19
+msgid "Open this help"
+msgstr "Открыть справку"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:23
+msgid "Next page"
+msgstr "Следующая страница"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:27
+msgid "Previous page"
+msgstr "Предыдущая страница"
+
+#: mkdocs/themes/mkdocs/search-modal.html:9
+msgid "From here you can search these documents. Enter your search terms below."
+msgstr "Чтобы воспользоваться поиском по документам, введите поисковый запрос ниже."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Search..."
+msgstr "Поиск..."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Type search term here"
+msgstr "Введите поисковый запрос"
+
+#: mkdocs/themes/mkdocs/search-modal.html:15
+msgid "No results found"
+msgstr "Ничего не найдено"
+
+#: mkdocs/themes/mkdocs/toc.html:3
+msgid "Table of Contents"
+msgstr "Содержание"
+
diff --git a/mkdocs/themes/mkdocs/locales/uk/LC_MESSAGES/messages.po b/mkdocs/themes/mkdocs/locales/uk/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..681b791c
--- /dev/null
+++ b/mkdocs/themes/mkdocs/locales/uk/LC_MESSAGES/messages.po
@@ -0,0 +1,101 @@
+# Ukrainian translations for MkDocs.
+# Copyright (C) 2022 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.3.1\n"
+"Report-Msgid-Bugs-To: \"https://github.com/mkdocs/mkdocs/issues\"\n"
+"POT-Creation-Date: 2022-08-15 17:05+0200\n"
+"PO-Revision-Date: 2022-09-17 15:51+0200\n"
+"Last-Translator: Oleh Prypin <@oprypin>\n"
+"Language: uk\n"
+"Language-Team: uk <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.10.3\n"
+
+#: mkdocs/themes/mkdocs/404.html:8
+msgid "Page not found"
+msgstr "Сторінку не знайдено"
+
+#: mkdocs/themes/mkdocs/base.html:116
+#: mkdocs/themes/mkdocs/keyboard-modal.html:31
+#: mkdocs/themes/mkdocs/search-modal.html:5
+msgid "Search"
+msgstr "Пошук"
+
+#: mkdocs/themes/mkdocs/base.html:126
+msgid "Previous"
+msgstr "Назад"
+
+#: mkdocs/themes/mkdocs/base.html:131
+msgid "Next"
+msgstr "Вперед"
+
+#: mkdocs/themes/mkdocs/base.html:142 mkdocs/themes/mkdocs/base.html:144
+#: mkdocs/themes/mkdocs/base.html:146 mkdocs/themes/mkdocs/base.html:148
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "Редагувати в %(repo_name)s"
+
+#: mkdocs/themes/mkdocs/base.html:150
+msgid "Edit"
+msgstr "Редагувати"
+
+#: mkdocs/themes/mkdocs/base.html:190
+#, python-format
+msgid "Documentation built with %(mkdocs_link)s."
+msgstr "Документація збудована з %(mkdocs_link)s."
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:5
+msgid "Keyboard Shortcuts"
+msgstr "Комбінації клавіш"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:6
+#: mkdocs/themes/mkdocs/search-modal.html:6
+msgid "Close"
+msgstr "Закрити"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:12
+msgid "Keys"
+msgstr "Клавіші"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:13
+msgid "Action"
+msgstr "Дія"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:19
+msgid "Open this help"
+msgstr "Відкрити цю довідку"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:23
+msgid "Next page"
+msgstr "Наступна сторінка"
+
+#: mkdocs/themes/mkdocs/keyboard-modal.html:27
+msgid "Previous page"
+msgstr "Попередня сторінка"
+
+#: mkdocs/themes/mkdocs/search-modal.html:9
+msgid "From here you can search these documents. Enter your search terms below."
+msgstr "Звідси ви можете шукати документи. Введіть пошуковий запит знизу."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Search..."
+msgstr "Пошук..."
+
+#: mkdocs/themes/mkdocs/search-modal.html:12
+msgid "Type search term here"
+msgstr "Введіть пошуковий запит"
+
+#: mkdocs/themes/mkdocs/search-modal.html:15
+msgid "No results found"
+msgstr "Нічого не знайдено"
+
+#: mkdocs/themes/mkdocs/toc.html:3
+msgid "Table of Contents"
+msgstr "Зміст"
diff --git a/mkdocs/themes/readthedocs/locales/ru/LC_MESSAGES/messages.po b/mkdocs/themes/readthedocs/locales/ru/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..41c21774
--- /dev/null
+++ b/mkdocs/themes/readthedocs/locales/ru/LC_MESSAGES/messages.po
@@ -0,0 +1,110 @@
+# Russian translations for MkDocs.
+# Copyright (C) 2022 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.2\n"
+"Report-Msgid-Bugs-To: https://github.com/mkdocs/mkdocs/issues\n"
+"POT-Creation-Date: 2022-09-13 23:23+0300\n"
+"PO-Revision-Date: 2022-09-13 23:23+0300\n"
+"Last-Translator: Alexander Krasnikov <alexander@krasnikov.net>\n"
+"Language: ru\n"
+"Language-Team: ru\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.10.3\n"
+
+#: mkdocs/themes/readthedocs/404.html:7
+msgid "Page not found"
+msgstr "Страница не найдена"
+
+#: mkdocs/themes/readthedocs/base.html:97
+msgid "Logo"
+msgstr "Логотип"
+
+#: mkdocs/themes/readthedocs/base.html:111
+msgid "Navigation menu"
+msgstr "Навигационное меню"
+
+#: mkdocs/themes/readthedocs/base.html:140
+msgid "Mobile navigation menu"
+msgstr "Мобильная версия навигации"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:3
+msgid "Docs"
+msgstr "Документация"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:18
+#: mkdocs/themes/readthedocs/breadcrumbs.html:20
+#: mkdocs/themes/readthedocs/breadcrumbs.html:22
+#: mkdocs/themes/readthedocs/breadcrumbs.html:24
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "Редактировать в %(repo_name)s"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:26
+msgid "Edit"
+msgstr "Редактировать"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:34
+msgid "Breadcrumb Navigation"
+msgstr "Навигационная цепочка"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:36
+#: mkdocs/themes/readthedocs/footer.html:7
+#: mkdocs/themes/readthedocs/versions.html:17
+msgid "Previous"
+msgstr "Назад"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:39
+#: mkdocs/themes/readthedocs/footer.html:10
+#: mkdocs/themes/readthedocs/versions.html:20
+msgid "Next"
+msgstr "Вперед"
+
+#: mkdocs/themes/readthedocs/footer.html:5
+msgid "Footer Navigation"
+msgstr "Навигация по странице"
+
+#: mkdocs/themes/readthedocs/footer.html:25
+#, python-format
+msgid ""
+"Built with %(mkdocs_link)s using a %(sphinx_link)s provided by "
+"%(rtd_link)s."
+msgstr ""
+"Собрано в %(mkdocs_link)s с использованием %(sphinx_link)s от "
+"%(rtd_link)s."
+
+#: mkdocs/themes/readthedocs/search.html:5
+msgid "Search Results"
+msgstr "Результаты поиска"
+
+#: mkdocs/themes/readthedocs/search.html:9
+msgid "Search the Docs"
+msgstr "Поиск в документах"
+
+#: mkdocs/themes/readthedocs/search.html:9
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Type search term here"
+msgstr "Введите поисковый запрос"
+
+#: mkdocs/themes/readthedocs/search.html:12
+msgid "No results found"
+msgstr "Ничего не найдено"
+
+#: mkdocs/themes/readthedocs/search.html:13
+msgid "Searching..."
+msgstr "Поиск..."
+
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Search docs"
+msgstr "Поиск в документации"
+
+#: mkdocs/themes/readthedocs/versions.html:1
+msgid "Versions"
+msgstr "Версии"
+
diff --git a/mkdocs/themes/readthedocs/locales/uk/LC_MESSAGES/messages.po b/mkdocs/themes/readthedocs/locales/uk/LC_MESSAGES/messages.po
new file mode 100644
index 00000000..739f4184
--- /dev/null
+++ b/mkdocs/themes/readthedocs/locales/uk/LC_MESSAGES/messages.po
@@ -0,0 +1,110 @@
+# Ukrainian translations for MkDocs.
+# Copyright (C) 2022 MkDocs
+# This file is distributed under the same license as the MkDocs project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2021.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: MkDocs 1.3.1\n"
+"Report-Msgid-Bugs-To: \"https://github.com/mkdocs/mkdocs/issues\"\n"
+"POT-Creation-Date: 2022-08-15 17:05+0200\n"
+"PO-Revision-Date: 2022-09-17 15:51+0200\n"
+"Last-Translator: Oleh Prypin <@oprypin>\n"
+"Language: uk\n"
+"Language-Team: uk <LL@li.org>\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.10.3\n"
+
+#: mkdocs/themes/readthedocs/404.html:7
+msgid "Page not found"
+msgstr "Сторінку не знайдено"
+
+#: mkdocs/themes/readthedocs/base.html:97
+msgid "Logo"
+msgstr "Логотип"
+
+#: mkdocs/themes/readthedocs/base.html:111
+msgid "Navigation menu"
+msgstr "Меню навігації"
+
+#: mkdocs/themes/readthedocs/base.html:140
+msgid "Mobile navigation menu"
+msgstr "Мобільна версія меню"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:3
+msgid "Docs"
+msgstr "Документація"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:18
+#: mkdocs/themes/readthedocs/breadcrumbs.html:20
+#: mkdocs/themes/readthedocs/breadcrumbs.html:22
+#: mkdocs/themes/readthedocs/breadcrumbs.html:24
+#, python-format
+msgid "Edit on %(repo_name)s"
+msgstr "Редагувати в %(repo_name)s"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:26
+msgid "Edit"
+msgstr "Редагувати"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:34
+msgid "Breadcrumb Navigation"
+msgstr "Навігаційна стежка"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:36
+#: mkdocs/themes/readthedocs/footer.html:7
+#: mkdocs/themes/readthedocs/versions.html:17
+msgid "Previous"
+msgstr "Назад"
+
+#: mkdocs/themes/readthedocs/breadcrumbs.html:39
+#: mkdocs/themes/readthedocs/footer.html:10
+#: mkdocs/themes/readthedocs/versions.html:20
+msgid "Next"
+msgstr "Вперед"
+
+#: mkdocs/themes/readthedocs/footer.html:5
+msgid "Footer Navigation"
+msgstr "Нижнє меню навігації"
+
+#: mkdocs/themes/readthedocs/footer.html:25
+#, python-format
+msgid ""
+"Built with %(mkdocs_link)s using a %(sphinx_link)s provided by "
+"%(rtd_link)s."
+msgstr ""
+"Зібрано у %(mkdocs_link)s з використанням %(sphinx_link)s від "
+"%(rtd_link)s."
+
+#: mkdocs/themes/readthedocs/search.html:5
+msgid "Search Results"
+msgstr "Результати пошуку"
+
+#: mkdocs/themes/readthedocs/search.html:9
+msgid "Search the Docs"
+msgstr "Пошук у документах"
+
+#: mkdocs/themes/readthedocs/search.html:9
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Type search term here"
+msgstr "Введіть пошуковий запит"
+
+#: mkdocs/themes/readthedocs/search.html:12
+msgid "No results found"
+msgstr "Нічого не знайдено"
+
+#: mkdocs/themes/readthedocs/search.html:13
+msgid "Searching..."
+msgstr "Пошук..."
+
+#: mkdocs/themes/readthedocs/searchbox.html:3
+msgid "Search docs"
+msgstr "Пошук у документації"
+
+#: mkdocs/themes/readthedocs/versions.html:1
+msgid "Versions"
+msgstr "Версії"


Test patch:
diff --git a/mkdocs/tests/config/config_options_tests.py b/mkdocs/tests/config/config_options_tests.py
index a1b0b3c1..738c7fbe 100644
--- a/mkdocs/tests/config/config_options_tests.py
+++ b/mkdocs/tests/config/config_options_tests.py
@@ -9,7 +9,7 @@ from unittest.mock import patch
 import mkdocs
 from mkdocs.config import base, config_options
 from mkdocs.tests.base import tempdir
-from mkdocs.utils import yaml_load
+from mkdocs.utils import write_file, yaml_load
 
 
 class UnexpectedError(Exception):
@@ -1536,3 +1536,34 @@ class MarkdownExtensionsTest(TestCase):
             {},
         )
         self.assertIsNone(conf['mdx_configs'].get('toc'))
+
+
+class TestHooks(TestCase):
+    class Schema:
+        plugins = config_options.Plugins(default=[])
+        hooks = config_options.Hooks('plugins')
+
+    @tempdir()
+    def test_hooks(self, src_dir):
+        write_file(
+            b'def on_page_markdown(markdown, **kwargs): return markdown.replace("f", "z")',
+            os.path.join(src_dir, 'hooks', 'my_hook.py'),
+        )
+        write_file(
+            b'foo foo',
+            os.path.join(src_dir, 'docs', 'index.md'),
+        )
+        conf = self.get_config(
+            self.Schema,
+            {'hooks': ['hooks/my_hook.py']},
+            config_file_path=os.path.join(src_dir, 'mkdocs.yml'),
+        )
+        self.assertIn('hooks/my_hook.py', conf['plugins'])
+        hook = conf['plugins']['hooks/my_hook.py']
+        self.assertTrue(hasattr(hook, 'on_page_markdown'))
+        self.assertEqual(
+            {**conf['plugins'].events, 'page_markdown': [hook.on_page_markdown]},
+            conf['plugins'].events,
+        )
+        self.assertEqual(hook.on_page_markdown('foo foo'), 'zoo zoo')
+        self.assertFalse(hasattr(hook, 'on_nav'))