mirror of
https://github.com/andwati/zitadel.git
synced 2026-11-12 00:55:12 +00:00
feat(login): translations (#10849)
# Which Problems Are Solved Replace this example text with a concise list of problems that this PR solves. For example: - password complexity requirements have hardcoded English text - password, loginname, register and verify components have hardcoded Engilsh error messages/alerts # How the Problems Are Solved Replace this example text with a concise list of changes that this PR introduces. For example: - adds i18n for password complexity requirements - adds i18n for password, loginname, register and verify components error messages/alerts # Additional Changes - small change in code/styles for icons in PasswordComplexity to make sure that icons keep size # Additional Context N.A --------- Co-authored-by: Adam Kida <[email protected]>
This commit is contained in:
co-authored by
Adam Kida
parent
794054c075
commit
64e14de091
@@ -70,7 +70,7 @@ export function ChangePasswordForm({
|
||||
password: values.password,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not change password");
|
||||
setError(t("change.errors.couldNotChangePassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -81,13 +81,13 @@ export function ChangePasswordForm({
|
||||
setError(
|
||||
typeof changeResponse.error === "string"
|
||||
? changeResponse.error
|
||||
: "Unknown error",
|
||||
: t("change.errors.unknownError")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changeResponse) {
|
||||
setError("Could not change password");
|
||||
setError(t("change.errors.couldNotChangePassword"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export function ChangePasswordForm({
|
||||
requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not verify password");
|
||||
setError(t("change.errors.couldNotVerifyPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -1,64 +1,148 @@
|
||||
import {
|
||||
cleanup,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, test } from "vitest";
|
||||
import { PasswordComplexity } from "./password-complexity";
|
||||
|
||||
const matchesTitle = `Matches`;
|
||||
const doesntMatchTitle = `Doesn't match`;
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
|
||||
describe("<PasswordComplexity/>", () => {
|
||||
describe.each`
|
||||
settingsMinLength | password | expectSVGTitle
|
||||
${5} | ${"Password1!"} | ${matchesTitle}
|
||||
${30} | ${"Password1!"} | ${doesntMatchTitle}
|
||||
${0} | ${"Password1!"} | ${matchesTitle}
|
||||
${undefined} | ${"Password1!"} | ${false}
|
||||
`(
|
||||
`With settingsMinLength=$settingsMinLength, password=$password, expectSVGTitle=$expectSVGTitle`,
|
||||
({ settingsMinLength, password, expectSVGTitle }) => {
|
||||
const feedbackElementLabel = /password length/i;
|
||||
beforeEach(() => {
|
||||
render(
|
||||
<PasswordComplexity
|
||||
password={password}
|
||||
equals
|
||||
passwordComplexitySettings={{
|
||||
minLength: settingsMinLength,
|
||||
const messages = {
|
||||
password: {
|
||||
complexity: {
|
||||
length: "Must be at least {minLength} characters long.",
|
||||
hasSymbol: "Must include a symbol.",
|
||||
hasNumber: "Must include a number.",
|
||||
hasUppercase: "Must include an uppercase letter.",
|
||||
hasLowercase: "Must include a lowercase letter.",
|
||||
equals: "Password confirmation matched.",
|
||||
matches: "Matches",
|
||||
doesNotMatch: "Doesn't match",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
test("should render length check when minLength is defined", () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<PasswordComplexity
|
||||
password="Password1!"
|
||||
equals
|
||||
passwordComplexitySettings={
|
||||
{
|
||||
minLength: BigInt(5),
|
||||
requiresLowercase: false,
|
||||
requiresUppercase: false,
|
||||
requiresNumber: false,
|
||||
requiresSymbol: false,
|
||||
resourceOwnerType: 0, // ResourceOwnerType.RESOURCE_OWNER_TYPE_UNSPECIFIED,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
afterEach(cleanup);
|
||||
resourceOwnerType: 0,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
if (expectSVGTitle === false) {
|
||||
test(`should not render the feedback element`, async () => {
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByText(feedbackElementLabel),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
test(`Should show one SVG with title ${expectSVGTitle}`, async () => {
|
||||
await waitFor(async () => {
|
||||
const svg = within(
|
||||
screen.getByText(feedbackElementLabel)
|
||||
.parentElement as HTMLElement,
|
||||
).findByRole("img");
|
||||
expect(await svg).toHaveTextContent(expectSVGTitle);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
const lengthCheck = screen.getByTestId("length-check");
|
||||
expect(lengthCheck).toBeInTheDocument();
|
||||
expect(lengthCheck.querySelector("svg")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should not render length check when minLength is undefined", () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<PasswordComplexity
|
||||
password="Password1!"
|
||||
equals
|
||||
passwordComplexitySettings={
|
||||
{
|
||||
minLength: BigInt(0),
|
||||
requiresLowercase: false,
|
||||
requiresUppercase: false,
|
||||
requiresNumber: false,
|
||||
requiresSymbol: false,
|
||||
resourceOwnerType: 0,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("length-check")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should render check icon when password meets length requirement", () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<PasswordComplexity
|
||||
password="Password1!"
|
||||
equals
|
||||
passwordComplexitySettings={
|
||||
{
|
||||
minLength: BigInt(5),
|
||||
requiresLowercase: false,
|
||||
requiresUppercase: false,
|
||||
requiresNumber: false,
|
||||
requiresSymbol: false,
|
||||
resourceOwnerType: 0,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
const lengthCheck = screen.getByTestId("length-check");
|
||||
const svg = lengthCheck.querySelector("svg");
|
||||
expect(svg).toHaveClass("text-green-500");
|
||||
});
|
||||
|
||||
test("should render cross icon when password does not meet length requirement", () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<PasswordComplexity
|
||||
password="Pass"
|
||||
equals
|
||||
passwordComplexitySettings={
|
||||
{
|
||||
minLength: BigInt(10),
|
||||
requiresLowercase: false,
|
||||
requiresUppercase: false,
|
||||
requiresNumber: false,
|
||||
requiresSymbol: false,
|
||||
resourceOwnerType: 0,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
const lengthCheck = screen.getByTestId("length-check");
|
||||
const svg = lengthCheck.querySelector("svg");
|
||||
expect(svg).toHaveClass("text-warn-light-500");
|
||||
});
|
||||
|
||||
test("should render all complexity checks", () => {
|
||||
render(
|
||||
<NextIntlClientProvider locale="en" messages={messages}>
|
||||
<PasswordComplexity
|
||||
password="Password1!"
|
||||
equals
|
||||
passwordComplexitySettings={
|
||||
{
|
||||
minLength: BigInt(8),
|
||||
requiresLowercase: true,
|
||||
requiresUppercase: true,
|
||||
requiresNumber: true,
|
||||
requiresSymbol: true,
|
||||
resourceOwnerType: 0,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("length-check")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("symbol-check")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("number-check")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("uppercase-check")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("lowercase-check")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
upperCaseValidator,
|
||||
} from "@/helpers/validators";
|
||||
import { PasswordComplexitySettings } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Translated } from "@/components/translated";
|
||||
|
||||
type Props = {
|
||||
passwordComplexitySettings: PasswordComplexitySettings;
|
||||
@@ -12,42 +14,55 @@ type Props = {
|
||||
equals: boolean;
|
||||
};
|
||||
|
||||
const check = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="las la-check mr-2 h-6 w-6 text-lg text-green-500 dark:text-green-500"
|
||||
role="img"
|
||||
>
|
||||
<title>Matches</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
const cross = (
|
||||
<svg
|
||||
className="las la-times mr-2 h-6 w-6 text-lg text-warn-light-500 dark:text-warn-dark-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
role="img"
|
||||
>
|
||||
<title>Doesn't match</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
function CheckIcon({ title }: { title: string }) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="las la-check mr-2 h-6 w-6 flex-none text-lg text-green-500 dark:text-green-500"
|
||||
role="img"
|
||||
>
|
||||
<title>{title}</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function CrossIcon({ title }: { title: string }) {
|
||||
return (
|
||||
<svg
|
||||
className="las la-times mr-2 h-6 w-6 flex-none text-lg text-warn-light-500 dark:text-warn-dark-500"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
role="img"
|
||||
>
|
||||
<title>{title}</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function renderIcon(matched: boolean, t: ReturnType<typeof useTranslations>) {
|
||||
return matched ? (
|
||||
<CheckIcon title={t("complexity.matches")} />
|
||||
) : (
|
||||
<CrossIcon title={t("complexity.doesNotMatch")} />
|
||||
);
|
||||
}
|
||||
const desc =
|
||||
"text-14px leading-4 text-input-light-label dark:text-input-dark-label";
|
||||
|
||||
@@ -56,6 +71,7 @@ export function PasswordComplexity({
|
||||
password,
|
||||
equals,
|
||||
}: Props) {
|
||||
const t = useTranslations("password");
|
||||
const hasMinLength = password?.length >= passwordComplexitySettings.minLength;
|
||||
const hasSymbol = symbolValidator(password);
|
||||
const hasNumber = numberValidator(password);
|
||||
@@ -66,33 +82,47 @@ export function PasswordComplexity({
|
||||
<div className="mb-4 grid grid-cols-2 gap-x-8 gap-y-2">
|
||||
{passwordComplexitySettings.minLength != undefined ? (
|
||||
<div className="flex flex-row items-center" data-testid="length-check">
|
||||
{hasMinLength ? check : cross}
|
||||
{renderIcon(hasMinLength, t)}
|
||||
<span className={desc}>
|
||||
Password length {passwordComplexitySettings.minLength.toString()}
|
||||
<Translated
|
||||
i18nKey="complexity.length"
|
||||
namespace="password"
|
||||
data={{ minLength: passwordComplexitySettings.minLength.toString() }}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<div className="flex flex-row items-center" data-testid="symbol-check">
|
||||
{hasSymbol ? check : cross}
|
||||
<span className={desc}>has Symbol</span>
|
||||
{renderIcon(hasSymbol, t)}
|
||||
<span className={desc}>
|
||||
<Translated i18nKey="complexity.hasSymbol" namespace="password" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center" data-testid="number-check">
|
||||
{hasNumber ? check : cross}
|
||||
<span className={desc}>has Number</span>
|
||||
{renderIcon(hasNumber, t)}
|
||||
<span className={desc}>
|
||||
<Translated i18nKey="complexity.hasNumber" namespace="password" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center" data-testid="uppercase-check">
|
||||
{hasUppercase ? check : cross}
|
||||
<span className={desc}>has uppercase</span>
|
||||
{renderIcon(hasUppercase, t)}
|
||||
<span className={desc}>
|
||||
<Translated i18nKey="complexity.hasUppercase" namespace="password" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center" data-testid="lowercase-check">
|
||||
{hasLowercase ? check : cross}
|
||||
<span className={desc}>has lowercase</span>
|
||||
{renderIcon(hasLowercase, t)}
|
||||
<span className={desc}>
|
||||
<Translated i18nKey="complexity.hasLowercase" namespace="password" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center" data-testid="equal-check">
|
||||
{equals ? check : cross}
|
||||
<span className={desc}>equals</span>
|
||||
{renderIcon(equals, t)}
|
||||
<span className={desc}>
|
||||
<Translated i18nKey="complexity.equals" namespace="password" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -52,9 +52,8 @@ export function PasswordForm({ loginSettings, loginName, organization, requestId
|
||||
}),
|
||||
requestId,
|
||||
})
|
||||
.catch((error) => {
|
||||
setError("Could not verify password");
|
||||
console.error("Error verifying password:", error);
|
||||
.catch(() => {
|
||||
setError(t("verify.errors.couldNotVerifyPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -82,7 +81,7 @@ export function PasswordForm({ loginSettings, loginName, organization, requestId
|
||||
requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not reset password");
|
||||
setError(t("verify.errors.couldNotResetPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -94,7 +93,7 @@ export function PasswordForm({ loginSettings, loginName, organization, requestId
|
||||
return;
|
||||
}
|
||||
|
||||
setInfo("Password was reset. Please check your email.");
|
||||
setInfo(t("verify.info.passwordResetSent"));
|
||||
|
||||
const params = new URLSearchParams({
|
||||
loginName: loginName,
|
||||
|
||||
@@ -72,7 +72,7 @@ export function RegisterForm({
|
||||
requestId: requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not register user");
|
||||
setError(t("errors.couldNotRegisterUser"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export function SetPasswordForm({
|
||||
requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not reset password");
|
||||
setError(t("set.errors.couldNotResetPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -104,7 +104,7 @@ export function SetPasswordForm({
|
||||
|
||||
const changeResponse = await changePassword(payload)
|
||||
.catch(() => {
|
||||
setError("Could not set password");
|
||||
setError(t("set.errors.couldNotSetPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -117,7 +117,7 @@ export function SetPasswordForm({
|
||||
}
|
||||
|
||||
if (!changeResponse) {
|
||||
setError("Could not set password");
|
||||
setError(t("set.errors.couldNotSetPassword"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ export function SetPasswordForm({
|
||||
requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not verify password");
|
||||
setError(t("set.errors.couldNotVerifyPassword"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -71,7 +71,7 @@ export function SetRegisterPasswordForm({
|
||||
password: values.password,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not register user");
|
||||
setError(t("errors.couldNotRegisterUser"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -60,7 +60,7 @@ export function UsernameForm({
|
||||
suffix,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("An internal error occurred");
|
||||
setError(t("errors.internalError"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function VerifyForm({
|
||||
isInvite: isInvite,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not resend email");
|
||||
setError(t("errors.couldNotResendEmail"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -87,7 +87,7 @@ export function VerifyForm({
|
||||
requestId: requestId,
|
||||
})
|
||||
.catch(() => {
|
||||
setError("Could not verify user");
|
||||
setError(t("errors.couldNotVerifyUser"));
|
||||
return;
|
||||
})
|
||||
.finally(() => {
|
||||
|
||||
Reference in New Issue
Block a user