Translate portal chrome and page strings

Replace the remaining hard-coded UI copy in the top bar, user menu, home
hero, and the documents/subprocessors/updates/requests pages with i18next
keys, and supply the en-US and fr-FR catalog entries for them. The home
hero title uses value interpolation rather than string concatenation so
word order stays correct across locales.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-28 14:04:31 +02:00
parent 86077e6c27
commit a5e0fb59e9
8 changed files with 84 additions and 14 deletions

View File

@@ -1,5 +1,32 @@
{ {
"topBar": {
"tagline": "Compliance Portal",
"getAccess": "Get Access",
"nav": {
"documents": "Documents",
"subprocessors": "Subprocessors",
"updates": "Updates",
"requests": "Requests"
}
},
"userMenu": {
"signOut": "Sign out"
},
"home": {
"heroTitle": "Trust at {{name}}."
},
"documents": { "documents": {
"title": "Documents" "title": "Documents"
},
"subprocessors": {
"title": "Subprocessors"
},
"updates": {
"title": "Updates",
"subscribe": "Subscribe to updates"
},
"requests": {
"title": "Data Requests",
"newRequest": "New Request"
} }
} }

View File

@@ -1,5 +1,32 @@
{ {
"topBar": {
"tagline": "Portail de conformité",
"getAccess": "Obtenir l'accès",
"nav": {
"documents": "Documents",
"subprocessors": "Sous-traitants",
"updates": "Mises à jour",
"requests": "Demandes"
}
},
"userMenu": {
"signOut": "Se déconnecter"
},
"home": {
"heroTitle": "La confiance chez {{name}}."
},
"documents": { "documents": {
"title": "Documents" "title": "Documents"
},
"subprocessors": {
"title": "Sous-traitants"
},
"updates": {
"title": "Mises à jour",
"subscribe": "S'abonner aux mises à jour"
},
"requests": {
"title": "Demandes de données",
"newRequest": "Nouvelle demande"
} }
} }

View File

@@ -17,6 +17,7 @@ import { Avatar } from "@probo/ui/src/v2/Avatar/Avatar";
import { Button } from "@probo/ui/src/v2/Button/Button"; import { Button } from "@probo/ui/src/v2/Button/Button";
import { Link } from "@probo/ui/src/v2/Button/Link"; import { Link } from "@probo/ui/src/v2/Button/Link";
import { Text } from "@probo/ui/src/v2/typography/Text"; import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import { Link as RouterLink, useLocation } from "react-router"; import { Link as RouterLink, useLocation } from "react-router";
@@ -25,10 +26,10 @@ import { TopBarUserMenu } from "./TopBarUserMenu";
import { topBar } from "./variants"; import { topBar } from "./variants";
const NAV_ITEMS = [ const NAV_ITEMS = [
{ to: "/documents", label: "Documents" }, { to: "/documents", labelKey: "topBar.nav.documents" },
{ to: "/subprocessors", label: "Subprocessors" }, { to: "/subprocessors", labelKey: "topBar.nav.subprocessors" },
{ to: "/updates", label: "Updates" }, { to: "/updates", labelKey: "topBar.nav.updates" },
{ to: "/requests", label: "Requests" }, { to: "/requests", labelKey: "topBar.nav.requests" },
] as const; ] as const;
const topBarFragment = graphql` const topBarFragment = graphql`
@@ -57,6 +58,7 @@ function isActive(pathname: string, to: string): boolean {
} }
export function TopBar({ queryKey }: TopBarProps) { export function TopBar({ queryKey }: TopBarProps) {
const { t } = useTranslation();
const data = useFragment(topBarFragment, queryKey); const data = useFragment(topBarFragment, queryKey);
const { pathname } = useLocation(); const { pathname } = useLocation();
@@ -83,7 +85,7 @@ export function TopBar({ queryKey }: TopBarProps) {
{organizationName} {organizationName}
</Text> </Text>
<Text size={2} color="neutral"> <Text size={2} color="neutral">
Compliance Portal {t("topBar.tagline")}
</Text> </Text>
</RouterLink> </RouterLink>
@@ -99,13 +101,13 @@ export function TopBar({ queryKey }: TopBarProps) {
size={2} size={2}
active={isActive(pathname, item.to)} active={isActive(pathname, item.to)}
> >
{item.label} {t(item.labelKey)}
</Link> </Link>
))} ))}
{data.viewer == null {data.viewer == null
? ( ? (
<Button variant="solid" color="neutral" highContrast iconStart={<LockSimpleIcon />}> <Button variant="solid" color="neutral" highContrast iconStart={<LockSimpleIcon />}>
Get Access {t("topBar.getAccess")}
</Button> </Button>
) )
: <TopBarUserMenu identityKey={data.viewer} />} : <TopBarUserMenu identityKey={data.viewer} />}

View File

