@reformer/core 11.0.0-beta.5 → 11.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/behaviors.js +124 -124
  2. package/dist/create-model-B8U-COv-.js +103 -0
  3. package/dist/en-CjAqn7h0.js +41 -0
  4. package/dist/form/behaviors/internals.d.ts +9 -0
  5. package/dist/form/create-form.d.ts +6 -6
  6. package/dist/form/factories/node-factory.d.ts +16 -16
  7. package/dist/form/nodes/array-node.d.ts +3 -3
  8. package/dist/form/nodes/field-node.d.ts +3 -3
  9. package/dist/form/nodes/group-node.d.ts +7 -7
  10. package/dist/form/nodes/model-array-node.d.ts +2 -2
  11. package/dist/form/types/contracts.d.ts +6 -0
  12. package/dist/form/types/deep-schema.d.ts +5 -5
  13. package/dist/form/types/validation-schema.d.ts +13 -1
  14. package/dist/form/validation/operators.d.ts +6 -1
  15. package/dist/form/validators/validation-error.d.ts +8 -0
  16. package/dist/i18n/descriptor.d.ts +54 -0
  17. package/dist/i18n/en.json.d.ts +45 -0
  18. package/dist/i18n/loader.d.ts +71 -0
  19. package/dist/i18n/locale-store.d.ts +39 -0
  20. package/dist/i18n/locale.d.ts +90 -0
  21. package/dist/i18n/message-format.d.ts +116 -0
  22. package/dist/i18n/resolve-localized.d.ts +17 -0
  23. package/dist/i18n/ru.json.d.ts +45 -0
  24. package/dist/i18n/translator.d.ts +50 -0
  25. package/dist/i18n/validation-message.d.ts +18 -0
  26. package/dist/i18n.d.ts +31 -0
  27. package/dist/i18n.js +303 -0
  28. package/dist/index-BrNkQj1y.js +105 -0
  29. package/dist/index.js +548 -601
  30. package/dist/loader-Dv_Hl7JC.js +216 -0
  31. package/dist/locale/en.d.ts +2 -0
  32. package/dist/locale/en.js +5 -0
  33. package/dist/locale/en.json +42 -0
  34. package/dist/locale/index.d.ts +18 -0
  35. package/dist/locale/ru.d.ts +2 -0
  36. package/dist/locale/ru.js +41 -0
  37. package/dist/locale/ru.json +42 -0
  38. package/dist/locale.d.ts +2 -0
  39. package/dist/locale.js +9 -0
  40. package/dist/model/create-model.d.ts +17 -3
  41. package/dist/model/index.d.ts +3 -3
  42. package/dist/model/model-signals-proxy.d.ts +25 -0
  43. package/dist/model/types.d.ts +19 -5
  44. package/dist/model-signals-proxy-2vqS243J.js +250 -0
  45. package/dist/model.js +18 -16
  46. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  47. package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
  48. package/dist/platforms/react/hooks/types.d.ts +6 -3
  49. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  50. package/dist/platforms/react/i18n/context.d.ts +13 -0
  51. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  52. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  53. package/dist/platforms/react/index.d.ts +4 -0
  54. package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
  55. package/dist/validation-error-B-U3RsV0.js +11 -0
  56. package/dist/validation.js +1 -1
  57. package/dist/validators/email.js +5 -8
  58. package/dist/validators/future-date.js +8 -11
  59. package/dist/validators/integer.js +4 -7
  60. package/dist/validators/is-date.js +5 -8
  61. package/dist/validators/is-number.js +4 -7
  62. package/dist/validators/max-age.js +8 -11
  63. package/dist/validators/max-date.js +8 -11
  64. package/dist/validators/max-length.js +5 -8
  65. package/dist/validators/max.js +4 -7
  66. package/dist/validators/min-age.js +8 -11
  67. package/dist/validators/min-date.js +8 -11
  68. package/dist/validators/min-length.js +5 -8
  69. package/dist/validators/min.js +4 -7
  70. package/dist/validators/multiple-of.js +5 -8
  71. package/dist/validators/non-negative.js +4 -7
  72. package/dist/validators/non-zero.js +4 -7
  73. package/dist/validators/past-date.js +8 -11
  74. package/dist/validators/pattern.js +5 -8
  75. package/dist/validators/phone.js +6 -9
  76. package/dist/validators/required.js +4 -7
  77. package/dist/validators/url.js +9 -14
  78. package/dist/validators.js +84 -106
  79. package/docs/llms/02-quick-start.md +7 -7
  80. package/docs/llms/03-api-signatures.md +11 -5
  81. package/docs/llms/05-common-mistakes.md +1 -1
  82. package/docs/llms/09-formschema.md +12 -12
  83. package/docs/llms/10-arrays.md +34 -4
  84. package/docs/llms/14-extended-mistakes.md +1 -1
  85. package/docs/llms/15-project-structure.md +15 -13
  86. package/docs/llms/16-ui-components.md +36 -9
  87. package/docs/llms/17-nonexistent-api.md +16 -2
  88. package/docs/llms/29-async-preload.md +4 -4
  89. package/docs/llms/30-type-safety-recipes.md +1 -1
  90. package/docs/llms/34-i18n.md +272 -0
  91. package/llms-index.json +1 -1
  92. package/llms.txt +1925 -175
  93. package/package.json +22 -3
  94. package/dist/create-model-Dxh9cfLp.js +0 -97
  95. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
package/llms.txt CHANGED
@@ -38,6 +38,7 @@
38
38
  - 31-async-validator-debounce.md — async-validator-debounce
39
39
  - 32-async-options-loading.md — async-options-loading
40
40
  - 33-validation-strategy.md — useFormValidation — Единый выбор стратегии валидации
41
+ - 34-i18n.md — i18n — локаль, загрузка словарей и провайдер
41
42
  - API Reference (auto-generated from JSDoc)
42
43
 
43
44
  ## 1. 1. API Reference
@@ -128,7 +129,7 @@ const { value, errors, disabled } = useFormControl(control.loanType);
128
129
  import { createCoreForm, createModel, useFormBundle, type FormProxy } from '@reformer/core';
129
130
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
130
131
  import { required, email } from '@reformer/core/validators';
131
- import { FormField, InputField, Button } from '@reformer/ui-kit';
132
+ import { FormField, Input, Button } from '@reformer/ui-kit';
132
133
 
133
134
  // 1. Define form type as `type` alias (not `interface` — see Recipe 2)
