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:
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:
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:
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:
setMessages({
minLength: 'Must be at least {min} {min|character|characters} long',
});
// 1 character, 2 characters| Language | Number of forms | Example |
|---|---|---|
| Russian, Ukrainian | 3: one, few, many | {n|файл|файла|файлов} |
| English, German | 2: one, other | {n|file|files} |
| Chinese, Japanese | 1 | {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:
import { setMessages } from 'tinyval';
import ru from 'tinyval/locales/ru';
setMessages(ru); // everything in Russian
setMessages({ required: 'Please fill in' }); // override some textsLanguage 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:
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:
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:
validate(Signup, body).issues;
// [{ code: 'minLength', params: { min: 8 }, path: ['password'], message: '...' }]Locales
| Import | Language |
|---|---|
tinyval/locales/en | English (default) |
tinyval/locales/ru | Russian |
tinyval/locales/zh | Simplified 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
| Code | Raised by | Message (en) |
|---|---|---|
invalid | check, checkAsync | Invalid value |
required | an empty value in any schema | Required |
string | string | Must be a string |
number | number | Must be a number |
boolean | boolean | Must be a boolean |
date | date | Must be a valid date |
object | object | Must be an object |
array | array | Must be an array |
literal | literal | Must be {expected} |
oneOf | oneOf, enumOf | Must be one of: {options} |
union | union | Does not match any allowed type |
variant | variant | Unknown type, expected one of: {options} |
depth | lazy | Too deeply nested (max {max}) |
unknownKey | object with unknown: 'strict' | Unknown field |
minLength | minLength on a string | Must be at least {min} {min|character|characters} long |
maxLength | maxLength on a string | Must be at most {max} {max|character|characters} long |
length | length on a string | Must be exactly {length} {length|character|characters} long |
min | min | Must be at least {min} |
max | max | Must be at most {max} |
integer | integer | Must be an integer |
pattern | pattern | Invalid format |
email | email | Invalid email |
minItems | minLength on an array | Must have at least {min} {min|item|items} |
maxItems | maxLength on an array | Must have at most {max} {max|item|items} |
items | length on an array, tuple | Must have exactly {length} {length|item|items} |
between | between | Must be between {min} and {max} |
url | url | Invalid URL |
uuid | uuid | Invalid UUID |
startsWith | startsWith | Must start with "{value}" |
endsWith | endsWith | Must end with "{value}" |
includes | includes | Must contain "{value}" |
unique | unique | Must not contain duplicates |
minDate | minDate | Must be on or after {date} |
maxDate | maxDate | Must be on or before {date} |
after | after | Must be after {field} |
before | before | Must be before {field} |
inn | inn | Invalid INN |
snils | snils | Invalid SNILS |
luhn | luhn | Invalid card number |
prohibited | prohibitedIf | Must be empty |
file | file | Must be a file |
fileSize | file — maxSize | File must be at most {max} |
fileType | file — types | File type must be one of: {types} |
sameAs | sameAs | Must match {field} |
Unknown {params} in your own messages stay in the text as is.
See also
- setMessages
- ValidationError — how messages get into the exception
- Custom rules & schemas — your own error codes