feat(i18n): le bloc de référence passe en anglais, et --lang devient réel

Le vocabulaire des deux générateurs sortait en français en dur. Il est
externalisé dans un dict REF par générateur, EN ANGLAIS, un seul jeu servi
identique dans les trois langues.

La raison est une MESURE, pas un goût : nymea ne sert aucune traduction sur
notre parc. Vérifié le 2026-10-12 sur le banc `hems` —
Integrations.GetThingClasses rend la MÊME charge utile au sha256 près
(243 340 octets) pour fr_FR, de_DE et en_US, et nymea-plugins-translations
n'est pas installé. Les noms de modèles, de registres et d'unités sont donc
déjà en anglais ; traduire les seuls en-têtes de colonnes autour aurait
fabriqué des pages bilingues sans rien gagner. Côté API c'est plus direct
encore : méthodes, champs, types et `description` viennent d'introspect.json.

Ce qui se TRADUIT, en revanche, c'est la prose et la navigation. D'où deux
ajouts côté fiches appareils :

- SCAFFOLD : l'échafaudage de prose par langue (titre de catégorie, intertitre
  « Référence », mention de guide à venir). Sans lui, chaque nouvel appareil
  créerait sa fiche dans la seule langue par défaut, et fallback_to_default la
  servirait EN FRANÇAIS sous /de/ et /en/ — par le simple fait d'être créée.
- CATEGORY_LABELS_I18N pour les titres de pages d'index de catégorie.

`--lang` existait depuis la CI mais n'était PAS lu (« Conservé pour
compatibilité CI »). Il désigne maintenant la langue par défaut — celle dont
les fichiers n'ont pas de suffixe — et `--locales` dit lesquelles générer.
ensure_device_pages() et ensure_category_indexes() bouclent dessus, et ne
réécrivent JAMAIS une page existante : la prose traduite à la main survit aux
régénérations. Vérifié : second passage, « Doc à jour », 0 page régénérée.

Les messages de diagnostic (ERREUR, AVERTISSEMENT, BLOQUANT) restent en
français : ils s'adressent au mainteneur, pas au site.

Note de méthode : la validation par ast.parse() ne voit pas une erreur de table
des symboles (`global` après usage). compile() la voit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-13 06:38:45 +02:00
parent ec3dbd9650
commit e88c283243
2 changed files with 223 additions and 78 deletions

View File