134
135
  type ContactForm = {
@@ -144,12 +145,12 @@ const model = createModel<ContactForm>({ name: '', email: '' });
144
145
  const schema = {
145
146
  name: {
146
147
  value: model.$.name,
147
- component: InputField,
148
+ component: Input,
148
149
  componentProps: { label: 'Name', placeholder: 'Your name' },
149
150
  },
150
151
  email: {
151
152
  value: model.$.email,
152
- component: InputField,
153
+ component: Input,
153
154
  componentProps: { label: 'Email', type: 'email' },
154
155
  },
155
156
  };
@@ -223,14 +224,14 @@ const model = createModel<MyForm>({ properties: [] });
223
224
  const propertyItem = (item: FormModel<PropertyItem>) => ({
224
225
  type: {
225
226
  value: item.$.type,
226
- component: SelectField,
227
+ component: SelectAsync,
227
228
  componentProps: { label: 'Тип', options: [/* ... */] },
228
229
  },
229
- description: { value: item.$.description, component: TextareaField, componentProps: { label: 'Описание' } },
230
+ description: { value: item.$.description, component: Textarea, componentProps: { label: 'Описание' } },
230
231
  estimatedValue: {
231
232
  value: item.$.estimatedValue,
232
- component: InputField,
233
- componentProps: { label: 'Стоимость', type: 'number' },
233
+ component: InputNumber,
234
+ componentProps: { label: 'Стоимость' },
234
235
  },
235
236
  });
236
237
 
@@ -320,6 +321,12 @@ minAge(years: number, options?: { message?: string })
320
321
  maxAge(years: number, options?: { message?: string })
321
322
  ```
322
323
 
324
+ Кроме `message`, каждое правило принимает `messageKey` (ключ словаря приложения) и `params`.
325
+ Без `message` правило кладёт в ошибку пустую строку, а текст при отображении берётся из словаря
326
+ локали по коду (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без
327
+ провайдера. Явное `message` важнее словаря, но языком не переключается; для локализуемого текста —
328
+ `messageKey`. Подробно — [34-i18n.md](./34-i18n.md).
329
+
323
330
  Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
324
331
  `ValidationSchema` и гоняются раннером `validateModel` по требованию.
325
332
 
@@ -328,16 +335,16 @@ import { createModel, createForm } from '@reformer/core';
328
335
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
329
336
  import { required, min, max, email } from '@reformer/core/validators';
330
337
 
331
- type Loan = { email: string; age: number; amount: number };
332
- const model = createModel<Loan>({ email: '', age: 0, amount: 0 });
338
+ type Loan = { email: string; age: number | null; amount: number | null };
339
+ const model = createModel<Loan>({ email: '', age: null, amount: null });
333
340
 
334
341
  // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
335
342
  const form = createForm({
336
343
  model,
337
344
  schema: {
338
- email: { value: model.$.email, component: InputField },
339
- age: { value: model.$.age, component: InputField },
340
- amount: { value: model.$.amount, component: InputField },
345
+ email: { value: model.$.email, component: Input },
346
+ age: { value: model.$.age, component: InputNumber },
347
+ amount: { value: model.$.amount, component: InputNumber },
341
348
  },
342
349
  });
343
350
 
@@ -837,7 +844,7 @@ computeFrom([model.$.price, model.$.quantity], model.$.total, (price, qty) => pr
837
844
  ```typescript
838
845
  // ❌ WRONG - дерево { value, validators } и позиционная строка удалены
839
846
  const schema = {
840
- email: { value: model.$.email, component: InputField, validators: [required(), email()] },
847
+ email: { value: model.$.email, component: Input, validators: [required(), email()] },
841
848
  };
842
849
  validate(path.email, required(), 'Email is required'); // старая сигнатура validate() — удалена
843
850
 
@@ -1090,7 +1097,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1090
1097
  ```typescript
1091
1098
  {
1092
1099
  value: model.$.fieldName, // сигнал модели (PathAwareSignal) — обязателен
1093
- component: InputField, // React-компонент
1100
+ component: Input, // React-компонент
1094
1101
  componentProps?: object, // пропсы (label, placeholder, options, type, ...)
1095
1102
  disabled?: boolean,
1096
1103
  updateOn?: 'change' | 'blur' | 'submit',
@@ -1108,7 +1115,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1108
1115
  import { createModel, createForm } from '@reformer/core';
1109
1116
  import { defineValidationSchema, validate } from '@reformer/core/validation';
1110
1117
  import { required } from '@reformer/core/validators';
1111
- import { InputField, SelectField, CheckboxField } from '@reformer/ui-kit';
1118
+ import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
1112
1119
 
1113
1120
  const model = createModel<MyForm>({ name: '', age: null, agree: false, status: 'active' });
1114
1121
 
@@ -1120,22 +1127,22 @@ const myValidation = defineValidationSchema<MyForm>(({ model }) => {
1120
1127
  const schema = {
1121
1128
  name: {
1122
1129
  value: model.$.name,
1123
- component: InputField,
1130
+ component: Input,
1124
1131
  componentProps: { label: 'Name', placeholder: 'Enter name' },
1125
1132
  },
1126
1133
  age: {
1127
1134
  value: model.$.age,
1128
- component: InputField,
1129
- componentProps: { type: 'number', label: 'Age' },
1135
+ component: InputNumber,
1136
+ componentProps: { label: 'Age' },
1130
1137
  },
1131
1138
  agree: {
1132
1139
  value: model.$.agree,
1133
- component: CheckboxField,
1140
+ component: CheckboxWithLabel,
1134
1141
  componentProps: { label: 'I agree to terms' },
1135
1142
  },
1136
1143
  status: {
1137
1144
  value: model.$.status,
1138
- component: SelectField,
1145
+ component: SelectAsync,
1139
1146
  componentProps: {
1140
1147
  label: 'Status',
1141
1148
  options: [
@@ -1159,9 +1166,9 @@ const form = createForm<MyForm>({ model, schema });
1159
1166
  import type { FormModel } from '@reformer/core';
1160
1167
 
1161
1168
  const addressNodes = (m: FormModel<Address>) => ({
1162
- street: { value: m.$.street, component: InputField, componentProps: { label: 'Street' } },
1163
- city: { value: m.$.city, component: InputField, componentProps: { label: 'City' } },
1164
- zip: { value: m.$.zip, component: InputField, componentProps: { label: 'ZIP' } },
1169
+ street: { value: m.$.street, component: Input, componentProps: { label: 'Street' } },
1170
+ city: { value: m.$.city, component: Input, componentProps: { label: 'City' } },
1171
+ zip: { value: m.$.zip, component: Input, componentProps: { label: 'ZIP' } },
1165
1172
  });
1166
1173
 
1167
1174
  const schema = {
@@ -1178,8 +1185,8 @@ const schema = {
1178
1185
  import type { FormModel } from '@reformer/core';
1179
1186
 
1180
1187
  const itemSchema = (item: FormModel<Item>) => ({
1181
- id: { value: item.$.id, component: InputField, componentProps: { label: 'ID' } },
1182
- name: { value: item.$.name, component: InputField, componentProps: { label: 'Name' } },
1188
+ id: { value: item.$.id, component: Input, componentProps: { label: 'ID' } },
1189
+ name: { value: item.$.name, component: Input, componentProps: { label: 'Name' } },
1183
1190
  });
1184
1191
 
1185
1192
  const schema = {
@@ -1233,7 +1240,7 @@ if (isFieldNode(node)) { /* ... */ }
1233
1240
 
1234
1241
  ```typescript
1235
1242
  import { createModel, createForm, type FormModel } from '@reformer/core';
1236
- import { InputField } from '@reformer/ui-kit';
1243
+ import { Input, InputNumber } from '@reformer/ui-kit';
1237
1244
 
1238
1245
  type Item = { id: string; name: string; price: number };
1239
1246
  type MyForm = { items: Item[] };
@@ -1242,9 +1249,9 @@ const model = createModel<MyForm>({ items: [] });
1242
1249
 
1243
1250
  // под-схема одного элемента (item.$.field — сигнал под-модели)
1244
1251
  const itemSchema = (item: FormModel<Item>) => ({
1245
- id: { value: item.$.id, component: InputField },
1246
- name: { value: item.$.name, component: InputField },
1247
- price: { value: item.$.price, component: InputField, componentProps: { type: 'number' } },
1252
+ id: { value: item.$.id, component: Input },
1253
+ name: { value: item.$.name, component: Input },
1254
+ price: { value: item.$.price, component: InputNumber },
1248
1255
  });
1249
1256
 
1250
1257
  const schema = {
@@ -1258,6 +1265,36 @@ const form = createForm<MyForm>({ model, schema });
1258
1265
  > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
1259
1266
  > См. `30-type-safety-recipes.md`.
1260
1267
 
1268
+ ### Массив как ОДНО значение поля
1269
+
1270
+ Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
1271
+ является, решает **схема**, а не данные: `{ array, item }` — набор под-форм, а узел поля
1272
+ `{ value: model.$.<путь>, component }` — одно значение. Массив, к которому в схеме ничего не
1273
+ привязано, в форму не попадает.
1274
+
1275
+ ```typescript
1276
+ type MyForm = { tags: string[] };
1277
+
1278
+ const model = createModel<MyForm>({ tags: [] });
1279
+
1280
+ const schema = {
1281
+ tags: { value: model.$.tags, component: SelectMulti }, // поле над массивом целиком
1282
+ };
1283
+ const form = createForm<MyForm>({ model, schema });
1284
+
1285
+ form.tags.setValue(['a', 'b']); // FieldNode<string[]>
1286
+ model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
1287
+ validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
1288
+ ```
1289
+
1290
+ - `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `value:` в
1291
+ схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
1292
+ - Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
1293
+ выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
1294
+ лист, и привязывается он так же.
1295
+ - Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
1296
+ `model.patch(...)`.
1297
+
1261
1298
  ### Один массив — три слоя (три разных движка)
1262
1299
 
1263
1300
  Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
@@ -1645,7 +1682,7 @@ import { revalidateWhen, type ValidationError } from '@reformer/core';
1645
1682
  // ❌ старое (УДАЛЕНО): дерево { value, validators }, ModelValidator (value, scope, root), validateFormModel
1646
1683
  const legacyRule: ModelValidator<number, unknown, Form> = (_value, _scope, root) =>
1647
1684
  root.field1 > root.field2 ? { code: 'error', message: 'Invalid' } : null;
1648
- const legacySchema = { field1: { value: model.$.field1, component: InputField, validators: [legacyRule] } };
1685
+ const legacySchema = { field1: { value: model.$.field1, component: Input, validators: [legacyRule] } };
1649
1686
  validateFormModel(model, legacySchema);
1650
1687
 
1651
1688
  // ✅ новое: cross-правило — обычная функция над снапшотом (model.get()), не читает scope/root
@@ -1677,11 +1714,13 @@ revalidateWhen([model.$.field2], () => void validateModel(model, schema));
1677
1714
 
1678
1715
  One form is one module folder. **Default layout — flat minimalist:** every concern is a single
1679
1716
  file at the module root, and the whole form (entry + all wizard steps inline) lives in one
1680
- `index.tsx`. Filenames are flat with no prefix — **the dot-prefix (`form.` / `renderer.`) is
1681
- carried only by `schema` and `behavior`**, the two concerns that have both a model-layer and a
1682
- render-layer version (`form.schema.ts` / `form.behavior.ts` here; renderer slices add
1683
- `renderer.schema.*` / `renderer.behavior.ts`). No `lib/` / `schema/` / `components/steps/`
1684
- nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
1717
+ `index.tsx`. Naming rule: **`form.<role>` is a form artefact, the suffix names its role** —
1718
+ `form.schema.*` (layout), `form.behavior.ts` (model behavior), `form.render.ts` (render-layer
1719
+ behavior, renderer targets only), `form.validation.ts` (validation rules); every other file has
1720
+ no prefix. A wizard may instead keep each
1721
+ step's code in `steps/<slug>/` (same file names inside) with a `steps/index.ts` aggregator. The old
1722
+ `renderer.schema.*` / `renderer.behavior.ts` / `renderer.wizard.tsx` / `validation.ts` names still
1723
+ work with a warning. No `lib/` / `schema/` / `components/` nesting until the form grows (see "Scaling up" below). Arrays are declared in `form.schema.ts`
1685
1724
  and rendered with `FormArraySection` — no per-step component files.
1686
1725
 
1687
1726
  ```
@@ -1695,13 +1734,13 @@ src/
1695
1734
  │ ├── model.ts # createModel + initial values + empty-array-element factories
1696
1735
  │ ├── form.schema.ts # FormSchema: { value: model.$.x, component, componentProps }
1697
1736
  │ ├── form.behavior.ts # defineFormBehavior: compute / enableWhen / hideWhen / copyFrom / onChange
1698
- │ ├── validation.ts # ALL validation → { validateStep, validateAll }
1737
+ │ ├── form.validation.ts # ALL validation → { validateStep, validateAll }
1699
1738
  │ ├── data-sources.ts # options + async loaders (dataSources)
1700
1739
  │ └── api.ts # submit + prefill/load
1701
1740
  ```
1702
1741
 
1703
1742
  Rule of thumb: **one concern → one file at the module root; the whole component tree (all steps) →
1704
- `index.tsx`; validation is a single `validation.ts`.** This is the working default for
1743
+ `index.tsx`; validation is a single `form.validation.ts`.** This is the working default for
1705
1744
  almost every form — reach for folders only when a file stops fitting on a screen.
1706
1745
 
1707
1746
  ### Key Files
@@ -1722,7 +1761,7 @@ export const createCreditApplicationModel = (): FormModel<CreditApplicationForm>
1722
1761
  // forms/credit-application/form.schema.ts
1723
1762
  import type { FormModel } from '@reformer/core';
1724
1763
  export const creditApplicationSchema = (model: FormModel<CreditApplicationForm>) => ({
1725
- loanType: { value: model.$.loanType, component: SelectField, componentProps: { /* ... */ } },
1764
+ loanType: { value: model.$.loanType, component: SelectAsync, componentProps: { /* ... */ } },
1726
1765
  personalData: personalDataNodes(model.$.personalData),
1727
1766
  properties: { array: model.properties, item: propertyItem },
1728
1767
  });
@@ -1770,7 +1809,7 @@ forms/
1770
1809
  ├── schema/ # The form definition
1771
1810
  │ ├── model.ts # createModel factory + initial values + array-element factories
1772
1811
  │ ├── form.schema.ts # schema builder (model) => tree ({ value: model.$.x, component })
1773
- │ ├── validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1812
+ │ ├── form.validation.ts # defineValidationSchema + validateModel config ({ validateStep, validateAll })
1774
1813
  │ ├── form.behavior.ts # defineFormBehavior(...)
1775
1814
  │ └── create-form.ts # Assembly: createCoreForm({ model, schema, behavior, validation })
1776
1815
  │
@@ -1782,7 +1821,7 @@ forms/
1782
1821
 
1783
1822
  Rule of thumb for this layout: **domain raw material → `lib/`; anything describing the form →
1784
1823
  `schema/`; React layout → `components/`; root = entry + `index.ts`.** In very large forms you
1785
- may co-locate each step's `form.schema.ts` / `validation.ts` / `form.behavior.ts` inside its
1824
+ may co-locate each step's `form.schema.ts` / `form.validation.ts` / `form.behavior.ts` inside its
1786
1825
  `steps/[Step]/` folder, keeping only the shared `model` and cross-step rules in `schema/` — see
1787
1826
  the guide below.
1788
1827
 
@@ -1798,9 +1837,9 @@ the guide below.
1798
1837
  > when registering the MCP server to choose which structure the generators lead with. Default
1799
1838
  > is `minimalist`.
1800
1839
 
1801
- > Cross-target variants (renderer-react `renderer.schema.ts` + `renderer.behavior.ts` /
1802
- > renderer-json `renderer.schema.ts` + `renderer.behavior.ts` + `registry.ts`, plus an optional
1803
- > `renderer.wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
1840
+ > Cross-target variants (renderer-react `form.schema.ts` + `form.render.ts` /
1841
+ > renderer-json `form.schema.ts` + `form.render.ts` + `registry.ts`, plus an optional
1842
+ > `wizard.tsx` shim), the `.tsx` / `.json` schema variants, the centralized-vs-co-located
1804
1843
  > choice, and the full reuse map live in the **form-directory-layout** guide (`@reformer/mcp`) —
1805
1844
  > `find_recipe directory-layout`.
1806
1845
 
@@ -1810,12 +1849,13 @@ the guide below.
1810
1849
 
1811
1850
  > **Default rule (read first)**: для UI используй `FormField` из
1812
1851
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
1813
- > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
1852
+ > строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
1814
1853
  > ui-kit не подходит (другая design system, особый low-level input).
1815
1854
  >
1816
1855
  > Канонический schema-driven подход:
1817
1856
  >
1818
- > - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)
1857
+ > - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
1858
+ > сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
1819
1859
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
1820
1860
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
1821
1861
  >
@@ -1827,7 +1867,7 @@ the guide below.
1827
1867
  ```tsx
1828
1868
  import { useMemo } from 'react';
1829
1869
  import { createModel, createForm } from '@reformer/core';
1830
- import { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';
1870
+ import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
1831
1871
 
1832
1872
  type RegistrationForm = {
1833
1873
  email: string;
@@ -1841,12 +1881,12 @@ function RegistrationPage() {
1841
1881
  const schema = {
1842
1882
  email: {
1843
1883
  value: model.$.email,
1844
- component: InputField,
1884
+ component: Input,
1845
1885
  componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
1846
1886
  },
1847
1887
  country: {
1848
1888
  value: model.$.country,
1849
- component: SelectField,
1889
+ component: SelectAsync,
1850
1890
  componentProps: {
1851
1891
  label: 'Country',
1852
1892
  options: [
@@ -1857,7 +1897,7 @@ function RegistrationPage() {
1857
1897
  },
1858
1898
  agree: {
1859
1899
  value: model.$.agree,
1860
- component: CheckboxField,
1900
+ component: CheckboxWithLabel,
1861
1901
  componentProps: { label: 'I agree to terms' },
1862
1902
  },
1863
1903
  };
@@ -1900,7 +1940,7 @@ function RegistrationPage() {
1900
1940
  ✅ Всё это в схеме:
1901
1941
 
1902
1942
  ```ts
1903
- { email: { component: InputField, componentProps: { label: 'Email' } } }
1943
+ { email: { component: Input, componentProps: { label: 'Email' } } }
1904
1944
  ```
1905
1945
 
1906
1946
  ```tsx
@@ -1921,7 +1961,8 @@ import { InputMask } from 'react-input-mask';
1921
1961
  ```
1922
1962
 
1923
1963
  `children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
1924
- props (`value`, `onChange`, `onBlur`, `aria-invalid`).
1964
+ props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
1965
+ статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
1925
1966
 
1926
1967
  ### Advanced — write your own from scratch (rare)
1927
1968
 
@@ -1963,12 +2004,37 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
1963
2004
  <MyFormField control={form.email} /> // ← без label-prop
1964
2005
  ```
1965
2006
 
2007
+ ### Связывание поля с контролом — `FieldAdapter` (`@reformer/core`)
2008
+
2009
+ Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
2010
+ в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
2011
+ описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
2012
+ `passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
2013
+ поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
2014
+ и связывает поле сама — отдельные «field-версии» компонентов не нужны.
2015
+
2016
+ | Символ | Назначение |
2017
+ | ----------------------------- | --------------------------------------------------------------------------- |
2018
+ | `FieldAdapter` | Тип адаптера: диалект контрола |
2019
+ | `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
2020
+ | `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
2021
+ | `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
2022
+ | `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
2023
+ | `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
2024
+
2025
+ В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
2026
+ `@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
2027
+ компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
2028
+
1966
2029
  ### Integration with UI libraries (shadcn etc.)
1967
2030
 
1968
2031
  > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
1969
2032
  > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
1970
2033
  > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
1971
- > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
2034
+ > контрола. Свой компонент можно подготовить и один раз — статикой
2035
+ > `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
2036
+ > и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
2037
+ > прямого JSX без `FormField` и рендерера.
1972
2038
 
1973
2039
  Если есть существующая design system — оборачивай её компоненты в один
1974
2040
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
@@ -2026,6 +2092,20 @@ function ShadcnFormField({ control }: { control: FieldNode<string> }) {
2026
2092
  | `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
2027
2093
  | `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
2028
2094
 
2095
+ ### UI: удалённые field-версии компонентов (`@reformer/ui-kit`)
2096
+
2097
+ Отдельных «field-версий» у компонентов кита больше нет: в `component` поля кладётся сам
2098
+ компонент, диалект он объявляет статикой `reformerAdapter`, связывает поле обёртка
2099
+ (`FormField.Control` / рендерер).
2100
+
2101
+ | Wrong | Correct |
2102
+ | ---------------------------------------------- | --------------------------------------------------------- |
2103
+ | `InputField`, `SelectField`, `CheckboxField`, … (любой `*Field`) | `Input`, `SelectAsync`, `CheckboxWithLabel`, … — таблица в ui-kit `01-overview.md` |
2104
+ | `InputField` + `type: 'number'` | `InputNumber` |
2105
+ | `InputField` + `suggestions` | `InputSuggest` |
2106
+ | `FileUploadField` + `variant: 'dropzone' \| 'input'` | `FileUploadDropzone` / `FileUploadInput` (без `variant`) |
2107
+ | `withFormControl(MyControl, adapter)` | `defineFieldControl(MyControl, { adapter })` из `@reformer/ui-kit/fields` |
2108
+
2029
2109
  ### Common Import Errors
2030
2110
 
2031
2111
  ```typescript
@@ -2064,8 +2144,8 @@ const schema = {
2064
2144
 
2065
2145
  // CORRECT - layout только связывает поле с сигналом модели...
2066
2146
  const layout = {
2067
- name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },
2068
- email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },
2147
+ name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
2148
+ email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
2069
2149
  };
2070
2150
 
2071
2151
  // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
@@ -3656,16 +3736,16 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
3656
3736
 
