diff --git a/src/govoplan_admin/backend/manifest.py b/src/govoplan_admin/backend/manifest.py index 61a2903..c9d0dc5 100644 --- a/src/govoplan_admin/backend/manifest.py +++ b/src/govoplan_admin/backend/manifest.py @@ -129,16 +129,18 @@ manifest = ModuleManifest( title="Manage reference language and language packages", summary="German is the reference and new-installation default while configured tenant and user language choices remain effective.", body=( - "German is the first-class product acceptance language and the default for a new system or tenant. English remains installed as the source-code fallback. System administrators control available and enabled language packages and the system default; tenant and user choices can select only languages allowed above them. Existing explicit preferences are preserved when this baseline is introduced. Disabling a language must not discard translated content or silently rewrite a stored preference." + "German is the first-class product acceptance language and the default for a new system or tenant. English remains installed as the source-code fallback. The dedicated Language packages administration surface separates package installation, activation, deactivation, uninstall eligibility, and the default for newly created tenants from general system settings. Its compatibility matrix derives each package state from the installed module ID, exact module version, and available runtime translation catalog; a missing catalog is shown as incompatible rather than treated as translated. German, English, and the active default cannot be uninstalled. System administrators control available and enabled language packages and the system default; tenant and user choices can select only languages allowed above them. Existing explicit preferences are preserved when this baseline is introduced. Disabling a language must not discard translated content or silently rewrite a stored preference." ), documentation_types=("admin",), audience=("system_admin", "tenant_admin"), related_modules=("tenancy", "access", "docs"), metadata={ "kind": "reference", + "route": "/admin?section=system-language-packages", + "screen": "Language packages", "help_contexts": [ - "admin.system-settings.locale", - "admin.system-settings.languages", + "admin.system-language-packages", + "admin.system-language-packages.compatibility", ], }, ), @@ -229,6 +231,7 @@ manifest = ModuleManifest( view_surfaces=( ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0), ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10), + ViewSurface(id="admin.section.system-language-packages", module_id="admin", kind="section", label="Language packages", order=15), ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20), ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30), ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40), diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py index b1a7cbf..0db2aaa 100644 --- a/tests/test_interface_documentation_contract.py +++ b/tests/test_interface_documentation_contract.py @@ -16,6 +16,10 @@ class InterfaceDocumentationContractTests(unittest.TestCase): "admin.configuration-packages", "admin.governance-templates", }, + "admin.language-reference-and-packages": { + "admin.system-language-packages", + "admin.system-language-packages.compatibility", + }, "admin.module-lifecycle-workflow": { "admin.module-lifecycle", "admin.module-lifecycle.queue-blocker", @@ -44,6 +48,7 @@ class InterfaceDocumentationContractTests(unittest.TestCase): { "admin.section.overview", "admin.section.system-settings", + "admin.section.system-language-packages", "admin.section.system-configuration-changes", "admin.section.system-configuration-packages", "admin.section.system-role-templates", diff --git a/tests/test_language_package_section.py b/tests/test_language_package_section.py new file mode 100644 index 0000000..3d44dc7 --- /dev/null +++ b/tests/test_language_package_section.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_language_lifecycle_has_a_dedicated_admin_section() -> None: + module_source = (ROOT / "webui/src/module.ts").read_text(encoding="utf-8") + panel_source = ( + ROOT / "webui/src/features/admin/LanguagePackagesPanel.tsx" + ).read_text(encoding="utf-8") + general_source = ( + ROOT / "webui/src/features/admin/SystemSettingsPanel.tsx" + ).read_text(encoding="utf-8") + + assert 'id: "system-language-packages"' in module_source + assert 'surfaceId: "admin.section.system-language-packages"' in module_source + assert "data-language-package-lifecycle" in panel_source + assert "module.version" in panel_source + assert "module.translations?.[language.code]" in panel_source + assert "uninstallPackage" in panel_source + assert "setPackageActive" in panel_source + assert "default_locale" in panel_source + assert "language_packages" not in general_source + assert "available_languages:" not in general_source + + +def test_language_lifecycle_names_every_required_state() -> None: + source = ( + ROOT / "webui/src/features/admin/LanguagePackagesPanel.tsx" + ).read_text(encoding="utf-8") + translations = ( + ROOT / "webui/src/i18n/generatedTranslations.ts" + ).read_text(encoding="utf-8") + + for marker in ( + "installed.7bb4405c", + "available.7c8f1005", + "active.a733b809", + "inactive.09af574c", + "incompatible.lp001", + "uninstallable.lp001", + ): + assert marker in source + for locale_marker in ('"en": {', '"de": {'): + assert locale_marker in translations + assert translations.count("language_package_administration.lp001") == 2 diff --git a/webui/src/features/admin/AdminOverviewPanel.tsx b/webui/src/features/admin/AdminOverviewPanel.tsx index 5d5abbb..ca87340 100644 --- a/webui/src/features/admin/AdminOverviewPanel.tsx +++ b/webui/src/features/admin/AdminOverviewPanel.tsx @@ -48,6 +48,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio {availableSections.has("system-tenant-modules") && onSelect("system-tenant-modules")} />} {availableSections.has("system-configuration-packages") && onSelect("system-configuration-packages")} />} {availableSections.has("system-settings") && onSelect("system-settings")} />} + {availableSections.has("system-language-packages") && onSelect("system-language-packages")} />} {availableSections.has("system-configuration-changes") && onSelect("system-configuration-changes")} />} {availableSections.has("system-audit") && onSelect("system-audit")} />} @@ -113,7 +114,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio } -const platformSectionIds = ["system-modules", "system-tenant-modules", "system-configuration-packages", "system-settings", "system-configuration-changes", "system-audit"]; +const platformSectionIds = ["system-modules", "system-tenant-modules", "system-configuration-packages", "system-settings", "system-language-packages", "system-configuration-changes", "system-audit"]; const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention", "system-view-policy"]; const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-view-policy", "tenant-modules", "tenant-settings", "tenant-audit"]; const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention", "group-view-policy"]; diff --git a/webui/src/features/admin/LanguagePackagesPanel.tsx b/webui/src/features/admin/LanguagePackagesPanel.tsx new file mode 100644 index 0000000..9b0c0d2 --- /dev/null +++ b/webui/src/features/admin/LanguagePackagesPanel.tsx @@ -0,0 +1,330 @@ +import { useEffect, useMemo, useState } from "react"; +import type { ApiSettings, PlatformWebModule } from "@govoplan/core-webui"; +import { + AdminPageLayout, + Button, + Card, + DocumentationHelpLink, + FormField, + FormGrid, + StatusBadge, + adminErrorMessage, + i18nMessage, + useDeltaWatermarks, + usePlatformModules, + useUnsavedDraftGuard +} from "@govoplan/core-webui"; +import { + fetchSystemSettingsDelta, + updateSystemSettings, + type LanguagePackage, + type SystemSettingsItem +} from "../../api/admin"; +import { + ADMIN_GOVERNANCE_DOCUMENTATION, + ADMIN_INTERFACE_I18N, + mutationDisabledReason +} from "./interfacePatterns"; +import { + SYSTEM_SETTINGS_FALLBACK, + applySystemSettingsSections, + languageOptionLabel, + normalizeLanguageCode +} from "./systemSettingsModel"; + +const DELTA_KEY = "admin:language-packages"; +const BUILT_IN_LANGUAGE_CODES = new Set(["de", "en"]); + +type LanguageLifecycle = { + code: string; + package: LanguagePackage | null; + catalogCount: number; + installed: boolean; + active: boolean; + uninstallable: boolean; +}; + +export default function LanguagePackagesPanel({ + settings, + canWrite +}: { + settings: ApiSettings; + canWrite: boolean; +}) { + const modules = usePlatformModules(); + const [draft, setDraft] = useState(SYSTEM_SETTINGS_FALLBACK); + const [savedDraft, setSavedDraft] = useState(SYSTEM_SETTINGS_FALLBACK); + const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" }); + const [selectedCode, setSelectedCode] = useState("de"); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); + const dirty = languageDraftKey(draft) !== languageDraftKey(savedDraft); + const lifecycle = useMemo( + () => languageLifecycle(draft, modules), + [draft, modules] + ); + const selectedLifecycle = lifecycle.find((item) => item.code === selectedCode) ?? lifecycle[0]; + + useUnsavedDraftGuard({ + dirty, + onSave: save, + onDiscard: () => { + setDraft(savedDraft); + setPackageDraft({ code: "", label: "", nativeLabel: "" }); + } + }); + + async function load() { + setLoading(true); + setError(""); + try { + const wasDirty = languageDraftKey(draft) !== languageDraftKey(savedDraft); + const response = await fetchSystemSettingsDelta(settings, { + since: getDeltaWatermark(DELTA_KEY) + }); + setDeltaWatermark(DELTA_KEY, response.watermark); + if (response.full && response.item) { + setSavedDraft(response.item); + if (!wasDirty) setDraft(response.item); + } else if (response.changed_sections.length) { + setSavedDraft((current) => applySystemSettingsSections(current, response.sections)); + if (!wasDirty) { + setDraft((current) => applySystemSettingsSections(current, response.sections)); + } + } + } catch (err) { + setError(adminErrorMessage(err)); + } finally { + setLoading(false); + } + } + + useEffect(() => { + resetDeltaWatermark(DELTA_KEY); + void load(); + }, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]); + + useEffect(() => { + if (lifecycle.length && !lifecycle.some((item) => item.code === selectedCode)) { + setSelectedCode(lifecycle[0].code); + } + }, [lifecycle, selectedCode]); + + async function save(): Promise { + setBusy(true); + setError(""); + setSuccess(""); + try { + const saved = await updateSystemSettings(settings, { + default_locale: draft.default_locale, + allow_tenant_custom_groups: draft.allow_tenant_custom_groups, + allow_tenant_custom_roles: draft.allow_tenant_custom_roles, + allow_tenant_api_keys: draft.allow_tenant_api_keys, + available_languages: draft.available_languages, + enabled_language_codes: draft.enabled_language_codes + }); + setDraft(saved); + setSavedDraft(saved); + resetDeltaWatermark(DELTA_KEY); + setSuccess("i18n:govoplan-admin.language_package_settings_saved.lp001"); + return true; + } catch (err) { + setError(adminErrorMessage(err)); + return false; + } finally { + setBusy(false); + } + } + + function installPackage(code: string, label?: string, nativeLabel?: string) { + const normalized = normalizeLanguageCode(code); + if (!normalized || draft.available_languages.some((item) => item.code === normalized)) return; + const resolvedLabel = label?.trim() || normalized.toUpperCase(); + setDraft({ + ...draft, + available_languages: [ + ...draft.available_languages, + { + code: normalized, + label: resolvedLabel, + native_label: nativeLabel?.trim() || resolvedLabel + } + ] + }); + setSelectedCode(normalized); + } + + function installDraftPackage() { + const code = normalizeLanguageCode(packageDraft.code); + if (!code || !packageDraft.label.trim()) return; + installPackage(code, packageDraft.label, packageDraft.nativeLabel); + setPackageDraft({ code: "", label: "", nativeLabel: "" }); + } + + function setPackageActive(code: string, active: boolean) { + const enabled = new Set(draft.enabled_language_codes); + if (active) enabled.add(code); + else enabled.delete(code); + setDraft({ + ...draft, + enabled_language_codes: draft.available_languages + .map((item) => item.code) + .filter((item) => enabled.has(item)) + }); + } + + function uninstallPackage(code: string) { + if (BUILT_IN_LANGUAGE_CODES.has(code) || code === draft.default_locale) return; + setDraft({ + ...draft, + available_languages: draft.available_languages.filter((item) => item.code !== code), + enabled_language_codes: draft.enabled_language_codes.filter((item) => item !== code) + }); + } + + const enabledDefaultOptions = draft.available_languages.filter((item) => + draft.enabled_language_codes.includes(item.code) + ); + + return ( + + + + + } + > +
+ + + + +

