diff --git a/be/apps/dashboard/src/hooks/useRequireStorageProvider.ts b/be/apps/dashboard/src/hooks/useRequireStorageProvider.ts index 4417c3e6..c7f483bc 100644 --- a/be/apps/dashboard/src/hooks/useRequireStorageProvider.ts +++ b/be/apps/dashboard/src/hooks/useRequireStorageProvider.ts @@ -3,6 +3,7 @@ import { useLocation, useNavigate } from 'react-router' import { PUBLIC_ROUTES } from '~/constants/routes' import type { SessionResponse } from '~/modules/auth/api/session' +import { useManagedStoragePlansQuery } from '~/modules/storage-plans' import { useStorageProvidersQuery } from '~/modules/storage-providers' const STORAGE_SETUP_PATH = '/photos/storage' @@ -28,11 +29,24 @@ export function useRequireStorageProvider({ session, isLoading }: UseRequireStor enabled: shouldCheck, }) + const managedStoragePlansQuery = useManagedStoragePlansQuery({ + enabled: shouldCheck, + }) + + const hasManagedStoragePlan = + managedStoragePlansQuery.isSuccess && + managedStoragePlansQuery.data.managedStorageEnabled && + Boolean(managedStoragePlansQuery.data.currentPlanId) + + const isCheckingManagedStoragePlan = managedStoragePlansQuery.isPending + const needsSetup = shouldCheck && storageProvidersQuery.isSuccess && (storageProvidersQuery.data?.providers.length ?? 0) === 0 && - !storageProvidersQuery.isFetching + !storageProvidersQuery.isFetching && + !isCheckingManagedStoragePlan && + !hasManagedStoragePlan const navigateOnceRef = useRef(false) useEffect(() => { diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationFooter.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationFooter.tsx index 39e2200c..dd6360c3 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationFooter.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationFooter.tsx @@ -1,5 +1,6 @@ import { Button } from '@afilmory/ui' import type { FC } from 'react' +import { useTranslation } from 'react-i18next' type FooterProps = { disableBack: boolean @@ -17,33 +18,37 @@ export const RegistrationFooter: FC = ({ disableNext, onBack, onNext, -}) => ( - + ) +} diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationHeader.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationHeader.tsx index fae72d36..06a4a83a 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationHeader.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationHeader.tsx @@ -1,17 +1,21 @@ import type { FC } from 'react' +import { useTranslation } from 'react-i18next' -import { REGISTRATION_STEPS } from './constants' +import { useRegistrationSteps } from './useRegistrationSteps' type HeaderProps = { currentStepIndex: number } export const RegistrationHeader: FC = ({ currentStepIndex }) => { - const step = REGISTRATION_STEPS[currentStepIndex] + const { t } = useTranslation() + const steps = useRegistrationSteps() + const step = steps[currentStepIndex] + return (
- Step {currentStepIndex + 1} of {REGISTRATION_STEPS.length} + {t('auth.registration.header.step_indicator', { current: currentStepIndex + 1, total: steps.length })}

{step.title}

{step.description}

diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationSidebar.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationSidebar.tsx index 81a7c7c6..34dc0bf8 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationSidebar.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationSidebar.tsx @@ -1,7 +1,9 @@ import { cx } from '@afilmory/utils' import type { FC } from 'react' +import { useTranslation } from 'react-i18next' -import { progressForStep, REGISTRATION_STEPS } from './constants' +import { progressForStep } from './constants' +import { useRegistrationSteps } from './useRegistrationSteps' type SidebarProps = { currentStepIndex: number @@ -9,104 +11,109 @@ type SidebarProps = { onStepSelect: (index: number) => void } -export const RegistrationSidebar: FC = ({ currentStepIndex, canNavigateTo, onStepSelect }) => ( - + ) +} diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationWizard.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationWizard.tsx index adbc5923..4d8db15e 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationWizard.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/RegistrationWizard.tsx @@ -3,6 +3,7 @@ import { useStore } from '@tanstack/react-form' import { useQuery } from '@tanstack/react-query' import type { FC, KeyboardEvent } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' import { Link } from 'react-router' import { toast } from 'sonner' @@ -52,6 +53,7 @@ export const RegistrationWizard: FC = () => { }) const [siteSchema, setSiteSchema] = useState | null>(null) + const { t } = useTranslation() const advanceStep = useCallback(() => { setCurrentStepIndex((prev) => { const nextIndex = Math.min(REGISTRATION_STEPS.length - 1, prev + 1) @@ -327,7 +329,7 @@ export const RegistrationWizard: FC = () => { const step = REGISTRATION_STEPS[currentStepIndex] if (step.id === 'login') { if (!authUser) { - toast.error('Please sign in to continue') + toast.error(t('auth.registration.wizard.toast.sign_in_required')) return } advanceStep() @@ -336,7 +338,11 @@ export const RegistrationWizard: FC = () => { if (step.id === 'site') { const result = siteSettingsSchema.safeParse(formValues) if (!result.success) { - toast.error(`Error in ${result.error.issues.map((issue) => issue.message).join(', ')}`) + toast.error( + t('auth.registration.wizard.toast.validation_error', { + issues: result.error.issues.map((issue) => issue.message).join(', '), + }), + ) return } @@ -556,9 +562,9 @@ export const RegistrationWizard: FC = () => {

- Already have an account?{' '} + {t('auth.registration.wizard.have_account')}{' '} - Sign in + {t('auth.registration.wizard.sign_in')} .

diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/constants.ts b/be/apps/dashboard/src/modules/auth/components/registration-wizard/constants.ts index e62919ad..cca14355 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/constants.ts +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/constants.ts @@ -3,28 +3,18 @@ import type { TenantRegistrationFormState } from '~/modules/auth/hooks/useRegist export const REGISTRATION_STEPS = [ { id: 'login', - title: 'Connect account', - description: 'Sign in with your identity provider to continue.', }, { id: 'workspace', - title: 'Workspace details', - description: 'Give your workspace a recognizable name and choose a slug for tenant URLs.', }, { id: 'site', - title: 'Site information', - description: 'Configure the public gallery branding your visitors will see.', }, { id: 'review', - title: 'Review & confirm', - description: 'Verify everything looks right and accept the terms before provisioning the workspace.', }, ] as const satisfies ReadonlyArray<{ id: 'login' | 'workspace' | 'site' | 'review' - title: string - description: string }> export type RegistrationStepId = (typeof REGISTRATION_STEPS)[number]['id'] diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/LoginStep.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/LoginStep.tsx index 7664c957..cf645822 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/LoginStep.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/LoginStep.tsx @@ -1,6 +1,7 @@ import { Button } from '@afilmory/ui' import { cx } from '@afilmory/utils' import type { FC } from 'react' +import { Trans, useTranslation } from 'react-i18next' import type { BetterAuthUser } from '~/modules/auth/types' @@ -13,50 +14,60 @@ type LoginStepProps = { isContinuing: boolean } -export const LoginStep: FC = ({ user, isAuthenticated, onContinue, isContinuing }) => ( -
-
-

- Afilmory is a modern SaaS{' '} - - photo gallery platform - {' '} - that auto-syncs your libraries, renders them with WebGL, and powers tenant workspaces. The dashboard is the - command center for those capabilities, so connecting your account lets us personalize the workspace setup for - your team. -

-

Sign in to continue

-

- Use your organization's identity provider to create a workspace. We'll use your profile details to set - up the initial administrator. -

-
+export const LoginStep: FC = ({ user, isAuthenticated, onContinue, isContinuing }) => { + const { t } = useTranslation() - {!isAuthenticated ? ( -
-
-

- Choose your provider below. After completing the sign-in flow you'll return here automatically. -

+ return ( +
+
+

+ + Afilmory is a modern SaaS{' '} + + photo gallery platform + {' '} + that auto-syncs your libraries, renders them with WebGL, and powers tenant workspaces. The dashboard is the + command center for those capabilities, so connecting your account lets us personalize the workspace setup + for your team. + +

+

{t('auth.registration.steps.login.sign_in_title')}

+

{t('auth.registration.steps.login.sign_in_description')}

+
+ + {!isAuthenticated ? ( +
+
+

{t('auth.registration.steps.login.provider_hint')}

+
+
- -
- ) : ( -
-

You're signed in as

-
{user?.name || user?.email}
-
{user?.email}
- -
- )} -
-) + ) : ( +
+

{t('auth.registration.steps.login.signed_in_as')}

+
{user?.name || user?.email}
+
{user?.email}
+ +
+ )} +
+ ) +} diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/ReviewStep.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/ReviewStep.tsx index dea3f1a0..805ed477 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/ReviewStep.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/ReviewStep.tsx @@ -3,6 +3,7 @@ import { cx, Spring } from '@afilmory/utils' import { m } from 'motion/react' import type { FC } from 'react' import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' import type { TenantRegistrationFormState, @@ -37,9 +38,13 @@ export const ReviewStep: FC = ({ serverError, onFieldInteraction, }) => { + const { t } = useTranslation() + const formatSiteValue = (value: SchemaFormValue | undefined) => { if (typeof value === 'boolean') { - return value ? 'Enabled' : 'Disabled' + return value + ? t('auth.registration.common.enabled', 'Enabled') + : t('auth.registration.common.disabled', 'Disabled') } if (value == null) { return '—' @@ -65,32 +70,40 @@ export const ReviewStep: FC = ({ return (
-

Confirm workspace configuration

-

- Double-check the details below. You can go back to make adjustments before creating the workspace. -

+

{t('auth.registration.steps.review.title_confirm')}

+

{t('auth.registration.steps.review.description_confirm')}

-
Workspace name
+
+ {t('auth.registration.steps.review.label_workspace_name')} +
{values.tenantName || '—'}
-
Workspace slug
+
+ {t('auth.registration.steps.review.label_workspace_slug')} +
{values.tenantSlug || '—'}
-
Administrator name
+
+ {t('auth.registration.steps.review.label_admin_name')} +
{authUser?.name || authUser?.email || '—'}
-
Administrator email
+
+ {t('auth.registration.steps.review.label_admin_email')} +
{authUser?.email || '—'}
-

Site details

+

+ {t('auth.registration.steps.review.section_site_details')} +

{siteSchemaLoading &&
} {!siteSchemaLoading && siteSchemaError && (
{siteSchemaError}
@@ -132,10 +145,8 @@ export const ReviewStep: FC = ({ )}
-

Policies

-

- Creating a workspace means you agree to comply with our usage guidelines and privacy practices. -

+

{t('auth.registration.steps.review.section_policies')}

+

{t('auth.registration.steps.review.policies_description')}

{(field) => { @@ -153,13 +164,13 @@ export const ReviewStep: FC = ({ className="mt-0.5" /> - I agree to the{' '} + {t('auth.registration.steps.review.terms_agree_pre')}{' '} - Terms of Service + {t('auth.registration.steps.review.terms_link')} {' '} - and{' '} + {t('auth.registration.steps.review.terms_and')}{' '} - Privacy Policy + {t('auth.registration.steps.review.privacy_link')} . diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/SiteSettingsStep.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/SiteSettingsStep.tsx index ffce6e7e..01c23dff 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/SiteSettingsStep.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/SiteSettingsStep.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react' +import { useTranslation } from 'react-i18next' import type { TenantRegistrationFormState, @@ -26,16 +27,15 @@ export const SiteSettingsStep: FC = ({ values, errors, }) => { + const { t } = useTranslation() + if (!schema) { if (isLoading) { return (
-

Site branding

-

- These details appear on your public gallery, metadata, and social sharing cards. You can change them later - from the dashboard. -

+

{t('auth.registration.steps.site.branding_title')}

+

{t('auth.registration.steps.site.branding_description')}

@@ -45,10 +45,8 @@ export const SiteSettingsStep: FC = ({ return (
-

Site branding

-

- We couldn't load the site configuration schema from the server. Refresh the page or contact support. -

+

{t('auth.registration.steps.site.branding_title')}

+

{t('auth.registration.steps.site.error_loading')}

{errorMessage && (
{errorMessage}
diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/WorkspaceStep.tsx b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/WorkspaceStep.tsx index d493e38e..913cf28c 100644 --- a/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/WorkspaceStep.tsx +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/steps/WorkspaceStep.tsx @@ -1,11 +1,21 @@ import { FormError, Input, Label } from '@afilmory/ui' +import { useStore } from '@tanstack/react-form' import type { FC, MutableRefObject } from 'react' +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' import type { useRegistrationForm } from '~/modules/auth/hooks/useRegistrationForm' import { slugify } from '~/modules/welcome/utils' import { firstErrorMessage } from '../utils' +const titleCaseFromSlug = (slug: string) => + slug + .split(/[-_]+/g) + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' ') + type WorkspaceStepProps = { form: ReturnType slugManuallyEditedRef: MutableRefObject @@ -20,83 +30,97 @@ export const WorkspaceStep: FC = ({ lockedTenantSlug, isSubmitting, onFieldInteraction, -}) => ( -
-
-

Workspace basics

-

- This information appears in navigation, invitations, and other tenant-facing areas. -

-
-
- - {(field) => { - const error = firstErrorMessage(field.state.meta.errors) +}) => { + const { t } = useTranslation() + const slugValue = useStore(form.store, (state) => state.values.tenantSlug) + const tenantNameValue = useStore(form.store, (state) => state.values.tenantName) - return ( -
- - { - onFieldInteraction() - const nextValue = event.currentTarget.value - field.handleChange(nextValue) - if (!slugManuallyEditedRef.current) { - const nextSlug = slugify(nextValue) - if (nextSlug !== form.getFieldValue('tenantSlug')) { - form.setFieldValue('tenantSlug', () => nextSlug) - void form.validateField('tenantSlug', 'change') + useEffect(() => { + if (tenantNameValue || !slugValue) { + return + } + const derivedName = titleCaseFromSlug(slugValue) + if (derivedName) { + form.setFieldValue('tenantName', () => derivedName) + } + }, [form, slugValue, tenantNameValue]) + + return ( +
+
+

{t('auth.registration.steps.workspace.basics_title')}

+

{t('auth.registration.steps.workspace.basics_description')}

+
+
+ + {(field) => { + const error = firstErrorMessage(field.state.meta.errors) + + return ( +
+ + { + onFieldInteraction() + const nextValue = event.currentTarget.value + field.handleChange(nextValue) + if (!slugManuallyEditedRef.current) { + const nextSlug = slugify(nextValue) + if (nextSlug !== form.getFieldValue('tenantSlug')) { + form.setFieldValue('tenantSlug', () => nextSlug) + void form.validateField('tenantSlug', 'change') + } } - } - }} - onBlur={field.handleBlur} - placeholder="Acme Studio" - disabled={isSubmitting} - error={Boolean(error)} - autoComplete="organization" - /> - {error} -
- ) - }} -
- - {(field) => { - const error = firstErrorMessage(field.state.meta.errors) - const isSlugLocked = Boolean(lockedTenantSlug) - const helperText = isSlugLocked - ? 'Workspace slug follows the current subdomain and cannot be changed in this flow.' - : 'Lowercase letters, numbers, and hyphen are allowed. We'll ensure the slug is unique.' + }} + onBlur={field.handleBlur} + placeholder="Acme Studio" + disabled={isSubmitting} + error={Boolean(error)} + autoComplete="organization" + /> + {error} +
+ ) + }} + + + {(field) => { + const error = firstErrorMessage(field.state.meta.errors) + const isSlugLocked = Boolean(lockedTenantSlug) + const helperText = isSlugLocked + ? t('auth.registration.steps.workspace.slug_locked_helper') + : t('auth.registration.steps.workspace.slug_helper') - return ( -
- - { - if (isSlugLocked) { - return - } - onFieldInteraction() - slugManuallyEditedRef.current = true - field.handleChange(event.currentTarget.value) - }} - onBlur={field.handleBlur} - placeholder="acme" - disabled={isSubmitting || isSlugLocked} - readOnly={isSlugLocked} - error={Boolean(error)} - autoComplete="off" - /> -

{helperText}

- {error} -
- ) - }} -
+ return ( +
+ + { + if (isSlugLocked) { + return + } + onFieldInteraction() + slugManuallyEditedRef.current = true + field.handleChange(event.currentTarget.value) + }} + onBlur={field.handleBlur} + placeholder="acme" + disabled={isSubmitting || isSlugLocked} + readOnly={isSlugLocked} + error={Boolean(error)} + autoComplete="off" + /> +

{helperText}

+ {error} +
+ ) + }} + +
-
-) + ) +} diff --git a/be/apps/dashboard/src/modules/auth/components/registration-wizard/useRegistrationSteps.ts b/be/apps/dashboard/src/modules/auth/components/registration-wizard/useRegistrationSteps.ts new file mode 100644 index 00000000..03ce342d --- /dev/null +++ b/be/apps/dashboard/src/modules/auth/components/registration-wizard/useRegistrationSteps.ts @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next' + +import { REGISTRATION_STEPS } from './constants' + +export const useRegistrationSteps = () => { + const { t } = useTranslation() + + return REGISTRATION_STEPS.map((step) => ({ + ...step, + title: t(`auth.registration.steps.${step.id}.title`), + description: t(`auth.registration.steps.${step.id}.description`), + })) +} diff --git a/be/apps/dashboard/src/modules/auth/hooks/useRegistrationForm.ts b/be/apps/dashboard/src/modules/auth/hooks/useRegistrationForm.ts index adee18b0..5eda993f 100644 --- a/be/apps/dashboard/src/modules/auth/hooks/useRegistrationForm.ts +++ b/be/apps/dashboard/src/modules/auth/hooks/useRegistrationForm.ts @@ -48,7 +48,7 @@ export function buildRegistrationInitialValues( return { tenantName: initial?.tenantName ?? '', tenantSlug: initial?.tenantSlug ?? '', - termsAccepted: initial?.termsAccepted ?? false, + termsAccepted: initial?.termsAccepted ?? true, ...siteValues, } } diff --git a/be/apps/dashboard/src/modules/storage-plans/hooks.ts b/be/apps/dashboard/src/modules/storage-plans/hooks.ts index e95065e2..e384af99 100644 --- a/be/apps/dashboard/src/modules/storage-plans/hooks.ts +++ b/be/apps/dashboard/src/modules/storage-plans/hooks.ts @@ -5,10 +5,11 @@ import type { ManagedStorageOverview } from './types' export const MANAGED_STORAGE_PLAN_QUERY_KEY = ['billing', 'storage-plan'] as const -export function useManagedStoragePlansQuery() { +export function useManagedStoragePlansQuery(options?: { enabled?: boolean }) { return useQuery({ queryKey: MANAGED_STORAGE_PLAN_QUERY_KEY, queryFn: getManagedStorageOverview, + enabled: options?.enabled ?? true, }) } diff --git a/locales/dashboard/en.json b/locales/dashboard/en.json index bbec06fe..767514c0 100644 --- a/locales/dashboard/en.json +++ b/locales/dashboard/en.json @@ -34,6 +34,57 @@ "analytics.sections.upload.total": "Total uploads", "analytics.units.photos": "{{value}} photos", "app.name": "Afilmory Dashboard", + "auth.registration.common.disabled": "Disabled", + "auth.registration.common.enabled": "Enabled", + "auth.registration.footer.back": "Back", + "auth.registration.footer.continue": "Continue", + "auth.registration.footer.create_workspace": "Create workspace", + "auth.registration.header.step_indicator": "Step {{current}} of {{total}}", + "auth.registration.sidebar.create_tenant": "Create your tenant", + "auth.registration.sidebar.progress": "Progress", + "auth.registration.sidebar.workspace_setup": "Workspace Setup", + "auth.registration.steps.login.continue_setup": "Continue setup", + "auth.registration.steps.login.continue_with": "Continue with", + "auth.registration.steps.login.description": "Sign in with your identity provider to continue.", + "auth.registration.steps.login.intro_text": "Afilmory is a modern SaaS <0>photo gallery platform that auto-syncs your libraries, renders them with WebGL, and powers tenant workspaces. The dashboard is the command center for those capabilities, so connecting your account lets us personalize the workspace setup for your team.", + "auth.registration.steps.login.platform_link": "photo gallery platform", + "auth.registration.steps.login.provider_hint": "Choose your provider below. After completing the sign-in flow you'll return here automatically.", + "auth.registration.steps.login.sign_in_description": "Use your organization's identity provider to create a workspace. We'll use your profile details to set up the initial administrator.", + "auth.registration.steps.login.sign_in_title": "Sign in to continue", + "auth.registration.steps.login.signed_in_as": "You're signed in as", + "auth.registration.steps.login.title": "Connect account", + "auth.registration.steps.review.description": "Verify everything looks right and accept the terms before provisioning the workspace.", + "auth.registration.steps.review.description_confirm": "Double-check the details below. You can go back to make adjustments before creating the workspace.", + "auth.registration.steps.review.label_admin_email": "Administrator email", + "auth.registration.steps.review.label_admin_name": "Administrator name", + "auth.registration.steps.review.label_workspace_name": "Workspace name", + "auth.registration.steps.review.label_workspace_slug": "Workspace slug", + "auth.registration.steps.review.policies_description": "Creating a workspace means you agree to comply with our usage guidelines and privacy practices.", + "auth.registration.steps.review.privacy_link": "Privacy Policy", + "auth.registration.steps.review.section_policies": "Policies", + "auth.registration.steps.review.section_site_details": "Site details", + "auth.registration.steps.review.terms_agree_pre": "I agree to the", + "auth.registration.steps.review.terms_and": "and", + "auth.registration.steps.review.terms_link": "Terms of Service", + "auth.registration.steps.review.title": "Review & confirm", + "auth.registration.steps.review.title_confirm": "Confirm workspace configuration", + "auth.registration.steps.site.branding_description": "These details appear on your public gallery, metadata, and social sharing cards. You can change them later from the dashboard.", + "auth.registration.steps.site.branding_title": "Site branding", + "auth.registration.steps.site.description": "Configure the public gallery branding your visitors will see.", + "auth.registration.steps.site.error_loading": "We couldn't load the site configuration schema from the server. Refresh the page or contact support.", + "auth.registration.steps.site.title": "Site information", + "auth.registration.steps.workspace.basics_description": "This information appears in navigation, invitations, and other tenant-facing areas.", + "auth.registration.steps.workspace.basics_title": "Workspace basics", + "auth.registration.steps.workspace.description": "Give your workspace a recognizable name and choose a slug for tenant URLs.", + "auth.registration.steps.workspace.label_name": "Workspace name", + "auth.registration.steps.workspace.label_slug": "Workspace slug", + "auth.registration.steps.workspace.slug_helper": "Lowercase letters, numbers, and hyphen are allowed. We'll ensure the slug is unique.", + "auth.registration.steps.workspace.slug_locked_helper": "Workspace slug follows the current subdomain and cannot be changed in this flow.", + "auth.registration.steps.workspace.title": "Workspace details", + "auth.registration.wizard.have_account": "Already have an account?", + "auth.registration.wizard.sign_in": "Sign in", + "auth.registration.wizard.toast.sign_in_required": "Please sign in to continue", + "auth.registration.wizard.toast.validation_error": "Error in {{issues}}", "auth.social.empty.description": "Super admins have not enabled any third-party login methods. This tenant cannot bind OAuth accounts yet.", "auth.social.empty.title": "No OAuth providers configured", "auth.social.error.accounts": "Unable to fetch binding status",