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

@ -19,6 +19,7 @@ import {
utilityStyles,
} from "../styles/shared";
import { validateProfileFields } from "../utils/validation";
import { useTranslation } from "../hooks/useTranslation";
// Use generated type from OpenAPI schema
type ProfileData = components["schemas"]["ProfileResponse"];
@ -41,6 +42,7 @@ function toFormData(data: ProfileData): FormData {
}
export default function ProfilePage() {
const t = useTranslation("profile");
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredPermission: Permission.MANAGE_OWN_PROFILE,
fallbackRedirect: "/admin/trades",
@ -88,7 +90,7 @@ export default function ProfilePage() {
setGodfatherEmail(data.godfather_email ?? null);
} catch (err) {
console.error("Profile load error:", err);
setToast({ message: "Failed to load profile", type: "error" });
setToast({ message: t("messages.loadError"), type: "error" });
} finally {
setIsLoadingProfile(false);
}
@ -141,16 +143,16 @@ export default function ProfilePage() {
const formValues = toFormData(data);
setFormData(formValues);
setOriginalData(formValues);
setToast({ message: "Profile saved successfully!", type: "success" });
setToast({ message: t("messages.saveSuccess"), type: "success" });
} catch (err) {
console.error("Profile save error:", err);
const fieldErrors = extractFieldErrors(err);
if (fieldErrors?.detail?.field_errors) {
setErrors(fieldErrors.detail.field_errors);
setToast({ message: "Please fix the errors below", type: "error" });
setToast({ message: t("messages.fixErrors"), type: "error" });
} else {
setToast({
message: extractApiErrorMessage(err, "Network error. Please try again."),
message: extractApiErrorMessage(err, t("messages.networkError")),
type: "error",
});
}
@ -181,16 +183,16 @@ export default function ProfilePage() {
<div style={layoutStyles.contentCentered}>
<div style={styles.profileCard}>
<div style={cardStyles.cardHeader}>
<h1 style={cardStyles.cardTitle}>My Profile</h1>
<p style={cardStyles.cardSubtitle}>Manage your contact information</p>
<h1 style={cardStyles.cardTitle}>{t("page.title")}</h1>
<p style={cardStyles.cardSubtitle}>{t("page.subtitle")}</p>
</div>
<form onSubmit={handleSubmit} style={formStyles.form}>
{/* Login email - read only */}
<div style={formStyles.field}>
<label style={styles.labelWithBadge}>
Login Email
<span style={utilityStyles.readOnlyBadge}>Read only</span>
{t("form.email")}
<span style={utilityStyles.readOnlyBadge}>{t("form.readOnly")}</span>
</label>
<input
type="email"
@ -198,36 +200,32 @@ export default function ProfilePage() {
style={{ ...formStyles.input, ...formStyles.inputReadOnly }}
disabled
/>
<span style={formStyles.hint}>
This is your login email and cannot be changed here.
</span>
<span style={formStyles.hint}>{t("form.emailHint")}</span>
</div>
{/* Godfather - shown if user was invited */}
{godfatherEmail && (
<div style={formStyles.field}>
<label style={styles.labelWithBadge}>
Invited By
<span style={utilityStyles.readOnlyBadge}>Read only</span>
{t("form.invitedBy")}
<span style={utilityStyles.readOnlyBadge}>{t("form.readOnly")}</span>
</label>
<div style={styles.godfatherBox}>
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
</div>
<span style={formStyles.hint}>The user who invited you to join.</span>
<span style={formStyles.hint}>{t("form.invitedByHint")}</span>
</div>
)}
<div style={utilityStyles.divider} />
<p style={styles.sectionLabel}>Contact Details</p>
<p style={styles.sectionHint}>
These are for communication purposes only they won&apos;t affect your login.
</p>
<p style={styles.sectionLabel}>{t("form.contactDetails")}</p>
<p style={styles.sectionHint}>{t("form.contactDetailsHint")}</p>
{/* Contact email */}
<div style={formStyles.field}>
<label htmlFor="contact_email" style={formStyles.label}>
Contact Email
{t("form.contactEmail")}
</label>
<input
id="contact_email"
@ -238,7 +236,7 @@ export default function ProfilePage() {
...formStyles.input,
...(errors.contact_email ? formStyles.inputError : {}),
}}
placeholder="alternate@example.com"
placeholder={t("placeholders.contactEmail")}
/>
{errors.contact_email && (
<span style={formStyles.errorText}>{errors.contact_email}</span>
@ -248,7 +246,7 @@ export default function ProfilePage() {
{/* Telegram */}
<div style={formStyles.field}>
<label htmlFor="telegram" style={formStyles.label}>
Telegram
{t("form.telegram")}
</label>
<input
id="telegram"
@ -259,7 +257,7 @@ export default function ProfilePage() {
...formStyles.input,
...(errors.telegram ? formStyles.inputError : {}),
}}
placeholder="@username"
placeholder={t("placeholders.telegram")}
/>
{errors.telegram && <span style={formStyles.errorText}>{errors.telegram}</span>}
</div>
@ -267,7 +265,7 @@ export default function ProfilePage() {
{/* Signal */}
<div style={formStyles.field}>
<label htmlFor="signal" style={formStyles.label}>
Signal
{t("form.signal")}
</label>
<input
id="signal"
@ -278,7 +276,7 @@ export default function ProfilePage() {
...formStyles.input,
...(errors.signal ? formStyles.inputError : {}),
}}
placeholder="username.01"
placeholder={t("placeholders.signal")}
/>
{errors.signal && <span style={formStyles.errorText}>{errors.signal}</span>}
</div>
@ -286,7 +284,7 @@ export default function ProfilePage() {
{/* Nostr npub */}
<div style={formStyles.field}>
<label htmlFor="nostr_npub" style={formStyles.label}>
Nostr (npub)
{t("form.nostrNpub")}
</label>
<input
id="nostr_npub"
@ -297,7 +295,7 @@ export default function ProfilePage() {
...formStyles.input,
...(errors.nostr_npub ? formStyles.inputError : {}),
}}
placeholder="npub1..."
placeholder={t("placeholders.nostrNpub")}
/>
{errors.nostr_npub && <span style={formStyles.errorText}>{errors.nostr_npub}</span>}
</div>
@ -311,7 +309,7 @@ export default function ProfilePage() {
}}
disabled={!canSubmit}
>
{isSubmitting ? "Saving..." : "Save Changes"}
{isSubmitting ? t("form.saving") : t("form.saveChanges")}
</button>
</form>
</div>