i18n:govoplan-admin.default_language_inheritance_help.lp001

+
+ + +
+ {lifecycle.map((item) => ( +
+
+
+ {item.package ? languageOptionLabel(item.package) : item.code.toUpperCase()} + {item.code} +
+
+ + {item.installed && } + + {item.uninstallable && } +
+
+ {i18nMessage("i18n:govoplan-admin.module_version_catalogs.lp001", { value0: item.catalogCount })} +
+
+
+ {!item.installed && } + {item.installed && } + {item.uninstallable && } +
+
+ ))} +
+ + + setPackageDraft({ ...packageDraft, code: event.target.value })} /> + + + setPackageDraft({ ...packageDraft, label: event.target.value })} /> + + + setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} /> + + +
+ +
+

i18n:govoplan-admin.language_packages_help

+
+ + + {selectedLifecycle && + + } + {selectedLifecycle && modules.length > 0 ? ( +
+ {modules.map((module) => )} +
+ ) :

i18n:govoplan-admin.no_module_language_catalogs.lp001

} +
+
+
+ ); +} + +function ModuleLanguageState({ module, language }: { module: PlatformWebModule; language: LanguageLifecycle }) { + const compatible = Boolean(module.translations?.[language.code]); + const state = language.installed && compatible + ? "i18n:govoplan-admin.installed.7bb4405c" + : compatible + ? "i18n:govoplan-admin.available.7c8f1005" + : "i18n:govoplan-admin.incompatible.lp001"; + return ( +
+
+
+ {module.label} + {module.id} + v{module.version} +
+
+ + {language.installed && compatible && } +
+
+
+ ); +} + +function languageLifecycle(item: SystemSettingsItem, modules: PlatformWebModule[]): LanguageLifecycle[] { + const installed = new Map(item.available_languages.map((language) => [language.code, language])); + const codes = new Set(installed.keys()); + modules.forEach((module) => Object.keys(module.translations ?? {}).forEach((code) => codes.add(code))); + return [...codes].sort().map((code) => { + const languagePackage = installed.get(code) ?? null; + const catalogCount = modules.filter((module) => Boolean(module.translations?.[code])).length; + return { + code, + package: languagePackage, + catalogCount, + installed: Boolean(languagePackage), + active: item.enabled_language_codes.includes(code), + uninstallable: Boolean( + languagePackage && !BUILT_IN_LANGUAGE_CODES.has(code) && code !== item.default_locale + ) + }; + }); +} + +function languageDraftKey(item: SystemSettingsItem): string { + return JSON.stringify({ + default_locale: item.default_locale, + available_languages: item.available_languages, + enabled_language_codes: item.enabled_language_codes + }); +} diff --git a/webui/src/features/admin/SystemSettingsPanel.tsx b/webui/src/features/admin/SystemSettingsPanel.tsx index 4509292..d2a960a 100644 --- a/webui/src/features/admin/SystemSettingsPanel.tsx +++ b/webui/src/features/admin/SystemSettingsPanel.tsx @@ -1,73 +1,32 @@ import { useEffect, useState } from "react"; -import type { FormGrid, ApiSettings } from "@govoplan/core-webui"; +import type { ApiSettings } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; -import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin"; -import { AdminPageLayout, AdminSelectionList, DocumentationHelpLink, NavigationPreferenceEditor, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformModulesChanged, useDeltaWatermarks, usePlatformModules, useUnsavedDraftGuard } from "@govoplan/core-webui"; +import { fetchSystemSettingsDelta, updateSystemSettings, type SystemSettingsItem } from "../../api/admin"; +import { AdminPageLayout, DocumentationHelpLink, NavigationPreferenceEditor, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformModulesChanged, useDeltaWatermarks, usePlatformModules, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N, mutationDisabledReason } from "./interfacePatterns"; +import { SYSTEM_SETTINGS_FALLBACK, applySystemSettingsSections } from "./systemSettingsModel"; const DELTA_KEY = "admin:system-settings"; -const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = { - store_raw_campaign_json: true, - raw_campaign_json_retention_days: true, - generated_eml_retention_days: true, - stored_report_detail_retention_days: true, - mock_mailbox_retention_days: true, - audit_detail_retention_days: true, - audit_detail_level: true -}; - -const defaultPrivacyPolicy: PrivacyRetentionPolicy = { - store_raw_campaign_json: true, - raw_campaign_json_retention_days: null, - generated_eml_retention_days: null, - stored_report_detail_retention_days: null, - mock_mailbox_retention_days: null, - audit_detail_retention_days: null, - audit_detail_level: "full", - allow_lower_level_limits: defaultAllowLowerLevelLimits -}; - -const fallback: SystemSettingsItem = { - default_locale: "de", - allow_tenant_custom_groups: true, - allow_tenant_custom_roles: true, - allow_tenant_api_keys: true, - privacy_retention_policy: defaultPrivacyPolicy, - maintenance_mode: { enabled: false, message: "" }, - available_languages: [ - { code: "de", label: "German", native_label: "Deutsch" }, - { code: "en", label: "English", native_label: "English" } - ], - enabled_language_codes: ["de", "en"], - settings: {}, - navigation: null -}; - export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) { const modules = usePlatformModules(); const navigationItems = configurableNavigationItemsForModules(modules); - const [draft, setDraft] = useState(fallback); - const [savedDraft, setSavedDraft] = useState(fallback); + const [draft, setDraft] = useState(SYSTEM_SETTINGS_FALLBACK); + const [savedDraft, setSavedDraft] = useState(SYSTEM_SETTINGS_FALLBACK); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); - const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" }); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); - const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes); const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft); useUnsavedDraftGuard({ dirty, onSave: save, - onDiscard: () => { - setDraft(savedDraft); - setPackageDraft({ code: "", label: "", nativeLabel: "" }); - } + onDiscard: () => setDraft(savedDraft) }); async function load() { @@ -104,8 +63,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint allow_tenant_custom_roles: draft.allow_tenant_custom_roles, allow_tenant_api_keys: draft.allow_tenant_api_keys, maintenance_mode: draft.maintenance_mode, - available_languages: draft.available_languages, - enabled_language_codes: draft.enabled_language_codes, navigation: draft.navigation }); setDraft(saved); @@ -118,30 +75,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint {setBusy(false);} } - function setEnabledLanguages(selected: string[]) { - const enabled = new Set(selected); - const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item)); - const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale); - setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale }); - } - - function installLanguagePackage() { - const code = normalizeLanguageCode(packageDraft.code); - if (!code || !packageDraft.label.trim()) return; - const nextPackage: LanguagePackage = { - code, - label: packageDraft.label.trim(), - native_label: packageDraft.nativeLabel.trim() || packageDraft.label.trim() - }; - const existing = new Set(draft.available_languages.map((item) => item.code)); - const available = existing.has(code) - ? draft.available_languages.map((item) => item.code === code ? nextPackage : item) - : [...draft.available_languages, nextPackage]; - const enabled = draft.enabled_language_codes.includes(code) ? draft.enabled_language_codes : [...draft.enabled_language_codes, code]; - setDraft({ ...draft, available_languages: available, enabled_language_codes: enabled, default_locale: draft.default_locale || code }); - setPackageDraft({ code: "", label: "", nativeLabel: "" }); - } - return ( }>
- - - - - - - ({ id: item.code, label: item.code.toUpperCase(), description: languageOptionLabel(item), disabled: !canWrite || busy || item.code === draft.default_locale }))} - selected={draft.enabled_language_codes} - onChange={setEnabledLanguages} - /> - - - setPackageDraft({ ...packageDraft, code: event.target.value })} /> - - - setPackageDraft({ ...packageDraft, label: event.target.value })} /> - - - setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} /> - -
- -
-
-

