diff --git a/pyproject.toml b/pyproject.toml index 0c6dcb3..cec44e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-core" -version = "0.1.37" +version = "0.1.38" description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." readme = "README.md" requires-python = ">=3.12" diff --git a/src/govoplan_core/core/modules.py b/src/govoplan_core/core/modules.py index 21a30c4..5583ee4 100644 --- a/src/govoplan_core/core/modules.py +++ b/src/govoplan_core/core/modules.py @@ -1,7 +1,7 @@ from __future__ import annotations from collections.abc import Callable, Iterable, Mapping, Sequence -from dataclasses import dataclass, field +from dataclasses import dataclass, field, replace from typing import Any, Literal, Protocol, TYPE_CHECKING from govoplan_core.core.information_governance import ModuleInformationGovernance @@ -687,3 +687,53 @@ class ModuleManifest: # runtime module ID changes. permission_namespace: str | None = None workflow_definitions: tuple["WorkflowDefinitionContribution", ...] = () + + +def with_documentation_structured_translations( + manifest: ModuleManifest, + *, + locale: str, + translations: Mapping[str, Mapping[str, Any]], +) -> ModuleManifest: + """Merge module-owned structured documentation translations by topic id. + + The helper keeps feature prose in its owning module while giving every + manifest the same fail-closed merge behavior. Unknown topic ids and + incomplete or shape-changing locale maps are rejected immediately. + """ + + locale = locale.strip() + if not locale: + raise ValueError("structured documentation locale must not be empty") + + topics_by_id = {topic.id: topic for topic in manifest.documentation} + unknown_topic_ids = sorted(set(translations) - set(topics_by_id)) + if unknown_topic_ids: + raise ValueError( + "structured documentation translations reference unknown topic ids: " + + ", ".join(unknown_topic_ids) + ) + + localized_topics: list[DocumentationTopic] = [] + for topic in manifest.documentation: + translation = translations.get(topic.id) + if translation is None: + localized_topics.append(topic) + continue + + structured_translations = dict(topic.structured_translations) + structured_translations[locale] = translation + localized_topic = replace( + topic, + structured_translation_version=DOCUMENTATION_STRUCTURED_TRANSLATION_VERSION, + structured_translations=structured_translations, + ) + issues = documentation_structured_translation_issues(localized_topic) + if issues: + raise ValueError( + f"invalid {locale!r} structured documentation translation for " + f"{topic.id!r}: {'; '.join(issues)}" + ) + localized_topics.append(localized_topic) + + return replace(manifest, documentation=tuple(localized_topics)) diff --git a/tests/test_documentation_topic_contract.py b/tests/test_documentation_topic_contract.py index d7675fa..04b1219 100644 --- a/tests/test_documentation_topic_contract.py +++ b/tests/test_documentation_topic_contract.py @@ -12,6 +12,7 @@ from govoplan_core.core.modules import ( ModuleManifest, localized_documentation_metadata, user_workflow_scope_condition_issues, + with_documentation_structured_translations, ) from govoplan_core.core.registry import PlatformRegistry, RegistryError @@ -136,6 +137,45 @@ class DocumentationTopicContractTests(unittest.TestCase): with self.assertRaisesRegex(RegistryError, "preserve list length"): registry_for(incomplete_shape).validate() + def test_manifest_helper_merges_and_validates_owner_translations(self) -> None: + topic = DocumentationTopic( + id="example.workflow.localized", + title="Run task", + summary="Run the task.", + metadata={"steps": ["Review", "Execute"]}, + ) + manifest = ModuleManifest( + id="example", + name="Example", + version="1.0.0", + documentation=(topic,), + ) + + localized = with_documentation_structured_translations( + manifest, + locale="de", + translations={ + topic.id: {"steps": ["Prüfen", "Ausführen"]}, + }, + ) + + self.assertEqual( + ["Prüfen", "Ausführen"], + localized.documentation[0].structured_translations["de"]["steps"], + ) + with self.assertRaisesRegex(ValueError, "unknown topic ids"): + with_documentation_structured_translations( + manifest, + locale="de", + translations={"missing.topic": {"steps": ["Prüfen", "Ausführen"]}}, + ) + with self.assertRaisesRegex(ValueError, "preserve list length"): + with_documentation_structured_translations( + manifest, + locale="de", + translations={topic.id: {"steps": ["Prüfen"]}}, + ) + def test_documentation_configuration_and_source_extensions_are_validated(self) -> None: resolver = lambda _context, keys: { # noqa: E731 key: DocumentationConfigurationDecision(key=key, state="enabled") diff --git a/webui/package-lock.json b/webui/package-lock.json index ff4b706..1dd884f 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "dependencies": { "@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", diff --git a/webui/package-lock.release.json b/webui/package-lock.release.json index 09584a8..0823f77 100644 --- a/webui/package-lock.release.json +++ b/webui/package-lock.release.json @@ -1,12 +1,12 @@ { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "dependencies": { "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.20", "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.19", diff --git a/webui/package.json b/webui/package.json index f4f7b3d..c4f5372 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/package.release.json b/webui/package.release.json index 76772d6..ad47c9f 100644 --- a/webui/package.release.json +++ b/webui/package.release.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.37", + "version": "0.1.38", "private": true, "type": "module", "main": "src/index.ts",