feat: add internationalization support for auth components and pages, including translations for Amharic and English

This commit is contained in:
estifanos
2026-08-14 07:54:30 +00:00
parent d71483be4a
commit e0b3a4887a
8 changed files with 412 additions and 109 deletions

View File

@@ -2,20 +2,32 @@ import { Stack, Text, Group } from '@mantine/core';
import { IconCheck, IconX } from '@tabler/icons-react';
import { z } from 'zod';
export function passwordRules(minLength: number) {
const DEFAULT_LABELS = (minLength: number) => [
`At least ${minLength} characters`,
'One lowercase letter',
'One uppercase letter',
'One number',
'One special character',
];
/** `labels`, when given, overrides the default English text in the same
* order — the caller's translated strings, so this stays a plain function
* usable from a zod schema with no i18n context of its own. */
export function passwordRules(minLength: number, labels?: string[]) {
const text = labels ?? DEFAULT_LABELS(minLength);
return [
{ label: `At least ${minLength} characters`, test: (p: string) => p.length >= minLength },
{ label: 'One lowercase letter', test: (p: string) => /[a-z]/.test(p) },
{ label: 'One uppercase letter', test: (p: string) => /[A-Z]/.test(p) },
{ label: 'One number', test: (p: string) => /\d/.test(p) },
{ label: 'One special character', test: (p: string) => /[^A-Za-z0-9]/.test(p) },
{ label: text[0], test: (p: string) => p.length >= minLength },
{ label: text[1], test: (p: string) => /[a-z]/.test(p) },
{ label: text[2], test: (p: string) => /[A-Z]/.test(p) },
{ label: text[3], test: (p: string) => /\d/.test(p) },
{ label: text[4], test: (p: string) => /[^A-Za-z0-9]/.test(p) },
];
}
/** Zod field schema enforcing every rule; unmet rules surface as separate issues. */
export const passwordSchema = (minLength: number) =>
export const passwordSchema = (minLength: number, labels?: string[]) =>
z.string().superRefine((val, ctx) => {
for (const rule of passwordRules(minLength)) {
for (const rule of passwordRules(minLength, labels)) {
if (!rule.test(val)) {
ctx.addIssue({ code: 'custom', message: rule.label });
}
@@ -34,10 +46,10 @@ interface PasswordRequirementsProps {
/** Live checklist of password requirements, ticking off as the user types. Hidden until the user starts typing. */
export function PasswordRequirements({ password, minLength, labels }: PasswordRequirementsProps) {
if (!password) return null;
const rules = passwordRules(minLength);
const rules = passwordRules(minLength, labels);
return (
<Stack gap={6} mt={6}>
{rules.map((rule, i) => {
{rules.map((rule) => {
const met = rule.test(password);
return (
<Group key={rule.label} gap={6} wrap="nowrap">
@@ -47,7 +59,7 @@ export function PasswordRequirements({ password, minLength, labels }: PasswordRe
<IconX size={14} color="var(--mantine-color-gray-5)" />
)}
<Text fz="xs" c={met ? 'teal' : 'dimmed'}>
{labels?.[i] ?? rule.label}
{rule.label}
</Text>
</Group>
);