3657
3737
  ```typescript
3658
3738
  import { createModel, createForm } from '@reformer/core';
3659
- import { InputField, SelectField } from '@reformer/ui-kit';
3739
+ import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
3660
3740
 
3661
3741
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
3662
3742
 
3663
3743
  const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
3664
3744
  const schema = {
3665
- username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },
3745
+ username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
3666
3746
  language: {
3667
3747
  value: model.$.language,
3668
- component: SelectField,
3748
+ component: SelectAsync,
3669
3749
  componentProps: {
3670
3750
  label: 'Язык',
3671
3751
  options: [
@@ -3674,7 +3754,7 @@ const schema = {
3674
3754
  ],
3675
3755
  },
3676
3756
  },
3677
- marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },
3757
+ marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
3678
3758
  };
3679
3759
  const form = createForm({ model, schema });
3680
3760
  // model.get() === { username: '', language: 'ru', marketing: true }
@@ -3880,7 +3960,7 @@ Layout-схема БЕЗ валидаторов — правила живут в
3880
3960
  const model = createModel<CreditApplicationForm>(initial);
3881
3961
 
3882
3962
  const schema = {
3883
- loanAmount: { value: model.$.loanAmount, component: InputField },
3963
+ loanAmount: { value: model.$.loanAmount, component: InputNumber },
3884
3964
  // вложенная группа — builder, принимающий ModelSignals<Sub>
3885
3965
  personalData: personalDataNodes(model.$.personalData),
3886
3966
  // массив — { array, item }
@@ -4422,7 +4502,280 @@ if (typeof window !== 'undefined') ctrl.start();
4422
4502
  - [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
4423
4503
  - [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)
4424
4504
 
4425
- ## 82. API Reference
4505
+ ## 82. Purpose
4506
+
4507
+ **i18n — локаль, загрузка словарей и провайдер**
4508
+
4509
+ Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*`: объект
4510
+ локали, загрузка словарей, React-провайдер, перевод и форматирование. Язык задаётся провайдером,
4511
+ подгружается динамически и переключается на лету; узлы формы при этом не пересоздаются.
4512
+
4513
+ Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
4514
+
4515
+ > Импорт всегда из `@reformer/core/i18n`. Из главной бочки `@reformer/core` этот модуль не
4516
+ > реэкспортируется: React-контекст локализации обязан существовать в одном экземпляре.
4517
+
4518
+ ## 83. Локаль — это данные
4519
+
4520
+ ```typescript
4521
+ interface FormLocale {
4522
+ code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
4523
+ messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
4524
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
4525
+ dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
4526
+ dateFormat?: string; // необязательно: токены date-fns для DatePicker
4527
+ }
4528
+ ```
4529
+
4530
+ Функций в локали нет, поэтому её можно положить JSON-файлом на сервер и получить по сети:
4531
+
4532
+ ```json
4533
+ { "code": "ru", "weekStartsOn": 1, "messages": { "profile.title": "Профиль" } }
4534
+ ```
4535
+
4536
+ `extendLocale(base, patch)` возвращает новую локаль: словари сливаются (ключи из `patch` важнее),
4537
+ остальные поля заменяются.
4538
+
4539
+ ## 84. Формат сообщений — подмножество ICU
4540
+
4541
+ | Конструкция | Пример |
4542
+ | ------------------- | ------------------------------------------------------------------------- |
4543
+ | подстановка | `Открыт файл {name}` |
4544
+ | множественные формы | `{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}` |
4545
+ | точное значение | `{count, plural, =0{файлов нет} other{# файлов}}` |
4546
+ | выбор | `{gender, select, male{он} female{она} other{оно}}` |
4547
+ | экранирование | `'{'` → `{`, `'}'` → `}`, `''` → `'` |
4548
+
4549
+ - Ветка `other` обязательна. `#` внутри `plural` — число, отформатированное по языку.
4550
+ - Типы `number`, `date`, `time`, `selectordinal` не поддерживаются: числа и даты форматируйте
4551
+ отдельно (`useI18n().number`, `.date`) и передавайте готовой строкой.
4552
+ - Категории (`one`/`few`/`many`) берутся из `Intl.PluralRules` языка локали.
4553
+
4554
+ ## 85. Загрузка локалей
4555
+
4556
+ Откуда брать словарь, решает приложение: источник — функция `(code) => локаль | словарь`.
4557
+
4558
+ ```typescript
4559
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
4560
+ import { loadCoreLocale } from '@reformer/core/locale';
4561
+
4562
+ export const loadLocale = createLocaleLoader([
4563
+ loadCoreLocale, // встроенные словари ядра — чанк нужного языка
4564
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
4565
+ ]);
4566
+ ```
4567
+
4568
+ - Источники опрашиваются параллельно, сливаются по порядку: поздний перекрывает ранний.
4569
+ - Источник отдаёт локаль целиком (`{ code, messages, … }`) или голый словарь (`{ "a.b": "…" }`);
4570
+ `null` означает «для этого языка у меня ничего нет» и ошибкой не считается.
4571
+ - Результат кэшируется по коду языка, идущий запрос не дублируется.
4572
+ - Отказ любого источника — отказ загрузки. Источник, которому можно промахнуться, оборачивайте:
4573
+ `(code) => source(code).catch(() => null)`.
4574
+ - `loadLocale.peek(code)` — уже загруженная локаль или `undefined`; `loadLocale.preload(code)` — то
4575
+ же, что вызов загрузчика.
4576
+
4577
+ Ядро отдаёт свои локали в трёх видах:
4578
+
4579
+ | Вид | Импорт | Для чего |
4580
+ | ----------------- | ------------------------------------------- | ----------------------------------------------- |
4581
+ | загрузчик | `loadCoreLocale` из `@reformer/core/locale` | основной путь: чанк языка по запросу |
4582
+ | JSON-файл | `@reformer/core/locale/ru.json` | выложить на свой сервер, править без пересборки |
4583
+ | синхронный модуль | `ru` из `@reformer/core/locale/ru` | SSR и тесты |
4584
+
4585
+ Встроенные языки ядра — `en` и `ru`. Словарь ядра несёт тексты ошибок валидации
4586
+ (`validation.<code>`) и единицы размера файла (`format.fileSize.b|kb|mb|gb|tb`).
4587
+
4588
+ Пакеты выше отдают локаль накопительно и в тех же трёх видах, поэтому источник встроенных словарей
4589
+ в загрузчике всегда один — самого верхнего из используемых пакетов:
4590
+
4591
+ | Пакеты приложения | Источник | Что в локали |
4592
+ | ------------------------------- | -------------------------------------------- | --------------------------------- |
4593
+ | только ядро | `loadCoreLocale` из `@reformer/core/locale` | ошибки валидации, размер файла |
4594
+ | ядро + `@reformer/cdk` | `loadCdkLocale` из `@reformer/cdk/locale` | то же плюс строки cdk (`cdk.*`) |
4595
+ | ядро + cdk + `@reformer/ui-kit` | `loadKitLocale` из `@reformer/ui-kit/locale` | то же плюс подписи кита (`kit.*`) |
4596
+
4597
+ ## 86. Провайдер
4598
+
4599
+ ```tsx
4600
+ import { I18nProvider } from '@reformer/core/i18n';
4601
+
4602
+ function App() {
4603
+ const [lang, setLang] = useState('ru');
4604
+ return (
4605
+ <I18nProvider lang={lang} load={loadLocale}>
4606
+ <LangSwitch value={lang} onChange={setLang} />
4607
+ <CreditForm />
4608
+ </I18nProvider>
4609
+ );
4610
+ }
4611
+ ```
4612
+
4613
+ | Проп | Назначение |
4614
+ | --------------- | ------------------------------------------------------------------------ |
4615
+ | `lang` + `load` | провайдер грузит локаль сам |
4616
+ | `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
4617
+ | `onError` | отказ загрузки `(error, lang)` |
4618
+ | `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
4619
+
4620
+ Поведение при `lang` + `load`:
4621
+
4622
+ - при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
4623
+ `useI18n().pending` — для индикатора;
4624
+ - при быстром переключении применяется только последний запрос;
4625
+ - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
4626
+ - если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
4627
+ синхронный и `fallback` не показывается.
4628
+
4629
+ SSR: эффекты на сервере не выполняются, поэтому там передавайте готовый объект —
4630
+ `<I18nProvider locale={ru}>` — либо заранее вызовите `preload`.
4631
+
4632
+ Вложенный провайдер заменяет локаль для своего поддерева целиком.
4633
+
4634
+ ## 87. Хуки
4635
+
4636
+ ```typescript
4637
+ const { code, locale, pending, error, t, has, number, date, fileSize } = useI18n();
4638
+
4639
+ t('profile.title'); // ключ приложения
4640
+ t('cart.items', { count: 3 }); // с подстановками
4641
+ t('cart.empty', undefined, 'Your cart is empty'); // английский текст на случай промаха
4642
+ number(1234.5); // '1 234,5' в ru
4643
+ date(new Date(), { dateStyle: 'long' }); // '15 января 2024 г.' в ru
4644
+ fileSize(1536); // '1,5 КБ' в ru, '1.5 KB' без провайдера
4645
+ ```
4646
+
4647
+ `useMessages(builtinEn)` — для авторов пакетов и китов: ищет ключ в локали провайдера, а при
4648
+ промахе берёт встроенную английскую таблицу пакета. Тип ключа выводится из таблицы.
4649
+
4650
+ ```tsx
4651
+ import en from './locale/en.json';
4652
+
4653
+ function ClearButton() {
4654
+ const t = useMessages(en);
4655
+ return <button aria-label={t('kit.select.clear')} />;
4656
+ }
4657
+ ```
4658
+
4659
+ ## 88. Тексты ошибок валидации
4660
+
4661
+ Правила из `@reformer/core/validators` несут только `code` и `params`; готового текста у них нет
4662
+ (`message` по умолчанию — пустая строка). Строку даёт резолвер при отображении ошибки:
4663
+
4664
+ 1. `messageKey` автора правила — если такой ключ есть в активной локали;
4665
+ 2. явное `message` автора правила;
4666
+ 3. `validation.<code>` из словаря локали;
4667
+ 4. `validation.<code>` из встроенной английской таблицы ядра;
4668
+ 5. сам код — для своих кодов, которых нет ни в одном словаре.
4669
+
4670
+ ```typescript
4671
+ validate(model.$.email, [
4672
+ required(), // «Обязательное поле» в ru, английская фраза без провайдера
4673
+ minLength(5, { message: 'Слишком коротко' }), // явный текст: важнее словаря, но языком не переключается
4674
+ email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
4675
+ ]);
4676
+ ```
4677
+
4678
+ - Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
4679
+ ошибки без повторной валидации.
4680
+ - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит ошибку сам. В своём компоненте —
4681
+ `useValidationMessage()`; вне React — `resolveValidationError(error, createI18n(locale))`.
4682
+ - Свой код ошибки локализуется ключом `validation.<свой_код>` в словаре приложения либо
4683
+ `messageKey` в самом правиле.
4684
+ - Встроенный текст меняется тем же ключом в словаре приложения:
4685
+ `extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
4686
+ - Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
4687
+ размеры в байтах (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) —
4688
+ строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
4689
+
4690
+ ```tsx
4691
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
4692
+ const message = useValidationMessage();
4693
+ const { errors, shouldShowError } = useFormControl(control);
4694
+ return shouldShowError ? <span role="alert">{message(errors[0])}</span> : null;
4695
+ }
4696
+ ```
4697
+
4698
+ Смонтированный `ValidationMessagesProvider` из `@reformer/cdk` заменяет этот резолвер целиком:
4699
+ локаль для ошибок в его поддереве не участвует.
4700
+
4701
+ ## 89. Что показывается, когда ключа нет
4702
+
4703
+ | Ключ | Нет в локали | Сообщение не разбирается |
4704
+ | -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
4705
+ | пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
4706
+ | приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
4707
+
4708
+ Пропущенная подстановка выводится как `{name}`. Форма не падает ни в одном из случаев: неполный
4709
+ словарь, пришедший по сети, — законный сценарий.
4710
+
4711
+ ## 90. Проверка словаря в тестах
4712
+
4713
+ ```typescript
4714
+ import { validateLocale } from '@reformer/core/i18n';
4715
+
4716
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
4717
+ // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
4718
+ ```
4719
+
4720
+ ## 91. Описатели сообщений (низкий уровень)
4721
+
4722
+ `msg(key, values?, defaultMessage?)` создаёт описатель сообщения, `defineMessages(appEn)` — `msg` с
4723
+ типизированными ключами, `resolveLocalized(value, i18n)` раскрывает описатели в объекте пропсов.
4724
+
4725
+ > ⚠️ Рендереры и `FormField` описатели сами пока НЕ раскрывают. Не кладите `msg(...)` в
4726
+ > `componentProps` схемы: компонент получит объект вместо строки. До появления раскрытия в
4727
+ > рендерерах вызывайте `resolveLocalized` вручную или используйте `useI18n().t`.
4728
+
4729
+ ## 92. Anti-patterns
4730
+
4731
+ ```typescript
4732
+ // ❌ Импорт из главной бочки — i18n там нет, а второй путь импорта дал бы вторую копию контекста
4733
+ import { I18nProvider } from '@reformer/core';
4734
+
4735
+ // ✅ Единственный путь импорта — подпуть i18n
4736
+ import { I18nProvider } from '@reformer/core/i18n';
4737
+ ```
4738
+
4739
+ ```tsx
4740
+ // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль грузится заново, первый кадр — fallback
4741
+ <I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
4742
+
4743
+ // ✅ Загрузчик — константа модуля: один кэш на приложение
4744
+ const loadLocale = createLocaleLoader([loadCoreLocale]);
4745
+ <I18nProvider lang={lang} load={loadLocale}>
4746
+ ```
4747
+
4748
+ ```typescript
4749
+ // ❌ Число или дата типами ICU внутри сообщения — не поддерживается, сообщение не разберётся
4750
+ const message = 'Итого: {total, number}';
4751
+
4752
+ // ✅ Отформатировать отдельно и передать готовой строкой
4753
+ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) });
4754
+ ```
4755
+
4756
+ ```tsx
4757
+ // ❌ Ошибка выводится полем message: у правил без своего текста оно пустое — на экране ничего
4758
+ <span>{errors[0].message}</span>;
4759
+
4760
+ // ✅ Текст ошибки — через резолвер: он знает словарь локали, messageKey и явное сообщение
4761
+ const message = useValidationMessage();
4762
+ <span>{message(errors[0])}</span>;
4763
+ ```
4764
+
4765
+ ```typescript
4766
+ // ❌ Настройки локали без словаря: объект без поля messages принимается за словарь сообщений
4767
+ const source = () => ({ weekStartsOn: 1 });
4768
+
4769
+ // ✅ Часть локали узнаётся по полю messages — отдавайте его хотя бы пустым
4770
+ const source = () => ({ weekStartsOn: 1, messages: {} });
4771
+ ```
4772
+
4773
+ ## 93. See also
4774
+
4775
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
4776
+ - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
4777
+
4778
+ ## 94. API Reference
4426
4779
 
4427
4780
  _Auto-generated from JSDoc on public exports._
4428
4781
 
@@ -4693,8 +5046,8 @@ export class ArrayNode<T extends object> extends FormNode<T[]> {
4693
5046
 
4694
5047
  ```typescript
4695
5048
  const array = new ArrayNode({
4696
- title: { value: '', component: InputField },
4697
- price: { value: 0, component: InputField },
5049
+ title: { value: '', component: Input },
5050
+ price: { value: 0, component: Input },
4698
5051
  });
4699
5052
 
4700
5053
  array.push({ title: 'Item 1', price: 100 });
@@ -4823,6 +5176,35 @@ export type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };
4823
5176
 
4824
5177
  _Source: src/form/behaviors/types.ts_
4825
5178
 
5179
+ ### bindFieldProps
5180
+
5181
+ **Kind:** `function`
5182
+
5183
+ Props контрола под адаптер: срезает служебные ключи обёртки и `strip` адаптера, кладёт значение
5184
+ в `valueProp` (через `toValue`), вешает `changeProp`, который прогоняет эмит контрола через
5185
+ `fromEmit` в value-based `onChange` поля, и пробрасывает blur (`bindBlur` либо `onBlur`).
5186
+ Без адаптера seam проходит как есть (`value` / `onChange` / `onBlur`).
5187
+
5188
+ `componentProps` спредятся ПЕРВЫМИ — seam их перекрывает при совпадении ключей.
5189
+
5190
+ **Signature:**
5191
+ ```typescript
5192
+ export function bindFieldProps(
5193
+ adapter: FieldAdapter | undefined,
5194
+ seam: FieldSeam,
5195
+ componentProps: Record<string, unknown> = {}
5196
+ ): Record<string, unknown>
5197
+ ```
5198
+
5199
+ **Parameters:**
5200
+ - `adapter` — - Адаптер контрола или `undefined` (value-based контрол).
5201
+ - `seam` — - Значение поля и его колбэки.
5202
+ - `componentProps` — - Прочие props контрола.
5203
+
5204
+ **Returns:** Плоский объект props для спреда в контрол.
5205
+
5206
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
5207
+
4826
5208
  ### buildValidation
4827
5209
 
4828
5210
  **Kind:** `function`
@@ -4860,6 +5242,35 @@ await v!.validateAll(); // все шаги + extras
4860
5242
 
4861
5243
  _Source: src/form/validation/config.ts_
4862
5244
 
5245
+ ### bundledLocaleSource
5246
+
5247
+ **Kind:** `function`
5248
+
5249
+ Источник встроенных локалей пакета: по чанку на язык. Язык ищется точно, затем по основной части
5250
+ кода (`ru-RU` → `ru`). Языка нет — источник отдаёт `null`: подписи пакета останутся английскими,
5251
+ пока приложение не даст их своим источником.
5252
+
5253
+ **Signature:**
5254
+ ```typescript
5255
+ export function bundledLocaleSource(
5256
+ loaders: Readonly<Record<string, () => Promise<FormLocale>>>
5257
+ ): LocaleSource
5258
+ ```
5259
+
5260
+ **Parameters:**
5261
+ - `loaders` — - Код языка → ленивый импорт его локали.
5262
+
5263
+ **Examples:**
5264
+
5265
+ ```ts
5266
+ export const loadKitLocale = bundledLocaleSource({
5267
+ en: () => import('./en').then((m) => m.en),
5268
+ ru: () => import('./ru').then((m) => m.ru),
5269
+ });
5270
+ ```
5271
+
5272
+ _Source: src/i18n/loader.ts_
5273
+
4863
5274
  ### ChangeContext
4864
5275
 
4865
5276
  **Kind:** `interface`
@@ -4977,6 +5388,19 @@ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail }
4977
5388
 
4978
5389
  _Source: src/model/behaviors-value.ts_
4979
5390
 