i18n:govoplan-admin.language_packages_help

-
setDraft({ ...draft, allow_tenant_custom_groups: checked })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" /> @@ -226,40 +127,12 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint } -function normalizeLanguageCode(value: string): string { - return value.trim().toLowerCase().replace(/_/g, "-").split(/[^a-z0-9]+/).filter(Boolean).join("-"); -} - -function localeOptions(current: string, enabled: string[]): string[] { - return [...new Set([current, ...enabled].filter((item) => item && item.trim()))]; -} - -function languageOptionLabel(language: LanguagePackage): string { - return `${language.code.toUpperCase()} - ${language.native_label || language.label}`; -} - function systemSettingsDraftKey(item: SystemSettingsItem): string { return JSON.stringify({ - default_locale: item.default_locale, allow_tenant_custom_groups: item.allow_tenant_custom_groups, allow_tenant_custom_roles: item.allow_tenant_custom_roles, allow_tenant_api_keys: item.allow_tenant_api_keys, maintenance_mode: item.maintenance_mode, - available_languages: item.available_languages, - enabled_language_codes: item.enabled_language_codes, navigation: item.navigation }); } - -function applySystemSettingsSections(item: SystemSettingsItem, sections: SystemSettingsDeltaSections): SystemSettingsItem { - return { - ...item, - ...(sections.defaults ?? {}), - ...(sections.tenant_capabilities ?? {}), - ...(sections.languages ?? {}), - ...(sections.privacy_retention_policy ? { privacy_retention_policy: sections.privacy_retention_policy } : {}), - ...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}), - ...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}), - ...(sections.settings ? { settings: sections.settings } : {}) - }; -} diff --git a/webui/src/features/admin/systemSettingsModel.ts b/webui/src/features/admin/systemSettingsModel.ts new file mode 100644 index 0000000..e9368ba --- /dev/null +++ b/webui/src/features/admin/systemSettingsModel.ts @@ -0,0 +1,79 @@ +import type { + PrivacyRetentionLimitPermissions, + PrivacyRetentionPolicy, + SystemSettingsDeltaSections, + SystemSettingsItem +} from "../../api/admin"; + +const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = { + store_raw_campaign_json: true, + raw_campaign_json_retention_days: true, + generated_eml_retention_days: true, + stored_report_detail_retention_days: true, + mock_mailbox_retention_days: true, + audit_detail_retention_days: true, + audit_detail_level: true +}; + +const defaultPrivacyPolicy: PrivacyRetentionPolicy = { + store_raw_campaign_json: true, + raw_campaign_json_retention_days: null, + generated_eml_retention_days: null, + stored_report_detail_retention_days: null, + mock_mailbox_retention_days: null, + audit_detail_retention_days: null, + audit_detail_level: "full", + allow_lower_level_limits: defaultAllowLowerLevelLimits +}; + +export const SYSTEM_SETTINGS_FALLBACK: SystemSettingsItem = { + default_locale: "de", + allow_tenant_custom_groups: true, + allow_tenant_custom_roles: true, + allow_tenant_api_keys: true, + privacy_retention_policy: defaultPrivacyPolicy, + maintenance_mode: { enabled: false, message: "" }, + available_languages: [ + { code: "de", label: "German", native_label: "Deutsch" }, + { code: "en", label: "English", native_label: "English" } + ], + enabled_language_codes: ["de", "en"], + settings: {}, + navigation: null +}; + +export function applySystemSettingsSections( + item: SystemSettingsItem, + sections: SystemSettingsDeltaSections +): SystemSettingsItem { + return { + ...item, + ...(sections.defaults ?? {}), + ...(sections.tenant_capabilities ?? {}), + ...(sections.languages ?? {}), + ...(sections.privacy_retention_policy + ? { privacy_retention_policy: sections.privacy_retention_policy } + : {}), + ...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}), + ...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}), + ...(sections.settings ? { settings: sections.settings } : {}) + }; +} + +export function normalizeLanguageCode(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/_/g, "-") + .split(/[^a-z0-9]+/) + .filter(Boolean) + .join("-"); +} + +export function languageOptionLabel(language: { + code: string; + label: string; + native_label?: string | null; +}): string { + return `${language.code.toUpperCase()} - ${language.native_label || language.label}`; +} diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 9d0f112..e8e0010 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -219,6 +219,20 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.language_name_placeholder": "French", "i18n:govoplan-admin.language_packages": "Language packages", "i18n:govoplan-admin.language_packages_help": "Installed packages define which language codes can be enabled. A package only becomes fully translated when matching module translation catalogs are present.", + "i18n:govoplan-admin.language_package_administration.lp001": "Language packages and tenant defaults", + "i18n:govoplan-admin.language_package_administration_description.lp001": "Install and activate language packages, choose the default for new tenants, and inspect exact module-version compatibility.", + "i18n:govoplan-admin.language_package_administration_overview.lp001": "Package lifecycle, default tenant language, and module-version compatibility.", + "i18n:govoplan-admin.language_package_settings_saved.lp001": "Language package settings saved.", + "i18n:govoplan-admin.default_language_inheritance_help.lp001": "New tenants inherit this active language; tenant and user preferences can select only languages enabled above them.", + "i18n:govoplan-admin.compatible.lp001": "Compatible", + "i18n:govoplan-admin.incompatible.lp001": "Incompatible", + "i18n:govoplan-admin.uninstallable.lp001": "Uninstallable", + "i18n:govoplan-admin.module_version_catalogs.lp001": "{value0} module-version catalogs provide this language.", + "i18n:govoplan-admin.activate.lp001": "Activate", + "i18n:govoplan-admin.deactivate.lp001": "Deactivate", + "i18n:govoplan-admin.default_language_must_remain_active.lp001": "The default language must remain active.", + "i18n:govoplan-admin.module_version_compatibility.lp001": "Module-version compatibility", + "i18n:govoplan-admin.no_module_language_catalogs.lp001": "No installed module language catalogs are available. Package and default-language controls remain usable.", "i18n:govoplan-admin.native_language_name": "Native language name", "i18n:govoplan-admin.native_language_name_placeholder": "Francais", "i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants", @@ -732,6 +746,20 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-admin.language_name_placeholder": "Franzoesisch", "i18n:govoplan-admin.language_packages": "Sprachpakete", "i18n:govoplan-admin.language_packages_help": "Installierte Pakete legen fest, welche Sprachcodes aktiviert werden koennen. Ein Paket ist erst vollstaendig uebersetzt, wenn passende Uebersetzungskataloge der Module vorhanden sind.", + "i18n:govoplan-admin.language_package_administration.lp001": "Sprachpakete und Mandantenstandards", + "i18n:govoplan-admin.language_package_administration_description.lp001": "Sprachpakete installieren und aktivieren, den Standard fuer neue Mandanten waehlen und die exakte Modulversions-Kompatibilitaet pruefen.", + "i18n:govoplan-admin.language_package_administration_overview.lp001": "Paketlebenszyklus, Standard-Mandantensprache und Modulversions-Kompatibilitaet.", + "i18n:govoplan-admin.language_package_settings_saved.lp001": "Sprachpaket-Einstellungen gespeichert.", + "i18n:govoplan-admin.default_language_inheritance_help.lp001": "Neue Mandanten erben diese aktive Sprache; Mandanten- und Benutzereinstellungen koennen nur oberhalb aktivierte Sprachen waehlen.", + "i18n:govoplan-admin.compatible.lp001": "Kompatibel", + "i18n:govoplan-admin.incompatible.lp001": "Inkompatibel", + "i18n:govoplan-admin.uninstallable.lp001": "Deinstallierbar", + "i18n:govoplan-admin.module_version_catalogs.lp001": "{value0} Modulversions-Kataloge stellen diese Sprache bereit.", + "i18n:govoplan-admin.activate.lp001": "Aktivieren", + "i18n:govoplan-admin.deactivate.lp001": "Deaktivieren", + "i18n:govoplan-admin.default_language_must_remain_active.lp001": "Die Standardsprache muss aktiv bleiben.", + "i18n:govoplan-admin.module_version_compatibility.lp001": "Modulversions-Kompatibilitaet", + "i18n:govoplan-admin.no_module_language_catalogs.lp001": "Es sind keine Sprachkataloge installierter Module verfuegbar. Paket- und Standardspracheinstellungen bleiben nutzbar.", "i18n:govoplan-admin.native_language_name": "Nativer Sprachname", "i18n:govoplan-admin.native_language_name_placeholder": "Francais", "i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten", diff --git a/webui/src/index.ts b/webui/src/index.ts index b860782..f7e213e 100644 --- a/webui/src/index.ts +++ b/webui/src/index.ts @@ -11,4 +11,5 @@ export { export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel"; export { default as TenantModuleManagementPanel } from "./features/admin/TenantModuleManagementPanel"; export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel"; +export { default as LanguagePackagesPanel } from "./features/admin/LanguagePackagesPanel"; export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui"; diff --git a/webui/src/module.ts b/webui/src/module.ts index 2889781..9d59157 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -11,6 +11,7 @@ const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceT const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel")); const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel")); const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel")); +const LanguagePackagesPanel = lazy(() => import("./features/admin/LanguagePackagesPanel")); const DataSubjectRequestsPanel = lazy(() => import("./features/admin/DataSubjectRequestsPanel")); const translations = { @@ -50,6 +51,20 @@ const adminSections: AdminSectionsUiCapability = { canAccessMaintenance: hasScope(auth, "system:maintenance:access") }) }, + { + id: "system-language-packages", + moduleId: "admin", + kind: "settings", + surfaceId: "admin.section.system-language-packages", + label: "i18n:govoplan-admin.language_packages", + group: "SYSTEM", + order: 15, + allOf: ["system:settings:read"], + render: ({ settings, auth }) => createElement(LanguagePackagesPanel, { + settings, + canWrite: hasScope(auth, "system:settings:write") + }) + }, { id: "system-configuration-changes", moduleId: "admin", @@ -184,6 +199,7 @@ export const adminModule: PlatformWebModule = { viewSurfaces: [ { id: "admin.section.overview", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.overview.0efc2e6b", order: 0 }, { id: "admin.section.system-settings", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.system_general_settings.7cd662ed", order: 10 }, + { id: "admin.section.system-language-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.language_packages", order: 15 }, { id: "admin.section.system-configuration-changes", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_changes.82933bbb", order: 20 }, { id: "admin.section.system-configuration-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_packages.eb2f05f1", order: 30 }, { id: "admin.section.system-role-templates", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.tenant_roles.51aca82d", order: 40 },