diff --git a/apps/login/src/components/privacy-policy-checkboxes.test.tsx b/apps/login/src/components/privacy-policy-checkboxes.test.tsx new file mode 100644 index 000000000..ab73e3cb4 --- /dev/null +++ b/apps/login/src/components/privacy-policy-checkboxes.test.tsx @@ -0,0 +1,37 @@ +import { cleanup, render, screen } from "@testing-library/react"; +import { create } from "@zitadel/client"; +import { LegalAndSupportSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; +import type { AnchorHTMLAttributes } from "react"; +import { afterEach, describe, expect, test, vi } from "vitest"; +import { PrivacyPolicyCheckboxes } from "./privacy-policy-checkboxes"; + +vi.mock("next-intl", () => ({ + useLocale: () => "fr", + useTranslations: () => (key: string) => key, +})); + +vi.mock("next/link", () => ({ + default: ({ href, children, ...props }: AnchorHTMLAttributes & { href: string }) => ( + + {children} + + ), +})); + +describe("PrivacyPolicyCheckboxes", () => { + afterEach(cleanup); + + test("resolves language placeholders in legal links with the active locale", () => { + const legal = create(LegalAndSupportSettingsSchema, { + tosLink: "https://demo.com/tos-{{.Lang}}", + privacyPolicyLink: "https://demo.com/privacy-{{.Lang}}", + helpLink: "https://demo.com/help-{{.Lang}}", + }); + + render(); + + expect(screen.getByTestId("tos-link")).toHaveAttribute("href", "https://demo.com/tos-fr"); + expect(screen.getByTestId("privacy-policy-link")).toHaveAttribute("href", "https://demo.com/privacy-fr"); + expect(screen.getByTestId("help-link")).toHaveAttribute("href", "https://demo.com/help-fr"); + }); +}); diff --git a/apps/login/src/components/privacy-policy-checkboxes.tsx b/apps/login/src/components/privacy-policy-checkboxes.tsx index 597b7b623..0998ad54e 100644 --- a/apps/login/src/components/privacy-policy-checkboxes.tsx +++ b/apps/login/src/components/privacy-policy-checkboxes.tsx @@ -1,5 +1,7 @@ "use client"; +import { resolveLocalizedLegalLink } from "@/lib/legal-links"; import { LegalAndSupportSettings } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; +import { useLocale } from "next-intl"; import Link from "next/link"; import { useState } from "react"; import { Checkbox } from "./checkbox"; @@ -16,15 +18,19 @@ type AcceptanceState = { }; export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { + const locale = useLocale(); const [acceptanceState, setAcceptanceState] = useState({ tosAccepted: false, privacyPolicyAccepted: false, }); + const helpLink = resolveLocalizedLegalLink(legal?.helpLink, locale); + const tosLink = resolveLocalizedLegalLink(legal?.tosLink, locale); + const privacyPolicyLink = resolveLocalizedLegalLink(legal?.privacyPolicyLink, locale); // Helper function to check if all required checkboxes are accepted const checkAllAccepted = (newState: AcceptanceState) => { - const hasTosLink = !!legal?.tosLink; - const hasPrivacyLink = !!legal?.privacyPolicyLink; + const hasTosLink = !!tosLink; + const hasPrivacyLink = !!privacyPolicyLink; // Check that all required checkboxes are accepted return (!hasTosLink || newState.tosAccepted) && (!hasPrivacyLink || newState.privacyPolicyAccepted); @@ -34,9 +40,9 @@ export function PrivacyPolicyCheckboxes({ legal, onChange }: Props) { <>

- {legal?.helpLink && ( + {helpLink && ( - + )}

- {legal?.tosLink && ( + {tosLink && (

- +

)} - {legal?.privacyPolicyLink && ( + {privacyPolicyLink && (

- +

diff --git a/apps/login/src/lib/legal-links.test.ts b/apps/login/src/lib/legal-links.test.ts new file mode 100644 index 000000000..45be86ff9 --- /dev/null +++ b/apps/login/src/lib/legal-links.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { resolveLocalizedLegalLink } from "./legal-links"; + +describe("resolveLocalizedLegalLink", () => { + it("replaces the language placeholder with the active locale", () => { + expect(resolveLocalizedLegalLink("https://example.com/{{.Lang}}/terms", "de")).toBe("https://example.com/de/terms"); + }); + + it("replaces every language placeholder in the link", () => { + expect(resolveLocalizedLegalLink("https://example.com/{{.Lang}}/docs?lang={{.Lang}}", "fr")).toBe( + "https://example.com/fr/docs?lang=fr", + ); + }); + + it("returns the original link when no placeholder is present", () => { + expect(resolveLocalizedLegalLink("https://example.com/terms", "en")).toBe("https://example.com/terms"); + }); + + it("returns the original link when locale is missing", () => { + expect(resolveLocalizedLegalLink("https://example.com/{{.Lang}}/terms", undefined)).toBe( + "https://example.com/{{.Lang}}/terms", + ); + }); + + it("returns undefined when the link is missing", () => { + expect(resolveLocalizedLegalLink(undefined, "en")).toBeUndefined(); + }); +}); diff --git a/apps/login/src/lib/legal-links.ts b/apps/login/src/lib/legal-links.ts new file mode 100644 index 000000000..f8bc5ab7d --- /dev/null +++ b/apps/login/src/lib/legal-links.ts @@ -0,0 +1,9 @@ +const LANGUAGE_PLACEHOLDER = "{{.Lang}}"; + +export function resolveLocalizedLegalLink(link: string | undefined, locale: string | undefined): string | undefined { + if (!link || !locale) { + return link; + } + + return link.replaceAll(LANGUAGE_PLACEHOLDER, locale); +}