5391
+ ### CORE_LOCALES
5392
+
5393
+ **Kind:** `const`
5394
+
5395
+ Языки, которые ядро поставляет само.
5396
+
5397
+ **Signature:**
5398
+ ```typescript
5399
+ export const CORE_LOCALES: readonly string[]
5400
+ ```
5401
+
5402
+ _Source: src/locale/index.ts_
5403
+
4980
5404
  ### CORE_RUNTIME_TOKEN
4981
5405
 
4982
5406
  **Kind:** `const`
@@ -5114,8 +5538,8 @@ const model = createModel<UserForm>({ email: '', password: '' });
5114
5538
  // Layout-схема НЕ несёт validators — только component/componentProps.
5115
5539
  const schema = {
5116
5540
  children: [
5117
- { value: model.$.email, component: InputField },
5118
- { value: model.$.password, component: InputField },
5541
+ { value: model.$.email, component: Input },
5542
+ { value: model.$.password, component: Input },
5119
5543
  ],
5120
5544
  };
5121
5545
 
@@ -5207,9 +5631,9 @@ const schema = {
5207
5631
  component: Section,
5208
5632
  children: [
5209
5633
  // Layout несёт только component/componentProps; правила — в отдельной ValidationSchema.
5210
- { value: model.$.email, component: InputField },
5634
+ { value: model.$.email, component: Input },
5211
5635
  // вложенная группа: `model.$.profile.name` (≡ под-модель `model.profile.$.name` — тот же сигнал)
5212
- { value: model.$.profile.name, component: InputField },
5636
+ { value: model.$.profile.name, component: Input },
5213
5637
  ],
5214
5638
  };
5215
5639
 
@@ -5287,6 +5711,122 @@ dispose();
5287
5711
 
5288
5712
  _Source: src/form/validation/strategy.ts_
5289
5713
 
5714
+ ### createI18n
5715
+
5716
+ **Kind:** `function`
5717
+
5718
+ Ручка для локали. Для одного и того же объекта локали возвращается один и тот же объект —
5719
+ ручку можно держать в зависимостях хуков.
5720
+
5721
+ **Signature:**
5722
+ ```typescript
5723
+ export function createI18n(locale: FormLocale): I18nHandle
5724
+ ```
5725
+
5726
+ **Parameters:**
5727
+ - `locale` — - Локаль; см. {@link FormLocale}.
5728
+
5729
+ **Examples:**
5730
+
5731
+ ```ts
5732
+ const i18n = createI18n({ code: 'ru', messages: { 'cart.items': '{count, plural, one{# товар} few{# товара} other{# товаров}}' } });
5733
+ i18n.t('cart.items', { count: 3 }); // '3 товара'
5734
+ i18n.fileSize(1536); // '1,5 КБ'
5735
+ ```
5736
+
5737
+ _Source: src/i18n/translator.ts_
5738
+
5739
+ ### createLocaleLoader
5740
+
5741
+ **Kind:** `function`
5742
+
5743
+ Собирает загрузчик из источников. Источники опрашиваются параллельно, а сливаются по порядку:
5744
+ поздний перекрывает ранний. Результат кэшируется по коду языка, идущий запрос не дублируется.
5745
+ Отказ любого источника — отказ загрузки; из кэша такой запрос удаляется, и следующая попытка
5746
+ пойдёт заново. Источник, которому можно промахнуться, приложение оборачивает само:
5747
+ `(code) => source(code).catch(() => null)`.
5748
+
5749
+ Код языка у результата — всегда запрошенный: по нему работает `Intl`, и он не должен зависеть
5750
+ от того, что написано внутри файла.
5751
+
5752
+ **Signature:**
5753
+ ```typescript
5754
+ export function createLocaleLoader(sources: readonly LocaleSource[]): LocaleLoader
5755
+ ```
5756
+
5757
+ **Parameters:**
5758
+ - `sources` — - Источники в порядке возрастания приоритета.
5759
+
5760
+ **Examples:**
5761
+
5762
+ ```ts
5763
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
5764
+ import { loadKitLocale } from '@reformer/ui-kit/locale';
5765
+
5766
+ export const loadLocale = createLocaleLoader([
5767
+ loadKitLocale, // подписи кита — чанк нужного языка
5768
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
5769
+ ]);
5770
+ ```
5771
+
5772
+ _Source: src/i18n/loader.ts_
5773
+
5774
+ ### createLocaleStore
5775
+
5776
+ **Kind:** `function`
5777
+
5778
+ Создаёт хранилище. Загрузка при создании НЕ начинается: начальное состояние считается чисто —
5779
+ из кэша загрузчика, если локаль там уже есть, — а запрос уходит на первый {@link LocaleStore.setLang}.
5780
+ Это позволяет создавать хранилище прямо в рендере.
5781
+
5782
+ **Signature:**
5783
+ ```typescript
5784
+ export function createLocaleStore(
5785
+ load: LocaleLoad,
5786
+ lang: string,
5787
+ options: LocaleStoreOptions = {}
5788
+ ): LocaleStore
5789
+ ```
5790
+
5791
+ **Parameters:**
5792
+ - `load` — - Загрузчик локалей.
5793
+ - `lang` — - Начальный язык.
5794
+ - `options` — - См. {@link LocaleStoreOptions}.
5795
+
5796
+ _Source: src/i18n/locale-store.ts_
5797
+
5798
+ ### createMessageFormatter
5799
+
5800
+ **Kind:** `function`
5801
+
5802
+ Форматтер одной локали с кэшем разбора: сообщение разбирается один раз, на первый вызов.
5803
+ Это внутренность `I18nService.t()` — сам `t()` добавляет сверху поиск ключа в словаре, маркер
5804
+ пропущенного ключа и откат на резервную локаль.
5805
+
5806
+ **Signature:**
5807
+ ```typescript
5808
+ export function createMessageFormatter(
5809
+ locale: string,
5810
+ options: MessageFormatOptions = {}
5811
+ ): (source: string, values?: MessageValues) => string
5812
+ ```
5813
+
5814
+ **Parameters:**
5815
+ - `locale` — - Локаль форматтера.
5816
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
5817
+
5818
+ **Returns:** Функция `(сообщение, значения) => строка`.
5819
+
5820
+ **Examples:**
5821
+
5822
+ ```ts
5823
+ const format = createMessageFormatter('ru');
5824
+ format('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}', { count: 5 });
5825
+ // '5 файлов'
5826
+ ```
5827
+
5828
+ _Source: src/i18n/message-format.ts_
5829
+
5290
5830
  ### createModel
5291
5831
 
5292
5832
  **Kind:** `function`
