- );
-
- return (
- <>
-
-
-
- Organization
-
-
-
- {children}
-
-
-
- >
- );
-}
diff --git a/application/account-management/WebApp/shared/components/topMenu/index.tsx b/application/account-management/WebApp/shared/components/topMenu/index.tsx
index c3e90feaa..de1f6cb56 100644
--- a/application/account-management/WebApp/shared/components/topMenu/index.tsx
+++ b/application/account-management/WebApp/shared/components/topMenu/index.tsx
@@ -1,48 +1,21 @@
-import { t } from "@lingui/core/macro";
+import FederatedTopMenu from "@/federated-modules/topMenu/FederatedTopMenu";
import { Trans } from "@lingui/react/macro";
-import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher";
import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs";
-import { Button } from "@repo/ui/components/Button";
-import { Tooltip, TooltipTrigger } from "@repo/ui/components/Tooltip";
-import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector";
-import { LifeBuoyIcon } from "lucide-react";
import type { ReactNode } from "react";
-import AvatarButton from "../AvatarButton";
interface TopMenuProps {
children?: ReactNode;
- sidePaneOpen?: boolean;
}
-export function TopMenu({ children, sidePaneOpen = false }: Readonly) {
+export function TopMenu({ children }: Readonly) {
return (
-
+
);
}
diff --git a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx
index d3797f4aa..7ea27d471 100644
--- a/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx
+++ b/application/account-management/WebApp/shared/layouts/HorizontalHeroLayout.tsx
@@ -1,9 +1,8 @@
+import LocaleSwitcher from "@/federated-modules/common/LocaleSwitcher";
+import SupportButton from "@/federated-modules/common/SupportButton";
+import ThemeModeSelector from "@/federated-modules/common/ThemeModeSelector";
import { HeroImage } from "@/shared/components/HeroImage";
import { t } from "@lingui/core/macro";
-import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher";
-import { Button } from "@repo/ui/components/Button";
-import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector";
-import { LifeBuoyIcon } from "lucide-react";
import type { ReactNode } from "react";
interface HorizontalHeroLayoutProps {
@@ -14,15 +13,21 @@ export function HorizontalHeroLayout({ children }: Readonly
-
-
-
-
-
+
+
+
-
{children}
-
+
+ {children}
+ {/* Mobile-only icon controls at bottom of form */}
+
+
+
+
+
+
+
diff --git a/application/account-management/WebApp/shared/translations/locale/da-DK.po b/application/account-management/WebApp/shared/translations/locale/da-DK.po
index 7ece3029d..343d4c44b 100644
--- a/application/account-management/WebApp/shared/translations/locale/da-DK.po
+++ b/application/account-management/WebApp/shared/translations/locale/da-DK.po
@@ -75,6 +75,9 @@ msgstr "Er du sikker på, at du vil slette <0>{0} brugere0>?"
msgid "Are you sure you want to delete <0>{userDisplayName}0>?"
msgstr "Er du sikker på, at du vil slette <0>{userDisplayName}0>?"
+msgid "Back Office"
+msgstr "Back Office"
+
msgid "Back to login"
msgstr "Tilbage til login"
@@ -99,6 +102,9 @@ msgstr "Skift profilbillede"
msgid "Change role"
msgstr "Skift rolle"
+msgid "Change theme"
+msgstr "Skift tema"
+
msgid "Change user role"
msgstr "Skift brugerrolle"
@@ -115,9 +121,15 @@ msgstr "Ryd"
msgid "Clear filters"
msgstr "Ryd filtre"
+msgid "Close"
+msgstr "Luk"
+
msgid "Close user profile"
msgstr "Luk brugerprofil"
+msgid "Contact support"
+msgstr "Kontakt support"
+
msgid "Continue"
msgstr "Fortsæt"
@@ -133,6 +145,9 @@ msgstr "Farezone"
msgid "Dark"
msgstr "Mørk"
+msgid "Dashboard"
+msgstr "Dashboard"
+
msgid "Delete"
msgstr "Slet"
@@ -188,6 +203,9 @@ msgstr "Fejl: Noget gik galt!"
msgid "Europe"
msgstr "Europa"
+msgid "Feel free to reach out with any questions or issues you may have."
+msgstr "Du er velkommen til at kontakte os med eventuelle spørgsmål eller problemer, du måtte have."
+
msgid "Fetching data..."
msgstr "Henter data..."
@@ -197,9 +215,6 @@ msgstr "Filtre"
msgid "First name"
msgstr "Fornavn"
-msgid "Help"
-msgstr "Hjælp"
-
msgid "Here's your overview of what's happening."
msgstr "Her er din oversigt over, hvad der sker."
@@ -260,6 +275,9 @@ msgstr "Navn"
msgid "Navigation"
msgstr "Navigation"
+msgid "Need help? Our support team is here to assist you."
+msgstr "Har du brug for hjælp? Vores supportteam er klart til at hjælpe."
+
msgid "Next"
msgstr "Næste"
@@ -269,9 +287,15 @@ msgstr "OK"
msgid "Only account owners can modify the account name"
msgstr "Kun kontoejere kan ændre kontonavnet"
+msgid "Open navigation menu"
+msgstr "Åbn navigationsmenu"
+
msgid "Organization"
msgstr "Organisation"
+msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed."
+msgstr "Vores supportteam hjælper dig med at slette din konto og sikrer, at alle dine data bliver fjernet."
+
msgid "Owner"
msgstr "Ejer"
@@ -326,9 +350,6 @@ msgstr "Gemmer..."
msgid "Screenshots of the dashboard project with desktop and mobile versions"
msgstr "Skærmbilleder af dashboard-projektet med desktop- og mobilversioner"
-msgid "Screenshots of the dashboard project with mobile versions"
-msgstr "Skærmbilleder af dashboard-projektet med mobilversioner"
-
msgid "Search"
msgstr "Søg"
@@ -339,9 +360,6 @@ msgstr "Vælg en ny rolle for <0>{0}0>"
msgid "Select dates"
msgstr "Vælg datoer"
-msgid "Select language"
-msgstr "Vælg sprog"
-
msgid "Send invite"
msgstr "Send invitation"
@@ -363,9 +381,6 @@ msgstr "Tilmeldingsbekræftelseskode"
msgid "Success"
msgstr "Succes"
-msgid "Support"
-msgstr "Support"
-
msgid "System"
msgstr "System"
@@ -381,11 +396,11 @@ msgstr "Dette er den region, hvor dine data er lagret"
msgid "Title"
msgstr "Titel"
-msgid "Toggle collapsed menu"
-msgstr "Skift kollapset menu"
+msgid "To delete your account, please contact our support team."
+msgstr "For at slette din konto bedes du kontakte vores supportteam."
-msgid "Toggle theme"
-msgstr "Skift tema"
+msgid "Toggle sidebar"
+msgstr "Skift sidepanel"
msgid "Total users"
msgstr "Totalt antal brugere"
@@ -469,9 +484,6 @@ msgstr "Velkommen hjem"
msgid "Welcome home, {0}"
msgstr "Velkommen hjem, {0}"
-msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible."
-msgstr "Du er ved at slette kontoen og hele dataomgivelserne permanent via PlatformPlatform.<0/><1/>Denne handling er permanent og kan ikke fortrydes."
-
msgid "Your verification code has expired"
msgstr "Din bekræftelseskode er udløbet"
diff --git a/application/account-management/WebApp/shared/translations/locale/en-US.po b/application/account-management/WebApp/shared/translations/locale/en-US.po
index c129df86d..5cd3ed62b 100644
--- a/application/account-management/WebApp/shared/translations/locale/en-US.po
+++ b/application/account-management/WebApp/shared/translations/locale/en-US.po
@@ -75,6 +75,9 @@ msgstr "Are you sure you want to delete <0>{0} users0>?"
msgid "Are you sure you want to delete <0>{userDisplayName}0>?"
msgstr "Are you sure you want to delete <0>{userDisplayName}0>?"
+msgid "Back Office"
+msgstr "Back Office"
+
msgid "Back to login"
msgstr "Back to login"
@@ -99,6 +102,9 @@ msgstr "Change profile picture"
msgid "Change role"
msgstr "Change role"
+msgid "Change theme"
+msgstr "Change theme"
+
msgid "Change user role"
msgstr "Change user role"
@@ -115,9 +121,15 @@ msgstr "Clear"
msgid "Clear filters"
msgstr "Clear filters"
+msgid "Close"
+msgstr "Close"
+
msgid "Close user profile"
msgstr "Close user profile"
+msgid "Contact support"
+msgstr "Contact support"
+
msgid "Continue"
msgstr "Continue"
@@ -133,6 +145,9 @@ msgstr "Danger zone"
msgid "Dark"
msgstr "Dark"
+msgid "Dashboard"
+msgstr "Dashboard"
+
msgid "Delete"
msgstr "Delete"
@@ -188,6 +203,9 @@ msgstr "Error: Something went wrong!"
msgid "Europe"
msgstr "Europe"
+msgid "Feel free to reach out with any questions or issues you may have."
+msgstr "Feel free to reach out with any questions or issues you may have."
+
msgid "Fetching data..."
msgstr "Fetching data..."
@@ -197,9 +215,6 @@ msgstr "Filters"
msgid "First name"
msgstr "First name"
-msgid "Help"
-msgstr "Help"
-
msgid "Here's your overview of what's happening."
msgstr "Here's your overview of what's happening."
@@ -260,6 +275,9 @@ msgstr "Name"
msgid "Navigation"
msgstr "Navigation"
+msgid "Need help? Our support team is here to assist you."
+msgstr "Need help? Our support team is here to assist you."
+
msgid "Next"
msgstr "Next"
@@ -269,9 +287,15 @@ msgstr "OK"
msgid "Only account owners can modify the account name"
msgstr "Only account owners can modify the account name"
+msgid "Open navigation menu"
+msgstr "Open navigation menu"
+
msgid "Organization"
msgstr "Organization"
+msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed."
+msgstr "Our support team will assist you with the account deletion process and ensure all your data is properly removed."
+
msgid "Owner"
msgstr "Owner"
@@ -326,9 +350,6 @@ msgstr "Saving..."
msgid "Screenshots of the dashboard project with desktop and mobile versions"
msgstr "Screenshots of the dashboard project with desktop and mobile versions"
-msgid "Screenshots of the dashboard project with mobile versions"
-msgstr "Screenshots of the dashboard project with mobile versions"
-
msgid "Search"
msgstr "Search"
@@ -339,9 +360,6 @@ msgstr "Select a new role for <0>{0}0>"
msgid "Select dates"
msgstr "Select dates"
-msgid "Select language"
-msgstr "Select language"
-
msgid "Send invite"
msgstr "Send invite"
@@ -363,9 +381,6 @@ msgstr "Signup verification code"
msgid "Success"
msgstr "Success"
-msgid "Support"
-msgstr "Support"
-
msgid "System"
msgstr "System"
@@ -381,11 +396,11 @@ msgstr "This is the region where your data is stored"
msgid "Title"
msgstr "Title"
-msgid "Toggle collapsed menu"
-msgstr "Toggle collapsed menu"
+msgid "To delete your account, please contact our support team."
+msgstr "To delete your account, please contact our support team."
-msgid "Toggle theme"
-msgstr "Toggle theme"
+msgid "Toggle sidebar"
+msgstr "Toggle sidebar"
msgid "Total users"
msgstr "Total users"
@@ -469,9 +484,6 @@ msgstr "Welcome home"
msgid "Welcome home, {0}"
msgstr "Welcome home, {0}"
-msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible."
-msgstr "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible."
-
msgid "Your verification code has expired"
msgstr "Your verification code has expired"
diff --git a/application/account-management/WebApp/shared/translations/locale/nl-NL.po b/application/account-management/WebApp/shared/translations/locale/nl-NL.po
index c88a20732..7bc27d81d 100644
--- a/application/account-management/WebApp/shared/translations/locale/nl-NL.po
+++ b/application/account-management/WebApp/shared/translations/locale/nl-NL.po
@@ -75,6 +75,9 @@ msgstr "Weet je zeker dat je <0>{0} gebruikers0> wilt verwijderen?"
msgid "Are you sure you want to delete <0>{userDisplayName}0>?"
msgstr "Weet je zeker dat je <0>{userDisplayName}0> wilt verwijderen?"
+msgid "Back Office"
+msgstr "Backoffice"
+
msgid "Back to login"
msgstr "Terug naar inloggen"
@@ -99,6 +102,9 @@ msgstr "Profielfoto wijzigen"
msgid "Change role"
msgstr "Rol wijzigen"
+msgid "Change theme"
+msgstr "Thema wijzigen"
+
msgid "Change user role"
msgstr "Gebruikersrol wijzigen"
@@ -115,9 +121,15 @@ msgstr "Wissen"
msgid "Clear filters"
msgstr "Filters wissen"
+msgid "Close"
+msgstr "Sluiten"
+
msgid "Close user profile"
msgstr "Gebruikersprofiel sluiten"
+msgid "Contact support"
+msgstr "Ondersteuning contacteren"
+
msgid "Continue"
msgstr "Verder"
@@ -133,6 +145,9 @@ msgstr "Gevaarzone"
msgid "Dark"
msgstr "Donker"
+msgid "Dashboard"
+msgstr "Dashboard"
+
msgid "Delete"
msgstr "Verwijderen"
@@ -188,6 +203,9 @@ msgstr "Fout: Er is iets misgegaan!"
msgid "Europe"
msgstr "Europa"
+msgid "Feel free to reach out with any questions or issues you may have."
+msgstr "Voel je vrij om contact op te nemen met vragen of problemen die je hebt."
+
msgid "Fetching data..."
msgstr "Gegevens ophalen..."
@@ -197,9 +215,6 @@ msgstr "Filters"
msgid "First name"
msgstr "Voornaam"
-msgid "Help"
-msgstr "Help"
-
msgid "Here's your overview of what's happening."
msgstr "Hier is je overzicht van wat er gebeurt."
@@ -260,6 +275,9 @@ msgstr "Naam"
msgid "Navigation"
msgstr "Navigatie"
+msgid "Need help? Our support team is here to assist you."
+msgstr "Hulp nodig? Ons ondersteuningsteam staat voor je klaar."
+
msgid "Next"
msgstr "Volgende"
@@ -269,9 +287,15 @@ msgstr "OK"
msgid "Only account owners can modify the account name"
msgstr "Alleen accounteigenaren kunnen de accountnaam wijzigen"
+msgid "Open navigation menu"
+msgstr "Navigatiemenu openen"
+
msgid "Organization"
msgstr "Organisatie"
+msgid "Our support team will assist you with the account deletion process and ensure all your data is properly removed."
+msgstr "Ons ondersteuningsteam helpt je met het verwijderen van je account en al je gegevens."
+
msgid "Owner"
msgstr "Eigenaar"
@@ -326,9 +350,6 @@ msgstr "Opslaan..."
msgid "Screenshots of the dashboard project with desktop and mobile versions"
msgstr "Schermafbeeldingen van het dashboardproject met desktop en mobiele versies"
-msgid "Screenshots of the dashboard project with mobile versions"
-msgstr "Schermafbeeldingen van het dashboardproject met mobiele versies"
-
msgid "Search"
msgstr "Zoeken"
@@ -339,9 +360,6 @@ msgstr "Selecteer een nieuwe rol voor <0>{0}0>"
msgid "Select dates"
msgstr "Selecteer datums"
-msgid "Select language"
-msgstr "Selecteer taal"
-
msgid "Send invite"
msgstr "Uitnodiging verzenden"
@@ -363,9 +381,6 @@ msgstr "Verificatiecode voor aanmelding"
msgid "Success"
msgstr "Succes"
-msgid "Support"
-msgstr "Ondersteuning"
-
msgid "System"
msgstr "Systeem"
@@ -381,11 +396,11 @@ msgstr "Dit is de regio waar je gegevens zijn opgeslagen"
msgid "Title"
msgstr "Titel"
-msgid "Toggle collapsed menu"
-msgstr "Ingeklapt menu wisselen"
+msgid "To delete your account, please contact our support team."
+msgstr "Neem contact op met ons ondersteuningsteam om je account te verwijderen."
-msgid "Toggle theme"
-msgstr "Thema wisselen"
+msgid "Toggle sidebar"
+msgstr "Zijbalk wisselen"
msgid "Total users"
msgstr "Totale gebruikers"
@@ -469,9 +484,6 @@ msgstr "Welkom home"
msgid "Welcome home, {0}"
msgstr "Welkom home, {0}"
-msgid "You are about to permanently delete the account and the entire data environment via PlatformPlatform.<0/><1/>This action is permanent and irreversible."
-msgstr "Je staat op het punt om het account en de volledige gegevensomgeving permanent te verwijderen via PlatformPlatform.<0/><1/>Deze actie is definitief en onomkeerbaar."
-
msgid "Your verification code has expired"
msgstr "Je verificatiecode is verlopen"
diff --git a/application/back-office/WebApp/bootstrap.tsx b/application/back-office/WebApp/bootstrap.tsx
index ada41486d..9a26128c9 100644
--- a/application/back-office/WebApp/bootstrap.tsx
+++ b/application/back-office/WebApp/bootstrap.tsx
@@ -2,13 +2,13 @@ import "@repo/ui/tailwind.css";
import { router } from "@/shared/lib/router/router";
import { ApplicationInsightsProvider } from "@repo/infrastructure/applicationInsights/ApplicationInsightsProvider";
import { setupGlobalErrorHandlers } from "@repo/infrastructure/http/errorHandler";
-import { Translation } from "@repo/infrastructure/translations/Translation";
+import { createFederatedTranslation } from "@repo/infrastructure/translations/createFederatedTranslation";
import { GlobalToastRegion } from "@repo/ui/components/Toast";
import { RouterProvider } from "@tanstack/react-router";
import React from "react";
import reactDom from "react-dom/client";
-const { TranslationProvider } = await Translation.create(
+const { TranslationProvider } = await createFederatedTranslation(
(locale) => import(`@/shared/translations/locale/${locale}.ts`)
);
diff --git a/application/back-office/WebApp/routes/back-office/index.tsx b/application/back-office/WebApp/routes/back-office/index.tsx
index 897b58977..c798ffdd5 100644
--- a/application/back-office/WebApp/routes/back-office/index.tsx
+++ b/application/back-office/WebApp/routes/back-office/index.tsx
@@ -1,9 +1,8 @@
-import { SharedSideMenu } from "@/shared/components/SharedSideMenu";
import { TopMenu } from "@/shared/components/topMenu";
-import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { AppLayout } from "@repo/ui/components/AppLayout";
import { createFileRoute } from "@tanstack/react-router";
+import FederatedSideMenu from "account-management/FederatedSideMenu";
export const Route = createFileRoute("/back-office/")({
component: Home
@@ -12,7 +11,7 @@ export const Route = createFileRoute("/back-office/")({
export default function Home() {
return (
<>
-
+ }>
Welcome to the Back Office
diff --git a/application/back-office/WebApp/shared/components/SharedSideMenu.tsx b/application/back-office/WebApp/shared/components/SharedSideMenu.tsx
deleted file mode 100644
index 8c5df1006..000000000
--- a/application/back-office/WebApp/shared/components/SharedSideMenu.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { t } from "@lingui/core/macro";
-import { useUserInfo } from "@repo/infrastructure/auth/hooks";
-import { MenuButton, SideMenu, SideMenuSpacer } from "@repo/ui/components/SideMenu";
-import { BoxIcon, HomeIcon } from "lucide-react";
-import type React from "react";
-
-type SharedSideMenuProps = {
- children?: React.ReactNode;
- ariaLabel: string;
-};
-
-export function SharedSideMenu({ children, ariaLabel }: Readonly) {
- const userInfo = useUserInfo();
-
- return (
-
-
- {children}
-
-
-
-
-
- );
-}
diff --git a/application/back-office/WebApp/shared/components/topMenu/index.tsx b/application/back-office/WebApp/shared/components/topMenu/index.tsx
index 716c387a4..f3c0eff8e 100644
--- a/application/back-office/WebApp/shared/components/topMenu/index.tsx
+++ b/application/back-office/WebApp/shared/components/topMenu/index.tsx
@@ -1,14 +1,9 @@
-import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
-import { LocaleSwitcher } from "@repo/infrastructure/translations/LocaleSwitcher";
import { Breadcrumb, Breadcrumbs } from "@repo/ui/components/Breadcrumbs";
-import { Button } from "@repo/ui/components/Button";
-import { ThemeModeSelector } from "@repo/ui/theme/ThemeModeSelector";
-import { LifeBuoyIcon } from "lucide-react";
import type { ReactNode } from "react";
-import { lazy } from "react";
+import { Suspense, lazy } from "react";
-const AvatarButton = lazy(() => import("account-management/AvatarButton"));
+const FederatedTopMenu = lazy(() => import("account-management/FederatedTopMenu"));
interface TopMenuProps {
children?: ReactNode;
@@ -16,23 +11,15 @@ interface TopMenuProps {
export function TopMenu({ children }: Readonly) {
return (
-
+ }>
+
+
+
+ Home
+
+ {children}
+
+
+
);
}
diff --git a/application/back-office/WebApp/shared/translations/locale/da-DK.po b/application/back-office/WebApp/shared/translations/locale/da-DK.po
index 3665bf5c4..2f551a5f4 100644
--- a/application/back-office/WebApp/shared/translations/locale/da-DK.po
+++ b/application/back-office/WebApp/shared/translations/locale/da-DK.po
@@ -13,29 +13,11 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
-msgid "Account management"
-msgstr "Kontoadministration"
-
-msgid "Help"
-msgstr "Hjælp"
-
msgid "Home"
msgstr "Hjem"
msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams."
msgstr "Administrer lejere, se systemdata, se undtagelser og udfør forskellige opgaver for drifts- og supportteams."
-msgid "Select language"
-msgstr "Vælg sprog"
-
-msgid "Toggle collapsed menu"
-msgstr "Skift kollapset menu"
-
-msgid "Toggle theme"
-msgstr "Skift tema"
-
-msgid "User profile menu"
-msgstr "Brugerprofilmenu"
-
msgid "Welcome to the Back Office"
msgstr "Velkommen til Back Office"
diff --git a/application/back-office/WebApp/shared/translations/locale/en-US.po b/application/back-office/WebApp/shared/translations/locale/en-US.po
index 46e456001..b64e6f282 100644
--- a/application/back-office/WebApp/shared/translations/locale/en-US.po
+++ b/application/back-office/WebApp/shared/translations/locale/en-US.po
@@ -13,29 +13,11 @@ msgstr ""
"Language-Team: \n"
"Plural-Forms: \n"
-msgid "Account management"
-msgstr "Account management"
-
-msgid "Help"
-msgstr "Help"
-
msgid "Home"
msgstr "Home"
msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams."
msgstr "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams."
-msgid "Select language"
-msgstr "Select language"
-
-msgid "Toggle collapsed menu"
-msgstr "Toggle collapsed menu"
-
-msgid "Toggle theme"
-msgstr "Toggle theme"
-
-msgid "User profile menu"
-msgstr "User profile menu"
-
msgid "Welcome to the Back Office"
msgstr "Welcome to the Back Office"
diff --git a/application/back-office/WebApp/shared/translations/locale/nl-NL.po b/application/back-office/WebApp/shared/translations/locale/nl-NL.po
index 733610398..c43b1e885 100644
--- a/application/back-office/WebApp/shared/translations/locale/nl-NL.po
+++ b/application/back-office/WebApp/shared/translations/locale/nl-NL.po
@@ -13,29 +13,11 @@ msgstr ""
"Plural-Forms: \n"
"X-Generator: @lingui/cli\n"
-msgid "Account management"
-msgstr "Accountbeheer"
-
-msgid "Help"
-msgstr "Help"
-
msgid "Home"
msgstr "Home"
msgid "Manage tenants, view system data, see exceptions, and perform various tasks for operational and support teams."
msgstr "Beheer huurders, bekijk systeemgegevens, zie uitzonderingen en voer diverse taken uit voor operationele en ondersteuningsteams."
-msgid "Select language"
-msgstr "Selecteer taal"
-
-msgid "Toggle collapsed menu"
-msgstr "Ingeklapt menu wisselen"
-
-msgid "Toggle theme"
-msgstr "Schakel thema om"
-
-msgid "User profile menu"
-msgstr "Gebruikersprofielmenu"
-
msgid "Welcome to the Back Office"
msgstr "Welkom bij de Backoffice"
diff --git a/application/biome.json b/application/biome.json
index fc4b5ebbb..7985363d0 100644
--- a/application/biome.json
+++ b/application/biome.json
@@ -82,7 +82,7 @@
}
}
},
- "ignore": ["*/environment.d.ts", "*/tailwind-preset.ts"]
+ "ignore": ["*/environment.d.ts", "*/tailwind-preset.ts", "*/module-federation-types/*.d.ts"]
},
"files": {
"ignore": ["*.Api.json"]
diff --git a/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs b/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs
index c0068ab05..44f017dc0 100644
--- a/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs
+++ b/application/shared-kernel/SharedKernel/Authentication/UserInfo.cs
@@ -1,5 +1,6 @@
using System.Security.Claims;
using PlatformPlatform.SharedKernel.Domain;
+using PlatformPlatform.SharedKernel.Platform;
using PlatformPlatform.SharedKernel.SinglePageApp;
namespace PlatformPlatform.SharedKernel.Authentication;
@@ -18,7 +19,8 @@ public class UserInfo
public static readonly UserInfo System = new()
{
IsAuthenticated = false,
- Locale = DefaultLocale
+ Locale = DefaultLocale,
+ IsInternalUser = false
};
public bool IsAuthenticated { get; init; }
@@ -43,6 +45,8 @@ public class UserInfo
public string? TenantName { get; init; }
+ public bool IsInternalUser { get; init; }
+
public static UserInfo Create(ClaimsPrincipal? user, string? browserLocale)
{
if (user?.Identity?.IsAuthenticated != true)
@@ -50,25 +54,28 @@ public static UserInfo Create(ClaimsPrincipal? user, string? browserLocale)
return new UserInfo
{
IsAuthenticated = user?.Identity?.IsAuthenticated ?? false,
- Locale = GetValidLocale(browserLocale)
+ Locale = GetValidLocale(browserLocale),
+ IsInternalUser = false
};
}
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
var tenantId = user.FindFirstValue("tenant_id");
+ var email = user.FindFirstValue(ClaimTypes.Email);
return new UserInfo
{
IsAuthenticated = true,
Id = userId == null ? null : new UserId(userId),
TenantId = tenantId == null ? null : new TenantId(long.Parse(tenantId)),
Role = user.FindFirstValue(ClaimTypes.Role),
- Email = user.FindFirstValue(ClaimTypes.Email),
+ Email = email,
FirstName = user.FindFirstValue(ClaimTypes.GivenName),
LastName = user.FindFirstValue(ClaimTypes.Surname),
Title = user.FindFirstValue("title"),
AvatarUrl = user.FindFirstValue("avatar_url"),
TenantName = user.FindFirstValue("tenant_name"),
- Locale = GetValidLocale(user.FindFirstValue("locale"))
+ Locale = GetValidLocale(user.FindFirstValue("locale")),
+ IsInternalUser = IsInternalUserEmail(email)
};
}
@@ -91,4 +98,9 @@ private static string GetValidLocale(string? locale)
return foundLocale ?? DefaultLocale;
}
+
+ private static bool IsInternalUserEmail(string? email)
+ {
+ return email is not null && email.EndsWith(Settings.Current.Identity.InternalEmailDomain, StringComparison.OrdinalIgnoreCase);
+ }
}
diff --git a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs
index 8f07faf25..2b8bfa50e 100644
--- a/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs
+++ b/application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs
@@ -15,6 +15,7 @@
using PlatformPlatform.SharedKernel.Integrations.Email;
using PlatformPlatform.SharedKernel.Persistence;
using PlatformPlatform.SharedKernel.PipelineBehaviors;
+using PlatformPlatform.SharedKernel.Platform;
using PlatformPlatform.SharedKernel.Telemetry;
namespace PlatformPlatform.SharedKernel.Configuration;
@@ -39,6 +40,7 @@ public static IServiceCollection AddSharedServices(this IServiceCollection se
return services
.AddServiceDiscovery()
.AddSingleton(GetTokenSigningService())
+ .AddSingleton(Settings.Current)
.AddAuthentication()
.AddDefaultJsonSerializerOptions()
.AddPersistenceHelpers()
diff --git a/application/shared-kernel/SharedKernel/Platform/Settings.cs b/application/shared-kernel/SharedKernel/Platform/Settings.cs
new file mode 100644
index 000000000..eb5135e49
--- /dev/null
+++ b/application/shared-kernel/SharedKernel/Platform/Settings.cs
@@ -0,0 +1,44 @@
+using System.Text.Json;
+
+namespace PlatformPlatform.SharedKernel.Platform;
+
+public sealed class Settings
+{
+ private static readonly Lazy Instance = new(LoadFromEmbeddedResource);
+
+ public static Settings Current => Instance.Value;
+
+ public required IdentityConfig Identity { get; init; }
+
+ public required BrandingConfig Branding { get; init; }
+
+ private static Settings LoadFromEmbeddedResource()
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+ var resourceName = "PlatformPlatform.SharedKernel.Platform.platform-settings.jsonc";
+
+ using var stream = assembly.GetManifestResourceStream(resourceName)
+ ?? throw new InvalidOperationException($"Could not find embedded resource: {resourceName}");
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true,
+ ReadCommentHandling = JsonCommentHandling.Skip
+ };
+
+ return JsonSerializer.Deserialize(stream, options)
+ ?? throw new InvalidOperationException("Failed to deserialize platform settings");
+ }
+
+ public sealed class IdentityConfig
+ {
+ public required string InternalEmailDomain { get; init; }
+ }
+
+ public sealed class BrandingConfig
+ {
+ public required string ProductName { get; init; }
+
+ public required string SupportEmail { get; init; }
+ }
+}
diff --git a/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc b/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc
new file mode 100644
index 000000000..ed280d9b4
--- /dev/null
+++ b/application/shared-kernel/SharedKernel/Platform/platform-settings.jsonc
@@ -0,0 +1,27 @@
+{
+ // Platform-wide configuration settings
+ // This file is prepared for sharing between backend (.NET), frontend (TypeScript), and tests
+ //
+ // IMPORTANT: This configuration is embedded in the backend and injected at build time in the frontend
+ // Not all values are currently used - some are placeholders for future functionality
+ //
+ // Security Note: Only include non-sensitive configuration here
+ // Sensitive values (like API keys) should be stored in environment variables or key vaults
+
+ "identity": {
+ // Email domain suffix that identifies internal users
+ // Users with this domain get access to BackOffice and other internal features
+ // Currently used by backend only - frontend relies on isInternalUser flag from backend
+ "internalEmailDomain": "@platformplatform.net"
+ },
+
+ "branding": {
+ // Product/platform name used throughout the application
+ // Placeholder for future use - currently hardcoded in various places
+ "productName": "PlatformPlatform",
+
+ // Support email address for user inquiries
+ // Placeholder for future use - not currently referenced in the codebase
+ "supportEmail": "support@platformplatform.net"
+ }
+}
\ No newline at end of file
diff --git a/application/shared-kernel/SharedKernel/SharedKernel.csproj b/application/shared-kernel/SharedKernel/SharedKernel.csproj
index 7ce0d6a93..c1d87707c 100644
--- a/application/shared-kernel/SharedKernel/SharedKernel.csproj
+++ b/application/shared-kernel/SharedKernel/SharedKernel.csproj
@@ -57,4 +57,8 @@
+
+
+
+
diff --git a/application/shared-webapp/build/environment.d.ts b/application/shared-webapp/build/environment.d.ts
index ab4212b28..8bd9d5ce5 100644
--- a/application/shared-webapp/build/environment.d.ts
+++ b/application/shared-webapp/build/environment.d.ts
@@ -84,6 +84,10 @@ export declare global {
* Tenant name
**/
tenantName?: string;
+ /**
+ * Is internal user (has access to BackOffice)
+ **/
+ isInternalUser?: boolean;
}
/**
diff --git a/application/shared-webapp/build/module-federation-types/account-management.d.ts b/application/shared-webapp/build/module-federation-types/account-management.d.ts
index 137a5fe17..a84b904fc 100644
--- a/application/shared-webapp/build/module-federation-types/account-management.d.ts
+++ b/application/shared-webapp/build/module-federation-types/account-management.d.ts
@@ -1,5 +1,20 @@
// This file was auto-generated by the ModuleFederationPlugin
-declare module "account-management/AvatarButton" {
+declare module "account-management/FederatedSideMenu" {
export default ReactNode;
}
+declare module "account-management/FederatedTopMenu" {
+ export default ReactNode;
+}
+declare module "account-management/translations/en-US" {
+ import type { Messages } from "@lingui/core";
+ export const messages: Messages;
+}
+declare module "account-management/translations/da-DK" {
+ import type { Messages } from "@lingui/core";
+ export const messages: Messages;
+}
+declare module "account-management/translations/nl-NL" {
+ import type { Messages } from "@lingui/core";
+ export const messages: Messages;
+}
diff --git a/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts b/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts
index 8928a0431..b8529b83d 100644
--- a/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts
+++ b/application/shared-webapp/build/plugin/ModuleFederationPlugin.ts
@@ -18,7 +18,7 @@ const { dependencies } = require(applicationPackageJson);
const SYSTEM_ID = getSystemId();
type ModuleFederationPluginOptions = {
- exposes?: Record<`./${string}`, `./${string}.tsx` | `./${string}.ts`>;
+ exposes?: Record<`./${string}`, `./${string}.tsx` | `./${string}.ts` | `./${string}`>;
remotes?: Record;
};
@@ -109,6 +109,15 @@ function generateModuleFederationTypesFolder(system: string, exposes: Record {
logger.info(`[Module Federation] Expose: ${exportPath} => ${importPath}`);
+
+ // Pattern matching for different module types
+ // Translation files follow the pattern: ./translations/xx-XX (e.g., ./translations/en-US)
+ const translationPattern = /^\.\/translations\/[a-z]{2}-[A-Z]{2}$/;
+ if (translationPattern.test(exportPath)) {
+ return `declare module "${exportPath.replace(/^\./, system)}" {\n import type { Messages } from "@lingui/core";\n export const messages: Messages;\n}`;
+ }
+
+ // Default to ReactNode export for components
return `declare module "${exportPath.replace(/^\./, system)}" {\n export default ReactNode;\n}`;
})
.join("\n");
diff --git a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx b/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx
deleted file mode 100644
index e13e354a7..000000000
--- a/application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useLingui } from "@lingui/react";
-import type { Key } from "@react-types/shared";
-import { AuthenticationContext } from "@repo/infrastructure/auth/AuthenticationProvider";
-import { enhancedFetch } from "@repo/infrastructure/http/httpClient";
-import { Button } from "@repo/ui/components/Button";
-import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu";
-import { CheckIcon, GlobeIcon } from "lucide-react";
-import { use, useContext, useMemo } from "react";
-import { type Locale, translationContext } from "./TranslationContext";
-import { preferredLocaleKey } from "./constants";
-
-export function LocaleSwitcher({ "aria-label": ariaLabel }: { "aria-label": string }) {
- const { setLocale, getLocaleInfo, locales } = use(translationContext);
- const { i18n } = useLingui();
- const { userInfo } = useContext(AuthenticationContext);
-
- const items = useMemo(
- () =>
- locales.map((locale) => ({
- id: locale,
- label: getLocaleInfo(locale).label
- })),
- [locales, getLocaleInfo]
- );
-
- const handleLocaleChange = (key: Key) => {
- const locale = key.toString() as Locale;
- if (locale !== currentLocale) {
- if (userInfo?.isAuthenticated) {
- enhancedFetch("/api/account-management/users/me/change-locale", {
- method: "PUT",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ locale })
- }).then(async (_: Response) => {
- await setLocale(locale);
- localStorage.setItem(preferredLocaleKey, locale);
- });
- } else {
- setLocale(locale).then(() => {
- localStorage.setItem(preferredLocaleKey, locale);
- });
- }
- }
- };
-
- const currentLocale = i18n.locale as Locale;
-
- return (
-
-
-
-
-
-
- );
-}
diff --git a/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts b/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts
new file mode 100644
index 000000000..20fa98d29
--- /dev/null
+++ b/application/shared-webapp/infrastructure/translations/createFederatedTranslation.ts
@@ -0,0 +1,97 @@
+import type { Messages } from "@lingui/core";
+import { type Locale, type LocaleFile, Translation } from "./Translation";
+
+// Module federation container type
+type FederatedContainer = {
+ get(module: string): Promise<() => { messages: Messages }>;
+};
+
+// Cache for loaded translation modules
+const translationModuleCache = new Map();
+
+/**
+ * Configuration for federated translations
+ * Each application that consumes federated modules should configure
+ * which remotes might provide translations
+ */
+const FEDERATED_TRANSLATION_REMOTES = [
+ "account-management"
+ // Add more remotes here as they are created
+] as const;
+
+/**
+ * Creates a Translation instance that automatically loads and merges translations
+ * from all federated modules configured in the current application.
+ *
+ * This function:
+ * 1. Uses the base translation loader for the host application
+ * 2. Automatically discovers and loads translations from configured federated remotes
+ * 3. Merges all translations together with remote translations taking precedence
+ *
+ * @param baseLoader - Function to load base translations for the host application
+ * @returns Translation instance with federated translation support
+ */
+export function createFederatedTranslation(baseLoader: (locale: Locale) => Promise): Promise {
+ const federatedLoader = createFederatedLoader(baseLoader);
+ return Translation.create(federatedLoader);
+}
+
+/**
+ * Try to load translations from a federated module
+ */
+async function loadRemoteTranslations(remoteName: string, locale: Locale): Promise {
+ // Check cache first
+ const cacheKey = `${remoteName}:${locale}`;
+ const cached = translationModuleCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
+ // Get container using RSBuild's naming convention (hyphens to underscores)
+ const containerName = remoteName.replace(/-/g, "_");
+ const container = (window as unknown as Record)[containerName] as FederatedContainer | null;
+
+ if (!container?.get) {
+ return null;
+ }
+
+ try {
+ const factory = await container.get(`./translations/${locale}`);
+ const module = factory();
+
+ if (module?.messages) {
+ translationModuleCache.set(cacheKey, module.messages);
+ return module.messages;
+ }
+ } catch (_error) {
+ // Silently fail - the remote might not have translations for this locale
+ }
+
+ return null;
+}
+
+/**
+ * Creates a translation loader that merges translations from federated modules
+ */
+function createFederatedLoader(
+ baseLoader: (locale: Locale) => Promise
+): (locale: Locale) => Promise {
+ return async (locale: Locale): Promise => {
+ // Load base translations first
+ const baseMessages = await baseLoader(locale);
+
+ // Load and merge translations from all configured remotes
+ const allMessages = { ...baseMessages.messages };
+
+ await Promise.all(
+ FEDERATED_TRANSLATION_REMOTES.map(async (remoteName) => {
+ const remoteMessages = await loadRemoteTranslations(remoteName, locale);
+ if (remoteMessages) {
+ Object.assign(allMessages, remoteMessages);
+ }
+ })
+ );
+
+ return { messages: allMessages };
+ };
+}
diff --git a/application/shared-webapp/ui/components/SideMenu.tsx b/application/shared-webapp/ui/components/SideMenu.tsx
index 27e75c4f9..d1f3267d0 100644
--- a/application/shared-webapp/ui/components/SideMenu.tsx
+++ b/application/shared-webapp/ui/components/SideMenu.tsx
@@ -88,6 +88,10 @@ type MenuButtonProps = {
forceReload: true;
href: string;
}
+ | {
+ federatedNavigation: true;
+ href: string;
+ }
);
// Helper function to get target path from href
@@ -152,13 +156,9 @@ function ActiveIndicator({
);
}
-export function MenuButton({
- icon: Icon,
- label,
- href: to,
- isDisabled = false,
- forceReload = false
-}: Readonly) {
+export function MenuButton({ icon: Icon, label, href: to, isDisabled = false, ...props }: Readonly) {
+ const forceReload = "forceReload" in props ? props.forceReload : false;
+ const federatedNavigation = "federatedNavigation" in props ? props.federatedNavigation : false;
const isCollapsed = useContext(collapsedContext);
const overlayCtx = useContext(overlayContext);
const router = useRouter();
@@ -201,8 +201,25 @@ export function MenuButton({
overlayCtx.close();
}
- // Handle navigation for React Aria Link
- if (forceReload) {
+ // Smart navigation for federated modules
+ if (federatedNavigation) {
+ // Check if the target route exists in the current router
+ try {
+ const matchResult = router.matchRoute({ to });
+ if (matchResult !== false) {
+ // Route exists in current system - use SPA navigation
+ // Don't do anything, let React Aria handle the navigation
+ return;
+ }
+ } catch {
+ // Route doesn't exist in current system
+ }
+
+ // Route doesn't exist in current system - force reload
+ window.location.href = to;
+ }
+ // Legacy forceReload behavior
+ else if (forceReload) {
window.location.href = to;
}
};
@@ -214,7 +231,7 @@ export function MenuButton({
+
+
+
+
+
+ );
+ }
+
+ // For regular navigation, use TanStack Router Link
return (
@@ -249,6 +287,122 @@ export function MenuButton({
);
}
+// Federated menu button for module federation
+type FederatedMenuButtonProps = {
+ icon: LucideIcon;
+ label: string;
+ href: string;
+ isCurrentSystem: boolean;
+ isDisabled?: boolean;
+};
+
+export function FederatedMenuButton({
+ icon: Icon,
+ label,
+ href: to,
+ isCurrentSystem,
+ isDisabled = false
+}: Readonly) {
+ const isCollapsed = useContext(collapsedContext);
+ const overlayCtx = useContext(overlayContext);
+ const router = useRouter();
+
+ // Check if this menu item is active
+ const currentPath = router.state.location.pathname;
+ const targetPath = to;
+ const isActive = normalizePath(currentPath) === normalizePath(targetPath);
+
+ // Check if we're in the mobile menu context
+ const isMobileMenu = !window.matchMedia(MEDIA_QUERIES.sm).matches && !!overlayCtx?.isOpen;
+
+ const linkClassName = menuButtonStyles({ isCollapsed, isActive, isDisabled });
+
+ const handleClick = (e: React.MouseEvent) => {
+ if (isDisabled) {
+ e.preventDefault();
+ return;
+ }
+
+ // Auto-close overlay after navigation
+ if (overlayCtx?.isOpen) {
+ overlayCtx.close();
+ }
+
+ // Always prevent default to handle navigation ourselves
+ e.preventDefault();
+
+ if (isCurrentSystem) {
+ // Same system - use programmatic navigation
+ window.history.pushState({}, "", to);
+ // Dispatch a popstate event using the standard Event constructor
+ window.dispatchEvent(new Event("popstate"));
+ } else {
+ // Different system - force reload
+ window.location.href = to;
+ }
+ };
+
+ // For collapsed menu, wrap in TooltipTrigger
+ if (isCollapsed) {
+ return (
+
+
+
+ {
+ if (isDisabled) {
+ return;
+ }
+
+ // Auto-close overlay after navigation
+ if (overlayCtx?.isOpen) {
+ overlayCtx.close();
+ }
+
+ if (isCurrentSystem) {
+ // Same system - use programmatic navigation
+ window.history.pushState({}, "", to);
+ // Dispatch a popstate event using the standard Event constructor
+ window.dispatchEvent(new Event("popstate"));
+ } else {
+ // Different system - force reload
+ window.location.href = to;
+ }
+ }}
+ >
+
+
+
+ {label}
+
+
+
+ );
+ }
+
+ // For expanded menu, use a regular anchor tag with onClick handler
+ return (
+