From fa815dc3770319cfbea4c780b9f1f67362ddcd1b Mon Sep 17 00:00:00 2001 From: Augusto Brizio <69070819+augustobrizio@users.noreply.github.com> Date: Thu, 22 Oct 2026 17:55:02 +0000 Subject: [PATCH] feat: Localize interface.customWelcome by the user language customWelcome now accepts the same LocalizedString as mcpServers.trustCheckbox: a plain string, or a map from language to message resolved with useLocalizedConfig (exact locale, base language, en, first entry). A plain string behaves as before. --- client/src/components/Chat/Landing.tsx | 8 ++--- .../__tests__/Landing.agent-contact.spec.tsx | 29 +++++++++++++++++-- librechat.example.yaml | 6 ++++ packages/data-provider/src/config.ts | 2 +- 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index b799b6e871e..95c5a59705b 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -4,10 +4,10 @@ import { easings } from '@react-spring/web'; import { EModelEndpoint } from 'librechat-data-provider'; import { BirthdayIcon, TooltipAnchor, SplitText, useRemScale } from '@librechat/client'; import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers'; +import { useLocalize, useAuthContext, useGreeting, useLocalizedConfig } from '~/hooks'; import Description, { isHtmlDescription } from '~/components/ui/Description'; import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; import { getIconEndpoint, getEntity, getModelSpec } from '~/utils'; -import { useLocalize, useAuthContext, useGreeting } from '~/hooks'; import { useChatSettings } from '~/Providers/ChatSettingsContext'; import AgentContact from '~/components/Agents/AgentContact'; import ConvoIcon from '~/components/Endpoints/ConvoIcon'; @@ -44,6 +44,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: const { data: endpointsConfig } = useGetEndpointsQuery(); const { user } = useAuthContext(); const localize = useLocalize(); + const getLocalizedValue = useLocalizedConfig(); const remScale = useRemScale(); const { isTemporary } = useChatSettings(); @@ -90,10 +91,9 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: const selectedAgent = isAgent && conversation?.agent_id != null ? agentsMap?.[conversation.agent_id] : undefined; + const configuredWelcome = startupConfig?.interface?.customWelcome; const customWelcome = - typeof startupConfig?.interface?.customWelcome === 'string' - ? startupConfig.interface.customWelcome - : undefined; + configuredWelcome != null ? getLocalizedValue(configuredWelcome, '') : undefined; const scheduledGreeting = useGreeting(user?.name); diff --git a/client/src/components/Chat/__tests__/Landing.agent-contact.spec.tsx b/client/src/components/Chat/__tests__/Landing.agent-contact.spec.tsx index ee6afed6dc7..624e4b12597 100644 --- a/client/src/components/Chat/__tests__/Landing.agent-contact.spec.tsx +++ b/client/src/components/Chat/__tests__/Landing.agent-contact.spec.tsx @@ -3,6 +3,7 @@ import { RecoilRoot } from 'recoil'; import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { ChatSettingsContext, defaultChatSettings } from '~/Providers/ChatSettingsContext'; +import store from '~/store/language'; import Landing from '../Landing'; let mockConversation: Record | null = null; @@ -36,6 +37,11 @@ jest.mock('~/Providers', () => ({ useAssistantsMapContext: () => mockAssistantMap, })); +jest.mock('~/store', () => ({ + __esModule: true, + default: jest.requireActual('~/store/language').default, +})); + jest.mock('~/data-provider', () => ({ useGetStartupConfig: () => ({ data: { interface: mockInterfaceConfig } }), useGetEndpointsQuery: () => ({ data: {} }), @@ -44,6 +50,7 @@ jest.mock('~/data-provider', () => ({ jest.mock('~/hooks', () => ({ useAuthContext: () => ({ user: undefined }), useGreeting: () => 'Welcome', + useLocalizedConfig: jest.requireActual('~/hooks/useLocalizedConfig').default, useLocalize: () => (key: string) => { const translations: Record = { com_agents_contact: 'Contact', @@ -88,9 +95,12 @@ jest.mock('~/utils', () => ({ jest.mock('~/components/Endpoints/ConvoIcon', () => () => ); -function renderLanding({ isTemporary = false }: { isTemporary?: boolean } = {}) { +function renderLanding({ + isTemporary = false, + lang, +}: { isTemporary?: boolean; lang?: string } = {}) { return render( - + lang != null && set(store.lang, lang)}> @@ -270,4 +280,19 @@ describe('Landing heading', () => { expect(screen.getByText('Hello there')).toBeInTheDocument(); expect(screen.queryByText('Data Analyst')).not.toBeInTheDocument(); }); + + it.each([ + ['es-AR', 'Hola'], + ['en-US', 'Hello'], + ['fr', 'Hello'], + ])('picks the configured welcome for the %s interface language', (lang, expected) => { + mockInterfaceConfig = { + landingHeading: 'greeting', + customWelcome: { en: 'Hello', es: 'Hola' }, + }; + + renderLanding({ lang }); + + expect(screen.getByText(expected)).toBeInTheDocument(); + }); }); diff --git a/librechat.example.yaml b/librechat.example.yaml index 5207e790c2c..447558c319a 100644 --- a/librechat.example.yaml +++ b/librechat.example.yaml @@ -189,6 +189,12 @@ projects: # Custom interface configuration interface: customWelcome: 'Welcome to LibreChat! Enjoy your experience.' + # Or one message per language, picked by the user's UI language (exact locale, then base + # language, then 'en', then the first entry): + # customWelcome: + # en: 'Welcome, {{user.name}}!' + # es: '¡Bienvenido, {{user.name}}!' + # pt-BR: 'Boas-vindas, {{user.name}}!' # What heads the welcome screen when an agent, assistant or model spec is selected (default: entity). # 'entity' shows its name; 'greeting' shows the time-of-day greeting (or customWelcome) with the # user's name, for deployments where the header selector already names the agent. diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 8b410829ddb..32c2f6b9657 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -2882,7 +2882,7 @@ export const interfaceSchema = z }) .optional(), termsOfService: termsOfServiceSchema.optional(), - customWelcome: z.string().optional(), + customWelcome: localizedStringSchema.optional(), /** What heads the welcome screen when an agent, assistant or branded spec is * selected: its name (`entity`), or the time-of-day greeting with the user's * name (`greeting`) for deployments whose header selector already names it. */