Phase 6: Translate User Pages - exchange, trades, invites, profile

- Expand exchange.json with all exchange page strings (page, steps, detailsStep, bookingStep, confirmationStep, priceDisplay)
- Create trades.json translation files for es, en, ca
- Create invites.json translation files for es, en, ca
- Create profile.json translation files for es, en, ca
- Translate exchange page and all components (ExchangeDetailsStep, BookingStep, ConfirmationStep, StepIndicator, PriceDisplay)
- Translate trades page (titles, sections, buttons, status labels)
- Translate invites page (titles, sections, status badges, copy button)
- Translate profile page (form labels, hints, placeholders, messages)
- Update IntlProvider to load all new namespaces
- All frontend tests passing
This commit is contained in:
counterweight 2025-12-25 22:19:13 +01:00
parent 7dd13292a0
commit 246553c402
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
22 changed files with 559 additions and 115 deletions

View file

@ -11,11 +11,13 @@ import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import { Permission } from "../auth-context";
import { cardStyles, typographyStyles, buttonStyles } from "../styles/shared";
import { useTranslation } from "../hooks/useTranslation";
// Use generated type from OpenAPI schema
type Invite = components["schemas"]["UserInviteResponse"];
export default function InvitesPage() {
const t = useTranslation("invites");
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredPermission: Permission.VIEW_OWN_INVITES,
fallbackRedirect: "/admin/trades",
@ -62,26 +64,21 @@ export default function InvitesPage() {
>
<div style={styles.pageCard}>
<div style={cardStyles.cardHeader}>
<h1 style={cardStyles.cardTitle}>My Invites</h1>
<p style={cardStyles.cardSubtitle}>
Share your invite codes with friends to let them join
</p>
<h1 style={cardStyles.cardTitle}>{t("page.title")}</h1>
<p style={cardStyles.cardSubtitle}>{t("page.subtitle")}</p>
</div>
{(invites?.length ?? 0) === 0 ? (
<EmptyState
message="You don't have any invites yet."
hint="Contact an admin if you need invite codes to share."
/>
<EmptyState message={t("page.noInvites")} hint={t("page.noInvitesHint")} />
) : (
<div style={styles.sections}>
{/* Ready Invites */}
{readyInvites.length > 0 && (
<div style={styles.section}>
<h2 style={typographyStyles.sectionTitle}>Available ({readyInvites.length})</h2>
<p style={typographyStyles.sectionHint}>
Share these links with people you want to invite
</p>
<h2 style={typographyStyles.sectionTitle}>
{t("page.available", { count: readyInvites.length })}
</h2>
<p style={typographyStyles.sectionHint}>{t("page.availableHint")}</p>
<div style={styles.inviteList}>
{readyInvites.map((invite) => (
<div key={invite.id} style={styles.inviteCard}>
@ -91,7 +88,7 @@ export default function InvitesPage() {
onClick={() => copyToClipboard(invite)}
style={buttonStyles.accentButton}
>
{copiedId === invite.id ? "Copied!" : "Copy Link"}
{copiedId === invite.id ? t("page.copied") : t("page.copyLink")}
</button>
</div>
</div>
@ -103,14 +100,20 @@ export default function InvitesPage() {
{/* Spent Invites */}
{spentInvites.length > 0 && (
<div style={styles.section}>
<h2 style={typographyStyles.sectionTitle}>Used ({spentInvites.length})</h2>
<h2 style={typographyStyles.sectionTitle}>
{t("page.used", { count: spentInvites.length })}
</h2>
<div style={styles.inviteList}>
{spentInvites.map((invite) => (
<div key={invite.id} style={styles.inviteCardSpent}>
<div style={styles.inviteCode}>{invite.identifier}</div>
<div style={styles.inviteeMeta}>
<StatusBadge variant="success">Used</StatusBadge>
<span style={styles.inviteeEmail}>by {invite.used_by_email}</span>
<StatusBadge variant="success">{t("page.usedStatus")}</StatusBadge>
{invite.used_by_email && (
<span style={styles.inviteeEmail}>
{t("page.usedBy", { email: invite.used_by_email })}
</span>
)}
</div>
</div>
))}
@ -121,12 +124,14 @@ export default function InvitesPage() {
{/* Revoked Invites */}
{revokedInvites.length > 0 && (
<div style={styles.section}>
<h2 style={typographyStyles.sectionTitle}>Revoked ({revokedInvites.length})</h2>
<h2 style={typographyStyles.sectionTitle}>
{t("page.revoked", { count: revokedInvites.length })}
</h2>
<div style={styles.inviteList}>
{revokedInvites.map((invite) => (
<div key={invite.id} style={styles.inviteCardRevoked}>
<div style={styles.inviteCode}>{invite.identifier}</div>
<StatusBadge variant="error">Revoked</StatusBadge>
<StatusBadge variant="error">{t("page.revokedStatus")}</StatusBadge>
</div>
))}
</div>