@@ -5341,6 +5881,20 @@ export function cross<TSnapshot>(
5341
5881
 
5342
5882
  _Source: src/form/validation/operators.ts_
5343
5883
 
5884
+ ### DEFAULT_LOCALE
5885
+
5886
+ **Kind:** `const`
5887
+
5888
+ Локаль «провайдера нет»: английский, пустой словарь. Каждый пакет при промахе по словарю берёт
5889
+ свою встроенную английскую таблицу, поэтому пустой словарь означает «всё по-английски».
5890
+
5891
+ **Signature:**
5892
+ ```typescript
5893
+ export const DEFAULT_LOCALE: FormLocale
5894
+ ```
5895
+
5896
+ _Source: src/i18n/locale.ts_
5897
+
5344
5898
  ### defer
5345
5899
 
5346
5900
  **Kind:** `function`
@@ -5377,6 +5931,37 @@ export const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {
5377
5931
 
5378
5932
  _Source: src/form/behaviors/context.ts_
5379
5933
 
5934
+ ### defineMessages
5935
+
5936
+ **Kind:** `function`
5937
+
5938
+ `msg` с типизированными ключами: принимает английский словарь приложения, проверяет ключ при
5939
+ компиляции и подставляет английский текст как `defaultMessage` — без провайдера и при промахе
5940
+ по локали форма покажет его, а не ключ.
5941
+
5942
+ **Signature:**
5943
+ ```typescript
5944
+ export function defineMessages<D extends Messages>(
5945
+ defaults: D
5946
+ ): {
5947
+ msg(key: keyof D & string, values?: Readonly<Record<string, unknown>>): MessageDescriptor;
5948
+ }
5949
+ ```
5950
+
5951
+ **Parameters:**
5952
+ - `defaults` — - Английский словарь приложения.
5953
+
5954
+ **Examples:**
5955
+
5956
+ ```ts
5957
+ import appEn from './locales/en.json';
5958
+
5959
+ export const { msg } = defineMessages(appEn);
5960
+ msg('profile.email.label'); // опечатка в ключе — ошибка компиляции
5961
+ ```
5962
+
5963
+ _Source: src/i18n/descriptor.ts_
5964
+
5380
5965
  ### defineValidationSchema
5381
5966
 
5382
5967
  **Kind:** `function`
@@ -5435,7 +6020,8 @@ _Source: src/form/behaviors/node.ts_
5435
6020
  **Kind:** `function`
5436
6021
 
5437
6022
  Применить под-правила к КАЖДОМУ элементу текущего массива модели.
5438
- `U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.
6023
+ `U extends object` — элементы должны быть под-моделями (объектами); массив примитивов — одно
6024
+ значение поля, его проверяют целиком: `validate(model.$.<массив>, [...])`.
5439
6025
 
5440
6026
  **Signature:**
5441
6027
  ```typescript
@@ -5454,9 +6040,11 @@ _Source: src/form/validation/operators.ts_
5454
6040
  Обойти ВСЕ листовые сигналы модели (включая элементы массивов), вызвав `visit` на каждом.
5455
6041
 
5456
6042
  Внутри реактивного `effect` служит подпиской «любое поле изменилось»: `visit(sig => void sig.value)`
5457
- подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Так строятся
5458
- триггеры `change`/`blur` стратегий валидации (см. `createFormValidation`), тем же паттерном, что
5459
- `revalidateWhen`, но без ручного перечисления зависимостей.
6043
+ подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Тот же
6044
+ паттерн, что `revalidateWhen`, но без ручного перечисления зависимостей.
6045
+
6046
+ Сами массивы обход не посещает — только их элементы. Поле, привязанное к массиву целиком
6047
+ (мультивыбор), живёт на сигнале массива; чтобы увидеть и его, берите {@link eachValueSignal}.
5460
6048
 
5461
6049
  **Signature:**
5462
6050
  ```typescript
@@ -5468,18 +6056,40 @@ export function eachLeafSignal<T>(
5468
6056
 
5469
6057
  _Source: src/model/create-model.ts_
5470
6058
 
5471
- ### effect
6059
+ ### eachValueSignal
5472
6060
 
5473
6061
  **Kind:** `function`
5474
6062
 
5475
- Реактивный эффект (авто-dispose). Колбэк может вернуть собственный cleanup.
6063
+ Обойти ВСЕ ручки значений модели: листья (включая элементы массивов) И сами массивы.
6064
+
6065
+ Отличие от {@link eachLeafSignal} — узлы-массивы: массив бывает одним значением поля
6066
+ (`{ value: model.$.tags, component }`), и нода такого поля привязана к сигналу массива, а не
6067
+ к сигналам элементов. Обходчику, которому нужна именно НОДА поля (стратегия `blur` подписывается
6068
+ на её `touched`), одних листьев мало — он не увидел бы мультивыбор. Так строятся триггеры
6069
+ `change`/`blur` стратегий валидации (см. `createFormValidation`).
5476
6070
 
5477
6071
  **Signature:**
5478
6072
  ```typescript
5479
- export function effect(fn: () => void | (() => void)): void
6073
+ export function eachValueSignal<T>(
6074
+ model: FormModel<T>,
6075
+ visit: (signal: PathAwareSignal<unknown>) => void
6076
+ ): void
5480
6077
  ```
5481
6078
 
5482
- _Source: src/form/behaviors/context.ts_
6079
+ _Source: src/model/create-model.ts_
6080
+
6081
+ ### effect
6082
+
6083
+ **Kind:** `function`
6084
+
6085
+ Реактивный эффект (авто-dispose). Колбэк может вернуть собственный cleanup.
6086
+
6087
+ **Signature:**
6088
+ ```typescript
6089
+ export function effect(fn: () => void | (() => void)): void
6090
+ ```
6091
+
6092
+ _Source: src/form/behaviors/context.ts_
5483
6093
 
5484
6094
  ### email
5485
6095
 
@@ -5641,6 +6251,136 @@ exclusiveFlag(model.$.contacts, (row) => row.$.primary);
5641
6251
 
5642
6252
  _Source: src/form/behaviors/collections.ts_
5643
6253
 
6254
+ ### extendLocale
6255
+
6256
+ **Kind:** `function`
6257
+
6258
+ Новая локаль на основе существующей: словари сливаются (ключи из `patch` важнее), остальные
6259
+ поля заменяются. Исходный объект не меняется.
6260
+
6261
+ **Signature:**
6262
+ ```typescript
6263
+ export function extendLocale(base: FormLocale, patch: Partial<FormLocale>): FormLocale
6264
+ ```
6265
+
6266
+ **Parameters:**
6267
+ - `base` — - Локаль-основа, например поставляемая китом.
6268
+ - `patch` — - Свои ключи, переопределения китовых подписей и поля локали.
6269
+
6270
+ **Examples:**
6271
+
6272
+ ```ts
6273
+ import { ru } from '@reformer/ui-kit/locale/ru';
6274
+
6275
+ const appRu = extendLocale(ru, {
6276
+ messages: { 'profile.title': 'Профиль', 'kit.formWizard.submit': 'Отправить' },
6277
+ });
6278
+ ```
6279
+
6280
+ _Source: src/i18n/locale.ts_
6281
+
6282
+ ### fetchMessages
6283
+
6284
+ **Kind:** `function`
6285
+
6286
+ Источник локали по сети: забирает JSON по адресу, построенному из кода языка. Файл может быть
6287
+ словарём (`{ "profile.title": "Профиль" }`) или локалью целиком (`{ "code": "ru", "messages": … }`).
6288
+ Ответ не `2xx` — ошибка загрузки.
6289
+
6290
+ **Signature:**
6291
+ ```typescript
6292
+ export function fetchMessages(url: (code: string) => string, init?: RequestInit): LocaleSource
6293
+ ```
6294
+
6295
+ **Parameters:**
6296
+ - `url` — - Адрес файла для языка.
6297
+ - `init` — - Параметры запроса, передаются в `fetch` как есть.
6298
+
6299
+ **Examples:**
6300
+
6301
+ ```ts
6302
+ fetchMessages((code) => `/locales/${code}.json`);
6303
+ ```
6304
+
6305
+ _Source: src/i18n/loader.ts_
6306
+
6307
+ ### FIELD_WRAPPER_ONLY_PROPS
6308
+
6309
+ **Kind:** `const`
6310
+
6311
+ Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
6312
+ подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
6313
+
6314
+ **Signature:**
6315
+ ```typescript
6316
+ export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
6317
+ ```
6318
+
6319
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6320
+
6321
+ ### FieldAdapter
6322
+
6323
+ **Kind:** `interface`
6324
+
6325
+ Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
6326
+ DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
6327
+
6328
+ **Signature:**
6329
+ ```typescript
6330
+ export interface FieldAdapter {
6331
+ /** Проп, из которого контрол читает значение (по умолчанию `'value'`). */
6332
+ valueProp?: string;
6333
+ /** Колбэк, через который контрол эмитит изменение (по умолчанию `'onChange'`). */
6334
+ changeProp?: string;
6335
+ /**
6336
+ * Эмит контрола → значение поля (по умолчанию — как есть). `rest` — прочие props контрола
6337
+ * (например, чтобы достать `options` при резолве значения).
6338
+ */
6339
+ fromEmit?: (arg: unknown, rest: Record<string, unknown>) => unknown;
6340
+ /** Значение поля → `valueProp` контрола (коэрсия `null`/`undefined`; по умолчанию — как есть). */
6341
+ toValue?: (value: unknown) => unknown;
6342
+ /** Проброс blur нестандартным каналом (по умолчанию — `onBlur`). */
6343
+ bindBlur?: (onBlur: () => void) => Record<string, unknown>;
6344
+ /** Ключи, которые убрать из `componentProps` перед спредом в контрол. */
6345
+ strip?: string[];
6346
+ /**
6347
+ * Передавать ли контролу ноду формы пропом `control` (только рендерер). По умолчанию `false`:
6348
+ * errors/touched обслуживает обёртка поля. Ставь `true`, если контрол сам вызывает
6349
+ * `useFormControl(control)`.
6350
+ */
6351
+ passControl?: boolean;
6352
+ }
6353
+ ```
6354
+
6355
+ **Examples:**
6356
+
6357
+ Radix Checkbox
6358
+ ```ts
6359
+ const checkedAdapter: FieldAdapter = {
6360
+ valueProp: 'checked',
6361
+ changeProp: 'onCheckedChange',
6362
+ fromEmit: (c) => c === true,
6363
+ toValue: (v) => v ?? false,
6364
+ };
6365
+ ```
6366
+
6367
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6368
+
6369
+ ### FieldAdapterCarrier
6370
+
6371
+ **Kind:** `interface`
6372
+
6373
+ Компонент, объявивший свой диалект статикой.
6374
+
6375
+ **Signature:**
6376
+ ```typescript
6377
+ export interface FieldAdapterCarrier {
6378
+ reformerAdapter?: FieldAdapter;
6379
+ }
6380
+ ```
6381
+
6382
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6383
+
5644
6384
  ### FieldConfig
5645
6385
 
5646
6386
  **Kind:** `interface`
@@ -5746,11 +6486,12 @@ export interface FieldControlState<T> {
5746
6486
  * @example
5747
6487
  * ```tsx
5748
6488
  * const { errors } = useFormControl(field);
6489
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5749
6490
  *
5750
6491
  * return (
5751
6492
  * <ul className="error-list">
5752
6493
  * {errors.map((error, i) => (
5753
- * <li key={i}>{error.message}</li>
6494
+ * <li key={i}>{message(error)}</li>
5754
6495
  * ))}
5755
6496
  * </ul>
5756
6497
  * );
@@ -5813,12 +6554,13 @@ export interface FieldControlState<T> {
5813
6554
  * @example
5814
6555
  * ```tsx
5815
6556
  * const { shouldShowError, errors } = useFormControl(field);
6557
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5816
6558
  *
5817
6559
  * return (
5818
6560
  * <div>
5819
6561
  * <input {...props} />
5820
6562
  * {shouldShowError && (
5821
- * <span className="error">{errors[0]?.message}</span>
6563
+ * <span className="error">{message(errors[0])}</span>
5822
6564
  * )}
5823
6565
  * </div>
5824
6566
  * );
@@ -5881,6 +6623,7 @@ control: FieldNode<string>;
5881
6623
 
5882
6624
  function TextField({ control }: Props) {
5883
6625
  const state = useFormControl(control);
6626
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
5884
6627
 
5885
6628
  return (
5886
6629
  <div>
@@ -5890,7 +6633,7 @@ disabled={state.disabled}
5890
6633
  onChange={e => control.setValue(e.target.value)}
5891
6634
  />
5892
6635
  {state.shouldShowError && state.errors[0] && (
5893
- <span className="error">{state.errors[0].message}</span>
6636
+ <span className="error">{message(state.errors[0])}</span>
5894
6637
  )}
5895
6638
  </div>
5896
6639
  );
@@ -5903,6 +6646,38 @@ onChange={e => control.setValue(e.target.value)}
5903
6646
 
5904
6647
  _Source: src/platforms/react/hooks/types.ts_
5905
6648
 
6649
+ ### FieldHandle
6650
+
6651
+ **Kind:** `interface`
6652
+
6653
+ Базовый императивный контракт поля формы.
6654
+
6655
+ Обёртка поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`)
6656
+ строит его из DOM-узла контрола. Композиты со своим handle (`open`/`close`/`reload`/…) отдают
6657
+ его как есть — он расширяет этот контракт.
6658
+
6659
+ Достаётся из render-схемы по селектору:
6660
+ `schema.node(sel).getRef<FieldHandle>().current?.focus()`.
6661
+
6662
+ Покрывает ТОЛЬКО императивные действия. Реактивное состояние (value / disabled / options /
6663
+ validation) живёт в behaviors и через handle не дублируется.
6664
+
6665
+ **Signature:**
6666
+ ```typescript
6667
+ export interface FieldHandle {
6668
+ /** Сфокусировать поле (делегирует на DOM-элемент контрола). */
6669
+ focus(): void;
6670
+ /** Снять фокус. */
6671
+ blur(): void;
6672
+ /** Проскроллить поле в область видимости. */
6673
+ scrollIntoView(opts?: ScrollIntoViewOptions): void;
6674
+ /** Живой DOM-элемент поля (или `null` до монтирования / для размонтированной ноды). */
6675
+ getElement(): HTMLElement | null;
6676
+ }
6677
+ ```
6678
+
6679
+ _Source: src/platforms/react/field-binding/field-handle.ts_
6680
+
5906
6681
  ### FieldNode
5907
6682
 
5908
6683
  **Kind:** `class`
@@ -5923,7 +6698,7 @@ export class FieldNode<T> extends FormNode<T> {
5923
6698
  ```typescript
5924
6699
  const field = new FieldNode({
5925
6700
  value: '',
5926
- component: InputField,
6701
+ component: Input,
5927
6702
  validators: [required, email],
5928
6703
  });
5929
6704
 
@@ -5934,6 +6709,23 @@ console.log(field.valid.value); // true
5934
6709
 
5935
6710
  _Source: src/form/nodes/field-node.ts_
5936
6711
 
6712
+ ### FieldSeam
6713
+
6714
+ **Kind:** `interface`
6715
+
6716
+ Seam поля: текущее значение и value-based колбэки.
6717
+
6718
+ **Signature:**
6719
+ ```typescript
6720
+ export interface FieldSeam {
6721
+ value: unknown;
6722
+ onChange: (value: unknown) => void;
6723
+ onBlur: () => void;
6724
+ }
6725
+ ```
6726
+
6727
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6728
+
5937
6729
  ### FieldStatus
5938
6730
 
5939
6731
  **Kind:** `type`
@@ -6064,6 +6856,33 @@ todos.forEach((item, i) => {
6064
6856
 
6065
6857
  _Source: src/form/types/form-proxy.ts_
6066
6858
 
6859
+ ### formatPattern
6860
+
6861
+ **Kind:** `function`
6862
+
6863
+ Собирает строку из разобранного сообщения. Значения подставляются как текст и повторно
6864
+ не разбираются.
6865
+
6866
+ **Signature:**
6867
+ ```typescript
6868
+ export function formatPattern(
6869
+ pattern: MessagePattern,
6870
+ values: MessageValues,
6871
+ locale: string,
6872
+ options: MessageFormatOptions = {}
6873
+ ): string
6874
+ ```
6875
+
6876
+ **Parameters:**
6877
+ - `pattern` — - Результат {@link parseMessage}.
6878
+ - `values` — - Значения аргументов.
6879
+ - `locale` — - Локаль для {@link Intl.PluralRules} и {@link Intl.NumberFormat} (`ru`, `en`, …).
6880
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
6881
+
6882
+ **Returns:** Готовая строка интерфейса.
6883
+
6884
+ _Source: src/i18n/message-format.ts_
6885
+
6067
6886
  ### FormBehavior
6068
6887
 
6069
6888
  **Kind:** `interface`
@@ -6209,6 +7028,43 @@ try {
6209
7028
 
6210
7029
  _Source: src/form/validation/error-handler.ts_
6211
7030
 
7031
+ ### FormLocale
7032
+
7033
+ **Kind:** `interface`
7034
+
7035
+ Локаль формы.
7036
+
7037
+ **Signature:**
7038
+ ```typescript
7039
+ export interface FormLocale {
7040
+ /** Код языка BCP 47 (`en`, `ru`, `pt-BR`): по нему работают `Intl` и множественные формы. */
7041
+ readonly code: string;
7042
+ /** Словарь сообщений. Ключа нет — пакет подставит встроенный английский. */
7043
+ readonly messages: Messages;
7044
+ /** Первый день недели календаря. По умолчанию — воскресенье. */
7045
+ readonly weekStartsOn?: WeekStart;
7046
+ /**
7047
+ * Необязательное переопределение: объект локали `react-day-picker` / `date-fns`. Для ядра
7048
+ * непрозрачен. Без него даты форматируются через `Intl` по {@link FormLocale.code}.
7049
+ */
7050
+ readonly dateLocale?: unknown;
7051
+ /** Необязательное переопределение: формат даты в токенах `date-fns` для DatePicker. */
7052
+ readonly dateFormat?: string;
7053
+ }
7054
+ ```
7055
+
7056
+ **Examples:**
7057
+
7058
+ ```ts
7059
+ const ru: FormLocale = {
7060
+ code: 'ru',
7061
+ weekStartsOn: 1,
7062
+ messages: { 'kit.select.selected': 'Выбрано: {count}' },
7063
+ };
7064
+ ```
7065
+
7066
+ _Source: src/i18n/locale.ts_
7067
+
6212
7068
  ### FormModel
6213
7069
 
6214
7070
  **Kind:** `type`
@@ -6374,14 +7230,14 @@ interface Form {
6374
7230
  }
6375
7231
 
6376
7232
  const schema: FormSchema<Form> = {
6377
- name: { value: '', component: InputField },
7233
+ name: { value: '', component: Input },
6378
7234
  address: {
6379
- city: { value: '', component: InputField },
6380
- street: { value: '', component: InputField },
7235
+ city: { value: '', component: Input },
7236
+ street: { value: '', component: Input },
6381
7237
  },
6382
7238
  items: [{
6383
- title: { value: '', component: InputField },
6384
- price: { value: 0, component: InputField },
7239
+ title: { value: '', component: Input },
7240
+ price: { value: 0, component: Input },
6385
7241
  }],
6386
7242
  };
6387
7243
  ```
@@ -6642,6 +7498,26 @@ validate(model.$.appointmentDate, [required(), futureDate({ message: 'Дата
6642
7498
 
6643
7499
  _Source: src/form/validators/future-date.ts_
6644
7500
 
7501
+ ### getFieldAdapter
7502
+
7503
+ **Kind:** `function`
7504
+
7505
+ Адаптер, объявленный компонентом статикой `reformerAdapter`, либо `undefined`.
7506
+
7507
+ **Signature:**
7508
+ ```typescript
7509
+ export function getFieldAdapter(component: unknown): FieldAdapter | undefined
7510
+ ```
7511
+
7512
+ **Examples:**
7513
+
7514
+ ```ts
7515
+ Checkbox.reformerAdapter = checkedAdapter;
7516
+ getFieldAdapter(Checkbox); // → checkedAdapter
7517
+ ```
7518
+
7519
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
7520
+
6645
7521
  ### getNodeForSignal
6646
7522
 
6647
7523
  **Kind:** `function`
@@ -6738,8 +7614,8 @@ export class GroupNode<T> extends FormNode<T> {
6738
7614
 
6739
7615
  ```typescript
6740
7616
  const form = new GroupNode({
6741
- email: { valueSignal: model.$.email, component: InputField },
6742
- password: { valueSignal: model.$.password, component: InputField },
7617
+ email: { valueSignal: model.$.email, component: Input },
7618
+ password: { valueSignal: model.$.password, component: Input },
6743
7619
  });
6744
7620
 
6745
7621
  // Прямой доступ к полям через Proxy
@@ -6770,6 +7646,163 @@ export interface GroupNodeConfig<T> {
6770
7646
 
6771
7647
  _Source: src/form/types/index.ts_
6772
7648
 
7649
+ ### I18N_CONTEXT_MARKER
7650
+
7651
+ **Kind:** `const`
7652
+
7653
+ Имя контекста и одновременно маркер, по которому страж ищет его копии в сборках.
7654
+
7655
+ **Signature:**
7656
+ ```typescript
7657
+ export const I18N_CONTEXT_MARKER
7658
+ ```
7659
+
7660
+ _Source: src/platforms/react/i18n/context.ts_
7661
+
7662
+ ### I18nContextValue
7663
+
7664
+ **Kind:** `interface`
7665
+
7666
+ Что видит компонент: ручка активной локали и состояние её загрузки.
7667
+
7668
+ **Signature:**
7669
+ ```typescript
7670
+ export interface I18nContextValue extends I18nHandle {
7671
+ /** Идёт загрузка другого языка; на экране пока прежний. Без загрузчика всегда `false`. */
7672
+ readonly pending: boolean;
7673
+ /** Ошибка последней загрузки языка. */
7674
+ readonly error: unknown;
7675
+ }
7676
+ ```
7677
+
7678
+ _Source: src/platforms/react/i18n/context.ts_
7679
+
7680
+ ### I18nHandle
7681
+
7682
+ **Kind:** `interface`
7683
+
7684
+ Ручка локали: перевод и форматирование по одному языку.
7685
+
7686
+ **Signature:**
7687
+ ```typescript
7688
+ export interface I18nHandle {
7689
+ /** Локаль, по которой работает ручка. */
7690
+ readonly locale: FormLocale;
7691
+ /** Код языка — то же, что `locale.code`. */
7692
+ readonly code: string;
7693
+ /** Есть ли ключ в словаре локали (встроенные английские таблицы не учитываются). */
7694
+ has(key: string): boolean;
7695
+ /**
7696
+ * Перевод ключа приложения: словарь локали → `defaultMessage` → встроенная таблица ядра → ключ.
7697
+ *
7698
+ * @param key - Ключ сообщения.
7699
+ * @param values - Значения подстановок.
7700
+ * @param defaultMessage - Английский текст на случай, если ключа в локали нет.
7701
+ */
7702
+ t(key: string, values?: MessageValues, defaultMessage?: string): string;
7703
+ /** Число по правилам языка. */
7704
+ number(value: number, options?: Intl.NumberFormatOptions): string;
7705
+ /** Дата по правилам языка. */
7706
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
7707
+ /** Размер файла: `1536` → `1.5 KB` (`1,5 КБ` в русской локали). База — 1024. */
7708
+ fileSize(bytes: number): string;
7709
+ }
7710
+ ```
7711
+
7712
+ _Source: src/i18n/translator.ts_
7713
+
7714
+ ### I18nLoadingProviderProps
7715
+
7716
+ **Kind:** `interface`
7717
+
7718
+ Режим «провайдер грузит сам».
7719
+
7720
+ **Signature:**
7721
+ ```typescript
7722
+ export interface I18nLoadingProviderProps {
7723
+ /** Код языка. Смена кода запускает загрузку; до её конца на экране прежний язык. */
7724
+ lang: string;
7725
+ /** Загрузчик локалей — обычно результат `createLocaleLoader`. */
7726
+ load: LocaleLoad;
7727
+ /**
7728
+ * Что показать, пока не загружена самая первая локаль. По умолчанию — ничего: иначе на кадр
7729
+ * мелькнул бы встроенный английский. Если локаль уже в кэше загрузчика (`load.preload(lang)`
7730
+ * до монтирования), первый рендер синхронный и `fallback` не показывается.
7731
+ */
7732
+ fallback?: ReactNode;
7733
+ /** Отказ загрузки. Язык на экране при этом не меняется. */
7734
+ onError?: (error: unknown, lang: string) => void;
7735
+ children?: ReactNode;
7736
+ }
7737
+ ```
7738
+
7739
+ _Source: src/platforms/react/i18n/provider.ts_
7740
+
7741
+ ### I18nProvider
7742
+
7743
+ **Kind:** `function`
7744
+
7745
+ Провайдер локализации для подписей компонентов, ошибок валидации и авторских подписей формы.
7746
+ Без провайдера компоненты говорят встроенным английским.
7747
+
7748
+ При смене `lang` на экране остаётся прежний язык, пока новый не загрузится (`useI18n().pending`
7749
+ — для индикатора). При быстром переключении применяется только последний запрос. Вложенный
7750
+ провайдер заменяет локаль для своего поддерева целиком.
7751
+
7752
+ **Signature:**
7753
+ ```typescript
7754
+ export function I18nProvider(props: I18nProviderProps): ReactElement | null
7755
+ ```
7756
+
7757
+ **Examples:**
7758
+
7759
+ Провайдер грузит локаль сам
7760
+ ```tsx
7761
+ const [lang, setLang] = useState('ru');
7762
+
7763
+ <I18nProvider lang={lang} load={loadLocale}>
7764
+ <LangSwitch value={lang} onChange={setLang} />
7765
+ <CreditForm />
7766
+ </I18nProvider>
7767
+ ```
7768
+
7769
+ Готовая локаль — SSR и тесты
7770
+ ```tsx
7771
+ import { ru } from '@reformer/ui-kit/locale/ru';
7772
+
7773
+ <I18nProvider locale={ru}><CreditForm /></I18nProvider>
7774
+ ```
7775
+
7776
+ _Source: src/platforms/react/i18n/provider.ts_
7777
+
7778
+ ### I18nProviderProps
7779
+
7780
+ **Kind:** `type`
7781
+
7782
+ **Signature:**
7783
+ ```typescript
7784
+ export type I18nProviderProps = I18nStaticProviderProps | I18nLoadingProviderProps;
7785
+ ```
7786
+
7787
+ _Source: src/platforms/react/i18n/provider.ts_
7788
+
7789
+ ### I18nStaticProviderProps
7790
+
7791
+ **Kind:** `interface`
7792
+
7793
+ Режим «готовая локаль».
7794
+
7795
+ **Signature:**
7796
+ ```typescript
7797
+ export interface I18nStaticProviderProps {
7798
+ /** Локаль. Смена объекта переключает язык сразу. */
7799
+ locale: FormLocale;
7800
+ children?: ReactNode;
7801
+ }
7802
+ ```
7803
+
7804
+ _Source: src/platforms/react/i18n/provider.ts_
7805
+
6773
7806
  ### integer
6774
7807
 
6775
7808
  **Kind:** `function`
@@ -7015,6 +8048,19 @@ if (isGroupNode(node)) {
7015
8048
 
7016
8049
  _Source: src/form/type-guards.ts_
7017
8050
 
8051
+ ### isMessageDescriptor
8052
+
8053
+ **Kind:** `function`
8054
+
8055
+ Является ли значение описателем сообщения.
8056
+
8057
+ **Signature:**
8058
+ ```typescript
8059
+ export function isMessageDescriptor(value: unknown): value is MessageDescriptor
8060
+ ```
8061
+
8062
+ _Source: src/i18n/descriptor.ts_
8063
+
7018
8064
  ### isModelContainerSignal
7019
8065
 
7020
8066
  **Kind:** `function`
@@ -7075,25 +8121,240 @@ validate(model.$.amount, [required(), isNumber({ message: 'Введите чис
7075
8121
 
7076
8122
  _Source: src/form/validators/is-number.ts_
7077
8123
 
7078
- ### markDerived
8124
+ ### isValueSignal
7079
8125
 
7080
8126
  **Kind:** `function`
7081
8127
 
7082
- Пометить сигнал производным (вычисляемым).
7083
-
7084
- Вызывается операторами {@link computeFrom}/`compute` для их сигнала-цели. После этого
7085
- bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
7086
- значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
8128
+ «Ручка значения», к которой можно привязать поле: лист (`instanceof Signal`) либо узел-массив
8129
+ дерева `model.$` (значение — массив целиком, `.value` читается и пишется).
7087
8130
 
7088
- Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
7089
- пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
8131
+ Группа сюда не входит: она читается целиком, но не пишется и полем не бывает. Этой проверкой
8132
+ `createForm` и рендереры отличают узел-поле схемы (`{ value, component }`) от контейнера —
8133
+ вместо голого `value instanceof Signal`, который узел-массив не проходит.
7090
8134
 
7091
8135
  **Signature:**
7092
8136
  ```typescript
7093
- export function markDerived(signal: Signal<any>): void
8137
+ export function isValueSignal(value: unknown): value is Signal<unknown>
7094
8138
  ```
7095
8139
 
7096
- **Parameters:**
8140
+ **Examples:**
8141
+
8142
+ ```typescript
8143
+ const model = createModel({ name: '', tags: [] as string[], address: { city: '' } });
8144
+ isValueSignal(model.$.name); // true — лист
8145
+ isValueSignal(model.$.tags); // true — массив как значение
8146
+ isValueSignal(model.$.address); // false — группа
8147
+ ```
8148
+
8149
+ _Source: src/model/model-signals-proxy.ts_
8150
+
8151
+ ### loadCoreLocale
8152
+
8153
+ **Kind:** `const`
8154
+
8155
+ Источник локалей ядра для `createLocaleLoader`: чанк нужного языка по запросу.
8156
+
8157
+ **Signature:**
8158
+ ```typescript
8159
+ export const loadCoreLocale
8160
+ ```
8161
+
8162
+ **Examples:**
8163
+
8164
+ ```ts
8165
+ const loadLocale = createLocaleLoader([loadCoreLocale, fetchMessages((code) => `/locales/${code}.json`)]);
8166
+ ```
8167
+
8168
+ _Source: src/locale/index.ts_
8169
+
8170
+ ### LocaleIssue
8171
+
8172
+ **Kind:** `interface`
8173
+
8174
+ Что не так со словарём.
8175
+
8176
+ **Signature:**
8177
+ ```typescript
8178
+ export interface LocaleIssue {
8179
+ readonly key: string;
8180
+ /**
8181
+ * - `syntax` — сообщение не разбирается;
8182
+ * - `missing` — ключ есть в образце, но его нет в словаре;
8183
+ * - `extra` — ключа нет в образце;
8184
+ * - `arguments` — набор подстановок не совпадает с образцом.
8185
+ */
8186
+ readonly kind: 'syntax' | 'missing' | 'extra' | 'arguments';
8187
+ readonly message: string;
8188
+ }
8189
+ ```
8190
+
8191
+ _Source: src/i18n/locale.ts_
8192
+
8193
+ ### LocaleLoad
8194
+
8195
+ **Kind:** `interface`
8196
+
8197
+ Функция загрузки локали. `peek` — необязательный синхронный кэш (есть у `createLocaleLoader`).
8198
+
8199
+ **Signature:**
8200
+ ```typescript
8201
+ export interface LocaleLoad {
8202
+ (code: string): Promise<FormLocale>;
8203
+ peek?(code: string): FormLocale | undefined;
8204
+ }
8205
+ ```
8206
+
8207
+ _Source: src/i18n/locale-store.ts_
8208
+
8209
+ ### LocaleLoader
8210
+
8211
+ **Kind:** `interface`
8212
+
8213
+ Загрузчик локалей с кэшем.
8214
+
8215
+ **Signature:**
8216
+ ```typescript
8217
+ export interface LocaleLoader {
8218
+ /** Загрузить локаль языка. Повторный вызов возвращает тот же промис и ту же локаль. */
8219
+ (code: string): Promise<FormLocale>;
8220
+ /** Уже загруженная локаль или `undefined`. Позволяет провайдеру отрисоваться синхронно. */
8221
+ peek(code: string): FormLocale | undefined;
8222
+ /** То же, что вызов загрузчика; имя для чтения в коде старта приложения. */
8223
+ preload(code: string): Promise<FormLocale>;
8224
+ }
8225
+ ```
8226
+
8227
+ _Source: src/i18n/loader.ts_
8228
+
8229
+ ### LocaleSource
8230
+
8231
+ **Kind:** `type`
8232
+
8233
+ Источник локали. Получает код языка, отдаёт локаль или словарь — сразу или промисом.
8234
+ `null`/`undefined` означает «для этого языка у меня ничего нет» и ошибкой не считается.
8235
+
8236
+ **Signature:**
8237
+ ```typescript
8238
+ export type LocaleSource = (code: string) => LocaleSourceResult | Promise<LocaleSourceResult>;
8239
+ ```
8240
+
8241
+ _Source: src/i18n/loader.ts_
8242
+
8243
+ ### LocaleSourceResult
8244
+
8245
+ **Kind:** `type`
8246
+
8247
+ Что может вернуть источник: локаль целиком, её часть, голый словарь или ничего.
8248
+
8249
+ **Signature:**
8250
+ ```typescript
8251
+ export type LocaleSourceResult = Partial<FormLocale> | Messages | null | undefined;
8252
+ ```
8253
+
8254
+ _Source: src/i18n/loader.ts_
8255
+
8256
+ ### LocaleStore
8257
+
8258
+ **Kind:** `interface`
8259
+
8260
+ Хранилище активной локали.
8261
+
8262
+ **Signature:**
8263
+ ```typescript
8264
+ export interface LocaleStore {
8265
+ getState(): LocaleStoreState;
8266
+ subscribe(listener: () => void): () => void;
8267
+ /** Запросить язык. Повторный запрос того же языка, пока он грузится или уже показан, — no-op. */
8268
+ setLang(lang: string): void;
8269
+ }
8270
+ ```
8271
+
8272
+ _Source: src/i18n/locale-store.ts_
8273
+
8274
+ ### LocaleStoreOptions
8275
+
8276
+ **Kind:** `interface`
8277
+
8278
+ Настройки хранилища.
8279
+
8280
+ **Signature:**
8281
+ ```typescript
8282
+ export interface LocaleStoreOptions {
8283
+ /** Вызывается при отказе загрузки языка, который всё ещё запрошен. */
8284
+ readonly onError?: (error: unknown, lang: string) => void;
8285
+ }
8286
+ ```
8287
+
8288
+ _Source: src/i18n/locale-store.ts_
8289
+
8290
+ ### LocaleStoreState
8291
+
8292
+ **Kind:** `interface`
8293
+
8294
+ Состояние хранилища. Объект неизменяемый: новая ссылка — новое состояние.
8295
+
8296
+ **Signature:**
8297
+ ```typescript
8298
+ export interface LocaleStoreState {
8299
+ /** Запрошенный язык. Пока `pending`, может отличаться от языка `locale`. */
8300
+ readonly lang: string;
8301
+ /** Локаль, которая сейчас на экране; `null` — ни одна ещё не загружена. */
8302
+ readonly locale: FormLocale | null;
8303
+ /** Идёт загрузка запрошенного языка. */
8304
+ readonly pending: boolean;
8305
+ /** Ошибка последней загрузки; сбрасывается следующей удачной. */
8306
+ readonly error: unknown;
8307
+ }
8308
+ ```
8309
+
8310
+ _Source: src/i18n/locale-store.ts_
8311
+
8312
+ ### LocalizableText
8313
+
8314
+ **Kind:** `type`
8315
+
8316
+ Текст, который можно задать строкой или описателем.
8317
+
8318
+ **Signature:**
8319
+ ```typescript
8320
+ export type LocalizableText = string | MessageDescriptor;
8321
+ ```
8322
+
8323
+ _Source: src/i18n/descriptor.ts_
8324
+
8325
+ ### makeElementFieldHandle
8326
+
8327
+ **Kind:** `function`
8328
+
8329
+ Собирает базовый {@link FieldHandle}, делегирующий на DOM-элемент по ссылке `el`.
8330
+ Все вызовы null-safe: до монтирования (`el.current === null`) — no-op, без исключений.
8331
+
8332
+ **Signature:**
8333
+ ```typescript
8334
+ export function makeElementFieldHandle(el: RefObject<HTMLElement | null>): FieldHandle
8335
+ ```
8336
+
8337
+ _Source: src/platforms/react/field-binding/field-handle.ts_
8338
+
8339
+ ### markDerived
8340
+
8341
+ **Kind:** `function`
8342
+
8343
+ Пометить сигнал производным (вычисляемым).
8344
+
8345
+ Вызывается операторами {@link computeFrom}/`compute` для их сигнала-цели. После этого
8346
+ bulk-сеттеры (`model.set`/`model.patch`, `patchValue`/`setValue`) пропускают это поле, чтобы
8347
+ значение из payload не затирало вычисляемое. Прикладной код напрямую обычно не вызывает.
8348
+
8349
+ Идемпотентна на уровне флага: повторные вызовы наращивают счётчик ссылок, чтобы каждый оператор,
8350
+ пишущий в этот сигнал, был сбалансирован своим {@link unmarkDerived} при dispose.
8351
+
8352
+ **Signature:**
8353
+ ```typescript
8354
+ export function markDerived(signal: Signal<any>): void
8355
+ ```
8356
+
8357
+ **Parameters:**
7097
8358
  - `signal` — - Сигнал значения из {@link FormModel}, которым владеет compute
7098
8359
 
7099
8360
  **Examples:**
@@ -7317,103 +8578,261 @@ export function maxFileSize<TForm = unknown, TField = unknown>(
7317
8578
  - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7318
8579
  попадают `maxFileSize`, `fileName` и `actualSize`.
7319
8580
 
7320
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8581
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8582
+
8583
+ **Examples:**
8584
+
8585
+ Ограничение размера вложений
8586
+ ```typescript
8587
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8588
+ import { maxFileSize } from '@reformer/core/validators';
8589
+
8590
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8591
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8592
+ validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
8593
+ });
8594
+ ```
8595
+
8596
+ _Source: src/form/validators/max-file-size.ts_
8597
+
8598
+ ### maxLength
8599
+
8600
+ **Kind:** `function`
8601
+
8602
+ Фабрика валидатора максимальной длины строки или массива.
8603
+
8604
+ Работает со строкой или массивом (проверяется `value.length`). Пустые значения
8605
+ (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
8606
+ (используйте {@link required} для обязательности).
8607
+
8608
+ **Signature:**
8609
+ ```typescript
8610
+ export function maxLength<TForm = unknown, TField = unknown>(
8611
+ maxLen: number,
8612
+ options?: ValidateOptions
8613
+ ): Validator<TForm, TField>
8614
+ ```
8615
+
8616
+ **Parameters:**
8617
+ - `maxLen` — - Максимально допустимая длина (включительно)
8618
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8619
+ попадают `maxLength` и `actualLength`.
8620
+
8621
+ **Returns:** Чистый валидатор {@link Validator} для строки или массива
8622
+
8623
+ **Examples:**
8624
+
8625
+ Максимальная длина строки
8626
+ ```typescript
8627
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8628
+ import { maxLength } from '@reformer/core/validators';
8629
+
8630
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8631
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8632
+ validate(model.$.name, [maxLength(50)]);
8633
+ validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
8634
+ });
8635
+ ```
8636
+
8637
+ _Source: src/form/validators/max-length.ts_
8638
+
8639
+ ### maxTotalFileSize
8640
+
8641
+ **Kind:** `function`
8642
+
8643
+ Фабрика валидатора суммарного размера всех файлов.
8644
+
8645
+ Складывает `size` всех файлов значения (элементы без числового `size` в сумму
8646
+ не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
8647
+ (используйте {@link required} для обязательности).
8648
+
8649
+ **Signature:**
8650
+ ```typescript
8651
+ export function maxTotalFileSize<TForm = unknown, TField = unknown>(
8652
+ maxTotal: number,
8653
+ options?: ValidateOptions
8654
+ ): Validator<TForm, TField>
8655
+ ```
8656
+
8657
+ **Parameters:**
8658
+ - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
8659
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8660
+ попадают `maxTotalFileSize` и `actualTotal`.
8661
+
8662
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8663
+
8664
+ **Examples:**
8665
+
8666
+ Суммарно не более 20 МБ
8667
+ ```typescript
8668
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8669
+ import { maxTotalFileSize } from '@reformer/core/validators';
8670
+
8671
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8672
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8673
+ validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
8674
+ });
8675
+ ```
8676
+
8677
+ _Source: src/form/validators/max-total-file-size.ts_
8678
+
8679
+ ### MESSAGE_DESCRIPTOR
8680
+
8681
+ **Kind:** `const`
8682
+
8683
+ Бренд описателя. `Symbol.for` — чтобы описатель узнавался и через границу двух копий модуля
8684
+ (разные чанки, разные версии пакета на странице).
8685
+
8686
+ **Signature:**
8687
+ ```typescript
8688
+ export const MESSAGE_DESCRIPTOR: unique symbol
8689
+ ```
8690
+
8691
+ _Source: src/i18n/descriptor.ts_
8692
+
8693
+ ### messageArguments
8694
+
8695
+ **Kind:** `function`
8696
+
8697
+ Имена аргументов сообщения, включая те, что встречаются только внутри веток `plural`/`select`.
8698
+
8699
+ **Signature:**
8700
+ ```typescript
8701
+ export function messageArguments(
8702
+ pattern: MessagePattern,
8703
+ out: Set<string> = new Set()
8704
+ ): Set<string>
8705
+ ```
8706
+
8707
+ **Parameters:**
8708
+ - `pattern` — - Результат `parseMessage`.
8709
+
8710
+ _Source: src/i18n/locale.ts_
8711
+
8712
+ ### MessageBranches
8713
+
8714
+ **Kind:** `type`
8715
+
8716
+ Ветки `plural`/`select`: категория CLDR (`one`, `few`, …), точное совпадение (`=0`) или значение `select`.
8717
+
8718
+ **Signature:**
8719
+ ```typescript
8720
+ export type MessageBranches = ReadonlyMap<string, MessagePattern>;
8721
+ ```
8722
+
8723
+ _Source: src/i18n/message-format.ts_
8724
+
8725
+ ### MessageDescriptor
8726
+
8727
+ **Kind:** `interface`
8728
+
8729
+ Описатель сообщения. Намеренно без ключей `value`, `array`, `item`, `component`: обход схемы в
8730
+ `createForm` узнаёт по ним узел, а описатель узлом не является.
8731
+
8732
+ **Signature:**
8733
+ ```typescript
8734
+ export interface MessageDescriptor {
8735
+ readonly [MESSAGE_DESCRIPTOR]: true;
8736
+ /** Ключ сообщения в словаре приложения. */
8737
+ readonly key: string;
8738
+ /** Значения подстановок. */
8739
+ readonly values?: Readonly<Record<string, unknown>>;
8740
+ /** Английский текст на случай, если ключа в локали нет. */
8741
+ readonly defaultMessage?: string;
8742
+ }
8743
+ ```
8744
+
8745
+ _Source: src/i18n/descriptor.ts_
8746
+
8747
+ ### MessageFormatOptions
8748
+
8749
+ **Kind:** `interface`
8750
+
8751
+ Настройки форматирования.
8752
+
8753
+ **Signature:**
8754
+ ```typescript
8755
+ export interface MessageFormatOptions {
8756
+ /**
8757
+ * Что подставить вместо аргумента, которого нет в значениях (или чьё значение не число там, где
8758
+ * ожидалось число). По умолчанию — заметный маркер `⟦name⟧`, тот же приём, что и для пропущенного
8759
+ * ключа: пропуск должен попадаться разработчику, а не пользователю.
8760
+ */
8761
+ readonly onMissingArgument?: (name: string) => string;
8762
+ }
8763
+ ```
8764
+
8765
+ _Source: src/i18n/message-format.ts_
8766
+
8767
+ ### MessageNode
8768
+
8769
+ **Kind:** `type`
7321
8770
 
7322
- **Examples:**
8771
+ Узел разобранного сообщения.
7323
8772
 
7324
- Ограничение размера вложений
8773
+ **Signature:**
7325
8774
  ```typescript
7326
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7327
- import { maxFileSize } from '@reformer/core/validators';
7328
-
7329
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7330
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7331
- validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
7332
- });
8775
+ export type MessageNode =
8776
+ | { readonly kind: 'text'; readonly value: string }
8777
+ | { readonly kind: 'arg'; readonly name: string }
8778
+ | { readonly kind: 'number'; readonly name: string }
8779
+ | { readonly kind: 'plural'; readonly name: string; readonly branches: MessageBranches }
8780
+ | { readonly kind: 'select'; readonly name: string; readonly branches: MessageBranches };
7333
8781
  ```
7334
8782
 
7335
- _Source: src/form/validators/max-file-size.ts_
7336
-
7337
- ### maxLength
8783
+ _Source: src/i18n/message-format.ts_
7338
8784
 
7339
- **Kind:** `function`
8785
+ ### MessagePattern
7340
8786
 
7341
- Фабрика валидатора максимальной длины строки или массива.
8787
+ **Kind:** `type`
7342
8788
 
7343
- Работает со строкой или массивом (проверяется `value.length`). Пустые значения
7344
- (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
7345
- (используйте {@link required} для обязательности).
8789
+ Разобранное сообщение — плоская последовательность узлов.
7346
8790
 
7347
8791
  **Signature:**
7348
8792
  ```typescript
