Merge pull request #1214 from danielaskdd/upload-error
Feat: Improve file upload error handling
This commit is contained in:
@@ -1 +1 @@
|
|||||||
__api_version__ = "1.2.7"
|
__api_version__ = "1.2.8"
|
||||||
|
@@ -542,6 +542,7 @@ def create_document_routes(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
InsertResponse: A response object containing the upload status and a message.
|
InsertResponse: A response object containing the upload status and a message.
|
||||||
|
status can be "success", "duplicated", or error is thrown.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
HTTPException: If the file type is not supported (400) or other errors occur (500).
|
HTTPException: If the file type is not supported (400) or other errors occur (500).
|
||||||
@@ -554,6 +555,13 @@ def create_document_routes(
|
|||||||
)
|
)
|
||||||
|
|
||||||
file_path = doc_manager.input_dir / file.filename
|
file_path = doc_manager.input_dir / file.filename
|
||||||
|
# Check if file already exists
|
||||||
|
if file_path.exists():
|
||||||
|
return InsertResponse(
|
||||||
|
status="duplicated",
|
||||||
|
message=f"File '{file.filename}' already exists in the input directory.",
|
||||||
|
)
|
||||||
|
|
||||||
with open(file_path, "wb") as buffer:
|
with open(file_path, "wb") as buffer:
|
||||||
shutil.copyfileobj(file.file, buffer)
|
shutil.copyfileobj(file.file, buffer)
|
||||||
|
|
||||||
|
1
lightrag/api/webui/assets/index-Bwboeqcm.css
generated
1
lightrag/api/webui/assets/index-Bwboeqcm.css
generated
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
lightrag/api/webui/assets/index-Cl6-O9yL.css
generated
Normal file
1
lightrag/api/webui/assets/index-Cl6-O9yL.css
generated
Normal file
File diff suppressed because one or more lines are too long
4
lightrag/api/webui/index.html
generated
4
lightrag/api/webui/index.html
generated
@@ -8,8 +8,8 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
<link rel="icon" type="image/svg+xml" href="logo.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Lightrag</title>
|
<title>Lightrag</title>
|
||||||
<script type="module" crossorigin src="/webui/assets/index-B4QL89Xd.js"></script>
|
<script type="module" crossorigin src="/webui/assets/index-Bz28HSH8.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/webui/assets/index-Bwboeqcm.css">
|
<link rel="stylesheet" crossorigin href="/webui/assets/index-Cl6-O9yL.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
@@ -109,7 +109,7 @@ export type QueryResponse = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DocActionResponse = {
|
export type DocActionResponse = {
|
||||||
status: 'success' | 'partial_success' | 'failure'
|
status: 'success' | 'partial_success' | 'failure' | 'duplicated'
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
|
import { FileRejection } from 'react-dropzone'
|
||||||
import Button from '@/components/ui/Button'
|
import Button from '@/components/ui/Button'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -23,57 +24,132 @@ export default function UploadDocumentsDialog() {
|
|||||||
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
const [progresses, setProgresses] = useState<Record<string, number>>({})
|
||||||
const [fileErrors, setFileErrors] = useState<Record<string, string>>({})
|
const [fileErrors, setFileErrors] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
const handleRejectedFiles = useCallback(
|
||||||
|
(rejectedFiles: FileRejection[]) => {
|
||||||
|
// Process rejected files and add them to fileErrors
|
||||||
|
rejectedFiles.forEach(({ file, errors }) => {
|
||||||
|
// Get the first error message
|
||||||
|
let errorMsg = errors[0]?.message || t('documentPanel.uploadDocuments.fileUploader.fileRejected', { name: file.name })
|
||||||
|
|
||||||
|
// Simplify error message for unsupported file types
|
||||||
|
if (errorMsg.includes('file-invalid-type')) {
|
||||||
|
errorMsg = t('documentPanel.uploadDocuments.fileUploader.unsupportedType')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set progress to 100% to display error message
|
||||||
|
setProgresses((pre) => ({
|
||||||
|
...pre,
|
||||||
|
[file.name]: 100
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Add error message to fileErrors
|
||||||
|
setFileErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[file.name]: errorMsg
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setProgresses, setFileErrors, t]
|
||||||
|
)
|
||||||
|
|
||||||
const handleDocumentsUpload = useCallback(
|
const handleDocumentsUpload = useCallback(
|
||||||
async (filesToUpload: File[]) => {
|
async (filesToUpload: File[]) => {
|
||||||
setIsUploading(true)
|
setIsUploading(true)
|
||||||
setFileErrors({})
|
|
||||||
|
// Only clear errors for files that are being uploaded, keep errors for rejected files
|
||||||
|
setFileErrors(prev => {
|
||||||
|
const newErrors = { ...prev };
|
||||||
|
filesToUpload.forEach(file => {
|
||||||
|
delete newErrors[file.name];
|
||||||
|
});
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show uploading toast
|
||||||
|
const toastId = toast.loading(t('documentPanel.uploadDocuments.batch.uploading'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toast.promise(
|
// Track errors locally to ensure we have the final state
|
||||||
(async () => {
|
const uploadErrors: Record<string, string> = {}
|
||||||
try {
|
|
||||||
await Promise.all(
|
|
||||||
filesToUpload.map(async (file) => {
|
|
||||||
try {
|
|
||||||
const result = await uploadDocument(file, (percentCompleted: number) => {
|
|
||||||
console.debug(t('documentPanel.uploadDocuments.single.uploading', { name: file.name, percent: percentCompleted }))
|
|
||||||
setProgresses((pre) => ({
|
|
||||||
...pre,
|
|
||||||
[file.name]: percentCompleted
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.status !== 'success') {
|
await Promise.all(
|
||||||
setFileErrors(prev => ({
|
filesToUpload.map(async (file) => {
|
||||||
...prev,
|
try {
|
||||||
[file.name]: result.message
|
// Initialize upload progress
|
||||||
}))
|
setProgresses((pre) => ({
|
||||||
}
|
...pre,
|
||||||
} catch (err) {
|
[file.name]: 0
|
||||||
setFileErrors(prev => ({
|
}))
|
||||||
...prev,
|
|
||||||
[file.name]: errorMessage(err)
|
const result = await uploadDocument(file, (percentCompleted: number) => {
|
||||||
}))
|
console.debug(t('documentPanel.uploadDocuments.single.uploading', { name: file.name, percent: percentCompleted }))
|
||||||
}
|
setProgresses((pre) => ({
|
||||||
})
|
...pre,
|
||||||
)
|
[file.name]: percentCompleted
|
||||||
} catch (error) {
|
}))
|
||||||
console.error('Upload failed:', error)
|
})
|
||||||
|
|
||||||
|
if (result.status === 'duplicated') {
|
||||||
|
uploadErrors[file.name] = t('documentPanel.uploadDocuments.fileUploader.duplicateFile')
|
||||||
|
setFileErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[file.name]: t('documentPanel.uploadDocuments.fileUploader.duplicateFile')
|
||||||
|
}))
|
||||||
|
} else if (result.status !== 'success') {
|
||||||
|
uploadErrors[file.name] = result.message
|
||||||
|
setFileErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[file.name]: result.message
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Upload failed for ${file.name}:`, err)
|
||||||
|
|
||||||
|
// Handle HTTP errors, including 400 errors
|
||||||
|
let errorMsg = errorMessage(err)
|
||||||
|
|
||||||
|
// If it's an axios error with response data, try to extract more detailed error info
|
||||||
|
if (err && typeof err === 'object' && 'response' in err) {
|
||||||
|
const axiosError = err as { response?: { status: number, data?: { detail?: string } } }
|
||||||
|
if (axiosError.response?.status === 400) {
|
||||||
|
// Extract specific error message from backend response
|
||||||
|
errorMsg = axiosError.response.data?.detail || errorMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set progress to 100% to display error message
|
||||||
|
setProgresses((pre) => ({
|
||||||
|
...pre,
|
||||||
|
[file.name]: 100
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record error message in both local tracking and state
|
||||||
|
uploadErrors[file.name] = errorMsg
|
||||||
|
setFileErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
[file.name]: errorMsg
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
})(),
|
})
|
||||||
{
|
|
||||||
loading: t('documentPanel.uploadDocuments.batch.uploading'),
|
|
||||||
success: t('documentPanel.uploadDocuments.batch.success'),
|
|
||||||
error: t('documentPanel.uploadDocuments.batch.error')
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check if any files failed to upload using our local tracking
|
||||||
|
const hasErrors = Object.keys(uploadErrors).length > 0
|
||||||
|
|
||||||
|
// Update toast status
|
||||||
|
if (hasErrors) {
|
||||||
|
toast.error(t('documentPanel.uploadDocuments.batch.error'), { id: toastId })
|
||||||
|
} else {
|
||||||
|
toast.success(t('documentPanel.uploadDocuments.batch.success'), { id: toastId })
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }))
|
console.error('Unexpected error during upload:', err)
|
||||||
|
toast.error(t('documentPanel.uploadDocuments.generalError', { error: errorMessage(err) }), { id: toastId })
|
||||||
} finally {
|
} finally {
|
||||||
setIsUploading(false)
|
setIsUploading(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[setIsUploading, setProgresses, t]
|
[setIsUploading, setProgresses, setFileErrors, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -107,6 +183,7 @@ export default function UploadDocumentsDialog() {
|
|||||||
maxSize={200 * 1024 * 1024}
|
maxSize={200 * 1024 * 1024}
|
||||||
description={t('documentPanel.uploadDocuments.fileTypes')}
|
description={t('documentPanel.uploadDocuments.fileTypes')}
|
||||||
onUpload={handleDocumentsUpload}
|
onUpload={handleDocumentsUpload}
|
||||||
|
onReject={handleRejectedFiles}
|
||||||
progresses={progresses}
|
progresses={progresses}
|
||||||
fileErrors={fileErrors}
|
fileErrors={fileErrors}
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
|
@@ -39,6 +39,14 @@ interface FileUploaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
*/
|
*/
|
||||||
onUpload?: (files: File[]) => Promise<void>
|
onUpload?: (files: File[]) => Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be called when files are rejected.
|
||||||
|
* @type (rejections: FileRejection[]) => void
|
||||||
|
* @default undefined
|
||||||
|
* @example onReject={(rejections) => handleRejectedFiles(rejections)}
|
||||||
|
*/
|
||||||
|
onReject?: (rejections: FileRejection[]) => void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Progress of the uploaded files.
|
* Progress of the uploaded files.
|
||||||
* @type Record<string, number> | undefined
|
* @type Record<string, number> | undefined
|
||||||
@@ -125,6 +133,7 @@ function FileUploader(props: FileUploaderProps) {
|
|||||||
value: valueProp,
|
value: valueProp,
|
||||||
onValueChange,
|
onValueChange,
|
||||||
onUpload,
|
onUpload,
|
||||||
|
onReject,
|
||||||
progresses,
|
progresses,
|
||||||
fileErrors,
|
fileErrors,
|
||||||
accept = supportedFileTypes,
|
accept = supportedFileTypes,
|
||||||
@@ -144,38 +153,77 @@ function FileUploader(props: FileUploaderProps) {
|
|||||||
|
|
||||||
const onDrop = React.useCallback(
|
const onDrop = React.useCallback(
|
||||||
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
|
||||||
if (!multiple && maxFileCount === 1 && acceptedFiles.length > 1) {
|
// Calculate total file count including both accepted and rejected files
|
||||||
|
const totalFileCount = (files?.length ?? 0) + acceptedFiles.length + rejectedFiles.length
|
||||||
|
|
||||||
|
// Check file count limits
|
||||||
|
if (!multiple && maxFileCount === 1 && (acceptedFiles.length + rejectedFiles.length) > 1) {
|
||||||
toast.error(t('documentPanel.uploadDocuments.fileUploader.singleFileLimit'))
|
toast.error(t('documentPanel.uploadDocuments.fileUploader.singleFileLimit'))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((files?.length ?? 0) + acceptedFiles.length > maxFileCount) {
|
if (totalFileCount > maxFileCount) {
|
||||||
toast.error(t('documentPanel.uploadDocuments.fileUploader.maxFilesLimit', { count: maxFileCount }))
|
toast.error(t('documentPanel.uploadDocuments.fileUploader.maxFilesLimit', { count: maxFileCount }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const newFiles = acceptedFiles.map((file) =>
|
// Handle rejected files first - this will set error states
|
||||||
|
if (rejectedFiles.length > 0) {
|
||||||
|
if (onReject) {
|
||||||
|
// Use the onReject callback if provided
|
||||||
|
onReject(rejectedFiles)
|
||||||
|
} else {
|
||||||
|
// Fall back to toast notifications if no callback is provided
|
||||||
|
rejectedFiles.forEach(({ file }) => {
|
||||||
|
toast.error(t('documentPanel.uploadDocuments.fileUploader.fileRejected', { name: file.name }))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process accepted files
|
||||||
|
const newAcceptedFiles = acceptedFiles.map((file) =>
|
||||||
Object.assign(file, {
|
Object.assign(file, {
|
||||||
preview: URL.createObjectURL(file)
|
preview: URL.createObjectURL(file)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const updatedFiles = files ? [...files, ...newFiles] : newFiles
|
// Process rejected files for UI display
|
||||||
|
const newRejectedFiles = rejectedFiles.map(({ file }) =>
|
||||||
|
Object.assign(file, {
|
||||||
|
preview: URL.createObjectURL(file),
|
||||||
|
rejected: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
// Combine all files for display
|
||||||
|
const allNewFiles = [...newAcceptedFiles, ...newRejectedFiles]
|
||||||
|
const updatedFiles = files ? [...files, ...allNewFiles] : allNewFiles
|
||||||
|
|
||||||
|
// Update the files state with all files
|
||||||
setFiles(updatedFiles)
|
setFiles(updatedFiles)
|
||||||
|
|
||||||
if (rejectedFiles.length > 0) {
|
// Only upload accepted files - make sure we're not uploading rejected files
|
||||||
rejectedFiles.forEach(({ file }) => {
|
if (onUpload && acceptedFiles.length > 0) {
|
||||||
toast.error(t('documentPanel.uploadDocuments.fileUploader.fileRejected', { name: file.name }))
|
// Filter out any files that might have been rejected by our custom validator
|
||||||
})
|
const validFiles = acceptedFiles.filter(file => {
|
||||||
}
|
// Check if file type is accepted
|
||||||
|
const fileExt = `.${file.name.split('.').pop()?.toLowerCase() || ''}`;
|
||||||
|
const isAccepted = Object.entries(accept || {}).some(([mimeType, extensions]) => {
|
||||||
|
return file.type === mimeType || extensions.includes(fileExt);
|
||||||
|
});
|
||||||
|
|
||||||
if (onUpload && updatedFiles.length > 0 && updatedFiles.length <= maxFileCount) {
|
// Check file size
|
||||||
onUpload(updatedFiles)
|
const isSizeValid = file.size <= maxSize;
|
||||||
|
|
||||||
|
return isAccepted && isSizeValid;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
onUpload(validFiles);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[files, maxFileCount, multiple, onUpload, onReject, setFiles, t, accept, maxSize]
|
||||||
[files, maxFileCount, multiple, onUpload, setFiles, t]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function onRemove(index: number) {
|
function onRemove(index: number) {
|
||||||
@@ -204,11 +252,39 @@ function FileUploader(props: FileUploaderProps) {
|
|||||||
<div className="relative flex flex-col gap-6 overflow-hidden">
|
<div className="relative flex flex-col gap-6 overflow-hidden">
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={onDrop}
|
onDrop={onDrop}
|
||||||
accept={accept}
|
// remove accept,use customizd validator
|
||||||
|
noClick={false}
|
||||||
|
noKeyboard={false}
|
||||||
maxSize={maxSize}
|
maxSize={maxSize}
|
||||||
maxFiles={maxFileCount}
|
maxFiles={maxFileCount}
|
||||||
multiple={maxFileCount > 1 || multiple}
|
multiple={maxFileCount > 1 || multiple}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
validator={(file) => {
|
||||||
|
// Check if file type is accepted
|
||||||
|
const fileExt = `.${file.name.split('.').pop()?.toLowerCase() || ''}`;
|
||||||
|
const isAccepted = Object.entries(accept || {}).some(([mimeType, extensions]) => {
|
||||||
|
return file.type === mimeType || extensions.includes(fileExt);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAccepted) {
|
||||||
|
return {
|
||||||
|
code: 'file-invalid-type',
|
||||||
|
message: t('documentPanel.uploadDocuments.fileUploader.unsupportedType')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
return {
|
||||||
|
code: 'file-too-large',
|
||||||
|
message: t('documentPanel.uploadDocuments.fileUploader.fileTooLarge', {
|
||||||
|
maxSize: formatBytes(maxSize)
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{({ getRootProps, getInputProps, isDragActive }) => (
|
{({ getRootProps, getInputProps, isDragActive }) => (
|
||||||
<div
|
<div
|
||||||
@@ -279,18 +355,21 @@ function FileUploader(props: FileUploaderProps) {
|
|||||||
interface ProgressProps {
|
interface ProgressProps {
|
||||||
value: number
|
value: number
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
showIcon?: boolean // New property to control icon display
|
||||||
}
|
}
|
||||||
|
|
||||||
function Progress({ value, error }: ProgressProps) {
|
function Progress({ value, error }: ProgressProps) {
|
||||||
return (
|
return (
|
||||||
<div className="h-2 w-full overflow-hidden rounded-full bg-secondary">
|
<div className="relative h-2 w-full">
|
||||||
<div
|
<div className="h-full w-full overflow-hidden rounded-full bg-secondary">
|
||||||
className={cn(
|
<div
|
||||||
'h-full transition-all',
|
className={cn(
|
||||||
error ? 'bg-destructive' : 'bg-primary'
|
'h-full transition-all',
|
||||||
)}
|
error ? 'bg-red-400' : 'bg-primary'
|
||||||
style={{ width: `${value}%` }}
|
)}
|
||||||
/>
|
style={{ width: `${value}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -307,16 +386,22 @@ function FileCard({ file, progress, error, onRemove }: FileCardProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex items-center gap-2.5">
|
<div className="relative flex items-center gap-2.5">
|
||||||
<div className="flex flex-1 gap-2.5">
|
<div className="flex flex-1 gap-2.5">
|
||||||
{isFileWithPreview(file) ? <FilePreview file={file} /> : null}
|
{error ? (
|
||||||
|
<FileText className="text-red-400 size-10" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
isFileWithPreview(file) ? <FilePreview file={file} /> : null
|
||||||
|
)}
|
||||||
<div className="flex w-full flex-col gap-2">
|
<div className="flex w-full flex-col gap-2">
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
<p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
|
<p className="text-foreground/80 line-clamp-1 text-sm font-medium">{file.name}</p>
|
||||||
<p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
|
<p className="text-muted-foreground text-xs">{formatBytes(file.size)}</p>
|
||||||
</div>
|
</div>
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="text-destructive text-sm">
|
<div className="text-red-400 text-sm">
|
||||||
<Progress value={100} error={true} />
|
<div className="relative mb-2">
|
||||||
<p className="mt-1">{error}</p>
|
<Progress value={100} error={true} />
|
||||||
|
</div>
|
||||||
|
<p>{error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
progress ? <Progress value={progress} /> : null
|
progress ? <Progress value={progress} /> : null
|
||||||
|
@@ -65,10 +65,13 @@
|
|||||||
"singleFileLimit": "لا يمكن رفع أكثر من ملف واحد في المرة الواحدة",
|
"singleFileLimit": "لا يمكن رفع أكثر من ملف واحد في المرة الواحدة",
|
||||||
"maxFilesLimit": "لا يمكن رفع أكثر من {{count}} ملفات",
|
"maxFilesLimit": "لا يمكن رفع أكثر من {{count}} ملفات",
|
||||||
"fileRejected": "تم رفض الملف {{name}}",
|
"fileRejected": "تم رفض الملف {{name}}",
|
||||||
|
"unsupportedType": "نوع الملف غير مدعوم",
|
||||||
|
"fileTooLarge": "حجم الملف كبير جدًا، الحد الأقصى {{maxSize}}",
|
||||||
"dropHere": "أفلت الملفات هنا",
|
"dropHere": "أفلت الملفات هنا",
|
||||||
"dragAndDrop": "اسحب وأفلت الملفات هنا، أو انقر للاختيار",
|
"dragAndDrop": "اسحب وأفلت الملفات هنا، أو انقر للاختيار",
|
||||||
"removeFile": "إزالة الملف",
|
"removeFile": "إزالة الملف",
|
||||||
"uploadDescription": "يمكنك رفع {{isMultiple ? 'عدة' : count}} ملفات (حتى {{maxSize}} لكل منها)"
|
"uploadDescription": "يمكنك رفع {{isMultiple ? 'عدة' : count}} ملفات (حتى {{maxSize}} لكل منها)",
|
||||||
|
"duplicateFile": "اسم الملف موجود بالفعل في ذاكرة التخزين المؤقت للخادم"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documentManager": {
|
"documentManager": {
|
||||||
|
@@ -65,10 +65,13 @@
|
|||||||
"singleFileLimit": "Cannot upload more than 1 file at a time",
|
"singleFileLimit": "Cannot upload more than 1 file at a time",
|
||||||
"maxFilesLimit": "Cannot upload more than {{count}} files",
|
"maxFilesLimit": "Cannot upload more than {{count}} files",
|
||||||
"fileRejected": "File {{name}} was rejected",
|
"fileRejected": "File {{name}} was rejected",
|
||||||
|
"unsupportedType": "Unsupported file type",
|
||||||
|
"fileTooLarge": "File too large, maximum size is {{maxSize}}",
|
||||||
"dropHere": "Drop the files here",
|
"dropHere": "Drop the files here",
|
||||||
"dragAndDrop": "Drag and drop files here, or click to select files",
|
"dragAndDrop": "Drag and drop files here, or click to select files",
|
||||||
"removeFile": "Remove file",
|
"removeFile": "Remove file",
|
||||||
"uploadDescription": "You can upload {{isMultiple ? 'multiple' : count}} files (up to {{maxSize}} each)"
|
"uploadDescription": "You can upload {{isMultiple ? 'multiple' : count}} files (up to {{maxSize}} each)",
|
||||||
|
"duplicateFile": "File name already exists in server cache"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documentManager": {
|
"documentManager": {
|
||||||
|
@@ -65,10 +65,13 @@
|
|||||||
"singleFileLimit": "Impossible de télécharger plus d'un fichier à la fois",
|
"singleFileLimit": "Impossible de télécharger plus d'un fichier à la fois",
|
||||||
"maxFilesLimit": "Impossible de télécharger plus de {{count}} fichiers",
|
"maxFilesLimit": "Impossible de télécharger plus de {{count}} fichiers",
|
||||||
"fileRejected": "Le fichier {{name}} a été rejeté",
|
"fileRejected": "Le fichier {{name}} a été rejeté",
|
||||||
|
"unsupportedType": "Type de fichier non pris en charge",
|
||||||
|
"fileTooLarge": "Fichier trop volumineux, taille maximale {{maxSize}}",
|
||||||
"dropHere": "Déposez les fichiers ici",
|
"dropHere": "Déposez les fichiers ici",
|
||||||
"dragAndDrop": "Glissez et déposez les fichiers ici, ou cliquez pour sélectionner",
|
"dragAndDrop": "Glissez et déposez les fichiers ici, ou cliquez pour sélectionner",
|
||||||
"removeFile": "Supprimer le fichier",
|
"removeFile": "Supprimer le fichier",
|
||||||
"uploadDescription": "Vous pouvez télécharger {{isMultiple ? 'plusieurs' : count}} fichiers (jusqu'à {{maxSize}} chacun)"
|
"uploadDescription": "Vous pouvez télécharger {{isMultiple ? 'plusieurs' : count}} fichiers (jusqu'à {{maxSize}} chacun)",
|
||||||
|
"duplicateFile": "Le nom du fichier existe déjà dans le cache du serveur"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documentManager": {
|
"documentManager": {
|
||||||
|
@@ -65,10 +65,13 @@
|
|||||||
"singleFileLimit": "一次只能上传一个文件",
|
"singleFileLimit": "一次只能上传一个文件",
|
||||||
"maxFilesLimit": "最多只能上传 {{count}} 个文件",
|
"maxFilesLimit": "最多只能上传 {{count}} 个文件",
|
||||||
"fileRejected": "文件 {{name}} 被拒绝",
|
"fileRejected": "文件 {{name}} 被拒绝",
|
||||||
|
"unsupportedType": "不支持的文件类型",
|
||||||
|
"fileTooLarge": "文件过大,最大允许 {{maxSize}}",
|
||||||
"dropHere": "将文件拖放到此处",
|
"dropHere": "将文件拖放到此处",
|
||||||
"dragAndDrop": "拖放文件到此处,或点击选择文件",
|
"dragAndDrop": "拖放文件到此处,或点击选择文件",
|
||||||
"removeFile": "移除文件",
|
"removeFile": "移除文件",
|
||||||
"uploadDescription": "您可以上传{{isMultiple ? '多个' : count}}个文件(每个文件最大{{maxSize}})"
|
"uploadDescription": "您可以上传{{isMultiple ? '多个' : count}}个文件(每个文件最大{{maxSize}})",
|
||||||
|
"duplicateFile": "文件名与服务器上的缓存重复"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"documentManager": {
|
"documentManager": {
|
||||||
|
Reference in New Issue
Block a user