From 6f37f0661d30ed15163c93ebde91421bf4369136 Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 4 Sep 2026 10:02:23 +0000 Subject: [PATCH] chore: use semantic tags for interactive elements (#41813) --- oxlint-suppressions.json | 93 ------------------ .../view-annotation-modal/index.tsx | 11 ++- .../agent-tools/setting-built-in-tool.tsx | 7 +- .../chat/chat-with-history/sidebar/item.tsx | 14 ++- .../image-gallery/__tests__/index.spec.tsx | 9 +- .../components/base/image-gallery/index.tsx | 23 +++-- .../base/image-gallery/style.module.css | 21 +++- .../__tests__/image-list.spec.tsx | 95 +++++++++++-------- .../base/image-uploader/image-list.tsx | 44 ++++++--- .../markdown-blocks/__tests__/link.spec.tsx | 20 ++-- .../components/base/markdown-blocks/link.tsx | 7 +- .../base/tab-slider-plain/index.tsx | 17 ++-- web/app/components/datasets/chunk.tsx | 4 +- .../__tests__/upload-dropzone.spec.tsx | 28 ++++-- .../components/upload-dropzone.tsx | 8 +- .../components/__tests__/inputs.spec.tsx | 4 +- .../create/step-two/components/inputs.tsx | 19 ++-- .../__tests__/upload-dropzone.spec.tsx | 28 ++++-- .../local-file/components/upload-dropzone.tsx | 8 +- .../create/ExternalApiSelect.tsx | 42 ++++---- .../create/ExternalApiSelection.tsx | 6 +- .../__tests__/ExternalApiSelect.spec.tsx | 15 +++ .../__tests__/ExternalApiSelection.spec.tsx | 11 +++ .../_base/components/form-input-boolean.tsx | 14 +-- .../components/authorization/radio-group.tsx | 7 +- .../__tests__/monthly-days-selector.spec.tsx | 8 ++ .../__tests__/weekday-selector.spec.tsx | 8 ++ .../components/monthly-days-selector.tsx | 9 +- .../components/weekday-selector.tsx | 9 +- web/i18n/ar-TN/common.json | 1 + web/i18n/de-DE/common.json | 1 + web/i18n/en-US/common.json | 1 + web/i18n/es-ES/common.json | 1 + web/i18n/fa-IR/common.json | 1 + web/i18n/fr-FR/common.json | 1 + web/i18n/hi-IN/common.json | 1 + web/i18n/id-ID/common.json | 1 + web/i18n/it-IT/common.json | 1 + web/i18n/ja-JP/common.json | 1 + web/i18n/ko-KR/common.json | 1 + web/i18n/lo-LA/common.json | 1 + web/i18n/nl-NL/common.json | 1 + web/i18n/pl-PL/common.json | 1 + web/i18n/pt-BR/common.json | 1 + web/i18n/ro-RO/common.json | 1 + web/i18n/ru-RU/common.json | 1 + web/i18n/sl-SI/common.json | 1 + web/i18n/th-TH/common.json | 1 + web/i18n/tr-TR/common.json | 1 + web/i18n/uk-UA/common.json | 1 + web/i18n/vi-VN/common.json | 1 + web/i18n/zh-Hans/common.json | 1 + web/i18n/zh-Hant/common.json | 1 + 53 files changed, 351 insertions(+), 262 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 3c3e2bcf88e..f5a87615ccb 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -191,12 +191,6 @@ "eslint-react/set-state-in-effect": { "count": 5 }, - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 1 } @@ -295,12 +289,6 @@ "eslint-react/set-state-in-effect": { "count": 1 }, - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 4 } @@ -618,14 +606,6 @@ "count": 3 } }, - "web/app/components/base/chat/chat-with-history/sidebar/item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - } - }, "web/app/components/base/chat/chat/__tests__/hooks.spec.tsx": { "no-restricted-imports": { "count": 1 @@ -1209,14 +1189,6 @@ "count": 3 } }, - "web/app/components/base/image-gallery/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-noninteractive-element-interactions": { - "count": 1 - } - }, "web/app/components/base/image-uploader/__tests__/image-preview.spec.tsx": { "erasable-syntax-only/parameter-properties": { "count": 1 @@ -1240,14 +1212,6 @@ "count": 1 } }, - "web/app/components/base/image-uploader/image-list.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-noninteractive-element-interactions": { - "count": 1 - } - }, "web/app/components/base/image-uploader/image-preview.tsx": { "jsx-a11y/no-static-element-interactions": { "count": 1 @@ -1309,12 +1273,6 @@ } }, "web/app/components/base/markdown-blocks/link.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-noninteractive-element-interactions": { - "count": 1 - }, "typescript/no-explicit-any": { "count": 1 } @@ -1628,14 +1586,6 @@ "count": 1 } }, - "web/app/components/base/tab-slider-plain/index.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/base/tag-input/index.stories.tsx": { "jsx-a11y/label-has-associated-control": { "count": 12 @@ -1698,11 +1648,6 @@ "count": 3 } }, - "web/app/components/datasets/chunk.tsx": { - "jsx-a11y/label-has-associated-control": { - "count": 2 - } - }, "web/app/components/datasets/common/document-status-with-action/status-with-action.tsx": { "eslint-react/static-components": { "count": 2 @@ -1794,14 +1739,6 @@ "count": 2 } }, - "web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-noninteractive-element-interactions": { - "count": 1 - } - }, "web/app/components/datasets/create/file-uploader/hooks/use-file-upload.ts": { "no-restricted-imports": { "count": 1 @@ -1963,14 +1900,6 @@ "count": 2 } }, - "web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-noninteractive-element-interactions": { - "count": 1 - } - }, "web/app/components/datasets/documents/create-from-pipeline/data-source/online-documents/index.tsx": { "no-restricted-imports": { "count": 1 @@ -2160,12 +2089,6 @@ "web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx": { "eslint-react/set-state-in-effect": { "count": 1 - }, - "jsx-a11y/click-events-have-key-events": { - "count": 3 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 3 } }, "web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx": { @@ -3369,14 +3292,6 @@ "count": 2 } }, - "web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 2 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 2 - } - }, "web/app/components/workflow/nodes/_base/components/form-input-item.tsx": { "no-restricted-imports": { "count": 1 @@ -3754,14 +3669,6 @@ "count": 1 } }, - "web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/nodes/http/components/key-value/key-value-edit/index.tsx": { "typescript/no-explicit-any": { "count": 2 diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index 26feeab2343..5d6a5dd8c7e 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -294,13 +294,16 @@ const ViewAnnotationModal: FC = ({ appId, isShow, onHide, item, onSave, o {id && (
-
setShowModal(true)} > -
{t(($) => $['editModal.removeThisCache'], { ns: 'appAnnotation' })}
-
+ + {t(($) => $['editModal.removeThisCache'], { ns: 'appAnnotation' })} + +
{t(($) => $['editModal.createdAt'], { ns: 'appAnnotation' })}   diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index 480ba2a0883..1d5cd5c7dde 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -197,13 +197,14 @@ const SettingBuiltInTool: FC = ({
{showBackButton && ( -
{t(($) => $['detailPanel.operation.back'], { ns: 'plugin' })} -
+ )}
diff --git a/web/app/components/base/chat/chat-with-history/sidebar/item.tsx b/web/app/components/base/chat/chat-with-history/sidebar/item.tsx index f4afa8b203d..c6c8a9c1966 100644 --- a/web/app/components/base/chat/chat-with-history/sidebar/item.tsx +++ b/web/app/components/base/chat/chat-with-history/sidebar/item.tsx @@ -28,16 +28,20 @@ const Item: FC = ({ ref={ref} key={item.id} className={cn( - 'group flex cursor-pointer rounded-lg p-1 pl-3 system-sm-medium text-components-menu-item-text hover:bg-state-base-hover', + 'group flex rounded-lg p-1 pl-3 system-sm-medium text-components-menu-item-text hover:bg-state-base-hover', isSelected && 'bg-state-accent-active text-text-accent hover:bg-state-accent-active', )} - onClick={() => onChangeConversation(item.id)} > -
+
+ {item.id !== '' && ( -
e.stopPropagation()}> +
+ createReactI18nextMock({ + 'imageGallery.previewImage': 'Preview image {{index}} of {{total}}', + }), +) + describe('ImageGallery', () => { it('previews the selected image and closes on Escape', async () => { const user = userEvent.setup() @@ -9,7 +16,7 @@ describe('ImageGallery', () => { , ) - await user.click(screen.getAllByTestId('gallery-image')[1]!) + await user.click(screen.getByRole('button', { name: 'Preview image 2 of 2' })) expect(screen.getByTestId('image-preview-container').querySelector('img')).toHaveAttribute( 'src', diff --git a/web/app/components/base/image-gallery/index.tsx b/web/app/components/base/image-gallery/index.tsx index 98659a1b7ec..62147bbf087 100644 --- a/web/app/components/base/image-gallery/index.tsx +++ b/web/app/components/base/image-gallery/index.tsx @@ -3,6 +3,7 @@ import type { FC } from 'react' import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useState } from 'react' +import { useTranslation } from 'react-i18next' import ImagePreview from '@/app/components/base/image-uploader/image-preview' import s from './style.module.css' @@ -29,6 +30,7 @@ const getWidthStyle = (imgNum: number) => { } const ImageGallery: FC = ({ srcs }) => { + const { t } = useTranslation('common') const [imagePreviewUrl, setImagePreviewUrl] = useState('') const imgNum = srcs.length @@ -37,16 +39,25 @@ const ImageGallery: FC = ({ srcs }) => {
{srcs.map((src, index) => !src ? null : ( - $['imageGallery.previewImage'], { + index: index + 1, + total: imgNum, + })} onClick={() => setImagePreviewUrl(src)} - onError={(e) => e.currentTarget.remove()} - /> + > + e.currentTarget.parentElement?.remove()} + /> + ), )} {imagePreviewUrl && ( diff --git a/web/app/components/base/image-gallery/style.module.css b/web/app/components/base/image-gallery/style.module.css index 2e4c62e4563..cb684da8ca0 100644 --- a/web/app/components/base/image-gallery/style.module.css +++ b/web/app/components/base/image-gallery/style.module.css @@ -2,12 +2,29 @@ max-height: 200px; margin-right: 8px; margin-bottom: 8px; - object-fit: contain; - object-position: center; + padding: 0; + overflow: hidden; + line-height: 0; + appearance: none; + background: transparent; + border: 0; border-radius: 8px; cursor: pointer; } +.image { + display: block; + width: 100%; + max-height: 200px; + object-fit: contain; + object-position: center; +} + +.img-1 .image { + width: auto; + max-width: 100%; +} + .item:nth-child(3n) { margin-right: 0; } diff --git a/web/app/components/base/image-uploader/__tests__/image-list.spec.tsx b/web/app/components/base/image-uploader/__tests__/image-list.spec.tsx index 0290d011070..d0727aff915 100644 --- a/web/app/components/base/image-uploader/__tests__/image-list.spec.tsx +++ b/web/app/components/base/image-uploader/__tests__/image-list.spec.tsx @@ -1,9 +1,18 @@ import type { ImageFile } from '@/types/app' -import { fireEvent, render, screen } from '@testing-library/react' +import { fireEvent, render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { createReactI18nextMock } from '@/test/i18n-mock' import { TransferMethod } from '@/types/app' import ImageList from '../image-list' +vi.mock('react-i18next', () => + createReactI18nextMock({ + 'imageGallery.previewImage': 'Preview image {{index}} of {{total}}', + }), +) + +const previewButtonName = 'Preview image 1 of 1' + const createLocalFile = (overrides: Partial = {}): ImageFile => ({ type: TransferMethod.local_file, _id: `local-${Date.now()}-${Math.random()}`, @@ -31,32 +40,32 @@ describe('ImageList', () => { describe('Rendering', () => { it('should render images for each item in the list', () => { const list = [createLocalFile({ _id: 'file-1' }), createLocalFile({ _id: 'file-2' })] - render() + const { container } = render() - const images = screen.getAllByRole('img') + const images = container.querySelectorAll('img') expect(images).toHaveLength(2) }) it('should use base64Url as src for local files', () => { const list = [createLocalFile({ _id: 'file-1', base64Url: 'data:image/png;base64,xyz' })] - render() + const { container } = render() - expect(screen.getByRole('img')).toHaveAttribute('src', 'data:image/png;base64,xyz') + expect(container.querySelector('img')).toHaveAttribute('src', 'data:image/png;base64,xyz') }) it('should use url as src for remote files', () => { const list = [createRemoteFile({ _id: 'file-1', url: 'https://example.com/img.jpg' })] - render() + const { container } = render() - expect(screen.getByRole('img')).toHaveAttribute('src', 'https://example.com/img.jpg') + expect(container.querySelector('img')).toHaveAttribute('src', 'https://example.com/img.jpg') }) - it('should set alt attribute from file name', () => { + it('should use the file name for a non-interactive local thumbnail', () => { const file = new File(['test'], 'my-image.png', { type: 'image/png' }) - const list = [createLocalFile({ _id: 'file-1', file })] + const list = [createLocalFile({ _id: 'file-1', file, progress: 50 })] render() - expect(screen.getByRole('img')).toHaveAttribute('alt', 'my-image.png') + expect(screen.getByRole('img', { name: 'my-image.png' })).toBeInTheDocument() }) }) @@ -72,7 +81,9 @@ describe('ImageList', () => { const list = [createLocalFile({ _id: 'file-1' })] render() - expect(screen.queryByRole('button')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'common.operation.remove' }), + ).not.toBeInTheDocument() }) }) @@ -94,9 +105,10 @@ describe('ImageList', () => { it('should show retry icon when local file upload fails (progress -1)', () => { const onReUpload = vi.fn() const list = [createLocalFile({ _id: 'file-1', progress: -1 })] - render() + render() expect(screen.getByRole('button', { name: 'common.operation.retry' })).toBeInTheDocument() + expect(within(screen.getByRole('listitem')).getAllByRole('button')).toHaveLength(1) expect(screen.queryByText(/\d+\s*%/)).not.toBeInTheDocument() }) }) @@ -131,7 +143,7 @@ describe('ImageList', () => { const list = [createLocalFile({ _id: 'file-1' })] render() - await user.click(screen.getByRole('button')) + await user.click(screen.getByRole('button', { name: 'common.operation.remove' })) expect(onRemove).toHaveBeenCalledTimes(1) expect(onRemove).toHaveBeenCalledWith('file-1') @@ -153,19 +165,17 @@ describe('ImageList', () => { ] render() - await user.click(screen.getByRole('img')) + await user.click(screen.getByRole('button', { name: previewButtonName })) const preview = screen.getByTestId('image-preview-container') expect(preview).toBeInTheDocument() }) - it('should not open image preview when clicking an in-progress image', async () => { - const user = userEvent.setup() + it('should not expose a preview action for an in-progress image', () => { const list = [createLocalFile({ _id: 'file-1', progress: 50 })] - render() - - await user.click(screen.getByRole('img')) + render() + expect(screen.queryByRole('button')).not.toBeInTheDocument() expect(screen.queryByTestId('image-preview-container')).not.toBeInTheDocument() }) @@ -175,7 +185,7 @@ describe('ImageList', () => { render() // Open preview - await user.click(screen.getByRole('img')) + await user.click(screen.getByRole('button', { name: previewButtonName })) expect(screen.queryByTestId('image-preview-container')).toBeInTheDocument() // Close preview @@ -195,7 +205,7 @@ describe('ImageList', () => { ] render() - await user.click(screen.getByRole('img')) + await user.click(screen.getByRole('button', { name: previewButtonName })) const previewImage = screen.getByTestId('image-preview-image') expect(previewImage).toBeInTheDocument() @@ -207,9 +217,11 @@ describe('ImageList', () => { it('should call onImageLinkLoadSuccess for remote URL on load when progress is not -1', () => { const onImageLinkLoadSuccess = vi.fn() const list = [createRemoteFile({ _id: 'file-1', progress: 0 })] - render() + const { container } = render( + , + ) - const img = screen.getByRole('img') + const img = container.querySelector('img')! fireEvent.load(img) expect(onImageLinkLoadSuccess).toHaveBeenCalledWith('file-1') }) @@ -217,9 +229,11 @@ describe('ImageList', () => { it('should not call onImageLinkLoadSuccess for remote URL when progress is -1', () => { const onImageLinkLoadSuccess = vi.fn() const list = [createRemoteFile({ _id: 'file-1', progress: -1 })] - render() + const { container } = render( + , + ) - const img = screen.getByRole('img') + const img = container.querySelector('img')! fireEvent.load(img) expect(onImageLinkLoadSuccess).not.toHaveBeenCalled() @@ -228,9 +242,11 @@ describe('ImageList', () => { it('should not call onImageLinkLoadSuccess for local file type', () => { const onImageLinkLoadSuccess = vi.fn() const list = [createLocalFile({ _id: 'file-1', progress: 50 })] - render() + const { container } = render( + , + ) - const img = screen.getByRole('img') + const img = container.querySelector('img')! fireEvent.load(img) expect(onImageLinkLoadSuccess).not.toHaveBeenCalled() @@ -239,9 +255,11 @@ describe('ImageList', () => { it('should call onImageLinkLoadError for remote URL on error', () => { const onImageLinkLoadError = vi.fn() const list = [createRemoteFile({ _id: 'file-1', progress: 0 })] - render() + const { container } = render( + , + ) - const img = screen.getByRole('img') + const img = container.querySelector('img')! fireEvent.error(img) expect(onImageLinkLoadError).toHaveBeenCalledWith('file-1') @@ -250,9 +268,11 @@ describe('ImageList', () => { it('should not call onImageLinkLoadError for local file type', () => { const onImageLinkLoadError = vi.fn() const list = [createLocalFile({ _id: 'file-1', progress: 50 })] - render() + const { container } = render( + , + ) - const img = screen.getByRole('img') + const img = container.querySelector('img')! fireEvent.error(img) expect(onImageLinkLoadError).not.toHaveBeenCalled() @@ -262,17 +282,16 @@ describe('ImageList', () => { describe('Edge Cases', () => { it('should handle list with mixed local and remote files', () => { const list = [createLocalFile({ _id: 'local-1' }), createRemoteFile({ _id: 'remote-1' })] - render() + const { container } = render() - expect(screen.getAllByRole('img')).toHaveLength(2) + expect(container.querySelectorAll('img')).toHaveLength(2) }) it('should handle item without file property for alt attribute', () => { const list = [createLocalFile({ _id: 'file-1', file: undefined })] - render() + const { container } = render() - const img = screen.getByRole('img') - expect(img).toBeInTheDocument() + expect(container.querySelector('img')).toHaveAttribute('alt', '') }) it('should handle onRemove not provided gracefully', async () => { @@ -280,8 +299,8 @@ describe('ImageList', () => { const list = [createLocalFile({ _id: 'file-1' })] render() - // Button exists, clicking it should not throw - await user.click(screen.getByRole('button')) + // Remove button exists, clicking it should not throw + await user.click(screen.getByRole('button', { name: 'common.operation.remove' })) }) }) }) diff --git a/web/app/components/base/image-uploader/image-list.tsx b/web/app/components/base/image-uploader/image-list.tsx index 61808f69838..02477724c91 100644 --- a/web/app/components/base/image-uploader/image-list.tsx +++ b/web/app/components/base/image-uploader/image-list.tsx @@ -37,6 +37,15 @@ const ImageList: FC = ({ if (item.type === TransferMethod.remote_url && onImageLinkLoadError) onImageLinkLoadError(item._id) } + const renderImage = (item: ImageFile) => ( + {item.progress handleImageLinkLoadSuccess(item)} + onError={() => handleImageLinkLoadError(item)} + src={item.type === TransferMethod.remote_url ? item.url : item.base64Url} + /> + ) return ( <> @@ -47,7 +56,7 @@ const ImageList: FC = ({ role="list" aria-label={t(($) => $['imageUploader.imageList'], { ns: 'common' })} > - {list.map((item) => ( + {list.map((item, index) => (
  • = ({ )}
  • )} - {item.file?.name} handleImageLinkLoadSuccess(item)} - onError={() => handleImageLinkLoadError(item)} - src={item.type === TransferMethod.remote_url ? item.url : item.base64Url} - onClick={() => - item.progress === 100 && - setImagePreviewUrl( - (item.type === TransferMethod.remote_url ? item.url : item.base64Url) as string, - ) - } - /> + {item.progress === 100 ? ( + + ) : ( + renderImage(item) + )} {!readonly && ( ) } else { const rawHref = props.href || node.properties?.href diff --git a/web/app/components/base/tab-slider-plain/index.tsx b/web/app/components/base/tab-slider-plain/index.tsx index f4adf32ebcc..5ac8027fbb4 100644 --- a/web/app/components/base/tab-slider-plain/index.tsx +++ b/web/app/components/base/tab-slider-plain/index.tsx @@ -17,30 +17,31 @@ type ItemProps = Readonly<{ }> const Item: FC = ({ className, isActive, onClick, option, smallItem }) => { return ( -
    !isActive && onClick(option.value)} > -
    {option.text} -
    + {isActive && ( -
    + > )} -
    + ) } diff --git a/web/app/components/datasets/chunk.tsx b/web/app/components/datasets/chunk.tsx index 3b10e2cb7d8..ae394f718d8 100644 --- a/web/app/components/datasets/chunk.tsx +++ b/web/app/components/datasets/chunk.tsx @@ -42,11 +42,11 @@ export const QAPreview: FC = (props) => { return (
    - + Q

    {qa.question}

    - + A

    {qa.answer}

    diff --git a/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx b/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx index 1de0d4c0bba..dfe04d39361 100644 --- a/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx +++ b/web/app/components/datasets/create/file-uploader/components/__tests__/upload-dropzone.spec.tsx @@ -60,14 +60,18 @@ describe('UploadDropzone', () => { expect(icon).toBeInTheDocument() }) - it('should render browse label when extensions are allowed', () => { + it('should render browse button when extensions are allowed', () => { render() - expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }), + ).toBeInTheDocument() }) - it('should not render browse label when no extensions allowed', () => { + it('should not render browse button when no extensions allowed', () => { render() - expect(screen.queryByText('datasetCreation.stepOne.uploader.browse')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }), + ).not.toBeInTheDocument() }) it('should render file size and count limits', () => { @@ -195,12 +199,14 @@ describe('UploadDropzone', () => { }) describe('event handlers', () => { - it('should call onSelectFile when browse label is clicked', () => { + it('should call onSelectFile when browse button is clicked', () => { const onSelectFile = vi.fn() render() - const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse') - fireEvent.click(browseLabel) + const browseButton = screen.getByRole('button', { + name: 'datasetCreation.stepOne.uploader.browse', + }) + fireEvent.click(browseButton) expect(onSelectFile).toHaveBeenCalledTimes(1) }) @@ -252,10 +258,12 @@ describe('UploadDropzone', () => { }) describe('styling', () => { - it('should have cursor-pointer on browse label', () => { + it('should have cursor-pointer on browse button', () => { render() - const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse') - expect(browseLabel).toHaveClass('cursor-pointer') + const browseButton = screen.getByRole('button', { + name: 'datasetCreation.stepOne.uploader.browse', + }) + expect(browseButton).toHaveClass('cursor-pointer') }) }) diff --git a/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx b/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx index 2998f265411..0531f399479 100644 --- a/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx +++ b/web/app/components/datasets/create/file-uploader/components/upload-dropzone.tsx @@ -58,9 +58,13 @@ const UploadDropzone = ({ ? t(($) => $['stepOne.uploader.button'], { ns: 'datasetCreation' }) : t(($) => $['stepOne.uploader.buttonSingleFile'], { ns: 'datasetCreation' })} {acceptTypes.length > 0 && ( -
    diff --git a/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx b/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx index feeb7d1bb93..de21bafd052 100644 --- a/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx +++ b/web/app/components/datasets/create/step-two/components/__tests__/inputs.spec.tsx @@ -10,9 +10,9 @@ describe('DelimiterInput', () => { vi.clearAllMocks() }) - it('should render separator label', () => { + it('should associate the separator label with its input', () => { render() - expect(screen.getByText(`${ns}.stepTwo.separator`))!.toBeInTheDocument() + expect(screen.getByRole('textbox', { name: `${ns}.stepTwo.separator` })).toBeInTheDocument() }) it('should render text input with placeholder', () => { diff --git a/web/app/components/datasets/create/step-two/components/inputs.tsx b/web/app/components/datasets/create/step-two/components/inputs.tsx index efdbf9b6634..dc092db1954 100644 --- a/web/app/components/datasets/create/step-two/components/inputs.tsx +++ b/web/app/components/datasets/create/step-two/components/inputs.tsx @@ -15,20 +15,12 @@ import { NumberFieldInput, NumberFieldUnit, } from '@langgenius/dify-ui/number-field' -import { useRef, useState } from 'react' +import { useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' import Input from '@/app/components/base/input' import { env } from '@/env' -const TextLabel: FC = (props) => { - return ( - - ) -} - const FormField: FC> = (props) => { return ( // Reflow on the container (a @container/chunkfields ancestor), not the @@ -37,7 +29,7 @@ const FormField: FC> = (props) => { // At/above 552px this restores flex-1 with no cap, so three columns resolve // to (container - gaps)/3 — pixel-identical to the stock flex-1 layout.
    - {props.label} +
    {props.label}
    {props.children}
    ) @@ -50,6 +42,8 @@ export const DelimiterInput: FC = ({ ...rest }) => { const { t } = useTranslation() + const generatedInputId = useId() + const inputId = rest.id ?? generatedInputId const isComposing = useRef(false) const [compositionValue, setCompositionValue] = useState('') @@ -57,9 +51,9 @@ export const DelimiterInput: FC = ({ - + + $['stepTwo.separatorTip'], { ns: 'datasetCreation' })} popupClassName="max-w-[200px]" @@ -70,6 +64,7 @@ export const DelimiterInput: FC = ({ } > $['stepTwo.separatorPlaceholder'], { ns: 'datasetCreation' })!} diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx index 938d3392d21..e7e1c43b766 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/__tests__/upload-dropzone.spec.tsx @@ -64,16 +64,20 @@ describe('UploadDropzone', () => { expect(icon).toBeInTheDocument() }) - it('should render browse label when extensions are allowed', () => { + it('should render browse button when extensions are allowed', () => { render() - expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument() + expect( + screen.getByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }), + ).toBeInTheDocument() }) - it('should not render browse label when no extensions allowed', () => { + it('should not render browse button when no extensions allowed', () => { render() - expect(screen.queryByText('datasetCreation.stepOne.uploader.browse')).not.toBeInTheDocument() + expect( + screen.queryByRole('button', { name: 'datasetCreation.stepOne.uploader.browse' }), + ).not.toBeInTheDocument() }) it('should render file size and count limits', () => { @@ -209,12 +213,14 @@ describe('UploadDropzone', () => { }) describe('event handlers', () => { - it('should call onSelectFile when browse label is clicked', () => { + it('should call onSelectFile when browse button is clicked', () => { const onSelectFile = vi.fn() render() - const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse') - fireEvent.click(browseLabel) + const browseButton = screen.getByRole('button', { + name: 'datasetCreation.stepOne.uploader.browse', + }) + fireEvent.click(browseButton) expect(onSelectFile).toHaveBeenCalledTimes(1) }) @@ -269,11 +275,13 @@ describe('UploadDropzone', () => { }) describe('styling', () => { - it('should have cursor-pointer on browse label', () => { + it('should have cursor-pointer on browse button', () => { render() - const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse') - expect(browseLabel).toHaveClass('cursor-pointer') + const browseButton = screen.getByRole('button', { + name: 'datasetCreation.stepOne.uploader.browse', + }) + expect(browseButton).toHaveClass('cursor-pointer') }) }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx index a58cf8f0c99..d1d0f06af05 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/components/upload-dropzone.tsx @@ -64,9 +64,13 @@ const UploadDropzone = ({ ? t(($) => $['stepOne.uploader.button'], { ns: 'datasetCreation' }) : t(($) => $['stepOne.uploader.buttonSingleFile'], { ns: 'datasetCreation' })} {allowedExtensions.length > 0 && ( -
    diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx index 1bd52132e57..251d5ce1ad8 100644 --- a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx @@ -15,12 +15,18 @@ type ApiItem = { } type ExternalApiSelectProps = { + 'aria-labelledby'?: string items: ApiItem[] value?: string onSelect: (item: ApiItem) => void } -const ExternalApiSelect: React.FC = ({ items, value, onSelect }) => { +const ExternalApiSelect: React.FC = ({ + 'aria-labelledby': ariaLabelledBy, + items, + value, + onSelect, +}) => { const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) const [selectedItem, setSelectedItem] = useState( @@ -57,19 +63,21 @@ const ExternalApiSelect: React.FC = ({ items, value, onS return (
    -
    setIsOpen(!isOpen)} > {selectedItem ? ( -
    + -
    + {selectedItem.name} -
    -
    + + ) : ( {t(($) => $['selectExternalKnowledgeAPI.placeholder'], { ns: 'dataset' })} @@ -78,16 +86,17 @@ const ExternalApiSelect: React.FC = ({ items, value, onS -
    + {isOpen && (
    {items.map((item) => ( -
    handleSelect(item)} > -
    + {item.name} @@ -95,19 +104,20 @@ const ExternalApiSelect: React.FC = ({ items, value, onS {item.url} -
    -
    + + ))}
    -
    {t(($) => $.createNewExternalAPI, { ns: 'dataset' })} -
    +
    )} diff --git a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx index c14ebf3fcdb..bf404ac2b30 100644 --- a/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/ExternalApiSelection.tsx @@ -30,6 +30,7 @@ const ExternalApiSelection: React.FC = ({ consoleQuery.datasets.externalKnowledgeApi.get.queryOptions({ input: {} }) const { data } = useQuery(externalKnowledgeApiQueryOptions) const externalKnowledgeApiList = data?.data ?? [] + const externalKnowledgeApiLabelId = useId() const externalKnowledgeIdInputId = useId() const [selectedApiId, setSelectedApiId] = useState(external_knowledge_api_id) const { setShowExternalKnowledgeAPIModal } = useModalContext() @@ -71,12 +72,13 @@ const ExternalApiSelection: React.FC = ({
    - +
    {apiItems.length > 0 ? ( { diff --git a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx index 5ac1d084bc2..74e27ff89bf 100644 --- a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelect.spec.tsx @@ -64,6 +64,21 @@ describe('ExternalApiSelect', () => { }) describe('rendering', () => { + it('uses the referenced visible label to name the trigger', () => { + render( + <> + External API + + , + ) + + expect(screen.getByRole('button', { name: 'External API' })).toBeInTheDocument() + }) + it('should show placeholder when no value selected', () => { render() expect(screen.getByText('dataset.selectExternalKnowledgeAPI.placeholder')).toBeInTheDocument() diff --git a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx index caabcdf7f57..f59765cb4fb 100644 --- a/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/create/__tests__/ExternalApiSelection.spec.tsx @@ -53,11 +53,14 @@ vi.mock('../ExternalApiSelect', () => ({ default: ({ items, onSelect, + 'aria-labelledby': ariaLabelledBy, }: { items: Array<{ value: string; name: string }> onSelect: (item: { value: string; name: string }) => void + 'aria-labelledby'?: string }) => (
    +
    +
    ) } diff --git a/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx b/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx index 659eeae3a31..16f183419b9 100644 --- a/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx +++ b/web/app/components/workflow/nodes/http/components/authorization/radio-group.tsx @@ -16,9 +16,10 @@ type ItemProps = { } const Item: FC = ({ title, onClick, isSelected }) => { return ( -
    = ({ title, onClick, isSelected }) => { onClick={onClick} > {title} -
    + ) } diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/monthly-days-selector.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/monthly-days-selector.spec.tsx index 57e9a6e6915..27c96c1d818 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/monthly-days-selector.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/monthly-days-selector.spec.tsx @@ -3,6 +3,14 @@ import userEvent from '@testing-library/user-event' import MonthlyDaysSelector from '../monthly-days-selector' describe('trigger-schedule/monthly-days-selector', () => { + it('names the monthly day selection group', () => { + render() + + expect( + screen.getByRole('group', { name: 'workflow.nodes.triggerSchedule.days' }), + ).toBeInTheDocument() + }) + it('toggles monthly days and shows the day-31 warning', async () => { const user = userEvent.setup() const onChange = vi.fn() diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/weekday-selector.spec.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/weekday-selector.spec.tsx index 8672bf26bba..4992a047342 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/weekday-selector.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/__tests__/weekday-selector.spec.tsx @@ -3,6 +3,14 @@ import userEvent from '@testing-library/user-event' import WeekdaySelector from '../weekday-selector' describe('trigger-schedule/weekday-selector', () => { + it('names the weekday selection group', () => { + render() + + expect( + screen.getByRole('group', { name: 'workflow.nodes.triggerSchedule.weekdays' }), + ).toBeInTheDocument() + }) + it('keeps at least one weekday selected', async () => { const user = userEvent.setup() const onChange = vi.fn() diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx index c56da410917..7254de1f316 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/monthly-days-selector.tsx @@ -1,3 +1,4 @@ +import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset' import * as React from 'react' import { useTranslation } from 'react-i18next' import { Infotip } from '@/app/components/base/infotip' @@ -29,10 +30,10 @@ const MonthlyDaysSelector = ({ selectedDays, onChange }: MonthlyDaysSelectorProp ] return ( -
    -
    )} -
    + ) } diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/weekday-selector.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/weekday-selector.tsx index 3e6911c18f5..ecde0844388 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/weekday-selector.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/weekday-selector.tsx @@ -1,3 +1,4 @@ +import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset' import * as React from 'react' import { useTranslation } from 'react-i18next' @@ -30,10 +31,10 @@ const WeekdaySelector = ({ selectedDays, onChange }: WeekdaySelectorProps) => { const isDaySelected = (dayKey: string) => selectedDays.includes(dayKey) return ( -
    -
    + ) } diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index b348de9e896..cb58e11eacb 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "تحميل {{type}} لا يمكن أن يتجاوز {{size}}", "fileUploader.uploadFromComputerReadError": "فشل قراءة الملف، يرجى المحاولة مرة أخرى.", "fileUploader.uploadFromComputerUploadError": "فشل تحميل الملف، يرجى التحميل مرة أخرى.", + "imageGallery.previewImage": "معاينة الصورة {{index}} من {{total}}", "imageInput.browse": "تصفح", "imageInput.dropImageHere": "أسقط صورتك هنا، أو", "imageInput.supportedFormats": "يدعم PNG و JPG و JPEG و WEBP و GIF", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index 46cbebdd639..3eca0f3ce2f 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Der Upload von {{type}} darf {{size}} nicht überschreiten", "fileUploader.uploadFromComputerReadError": "Lesen der Datei fehlgeschlagen, bitte versuchen Sie es erneut.", "fileUploader.uploadFromComputerUploadError": "Datei-Upload fehlgeschlagen, bitte erneut hochladen.", + "imageGallery.previewImage": "Bild {{index}} von {{total}} in der Vorschau anzeigen", "imageInput.browse": "blättern", "imageInput.dropImageHere": "Laden Sie Ihr Bild hierher hoch oder", "imageInput.supportedFormats": "Unterstützt PNG, JPG, JPEG, WEBP und GIF", diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index 76475612a39..f7f58407bab 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Upload {{type}} cannot exceed {{size}}", "fileUploader.uploadFromComputerReadError": "File reading failed, please try again.", "fileUploader.uploadFromComputerUploadError": "File upload failed, please upload again.", + "imageGallery.previewImage": "Preview image {{index}} of {{total}}", "imageInput.browse": "browse", "imageInput.dropImageHere": "Drop your image here, or", "imageInput.supportedFormats": "Supports PNG, JPG, JPEG, WEBP and GIF", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 7b1b6adeff3..87bba9f0858 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "La carga de {{type}} no puede exceder {{size}}", "fileUploader.uploadFromComputerReadError": "Error en la lectura del archivo, inténtelo de nuevo.", "fileUploader.uploadFromComputerUploadError": "Error en la carga del archivo, vuelva a cargarlo.", + "imageGallery.previewImage": "Vista previa de la imagen {{index}} de {{total}}", "imageInput.browse": "navegar", "imageInput.dropImageHere": "Deja tu imagen aquí, o", "imageInput.supportedFormats": "Soporta PNG, JPG, JPEG, WEBP y GIF", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index c974aef526d..69c94939366 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "آپلود {{type}} نمی تواند از {{size}} تجاوز کند", "fileUploader.uploadFromComputerReadError": "خواندن فایل انجام نشد، لطفا دوباره امتحان کنید.", "fileUploader.uploadFromComputerUploadError": "آپلود فایل انجام نشد، لطفا دوباره آپلود کنید.", + "imageGallery.previewImage": "پیش‌نمایش تصویر {{index}} از {{total}}", "imageInput.browse": "مرورگر", "imageInput.dropImageHere": "عکس خود را اینجا رها کنید، یا", "imageInput.supportedFormats": "از فرمت‌های PNG، JPG، JPEG، WEBP و GIF پشتیبانی می‌کند", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index b842ca44a2b..4640a108c3a 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Le téléchargement de {{type}} ne peut pas dépasser {{size}}", "fileUploader.uploadFromComputerReadError": "Échec de la lecture du fichier, veuillez réessayer.", "fileUploader.uploadFromComputerUploadError": "Le téléchargement du fichier a échoué, veuillez le télécharger à nouveau.", + "imageGallery.previewImage": "Prévisualiser l’image {{index}} sur {{total}}", "imageInput.browse": "naviguer", "imageInput.dropImageHere": "Déposez votre image ici, ou", "imageInput.supportedFormats": "Prend en charge PNG, JPG, JPEG, WEBP et GIF", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index a6c3ef31328..275177d98f1 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "{{type}} अपलोड {{size}} से ज़्यादा नहीं हो सकता", "fileUploader.uploadFromComputerReadError": "फ़ाइल पढ़ना विफल रहा, कृपया पुनः प्रयास करें.", "fileUploader.uploadFromComputerUploadError": "फ़ाइल अपलोड विफल रही, कृपया फिर से अपलोड करें।", + "imageGallery.previewImage": "{{total}} में से चित्र {{index}} का पूर्वावलोकन करें", "imageInput.browse": "ब्राउज़ करें", "imageInput.dropImageHere": "अपनी छवि यहाँ छोड़ें, या", "imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP और GIF का समर्थन करता है", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 343f59965a8..586dc35e203 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Unggahan {{type}} tidak boleh melebihi {{size}}", "fileUploader.uploadFromComputerReadError": "Pembacaan file gagal, silakan coba lagi.", "fileUploader.uploadFromComputerUploadError": "Unggahan file gagal, silakan unggah lagi.", + "imageGallery.previewImage": "Pratinjau gambar {{index}} dari {{total}}", "imageInput.browse": "Telusuri", "imageInput.dropImageHere": "Letakkan gambar Anda di sini, atau", "imageInput.supportedFormats": "Mendukung PNG, JPG, JPEG, WEBP dan GIF", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index df35dfb25f3..eab7932c4e4 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Il caricamento di {{type}} non può superare {{size}}", "fileUploader.uploadFromComputerReadError": "Lettura del file non riuscita, riprovare.", "fileUploader.uploadFromComputerUploadError": "Caricamento del file non riuscito, carica di nuovo.", + "imageGallery.previewImage": "Visualizza l'anteprima dell'immagine {{index}} di {{total}}", "imageInput.browse": "sfogliare", "imageInput.dropImageHere": "Trascina la tua immagine qui, oppure", "imageInput.supportedFormats": "Supporta PNG, JPG, JPEG, WEBP e GIF", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index 9456ebe50e1..364681fcde2 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "{{type}}のアップロードは{{size}}を超えてはなりません", "fileUploader.uploadFromComputerReadError": "ファイルの読み取りに失敗しました。もう一度やり直してください。", "fileUploader.uploadFromComputerUploadError": "ファイルのアップロードに失敗しました。再度アップロードしてください。", + "imageGallery.previewImage": "{{total}}枚中{{index}}枚目の画像をプレビュー", "imageInput.browse": "ブラウズする", "imageInput.dropImageHere": "ここに画像をドロップするか、", "imageInput.supportedFormats": "PNG、JPG、JPEG、WEBP、および GIF をサポートしています。", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index 788bc27b70c..31dfd475da1 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "{{type}} 업로드는 {{size}}를 초과할 수 없습니다.", "fileUploader.uploadFromComputerReadError": "파일 읽기에 실패했습니다. 다시 시도하십시오.", "fileUploader.uploadFromComputerUploadError": "파일 업로드에 실패했습니다. 다시 업로드하십시오.", + "imageGallery.previewImage": "이미지 {{total}}개 중 {{index}}번째 미리보기", "imageInput.browse": "찾아보기", "imageInput.dropImageHere": "여기에 이미지를 드롭하거나", "imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP 및 GIF 를 지원합니다.", diff --git a/web/i18n/lo-LA/common.json b/web/i18n/lo-LA/common.json index 0727087aea2..55b0e58ecd2 100644 --- a/web/i18n/lo-LA/common.json +++ b/web/i18n/lo-LA/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "ອັບໂຫຼດ {{type}} ຕ້ອງບໍ່ເກີນ {{size}}", "fileUploader.uploadFromComputerReadError": "ອ່ານໄຟລ໌ບໍ່ສຳເລັດ, ກະລຸນາລອງໃໝ່.", "fileUploader.uploadFromComputerUploadError": "ອັບໂຫຼດໄຟລ໌ບໍ່ສຳເລັດ, ກະລຸນາອັບໂຫຼດໃໝ່.", + "imageGallery.previewImage": "ສະແດງຕົວຢ່າງຮູບທີ {{index}} ຈາກ {{total}}", "imageInput.browse": "ເລືອກເບິ່ງ", "imageInput.dropImageHere": "ວາງຮູບຂອງທ່ານທີ່ນີ້, ຫຼື", "imageInput.supportedFormats": "ຮອງຮັບ PNG, JPG, JPEG, WEBP ແລະ GIF", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index a066a08717f..52fb04cec72 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Upload {{type}} cannot exceed {{size}}", "fileUploader.uploadFromComputerReadError": "File reading failed, please try again.", "fileUploader.uploadFromComputerUploadError": "File upload failed, please upload again.", + "imageGallery.previewImage": "Afbeelding {{index}} van {{total}} bekijken", "imageInput.browse": "browse", "imageInput.dropImageHere": "Drop your image here, or", "imageInput.supportedFormats": "Supports PNG, JPG, JPEG, WEBP and GIF", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index ad7c068017a..9068c36ff1d 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Przesyłanie {{type}} nie może przekraczać {{size}}", "fileUploader.uploadFromComputerReadError": "Odczyt pliku nie powiódł się, spróbuj ponownie.", "fileUploader.uploadFromComputerUploadError": "Przesyłanie pliku nie powiodło się, prześlij ponownie.", + "imageGallery.previewImage": "Wyświetl podgląd obrazu {{index}} z {{total}}", "imageInput.browse": "przeglądaj", "imageInput.dropImageHere": "Upuść swój obraz tutaj, lub", "imageInput.supportedFormats": "Obsługuje PNG, JPG, JPEG, WEBP i GIF", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index 19d4f1a554a..2a603c397c5 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "O upload de {{type}} não pode exceder {{size}}", "fileUploader.uploadFromComputerReadError": "Falha na leitura do arquivo, tente novamente.", "fileUploader.uploadFromComputerUploadError": "Falha no upload do arquivo, faça o upload novamente.", + "imageGallery.previewImage": "Visualizar imagem {{index}} de {{total}}", "imageInput.browse": "navegar", "imageInput.dropImageHere": "Arraste sua imagem aqui, ou", "imageInput.supportedFormats": "Suporta PNG, JPG, JPEG, WEBP e GIF", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index b920afc5b2f..18c8e7ac479 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Încărcarea {{type}} nu poate depăși {{size}}", "fileUploader.uploadFromComputerReadError": "Citirea fișierului a eșuat, vă rugăm să încercați din nou.", "fileUploader.uploadFromComputerUploadError": "Încărcarea fișierului a eșuat, vă rugăm să încărcați din nou.", + "imageGallery.previewImage": "Previzualizează imaginea {{index}} din {{total}}", "imageInput.browse": "naviga", "imageInput.dropImageHere": "Trageți imaginea aici sau", "imageInput.supportedFormats": "Suportă PNG, JPG, JPEG, WEBP și GIF", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index 150f549514d..2ed10ac7214 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Загрузка {{type}} не может превышать {{size}}", "fileUploader.uploadFromComputerReadError": "Чтение файла не удалось, пожалуйста, повторите попытку.", "fileUploader.uploadFromComputerUploadError": "Загрузка файла не удалась, пожалуйста, загрузите еще раз.", + "imageGallery.previewImage": "Просмотреть изображение {{index}} из {{total}}", "imageInput.browse": "просмотр", "imageInput.dropImageHere": "Перетащите ваше изображение сюда или", "imageInput.supportedFormats": "Поддерживает PNG, JPG, JPEG, WEBP и GIF", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index 95b4fd6ccc5..b2d67f7400d 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Nalaganje {{type}} ne sme presegati {{size}}", "fileUploader.uploadFromComputerReadError": "Branje datoteke ni uspelo, poskusite znova.", "fileUploader.uploadFromComputerUploadError": "Nalaganje datoteke ni uspelo, naložite ga znova.", + "imageGallery.previewImage": "Prikaži predogled slike {{index}} od {{total}}", "imageInput.browse": "brskati", "imageInput.dropImageHere": "Tukaj spustite svojo sliko ali", "imageInput.supportedFormats": "Podpira PNG, JPG, JPEG, WEBP in GIF", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index 64f2f9e3d88..ebe96553cb6 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "อัปโหลด {{type}} ต้องไม่เกิน {{size}}", "fileUploader.uploadFromComputerReadError": "การอ่านไฟล์ล้มเหลว โปรดลองอีกครั้ง", "fileUploader.uploadFromComputerUploadError": "อัปโหลดไฟล์ล้มเหลว โปรดอัปโหลดอีกครั้ง", + "imageGallery.previewImage": "ดูตัวอย่างรูปภาพที่ {{index}} จาก {{total}}", "imageInput.browse": "ท่องเว็บ", "imageInput.dropImageHere": "วางภาพของคุณที่นี่ หรือ", "imageInput.supportedFormats": "รองรับ PNG, JPG, JPEG, WEBP และ GIF", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index a0cc36eddfe..1a979ec22ea 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "{{type}} yüklemesi {{size}}'ı aşamaz", "fileUploader.uploadFromComputerReadError": "Dosya okuma başarısız oldu, lütfen tekrar deneyin.", "fileUploader.uploadFromComputerUploadError": "Dosya yükleme başarısız oldu, lütfen tekrar yükleyin.", + "imageGallery.previewImage": "{{total}} görselden {{index}}. görseli önizle", "imageInput.browse": "göz atın", "imageInput.dropImageHere": "Görüntünüzü buraya bırakın veya", "imageInput.supportedFormats": "PNG, JPG, JPEG, WEBP ve GIF'i destekler", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index b198a13d192..a97f8aacce3 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Завантаження {{type}} не може перевищувати {{size}}", "fileUploader.uploadFromComputerReadError": "Не вдалося прочитати файл, будь ласка, спробуйте ще раз.", "fileUploader.uploadFromComputerUploadError": "Не вдалося завантажити файл, будь ласка, завантажте ще раз.", + "imageGallery.previewImage": "Переглянути зображення {{index}} із {{total}}", "imageInput.browse": "перегляд", "imageInput.dropImageHere": "Перетягніть зображення сюди або", "imageInput.supportedFormats": "Підтримує PNG, JPG, JPEG, WEBP і GIF", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index 2b450a6d015..300eb97cbbc 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "Tải lên {{type}} không được vượt quá {{size}}", "fileUploader.uploadFromComputerReadError": "Đọc tệp không thành công, vui lòng thử lại.", "fileUploader.uploadFromComputerUploadError": "Tải lên tệp không thành công, vui lòng tải lên lại.", + "imageGallery.previewImage": "Xem trước ảnh {{index}} trên {{total}}", "imageInput.browse": "duyệt", "imageInput.dropImageHere": "Kéo hình ảnh của bạn vào đây, hoặc", "imageInput.supportedFormats": "Hỗ trợ PNG, JPG, JPEG, WEBP và GIF", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 72df809d2b9..26552a33ee8 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "上传 {{type}} 不能超过 {{size}}", "fileUploader.uploadFromComputerReadError": "文件读取失败,请重新选择。", "fileUploader.uploadFromComputerUploadError": "文件上传失败,请重新上传。", + "imageGallery.previewImage": "预览第 {{index}} 张图片,共 {{total}} 张", "imageInput.browse": "浏览", "imageInput.dropImageHere": "将图片拖放到此处,或", "imageInput.supportedFormats": "支持 PNG、JPG、JPEG、WEBP 和 GIF 格式", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index 1559004d997..7b001d7afb5 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -177,6 +177,7 @@ "fileUploader.uploadFromComputerLimit": "上傳 {{type}} 不能超過 {{size}}", "fileUploader.uploadFromComputerReadError": "檔案讀取失敗,請重試。", "fileUploader.uploadFromComputerUploadError": "檔案上傳失敗,請重新上傳。", + "imageGallery.previewImage": "預覽第 {{index}} 張圖片,共 {{total}} 張", "imageInput.browse": "瀏覽", "imageInput.dropImageHere": "將您的圖片放在這裡,或", "imageInput.supportedFormats": "支援 PNG、JPG、JPEG、WEBP 和 GIF",