7349
- export function maxLength<TForm = unknown, TField = unknown>(
7350
- maxLen: number,
7351
- options?: ValidateOptions
7352
- ): Validator<TForm, TField>
8793
+ export type MessagePattern = readonly MessageNode[];
7353
8794
  ```
7354
8795
 
7355
- **Parameters:**
7356
- - `maxLen` — - Максимально допустимая длина (включительно)
7357
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7358
- попадают `maxLength` и `actualLength`.
8796
+ _Source: src/i18n/message-format.ts_
7359
8797
 
7360
- **Returns:** Чистый валидатор {@link Validator} для строки или массива
8798
+ ### Messages
7361
8799
 
7362
- **Examples:**
8800
+ **Kind:** `type`
7363
8801
 
7364
- Максимальная длина строки
7365
- ```typescript
7366
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7367
- import { maxLength } from '@reformer/core/validators';
8802
+ Плоский словарь: ключ → сообщение в формате ICU (см. `message-format.ts`).
7368
8803
 
7369
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7370
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7371
- validate(model.$.name, [maxLength(50)]);
7372
- validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
7373
- });
8804
+ **Signature:**
8805
+ ```typescript
8806
+ export type Messages = Readonly<Record<string, string>>;
7374
8807
  ```
7375
8808
 
