Version:@objectstack/*@17.0.0-rc.6 (message originates in @objectstack/rest, packages/rest)
What happens
Save a record that violates a unique: true field and the REST layer answers with a hard-coded English sentence that no application can localize or override:
POST /api/v1/data/<object> → 409 Conflict
{"error":"A record with this value already exists","code":"UNIQUE_VIOLATION","object":"<object>"}
Console renders body.error verbatim — in the form's error banner and in the toast. So an application whose every label, option and validation message is fully translated still shows this one English sentence to its end users, on one of the most commonly hit validation paths there is.
This is a server-side string, not a Console one — I checked both directions before filing:
- the response body above carries it (read off the browser network panel on a real save, not inferred);
- it appears in none of the Console bundle chunks (
index-*.js, framework-*.js, ui-components-*.js, vendor-*.js — 0 hits, including the 2.8 MB ui-components chunk).
Minimal repro
- Any object with a
text field declared unique: true — e.g. invoice.serial_no. - Create a record with
serial_no = "A-001". - Create a second record with the same
serial_no, from the Console form or by POST /api/v1/data/invoice. - → 409 with the body above; the Console banner/toast show the raw English sentence.
The part that looks like an outright inconsistency
The same package already knows how to do better on the bulk / row path. sanitizeRowError resolves the offending column and names it:
varUNNAMED_CONFLICT="A record with this value already exists.";functionsanitizeRowError(raw){
...
if(isUniqueViolationError(msg)){constcolumn=uniqueViolationColumn(msg);returncolumn ? `A record with this ${column} already exists.` : UNNAMED_CONFLICT;}…while the single-record error envelope hard-codes the unnamed variant even though uniqueViolationColumn is right there and the error object is in hand:
if(isUniqueViolationError2(error)){return{status: 409,body: {error: "A record with this value already exists",code: "UNIQUE_VIOLATION",
...object ? { object } : {}}};}So the same platform gives two different answers to the same constraint depending on whether the write came in one row at a time or in a batch: the batch path tells the user which field collided, the single-record path does not. On a record with several unique fields, the user is told only that "a value" is taken and has to guess which one.
Why a client cannot work around it
code: "UNIQUE_VIOLATION" is in the body, which is enough to detect the class of failure — but the body carries no field / column. A client that wanted to render its own localized message therefore cannot name the field either; the only thing it can produce is a generic "some unique value is taken", which is strictly worse than what the platform itself already computes on the bulk path.
Note the asymmetry with application-authored errors: an error thrown from an app's own hook has its message surfaced verbatim, so app authors can localize those. It is specifically the platform's built-in constraint errors that are stuck in English.
Expected
Either of these would close it; (b) alone is already enough to unblock clients:
- (a) resolve the message through the i18n service the way other user-facing platform strings are, so a locale bundle can translate it and an application can override it;
- (b) include the offending field in the 409 body (
field / column, alongside the existing code and object), so a client can build its own localized message — and, while there, reuse uniqueViolationColumn in the single-record branch so its default message reaches parity with the bulk path.
Same treatment would apply to the sibling constants in sanitizeRowError ("... is required.", "The database rejected this row (a value may be invalid or already in use).", "Row failed"), which are user-visible on the import path for the same reason.
Version:
@objectstack/*@17.0.0-rc.6(message originates in@objectstack/rest,packages/rest)What happens
Save a record that violates a
unique: truefield and the REST layer answers with a hard-coded English sentence that no application can localize or override:Console renders
body.errorverbatim — in the form's error banner and in the toast. So an application whose every label, option and validation message is fully translated still shows this one English sentence to its end users, on one of the most commonly hit validation paths there is.This is a server-side string, not a Console one — I checked both directions before filing:
index-*.js,framework-*.js,ui-components-*.js,vendor-*.js— 0 hits, including the 2.8 MBui-componentschunk).Minimal repro
textfield declaredunique: true— e.g.invoice.serial_no.serial_no = "A-001".serial_no, from the Console form or byPOST /api/v1/data/invoice.The part that looks like an outright inconsistency
The same package already knows how to do better on the bulk / row path.
sanitizeRowErrorresolves the offending column and names it:…while the single-record error envelope hard-codes the unnamed variant even though
uniqueViolationColumnis right there and the error object is in hand:So the same platform gives two different answers to the same constraint depending on whether the write came in one row at a time or in a batch: the batch path tells the user which field collided, the single-record path does not. On a record with several unique fields, the user is told only that "a value" is taken and has to guess which one.
Why a client cannot work around it
code: "UNIQUE_VIOLATION"is in the body, which is enough to detect the class of failure — but the body carries nofield/column. A client that wanted to render its own localized message therefore cannot name the field either; the only thing it can produce is a generic "some unique value is taken", which is strictly worse than what the platform itself already computes on the bulk path.Note the asymmetry with application-authored errors: an error thrown from an app's own hook has its
messagesurfaced verbatim, so app authors can localize those. It is specifically the platform's built-in constraint errors that are stuck in English.Expected
Either of these would close it; (b) alone is already enough to unblock clients:
field/column, alongside the existingcodeandobject), so a client can build its own localized message — and, while there, reuseuniqueViolationColumnin the single-record branch so its default message reaches parity with the bulk path.Same treatment would apply to the sibling constants in
sanitizeRowError("... is required.","The database rejected this row (a value may be invalid or already in use).","Row failed"), which are user-visible on the import path for the same reason.