🌎 i18n: React-i18next & i18next Integration (#5720)

* better i18n support an internationalization-framework.

* removed unused package

* auto sort for translation.json

* fixed tests with the new locales function

* added new CI actions from locize

* to use locize a mention in the README.md

* to use locize a mention in the README.md

* updated README.md and added TRANSLATION.md to the repo

* updated TRANSLATION.md badges

* updated README.md to go to the TRANSLATION.md when clicking on the Translation Progress badge

* updated TRANSLATION.md and added a new issue template.

* updated TRANSLATION.md and added a new issue template.

* updated issue template to add the iso code link.

* updated the new GitHub actions for `locize`

* updated label for new issue template --> i18n

* fixed type issue

* Fix eslint

* Fix eslint with key-spacing spacing

* fix: error type

* fix: handle undefined values in SortFilterHeader component

* fix: typing in Image component

* fix: handle optional promptGroup in PromptCard component

* fix: update localize function to accept string type and remove unnecessary JSX element

* fix: update localize function to enforce TranslationKeys type for better type safety

* fix: improve type safety and handle null values in Assistants component

* fix: enhance null checks for fileId in FilesListView component

* fix: localize 'Go back' button text in FilesListView component

* fix: update aria-label for menu buttons and add translation for 'Close Menu'

* docs: add Reasoning UI section for Chain-of-Thought AI models in README

* fix: enhance type safety by adding type for message in MultiMessage component

* fix: improve null checks and optional chaining in useAutoSave hook

* fix: improve handling of optional properties in cleanupPreset function

* fix: ensure isFetchingNextPage defaults to false and improve null checks for messages in Search component

* fix: enhance type safety and null checks in useBuildMessageTree hook

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Ruben Talstra 2025-02-09 18:05:31 +01:00 committed by GitHub
parent 2e8d969e35
commit aae413cc71
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 13448 additions and 38224 deletions

View file

@ -1,4 +1,4 @@
import { useLocalize } from '~/hooks';
import { TranslationKeys, useLocalize } from '~/hooks';
import { BlinkAnimation } from './BlinkAnimation';
import { TStartupConfig } from 'librechat-data-provider';
import SocialLoginRender from './SocialLoginRender';
@ -33,7 +33,7 @@ function AuthLayout({
startupConfig: TStartupConfig | null | undefined;
startupConfigError: unknown | null | undefined;
pathname: string;
error: string | null;
error: TranslationKeys | null;
}) {
const localize = useLocalize();
@ -65,7 +65,7 @@ function AuthLayout({
<img
src="/assets/logo.svg"
className="h-full w-full object-contain"
alt={localize('com_ui_logo', startupConfig?.appTitle ?? 'LibreChat')}
alt={localize('com_ui_logo', { 0: startupConfig?.appTitle ?? 'LibreChat' })}
/>
</div>
</BlinkAnimation>

View file

@ -6,7 +6,7 @@ import type { TRegisterUser, TError } from 'librechat-data-provider';
import type { TLoginLayoutContext } from '~/common';
import { ErrorMessage } from './ErrorMessage';
import { Spinner } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { useLocalize, TranslationKeys } from '~/hooks';
const Registration: React.FC = () => {
const navigate = useNavigate();
@ -56,7 +56,7 @@ const Registration: React.FC = () => {
},
});
const renderInput = (id: string, label: string, type: string, validation: object) => (
const renderInput = (id: string, label: TranslationKeys, type: string, validation: object) => (
<div className="mb-4">
<div className="relative">
<input
@ -114,7 +114,7 @@ const Registration: React.FC = () => {
: 'com_auth_registration_success_insecure',
) +
' ' +
localize('com_auth_email_verification_redirecting', countdown.toString())}
localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
</div>
)}
{!startupConfigError && !isFetching && (

View file

@ -84,7 +84,7 @@ function RequestPasswordReset() {
</h1>
{countdown > 0 && (
<p className="text-center text-lg text-gray-600 dark:text-gray-400">
{localize('com_auth_email_verification_redirecting', countdown.toString())}
{localize('com_auth_email_verification_redirecting', { 0: countdown.toString() })}
</p>
)}
{showResendLink && countdown === 0 && (

View file

@ -220,8 +220,10 @@ export default function DataTable<TData, TValue>({ columns, data }: DataTablePro
<span className="hidden sm:inline">
{localize(
'com_files_number_selected',
`${table.getFilteredSelectedRowModel().rows.length}`,
`${table.getFilteredRowModel().rows.length}`,
{
0: `${table.getFilteredSelectedRowModel().rows.length}`,
1: `${table.getFilteredRowModel().rows.length}`,
},
)}
</span>
<span className="sm:hidden">

View file

@ -9,7 +9,7 @@ import {
DropdownMenuTrigger,
} from '~/components/ui/DropdownMenu';
import { Button } from '~/components/ui/Button';
import useLocalize from '~/hooks/useLocalize';
import { useLocalize, TranslationKeys } from '~/hooks';
import { cn } from '~/utils';
interface SortFilterHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
@ -78,9 +78,12 @@ export function SortFilterHeader<TData, TValue>({
<DropdownMenuSeparator className="dark:bg-gray-500" />
{filters &&
Object.entries(filters).map(([key, values]) =>
values.map((value: string | number) => {
const localizedValue = localize(valueMap?.[value] ?? '');
const filterValue = localizedValue.length ? localizedValue : valueMap?.[value];
values.map((value?: string | number) => {
const translationKey = valueMap?.[value ?? ''];
const filterValue =
translationKey != null && translationKey.length
? localize(translationKey as TranslationKeys)
: String(value);
if (!filterValue) {
return null;
}

View file

@ -44,8 +44,8 @@ export default function PopoverButtons({
const endpoint = overrideEndpoint ?? endpointType ?? _endpoint ?? '';
const model = overrideModel ?? _model;
const isGenerativeModel = model?.toLowerCase()?.includes('gemini') ?? false;
const isChatModel = (!isGenerativeModel && model?.toLowerCase()?.includes('chat')) ?? false;
const isGenerativeModel = model?.toLowerCase().includes('gemini') ?? false;
const isChatModel = (!isGenerativeModel && model?.toLowerCase().includes('chat')) ?? false;
const isTextModel = !isGenerativeModel && !isChatModel && /code|text/.test(model ?? '');
const { showExamples } = optionSettings;

View file

@ -9,12 +9,12 @@ const scaleImage = ({
originalHeight,
containerRef,
}: {
originalWidth: number;
originalHeight: number;
originalWidth?: number;
originalHeight?: number;
containerRef: React.RefObject<HTMLDivElement>;
}) => {
const containerWidth = containerRef.current?.offsetWidth ?? 0;
if (containerWidth === 0 || originalWidth === undefined || originalHeight === undefined) {
if (containerWidth === 0 || originalWidth == null || originalHeight == null) {
return { width: 'auto', height: 'auto' };
}
const aspectRatio = originalWidth / originalHeight;
@ -35,8 +35,8 @@ const Image = ({
height: number;
width: number;
placeholderDimensions?: {
height: string;
width: string;
height?: string;
width?: string;
};
}) => {
const [isLoaded, setIsLoaded] = useState(false);
@ -47,8 +47,8 @@ const Image = ({
const { width: scaledWidth, height: scaledHeight } = useMemo(
() =>
scaleImage({
originalWidth: Number(placeholderDimensions?.width?.split('px')[0]) ?? width,
originalHeight: Number(placeholderDimensions?.height?.split('px')[0]) ?? height,
originalWidth: Number(placeholderDimensions?.width?.split('px')[0] ?? width),
originalHeight: Number(placeholderDimensions?.height?.split('px')[0] ?? height),
containerRef,
}),
[placeholderDimensions, height, width],

View file

@ -64,7 +64,7 @@ const LogContent: React.FC<LogContentProps> = ({ output = '', renderImages, atta
}
// const expirationText = expiresAt
// ? ` ${localize('com_download_expires', format(expiresAt, 'MM/dd/yy HH:mm'))}`
// ? ` ${localize('com_download_expires', { 0: format(expiresAt, 'MM/dd/yy HH:mm') })}`
// : ` ${localize('com_click_to_download')}`;
return (

View file

@ -106,12 +106,12 @@ export default function ToolCall({
const getFinishedText = () => {
if (isMCPToolCall === true) {
return localize('com_assistants_completed_function', function_name);
return localize('com_assistants_completed_function', { 0: function_name });
}
if (domain != null && domain && domain.length !== Constants.ENCODED_DOMAIN_LENGTH) {
return localize('com_assistants_completed_action', domain);
return localize('com_assistants_completed_action', { 0: domain });
}
return localize('com_assistants_completed_function', function_name);
return localize('com_assistants_completed_function', { 0: function_name });
};
return (

View file

@ -34,8 +34,8 @@ export default function ToolPopover({
<div className="bg-token-surface-primary max-w-sm rounded-md p-2 shadow-[0_0_24px_0_rgba(0,0,0,0.05),inset_0_0.5px_0_0_rgba(0,0,0,0.05),0_2px_8px_0_rgba(0,0,0,0.05)]">
<div className="mb-2 text-sm font-medium dark:text-gray-100">
{domain != null && domain
? localize('com_assistants_domain_info', domain)
: localize('com_assistants_function_use', function_name)}
? localize('com_assistants_domain_info', { 0: domain })
: localize('com_assistants_function_use', { 0: function_name })}
</div>
<div className="bg-token-surface-secondary text-token-text-primary dark rounded-md text-xs">
<div className="max-h-32 overflow-y-auto rounded-md p-2 dark:bg-gray-700">

View file

@ -1,14 +1,14 @@
import { TPromptGroup } from 'librechat-data-provider';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
export default function PromptCard({ promptGroup }: { promptGroup: TPromptGroup }) {
export default function PromptCard({ promptGroup }: { promptGroup?: TPromptGroup }) {
return (
<div className="hover:bg-token-main-surface-secondary relative flex w-40 cursor-pointer flex-col gap-2 rounded-2xl border px-3 pb-4 pt-3 text-start align-top text-[15px] shadow-[0_0_2px_0_rgba(0,0,0,0.05),0_4px_6px_0_rgba(0,0,0,0.02)] transition-colors duration-300 ease-in-out fade-in hover:bg-slate-100 dark:border-gray-600 dark:hover:bg-gray-700">
<div className="">
<CategoryIcon className="size-4" category={promptGroup.category || ''} />
<CategoryIcon className="size-4" category={promptGroup?.category ?? ''} />
</div>
<p className="break-word line-clamp-3 text-balance text-gray-600 dark:text-gray-400">
{promptGroup?.oneliner || promptGroup?.productionPrompt?.prompt}
{(promptGroup?.oneliner ?? '') || promptGroup?.productionPrompt?.prompt}
</p>
</div>
);

View file

@ -1,8 +1,8 @@
import { useMemo, memo } from 'react';
import { parseISO, isToday } from 'date-fns';
import { TConversation } from 'librechat-data-provider';
import { useLocalize, TranslationKeys } from '~/hooks';
import { groupConversationsByDate } from '~/utils';
import { useLocalize } from '~/hooks';
import Convo from './Convo';
const Conversations = ({
@ -41,8 +41,7 @@ const Conversations = ({
paddingLeft: '10px',
}}
>
{/* eslint-disable-next-line @typescript-eslint/strict-boolean-expressions */}
{localize(groupName) || groupName}
{localize(groupName as TranslationKeys) || groupName}
</div>
{convos.map((convo, i) => (
<Convo

View file

@ -12,7 +12,7 @@ import {
HoverCardContent,
} from '~/components/ui';
import OptionHover from '~/components/SidePanel/Parameters/OptionHover';
import { useLocalize, useNavigateToConvo } from '~/hooks';
import { TranslationKeys, useLocalize, useNavigateToConvo } from '~/hooks';
import { useForkConvoMutation } from '~/data-provider';
import { useToastContext } from '~/Providers';
import { ESide } from '~/common';
@ -201,7 +201,7 @@ export default function Fork({
align="center"
>
<div className="flex h-6 w-full items-center justify-center text-sm dark:text-gray-200">
{localize(activeSetting)}
{localize(activeSetting as TranslationKeys)}
<HoverCard openDelay={50}>
<HoverCardTrigger asChild>
<InfoIcon className="ml-auto flex h-4 w-4 gap-2 text-gray-500 dark:text-white/50" />
@ -216,7 +216,9 @@ export default function Fork({
<span>{localize('com_ui_fork_info_1')}</span>
<span>{localize('com_ui_fork_info_2')}</span>
<span>
{localize('com_ui_fork_info_3', localize('com_ui_fork_split_target'))}
{localize('com_ui_fork_info_3', {
0: localize('com_ui_fork_split_target'),
})}
</span>
</div>
</HoverCardContent>
@ -233,7 +235,7 @@ export default function Fork({
hoverTitle={
<>
<GitCommit className="h-5 w-5 rotate-90" />
{localize(optionLabels[ForkOptions.DIRECT_PATH])}
{localize(optionLabels[ForkOptions.DIRECT_PATH] as TranslationKeys)}
</>
}
hoverDescription={localize('com_ui_fork_info_visible')}
@ -251,7 +253,7 @@ export default function Fork({
hoverTitle={
<>
<GitBranchPlus className="h-4 w-4 rotate-180" />
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES])}
{localize(optionLabels[ForkOptions.INCLUDE_BRANCHES] as TranslationKeys)}
</>
}
hoverDescription={localize('com_ui_fork_info_branches')}
@ -269,9 +271,9 @@ export default function Fork({
hoverTitle={
<>
<ListTree className="h-5 w-5" />
{`${localize(optionLabels[ForkOptions.TARGET_LEVEL])} (${localize(
'com_endpoint_default',
)})`}
{`${localize(
optionLabels[ForkOptions.TARGET_LEVEL] as TranslationKeys,
)} (${localize('com_endpoint_default')})`}
</>
}
hoverDescription={localize('com_ui_fork_info_target')}

View file

@ -121,7 +121,7 @@ export default function Settings({
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '1')})
({localize('com_endpoint_default_with_num', { 0: '1' })})
</small>
</Label>
<InputNumber

View file

@ -27,12 +27,12 @@ export default function Settings({ conversation, setOption, models, readonly }:
conversation ?? {};
const currentList = useMemo(
() => Object.values(assistantListMap?.[endpoint ?? ''] ?? {}) as Assistant[],
() => Object.values(assistantListMap[endpoint ?? ''] ?? {}) as Assistant[],
[assistantListMap, endpoint],
);
const assistants = useMemo(() => {
const currentAssistants = (currentList ?? []).map(({ id, name }) => ({
const currentAssistants = currentList.map(({ id, name }) => ({
label: name,
value: id,
}));
@ -52,8 +52,8 @@ export default function Settings({ conversation, setOption, models, readonly }:
});
const activeAssistant = useMemo(() => {
if (assistant_id) {
return assistantListMap[endpoint ?? '']?.[assistant_id];
if (assistant_id != null && assistant_id) {
return assistantListMap[endpoint ?? '']?.[assistant_id] as Assistant | null;
}
return null;
@ -70,11 +70,13 @@ export default function Settings({ conversation, setOption, models, readonly }:
}, [models, activeAssistant, localize]);
const [assistantValue, setAssistantValue] = useState<Option>(
activeAssistant ? { label: activeAssistant.name, value: activeAssistant.id } : defaultOption,
activeAssistant != null
? { label: activeAssistant.name ?? '', value: activeAssistant.id }
: defaultOption,
);
useEffect(() => {
if (assistantValue && assistantValue.value === '') {
if (assistantValue.value === '') {
setOption('presetOverride')({
assistant_id: assistantValue.value,
} as Partial<TPreset>);
@ -95,7 +97,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
return;
}
const assistant = assistantListMap[endpoint ?? '']?.[value];
const assistant = assistantListMap[endpoint ?? '']?.[value] as Assistant | null;
if (!assistant) {
setAssistantValue(defaultOption);
return;
@ -103,7 +105,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
setAssistantValue({
label: assistant.name ?? '',
value: assistant.id ?? '',
value: assistant.id || '',
});
setOption('assistant_id')(assistant.id);
if (assistant.model) {

View file

@ -37,7 +37,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
<TextareaAutosize
id={`input-${idx}`}
disabled={readonly}
value={example?.input?.content || ''}
value={example.input.content || ''}
onChange={(e) => setExample(idx, 'input', e.target.value ?? null)}
placeholder="Set example input. Example is ignored if empty."
className={cn(
@ -62,7 +62,7 @@ function Examples({ readonly, examples, setExample, addExample, removeExample }:
<TextareaAutosize
id={`output-${idx}`}
disabled={readonly}
value={example?.output?.content || ''}
value={example.output.content || ''}
onChange={(e) => setExample(idx, 'output', e.target.value ?? null)}
placeholder={'Set example output. Example is ignored if empty.'}
className={cn(

View file

@ -180,7 +180,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', google.topP.default + '')})
({localize('com_endpoint_default_with_num', { 0: google.topP.default + '' })})
</small>
</Label>
<InputNumber
@ -221,7 +221,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="top-k-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_k')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', google.topK.default + '')})
({localize('com_endpoint_default_with_num',{ 0: google.topK.default + '' })})
</small>
</Label>
<InputNumber
@ -261,7 +261,7 @@ export default function Settings({ conversation, setOption, models, readonly }:
<Label htmlFor="max-tokens-int" className="text-left text-sm font-medium">
{localize('com_endpoint_max_output_tokens')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', google.maxOutputTokens.default + '')})
({localize('com_endpoint_default_with_num', { 0: google.maxOutputTokens.default + '' })})
</small>
</Label>
<InputNumber

View file

@ -226,7 +226,7 @@ export default function Settings({
<Label htmlFor="temp-int" className="text-left text-sm font-medium">
{localize('com_endpoint_temperature')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '0.8')})
({localize('com_endpoint_default_with_num', { 0: '0.8' })})
</small>
</Label>
<InputNumber
@ -266,7 +266,7 @@ export default function Settings({
<Label htmlFor="top-p-int" className="text-left text-sm font-medium">
{localize('com_endpoint_top_p')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '1')})
({localize('com_endpoint_default_with_num', { 0: '1' })})
</small>
</Label>
<InputNumber
@ -307,7 +307,7 @@ export default function Settings({
<Label htmlFor="freq-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_frequency_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '0')})
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
</Label>
<InputNumber
@ -348,7 +348,7 @@ export default function Settings({
<Label htmlFor="pres-penalty-int" className="text-left text-sm font-medium">
{localize('com_endpoint_presence_penalty')}{' '}
<small className="opacity-40">
({localize('com_endpoint_default_with_num', '0')})
({localize('com_endpoint_default_with_num', { 0: '0' })})
</small>
</Label>
<InputNumber

View file

@ -9,7 +9,7 @@ const FileDashboardView = () => {
return (
<div className="bg-[#f9f9f9] p-0 lg:p-7">
<div className="ml-3 mt-3 flex flex-row justify-between">
{params?.vectorStoreId && (
{params.vectorStoreId && (
<Button
className="block lg:hidden"
variant={'outline'}

View file

@ -244,9 +244,10 @@ export default function DataTableFile<TData, TValue>({
<div className="ml-4 mr-4 mt-4 flex h-auto items-center justify-end space-x-2 py-4 sm:ml-0 sm:mr-0 sm:h-0">
<div className="text-muted-foreground ml-2 flex-1 text-sm">
{localize(
'com_files_number_selected',
`${table.getFilteredSelectedRowModel().rows.length}`,
`${table.getFilteredRowModel().rows.length}`,
'com_files_number_selected', {
0: `${table.getFilteredSelectedRowModel().rows.length}`,
1: `${table.getFilteredRowModel().rows.length}`,
},
)}
</div>
<Button

View file

@ -2,16 +2,18 @@ import React from 'react';
import FileSidePanel from './FileList/FileSidePanel';
import { Outlet, useNavigate, useParams } from 'react-router-dom';
import FilesSectionSelector from './FilesSectionSelector';
import { useLocalize } from '~/hooks';
import { Button } from '../ui';
export default function FilesListView() {
const params = useParams();
const navigate = useNavigate();
const localize = useLocalize();
return (
<div className="bg-[#f9f9f9] p-0 lg:p-7">
<div className="m-4 flex w-full flex-row justify-between md:m-2">
<FilesSectionSelector />
{params?.fileId && (
{params.fileId != null && params.fileId && (
<Button
className="block lg:hidden"
variant={'outline'}
@ -20,21 +22,21 @@ export default function FilesListView() {
navigate('/d/files');
}}
>
Go back
{localize('com_ui_go_back')}
</Button>
)}
</div>
<div className="flex w-full flex-row divide-x">
<div
className={`mr-2 w-full xl:w-1/3 ${
params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
params.fileId != null && params.fileId ? 'hidden w-1/2 lg:block lg:w-1/2' : 'md:w-full'
}`}
>
<FileSidePanel />
</div>
<div
className={`h-[85vh] w-full overflow-y-auto xl:w-2/3 ${
params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
params.fileId != null && params.fileId ? 'lg:w-1/2' : 'hidden md:w-1/2 lg:block'
}`}
>
<Outlet />

View file

@ -178,7 +178,7 @@ export default function VectorStorePreview() {
<Clock3 className="text-base text-gray-500 md:text-lg lg:text-xl" />
&nbsp;Created At
</span>
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt?.toString()}</span>
<span className="w-1/2 text-gray-500 md:w-3/5">{vectorStore.createdAt.toString()}</span>
</div>
</div>

View file

@ -29,7 +29,7 @@ export default function ModelSelect({
}
const { endpoint: _endpoint, endpointType } = conversation;
const models = modelsQuery?.data?.[_endpoint] ?? [];
const models = modelsQuery.data?.[_endpoint] ?? [];
const endpoint = endpointType ?? _endpoint;
const OptionComponent = multiChatOptions[endpoint];

View file

@ -47,11 +47,11 @@ const errorMessages = {
[ErrorTypes.NO_SYSTEM_MESSAGES]: `com_error_${ErrorTypes.NO_SYSTEM_MESSAGES}`,
[ErrorTypes.EXPIRED_USER_KEY]: (json: TExpiredKey, localize: LocalizeFunction) => {
const { expiredAt, endpoint } = json;
return localize('com_error_expired_user_key', endpoint, expiredAt);
return localize('com_error_expired_user_key', { 0: endpoint, 1: expiredAt });
},
[ErrorTypes.INPUT_LENGTH]: (json: TGenericError, localize: LocalizeFunction) => {
const { info } = json;
return localize('com_error_input_length', info);
return localize('com_error_input_length', { 0: info });
},
[ErrorTypes.GOOGLE_ERROR]: (json: TGenericError) => {
const { info } = json;

View file

@ -70,7 +70,7 @@ function Avatar() {
const megabytes =
fileConfig.avatarSizeLimit != null ? formatBytes(fileConfig.avatarSizeLimit) : 2;
showToast({
message: localize('com_ui_upload_invalid_var', megabytes + ''),
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error',
});
}

View file

@ -41,7 +41,7 @@ export const RevokeKeysButton = ({
const dialogTitle = all
? localize('com_ui_revoke_keys')
: localize('com_ui_revoke_key_endpoint', endpoint);
: localize('com_ui_revoke_key_endpoint', { 0: endpoint });
const dialogMessage = all
? localize('com_ui_revoke_keys_confirm')

View file

@ -15,7 +15,7 @@ export default function DecibelSelector() {
<div className="flex items-center justify-between">
<div>{localize('com_nav_db_sensitivity')}</div>
<div className="w-2" />
<small className="opacity-40">({localize('com_endpoint_default_with_num', '-45')})</small>
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '-45' })})</small>
</div>
<div className="flex items-center justify-between">
<Slider

View file

@ -15,7 +15,7 @@ export default function DecibelSelector() {
<div className="flex items-center justify-between">
<div>{localize('com_nav_playback_rate')}</div>
<div className="w-2" />
<small className="opacity-40">({localize('com_endpoint_default_with_num', '1')})</small>
<small className="opacity-40">({localize('com_endpoint_default_with_num', { 0: '1' })})</small>
</div>
<div className="flex items-center justify-between">
<Slider

View file

@ -42,7 +42,7 @@ const DeleteVersion = ({
htmlFor="dialog-delete-confirm-prompt"
className="text-left text-sm font-medium"
>
{localize('com_ui_delete_confirm_prompt_version_var', name)}
{localize('com_ui_delete_confirm_prompt_version_var', { 0: name })}
</Label>
</div>
</div>

View file

@ -169,7 +169,7 @@ export default function VariableForm({
return (
<InputCombobox
options={field.config.options || []}
placeholder={localize('com_ui_enter_var', field.config.variable)}
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
className={cn(
defaultTextProps,
'rounded px-3 py-2 focus:bg-surface-tertiary',
@ -192,7 +192,7 @@ export default function VariableForm({
defaultTextProps,
'rounded px-3 py-2 focus:bg-surface-tertiary',
)}
placeholder={localize('com_ui_enter_var', field.config.variable)}
placeholder={localize('com_ui_enter_var', { 0: field.config.variable })}
maxRows={8}
/>
);

View file

@ -70,7 +70,7 @@ const PromptForm = () => {
const selectedPrompt = useMemo(
() => (prompts.length > 0 ? prompts[selectionIndex] : undefined),
[prompts, /* eslint-disable-line react-hooks/exhaustive-deps */ selectionIndex],
[prompts, selectionIndex],
);
const { groupsQuery } = useOutletContext<ReturnType<typeof usePromptGroupsNav>>();
@ -102,7 +102,7 @@ const PromptForm = () => {
);
},
onSuccess(data) {
if (alwaysMakeProd && data.prompt._id && data.prompt.groupId) {
if (alwaysMakeProd && data.prompt._id != null && data.prompt._id && data.prompt.groupId) {
makeProductionMutation.mutate({
id: data.prompt._id,
groupId: data.prompt.groupId,
@ -336,7 +336,7 @@ const PromptForm = () => {
variant="ghost"
className="h-10 w-10 border border-border-light p-0 lg:hidden"
onClick={() => setShowSidePanel(true)}
aria-label={localize('com_ui_open_menu')}
aria-label={localize('com_endpoint_open_menu')}
>
<Menu className="size-5" />
</Button>
@ -382,8 +382,8 @@ const PromptForm = () => {
onClick={() => setShowSidePanel(false)}
aria-hidden={!showSidePanel}
tabIndex={showSidePanel ? 0 : -1}
aria-label={localize('com_ui_close_menu')}
/>
<div
className="absolute inset-y-0 right-0 z-50 lg:hidden"
style={{

View file

@ -111,12 +111,12 @@ const VersionCard = ({
onClick={onClick}
aria-selected={isSelected}
role="tab"
aria-label={localize('com_ui_version_var', `${totalVersions - index}`)}
aria-label={localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
>
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between lg:flex-col xl:flex-row">
<h3 className="font-bold text-text-primary">
{localize('com_ui_version_var', `${totalVersions - index}`)}
{localize('com_ui_version_var', { 0: `${totalVersions - index}` })}
</h3>
<time className="text-xs text-text-secondary" dateTime={prompt.createdAt}>
{format(new Date(prompt.createdAt), 'yyyy-MM-dd HH:mm')}

View file

@ -97,7 +97,7 @@ const SharePrompt = ({ group, disabled }: { group?: TPromptGroup; disabled: bool
</OGDialogTrigger>
<OGDialogContent className="w-11/12 max-w-lg" role="dialog" aria-labelledby="dialog-title">
<OGDialogTitle id="dialog-title" className="truncate pr-2" title={group.name}>
{localize('com_ui_share_var', `"${group.name}"`)}
{localize('com_ui_share_var', { 0: `"${group.name}"` })}
</OGDialogTitle>
<form className="p-2" onSubmit={handleSubmit(onSubmit)} aria-describedby="form-description">
<div id="form-description" className="sr-only">

View file

@ -21,7 +21,7 @@ export default function MessagesView({
}}
>
<div className="flex flex-col pb-9 text-sm dark:bg-transparent">
{(_messagesTree && _messagesTree?.length == 0) || _messagesTree === null ? (
{(_messagesTree && _messagesTree.length == 0) || _messagesTree === null ? (
<div className="flex w-full items-center justify-center gap-1 bg-gray-50 p-3 text-sm text-gray-500 dark:border-gray-800/50 dark:bg-gray-800 dark:text-gray-300">
Nothing found
</div>

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import type { TMessage } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
// eslint-disable-next-line import/no-cycle
import Message from './Message';
@ -25,17 +26,16 @@ export default function MultiMessage({
}, [messagesTree?.length]);
useEffect(() => {
if (messagesTree?.length && siblingIdx >= messagesTree?.length) {
if (messagesTree?.length != null && siblingIdx >= messagesTree.length) {
setSiblingIdx(0);
}
}, [siblingIdx, messagesTree?.length, setSiblingIdx]);
if (!(messagesTree && messagesTree?.length)) {
if (!(messagesTree && messagesTree.length)) {
return null;
}
const message = messagesTree[messagesTree.length - siblingIdx - 1];
const message = messagesTree[messagesTree.length - siblingIdx - 1] as TMessage | null;
if (!message) {
return null;
}

View file

@ -171,7 +171,7 @@ function Avatar({
} else {
const megabytes = sizeLimit ? formatBytes(sizeLimit) : 2;
showToast({
message: localize('com_ui_upload_invalid_var', megabytes + ''),
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error',
});
}

View file

@ -38,7 +38,7 @@ export default function FileSearchCheckbox() {
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(AgentCapabilities.file_search, !getValues(AgentCapabilities.file_search), {
shouldDirty: true,
})

View file

@ -20,7 +20,7 @@ export default function ImageVision() {
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value?.toString()}
/>
)}
/>

View file

@ -222,7 +222,7 @@ export default function Parameters({
className="btn btn-neutral flex w-full items-center justify-center gap-2 px-4 py-2 text-sm"
>
<RotateCcw className="h-4 w-4" aria-hidden="true" />
{localize('com_ui_reset_var', localize('com_ui_model_parameters'))}
{localize('com_ui_reset_var', { 0: localize('com_ui_model_parameters') })}
</button>
</div>
</div>

View file

@ -43,7 +43,7 @@ export default function Retrieval({ retrievalModels }: { retrievalModels: Set<st
disabled={isDisabled}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value?.toString()}
/>
)}
/>

View file

@ -38,7 +38,7 @@ export default function HideSequential() {
type="button"
className="flex items-center space-x-2"
onClick={() =>
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
setValue(
AgentCapabilities.hide_sequential_outputs,
!getValues(AgentCapabilities.hide_sequential_outputs),

View file

@ -137,7 +137,7 @@ export default function ShareAgent({
)}
aria-label={localize(
'com_ui_share_var',
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
{ 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
)}
type="button"
>
@ -150,7 +150,7 @@ export default function ShareAgent({
<OGDialogTitle>
{localize(
'com_ui_share_var',
agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent'),
{ 0: agentName != null && agentName !== '' ? `"${agentName}"` : localize('com_ui_agent') },
)}
</OGDialogTitle>
<form

View file

@ -200,7 +200,7 @@ function Avatar({
} else {
const megabytes = fileConfig.avatarSizeLimit ? formatBytes(fileConfig.avatarSizeLimit) : 2;
showToast({
message: localize('com_ui_upload_invalid_var', megabytes + ''),
message: localize('com_ui_upload_invalid_var', { 0: megabytes + '' }),
status: 'error',
});
}

View file

@ -20,7 +20,7 @@ export default function ImageVision() {
checked={field.value}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>

View file

@ -59,7 +59,7 @@ export default function Retrieval({
disabled={isDisabled}
onCheckedChange={field.onChange}
className="relative float-left mr-2 inline-flex h-4 w-4 cursor-pointer"
value={field?.value?.toString()}
value={field.value.toString()}
/>
)}
/>

View file

@ -3,7 +3,7 @@ import { OptionTypes } from 'librechat-data-provider';
import type { DynamicSettingProps } from 'librechat-data-provider';
import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui';
import { useChatContext, useToastContext } from '~/Providers';
import { useLocalize, useParameterEffects } from '~/hooks';
import { TranslationKeys, useLocalize, useParameterEffects } from '~/hooks';
import { cn, defaultTextProps } from '~/utils';
import OptionHover from './OptionHover';
import { ESide } from '~/common';
@ -75,7 +75,7 @@ function DynamicTags({
if (minTags != null && currentTags.length <= minTags) {
showToast({
message: localize('com_ui_min_tags', minTags + ''),
message: localize('com_ui_min_tags',{ 0: minTags + '' }),
status: 'warning',
});
return;
@ -94,7 +94,7 @@ function DynamicTags({
let update = [...(currentTags ?? []), tagText];
if (maxTags != null && update.length > maxTags) {
showToast({
message: localize('com_ui_max_tags', maxTags + ''),
message: localize('com_ui_max_tags', { 0: maxTags + '' }),
status: 'warning',
});
update = update.slice(-maxTags);
@ -126,7 +126,7 @@ function DynamicTags({
htmlFor={`${settingKey}-dynamic-input`}
className="text-left text-sm font-medium"
>
{labelCode ? localize(label) ?? label : label || settingKey}{' '}
{labelCode ? localize(label as TranslationKeys) ?? label : label || settingKey}{' '}
{showDefault && (
<small className="opacity-40">
(
@ -174,7 +174,7 @@ function DynamicTags({
}
}}
onChange={(e) => setTagText(e.target.value)}
placeholder={placeholderCode ? localize(placeholder) ?? placeholder : placeholder}
placeholder={placeholderCode ? localize(placeholder as TranslationKeys) ?? placeholder : placeholder}
className={cn('flex h-10 max-h-10 border-none bg-surface-secondary px-3 py-2')}
/>
</div>
@ -182,7 +182,7 @@ function DynamicTags({
</HoverCardTrigger>
{description && (
<OptionHover
description={descriptionCode ? localize(description) ?? description : description}
description={descriptionCode ? localize(description as TranslationKeys) ?? description : description}
side={descriptionSide as ESide}
/>
)}

View file

@ -27,7 +27,7 @@ function ToolItem({ tool, onAddTool, onRemoveTool, isInstalled = false }: ToolIt
{tool.icon != null && tool.icon ? (
<img
src={tool.icon}
alt={localize('com_ui_logo', tool.name)}
alt={localize('com_ui_logo', { 0: tool.name })}
className="h-full w-full rounded-[5px] bg-white"
/>
) : (

View file

@ -1,4 +1,4 @@
/* eslint-disable indent */
import { cn } from '~/utils/';
export default function AzureMinimalIcon({