Skip to content

Login page editor has no Spanish branding fields and erases stored Spanish text on save #1716

Open

Description

@joshunrau

The login page editor (Admin Panel → Branding → Login page) has English and French fields for the instance name, tagline, details and resource-link labels, but no Spanish ones. Spanish is an interface language, and the stored type $BrandingText has an es key whose doc comment says it exists "so branding can never silently render blank in a language the instance offers". The editor's form state is typed { en: string; fr: string }, its loader reads only en/fr from the saved branding, and its save payload sends only { en, fr }. An administrator therefore cannot write Spanish branding, and any es text already stored (for example, set through the API) is deleted the next time anyone saves the page. The preview's language toggle offers only EN and FR, so the Spanish login page cannot be previewed either. Spanish readers get the English or French text through getValueForLanguage's fallback, which the administrator can neither see nor change.

Where

apps/web/src/components/LoginPageEditor/types.ts:21-23,33:

instanceDetails: { en: string; fr: string };
instanceName: { en: string; fr: string };
instanceTagline: { en: string; fr: string };
// ...
resourceLinks: { href: string; label: { en: string; fr: string } }[];

apps/web/src/components/LoginPageEditor/hooks.ts:43-45 (load) and hooks.ts:336-346 (save):

const toText = (text: { en: string; fr: string }): BrandingText | null => {
  const en = trim(text.en);
  const fr = trim(text.fr);
  return (en ?? fr) ? { en, fr } : null;
};

Also hooks.ts:149-153 (updateText takes lang: 'en' | 'fr'), sections/SectionCards.tsx:296-316 and :397-410 (only English and French inputs), and preview/PreviewColumn.tsx:38-45 with LoginPageEditor.tsx:30 (useState<'en' | 'fr'>).

Reproduce

  1. Store a Spanish instance name, e.g. PATCH /v1/setup with branding.instanceName = { en: 'Clinic', es: 'Clínica', fr: 'Clinique' }, then switch the interface to Spanish and confirm the login page shows "Clínica".
  2. Sign in as an admin, open Admin Panel → Branding → Login page, change any setting (e.g. the tagline) and save.
  3. Sign out with the interface in Spanish.

Actual: the editor never offered a Spanish field, and after the save the login page shows "Clinic": the stored es value was dropped. The preview offers only EN and FR.
Expected: every branding text field has a Spanish input, saving round-trips all three languages, and the preview can show ES.

Tests

  • apps/web/src/components/LoginPageEditor/__tests__/hooks.test.tsx: it('should keep the Spanish branding text when saving, so an edit to another field does not erase it').
  • apps/web/src/components/LoginPageEditor/__tests__/LoginPageEditor.test.tsx: it('should offer a Spanish input for every branding text field').
  • testing/src/specs/admin-branding.spec.ts: enter a Spanish instance name, save, switch the login page to Spanish and assert the name is shown.

Suggested fix

Derive the form's text type from Language (e.g. { [L in Language]: string }) instead of hard-coding en/fr, so a missing language fails to compile. Then map LANGUAGES for the inputs, the preview tabs, toText and the resource-link labels.

