fix(i18n): complete Portal German headings and contextual help
Verified with the coordinated workspace changes by devkit full run 2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed). This shared UI pass does not mark the individual module reviews complete.
This commit is contained in:
@@ -47,6 +47,20 @@ logs. This reviewed boundary avoids duplicate or contradictory privacy exports.
|
||||
|
||||
See [docs/SERVICE_DIRECTORY_CONCEPT.md](docs/SERVICE_DIRECTORY_CONCEPT.md).
|
||||
|
||||
## Heading localization
|
||||
|
||||
Portal owns the English/German catalog entries for the function-postbox heading,
|
||||
authenticated-access heading, short-lived status-link request heading, and
|
||||
timeline heading. The native postbox heading translates during React rendering;
|
||||
status cards use Core's shared title translation contract. Provider-owned names
|
||||
and status access rules are not changed by this localization pass.
|
||||
|
||||
From the Meta checkout, run `./devkit docs audit --repo portal` and
|
||||
`./devkit check --profile ui --repo portal`. The latter discovers Portal's
|
||||
`test:translations` script, which verifies module registration, both languages,
|
||||
shared card rendering, and first-render native heading translation. These checks
|
||||
do not claim that every Portal sentence or dynamic value is localized.
|
||||
|
||||
## Git-source WebUI package
|
||||
|
||||
The repository root exposes `@govoplan/portal-webui` for Git-tagged release
|
||||
|
||||
@@ -315,7 +315,8 @@ manifest = ModuleManifest(
|
||||
body=(
|
||||
"Portal presents the applicant status projection owned by Forms Runtime. The service administrator chooses authenticated-only access, a short-lived email link, or a permanent public bearer link for each exact published Form revision. "
|
||||
"Authenticated access is checked against the applicant account. Email-link requests return the same response for matching and non-matching details, revoke the previous link when a new one is sent, and depend on configured Notifications and Mail delivery. A permanent link does not expire or require sign-in and must therefore be handled like a bearer secret. "
|
||||
"The page exposes only lifecycle states, update times, a tracking identifier, and the submission receipt. It does not display Form values, evidence, internal notes, actors, decision reasoning, or module handoff details."
|
||||
"The page exposes only lifecycle states, update times, a tracking identifier, and the submission receipt. It does not display Form values, evidence, internal notes, actors, decision reasoning, or module handoff details. "
|
||||
"The sign-in, short-lived status-link request, and timeline headings follow the selected interface language; changing language does not change the configured access policy."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
@@ -327,7 +328,8 @@ manifest = ModuleManifest(
|
||||
"body": (
|
||||
"Das Portal zeigt die von Forms Runtime verwaltete Antragsstatusprojektion. Für jede genaue veröffentlichte Formularrevision wählt die Dienstadministration zwischen ausschließlich authentifiziertem Zugriff, einem kurzlebigen E-Mail-Link und einem dauerhaften öffentlichen Inhaberlink. "
|
||||
"Beim authentifizierten Zugriff wird das Antragstellerkonto geprüft. Anforderungen eines E-Mail-Links liefern für passende und unpassende Angaben dieselbe Antwort, widerrufen beim erneuten Versand den vorherigen Link und setzen konfigurierte Notifications- und Mail-Zustellung voraus. Ein dauerhafter Link läuft nicht ab und erfordert keine Anmeldung; er ist deshalb wie ein Inhabergeheimnis zu behandeln. "
|
||||
"Die Seite zeigt ausschließlich Lebenszyklusstatus, Aktualisierungszeiten, eine Vorgangskennung und die Einreichungsbestätigung. Formularwerte, Nachweise, interne Notizen, handelnde Personen, Entscheidungsbegründungen und Details von Modulübergaben bleiben verborgen."
|
||||
"Die Seite zeigt ausschließlich Lebenszyklusstatus, Aktualisierungszeiten, eine Vorgangskennung und die Einreichungsbestätigung. Formularwerte, Nachweise, interne Notizen, handelnde Personen, Entscheidungsbegründungen und Details von Modulübergaben bleiben verborgen. "
|
||||
"Die Überschriften für die Anmeldung, die Anforderung eines kurzlebigen Statuslinks und den Verlauf folgen der gewählten Oberflächensprache; ein Sprachwechsel ändert die konfigurierte Zugriffsregel nicht."
|
||||
),
|
||||
}
|
||||
},
|
||||
@@ -354,6 +356,9 @@ manifest = ModuleManifest(
|
||||
title="Service directory",
|
||||
summary="Find services available in the configured institution and understand relevant availability limits.",
|
||||
body=(
|
||||
"Documentation books sit immediately beside the visible heading or contextual label for "
|
||||
"Services, not among operational action buttons. Field help remains beside its label. "
|
||||
"The My function postboxes section heading follows the selected interface language; postbox and service names remain provider-owned data. "
|
||||
"Portal presents provider-owned, versioned service definitions. "
|
||||
"Published services may be available, unavailable with a reason, "
|
||||
"or undiscoverable when they do not apply to the current audience. "
|
||||
@@ -374,6 +379,10 @@ manifest = ModuleManifest(
|
||||
"title": "Dienstverzeichnis",
|
||||
"summary": "Verfügbare Dienste der konfigurierten Institution finden und ihre maßgeblichen Verfügbarkeitsgrenzen verstehen.",
|
||||
"body": (
|
||||
"Dokumentationsbücher stehen unmittelbar neben der sichtbaren Überschrift oder "
|
||||
"Kontextbezeichnung für Dienste, nicht zwischen ausführbaren Aktionsschaltflächen. Feldhilfe "
|
||||
"bleibt neben der Feldbezeichnung. "
|
||||
"Die Abschnittsüberschrift Meine Funktionspostfächer folgt der gewählten Oberflächensprache; Postfach- und Leistungsnamen bleiben Daten des jeweiligen Anbieters. "
|
||||
"Das Portal zeigt versionierte Dienstdefinitionen ihrer jeweils verantwortlichen Anbieter. Veröffentlichte Dienste können verfügbar sein, mit einer Begründung als nicht verfügbar erscheinen oder unauffindbar bleiben, wenn sie für die aktuelle Zielgruppe nicht gelten. "
|
||||
"Beim Öffnen wird die genaue Revision erneut geprüft und der Start eines Falls, Formulars oder Workflows an die installierte Besitzerfunktion übergeben."
|
||||
),
|
||||
|
||||
+2
-1
@@ -14,7 +14,8 @@
|
||||
"./styles/portal.css": "./src/styles/portal.css"
|
||||
},
|
||||
"scripts": {
|
||||
"test:interface-pattern": "node scripts/test-interface-pattern.mjs"
|
||||
"test:interface-pattern": "node scripts/test-interface-pattern.mjs",
|
||||
"test:translations": "node --test scripts/test-translations.mjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve } from "node:path";
|
||||
import { test } from "node:test";
|
||||
|
||||
const webui = resolve(import.meta.dirname, "..");
|
||||
const core = resolve(webui, "../../govoplan-core/webui");
|
||||
const require = createRequire(resolve(core, "package.json"));
|
||||
const { buildSync } = require("esbuild");
|
||||
const expected = {
|
||||
"My function postboxes": "Meine Funktionspostfächer",
|
||||
"Sign in to view status": "Anmelden, um den Status anzuzeigen",
|
||||
"Request a short-lived status link": "Kurzlebigen Statuslink anfordern",
|
||||
"Timeline": "Verlauf",
|
||||
};
|
||||
|
||||
// Exercise the real locale provider and shared Card contract. Compile in memory:
|
||||
// no server, shared build directory, optional module imports, or browser effects.
|
||||
const { outputFiles } = buildSync({
|
||||
stdin: {
|
||||
contents: `
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { generatedTranslations } from ${JSON.stringify(resolve(webui, "src/i18n/generatedTranslations.ts"))};
|
||||
import { PlatformLanguageProvider, usePlatformLanguage } from ${JSON.stringify(resolve(core, "src/i18n/LanguageContext.tsx"))};
|
||||
import Card from ${JSON.stringify(resolve(core, "src/components/Card.tsx"))};
|
||||
export const catalog = generatedTranslations;
|
||||
function NativeHeading({ label }) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
return <h2 id="portal-postboxes-heading">{translateText(label)}</h2>;
|
||||
}
|
||||
export function heading(language, label, native) {
|
||||
return renderToStaticMarkup(
|
||||
<PlatformLanguageProvider preferredLanguageCode={language} moduleTranslations={[catalog]}>
|
||||
{native ? <NativeHeading label={label} /> : <Card title={label}>Fixture</Card>}
|
||||
</PlatformLanguageProvider>
|
||||
);
|
||||
}
|
||||
`,
|
||||
loader: "tsx",
|
||||
resolveDir: core,
|
||||
},
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: "node",
|
||||
format: "cjs",
|
||||
jsx: "automatic",
|
||||
external: ["react", "react-dom/server"],
|
||||
logLevel: "silent",
|
||||
});
|
||||
const compiled = { exports: {} };
|
||||
new Function("require", "module", "exports", outputFiles[0].text)(require, compiled, compiled.exports);
|
||||
const { catalog, heading } = compiled.exports;
|
||||
|
||||
test("Portal registers and owns all four translations", () => {
|
||||
const module = readFileSync(resolve(webui, "src/module.ts"), "utf8");
|
||||
assert.match(module, /import\s*\{\s*generatedTranslations\s*\}\s*from\s*["']\.\/i18n\/generatedTranslations["']/);
|
||||
assert.match(module, /translations:\s*generatedTranslations/);
|
||||
for (const [english, german] of Object.entries(expected)) {
|
||||
assert.equal(catalog.en[english], english);
|
||||
assert.equal(catalog.de[english], german);
|
||||
}
|
||||
});
|
||||
|
||||
test("each heading renders in English and German without the DOM translation bridge", () => {
|
||||
for (const [english, german] of Object.entries(expected)) {
|
||||
for (const [language, translation] of [["en", english], ["de", german]]) {
|
||||
const native = english === "My function postboxes";
|
||||
const markup = heading(language, english, native);
|
||||
const tag = native ? '<h2 id="portal-postboxes-heading">' : "<h2>";
|
||||
assert.ok(markup.includes(`${tag}${translation}</h2>`), `${language}: ${english}`);
|
||||
if (language === "de") assert.ok(!markup.includes(`>${english}</h2>`));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("Portal pages keep the verified native-heading and shared-card translation paths", () => {
|
||||
const directory = readFileSync(resolve(webui, "src/features/portal/PortalPage.tsx"), "utf8");
|
||||
assert.match(directory, /const\s*\{\s*translateText\s*\}\s*=\s*usePlatformLanguage\(\)/);
|
||||
assert.ok(directory.includes('<h2 id="portal-postboxes-heading">{translateText("My function postboxes")}</h2>'));
|
||||
const status = readFileSync(resolve(webui, "src/features/portal/PortalStatusPage.tsx"), "utf8");
|
||||
for (const english of Object.keys(expected).slice(1)) {
|
||||
assert.ok(status.includes(`<Card title="${english}"`), `Shared card title: ${english}`);
|
||||
}
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import { ActionBlockerHint,
|
||||
StatusBadge,
|
||||
ToggleSwitch,
|
||||
useGuardedNavigate,
|
||||
usePlatformLanguage,
|
||||
WorkspaceActionBar,
|
||||
WorkspaceFrame,
|
||||
type PlatformRouteContext
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
|
||||
export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const [query, setQuery] = useState("");
|
||||
const [submittedQuery, setSubmittedQuery] = useState("");
|
||||
const [includeUnavailable, setIncludeUnavailable] = useState(true);
|
||||
@@ -155,7 +157,9 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
onChange={setIncludeUnavailable}
|
||||
/>
|
||||
</>}
|
||||
helpAction={<DocumentationHelpLink
|
||||
title="Service directory"
|
||||
titleLevel={1}
|
||||
titleHelp={<DocumentationHelpLink
|
||||
reference={{ topicId: "portal.service-directory", documentationType: "user" }}
|
||||
label="Open service directory documentation"
|
||||
/>}
|
||||
@@ -177,7 +181,7 @@ export default function PortalPage({ settings }: PlatformRouteContext) {
|
||||
<section className="portal-postboxes" aria-labelledby="portal-postboxes-heading">
|
||||
<div className="portal-section-heading">
|
||||
<Inbox size={18} aria-hidden="true" />
|
||||
<h2 id="portal-postboxes-heading">My function postboxes</h2>
|
||||
<h2 id="portal-postboxes-heading">{translateText("My function postboxes")}</h2>
|
||||
</div>
|
||||
<div className="portal-postbox-list">
|
||||
{postboxes.map((entry) => (
|
||||
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
/** Module-owned translations for contextual headings. */
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
en: {
|
||||
"Service directory": "Service directory",
|
||||
"My function postboxes": "My function postboxes",
|
||||
"Sign in to view status": "Sign in to view status",
|
||||
"Request a short-lived status link": "Request a short-lived status link",
|
||||
"Timeline": "Timeline",
|
||||
},
|
||||
de: {
|
||||
"Service directory": "Leistungsverzeichnis",
|
||||
"My function postboxes": "Meine Funktionspostfächer",
|
||||
"Sign in to view status": "Anmelden, um den Status anzuzeigen",
|
||||
"Request a short-lived status link": "Kurzlebigen Statuslink anfordern",
|
||||
"Timeline": "Verlauf",
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createElement, lazy } from "react";
|
||||
import type { PlatformWebModule } from "@govoplan/core-webui";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import "./styles/portal.css";
|
||||
|
||||
|
||||
@@ -7,6 +8,7 @@ const PortalPage = lazy(() => import("./features/portal/PortalPage"));
|
||||
const PortalStatusPage = lazy(() => import("./features/portal/PortalStatusPage"));
|
||||
|
||||
export const portalModule: PlatformWebModule = {
|
||||
translations: generatedTranslations,
|
||||
id: "portal",
|
||||
label: "Services",
|
||||
version: "0.1.19",
|
||||
|
||||
Reference in New Issue
Block a user