@@ -21,6 +21,7 @@ import { DropdownPopup } from "@probo/ui/src/v2/Dropdown/DropdownPopup";
import { DropdownSeparator } from "@probo/ui/src/v2/Dropdown/DropdownSeparator"; import { DropdownSeparator } from "@probo/ui/src/v2/Dropdown/DropdownSeparator";
import { DropdownTrigger } from "@probo/ui/src/v2/Dropdown/DropdownTrigger"; import { DropdownTrigger } from "@probo/ui/src/v2/Dropdown/DropdownTrigger";
import { Text } from "@probo/ui/src/v2/typography/Text"; import { Text } from "@probo/ui/src/v2/typography/Text";
import { useTranslation } from "react-i18next";
import { graphql, useFragment } from "react-relay"; import { graphql, useFragment } from "react-relay";
import type { TopBarUserMenu_identity$key } from "./__generated__/TopBarUserMenu_identity.graphql"; import type { TopBarUserMenu_identity$key } from "./__generated__/TopBarUserMenu_identity.graphql";
@@ -38,6 +39,7 @@ interface TopBarUserMenuProps {
} }
export function TopBarUserMenu({ identityKey }: TopBarUserMenuProps) { export function TopBarUserMenu({ identityKey }: TopBarUserMenuProps) {
const { t } = useTranslation();
const identity = useFragment(topBarUserMenuFragment, identityKey); const identity = useFragment(topBarUserMenuFragment, identityKey);
return ( return (
@@ -63,7 +65,7 @@ export function TopBarUserMenu({ identityKey }: TopBarUserMenuProps) {
<DropdownGroupLabel>{identity.email}</DropdownGroupLabel> <DropdownGroupLabel>{identity.email}</DropdownGroupLabel>
<DropdownSeparator /> <DropdownSeparator />
<DropdownItem color="error" iconStart={<SignOutIcon />}> <DropdownItem color="error" iconStart={<SignOutIcon />}>
Sign out {t("userMenu.signOut")}
</DropdownItem> </DropdownItem>
</DropdownPopup> </DropdownPopup>
</Dropdown> </Dropdown>

View File

@@ -12,6 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import { useTranslation } from "react-i18next";
import type { PreloadedQuery } from "react-relay"; import type { PreloadedQuery } from "react-relay";
import { graphql, usePreloadedQuery } from "react-relay"; import { graphql, usePreloadedQuery } from "react-relay";
@@ -37,11 +38,15 @@ interface HomePageProps {
} }
export function HomePage({ queryRef }: HomePageProps) { export function HomePage({ queryRef }: HomePageProps) {
const { t } = useTranslation();
const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef); const data = usePreloadedQuery<HomePageQuery>(homePageQuery, queryRef);
const { organization } = data.currentTrustCenter; const { organization } = data.currentTrustCenter;
return ( return (
<Hero title={`Trust at ${organization.name}.`} description={organization.description}> <Hero
title={t("home.heroTitle", { name: organization.name })}
description={organization.description}
>
<OrganizationContactInfo organizationKey={organization} /> <OrganizationContactInfo organizationKey={organization} />
</Hero> </Hero>
); );

View File

@@ -14,16 +14,18 @@
import { PlusIcon } from "@phosphor-icons/react"; import { PlusIcon } from "@phosphor-icons/react";
import { Button } from "@probo/ui/src/v2/Button/Button"; import { Button } from "@probo/ui/src/v2/Button/Button";
import { useTranslation } from "react-i18next";
import { PageHeader } from "#/components/PageHeader/PageHeader"; import { PageHeader } from "#/components/PageHeader/PageHeader";
export default function RequestsPage() { export default function RequestsPage() {
const { t } = useTranslation();
return ( return (
<PageHeader <PageHeader
title="Data Requests" title={t("requests.title")}
actions={( actions={(
<Button variant="soft" color="neutral" highContrast iconStart={<PlusIcon />}> <Button variant="soft" color="neutral" highContrast iconStart={<PlusIcon />}>
New Request {t("requests.newRequest")}
</Button> </Button>
)} )}
/> />

View File

@@ -12,10 +12,13 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE. // PERFORMANCE OF THIS SOFTWARE.
import { useTranslation } from "react-i18next";
import { PageHeader } from "#/components/PageHeader/PageHeader"; import { PageHeader } from "#/components/PageHeader/PageHeader";
// Toolbar (category/region filters, search, Download CSV) and the subprocessor // Toolbar (category/region filters, search, Download CSV) and the subprocessor
// count are deferred until the v2 Select/TextField components exist. // count are deferred until the v2 Select/TextField components exist.
export default function SubprocessorsPage() { export default function SubprocessorsPage() {
return <PageHeader title="Subprocessors" />; const { t } = useTranslation();
return <PageHeader title={t("subprocessors.title")} />;
} }

View File

@@ -14,16 +14,18 @@
import { BellIcon } from "@phosphor-icons/react"; import { BellIcon } from "@phosphor-icons/react";
import { Button } from "@probo/ui/src/v2/Button/Button"; import { Button } from "@probo/ui/src/v2/Button/Button";
import { useTranslation } from "react-i18next";
import { PageHeader } from "#/components/PageHeader/PageHeader"; import { PageHeader } from "#/components/PageHeader/PageHeader";
export default function UpdatesPage() { export default function UpdatesPage() {
const { t } = useTranslation();
return ( return (
<PageHeader <PageHeader
title="Updates" title={t("updates.title")}
actions={( actions={(
<Button variant="soft" color="neutral" highContrast iconStart={<BellIcon />}> <Button variant="soft" color="neutral" highContrast iconStart={<BellIcon />}>
Subscribe to updates {t("updates.subscribe")}
</Button> </Button>
)} )}
/> />