From 92dc91885b0d9ac170296e22694150fce360a46f Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Sat, 22 Aug 2026 20:25:37 +0200 Subject: [PATCH] feat(docs): localize structured metadata and gate coverage --- README.md | 12 +- package.json | 2 +- pyproject.toml | 4 +- src/govoplan_docs/__init__.py | 2 +- src/govoplan_docs/backend/api/v1/routes.py | 24 ++- src/govoplan_docs/backend/manifest.py | 36 +++- src/govoplan_docs/public_export.py | 185 ++++++++++++++++++++- tests/test_docs_context.py | 44 +++++ tests/test_public_export.py | 80 ++++++++- webui/package.json | 2 +- webui/src/api/docs.ts | 2 + 11 files changed, 372 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index b48b95c..f7414d6 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ module, or every module checkout in a workspace, with: govoplan-docs-export-public \ --workspace-root /mnt/DATA/git \ --output public/docs/v1/catalog.json \ - --coverage-output docs/DOCUMENTATION_COVERAGE.md + --coverage-output docs/DOCUMENTATION_COVERAGE.md \ + --coverage-baseline docs/DOCUMENTATION_COVERAGE_BASELINE.json ``` Use `--check` in publication CI to reject a stale checked-in catalog. Dynamic @@ -90,6 +91,15 @@ Use `--check` in publication CI to reject a stale checked-in catalog. Dynamic on permissions, policy, configuration, and live provider state; the export records which modules have such additional documentation. +Static topics localize title, summary, and body through `translations`. +Rendered metadata such as steps, fields, limitations, consequences, and +verification uses Core's opt-in `structured_translation_version="1"` plus +`structured_translations` contract. The registry validates exact shape before +Docs overlays the requested locale. The public catalog reports structured +adoption separately. A reviewed coverage-baseline file sets monotonic minima +and maxima so publication CI also rejects localization or coverage regressions +after generated output is refreshed. + Pressing `F1` resolves the focused field or action first, then its containing dialog or section, current page, and owning module. The shell sends the focused context together with `fallback_context` and `module`; Docs selects the first diff --git a/package.json b/package.json index f741975..933d7cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/docs-webui", - "version": "0.1.20", + "version": "0.1.21", "private": true, "type": "module", "main": "webui/src/index.ts", diff --git a/pyproject.toml b/pyproject.toml index 4245528..dafa9e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-docs" -version = "0.1.20" +version = "0.1.21" description = "GovOPlaN documentation module for configured-system, available, and evidence documentation." readme = "README.md" requires-python = ">=3.12" authors = [{ name = "GovOPlaN" }] dependencies = [ - "govoplan-core>=0.1.18", + "govoplan-core>=0.1.37", "govoplan-access>=0.1.18", ] diff --git a/src/govoplan_docs/__init__.py b/src/govoplan_docs/__init__.py index 042193b..74ca00b 100644 --- a/src/govoplan_docs/__init__.py +++ b/src/govoplan_docs/__init__.py @@ -2,4 +2,4 @@ __all__ = ["__version__"] -__version__ = "0.1.18" +__version__ = "0.1.21" diff --git a/src/govoplan_docs/backend/api/v1/routes.py b/src/govoplan_docs/backend/api/v1/routes.py index 52e3914..7002d49 100644 --- a/src/govoplan_docs/backend/api/v1/routes.py +++ b/src/govoplan_docs/backend/api/v1/routes.py @@ -17,6 +17,7 @@ from govoplan_core.core.modules import ( ModuleManifest, NavItem, PermissionDefinition, + localized_documentation_metadata, user_workflow_scope_condition_issues, ) from govoplan_core.core.registry import PlatformRegistry @@ -1432,6 +1433,10 @@ def _documentation_topic_payload( ) -> dict[str, Any]: module_id = topic.source_module_id or source_module_id translation_locale, translation = _translation_for_locale(topic, locale) + structured_translation_locale = _structured_translation_locale(topic, locale) + localized_metadata = localized_documentation_metadata( + topic, structured_translation_locale + ) kind = _documentation_topic_kind(topic) payload = { "id": topic.id, @@ -1459,6 +1464,8 @@ def _documentation_topic_payload( "i18n_key": topic.i18n_key or topic.id, "locale": locale, "translation_locale": translation_locale, + "structured_translation_locale": structured_translation_locale, + "structured_translation_version": topic.structured_translation_version, "version": { "resolved": resolved_version, "minimum": topic.version_min, @@ -1483,7 +1490,7 @@ def _documentation_topic_payload( _documentation_configuration_payload(configuration[key]) for key in sorted(configuration) ], - "metadata": dict(topic.metadata), + "metadata": localized_metadata, } if documentation_type == "admin": return payload @@ -1511,6 +1518,12 @@ def _documentation_topic_payload( "i18n_key": "", "locale": locale, "translation_locale": payload["translation_locale"], + "structured_translation_locale": payload[ + "structured_translation_locale" + ], + "structured_translation_version": payload[ + "structured_translation_version" + ], "conditions": [], "links": [ _documentation_link_payload(link) @@ -1521,7 +1534,7 @@ def _documentation_topic_payload( "unlocks": list(topic.unlocks), "configuration_keys": [], "configuration_states": [], - "metadata": _user_topic_metadata(kind, topic.metadata), + "metadata": _user_topic_metadata(kind, localized_metadata), } @@ -1662,6 +1675,13 @@ def _translation_for_locale(topic: DocumentationTopic, locale: str) -> tuple[str return "source", {} +def _structured_translation_locale(topic: DocumentationTopic, locale: str) -> str: + for candidate in _locale_candidates(locale): + if candidate in topic.structured_translations: + return candidate + return "source" + + def _locale_candidates(locale: str) -> tuple[str, ...]: normalized = _normalize_locale(locale) base = normalized.split("-", 1)[0] diff --git a/src/govoplan_docs/backend/manifest.py b/src/govoplan_docs/backend/manifest.py index de0b925..168d501 100644 --- a/src/govoplan_docs/backend/manifest.py +++ b/src/govoplan_docs/backend/manifest.py @@ -131,7 +131,7 @@ def _dsar_provider(_context: ModuleContext) -> DocsDsarProvider: manifest = ModuleManifest( id="docs", name="Docs", - version="0.1.20", + version="0.1.21", required_capabilities=( CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR, @@ -458,7 +458,8 @@ manifest = ModuleManifest( "The public exporter reads DocumentationTopic contributions from all installed packages or sibling module checkouts, " "projects German and English content, and records documentation coverage. Runtime providers remain in the authenticated " "Docs surface because their output can depend on the current actor, policy, configuration, and live service state. " - "Publication CI should run the export check and reject a stale source digest." + "Rendered steps, fields, limitations, consequences, and verification use Core's versioned same-shape structured-translation contract. " + "Publication CI runs both the source-digest check and a reviewed monotonic coverage baseline, so regenerating the catalog cannot conceal a regression." ), layer="always", documentation_types=("admin", "user"), @@ -472,7 +473,8 @@ manifest = ModuleManifest( "Der öffentliche Export liest die DocumentationTopic-Beiträge aus allen installierten Paketen oder benachbarten Modulquellen, " "projiziert deutsche und englische Inhalte und weist Dokumentationslücken aus. Laufzeit-Provider verbleiben in der authentifizierten " "Dokumentationsoberfläche, da ihre Ausgabe von Rolle, Richtlinie, Konfiguration und Dienstzustand abhängen kann. " - "Die Veröffentlichungs-CI soll den Quelldigest prüfen und veraltete Exporte ablehnen." + "Gerenderte Schritte, Felder, Einschränkungen, Folgen und Prüfhinweise verwenden den versionierten, formgleichen Vertrag für strukturierte Übersetzungen in Core. " + "Die Veröffentlichungs-CI prüft sowohl den Quelldigest als auch einen freigegebenen monotonen Abdeckungsstand, damit das Neuerzeugen des Katalogs keine Verschlechterung verdecken kann." ), } }, @@ -573,6 +575,23 @@ manifest = ModuleManifest( "access.workflow.grant-user-access", ], }, + structured_translation_version="1", + structured_translations={ + "de": { + "purpose": ( + "Beschriftungen bleiben schnell erfassbar, während kurze Erläuterungen bei Bedarf verfügbar sind." + ), + "when_used": ( + "Formular- und Umschalterbeschriftungen, Zeilen mit wirksamen Werten und kompakte Verwaltungsbegriffe." + ), + "user_explanation": ( + "Öffnen Sie die Markierung, wenn eine Beschriftung unklar ist. Sie erläutert die lokale Auswahl in ein oder zwei Sätzen." + ), + "admin_explanation": ( + "Feldhilfe bleibt am Feld. Längere Verfahrens-, API- oder Richtlinienerläuterungen gehören in verknüpfte Ablauf- oder Referenzthemen." + ), + } + }, ), DocumentationTopic( id="docs.pattern.contextual-help", @@ -725,6 +744,17 @@ manifest = ModuleManifest( }, ], }, + structured_translation_version="1", + structured_translations={ + "de": { + "admin_explanation": ( + "Auswirkungen von Funktionen auf Rollen gehören Access. Änderungen an IDM-Zuordnungen können unabhängig von Änderungen am Organisationsmodell gesteuert werden." + ), + "user_explanation": ( + "Eine Person kann eine Funktion innehaben, weil IDM ihre Identität mit der Organisationsfunktion verknüpft. Access entscheidet, welche Anwendungsberechtigungen diese Funktion gewährt." + ), + } + }, ), ), documentation_sources=( diff --git a/src/govoplan_docs/public_export.py b/src/govoplan_docs/public_export.py index 330a794..aac0b3d 100644 --- a/src/govoplan_docs/public_export.py +++ b/src/govoplan_docs/public_export.py @@ -15,10 +15,12 @@ from typing import Any from govoplan_core.core.discovery import discover_module_manifests from govoplan_core.core.modules import ( + DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION, DocumentationCondition, DocumentationLink, DocumentationTopic, ModuleManifest, + localizable_documentation_metadata_keys, ) @@ -102,12 +104,12 @@ def documentation_coverage_markdown(catalog: Mapping[str, Any]) -> str: "2. Give every user-facing module at least one scope-conditioned workflow topic and one field/consequence reference.", "3. Give every configurable module an administrator topic covering permissions, policy provenance, retention, and operational consequences.", "4. Keep live provider-state and instance-specific limitations in `documentation_providers`; do not publish them as generic facts.", - "5. Add a versioned localization contract for structured metadata such as steps, fields, limitations, and verification; these values currently retain their manifest source language.", + "5. Adopt the versioned structured-localization contract for steps, fields, limitations, consequences, and verification; the coverage column tracks this migration independently from title/body completeness.", "", "## Module gaps", "", - "| Module | Topics | Missing German | Missing coverage |", - "| --- | ---: | ---: | --- |", + "| Module | Topics | Missing German | Structured German | Missing coverage |", + "| --- | ---: | ---: | ---: | --- |", ] gaps_by_id = {str(item.get("module_id")): item for item in gaps} for module in modules: @@ -117,7 +119,9 @@ def documentation_coverage_markdown(catalog: Mapping[str, Any]) -> str: missing = ", ".join(str(item) for item in gap.get("missing", ())) or "-" lines.append( f"| `{module_id}` | {coverage.get('topic_count', 0)} | " - f"{coverage.get('missing_german_topic_count', 0)} | {missing} |" + f"{coverage.get('missing_german_topic_count', 0)} | " + f"{coverage.get('german_structured_complete_topic_count', 0)}/" + f"{coverage.get('structured_localizable_topic_count', 0)} | {missing} |" ) lines.extend(("", "Generated file. Edit module manifests, then regenerate this report.", "")) return "\n".join(lines) @@ -238,6 +242,7 @@ def _topic_payload(module_id: str, topic: DocumentationTopic) -> dict[str, Any]: if key != "kind" } ), + **_structured_content_payload(topic), } @@ -258,6 +263,36 @@ def _localized_topic(topic: DocumentationTopic, locale: str) -> dict[str, Any]: } +def _localized_content(topic: DocumentationTopic, locale: str) -> dict[str, Any]: + localizable_keys = localizable_documentation_metadata_keys(topic) + translation = topic.structured_translations.get(locale, {}) + translated_fields = sorted(set(localizable_keys).intersection(translation)) + complete = locale == "en" or not localizable_keys or ( + topic.structured_translation_version + == DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION + and len(translated_fields) == len(localizable_keys) + ) + return { + "content": _public_value(dict(translation)), + "source_locale": locale if translated_fields else "en", + "translated_fields": translated_fields, + "complete": complete, + } + + +def _structured_content_payload(topic: DocumentationTopic) -> dict[str, Any]: + if not localizable_documentation_metadata_keys(topic): + return {} + return { + "structured_translation_version": topic.structured_translation_version, + "content_localizations": { + locale: _localized_content(topic, locale) + for locale in SUPPORTED_LOCALES + if locale != "en" + }, + } + + def _module_coverage( manifest: ModuleManifest, topics: Sequence[Mapping[str, Any]], @@ -270,6 +305,21 @@ def _module_coverage( for topic in topics if not _mapping(_mapping(topic.get("localizations")).get("de")).get("complete") ] + structured_localizable = [ + topic + for topic in manifest.documentation + if localizable_documentation_metadata_keys(topic) + ] + german_structured_complete = [ + topic + for topic in structured_localizable + if _localized_content(topic, "de")["complete"] + ] + missing_german_structured = [ + topic.id + for topic in structured_localizable + if not _localized_content(topic, "de")["complete"] + ] missing: list[str] = [] if not has_user: missing.append("user documentation") @@ -291,6 +341,15 @@ def _module_coverage( ), "missing_german_topic_count": len(missing_german), "missing_german_topic_ids": missing_german, + "structured_localizable_topic_count": len(structured_localizable), + "structured_translation_contract_topic_count": sum( + topic.structured_translation_version + == DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION + for topic in structured_localizable + ), + "german_structured_complete_topic_count": len(german_structured_complete), + "missing_german_structured_topic_count": len(missing_german_structured), + "missing_german_structured_topic_ids": missing_german_structured, "kinds": sorted(kinds), "missing": missing, } @@ -314,6 +373,30 @@ def _coverage_summary(modules: Sequence[Mapping[str, Any]]) -> dict[str, Any]: "module_count": len(modules), "topic_count": topic_count, "german_complete_topic_count": topic_count - missing_german, + "structured_localizable_topic_count": sum( + int( + _mapping(module.get("coverage")).get( + "structured_localizable_topic_count", 0 + ) + ) + for module in modules + ), + "structured_translation_contract_topic_count": sum( + int( + _mapping(module.get("coverage")).get( + "structured_translation_contract_topic_count", 0 + ) + ) + for module in modules + ), + "german_structured_complete_topic_count": sum( + int( + _mapping(module.get("coverage")).get( + "german_structured_complete_topic_count", 0 + ) + ) + for module in modules + ), "runtime_provider_module_count": sum( int(module.get("runtime_documentation_provider_count", 0)) > 0 for module in modules @@ -323,6 +406,79 @@ def _coverage_summary(modules: Sequence[Mapping[str, Any]]) -> dict[str, Any]: } +def coverage_baseline(catalog: Mapping[str, Any]) -> dict[str, Any]: + """Build the monotonic public-documentation coverage baseline.""" + + summary = _mapping(catalog.get("summary")) + topic_count = int(summary.get("topic_count", 0)) + german_complete = int(summary.get("german_complete_topic_count", 0)) + structured_localizable = int( + summary.get("structured_localizable_topic_count", 0) + ) + structured_complete = int( + summary.get("german_structured_complete_topic_count", 0) + ) + return { + "schema_version": "1", + "minimum": { + "german_complete_topic_count": german_complete, + "structured_translation_contract_topic_count": int( + summary.get("structured_translation_contract_topic_count", 0) + ), + "german_structured_complete_topic_count": structured_complete, + }, + "maximum": { + "missing_german_topic_count": topic_count - german_complete, + "gap_module_count": int(summary.get("gap_module_count", 0)), + "missing_german_structured_topic_count": ( + structured_localizable - structured_complete + ), + }, + } + + +def coverage_regression_issues( + catalog: Mapping[str, Any], baseline: Mapping[str, Any] +) -> tuple[str, ...]: + """Return monotonic coverage failures against a reviewed baseline.""" + + if str(baseline.get("schema_version")) != "1": + return ("unsupported coverage baseline schema_version",) + summary = _mapping(catalog.get("summary")) + minimum = _mapping(baseline.get("minimum")) + maximum = _mapping(baseline.get("maximum")) + actual = { + "german_complete_topic_count": int( + summary.get("german_complete_topic_count", 0) + ), + "structured_translation_contract_topic_count": int( + summary.get("structured_translation_contract_topic_count", 0) + ), + "german_structured_complete_topic_count": int( + summary.get("german_structured_complete_topic_count", 0) + ), + "missing_german_topic_count": int(summary.get("topic_count", 0)) + - int(summary.get("german_complete_topic_count", 0)), + "gap_module_count": int(summary.get("gap_module_count", 0)), + "missing_german_structured_topic_count": int( + summary.get("structured_localizable_topic_count", 0) + ) + - int(summary.get("german_structured_complete_topic_count", 0)), + } + issues: list[str] = [] + for key, expected in minimum.items(): + if key in actual and actual[key] < int(expected): + issues.append( + f"coverage {key} regressed: {actual[key]} is below {int(expected)}" + ) + for key, expected in maximum.items(): + if key in actual and actual[key] > int(expected): + issues.append( + f"coverage {key} regressed: {actual[key]} exceeds {int(expected)}" + ) + return tuple(issues) + + def _topic_kind(topic: DocumentationTopic) -> str: raw = topic.metadata.get("kind") if isinstance(raw, str): @@ -394,6 +550,7 @@ def _parser() -> argparse.ArgumentParser: parser.add_argument("--output", type=Path, required=True) parser.add_argument("--coverage-output", type=Path) parser.add_argument("--workspace-root", type=Path) + parser.add_argument("--coverage-baseline", type=Path) parser.add_argument( "--check", action="store_true", @@ -408,10 +565,22 @@ def main(argv: Iterable[str] | None = None) -> int: collect_manifest_sources(workspace_root=args.workspace_root) ) if args.check: - if catalog_matches_sources(args.output, catalog): - return 0 - print(f"Public documentation catalog is stale: {args.output}", file=sys.stderr) - return 1 + failed = False + if not catalog_matches_sources(args.output, catalog): + print(f"Public documentation catalog is stale: {args.output}", file=sys.stderr) + failed = True + if args.coverage_baseline is not None: + try: + baseline = json.loads( + args.coverage_baseline.read_text(encoding="utf-8") + ) + except (OSError, json.JSONDecodeError) as exc: + print(f"Invalid coverage baseline: {exc}", file=sys.stderr) + return 1 + for issue in coverage_regression_issues(catalog, _mapping(baseline)): + print(f"Public documentation {issue}", file=sys.stderr) + failed = True + return int(failed) write_public_catalog( args.output, catalog, diff --git a/tests/test_docs_context.py b/tests/test_docs_context.py index 696b65d..96b2e56 100644 --- a/tests/test_docs_context.py +++ b/tests/test_docs_context.py @@ -52,6 +52,50 @@ class FakePrincipal: class DocsContextTests(unittest.TestCase): + def test_structured_topic_metadata_uses_requested_locale(self) -> None: + registry = PlatformRegistry() + registry.register( + ModuleManifest( + id="example", + name="Example", + version="1.0.0", + documentation=( + DocumentationTopic( + id="example.reference.localized", + title="Reference", + summary="Reference summary.", + documentation_types=("admin",), + metadata={ + "kind": "reference", + "limitations": ["English limitation."], + }, + structured_translation_version="1", + structured_translations={ + "de": { + "limitations": ["Deutsche Einschränkung."] + } + }, + ), + ), + ) + ) + + layers = _classify_documentation( + registry, + FakePrincipal({"docs:documentation:read"}), + settings=None, + session=None, + documentation_type="admin", + locale="de-DE", + ) + + topic = layers["configured"][0] + self.assertEqual("de", topic["structured_translation_locale"]) + self.assertEqual("1", topic["structured_translation_version"]) + self.assertEqual( + ["Deutsche Einschränkung."], topic["metadata"]["limitations"] + ) + def test_user_provider_state_omits_binding_details(self) -> None: state = { "configured": True, diff --git a/tests/test_public_export.py b/tests/test_public_export.py index d13fbb3..6281486 100644 --- a/tests/test_public_export.py +++ b/tests/test_public_export.py @@ -11,6 +11,8 @@ from govoplan_docs.public_export import ( ManifestSource, build_public_catalog, catalog_matches_sources, + coverage_baseline, + coverage_regression_issues, documentation_coverage_markdown, write_public_catalog, ) @@ -25,6 +27,13 @@ class PublicDocumentationExportTests(unittest.TestCase): self.assertEqual(9, module["coverage"]["topic_count"]) self.assertEqual(0, module["coverage"]["missing_german_topic_count"]) self.assertEqual([], module["coverage"]["missing_german_topic_ids"]) + self.assertEqual(2, module["coverage"]["structured_localizable_topic_count"]) + self.assertEqual( + 2, module["coverage"]["german_structured_complete_topic_count"] + ) + self.assertEqual( + [], module["coverage"]["missing_german_structured_topic_ids"] + ) self.assertEqual([], module["coverage"]["missing"]) def test_catalog_projects_localized_manifest_topics_and_gaps(self) -> None: @@ -39,7 +48,11 @@ class PublicDocumentationExportTests(unittest.TestCase): summary="Perform the example workflow.", body="Open and finish it.", documentation_types=("user", "admin"), - metadata={"kind": "workflow"}, + metadata={ + "kind": "workflow", + "steps": ["Review", "Execute"], + "verification": "Confirm completion.", + }, translations={ "de": { "title": "Beispiel verwenden", @@ -47,6 +60,13 @@ class PublicDocumentationExportTests(unittest.TestCase): "body": "Öffnen und abschließen.", } }, + structured_translation_version="1", + structured_translations={ + "de": { + "steps": ["Prüfen", "Ausführen"], + "verification": "Den Abschluss bestätigen.", + } + }, ), ), ) @@ -59,7 +79,12 @@ class PublicDocumentationExportTests(unittest.TestCase): self.assertEqual("Beispiel verwenden", topic["localizations"]["de"]["title"]) self.assertTrue(topic["localizations"]["de"]["complete"]) self.assertEqual("workflow", topic["kind"]) - self.assertEqual({}, topic["content"]) + self.assertEqual(["Review", "Execute"], topic["content"]["steps"]) + self.assertEqual( + ["Prüfen", "Ausführen"], + topic["content_localizations"]["de"]["content"]["steps"], + ) + self.assertTrue(topic["content_localizations"]["de"]["complete"]) self.assertIn( "field/consequence reference", catalog["modules"][0]["coverage"]["missing"], @@ -159,6 +184,57 @@ class PublicDocumentationExportTests(unittest.TestCase): self.assertEqual("operator-workflow", topic["kind"]) + def test_coverage_baseline_rejects_text_and_structured_regressions(self) -> None: + healthy = build_public_catalog( + ( + ManifestSource( + ModuleManifest( + id="example", + name="Example", + version="1.0.0", + documentation=( + DocumentationTopic( + id="example.reference", + title="Reference", + summary="Reference summary.", + body="Reference body.", + translations={ + "de": { + "title": "Referenz", + "summary": "Referenzzusammenfassung.", + "body": "Referenzinhalt.", + } + }, + metadata={ + "kind": "reference", + "limitations": ["Source limitation."], + }, + structured_translation_version="1", + structured_translations={ + "de": { + "limitations": [ + "Einschränkung der Quelle." + ] + } + }, + ), + ), + ) + ), + ) + ) + baseline = coverage_baseline(healthy) + self.assertEqual((), coverage_regression_issues(healthy, baseline)) + + regressed = json.loads(json.dumps(healthy)) + regressed["summary"]["german_complete_topic_count"] = 0 + regressed["summary"]["german_structured_complete_topic_count"] = 0 + issues = coverage_regression_issues(regressed, baseline) + self.assertTrue(any("german_complete_topic_count" in issue for issue in issues)) + self.assertTrue( + any("german_structured_complete_topic_count" in issue for issue in issues) + ) + if __name__ == "__main__": unittest.main() diff --git a/webui/package.json b/webui/package.json index 109b39d..251acc3 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/docs-webui", - "version": "0.1.20", + "version": "0.1.21", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/api/docs.ts b/webui/src/api/docs.ts index 5edc743..d1fd893 100644 --- a/webui/src/api/docs.ts +++ b/webui/src/api/docs.ts @@ -166,6 +166,8 @@ export type DocsDocumentationTopic = { i18n_key: string; locale: string; translation_locale: string; + structured_translation_locale: string; + structured_translation_version?: string | null; version: { resolved: string; minimum?: string | null;