@ -121,10 +121,37 @@ def render_type_self(value: object) -> str:
return f"`{value!s}`"
# ═════════════════════════════════════════════════════════════════════════════
# VOCABULAIRE DU BLOC DE RÉFÉRENCE — EN ANGLAIS, UNE SEULE FOIS
#
# Décision du 2026-10-12 (multi-langue FR/DE/EN) : le contenu entre les marqueurs
# BEGIN/END GENERATED est servi IDENTIQUE dans les trois langues, en anglais.
# Ici la raison est encore plus directe que pour les fiches appareils : les noms
# de méthodes, de champs, de types et les `description` viennent d'introspect.json,
# c'est-à-dire de nymea, et sont déjà en anglais. Traduire les seuls en-têtes de
# colonnes aurait produit des tableaux bilingues.
#
# La PROSE des pages API (api/index.md, integrations/jsonrpc-api.md) est, elle,
# traduite dans `*.de.md` / `*.en.md`.
# ═════════════════════════════════════════════════════════════════════════════
REF = {
"no_params": "_No parameters._\n",
"field_cols": ["Field", "Type", "Notes"],
"optional": "optional",
"readonly": "read-only",
"params_lbl": "**Parameters:**",
"returns_lbl": "**Returns:**",
"methods_h": "## Methods",
"notifs_h": "## Notifications",
"unknown": "unknown",
}
def render_fields_table(fields: dict, types_path: str = "../types.md") -> str:
"""Render a params/returns dict as a Markdown table. Returns trailing newline."""
if not fields:
return "_Aucun paramètre._\n"
return REF["no_params"]
rows = []
for raw_name in sorted(fields):
value = fields[raw_name]
@ -132,13 +159,13 @@ def render_fields_table(fields: dict, types_path: str = "../types.md") -> str:
type_str = render_type(value, types_path)
notes = []
if optional:
notes.append("optionnel")
notes.append(REF["optional"])
if readonly:
notes.append("lecture seule")
notes.append(REF["readonly"])
rows.append([f"`{name}`", type_str, " · ".join(notes)])
lines = [
"| Champ | Type | Notes |",
"| --- | --- | --- |",
"| " + " | ".join(REF["field_cols"]) + " |",
"| " + " | ".join("---" for _ in REF["field_cols"]) + " |",
]
for r in rows:
lines.append("| " + " | ".join(r) + " |")
@ -151,7 +178,7 @@ def render_method(name: str, method: dict, types_path: str) -> str:
perm = method.get("permissionScope", "")
badge = PERM_BADGES.get(perm)
if not badge:
badge = f'<span class="badge perm-unknown">{perm or "inconnu"}</span>'
badge = f'<span class="badge perm-unknown">{perm or REF["unknown"]}</span>'
print(f" INFO : permissionScope inconnu '{perm}' pour {name}", file=sys.stderr)
desc = method.get("description", "").strip()
params = method.get("params") or {}
@ -165,11 +192,11 @@ def render_method(name: str, method: dict, types_path: str) -> str:
if desc:
parts.append(desc)
parts.append("")
parts.append("**Paramètres :**")
parts.append(REF["params_lbl"])
parts.append("")
parts.append(render_fields_table(params, types_path).rstrip())
parts.append("")
parts.append("**Retour :**")
parts.append(REF["returns_lbl"])
parts.append("")
parts.append(render_fields_table(returns, types_path).rstrip())
parts.append("")
@ -193,7 +220,7 @@ def render_namespace_content(
parts: list[str] = []
if ns_methods:
parts.append("## Méthodes")
parts.append(REF["methods_h"])
parts.append("")
for name in sorted(ns_methods):
parts.append(render_method(name, ns_methods[name], types_path))
@ -210,7 +237,7 @@ def render_namespace_content(
if desc:
parts.append(desc)
parts.append("")
parts.append("**Paramètres :**")
parts.append(REF["params_lbl"])
parts.append("")
parts.append(render_fields_table(notif_params, types_path).rstrip())
parts.append("")
@ -245,7 +272,7 @@ def render_notifications_content(notifications: dict, types_path: str = "types.m
if desc:
parts.append(desc)
parts.append("")
parts.append("**Paramètres :**")
parts.append(REF["params_lbl"])
parts.append("")
parts.append(render_fields_table(notif_params, types_path).rstrip())
parts.append("")
@ -279,11 +306,12 @@ def render_types_content(types: dict, enums: dict, flags: dict) -> str:
type_str = render_type_self(value)
notes = []
if optional:
notes.append("optionnel")
notes.append(REF["optional"])
if readonly:
notes.append("lecture seule")
notes.append(REF["readonly"])
rows.append([f"`{field_name}`", type_str, " · ".join(notes)])
lines = ["| Champ | Type | Notes |", "| --- | --- | --- |"]
lines = ["| " + " | ".join(REF["field_cols"]) + " |",
"| " + " | ".join("---" for _ in REF["field_cols"]) + " |"]
for r in rows:
lines.append("| " + " | ".join(r) + " |")
parts.append("\n".join(lines))

View File

@ -29,20 +29,56 @@ try:
except ImportError:
sys.exit("PyYAML est requis : pip install PyYAML")
# ═════════════════════════════════════════════════════════════════════════════
# VOCABULAIRE DU BLOC DE RÉFÉRENCE — EN ANGLAIS, UNE SEULE FOIS
#
# Décision du 2026-10-12 (multi-langue FR/DE/EN) : le contenu entre les
# marqueurs BEGIN/END GENERATED est servi IDENTIQUE dans les trois langues, en
# anglais. Raison : son contenu vient de l'introspection nymea, et nymea ne
# sert AUCUNE traduction sur notre parc — vérifié le 2026-10-12 sur le banc
# `hems`, où GetThingClasses rend la MÊME charge utile (sha256 identique,
# 243 340 octets) pour fr_FR, de_DE et en_US. Les noms de modèles, de registres
# et d'unités sont donc déjà en anglais ; traduire seulement les en-têtes de
# colonnes autour aurait produit des pages bilingues sans rien gagner.
#
# Ce qui est traduit, lui, c'est la NAVIGATION et la PROSE — voir NAV_LABELS
# plus bas et les fichiers `*.de.md` / `*.en.md`.
# ═════════════════════════════════════════════════════════════════════════════
INTERFACE_ROLE = {
"energymeter": "Compteur d'énergie",
"smartmeterconsumer": "Compteur de consommation",
"smartmeterproducer": "Compteur de production",
"evcharger": "Borne de recharge",
"heatpump": "Pompe à chaleur",
"smartmeter": "Compteur intelligent",
"energymeter": "Energy meter",
"smartmeterconsumer": "Consumption meter",
"smartmeterproducer": "Production meter",
"evcharger": "EV charger",
"heatpump": "Heat pump",
"smartmeter": "Smart meter",
}
TECH_INTERFACES = {"connectable", "networkdevice", "wirelessconnectable"}
CREATE_METHOD = {
"discovery": "Découverte automatique",
"user": "Ajout manuel",
"auto": "Automatique",
"discovery": "Auto-discovery",
"user": "Manual",
"auto": "Automatic",
}
# Libellés du bloc de référence. Un seul jeu, anglais (cf. en-tête ci-dessus).
REF = {
"manufacturer": "**Manufacturer:**",
"plugin": "**Plugin:**",
"models_h": "#### Supported models",
"models_cols": ["Model", "Role", "Transport", "Pairing", "Quantities"],
"detail_h": "#### Per-model detail",
"discovery_lbl": "_Discovery parameters:_",
"discovery_cols": ["Key", "Label", "Type", "Range", "Default"],
"settings_lbl": "_Settings:_",
"settings_cols": ["Key", "Label", "Type", "Range", "Default", "Read-only"],
"states_lbl": "_Measured quantities:_",
"states_cols": ["Key", "Quantity", "Type", "Unit"],
"yes": "yes",
"no": "no",
"cat_cols": ["Device", "Protocol", "Channel", "Origin", "Stability"],
"cat_empty": "_No device in this category._\n",
"matrix_cols": ["Brand / Model", "Protocol", "Channel", "Origin", "Stability"],
}
CATEGORY_LABELS = {
@ -56,6 +92,44 @@ CATEGORY_LABELS = {
"generic": "Types génériques",
}
# ═════════════════════════════════════════════════════════════════════════════
# PROSE PAR LANGUE — ce qui entoure le bloc de référence
#
# Le bloc de référence est en anglais une seule fois (REF ci-dessus) ; ce qui
# l'entoure — titre de catégorie, intertitre « Référence », mention de guide à
# venir — est de la PROSE et se traduit. Sans ça, chaque nouvel appareil ajouté
# réintroduirait du français dans /de/ et /en/ par le simple fait d'être créé.
# ═════════════════════════════════════════════════════════════════════════════
LOCALES = ["fr", "de", "en"] # fr = langue par défaut (fichier sans suffixe)
DEFAULT_LOCALE = "fr"
SCAFFOLD = {
"fr": {"ref_h": "## Référence", "guide_todo": "_Guide d'installation à venir._"},
"de": {"ref_h": "## Referenz", "guide_todo": "_Installationsanleitung folgt._"},
"en": {"ref_h": "## Reference", "guide_todo": "_Installation guide coming soon._"},
}
CATEGORY_LABELS_I18N = {
"fr": CATEGORY_LABELS,
"de": {
"compteur": "Zähler", "irve": "Wallboxen", "smartdevice": "SmartDevices",
"hvac": "HVAC", "onduleur": "Wechselrichter / PV", "batterie": "Speicher / ESS",
"tarif": "Tarife & Prognosen", "generic": "Generische Typen",
},
"en": {
"compteur": "Meters", "irve": "EV chargers", "smartdevice": "SmartDevices",
"hvac": "HVAC", "onduleur": "Inverters / PV", "batterie": "Batteries / ESS",
"tarif": "Tariffs & forecasts", "generic": "Generic types",
},
}
def loc_name(stem: str, locale: str) -> str:
"""Nom de fichier mkdocs-static-i18n en mode `suffix` : `x.md`, `x.de.md`, `x.en.md`."""
return f"{stem}.md" if locale == DEFAULT_LOCALE else f"{stem}.{locale}.md"
CATEGORY_FOLDER = {
"compteur": "compteurs",
"irve": "bornes",
@ -248,18 +322,18 @@ def render_params(tc: dict) -> list:
if disc:
rows = [[f"`{p['name']}`", p.get("displayName", ""), p.get("type", ""),
fmt_range(p), fmt_default(p)] for p in disc]
lines.append("_Paramètres de découverte :_")
lines.append(REF["discovery_lbl"])
lines.append("")
lines.append(md_table(["Clé", "Libellé", "Type", "Plage", "Défaut"], rows))
lines.append(md_table(REF["discovery_cols"], rows))
lines.append("")
settings = tc.get("paramTypes", [])
if settings:
rows = [[f"`{p['name']}`", p.get("displayName", ""), p.get("type", ""),
fmt_range(p), fmt_default(p),
"oui" if p.get("readOnly") else "non"] for p in settings]
lines.append("_Réglages :_")
REF["yes"] if p.get("readOnly") else REF["no"]] for p in settings]
lines.append(REF["settings_lbl"])
lines.append("")
lines.append(md_table(["Clé", "Libellé", "Type", "Plage", "Défaut", "Lecture seule"], rows))
lines.append(md_table(REF["settings_cols"], rows))
lines.append("")
return lines
@ -270,15 +344,15 @@ def render_states(tc: dict) -> list:
return []
rows = [[f"`{s['name']}`", s.get("displayName", ""), s.get("type", ""),
s.get("unit") or "—"] for s in states]
return [md_table(["Clé", "Grandeur", "Type", "Unité"], rows), ""]
return [md_table(REF["states_cols"], rows), ""]
def render_plugin(plugin: dict) -> str:
out = []
vendors = plugin.get("vendors", [])
vname = ", ".join(v.get("displayName", v.get("name", "")) for v in vendors)
out.append(f"**Fabricant :** {vname} ")
out.append(f"**Plugin :** `{plugin.get('name', '')}`")
out.append(f'{REF["manufacturer"]} {vname} ')
out.append(f'{REF["plugin"]} `{plugin.get("name", "")}`')
out.append("")
tcs = [(v, tc) for v in vendors for tc in v.get("thingClasses", [])]
@ -286,18 +360,18 @@ def render_plugin(plugin: dict) -> str:
roles(tc), transport_of(tc), add_method(tc),
str(len(tc.get("stateTypes", [])))]
for _, tc in tcs]
out.append("#### Modèles pris en charge")
out.append(md_table(["Modèle", "Rôle", "Transport", "Ajout", "Grandeurs"], rows))
out.append(REF["models_h"])
out.append(md_table(REF["models_cols"], rows))
out.append("")
out.append("#### Détail par modèle")
out.append(REF["detail_h"])
for _, tc in tcs:
title = tc.get("displayName", tc.get("name"))
out.append(f'??? abstract "{title} — `{tc.get("name")}`"')
body = []
body += render_params(tc)
if tc.get("stateTypes"):
body.append("_Grandeurs mesurées :_")
body.append(REF["states_lbl"])
body.append("")
body += render_states(tc)
for ln in ("\n".join(body)).splitlines():
@ -310,10 +384,10 @@ def render_nightly_stub(entry: dict, meta: dict) -> str:
"""Contenu généré pour une entrée nightly dont le JSON est absent."""
name = entry_name(entry, meta)
return (
f"_Support de **{name}** en cours d'intégration — canal nightly._\n"
f"_Support for **{name}** is being integrated — nightly channel._\n"
"\n"
"_La documentation technique sera disponible dès que le plugin sera "
"disponible sur le dépôt stable._\n"
"_Technical documentation will be available once the plugin reaches "
"the stable repository._\n"
)
@ -321,7 +395,7 @@ def render_category_index(cat: str, entries: list, plugins: dict, src: Path) ->
"""Tableau listant les appareils d'une catégorie, pour l'index.md auto-généré."""
cat_entries = [e for e in entries if e.get("category") == cat and e.get("plugin")]
if not cat_entries:
return "_Aucun appareil dans cette catégorie._\n"
return REF["cat_empty"]
rows = []
for e in cat_entries:
@ -338,7 +412,7 @@ def render_category_index(cat: str, entries: list, plugins: dict, src: Path) ->
rows.append([f"[{name}]({slug}.md)", protocol, channel_badge, origin_badge,
stability_badge])
return md_table(["Appareil", "Protocole", "Canal", "Origine", "Stabilité"], rows) + "\n"
return md_table(REF["cat_cols"], rows) + "\n"
def render_matrix(entries: list, plugins: dict, src: Path) -> str:
@ -372,7 +446,7 @@ def render_matrix(entries: list, plugins: dict, src: Path) -> str:
if not cat_rows:
continue
lines.append(f"## {label}\n")
lines.append("| Marque / Modèle | Protocole | Canal | Origine | Stabilité |")
lines.append("| " + " | ".join(REF["matrix_cols"]) + " |")
lines.append("|---|---|---|---|---|")
lines.extend(cat_rows)
lines.append("")
@ -451,8 +525,14 @@ def validate_entries(entries: list, plugins: dict, src: Path) -> None:
# ── Construction d'une page complète ─────────────────────────────────────────
def build_device_page(e: dict, plugin_data: dict | None, meta: dict) -> str:
"""Construit le contenu complet d'une fiche appareil générée."""
def build_device_page(e: dict, plugin_data: dict | None, meta: dict,
locale: str = DEFAULT_LOCALE) -> str:
"""Construit le contenu complet d'une fiche appareil générée.
`locale` ne change QUE la prose (SCAFFOLD) : le bloc de référence est
identique dans les trois langues, en anglais, par décision du 2026-10-12.
"""
sc = SCAFFOLD[locale]
title = entry_name(e, meta)
plugin = e["plugin"]
fname = f"integrationplugin{plugin}.json"
@ -475,15 +555,16 @@ def build_device_page(e: dict, plugin_data: dict | None, meta: dict) -> str:
header_parts = [f"# {title}", "", badges_block]
if tagline:
header_parts += ["", tagline]
header_parts += ["", "_Guide d'installation à venir._", "", "## Référence", ""]
header_parts += ["", sc["guide_todo"], "", sc["ref_h"], ""]
header = "\n".join(header_parts)
return header + "\n" + block + "\n"
def build_category_index_page(cat: str, entries: list, plugins: dict, src: Path) -> str:
def build_category_index_page(cat: str, entries: list, plugins: dict, src: Path,
locale: str = DEFAULT_LOCALE) -> str:
"""Construit le contenu complet d'un index.md de catégorie."""
label = CATEGORY_LABELS.get(cat, cat.capitalize())
label = CATEGORY_LABELS_I18N[locale].get(cat, cat.capitalize())
gen = render_category_index(cat, entries, plugins, src)
# Même format que process() : f"{m.group(1)}\n{gen}\n{m.group(4)}"
block = f"<!-- BEGIN GENERATED: __index_{cat}__ -->\n{gen}\n<!-- END GENERATED -->"
@ -493,56 +574,71 @@ def build_category_index_page(cat: str, entries: list, plugins: dict, src: Path)
# ── Création des fichiers manquants ───────────────────────────────────────────
def ensure_device_pages(entries: list, plugins: dict, src: Path,
docs: Path, check: bool) -> int:
"""Crée les fiches appareils inexistantes. Retourne 1 si manquant en mode --check."""
docs: Path, check: bool, locales: list | None = None) -> int:
"""Crée les fiches appareils inexistantes, dans CHAQUE langue.
Une fiche créée dans la seule langue par défaut réapparaîtrait en français
sous /de/ et /en/ par le repli `fallback_to_default` — d'où la boucle.
Une fiche déjà présente n'est jamais réécrite : la prose traduite à la main
survit aux régénérations.
"""
locales = locales or LOCALES
rc = 0
for e in entries:
if not e.get("plugin"):
continue
slug = e.get("slug") or e["plugin"]
folder = CATEGORY_FOLDER.get(e.get("category", ""), e.get("category", "autre"))
md_path = docs / "appareils" / folder / f"{slug}.md"
if md_path.exists():
continue
base = docs / "appareils" / folder
fname = f"integrationplugin{e['plugin']}.json"
plugin_data = plugins.get(fname)
meta = load_meta(src, e["plugin"])
content = build_device_page(e, plugin_data, meta)
meta = None
if check:
print(f" MANQUANT : {md_path.relative_to(docs.parent)}")
rc = 1
else:
md_path.parent.mkdir(parents=True, exist_ok=True)
md_path.write_text(content, encoding="utf-8")
print(f" CRÉÉ : {md_path.relative_to(docs.parent)}")
for locale in locales:
md_path = base / loc_name(slug, locale)
if md_path.exists():
continue
if meta is None:
meta = load_meta(src, e["plugin"])
content = build_device_page(e, plugin_data, meta, locale)
if check:
print(f" MANQUANT : {md_path.relative_to(docs.parent)}")
rc = 1
else:
md_path.parent.mkdir(parents=True, exist_ok=True)
md_path.write_text(content, encoding="utf-8")
print(f" CRÉÉ : {md_path.relative_to(docs.parent)}")
return rc
def ensure_category_indexes(entries: list, plugins: dict, src: Path,
docs: Path, check: bool) -> int:
"""Crée les index.md de catégorie inexistants. Retourne 1 si manquant en --check."""
docs: Path, check: bool,
locales: list | None = None) -> int:
"""Crée les index.md de catégorie inexistants, dans CHAQUE langue."""
locales = locales or LOCALES
cats_present = {e.get("category") for e in entries if e.get("category")}
rc = 0
for cat in cats_present:
for cat in sorted(cats_present):
folder = CATEGORY_FOLDER.get(cat, cat)
index_path = docs / "appareils" / folder / "index.md"
base = docs / "appareils" / folder
if index_path.exists():
continue
for locale in locales:
index_path = base / loc_name("index", locale)
if index_path.exists():
continue
content = build_category_index_page(cat, entries, plugins, src)
content = build_category_index_page(cat, entries, plugins, src, locale)
if check:
print(f" MANQUANT : {index_path.relative_to(docs.parent)}")
rc = 1
else:
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text(content, encoding="utf-8")
print(f" CRÉÉ : {index_path.relative_to(docs.parent)}")
if check:
print(f" MANQUANT : {index_path.relative_to(docs.parent)}")
rc = 1
else:
index_path.parent.mkdir(parents=True, exist_ok=True)
index_path.write_text(content, encoding="utf-8")
print(f" CRÉÉ : {index_path.relative_to(docs.parent)}")
return rc
@ -613,16 +709,37 @@ def process(docs: Path, entries: list, plugins: dict, src: Path, check: bool) ->
# ── Point d'entrée ────────────────────────────────────────────────────────────
def main() -> int:
global DEFAULT_LOCALE # `--lang` peut la redéfinir (voir plus bas)
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--src", default=".plugins-src", type=Path,
help="Dossier source des JSON (plat ou avec sous-dossiers repos)")
ap.add_argument("--docs", default="docs", type=Path)
ap.add_argument("--lang", default="fr", help="Conservé pour compatibilité CI")
# `--lang` existait depuis la CI mais n'était PAS lu (« Conservé pour
# compatibilité CI »). Il est devenu réel au passage multi-langue : il
# désigne la langue par défaut, celle dont les fichiers n'ont pas de suffixe.
ap.add_argument("--lang", default=DEFAULT_LOCALE, choices=LOCALES,
help=f"Langue par défaut, fichiers sans suffixe (défaut : {DEFAULT_LOCALE})")
ap.add_argument("--locales", default=",".join(LOCALES),
help=f"Langues à générer, séparées par des virgules (défaut : {','.join(LOCALES)})")
ap.add_argument("--check", action="store_true",
help="CI : exit 1 si la doc n'est pas à jour")
a = ap.parse_args()
src = a.src
DEFAULT_LOCALE = a.lang
locales = [x.strip() for x in a.locales.split(",") if x.strip()]
unknown = [x for x in locales if x not in SCAFFOLD]
if unknown:
print(f"ERREUR : langue(s) sans échafaudage SCAFFOLD : {', '.join(unknown)}",
file=sys.stderr)
return 2
if DEFAULT_LOCALE not in locales:
print(f"ERREUR : --lang {DEFAULT_LOCALE} absent de --locales {','.join(locales)}",
file=sys.stderr)
return 2
print(f"langues : {', '.join(locales)} · défaut (sans suffixe) : {DEFAULT_LOCALE}")
entries = load_porting_status(Path.cwd())
plugins = load_plugins(src)
@ -633,8 +750,8 @@ def main() -> int:
validate_entries(entries, plugins, src)
rc_pages = ensure_device_pages(entries, plugins, src, a.docs, a.check)
rc_indexes = ensure_category_indexes(entries, plugins, src, a.docs, a.check)
rc_pages = ensure_device_pages(entries, plugins, src, a.docs, a.check, locales)
rc_indexes = ensure_category_indexes(entries, plugins, src, a.docs, a.check, locales)
rc_summary = generate_summary(entries, a.docs, src, plugins, a.check)
rc_process = process(a.docs, entries, plugins, src, a.check)