7376
- _Source: src/form/validators/max-length.ts_
7377
-
7378
- ### maxTotalFileSize
8809
+ _Source: src/i18n/locale.ts_
7379
8810
 
7380
- **Kind:** `function`
8811
+ ### MessageSyntaxError
7381
8812
 
7382
- Фабрика валидатора суммарного размера всех файлов.
8813
+ **Kind:** `class`
7383
8814
 
7384
- Складывает `size` всех файлов значения (элементы без числового `size` в сумму
7385
- не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
7386
- (используйте {@link required} для обязательности).
8815
+ Отказ разбора сообщения: синтаксис или отсутствие обязательной ветки `other`.
7387
8816
 
7388
8817
  **Signature:**
7389
8818
  ```typescript
7390
- export function maxTotalFileSize<TForm = unknown, TField = unknown>(
7391
- maxTotal: number,
7392
- options?: ValidateOptions
7393
- ): Validator<TForm, TField>
8819
+ export class MessageSyntaxError extends Error { /* … */ }
7394
8820
  ```
7395
8821
 
7396
- **Parameters:**
7397
- - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
7398
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7399
- попадают `maxTotalFileSize` и `actualTotal`.
8822
+ _Source: src/i18n/message-format.ts_
7400
8823
 
7401
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8824
+ ### MessageValues
7402
8825
 
7403
- **Examples:**
8826
+ **Kind:** `type`
7404
8827
 
7405
- Суммарно не более 20 МБ
7406
- ```typescript
7407
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7408
- import { maxTotalFileSize } from '@reformer/core/validators';
8828
+ Значения аргументов сообщения.
7409
8829
 
7410
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7411
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7412
- validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
7413
- });
8830
+ **Signature:**
8831
+ ```typescript
8832
+ export type MessageValues = Readonly<Record<string, unknown>>;
7414
8833
  ```
7415
8834
 
7416
- _Source: src/form/validators/max-total-file-size.ts_
8835
+ _Source: src/i18n/message-format.ts_
7417
8836
 
7418
8837
  ### min
7419
8838
 
@@ -7691,7 +9110,10 @@ export interface ModelApi<T> {
7691
9110
  reset(): void;
7692
9111
  /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
7693
9112
  captureInitial(): void;
7694
- /** Резолв строкового пути в сигнал (для error-routing/мостов). */
9113
+ /**
9114
+ * Резолв строкового пути в сигнал (для error-routing/мостов): лист либо массив целиком
9115
+ * (ручка значения массива — см. {@link ModelArraySignals}). Для пути группы — `undefined`.
9116
+ */
7695
9117
  signalAt(path: string): PathAwareSignal<unknown> | undefined;
7696
9118
  }
7697
9119
  ```
@@ -7796,8 +9218,8 @@ export class ModelArrayNode<T extends object> extends FormNode<T[]> { /* … */
7796
9218
  const model = createModel<{ rows: { name: string; qty: number }[] }>({ rows: [] });
7797
9219
 
7798
9220
  const rowItem = (item: FormModel<{ name: string; qty: number }>) => ({
7799
- name: { value: item.$.name, component: InputField },
7800
- qty: { value: item.$.qty, component: InputField },
9221
+ name: { value: item.$.name, component: Input },
9222
+ qty: { value: item.$.qty, component: Input },
7801
9223
  });
7802
9224
 
7803
9225
  const form = createForm({
@@ -7820,12 +9242,23 @@ _Source: src/form/nodes/model-array-node.ts_
7820
9242
  **Kind:** `type`
7821
9243
 
7822
9244
  Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
7823
- и {@link ReadonlySignal} значения массива целиком (реагирует и на правку элемента, и на изменение
7824
- состава — push/removeAt/move).
9245
+ и сигнал значения массива целиком (реагирует и на правку элемента, и на изменение состава —
9246
+ push/removeAt/move).
9247
+
9248
+ В отличие от группы, узел ЗАПИСЫВАЕМ: `model.$.tags.value = ['a']` заменяет массив целиком. Это
9249
+ делает его {@link PathAwareSignal} — к нему привязывают поле (`{ value: model.$.tags, component }`),
9250
+ правило (`validate(model.$.tags, …)`) и поведение, как к листу. Так массив служит ОДНИМ значением
9251
+ поля: мультивыбор, теги, список файлов.
9252
+
9253
+ `V` — объявленный тип поля, включая `null`/`undefined`: поле `string[] | null` с начальным `null`
9254
+ в рантайме — лист с тем же `.value`/`peek`/`subscribe`, поэтому общая часть честна для обоих
9255
+ случаев. `length` и индексы есть только у настоящего узла-массива (начальное значение — массив).
7825
9256
 
7826
9257
  **Signature:**
7827
9258
  ```typescript
7828
- export type ModelArraySignals<U, V> = ContainerSignal<V, 'length'> & {
9259
+ export type ModelArraySignals<U, V> = Signal<V> & {
9260
+ /** Путь массива в модели (dot-нотация). */
9261
+ readonly __path: string;
7829
9262
  readonly length: number;
7830
9263
  readonly [index: number]: ModelSignalNode<U>;
7831
9264
  };
@@ -7908,6 +9341,34 @@ export type ModelValue<V> =
7908
9341
 
7909
9342
  _Source: src/model/types.ts_
7910
9343
 
9344
+ ### msg
9345
+
9346
+ **Kind:** `function`
9347
+
9348
+ Создаёт описатель сообщения.
9349
+
9350
+ **Signature:**
9351
+ ```typescript
9352
+ export function msg(
9353
+ key: string,
9354
+ values?: Readonly<Record<string, unknown>>,
9355
+ defaultMessage?: string
9356
+ ): MessageDescriptor
9357
+ ```
9358
+
9359
+ **Parameters:**
9360
+ - `key` — - Ключ в словаре приложения.
9361
+ - `values` — - Значения подстановок.
9362
+ - `defaultMessage` — - Английский текст на случай, если ключа в локали нет.
9363
+
9364
+ **Examples:**
9365
+
9366
+ ```ts
9367
+ { value: model.$.email, component: Input, componentProps: { label: msg('profile.email.label') } }
9368
+ ```
9369
+
9370
+ _Source: src/i18n/descriptor.ts_
9371
+
7911
9372
  ### multipleOf
7912
9373
 
7913
9374
  **Kind:** `function`
@@ -7980,25 +9441,25 @@ export class NodeFactory {
7980
9441
  * // FieldNode
7981
9442
  * const field = factory.createNode({
7982
9443
  * value: 'test@mail.com',
7983
- * component: InputField,
9444
+ * component: Input,
7984
9445
  * validators: [required, email]
7985
9446
  * });
7986
9447
  *
7987
9448
  * // GroupNode
7988
9449
  * const group = factory.createNode({
7989
- * email: { value: '', component: InputField },
7990
- * password: { value: '', component: InputField }
9450
+ * email: { value: '', component: Input },
9451
+ * password: { value: '', component: Input }
7991
9452
  * });
7992
9453
  *
7993
9454
  * // ArrayNode (объект)
7994
9455
  * const array = factory.createNode({
7995
- * schema: { title: { value: '', component: InputField } },
9456
+ * schema: { title: { value: '', component: Input } },
7996
9457
  * initialItems: [{ title: 'Item 1' }]
7997
9458
  * });
7998
9459
  *
7999
9460
  * // ArrayNode (массив) - новый формат
8000
9461
  * const array2 = factory.createNode([
8001
- * { title: { value: '', component: InputField } }, // schema
9462
+ * { title: { value: '', component: Input } }, // schema
8002
9463
  * { title: 'Item 1' }, // initial item 1
8003
9464
  * { title: 'Item 2' } // initial item 2
8004
9465
  * ]);
@@ -8012,7 +9473,7 @@ export class NodeFactory {
8012
9473
  import { NodeFactory } from '@reformer/core';
8013
9474
 
8014
9475
  const factory = new NodeFactory();
8015
- const node = factory.createNode({ value: '', component: InputField }); // → FieldNode<string>
9476
+ const node = factory.createNode({ value: '', component: Input }); // → FieldNode<string>
8016
9477
  ```
8017
9478
 
8018
9479
  _Source: src/form/factories/node-factory.ts_
@@ -8124,6 +9585,31 @@ export function onDispose(cleanup: BehaviorCleanup): void
8124
9585
 
8125
9586
  _Source: src/form/behaviors/context.ts_
8126
9587
 
9588
+ ### parseMessage
9589
+
9590
+ **Kind:** `function`
9591
+
9592
+ Разбирает сообщение в дерево. Бросает {@link MessageSyntaxError} на ошибке синтаксиса и на
9593
+ `plural`/`select` без ветки `other`.
9594
+
9595
+ **Signature:**
9596
+ ```typescript
9597
+ export function parseMessage(source: string): MessagePattern
9598
+ ```
9599
+
9600
+ **Parameters:**
9601
+ - `source` — - Текст сообщения из словаря.
9602
+
9603
+ **Returns:** Разобранное сообщение для {@link formatPattern}.
9604
+
9605
+ **Examples:**
9606
+
9607
+ ```ts
9608
+ parseMessage('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}');
9609
+ ```
9610
+
9611
+ _Source: src/i18n/message-format.ts_
9612
+
8127
9613
  ### pastDate
8128
9614
 
8129
9615
  **Kind:** `function`
@@ -8388,6 +9874,60 @@ resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue:
8388
9874
 
8389
9875
  _Source: src/model/behaviors-value.ts_
8390
9876
 
9877
+ ### resolveLocalized
9878
+
9879
+ **Kind:** `function`
9880
+
9881
+ Заменяет описатели сообщений на строки по активной локали. Обходит массивы и простые
9882
+ объекты-литералы — подписи опций, заголовки шагов; сигналы, React-элементы, функции и
9883
+ экземпляры классов (узлы формы) возвращает как есть. Поддерево без описателей сохраняет ссылку:
9884
+ если раскрывать было нечего, возвращается тот же объект.
9885
+
9886
+ **Signature:**
9887
+ ```typescript
9888
+ export function resolveLocalized<T>(value: T, i18n: I18nHandle): T
9889
+ ```
9890
+
9891
+ **Parameters:**
9892
+ - `value` — - Пропсы, массив или одиночное значение.
9893
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
9894
+
9895
+ **Examples:**
9896
+
9897
+ ```ts
9898
+ resolveLocalized({ label: msg('profile.email.label'), options: [{ value: 'a', label: msg('opt.a') }] }, i18n);
9899
+ // { label: 'Почта', options: [{ value: 'a', label: 'Вариант А' }] }
9900
+ ```
9901
+
9902
+ _Source: src/i18n/resolve-localized.ts_
9903
+
9904
+ ### resolveValidationError
9905
+
9906
+ **Kind:** `function`
9907
+
9908
+ Отображаемый текст ошибки валидации. Порядок источников — в шапке модуля.
9909
+
9910
+ **Signature:**
9911
+ ```typescript
9912
+ export function resolveValidationError(error: ValidationError, i18n: I18nHandle): string
9913
+ ```
9914
+
9915
+ **Parameters:**
9916
+ - `error` — - Ошибка от правила: `code`, `message`, `messageKey`, `params`.
9917
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
9918
+
9919
+ **Examples:**
9920
+
9921
+ ```ts
9922
+ resolveValidationError({ code: 'minLength', message: '', params: { minLength: 8 } }, i18n);
9923
+ // 'Enter at least 8 characters' · в русской локали — 'Не меньше 8 символов'
9924
+
9925
+ resolveValidationError({ code: 'required', message: 'Укажите телефон' }, i18n);
9926
+ // 'Укажите телефон' — явное сообщение автора важнее словаря
9927
+ ```
9928
+
9929
+ _Source: src/i18n/validation-message.ts_
9930
+
8391
9931
  ### revalidateWhen
8392
9932
 
8393
9933
  **Kind:** `function`
@@ -8752,6 +10292,32 @@ transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
8752
10292
 
8753
10293
  _Source: src/model/behaviors-value.ts_
8754
10294
 
10295
+ ### translateBuiltin
10296
+
10297
+ **Kind:** `function`
10298
+
10299
+ Перевод ключа пакета: словарь локали, затем встроенная английская таблица пакета, затем сам
10300
+ ключ. Английская таблица форматируется по английским правилам — её множественные формы и
10301
+ группировка чисел не зависят от активного языка.
10302
+
10303
+ **Signature:**
10304
+ ```typescript
10305
+ export function translateBuiltin(
10306
+ locale: FormLocale,
10307
+ builtin: Messages,
10308
+ key: string,
10309
+ values?: MessageValues
10310
+ ): string
10311
+ ```
10312
+
10313
+ **Parameters:**
10314
+ - `locale` — - Активная локаль.
10315
+ - `builtin` — - Встроенная английская таблица пакета-владельца ключа.
10316
+ - `key` — - Ключ сообщения.
10317
+ - `values` — - Значения подстановок.
10318
+
10319
+ _Source: src/i18n/translator.ts_
10320
+
8755
10321
  ### UnknownRecord
8756
10322
 
8757
10323
  **Kind:** `type`
@@ -8901,6 +10467,33 @@ function ArrayRenderer({ arrayNode }) {
8901
10467
 
8902
10468
  _Source: src/platforms/react/hooks/useArrayLength.ts_
8903
10469
 
10470
+ ### useFieldHandleRef
10471
+
10472
+ **Kind:** `function`
10473
+
10474
+ Ref для контрола поля + императивный handle для потребителя.
10475
+
10476
+ Возвращённый ref вешается на контрол. На `outerRef` публикуется:
10477
+ - handle самого контрола, если тот его реализует (`useImperativeHandle` композита);
10478
+ - иначе — базовый {@link FieldHandle}, построенный из DOM-узла контрола.
10479
+
10480
+ Хук вызывается безусловно; вешать ref на контрол стоит только при заданном `outerRef` —
10481
+ так function-компонент без ref-поддержки не получает лишний ref.
10482
+
10483
+ **Signature:**
10484
+ ```typescript
10485
+ export function useFieldHandleRef<H = FieldHandle>(
10486
+ outerRef: Ref<H> | undefined
10487
+ ): RefObject<unknown>
10488
+ ```
10489
+
10490
+ **Parameters:**
10491
+ - `outerRef` — - ref потребителя (из `forwardRef` или `schema.node(sel).getRef()`).
10492
+
10493
+ **Returns:** ref, который нужно передать контролу.
10494
+
10495
+ _Source: src/platforms/react/field-binding/field-handle.ts_
10496
+
8904
10497
  ### useFormBundle
8905
10498
 
8906
10499
  **Kind:** `function`
@@ -8968,6 +10561,7 @@ export function useFormControl(
8968
10561
  Текстовое поле с валидацией
8969
10562
  ```tsx
8970
10563
  import { useFormControl } from '@reformer/core';
10564
+ import { useValidationMessage } from '@reformer/core/i18n';
8971
10565
  import type { FieldNode } from '@reformer/core';
8972
10566
 
8973
10567
  interface TextFieldProps {
@@ -8983,6 +10577,8 @@ shouldShowError,
8983
10577
  errors,
8984
10578
  pending
8985
10579
  } = useFormControl(control);
10580
+ // Текст ошибки даёт резолвер: у правил без своего сообщения `message` пуст.
10581
+ const message = useValidationMessage();
8986
10582
 
8987
10583
  return (
8988
10584
  <div className="field">
@@ -9002,7 +10598,7 @@ return (
9002
10598
 
9003
10599
  {shouldShowError && errors[0] && (
9004
10600
  <span className="error" role="alert">
9005
- {errors[0].message}
10601
+ {message(errors[0])}
9006
10602
  </span>
9007
10603
  )}
9008
10604
  </div>
@@ -9050,6 +10646,7 @@ control: FieldNode<string>;
9050
10646
 
9051
10647
  function Select({ control }: SelectProps) {
9052
10648
  const { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);
10649
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9053
10650
  const options = componentProps.options as Array<{ value: string; label: string }>;
9054
10651
 
9055
10652
  return (
@@ -9067,7 +10664,7 @@ onBlur={() => control.markAsTouched()}
9067
10664
  </option>
9068
10665
  ))}
9069
10666
  </select>
9070
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
10667
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9071
10668
  </div>
9072
10669
  );
9073
10670
  }
@@ -9305,6 +10902,7 @@ dangerouslySetInnerHTML={{ __html: html }}
9305
10902
  // Основной редактор использует useFormControl для полного состояния
9306
10903
  function MarkdownEditor({ control }: MarkdownEditorProps) {
9307
10904
  const { value, shouldShowError, errors } = useFormControl(control);
10905
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9308
10906
 
9309
10907
  return (
9310
10908
  <div className="editor-container">
@@ -9312,7 +10910,7 @@ return (
9312
10910
  value={value}
9313
10911
  onChange={e => control.setValue(e.target.value)}
9314
10912
  />
9315
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
10913
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9316
10914
 
9317
10915
  {/* Preview обновляется только при изменении value *}
9318
10916
  <MarkdownPreview control={control} />
@@ -9416,12 +11014,104 @@ export interface UseFormValidationResult {
9416
11014
 
9417
11015
  _Source: src/platforms/react/hooks/use-form-validation.ts_
9418
11016
 
11017
+ ### useI18n
11018
+
11019
+ **Kind:** `function`
11020
+
11021
+ Активная локаль: перевод ключей приложения, форматирование чисел, дат и размеров, состояние
11022
+ загрузки языка. Без провайдера — английский с пустым словарём.
11023
+
11024
+ **Signature:**
11025
+ ```typescript
11026
+ export function useI18n(): I18nContextValue
11027
+ ```
11028
+
11029
+ **Examples:**
11030
+
11031
+ ```tsx
11032
+ function LangBadge() {
11033
+ const { code, pending, t } = useI18n();
11034
+ return <span aria-busy={pending}>{t('app.language')}: {code}</span>;
11035
+ }
11036
+ ```
11037
+
11038
+ _Source: src/platforms/react/i18n/hooks.ts_
11039
+
11040
+ ### useMessages
11041
+
11042
+ **Kind:** `function`
11043
+
11044
+ Переводчик пакета: ищет ключ в локали провайдера, а при промахе берёт встроенную английскую
11045
+ таблицу этого пакета. Так пакет говорит по-английски без провайдера и не зависит от того, знает
11046
+ ли локаль его ключи. Тип ключа выводится из таблицы — опечатка в ключе не скомпилируется.
11047
+
11048
+ `defaults` должен быть стабильной ссылкой (импорт JSON-модуля), иначе переводчик будет
11049
+ пересоздаваться на каждый рендер.
11050
+
11051
+ **Signature:**
11052
+ ```typescript
11053
+ export function useMessages<D extends Messages>(
11054
+ defaults: D
11055
+ ): (key: keyof D & string, values?: MessageValues) => string
11056
+ ```
11057
+
11058
+ **Parameters:**
11059
+ - `defaults` — - Встроенная английская таблица пакета.
11060
+
11061
+ **Returns:** Функция `(ключ, значения?) => строка`; её идентичность меняется вместе с локалью.
11062
+
11063
+ **Examples:**
11064
+
11065
+ ```tsx
11066
+ import en from './locale/en.json';
11067
+
11068
+ const useKitMessages = () => useMessages(en);
11069
+
11070
+ function ClearButton() {
11071
+ const t = useKitMessages();
11072
+ return <button aria-label={t('kit.select.clear')} />;
11073
+ }
11074
+ ```
11075
+
11076
+ _Source: src/platforms/react/i18n/hooks.ts_
11077
+
11078
+ ### useValidationMessage
11079
+
11080
+ **Kind:** `function`
11081
+
11082
+ Резолвер текста ошибки валидации по активной локали: `messageKey` автора → явное `message` →
11083
+ словарь локали по коду → встроенный английский → код. Идентичность функции меняется вместе с
11084
+ локалью, поэтому показанные ошибки переводятся при смене языка без повторной валидации.
11085
+
11086
+ `FormField` из `@reformer/cdk` использует его сам; вызывайте напрямую, если рисуете ошибки
11087
+ своим компонентом.
11088
+
11089
+ **Signature:**
11090
+ ```typescript
11091
+ export function useValidationMessage(): (error: ValidationError) => string
11092
+ ```
11093
+
11094
+ **Examples:**
11095
+
11096
+ ```tsx
11097
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
11098
+ const message = useValidationMessage();
11099
+ const { errors } = useFormControl(control);
11100
+ return <>{errors.map((error) => <span key={error.code}>{message(error)}</span>)}</>;
11101
+ }
11102
+ ```
11103
+
11104
+ _Source: src/platforms/react/i18n/hooks.ts_
11105
+
9419
11106
  ### validate
9420
11107
 
9421
11108
  **Kind:** `function`
9422
11109
 
9423
11110
  Синхронные правила поля.
9424
11111
 
11112
+ Поле — лист либо массив целиком: `model.$.<массив>` — такая же ручка значения, и правило
11113
+ получает массив (мультивыбор, теги, список файлов).
11114
+
9425
11115
  **Signature:**
9426
11116
  ```typescript
9427
11117
  export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void
@@ -9431,6 +11121,7 @@ export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TFiel
9431
11121
 
9432
11122
  ```ts
9433
11123
  validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
11124
+ validate(model.$.tags, [required(), maxLength(3)]); // tags: string[] — массив как значение
9434
11125
  ```
9435
11126
 
9436
11127
  _Source: src/form/validation/operators.ts_
@@ -9461,6 +11152,34 @@ validateAsync(model.$.username, [async (v, { signal }) => {
9461
11152
 
9462
11153
  _Source: src/form/validation/operators.ts_
9463
11154
 
11155
+ ### validateLocale
11156
+
11157
+ **Kind:** `function`
11158
+
11159
+ Проверяет словарь: каждое сообщение разбирается, а при заданном образце — набор ключей и имена
11160
+ подстановок совпадают с ним. Нужна там, где компилятор не поможет: словарь пришёл по сети или
11161
+ написан вне репозитория. В рантайме вызывать не обязательно — битое или пропущенное сообщение
11162
+ и так откатится на встроенный английский; проверка нужна тестам и CI.
11163
+
11164
+ **Signature:**
11165
+ ```typescript
11166
+ export function validateLocale(messages: Messages, reference?: Messages): LocaleIssue[]
11167
+ ```
11168
+
11169
+ **Parameters:**
11170
+ - `messages` — - Проверяемый словарь.
11171
+ - `reference` — - Образец, обычно английский словарь того же пакета.
11172
+
11173
+ **Returns:** Список расхождений; пустой — словарь в порядке.
11174
+
11175
+ **Examples:**
11176
+
11177
+ ```ts
11178
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
11179
+ ```
11180
+
11181
+ _Source: src/i18n/locale.ts_
11182
+
9464
11183
  ### validateModel
9465
11184
 
9466
11185
  **Kind:** `function`
@@ -9517,8 +11236,20 @@ _Source: src/form/validation/run.ts_
9517
11236
  ```typescript
9518
11237
  export interface ValidateOptions {
9519
11238
  /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
9520
- * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
11239
+ * берётся из словаря локали по `code` (`validation.<code>`; см. `resolveValidationError` в
11240
+ * `@reformer/core/i18n`). Явное сообщение важнее словаря, но не переключается вместе с языком —
11241
+ * для локализуемого текста используйте {@link ValidateOptions.messageKey}. */
9521
11242
  message?: string;
11243
+ /**
11244
+ * Ключ сообщения в словаре приложения — локализуемый текст автора правила. Если ключ есть в
11245
+ * активной локали, он важнее и `message`, и текста по `code`; если нет — в ход идёт `message`.
11246
+ *
11247
+ * @example
11248
+ * ```ts
11249
+ * required({ messageKey: 'profile.name.required', message: 'Name is required' });
11250
+ * ```
11251
+ */
11252
+ messageKey?: string;
9522
11253
  /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
9523
11254
  params?: Record<string, FormValue>;
9524
11255
  }
@@ -9550,7 +11281,13 @@ _Source: src/form/validation/operators.ts_
9550
11281
  ```typescript
9551
11282
  export interface ValidationError {
9552
11283
  code: string;
11284
+ /**
11285
+ * Готовый текст ошибки. Пустая строка — «автор правила текста не задавал»: отображаемую строку
11286
+ * тогда даёт резолвер по `code` (словарь локали `validation.<code>`).
11287
+ */
9553
11288
  message: string;
11289
+ /** Ключ сообщения в словаре приложения; при наличии в активной локали важнее `message`. */
11290
+ messageKey?: string;
9554
11291
  params?: Record<string, FormValue>;
9555
11292
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
9556
11293
  severity?: 'error' | 'warning';
@@ -9670,3 +11407,16 @@ watchField(model.$.country, async (country) => {
9670
11407
  ```
9671
11408
 
9672
11409
  _Source: src/model/behaviors-value.ts_
11410
+
11411
+ ### WeekStart
11412
+
11413
+ **Kind:** `type`
11414
+
11415
+ День недели, с которого начинается календарь: `0` — воскресенье, `1` — понедельник.
11416
+
11417
+ **Signature:**
11418
+ ```typescript
11419
+ export type WeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
11420
+ ```
11421
+
11422
+ _Source: src/i18n/locale.ts_