From 1415bd25171199896cc83ba3ccc7d8ceedd07a3f Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Sun, 31 May 2026 10:18:37 +0800 Subject: [PATCH 1/9] docs(html): KiCAD-inspired topbar, side TOC, language switcher, admonition tints Re-implemented on top of PR #4081's docs/build/ layout. Replaces the previous prototype branch (commit 766f2d9103) which targeted the pre-#4081 docs/html/ paths. - Sticky topbar with LinuxCNC logo on every page; matches asciidoctor's footer colour scheme. Logo SVG lives at docs/src/lcnc-docs.svg and is copied into docs/build/html/ by the shared-asset rule. - Side TOC via asciidoctor :toc=left; auto-hides below 800px (CSS). - Language switcher in the topbar (top-right) built from po4a.cfg + a LANG_LABEL_ map in Submakefile, so po4a.cfg stays the single source of truth. - Admonition blocks (note/tip/warning/caution/important) tinted by type with Unicode icons mapped onto asciidoctor's icons=font output (no FontAwesome dep). Build-time injection: - docs/src/docinfo-header.html generated from .html.in template; asciidoctor pages pick it up via docinfo=shared. - docs/src/gcode.html renamed to gcode.html.in (po4a master); per-lang docs/build/html//gcode.html generated from po4a's gcode-raw.html + per-lang topbar fragment. - Per-lang docs/build/html//index.html similarly gets topbar injection + CSS path rewrite + objects/index.incl appended so the manpage list is present on translated indexes too. - docs/src/index.tmpl's hardcoded Translations: list dropped; topbar switcher replaces it. - English + translated manpage HTML rules pass lcnc-lang-label and lcnc-subpath so the switcher works on manpages. Per-page language indication via lang_switcher_postprocess.py: walks docs/build/html at end of build, looks up each page's master in each lang's .po, counts msgid-with-master-loc vs translated, toggles class=lcnc-lang-unavail on the
  • when coverage falls below POKEEP (default 80). Same pass marks details-list entries (manpage index lists) whose target file does not exist for the current language. Idempotent: same toggle pattern across re-runs. POKEEP is overridable on the make command line (POKEEP=30 surfaces in-progress translations); contributing-to-linuxcnc.adoc, building-linuxcnc.adoc, and docs/README.adoc document the knob. CSS adds .lcnc-lang-unavail and .lcnc-link-unavail rules: dim text, pointer-events: none, cursor not-allowed; .lcnc-link-unavail also adds strikethrough for the index list context. --- docs/README.adoc | 7 + docs/po4a.cfg | 2 +- docs/src/.gitignore | 1 + docs/src/Submakefile | 145 ++++++++++-- docs/src/code/building-linuxcnc.adoc | 8 + docs/src/code/contributing-to-linuxcnc.adoc | 25 ++ docs/src/docinfo-header.html.in | 6 + docs/src/{gcode.html => gcode.html.in} | 22 -- docs/src/index.tmpl | 15 -- docs/src/lang_switcher_postprocess.py | 249 ++++++++++++++++++++ docs/src/lcnc-docs.svg | 190 +++++++++++++++ docs/src/lcnc-overrides.css | 208 ++++++++++++++++ 12 files changed, 826 insertions(+), 52 deletions(-) create mode 100644 docs/src/.gitignore create mode 100644 docs/src/docinfo-header.html.in rename docs/src/{gcode.html => gcode.html.in} (94%) create mode 100644 docs/src/lang_switcher_postprocess.py create mode 100644 docs/src/lcnc-docs.svg diff --git a/docs/README.adoc b/docs/README.adoc index ef6db1bd0e4..99d8f9b61ea 100644 --- a/docs/README.adoc +++ b/docs/README.adoc @@ -19,6 +19,13 @@ the index.tmpl. `po4a.cfg` is used to map source files to translated documentation when generating translated files using the files in `docs/po/`. +Every translated HTML page is emitted regardless of completeness; the +topbar language-switcher post-process greys out per-page entries whose +`.po` coverage falls below `POKEEP` percent (default 80). Override at +build time: `make POKEEP=30 docs` to surface in-progress translations, +`make POKEEP=0 docs` to keep every entry clickable. See +`docs/src/code/contributing-to-linuxcnc.adoc` for details. + == Notes on LinuxCNC documentation The main LinuxCNC Makefile can optionally build the documentation and diff --git a/docs/po4a.cfg b/docs/po4a.cfg index 9a97dd1abe2..e107c102b38 100644 --- a/docs/po4a.cfg +++ b/docs/po4a.cfg @@ -79,7 +79,7 @@ [type: AsciiDoc_def] src/examples/spindle.adoc $lang:build/adoc/$lang/examples/spindle.adoc [type: AsciiDoc_def] src/gcode/coordinates.adoc $lang:build/adoc/$lang/gcode/coordinates.adoc [type: AsciiDoc_def] src/gcode/g-code.adoc $lang:build/adoc/$lang/gcode/g-code.adoc -[type: xhtml_def] src/gcode.html $lang:build/html/$lang/gcode.html +[type: xhtml_def] src/gcode.html.in $lang:build/adoc/$lang/gcode-raw.html [type: AsciiDoc_def] src/gcode/machining-center.adoc $lang:build/adoc/$lang/gcode/machining-center.adoc [type: AsciiDoc_def] src/gcode/m-code.adoc $lang:build/adoc/$lang/gcode/m-code.adoc [type: AsciiDoc_def] src/gcode/o-code.adoc $lang:build/adoc/$lang/gcode/o-code.adoc diff --git a/docs/src/.gitignore b/docs/src/.gitignore new file mode 100644 index 00000000000..89cc0d93c3c --- /dev/null +++ b/docs/src/.gitignore @@ -0,0 +1 @@ +docinfo-header.html diff --git a/docs/src/Submakefile b/docs/src/Submakefile index 7611b346612..6c28aeded2e 100644 --- a/docs/src/Submakefile +++ b/docs/src/Submakefile @@ -59,6 +59,25 @@ ASCIIDOCTOR_DEFAULT_CSS := $(shell ruby -e 'require "asciidoctor"; print Asciido LANGUAGES := $(strip $(shell sed -e's/#.*//' < $(DOC_DIR)/po4a.cfg | grep '^\[po4a_langs\]' | cut -d" " -f2-)) LANGUAGES_MATCH := $(shell echo $(LANGUAGES) | tr " " "|") +# Native display labels used by the topbar language switcher. +LANG_LABEL_en := English +LANG_LABEL_de := Deutsch +LANG_LABEL_es := Español +LANG_LABEL_fr := Français +LANG_LABEL_nb := Norsk +LANG_LABEL_ru := Русский +LANG_LABEL_uk := Українська +LANG_LABEL_zh_CN := 中文 + +# Minimum per-master translation completeness (percent) below which the +# language-switcher post-process pass greys out a page's entry for that +# language. The translated HTML is still emitted (po4a runs with --keep 0) +# so deep links never 404; the post-process just demotes to . +# Default 80% matches po4a's own default and the MDN/Hugo/Sphinx +# convention. Translators previewing work-in-progress can lower it: +# `make POKEEP=30 docs`. +POKEEP ?= 80 + GENERATED_MANPAGES += ../docs/man/man1/linuxcnc.1 GENERATED_MANPAGES += $(patsubst ../docs/src/man/%.adoc, ../docs/man/%, $(wildcard ../docs/src/man/*/*.adoc)) @@ -442,10 +461,51 @@ endif # English gcode reference must land in the build tree even when only PDF docs # are enabled (debian/configure passes --enable-build-documentation=pdf, so # .copy-asciidoc-stamp -- which lives under htmldocs -- does not fire). +# Per-lang topbar fragments. Pre-substituted versions of docinfo-header.html +# with lcnc-cssrel and lcnc-lang-label resolved; consumed by the gcode.html +# and index.html rules below, which still need to substitute {lcnc-subpath}. +objects/topbar-en.html: $(DOC_SRCDIR)/docinfo-header.html + @mkdir -p $(@D) + @sed -e 's|{lcnc-cssrel}|../|g' -e 's|{lcnc-lang-label}|$(LANG_LABEL_en)|g' $< > $@ + +$(foreach L,$(LANGUAGES),objects/topbar-$(L).html): objects/topbar-%.html: $(DOC_SRCDIR)/docinfo-header.html + @mkdir -p $(@D) + @sed -e 's|{lcnc-cssrel}|../|g' -e 's|{lcnc-lang-label}|$(LANG_LABEL_$*)|g' $< > $@ + +# debian/linuxcnc-doc-en.docs installs docs/build/html/en/gcode.html, so the +# English gcode reference must land in the build tree even when only PDF docs +# are enabled (debian/configure passes --enable-build-documentation=pdf, so +# .copy-asciidoc-stamp -- which lives under htmldocs -- does not fire). +# Wraps the .in template with the English topbar fragment and rewrites the +# CSS path (the source sits at docs/build/html/en/, css at docs/build/html/). docs: $(DOC_OUT_HTML)/en/gcode.html -$(DOC_OUT_HTML)/en/gcode.html: $(DOC_SRCDIR)/gcode.html +$(DOC_OUT_HTML)/en/gcode.html: $(DOC_SRCDIR)/gcode.html.in objects/topbar-en.html $(DOC_SRCDIR)/Submakefile @mkdir -p $(@D) - cp -f $< $@ + $(Q){ \ + topbar=$$(mktemp) ; \ + sed 's|{lcnc-subpath}|gcode.html|g' objects/topbar-en.html > $$topbar ; \ + awk -v t="$$topbar" 'BEGIN{ while ((getline line < t) > 0) buf = buf line "\n" } //{ print; printf "%s", buf; next } { print }' $< \ + | sed 's|href="lcnc-overrides.css"|href="../lcnc-overrides.css"|g' > $@ ; \ + rm -f $$topbar ; \ + } + +# Per-language gcode.html: po4a translates docs/src/gcode.html.in into +# $(DOC_OUT_ADOC)//gcode-raw.html. Wrap each raw output with the +# lang-specific topbar to produce the final $(DOC_OUT_HTML)//gcode.html. +$(foreach L,$(LANGUAGES),$(DOC_OUT_HTML)/$(L)/gcode.html): $(DOC_OUT_HTML)/%/gcode.html: objects/topbar-%.html $(DOC_SRCDIR)/Submakefile | translateddocs + @mkdir -p $(dir $@) + $(Q){ \ + topbar=$$(mktemp) ; \ + sed 's|{lcnc-subpath}|gcode.html|g' objects/topbar-$*.html > $$topbar ; \ + awk -v t="$$topbar" 'BEGIN{ while ((getline line < t) > 0) buf = buf line "\n" } //{ print; printf "%s", buf; next } { print }' $(DOC_OUT_ADOC)/$*/gcode-raw.html \ + | sed 's|href="lcnc-overrides.css"|href="../lcnc-overrides.css"|g' > $@ ; \ + rm -f $$topbar ; \ + } + +# Hook per-lang gcode.html into the docs target when translations are on. +ifeq ($(BUILD_DOCS_TRANSLATED),yes) +docs: $(foreach L,$(LANGUAGES),$(DOC_OUT_HTML)/$(L)/gcode.html) +endif pdfdocs: svgs_made_from_dots $(PDF_TARGETS) $(DOC_OUT_HTML)/pdf/index.html @@ -475,9 +535,24 @@ endif # gen_complist aliases: a phony prereq is always "newer", so naming them # re-touched .htmldoc-stamp every run, dragging checkref and the css copy # with it. The stamps fire only when their real inputs change. -.htmldoc-stamp: .copy-asciidoc-stamp $(DOC_DIR)/.gen_complist-stamp $(HTML_TARGETS) .images-stamp .include-stamp $(DOC_OUT_HTML)/asciidoctor.css $(DOC_OUT_HTML)/rouge-github.css +.htmldoc-stamp: .copy-asciidoc-stamp $(DOC_DIR)/.gen_complist-stamp $(HTML_TARGETS) .images-stamp .include-stamp $(DOC_OUT_HTML)/asciidoctor.css $(DOC_OUT_HTML)/rouge-github.css .lang-switcher-stamp touch $@ +# Walk every generated HTML page and grey out language-switcher entries +# whose translated counterpart is below POKEEP coverage. Runs at the end +# of the build (depends on every HTML target so it sees the final tree). +# The script is idempotent so re-running over a processed tree is a +# content-stable no-op. Gated on BUILD_DOCS_TRANSLATED: with English-only +# builds there is nothing to grey out. +ifeq ($(BUILD_DOCS_TRANSLATED),yes) +.lang-switcher-stamp: $(DOC_SRCDIR)/lang_switcher_postprocess.py $(HTML_TARGETS) $(TRANSLATED_MAN_HTML_TARGETS) $(wildcard $(DOC_DIR)/po/*.po) + $(Q)python3 $(DOC_SRCDIR)/lang_switcher_postprocess.py $(DOC_OUT_HTML) $(DOC_DIR)/po $(POKEEP) $(LANGUAGES) + @touch $@ +else +.lang-switcher-stamp: + @touch $@ +endif + # Shared assets at the top of the html tree. Index templates and the # asciidoctor docinfo reference them via ../ relative paths so we ship # one copy regardless of how many languages render. gcode.html is NOT @@ -485,9 +560,32 @@ endif # to its sibling gcode.html. SHARED_HTML_ASSETS = \ $(DOC_SRCDIR)/lcnc-overrides.css \ + $(DOC_SRCDIR)/lcnc-docs.svg \ $(DOC_SRCDIR)/index.css \ $(DOC_SRCDIR)/linuxcnc-logo-chips.png +# docinfo-header.html: generated from .in template. The @LANGUAGE_SWITCHER@ +# placeholder expands to one
  • per language (English plus everything +# in $(LANGUAGES)), labelled via $(LANG_LABEL_). asciidoctor then +# substitutes the {lcnc-cssrel} / {lcnc-lang-label} / {lcnc-subpath} +# attributes per page. Po4a.cfg drives the language list and the +# LANG_LABEL_* mapping above, so po4a.cfg is the single source of truth. +$(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_DIR)/po4a.cfg $(DOC_SRCDIR)/Submakefile + @block=$$(mktemp); \ + printf '
    \n' > $$block ; \ + printf ' \n' >> $$block ; \ + printf ' \n' >> $$block ; \ + printf ' \n' >> $$block ; \ + printf '
    \n' >> $$block ; \ + awk -v block="$$block" ' \ + /@LANGUAGE_SWITCHER@/ { while ((getline line < block) > 0) print line; next } \ + { print } \ + ' $< > $@ ; \ + rm -f $$block + # Stamp-gated asset copy; copy_asciidoc_files stays as a phony alias. # The en/gcode.html copy lives in its own rule above so it fires for PDF-only # builds as well; .copy-asciidoc-stamp depends on it for HTML builds. @@ -607,7 +705,7 @@ $(foreach L,$(LANGUAGES), \ # from docs/man//manN. cssrel is ../../../ for both: each output # sits at docs/build/html//man/manN/X.html (4 levels under html/). define MAN_HTML_RULE -$(DOC_OUT_HTML)/$(1)/man/%.html: $(2)/% $(DOC_SRCDIR)/docinfo.html +$(DOC_OUT_HTML)/$(1)/man/%.html: $(2)/% $(DOC_SRCDIR)/docinfo.html $(DOC_SRCDIR)/docinfo-header.html @$$(ECHO) Formatting $$(notdir $$<) as HTML @mkdir -p $$(dir $$@) $$(Q)if grep -q '^\.so' $$<; then \ @@ -636,6 +734,8 @@ $(DOC_OUT_HTML)/$(1)/man/%.html: $(2)/% $(DOC_SRCDIR)/docinfo.html -a mansource=LinuxCNC \ -a manmanual='LinuxCNC Documentation' \ -a "lcnc-cssrel=../../../" \ + -a "lcnc-lang-label=$(LANG_LABEL_$(1))" \ + -a "lcnc-subpath=$$(patsubst $(DOC_OUT_HTML)/$(1)/%,%,$$@)" \ -a docinfo=shared \ -a docinfodir=$$(realpath $$(DOC_SRCDIR)) \ -a source-highlighter=rouge \ @@ -696,19 +796,34 @@ objects/index.incl: $(GENERATED_MANPAGES) objects/var-MAN_HTML_TARGETS $(DOC_SRC mkdir -p $(DOC_OUT_HTML)/en/man/man/images/ find $(DOC_DIR)/man -maxdepth 3 -name "*.png" ! -name "grohtml*" -exec mv {} "$(DOC_OUT_HTML)/en/man/man/images/" \; -$(DOC_OUT_HTML)/%/index.html: $(DOC_OUT_ADOC)/%/index.tmpl ../VERSION $(DOC_SRCDIR)/index.foot +$(DOC_OUT_HTML)/%/index.html: $(DOC_OUT_ADOC)/%/index.tmpl objects/index.incl ../VERSION $(DOC_SRCDIR)/index.foot $(DOC_SRCDIR)/docinfo-header.html $(DOC_SRCDIR)/Submakefile @mkdir -p $(dir $@) - cat $(filter-out ../VERSION, $^) | \ - sed "s/@VERSION@/`cat ../VERSION`/" | \ - if [ "yes" != "$(BUILD_DOCS_TRANSLATED)" ]; then sed '/@TRANSLATIONS@/,/@ENDTRANSLATIONS@/d' ; else grep -Ev '@(END)?TRANSLATIONS@'; fi > $@ + $(Q){ \ + topbar=$$(mktemp) ; \ + sed -e 's|{lcnc-cssrel}|../|g' -e 's|{lcnc-lang-label}|$(LANG_LABEL_$*)|g' -e 's|{lcnc-subpath}|index.html|g' $(DOC_SRCDIR)/docinfo-header.html > $$topbar ; \ + (cat $(DOC_OUT_ADOC)/$*/index.tmpl objects/index.incl $(DOC_SRCDIR)/index.foot) \ + | sed "s/@VERSION@/`cat ../VERSION`/" \ + | awk -v t="$$topbar" 'BEGIN{ while ((getline line < t) > 0) buf = buf line "\n" } //{ print; printf "%s", buf; next } { print }' \ + | sed 's|href="lcnc-overrides.css"|href="../lcnc-overrides.css"|g' \ + | if [ "yes" != "$(BUILD_DOCS_TRANSLATED)" ]; then sed '/@TRANSLATIONS@/,/@ENDTRANSLATIONS@/d' ; else grep -Ev '@(END)?TRANSLATIONS@'; fi > $@ ; \ + rm -f $$topbar ; \ + } # Rich English landing page lives at /en/index.html (sibling to the # translated //index.html files), generated from index.tmpl # plus the manpage index include. -$(DOC_OUT_HTML)/en/index.html: $(DOC_SRCDIR)/index.tmpl objects/index.incl $(DOC_SRCDIR)/index.foot ../VERSION $(DOC_SRCDIR)/Submakefile +$(DOC_OUT_HTML)/en/index.html: $(DOC_SRCDIR)/index.tmpl objects/index.incl $(DOC_SRCDIR)/index.foot ../VERSION $(DOC_SRCDIR)/docinfo-header.html $(DOC_SRCDIR)/Submakefile @mkdir -p $(dir $@) - (cat $(DOC_SRCDIR)/index.tmpl objects/index.incl $(DOC_SRCDIR)/index.foot) | sed "s/@VERSION@/`cat ../VERSION`/" | \ - if [ "yes" != "$(BUILD_DOCS_TRANSLATED)" ]; then sed '/@TRANSLATIONS@/,/@ENDTRANSLATIONS@/d' ; else grep -Ev '@(END)?TRANSLATIONS@'; fi > $@ + $(Q){ \ + topbar=$$(mktemp) ; \ + sed 's|{lcnc-subpath}|index.html|g' objects/topbar-en.html > $$topbar ; \ + (cat $(DOC_SRCDIR)/index.tmpl objects/index.incl $(DOC_SRCDIR)/index.foot) \ + | sed "s/@VERSION@/`cat ../VERSION`/" \ + | awk -v t="$$topbar" 'BEGIN{ while ((getline line < t) > 0) buf = buf line "\n" } //{ print; printf "%s", buf; next } { print }' \ + | sed 's|href="lcnc-overrides.css"|href="../lcnc-overrides.css"|g' \ + | if [ "yes" != "$(BUILD_DOCS_TRANSLATED)" ]; then sed '/@TRANSLATIONS@/,/@ENDTRANSLATIONS@/d' ; else grep -Ev '@(END)?TRANSLATIONS@'; fi > $@ ; \ + rm -f $$topbar ; \ + } # Tree-root redirect: docs/build/html/index.html bounces to en/. The # page is a checked-in static file under docs/src/; just copy it. @@ -950,7 +1065,7 @@ $(foreach L,$(LANGUAGES),$(eval $(call HTML_COPY_RULE,$(L)))) LANG=$$(echo $$HTML_REL | cut -d/ -f1); \ REST=$$(echo $$HTML_REL | cut -d/ -f2-); \ HTML_DIR=$$(dirname $$REST | cut -d/ -f1); \ - for IMAGE_FILE in $$(grep -oE 'src="[^"]+"' $$HTML_FILE | sed 's/src="//;s/"$$//' | grep -vE '^https?:|^data:|^/'); do \ + for IMAGE_FILE in $$(grep -oE 'src="[^"]+"' $$HTML_FILE | sed 's/src="//;s/"$$//' | grep -vE '^https?:|^data:|^/|lcnc-docs\.svg'); do \ IMAGE_DIR=$$(dirname $$IMAGE_FILE); \ IMAGE_PATH=$(DOC_SRCDIR)/$$HTML_DIR/$$IMAGE_FILE; \ mkdir -p $(DOC_OUT_HTML)/$$LANG/$$HTML_DIR/$$IMAGE_DIR; \ @@ -1024,7 +1139,7 @@ $(DOC_OUT_ADOC)/%.html: LCNC_CSSREL=$(shell python3 -c "print('../' * '$*'.count # $4 xref-exclude pattern (English filters all lang subdirs; translated # trees are rooted inside their own lang dir so the exclude is empty). define ASCIIDOCTOR_HTML_RULE -$$(patsubst %.adoc,$2/%.html,$$(DOC_SRCS_$(call toUC,$1)_SMALL)): $2/%.html: $2/%.adoc $$(DOC_SRCDIR)/docinfo.html +$$(patsubst %.adoc,$2/%.html,$$(DOC_SRCS_$(call toUC,$1)_SMALL)): $2/%.html: $2/%.adoc $$(DOC_SRCDIR)/docinfo.html $$(DOC_SRCDIR)/docinfo-header.html $$(ECHO) "Building '$1' adoc to html: " $$< $$(Q)asciidoctor -r $$(realpath $$(DOC_SRCDIR))/extensions/xref_resolver.rb \ -r $$(realpath $$(DOC_SRCDIR))/extensions/rouge_hal.rb \ @@ -1035,6 +1150,8 @@ $$(patsubst %.adoc,$2/%.html,$$(DOC_SRCS_$(call toUC,$1)_SMALL)): $2/%.html: $2/ -a "xref-exclude=$4" \ -a "relindir=$$(shell dirname $$*)" \ -a "lcnc-cssrel=$$(LCNC_CSSREL)" \ + -a "lcnc-lang-label=$(LANG_LABEL_$1)" \ + -a "lcnc-subpath=$$(patsubst $1/%,%,$$*).html" \ -a docinfo=shared \ -a docinfodir=$$(realpath $$(DOC_SRCDIR)) \ -a source-highlighter=rouge \ @@ -1042,7 +1159,7 @@ $$(patsubst %.adoc,$2/%.html,$$(DOC_SRCS_$(call toUC,$1)_SMALL)): $2/%.html: $2/ -a webfonts! \ -a linkcss -a copycss! \ -a "stylesdir=$$(LCNC_CSSREL)" \ - -d book -a toc -a numbered \ + -d book -a toc=left -a numbered \ -o $$@ $$< || (X=$$$$?; rm -f $$@; exit $$$$X) endef diff --git a/docs/src/code/building-linuxcnc.adoc b/docs/src/code/building-linuxcnc.adoc index c5dbd87c433..831f6276430 100644 --- a/docs/src/code/building-linuxcnc.adoc +++ b/docs/src/code/building-linuxcnc.adoc @@ -194,6 +194,14 @@ The most commonly used arguments are: Disable building the translated documentation for all available languages. The building of the translated documentation takes a huge amount of time, so it is recommend to skip that if not really needed. +Translation completeness threshold:: + `make POKEEP=N docs`. Per-master threshold (percent) below which a + language-switcher entry is greyed out on the corresponding page. + Translated HTML is always emitted; this knob only affects whether + the entry stays clickable. Default 80. Translators can lower the + threshold (`POKEEP=30`, `POKEEP=0`) to make their work-in-progress + show up as live links. See the contributing guide for details. + [[make-arguments]] ==== `make` arguments diff --git a/docs/src/code/contributing-to-linuxcnc.adoc b/docs/src/code/contributing-to-linuxcnc.adoc index 85e32473be4..edaaa75b7e3 100644 --- a/docs/src/code/contributing-to-linuxcnc.adoc +++ b/docs/src/code/contributing-to-linuxcnc.adoc @@ -260,6 +260,31 @@ https://hosted.weblate.org/projects/linuxcnc/ Documentation on how to use Weblate is here: https://docs.weblate.org/en/latest/user/basic.html +=== Previewing a translation locally + +Every translated HTML page is emitted regardless of how much of its +source has actually been translated. The topbar language switcher +greys out entries whose underlying `.po` coverage for that specific +master falls below *80%* by default. Greyed entries are still in the +DOM (so deep links keep working) but rendered as plain text rather +than clickable links, signalling to readers that the page is mostly +English. + +If you are actively translating and want your work-in-progress to +appear as a live link on its companion pages, lower the threshold at +build time: + +[source,sh] +---- +cd src +make POKEEP=30 docs # any page >= 30% translated stays clickable +make POKEEP=0 docs # everything clickable, even untouched pages +---- + +`POKEEP` is consumed by the post-process pass that walks every HTML +file once at the end of the build. The default value, used by the +published build, is 80. + == Other ways to contribute There are many ways to contribute to LinuxCNC, that are not addressed diff --git a/docs/src/docinfo-header.html.in b/docs/src/docinfo-header.html.in new file mode 100644 index 00000000000..7f035248368 --- /dev/null +++ b/docs/src/docinfo-header.html.in @@ -0,0 +1,6 @@ +
    + + LinuxCNC Documentation + +@LANGUAGE_SWITCHER@ +
    diff --git a/docs/src/gcode.html b/docs/src/gcode.html.in similarity index 94% rename from docs/src/gcode.html rename to docs/src/gcode.html.in index 2ae21a5d18b..6e4c641129f 100644 --- a/docs/src/gcode.html +++ b/docs/src/gcode.html.in @@ -197,28 +197,6 @@ } } -function fixup_urls() { -var links=document.evaluate('//a[@href]', document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); - for(var i=0; i diff --git a/docs/src/index.tmpl b/docs/src/index.tmpl index c8fa4a0e0f7..d8eaa6ca1ad 100644 --- a/docs/src/index.tmpl +++ b/docs/src/index.tmpl @@ -15,21 +15,6 @@ LinuxCNC Logo -@TRANSLATIONS@ -

    Translations: -Arabic -Deutsch -Español -Français -Norsk bokmål -Russian -Svensk - -Ukranian -中文 -

    -@ENDTRANSLATIONS@ -

    LinuxCNC version @VERSION@ Documentation

    diff --git a/docs/src/lang_switcher_postprocess.py b/docs/src/lang_switcher_postprocess.py new file mode 100644 index 00000000000..2fe0905e997 --- /dev/null +++ b/docs/src/lang_switcher_postprocess.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +"""Post-process every generated HTML page to grey out language-switcher +entries that point at a poorly-translated counterpart. + +Each rendered HTML page contains a full switcher (one
  • per +language) emitted at asciidoctor time before per-page completeness is +known. This pass walks the final docs/html/ tree and rewrites entries +that fall below the threshold to
  • ...
  • +so readers know at a glance which translations exist for the page they +are on. + +Decision per (page, language): + 1. If no translated HTML file at the link's target, grey out (the file + genuinely is missing, not just sparse). This covers manpages, the + gcode landing page, and any other page outside the AsciiDoc pipeline. + 2. Otherwise look up the page's master source in the language's .po + file, compute (translated / total) over msgids whose location + comment points at that master, grey out if below the threshold. + 3. Pages without a discoverable master (index.html, generated pages) + fall back to step 1 (file existence). + +Idempotent: a second run sees the same on-disk state plus the same .po +ratios, produces the same output, safe to bake into the build via a +stamp target.""" + +import os +import re +import sys +from collections import defaultdict + +LIST_RE = re.compile( + r'(
      )(.*?)(
    )', + re.DOTALL, +) +# Generic link entry inside
    . Used by the +# manpage / docs index lists on index.html pages; on translated indexes +# the list is reused from English so some targets do not exist for that +# language. Same class as the lang-switcher grey -- pointer-events: none +# and dim colour. +DETAILS_BLOCK_RE = re.compile( + r'(
    )(.*?)(
    )', + re.DOTALL, +) +DETAILS_ENTRY_RE = re.compile( + r'(\s*)
    ([^<]+)' +) +# Match every
  • in the list, capturing its class attr (if any), the +# href, and the visible label. The class attr is rewritten in-place each +# run so re-running with a different threshold is reversible. +ENTRY_RE = re.compile( + r'(\s*)' + r'([^<]+)
  • ' +) + + +def parse_po(path): + """Yield (msgid_nonempty, msgstr_nonempty, fuzzy, locations) per entry.""" + results = [] + entry = { + 'msgid': '', 'msgstr': '', 'fuzzy': False, 'locs': [], + 'in_msgid': False, 'in_msgstr': False, + } + + def reset(): + entry['msgid'] = '' + entry['msgstr'] = '' + entry['fuzzy'] = False + entry['locs'] = [] + entry['in_msgid'] = False + entry['in_msgstr'] = False + + def emit(): + results.append((bool(entry['msgid']), bool(entry['msgstr']), + entry['fuzzy'], list(entry['locs']))) + + str_re = re.compile(r'^"(.*)"\s*$') + with open(path, 'r', encoding='utf-8') as f: + for line in f: + line = line.rstrip('\n') + if line == '': + if entry['msgid'] or entry['msgstr'] or entry['locs']: + emit() + reset() + continue + if line.startswith('#~'): + continue + if line.startswith('#:'): + entry['locs'].extend(line[2:].split()) + continue + if line.startswith('#,'): + if 'fuzzy' in line: + entry['fuzzy'] = True + continue + if line.startswith('#'): + continue + if line.startswith('msgid '): + entry['in_msgid'] = True + entry['in_msgstr'] = False + m = str_re.match(line[6:]) + if m: + entry['msgid'] += m.group(1) + continue + if line.startswith('msgstr '): + entry['in_msgid'] = False + entry['in_msgstr'] = True + m = str_re.match(line[7:]) + if m: + entry['msgstr'] += m.group(1) + continue + m = str_re.match(line) + if m: + if entry['in_msgid']: + entry['msgid'] += m.group(1) + elif entry['in_msgstr']: + entry['msgstr'] += m.group(1) + continue + if entry['msgid'] or entry['msgstr'] or entry['locs']: + emit() + return results + + +def per_master_ratio(po_path): + """Return {master_path: (total, translated)} mined from .po.""" + ratios = defaultdict(lambda: [0, 0]) + loc_re = re.compile(r'^([^:]+\.[A-Za-z0-9]+):\d+$') + for has_id, has_str, fuzzy, locs in parse_po(po_path): + if not has_id: + continue + seen = set() + for loc in locs: + m = loc_re.match(loc) + if m: + seen.add(m.group(1)) + for master in seen: + ratios[master][0] += 1 + if has_str and not fuzzy: + ratios[master][1] += 1 + return ratios + + +# Map: page's docs/html-relative subpath -> master source path used by .po +# location comments. e.g. de/config/ini-config.html -> src/config/ini-config.adoc +def subpath_to_master(subpath): + # Strip leading language dir if present (de/foo -> foo). + parts = subpath.split('/') + if parts and parts[0] in LANGUAGES: + parts = parts[1:] + flat = '/'.join(parts) + if flat.endswith('.html'): + flat = flat[:-5] + '.adoc' + # Manpages: docs/html/man/man1/foo.1.html corresponds to src/man/man1/foo.1.adoc + # Other docs: docs/html/config/foo.html corresponds to src/config/foo.adoc + return 'src/' + flat + + +LANGUAGES = [] # populated from CLI + + +def rewrite_block(html_path, html_dir, html_root, po_ratios, threshold, block_body): + def repl(m): + indent, href, label = m.group(1), m.group(2), m.group(3) + target = os.path.normpath(os.path.join(html_dir, href)) + unavail = False + if target.startswith(html_root): + if not os.path.exists(target): + unavail = True + else: + rel = os.path.relpath(target, html_root) + parts = rel.split('/') + if parts and parts[0] in LANGUAGES: + lang = parts[0] + master = subpath_to_master(rel) + ratios = po_ratios.get(lang, {}) + tot, tr = ratios.get(master, (0, 0)) + if tot > 0 and (100.0 * tr / tot) < threshold: + unavail = True + cls = ' class="lcnc-lang-unavail"' if unavail else '' + return f'{indent}{label}' + return ENTRY_RE.sub(repl, block_body) + + +def rewrite_details_block(html_dir, block_body): + """Toggle the lcnc-link-unavail class on every
  • entry in a + details-list block based on whether the link target exists on disk. + Used so translated index pages do not surface dead manpage links.""" + def repl(m): + before, after, href, label = m.group(1), m.group(2), m.group(3), m.group(4) + target = os.path.normpath(os.path.join(html_dir, href)) + if os.path.exists(target): + cls = '' + else: + cls = ' class="lcnc-link-unavail"' + return f'{before}{cls}{after}{label}
  • ' + return DETAILS_ENTRY_RE.sub(repl, block_body) + + +def process(html_path, html_root, po_ratios, threshold): + with open(html_path, 'r', encoding='utf-8') as f: + content = f.read() + if 'lcnc-lang-list' not in content and 'details-list' not in content: + return False + html_dir = os.path.dirname(html_path) + new_content = content + + if 'lcnc-lang-list' in new_content: + def list_repl(m): + return m.group(1) + rewrite_block(html_path, html_dir, html_root, + po_ratios, threshold, m.group(2)) + m.group(3) + new_content = LIST_RE.sub(list_repl, new_content) + + if 'details-list' in new_content: + def details_repl(m): + return m.group(1) + rewrite_details_block(html_dir, m.group(2)) + m.group(3) + new_content = DETAILS_BLOCK_RE.sub(details_repl, new_content) + + if new_content == content: + return False + with open(html_path, 'w', encoding='utf-8') as f: + f.write(new_content) + return True + + +def main(html_root, po_dir, threshold, languages): + global LANGUAGES + LANGUAGES = languages + html_root = os.path.abspath(html_root) + po_ratios = {} + for lang in languages: + po_path = os.path.join(po_dir, f'{lang}.po') + if os.path.exists(po_path): + po_ratios[lang] = per_master_ratio(po_path) + changed = 0 + seen = 0 + for dirpath, _dirs, files in os.walk(html_root): + for name in files: + if not name.endswith('.html'): + continue + seen += 1 + if process(os.path.join(dirpath, name), html_root, po_ratios, threshold): + changed += 1 + print(f'lang_switcher_postprocess: scanned {seen} HTML files, rewrote {changed} ' + f'(threshold {threshold}%)') + + +if __name__ == '__main__': + if len(sys.argv) < 5: + sys.stderr.write('Usage: lang_switcher_postprocess.py [lang2 ...]\n') + sys.exit(2) + main(sys.argv[1], sys.argv[2], float(sys.argv[3]), sys.argv[4:]) diff --git a/docs/src/lcnc-docs.svg b/docs/src/lcnc-docs.svg new file mode 100644 index 00000000000..c10ca68c145 --- /dev/null +++ b/docs/src/lcnc-docs.svg @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/lcnc-overrides.css b/docs/src/lcnc-overrides.css index 1b0df7b90c7..3f5e97a83d4 100644 --- a/docs/src/lcnc-overrides.css +++ b/docs/src/lcnc-overrides.css @@ -63,6 +63,192 @@ body:is(.article,.book,.manpage) .literalblock pre.highlight { background: #f7f7f7; } +/* Top bar: fixed full-width bar above the page; offsets body content + and asciidoctor's :toc: left sidebar so they start below it. Padding + keyed off :has so it also applies to the static landing pages + (index.html, gcode.html) that have no asciidoctor body class. */ +body:has(.lcnc-topbar) { + padding-top: 3rem; +} +/* Static landing pages (index.html, gcode.html) have no asciidoctor + #header margin to space them off the topbar -- add some breathing room. */ +body:has(.lcnc-topbar):not(.article):not(.book):not(.manpage) { + padding-top: 5rem; +} +.lcnc-topbar { + position: fixed; + top: 0; + left: 0; + right: 0; + height: 3rem; + display: flex; + align-items: center; + padding: 0 1.5rem; + background: rgba(0,0,0,.8); + color: hsla(0,0%,100%,.8); + font-size: 1rem; + z-index: 1100; /* above asciidoctor's toc2 (auto/none) */ + box-shadow: 0 1px 3px rgba(0,0,0,0.15); +} +.lcnc-topbar-home { + color: hsla(0,0%,100%,.8); + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + height: 100%; +} +.lcnc-topbar-home img { + height: 2.25rem; /* fits inside the 3rem topbar with a little padding */ + width: auto; + aspect-ratio: 1203.31 / 227.01; /* matches the SVG viewBox so width: auto works without intrinsic dims */ + display: block; +} + +/* Language switcher: hidden checkbox + label drives the dropdown. + Total CSS control, no native disclosure widget to fight. */ +.lcnc-lang-switcher { + margin-left: auto; + position: relative; + display: flex; + align-items: center; + height: 100%; + /* Pin font: index.html and asciidoctor pages use different body fonts + (sans-serif vs Noto Serif), which the dropdown would otherwise inherit. */ + font-family: "Open Sans","DejaVu Sans",sans-serif; +} +.lcnc-lang-toggle { + position: absolute; + opacity: 0; + pointer-events: none; +} +.lcnc-lang-label { + cursor: pointer; + padding: 0 0.75em; + color: hsla(0,0%,100%,.8); + font-weight: 500; + user-select: none; +} +.lcnc-lang-label::after { + content: " \25BE"; /* ▾ */ + font-size: 0.8em; + margin-left: 0.25em; +} +.lcnc-lang-list { + position: absolute; + right: 0; + top: 100%; + margin: 0; + padding: 0.25em 0; + list-style: none; + background: rgba(0,0,0,.85); + border-radius: 0 0 4px 4px; + min-width: 10em; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + display: none; +} +.lcnc-lang-switcher:has(.lcnc-lang-toggle:checked) .lcnc-lang-list { + display: block; +} +.lcnc-lang-list li { + margin: 0; + padding: 0; + list-style: none; +} +/* The #lcnc-topbar id (1,0,0) cleanly outranks the index-page rules + body:not(.article):not(.book):not(.manpage) a / a:visited (0,4,2), so + visited and unvisited dropdown links both render in white. */ +#lcnc-topbar .lcnc-lang-list a, +#lcnc-topbar .lcnc-lang-list a:link, +#lcnc-topbar .lcnc-lang-list a:visited { + display: block; + padding: 0.4em 1em; + color: hsla(0,0%,100%,.85); + text-decoration: none; +} +#lcnc-topbar .lcnc-lang-list a:hover, +#lcnc-topbar .lcnc-lang-list a:focus { + background: hsla(0,0%,100%,.12); + color: #fff; +} +/* Languages whose translated counterpart is missing or sparse for the + current page get .lcnc-lang-unavail on the
  • from the post-process + pass. Grey the link and disable pointer events so the entry stays in + the DOM (idempotent reversible toggling at build time) but reads as + "exists but not here". */ +#lcnc-topbar .lcnc-lang-list li.lcnc-lang-unavail a, +#lcnc-topbar .lcnc-lang-list li.lcnc-lang-unavail a:link, +#lcnc-topbar .lcnc-lang-list li.lcnc-lang-unavail a:visited { + color: hsla(0,0%,100%,.35); + pointer-events: none; + cursor: not-allowed; +} +/* Generic "link is in this index but the target is not built for this + language" marker. Used on translated index.html pages where the + manpage list is reused from English; entries whose translated counterpart + was not emitted get this class so they read as muted, non-clickable + placeholders. */ +li.lcnc-link-unavail a, +li.lcnc-link-unavail a:link, +li.lcnc-link-unavail a:visited { + opacity: 0.4; + pointer-events: none; + cursor: not-allowed; + text-decoration: line-through; +} +body:is(.article,.book,.manpage).toc2 #toc.toc2 { + top: 3rem; +} + +/* Narrow screens: drop the side TOC, restore full-width content. */ +@media (max-width: 800px) { + body:is(.article,.book,.manpage).toc2 { + padding-left: 0; + } + body:is(.article,.book,.manpage).toc2 #toc.toc2 { + display: none; + } +} + +/* Admonition blocks: tint the whole block by type (borrowed from + KiCAD docs). Asciidoctor renders these as a wrapper div with an + inner table whose background is transparent, so the colour + on the wrapper shows through. Icon cell keeps asciidoctor's text + label ("Note" / "Warning" / ...). */ +body:is(.article,.book,.manpage) .admonitionblock { + border-radius: 4px; + padding: 0.5em 0; + margin: 1.25em 0; +} +body:is(.article,.book,.manpage) .admonitionblock.note, +body:is(.article,.book,.manpage) .admonitionblock.tip { + background-color: #d9edf7; + color: #31708f; +} +body:is(.article,.book,.manpage) .admonitionblock.warning { + background-color: #ffd3d3; + color: #8a6d3b; +} +body:is(.article,.book,.manpage) .admonitionblock.caution, +body:is(.article,.book,.manpage) .admonitionblock.important { + background-color: #fcf8e3; + color: #8a6d3b; +} +/* Admonition icons: map asciidoctor's "icons=font" FontAwesome classes + to Unicode glyphs so we get an icon (not the word "Note") without + loading FontAwesome. Asciidoctor emits + etc; default CSS already sizes them via [class^="fa icon-"]. */ +body:is(.article,.book,.manpage) .admonitionblock td.icon .fa::before { + font-family: inherit; + font-style: normal; + text-shadow: none; +} +body:is(.article,.book,.manpage) .admonitionblock td.icon .icon-note::before { content: "\2139"; } /* ℹ */ +body:is(.article,.book,.manpage) .admonitionblock td.icon .icon-tip::before { content: "\1F4A1"; } /* 💡 */ +body:is(.article,.book,.manpage) .admonitionblock td.icon .icon-warning::before { content: "\26A0"; } /* ⚠ */ +body:is(.article,.book,.manpage) .admonitionblock td.icon .icon-caution::before { content: "\26A1"; } /* ⚡ */ +body:is(.article,.book,.manpage) .admonitionblock td.icon .icon-important::before { content: "\2757"; } /* ❗ */ + /* ====================================================================== * Asciidoctor pages (dark, opt-in via system preference) * ====================================================================== */ @@ -159,6 +345,28 @@ body:is(.article,.book,.manpage) .literalblock pre.highlight { color: #e06464; font-weight: bold; } + body:is(.article,.book,.manpage) .admonitionblock.note, + body:is(.article,.book,.manpage) .admonitionblock.tip { + background-color: #1a3a4d; + color: #a8d3e8; + } + body:is(.article,.book,.manpage) .admonitionblock.warning { + background-color: #4d2828; + color: #f0c0c0; + } + body:is(.article,.book,.manpage) .admonitionblock.caution, + body:is(.article,.book,.manpage) .admonitionblock.important { + background-color: #4d4419; + color: #f0e090; + } + body:is(.article,.book,.manpage) #toc.toc2 { + background: #1e1e1e; + border-right-color: #333; + color: #e0e0e0; + } + body:is(.article,.book,.manpage) #toc.toc2 a { + color: #7fb8e8; + } /* Inkscape-exported SVG figures are transparent with black strokes and disappear on dark page bg. Wrap SVGs only in a white card; From 75425c4607757d30d893cd1de88387ff3331e660 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Thu, 4 Jun 2026 08:43:08 +0800 Subject: [PATCH 2/9] docs: read switcher labels from lang-labels file, not Submakefile Move the native language display names out of the makefile into docs/src/lang-labels (tagname). A language in po4a.cfg without an entry falls back to its tag with a make warning. --- docs/src/Submakefile | 22 ++++++++++------------ docs/src/lang-labels | 11 +++++++++++ 2 files changed, 21 insertions(+), 12 deletions(-) create mode 100644 docs/src/lang-labels diff --git a/docs/src/Submakefile b/docs/src/Submakefile index 6c28aeded2e..e13db9ad0a5 100644 --- a/docs/src/Submakefile +++ b/docs/src/Submakefile @@ -59,15 +59,13 @@ ASCIIDOCTOR_DEFAULT_CSS := $(shell ruby -e 'require "asciidoctor"; print Asciido LANGUAGES := $(strip $(shell sed -e's/#.*//' < $(DOC_DIR)/po4a.cfg | grep '^\[po4a_langs\]' | cut -d" " -f2-)) LANGUAGES_MATCH := $(shell echo $(LANGUAGES) | tr " " "|") -# Native display labels used by the topbar language switcher. -LANG_LABEL_en := English -LANG_LABEL_de := Deutsch -LANG_LABEL_es := Español -LANG_LABEL_fr := Français -LANG_LABEL_nb := Norsk -LANG_LABEL_ru := Русский -LANG_LABEL_uk := Українська -LANG_LABEL_zh_CN := 中文 +# Native switcher labels read from docs/src/lang-labels (tagname); +# a tag with no entry falls back to its code with a warning. +LANG_LABEL_FILE := $(DOC_SRCDIR)/lang-labels +define LANG_LABEL_template +LANG_LABEL_$(1) := $(or $(shell sed -ne 's/^$(1)[[:space:]][[:space:]]*//p' $(LANG_LABEL_FILE)),$(1)$(warning lang-labels: no entry for '$(1)', using tag)) +endef +$(foreach L,en $(LANGUAGES),$(eval $(call LANG_LABEL_template,$(L)))) # Minimum per-master translation completeness (percent) below which the # language-switcher post-process pass greys out a page's entry for that @@ -568,9 +566,9 @@ SHARED_HTML_ASSETS = \ # placeholder expands to one
  • per language (English plus everything # in $(LANGUAGES)), labelled via $(LANG_LABEL_). asciidoctor then # substitutes the {lcnc-cssrel} / {lcnc-lang-label} / {lcnc-subpath} -# attributes per page. Po4a.cfg drives the language list and the -# LANG_LABEL_* mapping above, so po4a.cfg is the single source of truth. -$(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_DIR)/po4a.cfg $(DOC_SRCDIR)/Submakefile +# attributes per page. po4a.cfg drives the language list, lang-labels +# the display names. +$(DOC_SRCDIR)/docinfo-header.html: $(DOC_SRCDIR)/docinfo-header.html.in $(DOC_DIR)/po4a.cfg $(LANG_LABEL_FILE) $(DOC_SRCDIR)/Submakefile @block=$$(mktemp); \ printf '
    \n' > $$block ; \ printf ' \n' >> $$block ; \ diff --git a/docs/src/lang-labels b/docs/src/lang-labels new file mode 100644 index 00000000000..c3e3e264f0d --- /dev/null +++ b/docs/src/lang-labels @@ -0,0 +1,11 @@ +# Native display labels for the docs language switcher. +# Format: . A language listed in +# po4a.cfg [po4a_langs] but absent here falls back to its bare tag. +en English +de Deutsch +es Español +fr Français +nb Norsk +ru Русский +uk Українська +zh_CN 中文 From 90f2023a6192bdf22395d7a57ac2bdfdd9706a6a Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:57:00 +0800 Subject: [PATCH 3/9] docs: per-page translation banner instead of greying switcher Drop the POKEEP percent-threshold hiding: every present translation stays clickable; the switcher only greys a target that does not exist. A page below 100% now carries a no-JS banner below the topbar stating its completeness, tinted red(0%) to green(100%). --- docs/src/lang_switcher_postprocess.py | 116 ++++++++++++++++---------- docs/src/lcnc-overrides.css | 23 +++++ 2 files changed, 93 insertions(+), 46 deletions(-) diff --git a/docs/src/lang_switcher_postprocess.py b/docs/src/lang_switcher_postprocess.py index 2fe0905e997..7122aae973b 100644 --- a/docs/src/lang_switcher_postprocess.py +++ b/docs/src/lang_switcher_postprocess.py @@ -1,27 +1,22 @@ #!/usr/bin/env python3 -"""Post-process every generated HTML page to grey out language-switcher -entries that point at a poorly-translated counterpart. - -Each rendered HTML page contains a full switcher (one
  • per -language) emitted at asciidoctor time before per-page completeness is -known. This pass walks the final docs/html/ tree and rewrites entries -that fall below the threshold to
  • ...
  • -so readers know at a glance which translations exist for the page they -are on. - -Decision per (page, language): - 1. If no translated HTML file at the link's target, grey out (the file - genuinely is missing, not just sparse). This covers manpages, the - gcode landing page, and any other page outside the AsciiDoc pipeline. - 2. Otherwise look up the page's master source in the language's .po - file, compute (translated / total) over msgids whose location - comment points at that master, grey out if below the threshold. - 3. Pages without a discoverable master (index.html, generated pages) - fall back to step 1 (file existence). - -Idempotent: a second run sees the same on-disk state plus the same .po -ratios, produces the same output, safe to bake into the build via a -stamp target.""" +"""Post-process every generated HTML page for translation status. + +Two passes, both reversible so a second run is a no-op (safe to bake into +the build via a stamp target): + + 1. Language switcher: grey a
  • only when its target file is + genuinely absent (so the switcher never offers a 404). Sparse but + present translations stay clickable -- completeness is conveyed by + the banner, not by hiding the link. + 2. Per-page banner: on a translated page that is not fully translated, + inject a no-JS notice just below the topbar stating "this page is + N% translated", tinted red(0%)..yellow..green(100%). N comes from + the language's .po: msgids whose location comment points at the + page's master source, counted translated / total. + +Pages without a discoverable master (English, generated index pages) get +no banner. Dead links in the manpage index lists are still greyed by +file existence (separate concern, see rewrite_details_block).""" import os import re @@ -51,6 +46,12 @@ r'(\s*)' r'([^<]+)
  • ' ) +# Topbar header element (HTML5
    , distinct from asciidoctor's +#