docs: translations: add translations links when they exist
authorVegard Nossum <vegard.nossum@oracle.com>
Fri, 15 Dec 2023 12:37:01 +0000 (13:37 +0100)
committerJonathan Corbet <corbet@lwn.net>
Tue, 19 Dec 2023 21:34:59 +0000 (14:34 -0700)
Add a new Sphinx extension that knows about the translations of kernel
documentation and can insert links to the translations at the top of
the document.

It basically works like this:

1. Register a new node type, LanguagesNode.

2. Register a new transform, TranslationsTransform, that inserts a new
   LanguageNode at the top of every document. The LanguageNode contains
   "pending references" to translations of the document. The key here
   is that these are pending (i.e. unresolved) references that may or
   may not actually exist.

3. Register a 'doctree-resolved' event that iterates over all the
   LanguageNode nodes. Any unresolved references are filtered out; the
   list of resolved references is passed to the 'translations.html'
   template and rendered as an HTML node (if HTML output is selected).

Testing: make htmldocs, make latexdocs with Sphinx v4.3.2 and Firefox.

v2:
- changed bar into a drop-down menu
- fixed language labels
- fixed hysteresis reported by Akira Yokosawa

Cc: Federico Vaga <federico.vaga@vaga.pv.it>
Cc: Jani Nikula <jani.nikula@linux.intel.com>
Cc: Akira Yokosawa <akiyks@gmail.com>
Cc: Yanteng Si <siyanteng@loongson.cn>
Signed-off-by: Vegard Nossum <vegard.nossum@oracle.com>
Signed-off-by: Jonathan Corbet <corbet@lwn.net>
Link: https://lore.kernel.org/r/20231215123701.2712807-1-vegard.nossum@oracle.com
Documentation/conf.py
Documentation/sphinx-static/custom.css
Documentation/sphinx/templates/translations.html [new file with mode: 0644]
Documentation/sphinx/translations.py [new file with mode: 0644]

index a3d1fe9dfcf92081e0d75b67f4b35a5f3a839f60..5830b01c56429d38f18e12778ebce543605b3296 100644 (file)
@@ -55,7 +55,7 @@ needs_sphinx = '2.4.4'
 extensions = ['kerneldoc', 'rstFlatTable', 'kernel_include',
               'kfigure', 'sphinx.ext.ifconfig', 'automarkup',
               'maintainers_include', 'sphinx.ext.autosectionlabel',
-              'kernel_abi', 'kernel_feat']
+              'kernel_abi', 'kernel_feat', 'translations']
 
 if major >= 3:
     if (major > 3) or (minor > 0 or patch >= 2):
index a88ef12428fa5d4555064be4caefbc5978785d00..f4285417c71aa9365d1110b5790fec8da6e149fc 100644 (file)
@@ -83,3 +83,56 @@ input.kernel-toc-toggle { display: none; }
     h3.kernel-toc-contents { display: inline; }
     div.kerneltoc a { color: black; }
 }
