11import React , { useRef , useState , useCallback } from 'react' ;
22import { Button , EmptyValue } from '@object-ui/components' ;
33import { useUpload } from '@object-ui/providers' ;
4+ import { useObjectTranslation } from '@object-ui/i18n' ;
45import { Upload , X , File as FileIcon , ImageIcon , Camera , Loader2 } from 'lucide-react' ;
56import { FieldWidgetProps } from './types' ;
67import { useUploadingSignal } from './useUploadingSignal' ;
@@ -20,6 +21,7 @@ function useFileUploads(opts: {
2021} ) {
2122 const { files, multiple, maxSize, onChange } = opts ;
2223 const { upload } = useUpload ( ) ;
24+ const { t } = useObjectTranslation ( ) ;
2325 const [ errors , setErrors ] = useState < string [ ] > ( [ ] ) ;
2426 const [ uploadProgress , setUploadProgress ] = useState < Record < string , number > > ( { } ) ;
2527 const [ uploading , setUploading ] = useState ( false ) ;
@@ -31,7 +33,10 @@ function useFileUploads(opts: {
3133 const validFiles = selectedFiles . filter ( file => {
3234 if ( maxSize && file . size > maxSize ) {
3335 const maxMB = ( maxSize / ( 1024 * 1024 ) ) . toFixed ( 1 ) ;
34- newErrors . push ( `"${ file . name } " exceeds max size (${ maxMB } MB)` ) ;
36+ newErrors . push ( t ( 'fields.file.exceedsMaxSize' , {
37+ defaultValue : '"{{name}}" exceeds max size ({{max}} MB)' ,
38+ name : file . name , max : maxMB ,
39+ } ) ) ;
3540 return false ;
3641 }
3742 return true ;
@@ -63,7 +68,10 @@ function useFileUploads(opts: {
6368 url : result . url ,
6469 } ;
6570 } catch ( err ) {
66- newErrors . push ( `Failed to upload "${ file . name } ": ${ ( err as Error ) . message } ` ) ;
71+ newErrors . push ( t ( 'fields.file.uploadFailed' , {
72+ defaultValue : 'Failed to upload "{{name}}": {{error}}' ,
73+ name : file . name , error : ( err as Error ) . message ,
74+ } ) ) ;
6775 setErrors ( [ ...newErrors ] ) ;
6876 return null ;
6977 }
@@ -81,7 +89,7 @@ function useFileUploads(opts: {
8189 setUploading ( false ) ;
8290 setUploadProgress ( { } ) ;
8391 }
84- } , [ files , multiple , onChange , maxSize , upload ] ) ;
92+ } , [ files , multiple , onChange , maxSize , upload , t ] ) ;
8593
8694 return { processFiles, errors, uploading, uploadProgress } ;
8795}
@@ -92,6 +100,7 @@ function useFileUploads(opts: {
92100 * L2: File size validation, per-file progress indicators, error messages.
93101 */
94102export function FileField ( { value, onChange, field, readonly, onUploadingChange, ...props } : FieldWidgetProps < any > ) {
103+ const { t } = useObjectTranslation ( ) ;
95104 const inputRef = useRef < HTMLInputElement > ( null ) ;
96105 const cameraRef = useRef < HTMLInputElement > ( null ) ;
97106 const fileField = ( field || ( props as any ) . schema ) as any ;
@@ -161,7 +170,7 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
161170 < div className = "flex flex-wrap gap-2" >
162171 { readonlyFiles . map ( ( file : any , idx : number ) => (
163172 < span key = { idx } className = "text-sm truncate max-w-xs" >
164- { file . name || file . original_name || ' File'}
173+ { file . name || file . original_name || t ( 'fields.file.fileFallback' , { defaultValue : ' File' } ) }
165174 </ span >
166175 ) ) }
167176 </ div >
@@ -204,7 +213,7 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
204213 capture = { cameraEnabled }
205214 onChange = { handleFileChange }
206215 className = "hidden"
207- aria-label = " Camera capture"
216+ aria-label = { t ( 'fields.file.cameraCapture' , { defaultValue : ' Camera capture' } ) }
208217 data-testid = "file-field-camera-input"
209218 />
210219 ) }
@@ -236,10 +245,14 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
236245 < Upload className = { `size-8 ${ isDragOver ? 'text-primary' : 'text-muted-foreground' } ` } />
237246 < div className = "text-center" >
238247 < p className = "text-sm font-medium" >
239- { isDragOver ? 'Drop files here' : 'Drag & drop files here' }
248+ { isDragOver
249+ ? t ( 'fields.file.dropFilesHere' , { defaultValue : 'Drop files here' } )
250+ : t ( 'fields.file.dragDropHere' , { defaultValue : 'Drag & drop files here' } ) }
240251 </ p >
241252 < p className = "text-xs text-muted-foreground mt-1" >
242- or click to browse{ cameraEnabled ? ' • use the camera button below' : '' }
253+ { cameraEnabled
254+ ? t ( 'fields.file.browseHintCamera' , { defaultValue : 'or click to browse • use the camera button below' } )
255+ : t ( 'fields.file.browseHint' , { defaultValue : 'or click to browse' } ) }
243256 </ p >
244257 </ div >
245258 </ div >
@@ -257,7 +270,9 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
257270 data-testid = "file-field-camera-button"
258271 >
259272 < Camera className = "size-4 mr-2" />
260- { cameraEnabled === 'user' ? 'Take selfie' : 'Take photo' }
273+ { cameraEnabled === 'user'
274+ ? t ( 'fields.file.takeSelfie' , { defaultValue : 'Take selfie' } )
275+ : t ( 'fields.file.takePhoto' , { defaultValue : 'Take photo' } ) }
261276 </ Button >
262277 ) }
263278
@@ -266,12 +281,15 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
266281 < div className = "flex items-center gap-2 text-xs text-muted-foreground" data-testid = "file-field-uploading" >
267282 < Loader2 className = "size-3 animate-spin" />
268283 < span >
269- Uploading…
270- { Object . keys ( uploadProgress ) . length > 0 &&
271- ` (${ Math . round (
272- ( Object . values ( uploadProgress ) . reduce ( ( s , v ) => s + v , 0 ) /
273- Object . keys ( uploadProgress ) . length ) * 100 ,
274- ) } %)`}
284+ { Object . keys ( uploadProgress ) . length > 0
285+ ? t ( 'fields.file.uploadingPct' , {
286+ defaultValue : 'Uploading… ({{pct}}%)' ,
287+ pct : Math . round (
288+ ( Object . values ( uploadProgress ) . reduce ( ( s , v ) => s + v , 0 ) /
289+ Object . keys ( uploadProgress ) . length ) * 100 ,
290+ ) ,
291+ } )
292+ : t ( 'fields.file.uploading' , { defaultValue : 'Uploading…' } ) }
275293 </ span >
276294 </ div >
277295 ) }
@@ -302,7 +320,7 @@ export function FileField({ value, onChange, field, readonly, onUploadingChange,
302320 < FileIcon className = "size-4 text-muted-foreground shrink-0" />
303321 ) }
304322 < span className = "text-sm truncate" >
305- { file . name || file . original_name || ' File'}
323+ { file . name || file . original_name || t ( 'fields.file.fileFallback' , { defaultValue : ' File' } ) }
306324 </ span >
307325 { file . size && (
308326 < span className = "text-xs text-muted-foreground" >
@@ -362,6 +380,7 @@ export function FileCell({
362380 /** Focus-grid coordinate (see GridField keyboard navigation). */
363381 'data-cell' ?: string ;
364382} ) {
383+ const { t } = useObjectTranslation ( ) ;
365384 const inputRef = useRef < HTMLInputElement > ( null ) ;
366385 const files = value ? ( Array . isArray ( value ) ? value : [ value ] ) : [ ] ;
367386 const { processFiles, errors, uploading } = useFileUploads ( {
@@ -379,7 +398,9 @@ export function FileCell({
379398
380399 const isImage = ( file : any ) => String ( file ?. mime_type || '' ) . startsWith ( 'image/' ) ;
381400 const nameOf = ( file : any ) =>
382- typeof file === 'string' ? file : file ?. name || file ?. original_name || 'File' ;
401+ typeof file === 'string'
402+ ? file
403+ : file ?. name || file ?. original_name || t ( 'fields.file.fileFallback' , { defaultValue : 'File' } ) ;
383404 const showUpload = ! disabled && ! uploading && ( multiple || files . length === 0 ) ;
384405
385406 return (
@@ -412,7 +433,7 @@ export function FileCell({
412433 < button
413434 type = "button"
414435 className = "shrink-0 rounded p-0.5 text-muted-foreground hover:text-foreground"
415- aria-label = { ` Remove ${ nameOf ( file ) } ` }
436+ aria-label = { t ( 'fields.file.remove' , { defaultValue : ' Remove {{name}}' , name : nameOf ( file ) } ) }
416437 onClick = { ( ) => removeAt ( idx ) }
417438 >
418439 < X className = "size-3" />
@@ -440,7 +461,7 @@ export function FileCell({
440461 disabled = { disabled }
441462 >
442463 < Upload className = "size-3.5" />
443- { files . length === 0 && ' Upload'}
464+ { files . length === 0 && t ( 'fields.file.upload' , { defaultValue : ' Upload' } ) }
444465 </ Button >
445466 ) }
446467 { errors . length > 0 && (
0 commit comments