Skip to content

Messages & i18n ​

Every error has a code (e.g. minLength) and a message. You can set the message for a single rule, for the whole app, or load a ready-made locale.

Message for a single rule ​

The last argument of every schema and rule is the message:

ts
string('Please enter your name')
pipe(string(), minLength(8, 'Password is too short'))

For schemas (string(), object()…) the message replaces all their errors — both "required" and "wrong type".

Parameters ​

Use rule parameters in curly braces:

ts
minLength(3, 'At least {min} characters')  // → "At least 3 characters"
oneOf(['a', 'b'], 'Choose: {options}')       // → "Choose: a, b"

If you need logic (e.g. plurals), pass a function — it receives the parameters:

ts
minLength(5, (p) => `At least ${p.min} character${p.min === 1 ? '' : 's'}`)

Plural forms: "1 character", "5 characters" ​

A word form that depends on a number is written in curly braces with |: the parameter first, then the forms. The form is chosen by the language rules (Intl.PluralRules); the language comes from the $locale key of the lang file:

ts
setMessages({
  minLength: 'Must be at least {min} {min|character|characters} long',
});
// 1 character, 2 characters
LanguageNumber of formsExample
Russian, Ukrainian3: one, few, many{n|файл|файла|файлов}
English, German2: one, other{n|file|files}
Chinese, Japanese1{n|个} or plain text without forms

The built-in English and Russian locales already use plural forms. If fewer forms are given than the language needs, the last one is used.

Messages for the whole app ​

setMessages changes the default texts. Call it once on startup:

ts
import { setMessages } from 'tinyval';
import ru from 'tinyval/locales/ru';

setMessages(ru);                               // everything in Russian
setMessages({ required: 'Please fill in' }); // override some texts

Language per call ​

setMessages applies to the whole app. On a server where every request has its own language, pass the messages to the call itself — other requests are not affected:

ts
import ru from 'tinyval/locales/ru';
import en from 'tinyval/locales/en';

const locales = { ru, en };

app.post('/users', (req, res) => {
  const lang = req.acceptsLanguages('ru', 'en') || 'en';
  const result = validate(CreateUser, req.body, { messages: locales[lang] });
});

Call messages can be partial — missing ones come from setMessages.

Field name — {label} ​

Every message can use the {label} parameter — like :attribute in Laravel. By default it is the field name (email); a custom name is set with label or the labels option:

ts
const Signup = object({
  email: label(pipe(string(), email()), 'Email address'),
  password: label(pipe(string(), minLength(8)), 'Password'),
});

setMessages({ required: '{label} is required', minLength: '{label} must be at least {min} characters' });

validate(Signup, { password: '123' }).errors;
// { email: 'Email address is required', password: 'Password must be at least 8 characters' }

Translating on the client — params ​

If the server doesn't know the user's language, send the error codes and parameters to the client and build the text on the frontend:

ts
validate(Signup, body).issues;
// [{ code: 'minLength', params: { min: 8 }, path: ['password'], message: '...' }]

Locales ​

ImportLanguage
tinyval/locales/enEnglish (default)
tinyval/locales/ruRussian
tinyval/locales/zhSimplified Chinese

setMessages(en) brings back the English texts.

To customize the texts, copy a lang file into your project with npx tinyval lang en — see Custom lang file.

All error codes ​

CodeRaised byMessage (en)
invalidcheck, checkAsyncInvalid value
requiredan empty value in any schemaRequired
stringstringMust be a string
numbernumberMust be a number
booleanbooleanMust be a boolean
datedateMust be a valid date
objectobjectMust be an object
arrayarrayMust be an array
literalliteralMust be {expected}
oneOfoneOf, enumOfMust be one of: {options}
unionunionDoes not match any allowed type
variantvariantUnknown type, expected one of: {options}
depthlazyToo deeply nested (max {max})
unknownKeyobject with unknown: 'strict'Unknown field
minLengthminLength on a stringMust be at least {min} {min|character|characters} long
maxLengthmaxLength on a stringMust be at most {max} {max|character|characters} long
lengthlength on a stringMust be exactly {length} {length|character|characters} long
minminMust be at least {min}
maxmaxMust be at most {max}
integerintegerMust be an integer
patternpatternInvalid format
emailemailInvalid email
minItemsminLength on an arrayMust have at least {min} {min|item|items}
maxItemsmaxLength on an arrayMust have at most {max} {max|item|items}
itemslength on an array, tupleMust have exactly {length} {length|item|items}
betweenbetweenMust be between {min} and {max}
urlurlInvalid URL
uuiduuidInvalid UUID
startsWithstartsWithMust start with "{value}"
endsWithendsWithMust end with "{value}"
includesincludesMust contain "{value}"
uniqueuniqueMust not contain duplicates
minDateminDateMust be on or after {date}
maxDatemaxDateMust be on or before {date}
afterafterMust be after {field}
beforebeforeMust be before {field}
inninnInvalid INN
snilssnilsInvalid SNILS
luhnluhnInvalid card number
prohibitedprohibitedIfMust be empty
filefileMust be a file
fileSizefile — maxSizeFile must be at most {max}
fileTypefile — typesFile type must be one of: {types}
sameAssameAsMust match {field}

Unknown {params} in your own messages stay in the text as is.

See also ​