Activity

  1. added
    BugType: existing behavior is wrong
    Difficulty: MediumStraightforward change, about a day or less
    Area: Webapps/web (clinician-facing React SPA)
    on Oct 7, 2026
  2. joshunrau commented on Oct 26, 2026

    @joshunrau
    CollaboratorAuthor

    Still valid on main (d86740e): the editor has no Spanish fields. The erase half cannot happen yet, though, because the API cannot store Spanish branding at all.

    The database rejects es

    9722a4e added es to $BrandingText, but on the Prisma side it only added es to LocalizedString. type BrandingText is still { en, fr }, and it backs instanceName, instanceTagline, instanceDetails and ResourceLink.label. Zod accepts es, so the value reaches setupStateModel.update (setup.service.ts:121-125). There the generated client (Prisma 6.19.3) throws PrismaClientValidationError ("Unknown argument es"), and libnest's GlobalExceptionFilter turns anything that is not an HttpException into a 500. tsc misses it because the text objects arrive through a spread variable, so no excess-property check runs.

    So step 1 of the reproduce returns a 500 and stores nothing. The suggested fix on its own makes this worse. Prisma rejects the key even as es: null, and extending toText to { en, es, fr } sends es: null whenever any language is filled in, so every save of a named instance would fail. useUpdateSetupStateMutation keeps throwOnError: true and a 500 is not a transient error, so ErrorPage replaces the editor and the unsaved edits are lost (the #1671 path).

    Plan, beyond the issue's

    1. apps/api/prisma/schema.prisma: delete type BrandingText and use LocalizedString for those four fields, as 9722a4e did for ErrorMessage. The stored shape is the same, so MongoDB needs no migration. apps/api/AGENTS.md:194-197 says nothing checks enum Language against LANGUAGES. Add a case to the agreement with schema.prisma block in apps/api/src/core/__tests__/prisma.test.ts that compares enum Language and the LocalizedString fields with LANGUAGES (Prisma.dmmf.datamodel exposes both), and update that note.
    2. hooks.ts: deriving the form type from Language makes tsc flag the object literals at lines 43-45, 54 and 174. Three reads of .en/.fr keep compiling and ignore es: toText (336-339), hasInvalidResourceLinks (line 268, so a link labelled only in Spanish disables Save) and the link cleanup at 342-347 (which drops that link). updateResourceLinkLabel (line 162) also takes 'en' | 'fr'. Write them over LANGUAGES. authoredLanguages in @/utils/language already does; omitBlankLanguages does too but keeps the untrimmed value.
    3. SectionCards.tsx: the four en/fr blocks (lines 56, 296, 397, 467) can map the languages and label each input with t(LANGUAGE_LABELS[code]). Keep the ${field}-${code} ids: the e2e page model finds #instanceName-en and LoginPageEditor.test.tsx finds instanceDetails-en.
    4. preview/FullscreenPreviewDialog.tsx is not in the issue. previewLang is typed 'en' | 'fr' (line 15), and the mock login form at lines 67-79 is previewLang === 'fr' ? … : …, so a Spanish preview shows English. libui's t() takes no language option, so these need a lookup keyed by previewLang that has every language.

    Which languages get inputs: I would follow #1810. Render inputs and preview tabs for setupState.activeLanguages (useBrandingForm already reads the setup state, so it can return them on editor), start previewLang at activeLanguages[0] instead of 'en', and keep every LANGUAGES key in the form state so text in an inactive language survives a save. Today an instance whose only active language is es gets no input in any language it offers.

    Tests

    • apps/api/test/suites/: PATCH /v1/setup with es in instanceName and in a link label, then GET it back. setup.service.test.ts mocks the model, so only a real-database suite catches this.
    • hooks.test.tsx: es survives from the saved branding into the payload, and a Spanish-only link is valid and kept.
    • SectionCards.test.tsx: the four "English and French … independently" tests (lines 76, 223, 286, 347) cover every language, and with activeLanguages: ['es'] only Spanish inputs render. PreviewColumn.test.tsx and FullscreenPreviewDialog.test.tsx: the tabs follow the active languages and the mock form renders in Spanish.
    • testing/src/specs/admin-branding.spec.ts: activate Spanish as admin-settings.spec.ts does, enable branding, fill #instanceName-es, save, reload, switch the login page to Español and assert the name. Restore all three at the end: the active languages tests in admin-settings.spec.ts assume only en and fr are on.

    Land this after #1825: ec2890880 edits hooks.ts, and e5ff0cac3 (#1704) makes the spec serial and adds enableBrandingCheckbox, instanceNameFrenchInput and saveAndConfirm to the page model.

    Triage note written with Claude Code. Code references point at main @ d86740e.

  3. added
    Area: APIapps/api (NestJS backend, Prisma models, CASL permissions)
    on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: APIapps/api (NestJS backend, Prisma models, CASL permissions)Area: Webapps/web (clinician-facing React SPA)BugType: existing behavior is wrongDifficulty: MediumStraightforward change, about a day or lessPriority: MediumReal value; schedule normally

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions