@@ -132,9 +132,17 @@ const IMPORT_DEFAULT_TRANSLATIONS: Record<string, string> = {
132132 'grid.import.validatePassed' : 'All {{ok}} rows are valid.' ,
133133 'grid.import.validateFailed' : '{{ok}} valid, {{errors}} with errors.' ,
134134 'grid.import.errorRowPrefix' : 'Row {{row}}: ' ,
135- // Friendly, localized renderings of the server's structured import errors.
135+ // Friendly, localized renderings of the server's structured import errors —
136+ // driven off the error `code` so the raw English server text (with its
137+ // baked-in api-name and internal object name) never reaches the user.
136138 'grid.import.referenceNotFound' : 'No matching record for "{{value}}"' ,
137139 'grid.import.referenceAmbiguous' : '"{{value}}" matches more than one record — use a unique value or the record id' ,
140+ 'grid.import.invalidBoolean' : '"{{value}}" is not a valid true/false value' ,
141+ 'grid.import.invalidNumber' : '"{{value}}" is not a valid number' ,
142+ 'grid.import.invalidDate' : '"{{value}}" is not a valid date' ,
143+ 'grid.import.invalidOption' : '"{{value}}" is not one of the allowed options' ,
144+ 'grid.import.requiredValue' : 'This field is required' ,
145+ 'grid.import.matchAmbiguous' : 'Matches more than one existing record — use a unique value or the record id' ,
138146 // Import-job history
139147 'grid.import.history' : 'History' ,
140148 'grid.import.historyBack' : 'Back to import' ,
@@ -304,7 +312,7 @@ export interface ImportResult {
304312 totalRows : number ;
305313 importedRows : number ;
306314 skippedRows : number ;
307- errors : Array < { row : number ; field : string ; message : string } > ;
315+ errors : Array < { row : number ; field : string ; message : string ; code ?: string } > ;
308316 /** Rows that created a new record (server-side import). */
309317 createdRows ?: number ;
310318 /** Rows that updated an existing record (server-side import). */
@@ -404,6 +412,18 @@ function formatDryRunError(
404412 return { fieldLabel, message : t ( 'grid.import.referenceNotFound' , { value : shown } ) } ;
405413 case 'reference_ambiguous' :
406414 return { fieldLabel, message : t ( 'grid.import.referenceAmbiguous' , { value : shown } ) } ;
415+ case 'invalid_boolean' :
416+ return { fieldLabel, message : t ( 'grid.import.invalidBoolean' , { value : shown } ) } ;
417+ case 'invalid_number' :
418+ return { fieldLabel, message : t ( 'grid.import.invalidNumber' , { value : shown } ) } ;
419+ case 'invalid_date' :
420+ return { fieldLabel, message : t ( 'grid.import.invalidDate' , { value : shown } ) } ;
421+ case 'invalid_option' :
422+ return { fieldLabel, message : t ( 'grid.import.invalidOption' , { value : shown } ) } ;
423+ case 'required' :
424+ return { fieldLabel, message : t ( 'grid.import.requiredValue' ) } ;
425+ case 'AMBIGUOUS_MATCH' :
426+ return { fieldLabel, message : t ( 'grid.import.matchAmbiguous' ) } ;
407427 }
408428 let message = ( r . error ?? r . code ?? '' ) . trim ( ) ;
409429 // Drop a leading `<api-name>:` the server prepended, so it isn't shown on top
@@ -424,7 +444,7 @@ function formatDryRunError(
424444 */
425445export function isPlausibleEmail ( value : string ) : boolean {
426446 if ( value . length === 0 || value . length > 254 || / \s / . test ( value ) ) return false ;
427- if ( / [ ^ \x00 - \x7f ] / . test ( value ) ) return false ; // ASCII only, like the server
447+ if ( / [ ^ \x20 - \x7e ] / . test ( value ) ) return false ; // printable ASCII only, like the server
428448 const at = value . indexOf ( '@' ) ;
429449 if ( at <= 0 || at !== value . lastIndexOf ( '@' ) || at === value . length - 1 ) return false ;
430450 const domain = value . slice ( at + 1 ) ;
@@ -624,7 +644,7 @@ function jobResultToImportResult(res: ImportJobResultsInfo): ImportResult {
624644 updatedRows : res . updated ,
625645 errors : ( res . results ?? [ ] )
626646 . filter ( ( r ) => ! r . ok )
627- . map ( ( r ) => ( { row : r . row , field : r . field ?? '' , message : r . error ?? r . code ?? 'Import failed' } ) ) ,
647+ . map ( ( r ) => ( { row : r . row , field : r . field ?? '' , message : r . error ?? r . code ?? 'Import failed' , code : r . code } ) ) ,
628648 resultsTruncated : res . resultsTruncated ,
629649 } ;
630650}
@@ -2005,7 +2025,7 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
20052025 updatedRows : res . updated ,
20062026 errors : res . results
20072027 . filter ( ( r ) => ! r . ok )
2008- . map ( ( r ) => ( { row : r . row , field : r . field ?? '' , message : r . error ?? r . code ?? 'Import failed' } ) ) ,
2028+ . map ( ( r ) => ( { row : r . row , field : r . field ?? '' , message : r . error ?? r . code ?? 'Import failed' , code : r . code } ) ) ,
20092029 serverResult : res ,
20102030 } ;
20112031 setProgress ( 100 ) ;
@@ -2380,9 +2400,24 @@ export const ImportWizard: React.FC<ImportWizardProps> = ({
23802400 { result . errors . length > 0 && (
23812401 < >
23822402 < div className = "max-h-32 w-full overflow-auto rounded border p-2 text-xs" >
2383- { result . errors . slice ( 0 , 10 ) . map ( ( err , i ) => (
2384- < p key = { i } className = "text-destructive" > { err . row >= 1 ? `Row ${ err . row } ${ err . field ? ` (${ err . field } )` : '' } : ` : '' } { err . message } </ p >
2385- ) ) }
2403+ { result . errors . slice ( 0 , 10 ) . map ( ( err , i ) => {
2404+ // Localize the same way the dry-run panel does (drive off
2405+ // `code`, resolve the api-name to its label, drop the raw
2406+ // server text for known codes) so the completion screen and
2407+ // the pre-check read identically (objectstack#3566).
2408+ const { fieldLabel, message } = formatDryRunError (
2409+ { field : err . field , error : err . message , code : err . code } ,
2410+ fieldLabelByName ,
2411+ dryRunCellValue ( err . row , err . field ) ,
2412+ t ,
2413+ ) ;
2414+ return (
2415+ < p key = { i } className = "text-destructive" >
2416+ { err . row >= 1 ? t ( 'grid.import.errorRowPrefix' , { row : err . row } ) : '' }
2417+ { fieldLabel ? `${ fieldLabel } : ` : '' } { message }
2418+ </ p >
2419+ ) ;
2420+ } ) }
23862421 { result . errors . length > 10 && < p className = "text-muted-foreground" > { t ( 'grid.import.moreErrors' , { count : result . errors . length - 10 } ) } </ p > }
23872422 </ div >
23882423 { result . errors . some ( ( e ) => e . row >= 1 ) && (
0 commit comments