🌎 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

@ -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>
);