Repository navigation
Description
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
- Store a Spanish instance name, e.g.
PATCH /v1/setupwithbranding.instanceName = { en: 'Clinic', es: 'Clínica', fr: 'Clinique' }, then switch the interface to Spanish and confirm the login page shows "Clínica". - Sign in as an admin, open Admin Panel → Branding → Login page, change any setting (e.g. the tagline) and save.
- 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
joshunrau commentedon Oct 26, 2026
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
es9722a4e added
esto$BrandingText, but on the Prisma side it only addedestoLocalizedString.type BrandingTextis still{ en, fr }, and it backsinstanceName,instanceTagline,instanceDetailsandResourceLink.label. Zod acceptses, so the value reachessetupStateModel.update(setup.service.ts:121-125). There the generated client (Prisma 6.19.3) throwsPrismaClientValidationError("Unknown argumentes"), and libnest'sGlobalExceptionFilterturns anything that is not anHttpExceptioninto a 500.tscmisses 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 extendingtoTextto{ en, es, fr }sendses: nullwhenever any language is filled in, so every save of a named instance would fail.useUpdateSetupStateMutationkeepsthrowOnError: trueand a 500 is not a transient error, soErrorPagereplaces the editor and the unsaved edits are lost (the #1671 path).Plan, beyond the issue's
apps/api/prisma/schema.prisma: deletetype BrandingTextand useLocalizedStringfor those four fields, as 9722a4e did forErrorMessage. The stored shape is the same, so MongoDB needs no migration.apps/api/AGENTS.md:194-197says nothing checksenum LanguageagainstLANGUAGES. Add a case to theagreement with schema.prismablock inapps/api/src/core/__tests__/prisma.test.tsthat comparesenum Languageand theLocalizedStringfields withLANGUAGES(Prisma.dmmf.datamodelexposes both), and update that note.hooks.ts: deriving the form type fromLanguagemakestscflag the object literals at lines 43-45, 54 and 174. Three reads of.en/.frkeep compiling and ignorees: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 overLANGUAGES.authoredLanguagesin@/utils/languagealready does;omitBlankLanguagesdoes too but keeps the untrimmed value.SectionCards.tsx: the four en/fr blocks (lines 56, 296, 397, 467) can map the languages and label each input witht(LANGUAGE_LABELS[code]). Keep the${field}-${code}ids: the e2e page model finds#instanceName-enandLoginPageEditor.test.tsxfindsinstanceDetails-en.preview/FullscreenPreviewDialog.tsxis not in the issue.previewLangis typed'en' | 'fr'(line 15), and the mock login form at lines 67-79 ispreviewLang === 'fr' ? … : …, so a Spanish preview shows English. libui'st()takes no language option, so these need a lookup keyed bypreviewLangthat has every language.Which languages get inputs: I would follow #1810. Render inputs and preview tabs for
setupState.activeLanguages(useBrandingFormalready reads the setup state, so it can return them oneditor), startpreviewLangatactiveLanguages[0]instead of'en', and keep everyLANGUAGESkey in the form state so text in an inactive language survives a save. Today an instance whose only active language isesgets no input in any language it offers.Tests
apps/api/test/suites/: PATCH/v1/setupwithesininstanceNameand in a link label, then GET it back.setup.service.test.tsmocks the model, so only a real-database suite catches this.hooks.test.tsx:essurvives 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 withactiveLanguages: ['es']only Spanish inputs render.PreviewColumn.test.tsxandFullscreenPreviewDialog.test.tsx: the tabs follow the active languages and the mock form renders in Spanish.testing/src/specs/admin-branding.spec.ts: activate Spanish asadmin-settings.spec.tsdoes, enable branding, fill#instanceName-es, save, reload, switch the login page to Español and assert the name. Restore all three at the end: theactive languagestests inadmin-settings.spec.tsassume onlyenandfrare on.Land this after #1825:
ec2890880editshooks.ts, ande5ff0cac3(#1704) makes the spec serial and addsenableBrandingCheckbox,instanceNameFrenchInputandsaveAndConfirmto the page model.Triage note written with Claude Code. Code references point at
main@d86740e.