/** * The complete auth locale bundle. Every key is required: the bundles this * package ships (`en`, `de`) satisfy the full shape, and consumer overrides * enter as {@link PartialAuthLocale}, deep-merged over the active built-in * bundle by `mergeAuthLocale`, so component markup reads keys directly, * without per-key `?? '…'` fallback literals. * * Placeholder convention: dynamic values use **single-brace** tokens * (`{n}`, `{name}`, `{email}`) that the consuming component substitutes itself * via `String.replace('{token}', value)`. There is deliberately **no** * `{{…}}` runtime interpolator in this package. The key-based translator twin * (`authT`/`at`) was removed, so `{{…}}` here would render verbatim. */ export interface AuthLocale { auth: { /** * Localized copy for the machine `AuthErrorCode` values the server handlers * return alongside the English `error` prose. The client `errorMessageFromCode` * maps a code here; an unknown/missing code falls back to the server prose. * * `AUTH_ERROR_MESSAGE_KEYS` (`i18n/error-keys.ts`) binds every code to one of * these keys — or to `null` where another surface owns the copy — and is * `satisfies`-checked against the union, so the compiler names a code that * has no key here. It is also the table the server's English prose is derived * from, so these strings are the only English text for a code that exists. */ errors: { invitationRequired: string; invitationUsed: string; invitationExpired: string; emailTaken: string; emailInvited: string; invalidCredentials: string; accountLocked: string; emailUnverified: string; invalidToken: string; currentPasswordIncorrect: string; notAuthenticated: string; forbidden: string; /** * The sign-in challenge rejected the code. The copy must stay generic * about where the code came from: this step also accepts a **backup * code** (the verify handler falls back to redeeming one), which is not * from the authenticator app. */ invalidCode: string; /** * Enrolment rejected the code. Separate copy from `invalidCode` because * enrolment verifies against the staged TOTP secret only — no backup-code * fallback — so this copy may name the authenticator app. */ twoFactorSetupCodeInvalid: string; no2faChallenge: string; twoFactorChallengeExpired: string; twoFactorAlreadyEnabled: string; twoFactorSetupRequired: string; totpSecretUnreadable: string; sessionNotFound: string; missingRefreshToken: string; invalidRefreshToken: string; featureUnavailable: string; validationError: string; /** 429 — too many *requests*; the reaction is to wait. */ rateLimited: string; /** * 429 — too many *concurrent connections* (SSE streams) for one account; * the reaction is to close another tab, not to wait. Split from * `rateLimited` because the two render under the same status code and the * correct reaction differs. */ connectionLimit: string; /** * 403 — the double-submit CSRF token was missing or stale (the tab left * open past the cookie's lifetime). Gates every mutating request, so it * can surface under any form; the only reaction is to reload the page. */ csrfFailed: string; /** * 400 — a passkey ceremony (sign-in or registration) did not verify and * a retry can succeed. One string for all of those: the server's causes * are either not actionable by the user (expired challenge) or must not * be shown to them at all (the cloned-authenticator signal); they are * separated in the audit hook and the log instead. */ passkeyVerificationFailed: string; /** * Enrolment's copy of the same ceremony failure. Separate from * `passkeyVerificationFailed` because the reader is signed in and adding * a device, so "sign in another way" — the way out at sign-in — is not * a step they can take. */ passkeyRegistrationVerificationFailed: string; /** * 401 — the passkey the browser offered at sign-in is not stored on the * server (deleted from another device). A retry offers the same passkey * again, so this copy names the way out: another sign-in, then re-enrol. */ passkeyCredentialDeleted: string; /** * 404 — the passkey a signed-in user named while managing their own list * is not on their account. Their session stands, so unlike * `passkeyCredentialDeleted` the way out is to reload the list. */ passkeyNotFound: string; serverError: string; /** Client-side only: the request never reached the server (offline, DNS, CORS). */ networkError: string; }; /** * Copy for the password rules, shared by every surface that shows them: * the sign-up page, the reset page, the account panel, and the message a * server-side refusal renders. It does NOT live under `register` — two of * those three surfaces are not registration. */ passwordRequirements: { /** aria-label of the live checklist. */ label: string; /** Accessible name of a satisfied rule's ✓ marker. */ met: string; /** Accessible name of an unsatisfied rule's ✗ marker. */ notMet: string; /** * Shown when a password is refused — client-side before the request, or * by the server afterwards. `{rules}` is replaced with the failing * `rules` labels joined by ` · `, so the message names them even when * the checklist is switched off. */ failed: string; /** * One label per `PasswordRuleId` (`password-policy.ts`). The checklist * annotates this object as `Record`, so a new * rule cannot ship without a label in both bundles. */ rules: { /** `{n}` is replaced with the resolved policy's `minLength`. */ minLength: string; uppercase: string; lowercase: string; digit: string; special: string; }; }; login: { title: string; email: string; password: string; rememberMe: string; submit: string; noAccount: string; register: string; forgotPassword: string; errors: { /** Generic credentials failure — the fallback when a login error carries no known code. */ invalid: string; }; }; register: { title: string; name: string; email: string; password: string; confirmPassword: string; submit: string; hasAccount: string; login: string; errors: { passwordMismatch: string; }; }; forgotPassword: { title: string; description: string; email: string; submit: string; backToLogin: string; success: string; }; resetPassword: { title: string; password: string; confirmPassword: string; submit: string; success: string; errors: { mismatch: string; invalidToken: string; }; }; verifyEmail: { title: string; verifying: string; success: string; error: string; }; /** * Copy for the default transactional emails the server sends (verification, * password-reset, email-change, invitation). Localized via `config.email.locale`; * a per-mail builder hook (`verificationEmail`, `resetEmail`, …) replaces these. * Placeholders: `{name}`, `{appName}`, `{email}` — substituted by the builder. */ emails: { verification: { subject: string; heading: string; body: string; cta: string; ignore: string; }; passwordReset: { subject: string; heading: string; body: string; cta: string; ignore: string; }; invitation: { subject: string; heading: string; body: string; cta: string; ignore: string; }; changeEmail: { subject: string; heading: string; body: string; cta: string; ignore: string; }; /** Awareness notice sent to the OLD address when an email change is requested. */ changeEmailNotice: { subject: string; heading: string; body: string; ignore: string; }; }; }; notifications: { /** Name of the unread-count badge. `{n}` is the badge's own text, so `99+` stays `99+`. */ badge: { unread: string; }; center: { title: string; empty: string; markAllRead: string; /** Row action; the component appends ` — {title}` so the name says which row. */ delete: string; /** Visually hidden marker inside an unread row — the coloured dot is decorative. */ unread: string; }; push: { prompt: string; enable: string; dismiss: string; /** Shown (aria-live) when subscribing fails — the prompt stays open for a retry. */ error: string; /** 409 push_endpoint_conflict — this device is registered to another account; retrying cannot succeed. */ errorConflict: string; /** 409 push_subscription_limit — per-user device cap reached; remove a device first. */ errorLimit: string; /** 429 — too many attempts right now. */ errorRateLimited: string; }; }; invitations: { title: string; email: string; role: string; pending: string; delete: string; send: string; empty: string; sendEmail: string; registered: string; linkSentAndCopyable: string; linkNotSent: string; linkCopy: string; linkCopied: string; linkTrustNote: string; }; passkeys: { title: string; add: string; empty: string; delete: string; lastUsed: string; loginWithPasskey: string; or: string; /** Passkey sign-in failed client-side (browser error other than a user cancel). */ loginFailed: string; /** * The browser's passkey dialog (setup or sign-in) ended without a * credential — user cancel, timeout, or an iframe policy denial * (indistinguishable by design: NotAllowedError). */ cancelled: string; /** Registering a new passkey failed client-side. */ addFailed: string; /** Opens the inline rename form on one row; also names it for a screen reader. */ rename: string; /** Label of the rename field. Visible, so the form is not an unlabelled box. */ renameLabel: string; /** Submits the rename. */ renameSave: string; /** Abandons the rename and restores the stored name. */ renameCancel: string; /** Announced through the panel's live region once the server confirms. */ renamed: string; }; account: { title: string; profile: { title: string; name: string; save: string; success: string; }; email: { title: string; current: string; newEmail: string; currentPassword: string; submit: string; success: string; }; password: { title: string; currentPassword: string; newPassword: string; submit: string; success: string; }; delete: { title: string; description: string; currentPassword: string; submit: string; confirmTitle: string; confirmBody: string; confirm: string; cancel: string; }; }; sessions: { title: string; thisDevice: string; lastActive: string; /** Row action; the component appends ` — {device}` so the name says which session. */ signOut: string; signOutOthers: string; unknownDevice: string; empty: string; unavailable: string; }; twoFactor: { title: string; description: string; statusEnabled: string; enable: string; disable: string; /** Heading of the setup step — the element focus moves to when the step opens. */ setupTitle: string; setupScan: string; setupSecret: string; setupCode: string; setupConfirm: string; cancel: string; backupTitle: string; backupDescription: string; backupDownload: string; backupDone: string; disableTitle: string; disableDescription: string; disablePassword: string; disableConfirm: string; loginTitle: string; loginPrompt: string; loginCode: string; loginSubmit: string; loginBackupHint: string; invalidCode: string; }; common: { error: string; timeAgo: { now: string; minutes: string; hours: string; days: string; }; }; } /** Recursive partial: every branch and leaf becomes optional. */ export type DeepPartial = T extends object ? { [K in keyof T]?: DeepPartial; } : T; /** * Consumer-facing locale input: any subset of {@link AuthLocale}. Components * accept this as their `t` prop and deep-merge it over the active built-in * bundle, so overriding a single string never silently blanks the rest. */ export type PartialAuthLocale = DeepPartial;