+
+/* Language selection menu */
+
+div.admonition {
+    /*
+     * Make sure we don't overlap notes and warnings at the top of the
+     * document.
+     */
+    clear: both;
+}
+
+div.language-selection {
+    background: #eeeeee;
+    border: 1px solid #cccccc;
+    margin-bottom: 1em;
+    padding: .5em;
+
+    position: relative;
+    float: right;
+}
+
+div.language-selection a {
+    display: block;
+    padding: 0.5em;
+    color: #333333;
+    text-decoration: none;
+}
+
+div.language-selection ul {
+    display: none;
+    position: absolute;
+
+    /* Align with the parent div */
+    top: 100%;
+    right: 0;
+    margin: 0;
+
+    list-style: none;
+
+    background: #fafafa;
+    border: 1px solid #cccccc;
+
+    /* Never break menu item lines */
+    white-space: nowrap;
+}
+
+div.language-selection:hover ul {
+    display: block;
+}
+
+div.language-selection ul li:hover {
+    background: #dddddd;
+}
diff --git a/Documentation/sphinx/templates/translations.html b/Documentation/sphinx/templates/translations.html
new file mode 100644 (file)
index 0000000..8df5d42
--- /dev/null
@@ -0,0 +1,15 @@
+<!-- SPDX-License-Identifier: GPL-2.0 -->
+<!-- Copyright © 2023, Oracle and/or its affiliates. -->
+
+{# Create a language menu for translations #}
+{% if languages|length > 0: %}
+<div class="language-selection">
+{{ current_language }}
+
+<ul>
+{% for ref in languages: %}
+<li><a href="{{ ref.refuri }}">{{ ref.astext() }}</a></li>
+{% endfor %}
+</ul>
+</div>
+{% endif %}
diff --git a/Documentation/sphinx/translations.py b/Documentation/sphinx/translations.py
new file mode 100644 (file)
index 0000000..47161e6
--- /dev/null
@@ -0,0 +1,101 @@
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright © 2023, Oracle and/or its affiliates.
+# Author: Vegard Nossum <vegard.nossum@oracle.com>
+#
+# Add translation links to the top of the document.
+#
+
+import os
+
+from docutils import nodes
+from docutils.transforms import Transform
+
+import sphinx
+from sphinx import addnodes
+from sphinx.errors import NoUri
+
+all_languages = {
+    # English is always first
+    None: 'English',
+
+    # Keep the rest sorted alphabetically
+    'zh_CN': 'Chinese (Simplified)',
+    'zh_TW': 'Chinese (Traditional)',
+    'it_IT': 'Italian',
+    'ja_JP': 'Japanese',
+    'ko_KR': 'Korean',
+    'sp_SP': 'Spanish',
+}
+
+class LanguagesNode(nodes.Element):
+    def __init__(self, current_language, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.current_language = current_language
+
+class TranslationsTransform(Transform):
+    default_priority = 900
+
+    def apply(self):
+        app = self.document.settings.env.app
+        docname = self.document.settings.env.docname
+
+        this_lang_code = None
+        components = docname.split(os.sep)
+        if components[0] == 'translations' and len(components) > 2:
+            this_lang_code = components[1]
+
+            # normalize docname to be the untranslated one
+            docname = os.path.join(*components[2:])
+
+        new_nodes = LanguagesNode(all_languages[this_lang_code])
+
+        for lang_code, lang_name in all_languages.items():
+            if lang_code == this_lang_code:
+                continue
+
+            if lang_code is None:
+                target_name = docname
+            else:
+                target_name = os.path.join('translations', lang_code, docname)
+
+            pxref = addnodes.pending_xref('', refdomain='std',
+                reftype='doc', reftarget='/' + target_name, modname=None,
+                classname=None, refexplicit=True)
+            pxref += nodes.Text(lang_name)
+            new_nodes += pxref
+
+        self.document.insert(0, new_nodes)
+
+def process_languages(app, doctree, docname):
+    for node in doctree.traverse(LanguagesNode):
+        if app.builder.format not in ['html']:
+            node.parent.remove(node)
+            continue
+
+        languages = []
+
+        # Iterate over the child nodes; any resolved links will have
+        # the type 'nodes.reference', while unresolved links will be
+        # type 'nodes.Text'.
+        languages = list(filter(lambda xref:
+            isinstance(xref, nodes.reference), node.children))
+
+        html_content = app.builder.templates.render('translations.html',
+            context={
+                'current_language': node.current_language,
+                'languages': languages,
+            })
+
+        node.replace_self(nodes.raw('', html_content, format='html'))
+
+def setup(app):
+    app.add_node(LanguagesNode)
+    app.add_transform(TranslationsTransform)
+    app.connect('doctree-resolved', process_languages)
+
+    return {
+        'parallel_read_safe': True,
+        'parallel_write_safe': True,
+    }