@reformer/core 11.0.0-beta.6 → 11.0.0-beta.8

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 (113) hide show
  1. package/README.md +7 -3
  2. package/dist/behaviors.js +216 -179
  3. package/dist/create-model-BBRHhHwK.js +30 -0
  4. package/dist/en-CjAqn7h0.js +41 -0
  5. package/dist/form/behaviors/collections.d.ts +14 -1
  6. package/dist/form/behaviors/context.d.ts +18 -2
  7. package/dist/form/behaviors/index.d.ts +3 -1
  8. package/dist/form/behaviors/internals.d.ts +9 -1
  9. package/dist/form/behaviors/schema-operators.d.ts +76 -0
  10. package/dist/form/behaviors/types.d.ts +15 -3
  11. package/dist/form/create-core-form.d.ts +5 -3
  12. package/dist/form/create-form.d.ts +28 -53
  13. package/dist/form/form-bundle.d.ts +136 -0
  14. package/dist/form/index.d.ts +6 -1
  15. package/dist/form/nodes/field-node.d.ts +2 -2
  16. package/dist/form/nodes/group-node.d.ts +1 -1
  17. package/dist/form/nodes/model-array-node.d.ts +10 -5
  18. package/dist/form/schema-controller.d.ts +137 -0
  19. package/dist/form/schema-subtree.d.ts +28 -0
  20. package/dist/form/signal-node-registry.d.ts +22 -16
  21. package/dist/form/types/contracts.d.ts +6 -0
  22. package/dist/form/types/deep-schema.d.ts +2 -2
  23. package/dist/form/types/form-proxy.d.ts +1 -1
  24. package/dist/form/types/schema-node.d.ts +37 -12
  25. package/dist/form/types/validation-schema.d.ts +13 -1
  26. package/dist/form/validation/context.d.ts +6 -1
  27. package/dist/form/validation/index.d.ts +1 -1
  28. package/dist/form/validation/operators.d.ts +74 -7
  29. package/dist/form/validation/types.d.ts +2 -1
  30. package/dist/form/validators/validation-error.d.ts +8 -0
  31. package/dist/i18n/descriptor.d.ts +54 -0
  32. package/dist/i18n/en.json.d.ts +45 -0
  33. package/dist/i18n/loader.d.ts +71 -0
  34. package/dist/i18n/locale-store.d.ts +39 -0
  35. package/dist/i18n/locale.d.ts +90 -0
  36. package/dist/i18n/message-format.d.ts +116 -0
  37. package/dist/i18n/resolve-localized.d.ts +17 -0
  38. package/dist/i18n/ru.json.d.ts +45 -0
  39. package/dist/i18n/translator.d.ts +50 -0
  40. package/dist/i18n/validation-message.d.ts +18 -0
  41. package/dist/i18n.d.ts +31 -0
  42. package/dist/i18n.js +303 -0
  43. package/dist/index-BrNkQj1y.js +105 -0
  44. package/dist/index.js +737 -743
  45. package/dist/loader-Dv_Hl7JC.js +216 -0
  46. package/dist/locale/en.d.ts +2 -0
  47. package/dist/locale/en.js +5 -0
  48. package/dist/locale/en.json +42 -0
  49. package/dist/locale/index.d.ts +18 -0
  50. package/dist/locale/ru.d.ts +2 -0
  51. package/dist/locale/ru.js +41 -0
  52. package/dist/locale/ru.json +42 -0
  53. package/dist/locale.d.ts +2 -0
  54. package/dist/locale.js +9 -0
  55. package/dist/model/create-model.d.ts +17 -3
  56. package/dist/model/index.d.ts +8 -3
  57. package/dist/model/model-nodes.d.ts +67 -4
  58. package/dist/model/model-signals-proxy.d.ts +33 -1
  59. package/dist/model/model-value-proxy.d.ts +76 -0
  60. package/dist/model/types.d.ts +26 -9
  61. package/dist/model-value-proxy-caTmAiUd.js +419 -0
  62. package/dist/model.js +21 -17
  63. package/dist/node-BeV_lV5i.js +100 -0
  64. package/dist/platforms/react/form-bundle-context.d.ts +27 -0
  65. package/dist/platforms/react/hooks/types.d.ts +6 -3
  66. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  67. package/dist/platforms/react/i18n/context.d.ts +13 -0
  68. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  69. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  70. package/dist/platforms/react/index.d.ts +1 -0
  71. package/dist/strategy-DgzELWno.js +204 -0
  72. package/dist/validation-error-B-U3RsV0.js +11 -0
  73. package/dist/validation.js +7 -6
  74. package/dist/validators/email.js +5 -8
  75. package/dist/validators/future-date.js +8 -11
  76. package/dist/validators/integer.js +4 -7
  77. package/dist/validators/is-date.js +5 -8
  78. package/dist/validators/is-number.js +4 -7
  79. package/dist/validators/max-age.js +8 -11
  80. package/dist/validators/max-date.js +8 -11
  81. package/dist/validators/max-length.js +5 -8
  82. package/dist/validators/max.js +4 -7
  83. package/dist/validators/min-age.js +8 -11
  84. package/dist/validators/min-date.js +8 -11
  85. package/dist/validators/min-length.js +5 -8
  86. package/dist/validators/min.js +4 -7
  87. package/dist/validators/multiple-of.js +5 -8
  88. package/dist/validators/non-negative.js +4 -7
  89. package/dist/validators/non-zero.js +4 -7
  90. package/dist/validators/past-date.js +8 -11
  91. package/dist/validators/pattern.js +5 -8
  92. package/dist/validators/phone.js +6 -9
  93. package/dist/validators/required.js +4 -7
  94. package/dist/validators/url.js +9 -14
  95. package/dist/validators.js +84 -106
  96. package/docs/llms/03-api-signatures.md +10 -4
  97. package/docs/llms/09-formschema.md +5 -5
  98. package/docs/llms/10-arrays.md +52 -3
  99. package/docs/llms/16-ui-components.md +2 -2
  100. package/docs/llms/18-conditional-fields.md +1 -1
  101. package/docs/llms/21-array-operations.md +26 -0
  102. package/docs/llms/28-submit-and-reset.md +3 -3
  103. package/docs/llms/29-async-preload.md +4 -4
  104. package/docs/llms/33-validation-strategy.md +2 -2
  105. package/docs/llms/34-i18n.md +272 -0
  106. package/docs/llms/35-unified-contract.md +246 -0
  107. package/llms-index.json +1 -1
  108. package/llms.txt +2746 -229
  109. package/package.json +22 -3
  110. package/dist/create-model-Dxh9cfLp.js +0 -97
  111. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
  112. package/dist/node-DGEFMTQu.js +0 -18
  113. package/dist/strategy-DA3JwxsR.js +0 -170
package/llms.txt CHANGED
@@ -38,6 +38,8 @@
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 — локаль, загрузка словарей и провайдер
42
+ - 35-unified-contract.md — Единый контракт формы — одна схема, одна сборка, одно поведение
41
43
  - API Reference (auto-generated from JSDoc)
42
44
 
43
45
  ## 1. 1. API Reference
@@ -274,7 +276,7 @@ createModel<T extends object>(initial: T): FormModel<T>
274
276
  // model.arrayField → ModelArray<Item> (push/removeAt/insertAt/move/swap/clear/at/map/length)
275
277
 
276
278
  // Форма (ноды поверх сигналов модели)
277
- createForm<T>({ model, schema, behavior? }): FormProxy<T>
279
+ createFormFromModel<T>({ model, schema, behavior? }): FormProxy<T>
278
280
  // form.<field> → FieldNode / GroupNode / FormArrayProxy
279
281
  // form.<field>.setValue(v) / .value.value / .errors.value / .disabled.value
280
282
  // form.<field>.enable() / .disable() / .reset() / .markAsTouched() / .setErrors([...])
@@ -320,11 +322,17 @@ minAge(years: number, options?: { message?: string })
320
322
  maxAge(years: number, options?: { message?: string })
321
323
  ```
322
324
 
325
+ Кроме `message`, каждое правило принимает `messageKey` (ключ словаря приложения) и `params`.
326
+ Без `message` правило кладёт в ошибку пустую строку, а текст при отображении берётся из словаря
327
+ локали по коду (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без
328
+ провайдера. Явное `message` важнее словаря, но языком не переключается; для локализуемого текста —
329
+ `messageKey`. Подробно — [34-i18n.md](./34-i18n.md).
330
+
323
331
  Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
324
332
  `ValidationSchema` и гоняются раннером `validateModel` по требованию.
325
333
 
326
334
  ```typescript
327
- import { createModel, createForm } from '@reformer/core';
335
+ import { createModel, createForm, createFormFromModel } from '@reformer/core';
328
336
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
329
337
  import { required, min, max, email } from '@reformer/core/validators';
330
338
 
@@ -332,7 +340,7 @@ type Loan = { email: string; age: number | null; amount: number | null };
332
340
  const model = createModel<Loan>({ email: '', age: null, amount: null });
333
341
 
334
342
  // layout-схема createForm НЕ несёт validators — только привязка поля к сигналу + компонент
335
- const form = createForm({
343
+ const form = createFormFromModel({
336
344
  model,
337
345
  schema: {
338
346
  email: { value: model.$.email, component: Input },
@@ -519,7 +527,7 @@ const behavior = defineFormBehavior<MyForm>(({ model, form }) => {
519
527
  });
520
528
  });
521
529
 
522
- const form = createForm({ model, schema, behavior });
530
+ const form = createFormFromModel({ model, schema, behavior });
523
531
  ```
524
532
 
525
533
  DSL-операторы: `compute` (auto-tracking, без явного списка источников), `computeFrom`, `copyFrom`,
@@ -1105,7 +1113,7 @@ layout-схеме НЕТ — правила живут в отдельной с
1105
1113
  ### Primitive Fields
1106
1114
 
1107
1115
  ```typescript
1108
- import { createModel, createForm } from '@reformer/core';
1116
+ import { createModel, createFormFromModel } from '@reformer/core';
1109
1117
  import { defineValidationSchema, validate } from '@reformer/core/validation';
1110
1118
  import { required } from '@reformer/core/validators';
1111
1119
  import { Input, InputNumber, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
@@ -1146,7 +1154,7 @@ const schema = {
1146
1154
  },
1147
1155
  };
1148
1156
 
1149
- const form = createForm<MyForm>({ model, schema });
1157
+ const form = createFormFromModel<MyForm>({ model, schema });
1150
1158
  ```
1151
1159
 
1152
1160
  ### Nested Objects
@@ -1191,7 +1199,7 @@ const schema = {
1191
1199
 
1192
1200
  ```typescript
1193
1201
  // M1: данные из модели + схема (+ опциональный декларативный behavior)
1194
- const form = createForm<MyForm>({
1202
+ const form = createFormFromModel<MyForm>({
1195
1203
  model, // FormModel<MyForm> — обязателен
1196
1204
  schema, // дерево узлов, привязанных к сигналам
1197
1205
  behavior: myBehavior, // опционально: defineFormBehavior(...) из @reformer/core/behaviors
@@ -1206,12 +1214,12 @@ model.items.push({ id: '1', name: 'Item' }); // операции над масс
1206
1214
  > **Тип `schema` в M1 — `unknown`, не `FormSchema<T>`.** Это осознанно: интерпретатор обходит
1207
1215
  > произвольную структуру и собирает листья `{ value: signal, component?, ... }`, поэтому обёртки
1208
1216
  > (`{ children: [...] }`, wizard/steps) компилируются. Строгий `FormSchema<T>` (keyed-map по полям
1209
- > `T`) применяется только на legacy-пути `createForm(schema)` без модели.
1217
+ > `T`) применяется только на legacy-пути `createLegacyForm(schema)` без модели.
1210
1218
 
1211
1219
  ### createForm Returns a Proxy
1212
1220
 
1213
1221
  ```typescript
1214
- const form = createForm<MyForm>({ model, schema });
1222
+ const form = createFormFromModel<MyForm>({ model, schema });
1215
1223
 
1216
1224
  form.email; // FieldNode<string> — TypeScript знает тип
1217
1225
  form.address.city; // FieldNode<string> — вложенный доступ
@@ -1232,7 +1240,7 @@ if (isFieldNode(node)) { /* ... */ }
1232
1240
  элемента из его под-модели (`FormModel<Item>`).
1233
1241
 
1234
1242
  ```typescript
1235
- import { createModel, createForm, type FormModel } from '@reformer/core';
1243
+ import { createModel, createFormFromModel, type FormModel } from '@reformer/core';
1236
1244
  import { Input, InputNumber } from '@reformer/ui-kit';
1237
1245
 
1238
1246
  type Item = { id: string; name: string; price: number };
@@ -1251,13 +1259,62 @@ const schema = {
1251
1259
  items: { array: model.items, item: itemSchema },
1252
1260
  };
1253
1261
 
1254
- const form = createForm<MyForm>({ model, schema });
1262
+ const form = createFormFromModel<MyForm>({ model, schema });
1255
1263
  ```
1256
1264
 
1257
1265
  > **Type constraint:** тип элемента `Item` объявляй через `type`-alias (не `interface`) — иначе
1258
1266
  > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
1259
1267
  > См. `30-type-safety-recipes.md`.
1260
1268
 
1269
+ ### Массив как ОДНО значение поля
1270
+
1271
+ Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
1272
+ является, решает **схема**, а не данные: `{ array, item }` — набор под-форм, а узел поля
1273
+ `{ value: model.$.<путь>, component }` — одно значение. Массив, к которому в схеме ничего не
1274
+ привязано, в форму не попадает.
1275
+
1276
+ ```typescript
1277
+ type MyForm = { tags: string[] };
1278
+
1279
+ const model = createModel<MyForm>({ tags: [] });
1280
+
1281
+ const schema = {
1282
+ tags: { value: model.$.tags, component: SelectMulti }, // поле над массивом целиком
1283
+ };
1284
+ const form = createFormFromModel<MyForm>({ model, schema });
1285
+
1286
+ form.tags.setValue(['a', 'b']); // FieldNode<string[]>
1287
+ model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
1288
+ validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
1289
+ ```
1290
+
1291
+ - `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `value:` в
1292
+ схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
1293
+ - Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
1294
+ выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
1295
+ лист, и привязывается он так же.
1296
+ - Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
1297
+ `model.patch(...)`.
1298
+
1299
+ ### Массив в группе и в строке другого массива
1300
+
1301
+ Узел `{ array, item }` стоит на любой глубине: массив в группе получает ноду `form.<группа>.<массив>`,
1302
+ массив в строке — `form.<массив>.at(i).<массив>`. Под-схема строки объявляет свои массивы так же.
1303
+
1304
+ ```typescript
1305
+ const contactItem = (contact: FormModel<Contact>) => ({
1306
+ name: { value: contact.$.name, component: Input },
1307
+ phones: { array: contact.phones, item: phoneItem }, // массив в строке массива
1308
+ });
1309
+
1310
+ const schema = {
1311
+ hotlines: { array: model.details.hotlines, item: phoneItem }, // массив в группе
1312
+ contacts: { array: model.contacts, item: contactItem },
1313
+ };
1314
+ ```
1315
+
1316
+ Поведение строк вложенного массива — `applyEach` внутри схемы строки, см. `21-array-operations.md`.
1317
+
1261
1318
  ### Один массив — три слоя (три разных движка)
1262
1319
 
1263
1320
  Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
@@ -1269,7 +1326,7 @@ const form = createForm<MyForm>({ model, schema });
1269
1326
  `model.$.properties`.
1270
1327
 
1271
1328
  ```typescript
1272
- // узел схемы для createForm({ model, schema })
1329
+ // узел схемы для createFormFromModel({ model, schema })
1273
1330
  properties: { array: model.properties, item: propertyItem },
1274
1331
  ```
1275
1332
 
@@ -1829,7 +1886,7 @@ the guide below.
1829
1886
 
1830
1887
  ```tsx
1831
1888
  import { useMemo } from 'react';
1832
- import { createModel, createForm } from '@reformer/core';
1889
+ import { createModel, createFormFromModel } from '@reformer/core';
1833
1890
  import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
1834
1891
 
1835
1892
  type RegistrationForm = {
@@ -1864,7 +1921,7 @@ function RegistrationPage() {
1864
1921
  componentProps: { label: 'I agree to terms' },
1865
1922
  },
1866
1923
  };
1867
- return createForm<RegistrationForm>({ model, schema });
1924
+ return createFormFromModel<RegistrationForm>({ model, schema });
1868
1925
  }, []);
1869
1926
 
1870
1927
  return (
@@ -2193,7 +2250,7 @@ export const creditBehavior = defineFormBehavior<CreditForm>(({ model }) => {
2193
2250
  - **Все поля должны быть материализованы в модели** (`createModel`), даже те, что показываются
2194
2251
  только под условием. `enableWhen`/`compute`/`copyFrom` принимают сигналы `model.$.field`, а не
2195
2252
  строковые пути; сигнала для несуществующего поля нет.
2196
- - Поведение подключается к форме через `createForm({ model, schema, behavior })` — форма владеет
2253
+ - Поведение подключается к форме через `createFormFromModel({ model, schema, behavior })` — форма владеет
2197
2254
  жизненным циклом, DSL-операторы регистрируют свой cleanup сами (ручной массив cleanup'ов не нужен).
2198
2255
  - Таргетом может быть и **группа**: `enableWhen(model.$.residenceAddress, () => model.sameAsRegistration === false)`
2199
2256
  проходит по поддереву; без `resetOnDisable` значение группы сохраняется (удобно, когда оно копируется
@@ -2640,6 +2697,32 @@ const behavior = defineFormBehavior<MyForm>(({ model }) => {
2640
2697
  });
2641
2698
  ```
2642
2699
 
2700
+ ### Вложенные области — массив в группе и в строке массива
2701
+
2702
+ Узел `{ array, item }` и операторы `applyEach` / `apply` / `enableWhen` работают на любой глубине:
2703
+ цель оператора — **ручка** `model.$.…`, под-модель и нода находятся по её идентичности, а не по
2704
+ пути.
2705
+
2706
+ ```typescript
2707
+ const phoneBehavior = defineFormBehavior<Phone>(({ model: phone }) => {
2708
+ enableWhen(phone.$.number, () => phone.primary);
2709
+ });
2710
+
2711
+ const contactBehavior = defineFormBehavior<Contact>(({ model: contact }) => {
2712
+ applyEach(contact.$.phones, phoneBehavior); // массив внутри строки
2713
+ enableWhen(contact.$.address, () => contact.quantity > 0); // группа внутри строки
2714
+ apply(contact.$.address, addressBehavior); // под-схема получает настоящую под-модель
2715
+ });
2716
+
2717
+ const behavior = defineFormBehavior<MyForm>(({ model }) => {
2718
+ applyEach(model.$.contacts, contactBehavior);
2719
+ });
2720
+ ```
2721
+
2722
+ Ручка → value-фасад: `modelOf(model.$.address)` — под-модель (`get` / `set` / `patch`),
2723
+ `modelOf(model.$.phones)` — фасад массива (`push` / `removeAt` / `at`). Нода формы по ручке —
2724
+ `getNodeForSignal(model.$.address)`.
2725
+
2643
2726
  ### Aggregate write — aggregateInto
2644
2727
 
2645
2728
  Агрегатная запись в строки (например, «последняя строка = 100 − Σ остальных»):
@@ -3456,7 +3539,7 @@ form.<field>.clearErrors();
3456
3539
 
3457
3540
  ```tsx
3458
3541
  import { useMemo } from 'react';
3459
- import { createModel, createForm } from '@reformer/core';
3542
+ import { createModel, createForm, createFormFromModel } from '@reformer/core';
3460
3543
  import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
3461
3544
  import { required, email, minLength } from '@reformer/core/validators';
3462
3545
 
@@ -3474,7 +3557,7 @@ function RegistrationForm() {
3474
3557
  const { model, form } = useMemo(() => {
3475
3558
  const m = createModel<RegistrationFormData>({ username: '', email: '', password: '' });
3476
3559
  // schema здесь — layout-дерево нод (компоненты/раскладка), без валидаторов.
3477
- return { model: m, form: createForm({ model: m, schema: buildLayout(m) }) };
3560
+ return { model: m, form: createFormFromModel({ model: m, schema: buildLayout(m) }) };
3478
3561
  }, []);
3479
3562
 
3480
3563
  const handleSubmit = async (e: React.FormEvent) => {
@@ -3606,7 +3689,7 @@ const handleSubmit = async (e) => {
3606
3689
 
3607
3690
  ```typescript
3608
3691
  // ❌ Валидаторы в layout-дереве, отдаваемом в createForm — layout не несёт правил
3609
- createForm({ model, schema: { username: { value: '', validators: [required()] } } });
3692
+ createFormFromModel({ model, schema: { username: { value: '', validators: [required()] } } });
3610
3693
 
3611
3694
  // ✅ Валидация — отдельная схема, прогоняется раннером
3612
3695
  const schema = defineValidationSchema<T>(({ model }) => {
@@ -3698,7 +3781,7 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
3698
3781
  ### Initial values в модели
3699
3782
 
3700
3783
  ```typescript
3701
- import { createModel, createForm } from '@reformer/core';
3784
+ import { createModel, createFormFromModel } from '@reformer/core';
3702
3785
  import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
3703
3786
 
3704
3787
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
@@ -3719,7 +3802,7 @@ const schema = {
3719
3802
  },
3720
3803
  marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
3721
3804
  };
3722
- const form = createForm({ model, schema });
3805
+ const form = createFormFromModel({ model, schema });
3723
3806
  // model.get() === { username: '', language: 'ru', marketing: true }
3724
3807
  // после правок: model.reset() возвращает к этим значениям
3725
3808
  ```
@@ -3806,14 +3889,14 @@ export const addressBehavior = defineFormBehavior<AddressForm>(({ model, form })
3806
3889
  // ❌ model/form пересоздаются на каждый рендер → preload запускается каждый раз
3807
3890
  function MyForm() {
3808
3891
  const model = createModel<T>(initial); // КАЖДЫЙ рендер!
3809
- const form = createForm({ model, schema });
3892
+ const form = createFormFromModel({ model, schema });
3810
3893
  }
3811
3894
 
3812
3895
  // ✅ Стабильные ссылки через useMemo
3813
3896
  function MyForm() {
3814
3897
  const { model, form } = useMemo(() => {
3815
3898
  const m = createModel<T>(initial);
3816
- return { model: m, form: createForm({ model: m, schema: buildSchema(m) }) };
3899
+ return { model: m, form: createFormFromModel({ model: m, schema: buildSchema(m) }) };
3817
3900
  }, []);
3818
3901
  }
3819
3902
  ```
@@ -4345,7 +4428,7 @@ function createFormValidation<T>(
4345
4428
  `markAsTouched` не нужен.
4346
4429
 
4347
4430
  ```tsx
4348
- import { createModel, createForm, useFormValidation } from '@reformer/core';
4431
+ import { createModel, createFormFromModel, useFormValidation } from '@reformer/core';
4349
4432
  import { defineValidationSchema, validate } from '@reformer/core/validation';
4350
4433
  import { required, email, minLength } from '@reformer/core/validators';
4351
4434
 
@@ -4360,7 +4443,7 @@ const registrationValidation = defineValidationSchema<RegistrationData>(({ model
4360
4443
 
4361
4444
  function RegistrationForm() {
4362
4445
  const model = useMemo(() => createModel<RegistrationData>({ username: '', email: '', password: '' }), []);
4363
- const form = useMemo(() => createForm({ model }), [model]);
4446
+ const form = useMemo(() => createFormFromModel({ model }), [model]);
4364
4447
 
4365
4448
  const { submit, isValidating } = useFormValidation({
4366
4449
  model,
@@ -4465,7 +4548,527 @@ if (typeof window !== 'undefined') ctrl.start();
4465
4548
  - [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
4466
4549
  - [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)
4467
4550
 
4468
- ## 82. API Reference
4551
+ ## 82. Purpose
4552
+
4553
+ **i18n — локаль, загрузка словарей и провайдер**
4554
+
4555
+ Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*`: объект
4556
+ локали, загрузка словарей, React-провайдер, перевод и форматирование. Язык задаётся провайдером,
4557
+ подгружается динамически и переключается на лету; узлы формы при этом не пересоздаются.
4558
+
4559
+ Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
4560
+
4561
+ > Импорт всегда из `@reformer/core/i18n`. Из главной бочки `@reformer/core` этот модуль не
4562
+ > реэкспортируется: React-контекст локализации обязан существовать в одном экземпляре.
4563
+
4564
+ ## 83. Локаль — это данные
4565
+
4566
+ ```typescript
4567
+ interface FormLocale {
4568
+ code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
4569
+ messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
4570
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
4571
+ dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
4572
+ dateFormat?: string; // необязательно: токены date-fns для DatePicker
4573
+ }
4574
+ ```
4575
+
4576
+ Функций в локали нет, поэтому её можно положить JSON-файлом на сервер и получить по сети:
4577
+
4578
+ ```json
4579
+ { "code": "ru", "weekStartsOn": 1, "messages": { "profile.title": "Профиль" } }
4580
+ ```
4581
+
4582
+ `extendLocale(base, patch)` возвращает новую локаль: словари сливаются (ключи из `patch` важнее),
4583
+ остальные поля заменяются.
4584
+
4585
+ ## 84. Формат сообщений — подмножество ICU
4586
+
4587
+ | Конструкция | Пример |
4588
+ | ------------------- | ------------------------------------------------------------------------- |
4589
+ | подстановка | `Открыт файл {name}` |
4590
+ | множественные формы | `{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}` |
4591
+ | точное значение | `{count, plural, =0{файлов нет} other{# файлов}}` |
4592
+ | выбор | `{gender, select, male{он} female{она} other{оно}}` |
4593
+ | экранирование | `'{'` → `{`, `'}'` → `}`, `''` → `'` |
4594
+
4595
+ - Ветка `other` обязательна. `#` внутри `plural` — число, отформатированное по языку.
4596
+ - Типы `number`, `date`, `time`, `selectordinal` не поддерживаются: числа и даты форматируйте
4597
+ отдельно (`useI18n().number`, `.date`) и передавайте готовой строкой.
4598
+ - Категории (`one`/`few`/`many`) берутся из `Intl.PluralRules` языка локали.
4599
+
4600
+ ## 85. Загрузка локалей
4601
+
4602
+ Откуда брать словарь, решает приложение: источник — функция `(code) => локаль | словарь`.
4603
+
4604
+ ```typescript
4605
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
4606
+ import { loadCoreLocale } from '@reformer/core/locale';
4607
+
4608
+ export const loadLocale = createLocaleLoader([
4609
+ loadCoreLocale, // встроенные словари ядра — чанк нужного языка
4610
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
4611
+ ]);
4612
+ ```
4613
+
4614
+ - Источники опрашиваются параллельно, сливаются по порядку: поздний перекрывает ранний.
4615
+ - Источник отдаёт локаль целиком (`{ code, messages, … }`) или голый словарь (`{ "a.b": "…" }`);
4616
+ `null` означает «для этого языка у меня ничего нет» и ошибкой не считается.
4617
+ - Результат кэшируется по коду языка, идущий запрос не дублируется.
4618
+ - Отказ любого источника — отказ загрузки. Источник, которому можно промахнуться, оборачивайте:
4619
+ `(code) => source(code).catch(() => null)`.
4620
+ - `loadLocale.peek(code)` — уже загруженная локаль или `undefined`; `loadLocale.preload(code)` — то
4621
+ же, что вызов загрузчика.
4622
+
4623
+ Ядро отдаёт свои локали в трёх видах:
4624
+
4625
+ | Вид | Импорт | Для чего |
4626
+ | ----------------- | ------------------------------------------- | ----------------------------------------------- |
4627
+ | загрузчик | `loadCoreLocale` из `@reformer/core/locale` | основной путь: чанк языка по запросу |
4628
+ | JSON-файл | `@reformer/core/locale/ru.json` | выложить на свой сервер, править без пересборки |
4629
+ | синхронный модуль | `ru` из `@reformer/core/locale/ru` | SSR и тесты |
4630
+
4631
+ Встроенные языки ядра — `en` и `ru`. Словарь ядра несёт тексты ошибок валидации
4632
+ (`validation.<code>`) и единицы размера файла (`format.fileSize.b|kb|mb|gb|tb`).
4633
+
4634
+ Пакеты выше отдают локаль накопительно и в тех же трёх видах, поэтому источник встроенных словарей
4635
+ в загрузчике всегда один — самого верхнего из используемых пакетов:
4636
+
4637
+ | Пакеты приложения | Источник | Что в локали |
4638
+ | ------------------------------- | -------------------------------------------- | --------------------------------- |
4639
+ | только ядро | `loadCoreLocale` из `@reformer/core/locale` | ошибки валидации, размер файла |
4640
+ | ядро + `@reformer/cdk` | `loadCdkLocale` из `@reformer/cdk/locale` | то же плюс строки cdk (`cdk.*`) |
4641
+ | ядро + cdk + `@reformer/ui-kit` | `loadKitLocale` из `@reformer/ui-kit/locale` | то же плюс подписи кита (`kit.*`) |
4642
+
4643
+ ## 86. Провайдер
4644
+
4645
+ ```tsx
4646
+ import { I18nProvider } from '@reformer/core/i18n';
4647
+
4648
+ function App() {
4649
+ const [lang, setLang] = useState('ru');
4650
+ return (
4651
+ <I18nProvider lang={lang} load={loadLocale}>
4652
+ <LangSwitch value={lang} onChange={setLang} />
4653
+ <CreditForm />
4654
+ </I18nProvider>
4655
+ );
4656
+ }
4657
+ ```
4658
+
4659
+ | Проп | Назначение |
4660
+ | --------------- | ------------------------------------------------------------------------ |
4661
+ | `lang` + `load` | провайдер грузит локаль сам |
4662
+ | `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
4663
+ | `onError` | отказ загрузки `(error, lang)` |
4664
+ | `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
4665
+
4666
+ Поведение при `lang` + `load`:
4667
+
4668
+ - при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
4669
+ `useI18n().pending` — для индикатора;
4670
+ - при быстром переключении применяется только последний запрос;
4671
+ - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
4672
+ - если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
4673
+ синхронный и `fallback` не показывается.
4674
+
4675
+ SSR: эффекты на сервере не выполняются, поэтому там передавайте готовый объект —
4676
+ `<I18nProvider locale={ru}>` — либо заранее вызовите `preload`.
4677
+
4678
+ Вложенный провайдер заменяет локаль для своего поддерева целиком.
4679
+
4680
+ ## 87. Хуки
4681
+
4682
+ ```typescript
4683
+ const { code, locale, pending, error, t, has, number, date, fileSize } = useI18n();
4684
+
4685
+ t('profile.title'); // ключ приложения
4686
+ t('cart.items', { count: 3 }); // с подстановками
4687
+ t('cart.empty', undefined, 'Your cart is empty'); // английский текст на случай промаха
4688
+ number(1234.5); // '1 234,5' в ru
4689
+ date(new Date(), { dateStyle: 'long' }); // '15 января 2024 г.' в ru
4690
+ fileSize(1536); // '1,5 КБ' в ru, '1.5 KB' без провайдера
4691
+ ```
4692
+
4693
+ `useMessages(builtinEn)` — для авторов пакетов и китов: ищет ключ в локали провайдера, а при
4694
+ промахе берёт встроенную английскую таблицу пакета. Тип ключа выводится из таблицы.
4695
+
4696
+ ```tsx
4697
+ import en from './locale/en.json';
4698
+
4699
+ function ClearButton() {
4700
+ const t = useMessages(en);
4701
+ return <button aria-label={t('kit.select.clear')} />;
4702
+ }
4703
+ ```
4704
+
4705
+ ## 88. Тексты ошибок валидации
4706
+
4707
+ Правила из `@reformer/core/validators` несут только `code` и `params`; готового текста у них нет
4708
+ (`message` по умолчанию — пустая строка). Строку даёт резолвер при отображении ошибки:
4709
+
4710
+ 1. `messageKey` автора правила — если такой ключ есть в активной локали;
4711
+ 2. явное `message` автора правила;
4712
+ 3. `validation.<code>` из словаря локали;
4713
+ 4. `validation.<code>` из встроенной английской таблицы ядра;
4714
+ 5. сам код — для своих кодов, которых нет ни в одном словаре.
4715
+
4716
+ ```typescript
4717
+ validate(model.$.email, [
4718
+ required(), // «Обязательное поле» в ru, английская фраза без провайдера
4719
+ minLength(5, { message: 'Слишком коротко' }), // явный текст: важнее словаря, но языком не переключается
4720
+ email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
4721
+ ]);
4722
+ ```
4723
+
4724
+ - Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
4725
+ ошибки без повторной валидации.
4726
+ - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит ошибку сам. В своём компоненте —
4727
+ `useValidationMessage()`; вне React — `resolveValidationError(error, createI18n(locale))`.
4728
+ - Свой код ошибки локализуется ключом `validation.<свой_код>` в словаре приложения либо
4729
+ `messageKey` в самом правиле.
4730
+ - Встроенный текст меняется тем же ключом в словаре приложения:
4731
+ `extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
4732
+ - Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
4733
+ размеры в байтах (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) —
4734
+ строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
4735
+
4736
+ ```tsx
4737
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
4738
+ const message = useValidationMessage();
4739
+ const { errors, shouldShowError } = useFormControl(control);
4740
+ return shouldShowError ? <span role="alert">{message(errors[0])}</span> : null;
4741
+ }
4742
+ ```
4743
+
4744
+ Смонтированный `ValidationMessagesProvider` из `@reformer/cdk` заменяет этот резолвер целиком:
4745
+ локаль для ошибок в его поддереве не участвует.
4746
+
4747
+ ## 89. Что показывается, когда ключа нет
4748
+
4749
+ | Ключ | Нет в локали | Сообщение не разбирается |
4750
+ | -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
4751
+ | пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
4752
+ | приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
4753
+
4754
+ Пропущенная подстановка выводится как `{name}`. Форма не падает ни в одном из случаев: неполный
4755
+ словарь, пришедший по сети, — законный сценарий.
4756
+
4757
+ ## 90. Проверка словаря в тестах
4758
+
4759
+ ```typescript
4760
+ import { validateLocale } from '@reformer/core/i18n';
4761
+
4762
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
4763
+ // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
4764
+ ```
4765
+
4766
+ ## 91. Описатели сообщений (низкий уровень)
4767
+
4768
+ `msg(key, values?, defaultMessage?)` создаёт описатель сообщения, `defineMessages(appEn)` — `msg` с
4769
+ типизированными ключами, `resolveLocalized(value, i18n)` раскрывает описатели в объекте пропсов.
4770
+
4771
+ > ⚠️ Рендереры и `FormField` описатели сами пока НЕ раскрывают. Не кладите `msg(...)` в
4772
+ > `componentProps` схемы: компонент получит объект вместо строки. До появления раскрытия в
4773
+ > рендерерах вызывайте `resolveLocalized` вручную или используйте `useI18n().t`.
4774
+
4775
+ ## 92. Anti-patterns
4776
+
4777
+ ```typescript
4778
+ // ❌ Импорт из главной бочки — i18n там нет, а второй путь импорта дал бы вторую копию контекста
4779
+ import { I18nProvider } from '@reformer/core';
4780
+
4781
+ // ✅ Единственный путь импорта — подпуть i18n
4782
+ import { I18nProvider } from '@reformer/core/i18n';
4783
+ ```
4784
+
4785
+ ```tsx
4786
+ // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль грузится заново, первый кадр — fallback
4787
+ <I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
4788
+
4789
+ // ✅ Загрузчик — константа модуля: один кэш на приложение
4790
+ const loadLocale = createLocaleLoader([loadCoreLocale]);
4791
+ <I18nProvider lang={lang} load={loadLocale}>
4792
+ ```
4793
+
4794
+ ```typescript
4795
+ // ❌ Число или дата типами ICU внутри сообщения — не поддерживается, сообщение не разберётся
4796
+ const message = 'Итого: {total, number}';
4797
+
4798
+ // ✅ Отформатировать отдельно и передать готовой строкой
4799
+ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) });
4800
+ ```
4801
+
4802
+ ```tsx
4803
+ // ❌ Ошибка выводится полем message: у правил без своего текста оно пустое — на экране ничего
4804
+ <span>{errors[0].message}</span>;
4805
+
4806
+ // ✅ Текст ошибки — через резолвер: он знает словарь локали, messageKey и явное сообщение
4807
+ const message = useValidationMessage();
4808
+ <span>{message(errors[0])}</span>;
4809
+ ```
4810
+
4811
+ ```typescript
4812
+ // ❌ Настройки локали без словаря: объект без поля messages принимается за словарь сообщений
4813
+ const source = () => ({ weekStartsOn: 1 });
4814
+
4815
+ // ✅ Часть локали узнаётся по полю messages — отдавайте его хотя бы пустым
4816
+ const source = () => ({ weekStartsOn: 1, messages: {} });
4817
+ ```
4818
+
4819
+ ## 93. See also
4820
+
4821
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
4822
+ - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
4823
+
4824
+ ## 94. Purpose
4825
+
4826
+ **Единый контракт формы — одна схема, одна сборка, одно поведение**
4827
+
4828
+ Форма описывается одинаково для всех способов реализации — разметка в JSX, рендерер по TS-схеме,
4829
+ рендерер по JSON. Схема одна (дерево узлов), привязка одна (ручка `model.$.…`), сборка одна
4830
+ (`createForm`), поведение одно (`defineFormBehavior`). Способы отличаются только видом схемы
4831
+ (билдер или документ) и тем, кто рисует результат.
4832
+
4833
+ Прежние записи — ключи `value` / `array`, оператор `each`, `createCoreForm` — пока принимаются.
4834
+ Новый код пишется так, как описано здесь.
4835
+
4836
+ | Слой | поле | массив под-форм | подформа |
4837
+ | --------- | --------------------------------- | ----------------------------------------- | ------------------------------------- |
4838
+ | схема | `{ model: model.$.x, component }` | `{ model: model.$.items, item }` | `{ model: model.$.group, part }` |
4839
+ | валидация | `validate(model.$.x, rules)` | `applyEach(model.$.items, itemRules)` | `apply(model.$.group, groupRules)` |
4840
+ | поведение | `compute(model.$.x, …)` | `applyEach(model.$.items, itemBehavior)` | `apply(model.$.group, groupBehavior)` |
4841
+
4842
+ Правило привязки: `model.$.…` — привязка (схема, правило, оператор). Без `$` — чтение значения
4843
+ (`model.loanType === 'mortgage'`), мутация (`model.items.push()`) и под-модель как область.
4844
+
4845
+ ## 95. Модель — начальные значения и шаблоны элементов
4846
+
4847
+ ```typescript
4848
+ import { arrayOf, createModel } from '@reformer/core';
4849
+
4850
+ const blankPhone = (): Phone => ({ number: '' });
4851
+ const blankCoBorrower = (): CoBorrower => ({ name: '', phones: arrayOf(blankPhone) });
4852
+
4853
+ export const createCreditModel = () =>
4854
+ createModel<CreditForm>({
4855
+ loanType: 'consumer',
4856
+ registrationAddress: { city: '', street: '' },
4857
+ coBorrowers: arrayOf(blankCoBorrower), // пустой массив + шаблон нового элемента
4858
+ tags: [], // массив как одно значение поля — шаблон не нужен
4859
+ });
4860
+ ```
4861
+
4862
+ `arrayOf(blank, items?)` возвращает обычный массив `U[]` и запоминает фабрику нового элемента.
4863
+ После этого `push()` и `insertAt(i)` без значения кладут элемент по шаблону — в модели, в ноде
4864
+ массива формы и в кнопке «Добавить» секции массива.
4865
+
4866
+ - Шаблон переживает `set` / `patch` / `reset` модели.
4867
+ - Вложенный массив объявляется внутри шаблона элемента; элементы, пришедшие обычными данными
4868
+ (загрузка с сервера), получают его шаблон из шаблона родителя.
4869
+ - У массива без шаблона `push()` без значения бросает ошибку с подсказкой.
4870
+ - Шаблон привязан к самому массиву: до `createModel` его нельзя копировать. `structuredClone`,
4871
+ `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона. Начальные значения с
4872
+ `arrayOf` держат в фабрике, а не в константе, которую клонируют.
4873
+
4874
+ ## 96. Схема — одно дерево
4875
+
4876
+ ```typescript
4877
+ import type { FormModel } from '@reformer/core';
4878
+
4879
+ // подформа: объявлена один раз, получает под-модель
4880
+ const address = (model: FormModel<Address>) => ({
4881
+ component: Box,
4882
+ children: [
4883
+ { model: model.$.city, component: Input, componentProps: { label: 'Город' } },
4884
+ { model: model.$.street, component: Input, componentProps: { label: 'Улица' } },
4885
+ ],
4886
+ });
4887
+
4888
+ // строка массива — такая же функция от под-модели
4889
+ const coBorrower = (model: FormModel<CoBorrower>) => ({
4890
+ selector: 'co-borrower',
4891
+ component: Box,
4892
+ children: [{ model: model.$.name, component: Input, componentProps: { label: 'ФИО' } }],
4893
+ });
4894
+
4895
+ export const creditSchema = (model: FormModel<CreditForm>) => ({
4896
+ component: Box,
4897
+ children: [
4898
+ { model: model.$.loanType, component: SelectAsync, componentProps: { label: 'Тип' } }, // поле
4899
+ { model: model.$.tags, component: SelectMulti }, // массив как одно значение
4900
+ {
4901
+ selector: 'registration',
4902
+ component: Section,
4903
+ componentProps: { title: 'Адрес регистрации' },
4904
+ children: [{ model: model.$.registrationAddress, part: address }], // подформа
4905
+ },
4906
+ { model: model.$.coBorrowers, component: FormArray, item: coBorrower }, // массив под-форм
4907
+ ],
4908
+ });
4909
+ ```
4910
+
4911
+ Чем узел является, решают ручка и соседние ключи:
4912
+
4913
+ | Узел | Как отличить |
4914
+ | --------------- | --------------------------------------------- |
4915
+ | поле | `model` — лист или массив; нет `item`, `part` |
4916
+ | массив под-форм | `model` — массив, `item` — функция |
4917
+ | подформа | `model` — группа, `part` — функция |
4918
+ | контейнер | есть `children`, нет `model` |
4919
+
4920
+ - Узел узнаётся по ЗНАЧЕНИЮ: в записи «имя поля → узел» поле данных может называться `model`,
4921
+ `item`, `part` или `value` — под таким ключом лежит обычный вложенный узел.
4922
+ - Поля подформы принадлежат той же форме: `form.registrationAddress.city`.
4923
+ - Билдер `item` / `part` вызывается один раз на под-модель; сборка и рендерер получают одно и то
4924
+ же поддерево (`schemaSubtree(builder, subModel)`).
4925
+ - `initialValue` узла-массива — запасной шаблон нового элемента для форм, чья модель создаётся из
4926
+ данных без кода. Шаблон модели (`arrayOf`) главнее.
4927
+
4928
+ ## 97. Валидация — подформа и массив подключаются привязкой
4929
+
4930
+ ```typescript
4931
+ import {
4932
+ apply,
4933
+ applyEach,
4934
+ cross,
4935
+ defineValidationSchema,
4936
+ validate,
4937
+ validateWhen,
4938
+ } from '@reformer/core/validation';
4939
+
4940
+ const addressRules = defineValidationSchema<Address>(({ model }) => {
4941
+ validate(model.$.city, [required()]);
4942
+ cross<Address>(model.$.street, (address) =>
4943
+ address.city !== '' && address.street === '' ? streetRequired : null
4944
+ );
4945
+ });
4946
+
4947
+ const coBorrowerRules = defineValidationSchema<CoBorrower>(({ model }) => {
4948
+ validate(model.$.name, [required()]);
4949
+ });
4950
+
4951
+ export const contactsRules = defineValidationSchema<CreditForm>(({ model }) => {
4952
+ apply(model.$.registrationAddress, addressRules);
4953
+ validateWhen(
4954
+ () => !model.sameAsRegistration,
4955
+ () => apply(model.$.residenceAddress, addressRules)
4956
+ );
4957
+ applyEach(model.$.coBorrowers, coBorrowerRules);
4958
+ });
4959
+ ```
4960
+
4961
+ - У схемы, подключённой через `apply(ручка, схема)` / `applyEach`, своя область: `model` — под-модель,
4962
+ `cross` получает её снапшот. Захватывать снапшот в замыкание больше не нужно.
4963
+ - Одна и та же схема подключается и к группе, и к элементам массива.
4964
+ - `apply(model.$.a, rules)` принимает и массив ручек: `apply([model.$.a, model.$.b], rules)`.
4965
+ - `apply(schemaA, schemaB)` — композиция схем над той же моделью — остаётся.
4966
+ - Правило получает `(value, scope, root)`: модель области и корень прогона.
4967
+ - `each(model.items, (item) => …)` — прежняя запись `applyEach`: принимает фасад и ручку, области
4968
+ не создаёт.
4969
+
4970
+ ## 98. Поведение — модель, форма и схема
4971
+
4972
+ ```typescript
4973
+ import {
4974
+ apply,
4975
+ applyEach,
4976
+ defineFormBehavior,
4977
+ enableWhen,
4978
+ hideWhen,
4979
+ onComponentEvent,
4980
+ onMount,
4981
+ } from '@reformer/core/behaviors';
4982
+
4983
+ const addressBehavior = defineFormBehavior<Address>(({ model, schema }) => {
4984
+ hideWhen(schema.node('street'), () => model.city === ''); // узел ищется внутри части
4985
+ });
4986
+
4987
+ export const creditBehavior = defineFormBehavior<CreditForm>(({ model, form, schema }) => {
4988
+ const isMortgage = () => model.loanType === 'mortgage';
4989
+
4990
+ enableWhen(model.$.propertyValue, isMortgage, { resetOnDisable: true });
4991
+ hideWhen(schema.node('mortgage'), () => !isMortgage());
4992
+ onComponentEvent(schema.node('wizard'), 'onSubmit', () => submitApplication(model.get()));
4993
+ onMount(schema.node('data-boundary'), () => void loadApplication(model));
4994
+
4995
+ apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
4996
+ applyEach(model.$.coBorrowers, coBorrowerBehavior);
4997
+ });
4998
+ ```
4999
+
5000
+ Операторы узлов схемы:
5001
+
5002
+ | Оператор | Что записывает |
5003
+ | ----------------------------------------- | ---------------------------------------------------------- |
5004
+ | `hideWhen(node, condition)` | узел скрыт, пока условие истинно |
5005
+ | `onComponentEvent(node, event, handler)` | обработчик пропа-события компонента |
5006
+ | `onMount(node, fn)` / `onUnmount(node, fn)` | хуки монтирования узла |
5007
+ | `onInit(node, fn)` | `fn` вызывается сразу, до первого рендера |
5008
+ | `renderEffect(schema, fn)` | реактивный эффект, пока схема области смонтирована |
5009
+ | `schema.node(sel).setHidden / patchProps` | принудительная видимость и пропсы узла |
5010
+ | `schema.node(sel).getRef()` | ref на компонент узла — объект `{ current }` |
5011
+
5012
+ - **Области изолированы.** `schema.node(selector)` ищет узел только в своей области: корневое
5013
+ поведение — в корневом дереве, поведение подформы и строки массива — в своём поддереве. До узла
5014
+ строки или части корень дотягивается через `applyEach` / `apply`. Два монтирования одной части
5015
+ не конфликтуют.
5016
+ - **Исключение — ref по пути модели**: `schema.node('phones.0.number').getRef()` ищется от корня.
5017
+ - **Операторы узлов только записывают правило — исполняет его рендерер.** При разметке в JSX они
5018
+ ничего не делают: видимость, отправка и загрузка данных остаются в JSX.
5019
+ - Правило на селектор, которого нет в дереве, ничего не делает; в dev — предупреждение.
5020
+
5021
+ ## 99. Сборка — `createForm`
5022
+
5023
+ ```tsx
5024
+ import { createForm, useFormBundle } from '@reformer/core';
5025
+
5026
+ const credit = useFormBundle(() =>
5027
+ createForm<CreditForm>({
5028
+ model: createCreditModel(), // либо initial: { … }
5029
+ schema: creditSchema, // (model) => узел; для JSON — документ + registry
5030
+ behavior: creditBehavior,
5031
+ validation: { steps: { loan: loanRules, contacts: contactsRules }, extras: crossRules },
5032
+ })
5033
+ );
5034
+ // credit = { model, form, validation, render }
5035
+
5036
+ <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />; // рендерер
5037
+ <FormField control={credit.form.loanType} />; // разметка в JSX — та же сборка
5038
+ ```
5039
+
5040
+ - Порядок: модель → `seed` → дерево схемы → ноды формы → поведение → валидация → `setup`. Дерево
5041
+ строится один раз.
5042
+ - `validation` в бандле типизирована по конфигу: правила переданы — поле есть всегда.
5043
+ - `render` — `{ tree, controller, node(selector) }`: готовое дерево и схема-контроллер сборки.
5044
+ - JSON: `createForm({ model, schema: document, registry })` — дерево собирает
5045
+ `registry.resolveSchema`. Ядро документ не разбирает.
5046
+ - Внутри рендерера бандл доступен компонентам через `useFormBundleContext()` — так визард берёт
5047
+ форму и валидацию сам.
5048
+
5049
+ ### Низкоуровневые фабрики
5050
+
5051
+ | Задача | Фабрика |
5052
+ | ---------------------------------------------- | ---------------------------------------- |
5053
+ | форма из модели и готового дерева, без бандла | `createFormFromModel({ model, schema })` |
5054
+ | форма без модели — плоская схема полей | `createLegacyForm(schema)` |
5055
+
5056
+ `createForm` с готовым деревом вместо билдера или без модели бросает ошибку с отсылкой к нужной
5057
+ фабрике.
5058
+
5059
+ ## 100. Частые ошибки
5060
+
5061
+ | Ошибка | Как правильно |
5062
+ | -------------------------------------------------- | ------------------------------------------------------- |
5063
+ | `model: model.address` (под-модель у поля) | `model: model.$.address.city` — привязка всегда с `$` |
5064
+ | `{ model: model.$.address, component }` без `part` | группа полем не бывает — нужен `part` |
5065
+ | `createForm({ model, schema: дерево })` | `schema` — билдер `(model) => узел` |
5066
+ | `hideWhen(schema.node('row-узел'), …)` из корня | внутри `applyEach(model.$.items, itemBehavior)` |
5067
+ | `push({})` для новой строки | `arrayOf(blank)` в модели и `push()` без значения |
5068
+ | `createModel(structuredClone(INITIAL))` с `arrayOf` | фабрика начальных значений — клон теряет шаблон массива |
5069
+ | `cross` в подформе читает корень | `apply(model.$.group, rules)` — область подформы |
5070
+
5071
+ ## 101. API Reference
4469
5072
 
4470
5073
  _Auto-generated from JSDoc on public exports._
4471
5074
 
@@ -4515,14 +5118,22 @@ _Source: src/form/types/index.ts_
4515
5118
 
4516
5119
  **Kind:** `function`
4517
5120
 
4518
- Композиция под-схем в текущую (над той же моделью scope). Заменяет пошаговую группировку.
5121
+ Применить под-схему к одному или нескольким полям-группам (переиспользование).
5122
+
5123
+ Под-схема получает scope группы: `model` — настоящая под-модель (`model.$.field`, чтение и
5124
+ запись значений, `get`/`set`/`patch`), `form` — нода группы. Цель — ручка группы
5125
+ (`model.$.address`); работает и в корневой схеме, и в схеме строки массива.
4519
5126
 
4520
5127
  **Signature:**
4521
5128
  ```typescript
4522
- export function apply<T>(...schemas: ValidationSchema<T>[]): void
5129
+ export function apply<TField>(targets: object | object[], subSchema: FormBehavior<TField>): void
4523
5130
  ```
4524
5131
 
4525
- _Source: src/form/validation/operators.ts_
5132
+ **Examples:**
5133
+
5134
+ apply([model.$.registrationAddress, model.$.residenceAddress], addressBehavior);
5135
+
5136
+ _Source: src/form/behaviors/collections.ts_
4526
5137
 
4527
5138
  ### applyEach
4528
5139
 
@@ -4538,6 +5149,9 @@ _Source: src/form/validation/operators.ts_
4538
5149
  рендерится, а её сигналы зарегистрированы (`enableWhen` резолвит ноду). Без материализации доступ
4539
5150
  к `form.*` бросит понятную ошибку (см. {@link unmaterializedRowForm}).
4540
5151
 
5152
+ Работает на любой глубине: массив в корне, в группе и в строке другого массива — в схеме строки
5153
+ можно снова вызвать `applyEach`.
5154
+
4541
5155
  **Signature:**
4542
5156
  ```typescript
4543
5157
  export function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>): void
@@ -4548,6 +5162,7 @@ export function applyEach<TItem>(array: object, itemSchema: FormBehavior<TItem>)
4548
5162
  applyEach(model.$.items, defineFormBehavior<Item>(({ model: row, form }) => {
4549
5163
  compute(row.$.lineTotal, () => row.qty * row.price); // value-op — всегда
4550
5164
  enableWhen(row.$.discount, () => row.qty > 10); // node-op — нужна материализация массива
5165
+ applyEach(row.$.phones, phoneBehavior); // массив внутри строки
4551
5166
  }));
4552
5167
 
4553
5168
  _Source: src/form/behaviors/collections.ts_
@@ -4764,6 +5379,52 @@ export interface ArrayNodeLike {
4764
5379
 
4765
5380
  _Source: src/form/types/index.ts_
4766
5381
 
5382
+ ### arrayOf
5383
+
5384
+ **Kind:** `function`
5385
+
5386
+ Массив модели с шаблоном нового элемента.
5387
+
5388
+ Шаблон — фабрика значения, которое получает `push()` / `insertAt(i)` без аргумента: кнопка
5389
+ «Добавить» секции массива, `model.items.push()` в поведении. Объявляется там же, где остальные
5390
+ начальные значения, — в модели; схеме и JSX знать его не нужно.
5391
+
5392
+ Возвращает обычный массив: тип поля остаётся `U[]`. Шаблон переживает `set` / `patch` / `reset`
5393
+ модели. Вложенные массивы объявляются внутри шаблона элемента — тогда и элементы, пришедшие
5394
+ обычными данными (загрузка с сервера), получают их шаблоны.
5395
+
5396
+ Шаблон привязан к самому массиву, поэтому до `createModel` его нельзя копировать:
5397
+ `structuredClone`, `JSON.parse(JSON.stringify(…))` и `[...array]` отдают массив без шаблона, и
5398
+ `push()` без значения бросит ошибку. Начальные значения с `arrayOf` держат в фабрике
5399
+ (`const initial = () => ({ items: arrayOf(blank) })`), а не в константе, которую клонируют.
5400
+
5401
+ **Signature:**
5402
+ ```typescript
5403
+ export function arrayOf<U>(blank: () => U, items: readonly U[] = []): U[]
5404
+ ```
5405
+
5406
+ **Parameters:**
5407
+ - `blank` — - Фабрика нового элемента. Вызывается на каждое добавление — значение не делится
5408
+ между элементами.
5409
+ - `items` — - Начальные элементы. По умолчанию массив пуст.
5410
+
5411
+ **Returns:** Массив начальных значений с шаблоном.
5412
+
5413
+ **Examples:**
5414
+
5415
+ ```typescript
5416
+ const blankPhone = () => ({ number: '' });
5417
+ const blankCoBorrower = () => ({ name: '', phones: arrayOf(blankPhone) });
5418
+
5419
+ const model = createModel({ coBorrowers: arrayOf(blankCoBorrower) });
5420
+
5421
+ model.coBorrowers.push(); // { name: '', phones: [] }
5422
+ model.coBorrowers.at(0).phones.push(); // { number: '' }
5423
+ model.coBorrowers.push({ name: 'Анна', phones: [] }); // значение целиком — как раньше
5424
+ ```
5425
+
5426
+ _Source: src/model/model-nodes.ts_
5427
+
4767
5428
  ### AsyncRule
4768
5429
 
4769
5430
  **Kind:** `type`
@@ -4857,13 +5518,21 @@ _Source: src/model/behaviors-value.ts_
4857
5518
 
4858
5519
  **Kind:** `type`
4859
5520
 
4860
- Контекст схемы поведения: модель (значения/сигналы) + форма (ноды).
5521
+ Область схемы поведения: модель (значения и сигналы), форма (ноды) и схема (узлы разметки).
5522
+
5523
+ `schema` адресует узлы СВОЕЙ области: в корневом поведении — корневое дерево, в поведении
5524
+ подформы (`apply`) и строки массива (`applyEach`) — поддерево этой части. Правила узлов
5525
+ (`hideWhen`, `onComponentEvent`, `onMount`) исполняет рендерер; без него они ничего не делают.
4861
5526
 
4862
5527
  **Signature:**
4863
5528
  ```typescript
4864
- export type BehaviorScope<T> = { model: FormModel<T>; form: FormProxy<T> };
4865
- ```
4866
-
5529
+ export type BehaviorScope<T> = {
5530
+ model: FormModel<T>;
5531
+ form: FormProxy<T>;
5532
+ schema: SchemaScope;
5533
+ };
5534
+ ```
5535
+
4867
5536
  _Source: src/form/behaviors/types.ts_
4868
5537
 
4869
5538
  ### bindFieldProps
@@ -4932,6 +5601,35 @@ await v!.validateAll(); // все шаги + extras
4932
5601
 
4933
5602
  _Source: src/form/validation/config.ts_
4934
5603
 
5604
+ ### bundledLocaleSource
5605
+
5606
+ **Kind:** `function`
5607
+
5608
+ Источник встроенных локалей пакета: по чанку на язык. Язык ищется точно, затем по основной части
5609
+ кода (`ru-RU` → `ru`). Языка нет — источник отдаёт `null`: подписи пакета останутся английскими,
5610
+ пока приложение не даст их своим источником.
5611
+
5612
+ **Signature:**
5613
+ ```typescript
5614
+ export function bundledLocaleSource(
5615
+ loaders: Readonly<Record<string, () => Promise<FormLocale>>>
5616
+ ): LocaleSource
5617
+ ```
5618
+
5619
+ **Parameters:**
5620
+ - `loaders` — - Код языка → ленивый импорт его локали.
5621
+
5622
+ **Examples:**
5623
+
5624
+ ```ts
5625
+ export const loadKitLocale = bundledLocaleSource({
5626
+ en: () => import('./en').then((m) => m.en),
5627
+ ru: () => import('./ru').then((m) => m.ru),
5628
+ });
5629
+ ```
5630
+
5631
+ _Source: src/i18n/loader.ts_
5632
+
4935
5633
  ### ChangeContext
4936
5634
 
4937
5635
  **Kind:** `interface`
@@ -5049,6 +5747,19 @@ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail }
5049
5747
 
5050
5748
  _Source: src/model/behaviors-value.ts_
5051
5749
 
5750
+ ### CORE_LOCALES
5751
+
5752
+ **Kind:** `const`
5753
+
5754
+ Языки, которые ядро поставляет само.
5755
+
5756
+ **Signature:**
5757
+ ```typescript
5758
+ export const CORE_LOCALES: readonly string[]
5759
+ ```
5760
+
5761
+ _Source: src/locale/index.ts_
5762
+
5052
5763
  ### CORE_RUNTIME_TOKEN
5053
5764
 
5054
5765
  **Kind:** `const`
@@ -5131,6 +5842,8 @@ const credit = useFormBundle(() =>
5131
5842
  return <FormWizard form={credit.form} config={credit.validation} steps={STEPS} />;
5132
5843
  ```
5133
5844
 
5845
+ **Deprecated:** Пишите `createForm` — та же сборка, в бандле вдобавок дерево для рендерера.
5846
+
5134
5847
  _Source: src/form/create-core-form.ts_
5135
5848
 
5136
5849
  ### CreateCoreFormConfig
@@ -5143,8 +5856,8 @@ _Source: src/form/create-core-form.ts_
5143
5856
  ```typescript
5144
5857
  export interface CreateCoreFormConfig<T> extends CreateFormConfigBase<T, CoreForm<T>> {
5145
5858
  /**
5146
- * Билдер схемы формы. Именно функция, а не готовое дерево: листья схемы держат сами сигналы
5147
- * модели (`value: model.$.email`), поэтому построить дерево до модели нечем.
5859
+ * Билдер схемы формы. Именно функция, а не готовое дерево: узлы схемы держат ручки модели
5860
+ * (`model: model.$.email`), поэтому построить дерево до модели нечем.
5148
5861
  */
5149
5862
  schema?: (model: FormModel<T>) => FormSchemaNode;
5150
5863
  }
@@ -5156,53 +5869,89 @@ _Source: src/form/create-core-form.ts_
5156
5869
 
5157
5870
  **Kind:** `function`
5158
5871
 
5159
- Создать форму из {@link FormModel} + единой схемы (архитектура M1, рекомендуемый путь).
5872
+ Собрать модель, форму, валидацию и дерево для рендера за один проход.
5160
5873
 
5161
5874
  **Signature:**
5162
5875
  ```typescript
5163
- export function createForm<T>(args: CreateFormFromModelArgs<T>): FormProxy<T>;
5876
+ export function createForm<T extends object>(
5877
+ config: CreateFormConfig<T> & { validation: FormValidation<T> | ValidationSchema<T> }
5878
+ ): ValidatedFormBundle<T>;
5164
5879
  ```
5165
5880
 
5166
5881
  **Parameters:**
5167
- - `args` — - Модель данных, layout-схема (component/componentProps) и (опционально) поведение
5882
+ - `config` — - {@link CreateFormConfig}: (`initial` | `model`) + опц. `schema`, `registry`,
5883
+ `behavior`, `validation`, `seed`, `setup`.
5168
5884
 
5169
- **Returns:** Типизированная форма с Proxy-доступом к полям
5885
+ **Returns:**
5170
5886
 
5171
5887
  **Examples:**
5172
5888
 
5173
- Архитектура M1: `createModel` + layout-схема + `createForm({ model, schema })`
5174
- ```typescript
5175
- import { createModel, createForm } from '@reformer/core';
5176
- import { defineValidationSchema, validate, validateModel } from '@reformer/core/validation';
5177
- import { required, email, minLength } from '@reformer/core/validators';
5889
+ Рендерер по TS-схеме
5890
+ ```tsx
5891
+ const credit = useFormBundle(() =>
5892
+ createForm<CreditForm>({
5893
+ model: createCreditModel(),
5894
+ schema: creditSchema, // (model) => узел
5895
+ behavior: creditBehavior,
5896
+ validation: { steps: { loan: loanRules }, extras: crossRules },
5897
+ })
5898
+ );
5899
+ return <FormRenderer form={credit} settings={{ fieldWrapper: FormField }} />;
5900
+ ```
5178
5901
 
5179
- interface UserForm {
5180
- email: string;
5181
- password: string;
5182
- }
5902
+ React-руками — та же сборка, разметка в JSX
5903
+ ```tsx
5904
+ const credit = useFormBundle(() => createForm<CreditForm>({ initial, schema: creditSchema }));
5905
+ return <FormField control={credit.form.loanType} />;
5906
+ ```
5907
+
5908
+ JSON — документ схемы и реестр
5909
+ ```tsx
5910
+ const credit = useFormBundle(() =>
5911
+ createForm<CreditForm>({ model: createCreditModel(), schema: creditJson, registry })
5912
+ );
5913
+ return <FormRenderer form={credit} />;
5914
+ ```
5183
5915
 
5184
- const model = createModel<UserForm>({ email: '', password: '' });
5916
+ _Source: src/form/form-bundle.ts_
5185
5917
 
5186
- // Layout-схема НЕ несёт validators — только component/componentProps.
5187
- const schema = {
5188
- children: [
5189
- { value: model.$.email, component: Input },
5190
- { value: model.$.password, component: Input },
5191
- ],
5192
- };
5918
+ ### CreateFormConfig
5193
5919
 
5194
- const form = createForm<UserForm>({ model, schema });
5195
- form.email.setValue('test@mail.com');
5920
+ **Kind:** `interface`
5196
5921
 
5197
- // Валидация — ОТДЕЛЬНАЯ схема, прогон внешним раннером (ошибки сами доезжают до нод):
5198
- const validation = defineValidationSchema<UserForm>(({ model }) => {
5199
- validate(model.$.email, [required(), email()]);
5200
- validate(model.$.password, [required(), minLength(8)]);
5201
- });
5202
- const ok = await validateModel(model, validation);
5922
+ Конфиг {@link createForm}.
5923
+
5924
+ **Signature:**
5925
+ ```typescript
5926
+ export interface CreateFormConfig<T> {
5927
+ /** Начальные значения — из них создаётся модель. Взаимоисключимо с `model`. */
5928
+ initial?: T;
5929
+ /** Готовая модель (если создана отдельной фабрикой). Приоритетнее `initial`. */
5930
+ model?: FormModel<T>;
5931
+ /**
5932
+ * Схема формы: билдер дерева `(model) => узел` либо документ (JSON) — тогда нужен `registry`.
5933
+ * Билдер, а не готовое дерево: узлы держат ручки модели (`model: model.$.email`), и построить
5934
+ * дерево до модели нечем.
5935
+ */
5936
+ schema?: ((model: FormModel<T>) => FormSchemaNode) | object;
5937
+ /** Реестр JSON-варианта: превращает документ схемы в дерево. */
5938
+ registry?: SchemaResolver;
5939
+ /** Поведение формы: значения, состояние нод и правила узлов схемы. */
5940
+ behavior?: FormBehavior<T>;
5941
+ /** Правила валидации: готовая схема либо {@link FormValidation} со стратегией и шагами. */
5942
+ validation?: FormValidation<T> | ValidationSchema<T>;
5943
+ /**
5944
+ * Правка модели ДО сборки формы. Нужна там, где значение обязано существовать к моменту
5945
+ * построения нод — например, массив, наполняемый из вычислений (реактивные `onChange` на
5946
+ * инициализации не срабатывают).
5947
+ */
5948
+ seed?: (model: FormModel<T>) => void;
5949
+ /** Донастройка ПОСЛЕ сборки: получает готовый бандл. */
5950
+ setup?: (bundle: FormBundle<T>) => void;
5951
+ }
5203
5952
  ```
5204
5953
 
5205
- _Source: src/form/create-form.ts_
5954
+ _Source: src/form/form-bundle.ts_
5206
5955
 
5207
5956
  ### CreateFormConfigBase
5208
5957
 
@@ -5229,7 +5978,7 @@ export interface CreateFormConfigBase<T, B> {
5229
5978
  seed?: (model: FormModel<T>) => void;
5230
5979
  /**
5231
5980
  * Донастройка ПОСЛЕ сборки. Единственное место для правок, которые обязаны идти следом за
5232
- * `createForm`: поле, чьё значение уже массив, ноды не получает (см. `buildModelConfig`),
5981
+ * сборкой формы: поле, чьё значение уже массив, ноды не получает (см. `buildModelConfig`),
5233
5982
  * поэтому такой префилл выполняется здесь.
5234
5983
  */
5235
5984
  setup?: (bundle: B) => void;
@@ -5246,12 +5995,14 @@ _Source: src/form/create-core-form.ts_
5246
5995
 
5247
5996
  Значения принадлежат модели (источник истины), ноды формы держат UI/валидационное состояние и
5248
5997
  ссылаются на сигналы модели по идентичности (`node.value === model.$.path`). Обходит структуру
5249
- модели, привязывает конфиг поля (component/componentProps) из схемы, материализует
5250
- top-level массивы как {@link ModelArrayNode}, заполняет реестр сигнал→нода (для `enableWhen`/
5251
- роутинга ошибок) и, при наличии, запускает декларативное поведение (cleanup живёт на форме).
5998
+ модели, привязывает конфиг поля (component/componentProps) из схемы, материализует массивы
5999
+ под-форм как {@link ModelArrayNode} (на любой глубине), заполняет реестр ручка→нода (для
6000
+ `enableWhen`, `apply`/`applyEach` и роутинга ошибок) и, при наличии, запускает декларативное
6001
+ поведение (cleanup живёт на форме).
5252
6002
 
5253
- Обычно вызывается неявно через {@link createForm} с аргументом `{ model, schema }` — прямой вызов
5254
- нужен редко (например, для построения формы элемента массива).
6003
+ Обычно вызывается сборкой одним вызовом (`createForm`): она строит дерево схемы, зовёт эту
6004
+ фабрику и собирает валидацию. Прямой вызов нужен там, где бандл не нужен, — форма элемента
6005
+ массива, тесты, свой слой сборки.
5255
6006
 
5256
6007
  **Signature:**
5257
6008
  ```typescript
@@ -5265,7 +6016,7 @@ export function createFormFromModel<T>(args: CreateFormFromModelArgs<T>): FormPr
5265
6016
 
5266
6017
  **Examples:**
5267
6018
 
5268
- Форма из модели + схемы (эквивалент `createForm({ model, schema })`)
6019
+ Форма из модели + схемы (эквивалент `createFormFromModel({ model, schema })`)
5269
6020
  ```typescript
5270
6021
  import { createModel, createFormFromModel } from '@reformer/core';
5271
6022
 
@@ -5293,7 +6044,7 @@ console.log(model.email); // 'user@mail.com'
5293
6044
  ```
5294
6045
 
5295
6046
  **See also:**
5296
- - {@link createForm} - основная фабрика (диспетчеризует сюда при аргументе `{ model, schema }`)
6047
+ - `createForm` — сборка одним вызовом: модель, форма, валидация и дерево для рендера
5297
6048
 
5298
6049
  _Source: src/form/create-form.ts_
5299
6050
 
@@ -5319,6 +6070,12 @@ export interface CreateFormFromModelArgs<T> {
5319
6070
  * заполнения реестра сигнал→нода; cleanup живёт на форме и вызывается в `form.dispose()`.
5320
6071
  */
5321
6072
  behavior?: FormBehavior<T>;
6073
+ /**
6074
+ * Схема-контроллер сборки: через него поведение получает `schema` своей области. Его передаёт
6075
+ * сборка одним вызовом, чтобы правила узлов (`hideWhen`, `onComponentEvent`) дошли до рендерера.
6076
+ * Без него поведение пишет правила в собственный контроллер, который никто не читает.
6077
+ */
6078
+ schemaController?: SchemaController;
5322
6079
  }
5323
6080
  ```
5324
6081
 
@@ -5359,6 +6116,153 @@ dispose();
5359
6116
 
5360
6117
  _Source: src/form/validation/strategy.ts_
5361
6118
 
6119
+ ### createI18n
6120
+
6121
+ **Kind:** `function`
6122
+
6123
+ Ручка для локали. Для одного и того же объекта локали возвращается один и тот же объект —
6124
+ ручку можно держать в зависимостях хуков.
6125
+
6126
+ **Signature:**
6127
+ ```typescript
6128
+ export function createI18n(locale: FormLocale): I18nHandle
6129
+ ```
6130
+
6131
+ **Parameters:**
6132
+ - `locale` — - Локаль; см. {@link FormLocale}.
6133
+
6134
+ **Examples:**
6135
+
6136
+ ```ts
6137
+ const i18n = createI18n({ code: 'ru', messages: { 'cart.items': '{count, plural, one{# товар} few{# товара} other{# товаров}}' } });
6138
+ i18n.t('cart.items', { count: 3 }); // '3 товара'
6139
+ i18n.fileSize(1536); // '1,5 КБ'
6140
+ ```
6141
+
6142
+ _Source: src/i18n/translator.ts_
6143
+
6144
+ ### createLegacyForm
6145
+
6146
+ **Kind:** `function`
6147
+
6148
+ Создать форму из плоской схемы полей с инлайн-значениями (`value: ''`) — путь ДО архитектуры M1.
6149
+
6150
+ Значения живут в нодах, модели нет: поведение (`defineFormBehavior`) и внешняя валидация
6151
+ (`validateModel`) к такой форме не подключаются. Для нового кода — {@link createFormFromModel}
6152
+ либо сборка одним вызовом.
6153
+
6154
+ **Signature:**
6155
+ ```typescript
6156
+ export function createLegacyForm<T>(schema: FormSchema<T>): FormProxy<T>;
6157
+ ```
6158
+
6159
+ **Parameters:**
6160
+ - `schema` — - Схема полей формы
6161
+
6162
+ **Returns:** Типизированная форма с Proxy-доступом к полям
6163
+
6164
+ **Examples:**
6165
+
6166
+ ```typescript
6167
+ const form = createLegacyForm<UserForm>({
6168
+ email: { value: '', component: Input },
6169
+ password: { value: '', component: Input },
6170
+ });
6171
+ ```
6172
+
6173
+ _Source: src/form/create-form.ts_
6174
+
6175
+ ### createLocaleLoader
6176
+
6177
+ **Kind:** `function`
6178
+
6179
+ Собирает загрузчик из источников. Источники опрашиваются параллельно, а сливаются по порядку:
6180
+ поздний перекрывает ранний. Результат кэшируется по коду языка, идущий запрос не дублируется.
6181
+ Отказ любого источника — отказ загрузки; из кэша такой запрос удаляется, и следующая попытка
6182
+ пойдёт заново. Источник, которому можно промахнуться, приложение оборачивает само:
6183
+ `(code) => source(code).catch(() => null)`.
6184
+
6185
+ Код языка у результата — всегда запрошенный: по нему работает `Intl`, и он не должен зависеть
6186
+ от того, что написано внутри файла.
6187
+
6188
+ **Signature:**
6189
+ ```typescript
6190
+ export function createLocaleLoader(sources: readonly LocaleSource[]): LocaleLoader
6191
+ ```
6192
+
6193
+ **Parameters:**
6194
+ - `sources` — - Источники в порядке возрастания приоритета.
6195
+
6196
+ **Examples:**
6197
+
6198
+ ```ts
6199
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
6200
+ import { loadKitLocale } from '@reformer/ui-kit/locale';
6201
+
6202
+ export const loadLocale = createLocaleLoader([
6203
+ loadKitLocale, // подписи кита — чанк нужного языка
6204
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
6205
+ ]);
6206
+ ```
6207
+
6208
+ _Source: src/i18n/loader.ts_
6209
+
6210
+ ### createLocaleStore
6211
+
6212
+ **Kind:** `function`
6213
+
6214
+ Создаёт хранилище. Загрузка при создании НЕ начинается: начальное состояние считается чисто —
6215
+ из кэша загрузчика, если локаль там уже есть, — а запрос уходит на первый {@link LocaleStore.setLang}.
6216
+ Это позволяет создавать хранилище прямо в рендере.
6217
+
6218
+ **Signature:**
6219
+ ```typescript
6220
+ export function createLocaleStore(
6221
+ load: LocaleLoad,
6222
+ lang: string,
6223
+ options: LocaleStoreOptions = {}
6224
+ ): LocaleStore
6225
+ ```
6226
+
6227
+ **Parameters:**
6228
+ - `load` — - Загрузчик локалей.
6229
+ - `lang` — - Начальный язык.
6230
+ - `options` — - См. {@link LocaleStoreOptions}.
6231
+
6232
+ _Source: src/i18n/locale-store.ts_
6233
+
6234
+ ### createMessageFormatter
6235
+
6236
+ **Kind:** `function`
6237
+
6238
+ Форматтер одной локали с кэшем разбора: сообщение разбирается один раз, на первый вызов.
6239
+ Это внутренность `I18nService.t()` — сам `t()` добавляет сверху поиск ключа в словаре, маркер
6240
+ пропущенного ключа и откат на резервную локаль.
6241
+
6242
+ **Signature:**
6243
+ ```typescript
6244
+ export function createMessageFormatter(
6245
+ locale: string,
6246
+ options: MessageFormatOptions = {}
6247
+ ): (source: string, values?: MessageValues) => string
6248
+ ```
6249
+
6250
+ **Parameters:**
6251
+ - `locale` — - Локаль форматтера.
6252
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
6253
+
6254
+ **Returns:** Функция `(сообщение, значения) => строка`.
6255
+
6256
+ **Examples:**
6257
+
6258
+ ```ts
6259
+ const format = createMessageFormatter('ru');
6260
+ format('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}', { count: 5 });
6261
+ // '5 файлов'
6262
+ ```
6263
+
6264
+ _Source: src/i18n/message-format.ts_
6265
+
5362
6266
  ### createModel
5363
6267
 
5364
6268
  **Kind:** `function`
@@ -5395,13 +6299,70 @@ model.get(); // { email: 'a@b.c', profile: { name: 'Ada' }, tag
5395
6299
 
5396
6300
  _Source: src/model/create-model.ts_
5397
6301
 
6302
+ ### createSchemaController
6303
+
6304
+ **Kind:** `function`
6305
+
6306
+ Схема-контроллер одной сборки формы.
6307
+
6308
+ **Signature:**
6309
+ ```typescript
6310
+ export function createSchemaController(): SchemaController
6311
+ ```
6312
+
6313
+ **Examples:**
6314
+
6315
+ ```typescript
6316
+ const controller = createSchemaController();
6317
+ const root = controller.scopeOf(model);
6318
+ root.node('mortgage').setHidden(true);
6319
+
6320
+ controller.scopeOf(model.items.at(0)); // область строки — свои селекторы
6321
+ ```
6322
+
6323
+ _Source: src/form/schema-controller.ts_
6324
+
6325
+ ### createSchemaOverrideMaps
6326
+
6327
+ **Kind:** `function`
6328
+
6329
+ Пустое хранилище переопределений области.
6330
+
6331
+ **Signature:**
6332
+ ```typescript
6333
+ export function createSchemaOverrideMaps(): SchemaOverrideMaps
6334
+ ```
6335
+
6336
+ _Source: src/form/schema-controller.ts_
6337
+
6338
+ ### createSchemaScope
6339
+
6340
+ **Kind:** `function`
6341
+
6342
+ Схема области поверх хранилища: `node(selector)` → управление узлом.
6343
+
6344
+ **Signature:**
6345
+ ```typescript
6346
+ export function createSchemaScope(
6347
+ maps: SchemaOverrideMaps = createSchemaOverrideMaps()
6348
+ ): SchemaScope
6349
+ ```
6350
+
6351
+ **Parameters:**
6352
+ - `maps` — - Хранилище области; по умолчанию создаётся новое.
6353
+
6354
+ _Source: src/form/schema-controller.ts_
6355
+
5398
6356
  ### cross
5399
6357
 
5400
6358
  **Kind:** `function`
5401
6359
 
5402
- Cross-field правило: `fn` получает СНАПШОТ модели текущего scope (`model.get()`) и вешает ошибку на `sig`.
5403
- Для элементов массива / под-моделей захватывайте нужный снапшот в замыкание (`const item = im.get()`),
5404
- т.к. `fn` всегда получает модель ТЕКУЩЕГО scope (корень прогона), а не под-модель.
6360
+ Cross-field правило: `fn` получает СНАПШОТ модели текущей области (`model.get()`) и вешает
6361
+ ошибку на `sig`.
6362
+
6363
+ Область — та модель, с которой запущена схема: в корне прогона это вся форма, а в схеме,
6364
+ подключённой через {@link apply} / {@link applyEach}, — под-модель группы или строки массива.
6365
+ Поэтому правила подформы пишутся один раз и не знают, где она стоит.
5405
6366
 
5406
6367
  **Signature:**
5407
6368
  ```typescript
@@ -5413,6 +6374,20 @@ export function cross<TSnapshot>(
5413
6374
 
5414
6375
  _Source: src/form/validation/operators.ts_
5415
6376
 
6377
+ ### DEFAULT_LOCALE
6378
+
6379
+ **Kind:** `const`
6380
+
6381
+ Локаль «провайдера нет»: английский, пустой словарь. Каждый пакет при промахе по словарю берёт
6382
+ свою встроенную английскую таблицу, поэтому пустой словарь означает «всё по-английски».
6383
+
6384
+ **Signature:**
6385
+ ```typescript
6386
+ export const DEFAULT_LOCALE: FormLocale
6387
+ ```
6388
+
6389
+ _Source: src/i18n/locale.ts_
6390
+
5416
6391
  ### defer
5417
6392
 
5418
6393
  **Kind:** `function`
@@ -5440,15 +6415,47 @@ export function defineFormBehavior<T>(setup: (scope: BehaviorScope<T>) => void):
5440
6415
  **Examples:**
5441
6416
 
5442
6417
  ```ts
5443
- export const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {
6418
+ export const myBehavior = defineFormBehavior<MyForm>(({ model, form, schema }) => {
5444
6419
  compute(model.$.total, () => model.price * model.qty);
5445
6420
  enableWhen([model.$.city], () => Boolean(model.country));
5446
6421
  onChange(model.$.country, async (c) => form.city.updateComponentProps({ options: await load(c) }));
6422
+ hideWhen(schema.node('delivery'), () => model.country === '');
5447
6423
  });
5448
6424
  ```
5449
6425
 
5450
6426
  _Source: src/form/behaviors/context.ts_
5451
6427
 
6428
+ ### defineMessages
6429
+
6430
+ **Kind:** `function`
6431
+
6432
+ `msg` с типизированными ключами: принимает английский словарь приложения, проверяет ключ при
6433
+ компиляции и подставляет английский текст как `defaultMessage` — без провайдера и при промахе
6434
+ по локали форма покажет его, а не ключ.
6435
+
6436
+ **Signature:**
6437
+ ```typescript
6438
+ export function defineMessages<D extends Messages>(
6439
+ defaults: D
6440
+ ): {
6441
+ msg(key: keyof D & string, values?: Readonly<Record<string, unknown>>): MessageDescriptor;
6442
+ }
6443
+ ```
6444
+
6445
+ **Parameters:**
6446
+ - `defaults` — - Английский словарь приложения.
6447
+
6448
+ **Examples:**
6449
+
6450
+ ```ts
6451
+ import appEn from './locales/en.json';
6452
+
6453
+ export const { msg } = defineMessages(appEn);
6454
+ msg('profile.email.label'); // опечатка в ключе — ошибка компиляции
6455
+ ```
6456
+
6457
+ _Source: src/i18n/descriptor.ts_
6458
+
5452
6459
  ### defineValidationSchema
5453
6460
 
5454
6461
  **Kind:** `function`
@@ -5506,17 +6513,21 @@ _Source: src/form/behaviors/node.ts_
5506
6513
 
5507
6514
  **Kind:** `function`
5508
6515
 
5509
- Применить под-правила к КАЖДОМУ элементу текущего массива модели.
5510
- `U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.
6516
+ Применить под-правила к КАЖДОМУ элементу массива — прежняя запись {@link applyEach}.
6517
+
6518
+ Колбэк получает под-модель элемента, но области не создаёт: `cross` внутри него видит снапшот
6519
+ корня прогона.
5511
6520
 
5512
6521
  **Signature:**
5513
6522
  ```typescript
5514
6523
  export function each<U extends object>(
5515
- arr: ModelArray<U>,
6524
+ arr: ArrayBinding<U> | ModelArray<U>,
5516
6525
  itemFn: (item: FormModel<U>) => void
5517
6526
  ): void
5518
6527
  ```
5519
6528
 
6529
+ **Deprecated:** Пишите `applyEach(model.$.<массив>, schema)`.
6530
+
5520
6531
  _Source: src/form/validation/operators.ts_
5521
6532
 
5522
6533
  ### eachLeafSignal
@@ -5526,9 +6537,11 @@ _Source: src/form/validation/operators.ts_
5526
6537
  Обойти ВСЕ листовые сигналы модели (включая элементы массивов), вызвав `visit` на каждом.
5527
6538
 
5528
6539
  Внутри реактивного `effect` служит подпиской «любое поле изменилось»: `visit(sig => void sig.value)`
5529
- подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Так строятся
5530
- триггеры `change`/`blur` стратегий валидации (см. `createFormValidation`), тем же паттерном, что
5531
- `revalidateWhen`, но без ручного перечисления зависимостей.
6540
+ подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Тот же
6541
+ паттерн, что `revalidateWhen`, но без ручного перечисления зависимостей.
6542
+
6543
+ Сами массивы обход не посещает — только их элементы. Поле, привязанное к массиву целиком
6544
+ (мультивыбор), живёт на сигнале массива; чтобы увидеть и его, берите {@link eachValueSignal}.
5532
6545
 
5533
6546
  **Signature:**
5534
6547
  ```typescript
@@ -5540,6 +6553,28 @@ export function eachLeafSignal<T>(
5540
6553
 
5541
6554
  _Source: src/model/create-model.ts_
5542
6555
 
6556
+ ### eachValueSignal
6557
+
6558
+ **Kind:** `function`
6559
+
6560
+ Обойти ВСЕ ручки значений модели: листья (включая элементы массивов) И сами массивы.
6561
+
6562
+ Отличие от {@link eachLeafSignal} — узлы-массивы: массив бывает одним значением поля
6563
+ (`{ value: model.$.tags, component }`), и нода такого поля привязана к сигналу массива, а не
6564
+ к сигналам элементов. Обходчику, которому нужна именно НОДА поля (стратегия `blur` подписывается
6565
+ на её `touched`), одних листьев мало — он не увидел бы мультивыбор. Так строятся триггеры
6566
+ `change`/`blur` стратегий валидации (см. `createFormValidation`).
6567
+
6568
+ **Signature:**
6569
+ ```typescript
6570
+ export function eachValueSignal<T>(
6571
+ model: FormModel<T>,
6572
+ visit: (signal: PathAwareSignal<unknown>) => void
6573
+ ): void
6574
+ ```
6575
+
6576
+ _Source: src/model/create-model.ts_
6577
+
5543
6578
  ### effect
5544
6579
 
5545
6580
  **Kind:** `function`
@@ -5713,23 +6748,76 @@ exclusiveFlag(model.$.contacts, (row) => row.$.primary);
5713
6748
 
5714
6749
  _Source: src/form/behaviors/collections.ts_
5715
6750
 
5716
- ### FIELD_WRAPPER_ONLY_PROPS
6751
+ ### extendLocale
5717
6752
 
5718
- **Kind:** `const`
6753
+ **Kind:** `function`
5719
6754
 
5720
- Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
5721
- подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
6755
+ Новая локаль на основе существующей: словари сливаются (ключи из `patch` важнее), остальные
6756
+ поля заменяются. Исходный объект не меняется.
5722
6757
 
5723
6758
  **Signature:**
5724
6759
  ```typescript
5725
- export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
6760
+ export function extendLocale(base: FormLocale, patch: Partial<FormLocale>): FormLocale
5726
6761
  ```
5727
6762
 
5728
- _Source: src/platforms/react/field-binding/field-adapter.ts_
6763
+ **Parameters:**
6764
+ - `base` — - Локаль-основа, например поставляемая китом.
6765
+ - `patch` — - Свои ключи, переопределения китовых подписей и поля локали.
5729
6766
 
5730
- ### FieldAdapter
6767
+ **Examples:**
5731
6768
 
5732
- **Kind:** `interface`
6769
+ ```ts
6770
+ import { ru } from '@reformer/ui-kit/locale/ru';
6771
+
6772
+ const appRu = extendLocale(ru, {
6773
+ messages: { 'profile.title': 'Профиль', 'kit.formWizard.submit': 'Отправить' },
6774
+ });
6775
+ ```
6776
+
6777
+ _Source: src/i18n/locale.ts_
6778
+
6779
+ ### fetchMessages
6780
+
6781
+ **Kind:** `function`
6782
+
6783
+ Источник локали по сети: забирает JSON по адресу, построенному из кода языка. Файл может быть
6784
+ словарём (`{ "profile.title": "Профиль" }`) или локалью целиком (`{ "code": "ru", "messages": … }`).
6785
+ Ответ не `2xx` — ошибка загрузки.
6786
+
6787
+ **Signature:**
6788
+ ```typescript
6789
+ export function fetchMessages(url: (code: string) => string, init?: RequestInit): LocaleSource
6790
+ ```
6791
+
6792
+ **Parameters:**
6793
+ - `url` — - Адрес файла для языка.
6794
+ - `init` — - Параметры запроса, передаются в `fetch` как есть.
6795
+
6796
+ **Examples:**
6797
+
6798
+ ```ts
6799
+ fetchMessages((code) => `/locales/${code}.json`);
6800
+ ```
6801
+
6802
+ _Source: src/i18n/loader.ts_
6803
+
6804
+ ### FIELD_WRAPPER_ONLY_PROPS
6805
+
6806
+ **Kind:** `const`
6807
+
6808
+ Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
6809
+ подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
6810
+
6811
+ **Signature:**
6812
+ ```typescript
6813
+ export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
6814
+ ```
6815
+
6816
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6817
+
6818
+ ### FieldAdapter
6819
+
6820
+ **Kind:** `interface`
5733
6821
 
5734
6822
  Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
5735
6823
  DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
@@ -5895,11 +6983,12 @@ export interface FieldControlState<T> {
5895
6983
  * @example
5896
6984
  * ```tsx
5897
6985
  * const { errors } = useFormControl(field);
6986
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5898
6987
  *
5899
6988
  * return (
5900
6989
  * <ul className="error-list">
5901
6990
  * {errors.map((error, i) => (
5902
- * <li key={i}>{error.message}</li>
6991
+ * <li key={i}>{message(error)}</li>
5903
6992
  * ))}
5904
6993
  * </ul>
5905
6994
  * );
@@ -5962,12 +7051,13 @@ export interface FieldControlState<T> {
5962
7051
  * @example
5963
7052
  * ```tsx
5964
7053
  * const { shouldShowError, errors } = useFormControl(field);
7054
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5965
7055
  *
5966
7056
  * return (
5967
7057
  * <div>
5968
7058
  * <input {...props} />
5969
7059
  * {shouldShowError && (
5970
- * <span className="error">{errors[0]?.message}</span>
7060
+ * <span className="error">{message(errors[0])}</span>
5971
7061
  * )}
5972
7062
  * </div>
5973
7063
  * );
@@ -6030,6 +7120,7 @@ control: FieldNode<string>;
6030
7120
 
6031
7121
  function TextField({ control }: Props) {
6032
7122
  const state = useFormControl(control);
7123
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
6033
7124
 
6034
7125
  return (
6035
7126
  <div>
@@ -6039,7 +7130,7 @@ disabled={state.disabled}
6039
7130
  onChange={e => control.setValue(e.target.value)}
6040
7131
  />
6041
7132
  {state.shouldShowError && state.errors[0] && (
6042
- <span className="error">{state.errors[0].message}</span>
7133
+ <span className="error">{message(state.errors[0])}</span>
6043
7134
  )}
6044
7135
  </div>
6045
7136
  );
@@ -6262,6 +7353,33 @@ todos.forEach((item, i) => {
6262
7353
 
6263
7354
  _Source: src/form/types/form-proxy.ts_
6264
7355
 
7356
+ ### formatPattern
7357
+
7358
+ **Kind:** `function`
7359
+
7360
+ Собирает строку из разобранного сообщения. Значения подставляются как текст и повторно
7361
+ не разбираются.
7362
+
7363
+ **Signature:**
7364
+ ```typescript
7365
+ export function formatPattern(
7366
+ pattern: MessagePattern,
7367
+ values: MessageValues,
7368
+ locale: string,
7369
+ options: MessageFormatOptions = {}
7370
+ ): string
7371
+ ```
7372
+
7373
+ **Parameters:**
7374
+ - `pattern` — - Результат {@link parseMessage}.
7375
+ - `values` — - Значения аргументов.
7376
+ - `locale` — - Локаль для {@link Intl.PluralRules} и {@link Intl.NumberFormat} (`ru`, `en`, …).
7377
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
7378
+
7379
+ **Returns:** Готовая строка интерфейса.
7380
+
7381
+ _Source: src/i18n/message-format.ts_
7382
+
6265
7383
  ### FormBehavior
6266
7384
 
6267
7385
  **Kind:** `interface`
@@ -6271,13 +7389,57 @@ _Source: src/form/types/form-proxy.ts_
6271
7389
  **Signature:**
6272
7390
  ```typescript
6273
7391
  export interface FormBehavior<T> {
6274
- /** @internal Запускается `createForm` после построения нод и заполнения реестра сигнал→нода. */
6275
- __run(model: FormModel<T>, form: FormProxy<T>): BehaviorCleanup;
7392
+ /**
7393
+ * @internal Запускается сборкой формы после построения нод и заполнения реестра сигнал→нода.
7394
+ * `controller` — схема-контроллер сборки; без него поведение получает собственный, который
7395
+ * никто не читает (модель без рендерера).
7396
+ */
7397
+ __run(model: FormModel<T>, form: FormProxy<T>, controller?: SchemaController): BehaviorCleanup;
6276
7398
  }
6277
7399
  ```
6278
7400
 
6279
7401
  _Source: src/form/behaviors/types.ts_
6280
7402
 
7403
+ ### FormBundle
7404
+
7405
+ **Kind:** `interface`
7406
+
7407
+ Результат {@link createForm}.
7408
+
7409
+ **Signature:**
7410
+ ```typescript
7411
+ export interface FormBundle<T> {
7412
+ model: FormModel<T>;
7413
+ form: FormProxy<T>;
7414
+ /** Собранная валидация; есть, если в конфиге заданы правила. */
7415
+ validation?: FormValidationBundle<T>;
7416
+ render: FormRender;
7417
+ }
7418
+ ```
7419
+
7420
+ _Source: src/form/form-bundle.ts_
7421
+
7422
+ ### FormBundleContext
7423
+
7424
+ **Kind:** `const`
7425
+
7426
+ Контекст сборки. Значение — бандл `createForm`; вне рендерера — `null`.
7427
+
7428
+ **Signature:**
7429
+ ```typescript
7430
+ export const FormBundleContext
7431
+ ```
7432
+
7433
+ **Examples:**
7434
+
7435
+ ```tsx
7436
+ <FormBundleContext.Provider value={bundle}>
7437
+ <MyWizard />
7438
+ </FormBundleContext.Provider>
7439
+ ```
7440
+
7441
+ _Source: src/platforms/react/form-bundle-context.ts_
7442
+
6281
7443
  ### FormBundleLike
6282
7444
 
6283
7445
  **Kind:** `interface`
@@ -6407,6 +7569,43 @@ try {
6407
7569
 
6408
7570
  _Source: src/form/validation/error-handler.ts_
6409
7571
 
7572
+ ### FormLocale
7573
+
7574
+ **Kind:** `interface`
7575
+
7576
+ Локаль формы.
7577
+
7578
+ **Signature:**
7579
+ ```typescript
7580
+ export interface FormLocale {
7581
+ /** Код языка BCP 47 (`en`, `ru`, `pt-BR`): по нему работают `Intl` и множественные формы. */
7582
+ readonly code: string;
7583
+ /** Словарь сообщений. Ключа нет — пакет подставит встроенный английский. */
7584
+ readonly messages: Messages;
7585
+ /** Первый день недели календаря. По умолчанию — воскресенье. */
7586
+ readonly weekStartsOn?: WeekStart;
7587
+ /**
7588
+ * Необязательное переопределение: объект локали `react-day-picker` / `date-fns`. Для ядра
7589
+ * непрозрачен. Без него даты форматируются через `Intl` по {@link FormLocale.code}.
7590
+ */
7591
+ readonly dateLocale?: unknown;
7592
+ /** Необязательное переопределение: формат даты в токенах `date-fns` для DatePicker. */
7593
+ readonly dateFormat?: string;
7594
+ }
7595
+ ```
7596
+
7597
+ **Examples:**
7598
+
7599
+ ```ts
7600
+ const ru: FormLocale = {
7601
+ code: 'ru',
7602
+ weekStartsOn: 1,
7603
+ messages: { 'kit.select.selected': 'Выбрано: {count}' },
7604
+ };
7605
+ ```
7606
+
7607
+ _Source: src/i18n/locale.ts_
7608
+
6410
7609
  ### FormModel
6411
7610
 
6412
7611
  **Kind:** `type`
@@ -6507,7 +7706,7 @@ interface UserForm {
6507
7706
  };
6508
7707
  }
6509
7708
 
6510
- const form = createForm<UserForm>(schema);
7709
+ const form = createLegacyForm<UserForm>(schema);
6511
7710
 
6512
7711
  // Доступ к методам GroupNode
6513
7712
  await form.validate();
@@ -6521,6 +7720,30 @@ form.profile.name.setValue('John');
6521
7720
 
6522
7721
  _Source: src/form/types/form-proxy.ts_
6523
7722
 
7723
+ ### FormRender
7724
+
7725
+ **Kind:** `interface`
7726
+
7727
+ Часть бандла для рендерера: готовое дерево и схема-контроллер сборки.
7728
+
7729
+ **Signature:**
7730
+ ```typescript
7731
+ export interface FormRender {
7732
+ /** Корневой узел дерева схемы; `undefined`, если схема не задана. */
7733
+ readonly tree: FormSchemaNode | undefined;
7734
+ /** Схема-контроллер сборки: области корня, строк массивов и подформ. */
7735
+ readonly controller: SchemaController;
7736
+ /** Управление узлом КОРНЕВОЙ области по его `selector`. */
7737
+ node(selector: string): SchemaNodeControl;
7738
+ /** Обёртка поля из реестра (JSON-вариант). */
7739
+ readonly fieldWrapper?: unknown;
7740
+ /** Граница ошибок из реестра (JSON-вариант). */
7741
+ readonly errorBoundary?: unknown;
7742
+ }
7743
+ ```
7744
+
7745
+ _Source: src/form/form-bundle.ts_
7746
+
6524
7747
  ### FormSchema
6525
7748
 
6526
7749
  **Kind:** `type`
@@ -6534,7 +7757,7 @@ _Source: src/form/types/form-proxy.ts_
6534
7757
  Использует NonNullable для корректной обработки опциональных полей.
6535
7758
 
6536
7759
  ⚠️ Не путать с {@link FormSchemaNode} — тот описывает **узел дерева** M1-схемы (лист/массив/
6537
- контейнер), передаваемой в `createForm({ model, schema })`. `FormSchema` — это data-shaped конфиг
7760
+ контейнер), передаваемой в `createFormFromModel({ model, schema })`. `FormSchema` — это data-shaped конфиг
6538
7761
  (ключи = поля данных), а не узел дерева.
6539
7762
 
6540
7763
  **Signature:**
@@ -6590,12 +7813,13 @@ _Source: src/form/types/deep-schema.ts_
6590
7813
 
6591
7814
  **Kind:** `interface`
6592
7815
 
6593
- Узел единой схемы M1 — layout-дерево, обходимое `createForm({ model, schema })`
7816
+ Узел единой схемы M1 — layout-дерево, обходимое `createFormFromModel({ model, schema })`
6594
7817
  и рендерерами (schema-валидация живёт отдельно — `@reformer/core/validation`).
6595
7818
 
6596
7819
  Узел совмещает несколько ролей (различаются рантаймом по форме):
6597
- - **поле** — несёт `value: Signal` (сигнал модели `model.$.x`) + `component`/`validators`;
6598
- - **массив** — `{ array: model.<path>, item(itemModel) }`;
7820
+ - **поле** — `{ model: model.$.x, component, componentProps }`;
7821
+ - **массив под-форм** — `{ model: model.$.items, item: (model) => узел }`;
7822
+ - **подформа** — `{ model: model.$.group, part: (model) => узел }`;
6599
7823
  - **контейнер/ветка** — вложенные узлы (`children`), опц. условие `when`;
6600
7824
  - **record-of-fields** — под-узлы под произвольными именованными ключами (индексная сигнатура).
6601
7825
 
@@ -6606,9 +7830,20 @@ _Source: src/form/types/deep-schema.ts_
6606
7830
  ```typescript
6607
7831
  export interface FormSchemaNode {
6608
7832
  /**
6609
- * «Ручка» значения поля — маркер узла-поля. Обычно сигнал модели (`model.$.<path>`), но форма
6610
- * зависит от таргета (для массива `model.$.x` — дерево сигналов; в renderer-типах сужается до
6611
- * `Signal`). Движок разбирает узел как поле рантаймом по `value instanceof Signal`.
7833
+ * Привязка узла к части модели — ручка из дерева `model.$`. Чем узел является, решают ручка и
7834
+ * соседние ключи:
7835
+ * - лист или массив (`model.$.email`, `model.$.tags`) — **поле**;
7836
+ * - массив вместе с {@link FormSchemaNode.item} — **массив под-форм**;
7837
+ * - группа вместе с {@link FormSchemaNode.part} — **подформа**.
7838
+ *
7839
+ * Узел распознаётся по значению, а не по имени ключа: в записи «имя поля → узел» поле данных
7840
+ * может называться `model`, и тогда под этим ключом лежит обычный вложенный узел.
7841
+ */
7842
+ model?: unknown;
7843
+ /**
7844
+ * Прежняя запись привязки поля — то же, что {@link FormSchemaNode.model}.
7845
+ *
7846
+ * @deprecated Пишите `model: model.$.<path>`.
6612
7847
  */
6613
7848
  value?: unknown;
6614
7849
  /**
@@ -6638,14 +7873,27 @@ export interface FormSchemaNode {
6638
7873
  * выводит на своём месте в порядке следования.
6639
7874
  */
6640
7875
  children?: readonly (FormSchemaNode | string | number | Signal<any>)[];
6641
- /** Реактивный массив модели (`model.<path>`) — маркер узла-массива (вместе с `item`). */
7876
+ /**
7877
+ * Прежняя запись привязки массива под-форм — фасад `model.<path>` (вместе с `item`).
7878
+ *
7879
+ * @deprecated Пишите `model: model.$.<path>`.
7880
+ */
6642
7881
  array?: SchemaArrayControl;
6643
7882
  /** Схема элемента массива: под-модель элемента → узел поддерева. */
6644
- item?: (itemModel: any) => FormSchemaNode;
7883
+ item?: (model: any) => FormSchemaNode;
7884
+ /**
7885
+ * Подформа: под-модель группы → узел поддерева. Часть объявляется один раз и подключается к
7886
+ * любой группе той же формы данных: `{ model: model.$.registrationAddress, part: address }`.
7887
+ * Привязки внутри части идут через `$` полученной под-модели.
7888
+ */
7889
+ part?: (model: any) => FormSchemaNode;
6645
7890
  /**
6646
- * Значение нового элемента массива для кнопки «Добавить»: либо готовое значение,
6647
- * либо фабрика `() => value`. Тип не различает варианты (union `unknown | (() => unknown)`
6648
- * схлопывается в `unknown`) — рантайм различает по `typeof initialValue === 'function'`.
7891
+ * Шаблон нового элемента массива для кнопки «Добавить»: либо готовое значение, либо фабрика
7892
+ * `() => value`. Запасной путь — для форм, чья модель создаётся из данных без кода: шаблон,
7893
+ * объявленный в модели (`arrayOf(blank)`), главнее.
7894
+ *
7895
+ * Тип не различает варианты (union `unknown | (() => unknown)` схлопывается в `unknown`) —
7896
+ * рантайм различает по `typeof initialValue === 'function'`.
6649
7897
  */
6650
7898
  initialValue?: unknown;
6651
7899
  /** Свободная вложенность: record-of-fields и произвольные под-узлы. */
@@ -6864,21 +8112,22 @@ _Source: src/platforms/react/field-binding/field-adapter.ts_
6864
8112
 
6865
8113
  **Kind:** `function`
6866
8114
 
6867
- Найти ноду формы по сигналу модели.
8115
+ Найти ноду формы по ручке модели.
6868
8116
 
6869
- Используется state-операциями behavior (`enableWhen`/`disableWhen`) и роутингом ошибок
6870
- валидации в ноды. Возвращает `undefined`, если форма ещё не построена или поле не
6871
- материализовано в этой форме (например, элемент массива, который строится per-item).
8117
+ Используется state-операциями behavior (`enableWhen`/`disableWhen`), подключением под-схем
8118
+ (`apply`/`applyEach`) и роутингом ошибок валидации в ноды. Возвращает `undefined`, если форма
8119
+ ещё не построена или эта часть модели в форме не материализована (например, массив без
8120
+ item-схемы).
6872
8121
 
6873
8122
  **Signature:**
6874
8123
  ```typescript
6875
- export function getNodeForSignal(signal: Signal<any>): FormNode<any> | undefined
8124
+ export function getNodeForSignal(signal: object): FormNode<any> | undefined
6876
8125
  ```
6877
8126
 
6878
8127
  **Parameters:**
6879
- - `signal` — - Сигнал значения из {@link FormModel}
8128
+ - `signal` — - Ручка из {@link FormModel}: `model.$.<поле>`, `model.$.<группа>`, `model.$.<массив>`
6880
8129
 
6881
- **Returns:** Нода формы для этого поля или `undefined`
8130
+ **Returns:** Нода формы — поля, группы или массива — либо `undefined`
6882
8131
 
6883
8132
  **Examples:**
6884
8133
 
@@ -6890,8 +8139,14 @@ const sig = model.$.email;
6890
8139
  getNodeForSignal(sig)?.setErrors([{ code: 'required', message: 'Обязательно' }]);
6891
8140
  ```
6892
8141
 
8142
+ Нода группы и строки массива
8143
+ ```typescript
8144
+ getNodeForSignal(model.$.address)?.disable(); // группа целиком
8145
+ getNodeForSignal(model.$.phones[0])?.markAsTouched(); // форма строки массива
8146
+ ```
8147
+
6893
8148
  **See also:**
6894
- - {@link registerSignalNode} - регистрация связи сигнал→нода
8149
+ - {@link registerSignalNode} - регистрация связи ручка→нода
6895
8150
 
6896
8151
  _Source: src/form/signal-node-registry.ts_
6897
8152
 
@@ -6925,7 +8180,7 @@ _Source: src/form/type-guards.ts_
6925
8180
 
6926
8181
  **Kind:** `function`
6927
8182
 
6928
- Текущий scope ({ model, form }) активной схемы (escape hatch для кросс-операторов).
8183
+ Текущая область ({ model, form, schema }) активной схемы (escape hatch для кросс-операторов).
6929
8184
 
6930
8185
  **Signature:**
6931
8186
  ```typescript
@@ -6941,7 +8196,7 @@ _Source: src/form/behaviors/context.ts_
6941
8196
  GroupNode - узел для группы полей
6942
8197
 
6943
8198
  Создаётся из {@link FormSchema} (дерево field-конфигов). Обычно строится через `createForm`
6944
- (M1: `createForm({ model, schema })`); schema-валидация/behavior живут на слое модели
8199
+ (M1: `createFormFromModel({ model, schema })`); schema-валидация/behavior живут на слое модели
6945
8200
  (`validateModel` из `@reformer/core/validation`, `computeFrom`/`enableWhen`/…), а не на ноде.
6946
8201
 
6947
8202
  **Signature:**
@@ -6988,6 +8243,185 @@ export interface GroupNodeConfig<T> {
6988
8243
 
6989
8244
  _Source: src/form/types/index.ts_
6990
8245
 
8246
+ ### hideWhen
8247
+
8248
+ **Kind:** `function`
8249
+
8250
+ Скрывать узел, пока условие истинно.
8251
+
8252
+ Условие реактивно: пересчитывается при изменении любого сигнала, прочитанного внутри него.
8253
+ Принудительное `schema.node(selector).setHidden(...)` главнее условия.
8254
+
8255
+ **Signature:**
8256
+ ```typescript
8257
+ export function hideWhen(node: SchemaNodeControl, condition: () => boolean): void
8258
+ ```
8259
+
8260
+ **Examples:**
8261
+
8262
+ ```typescript
8263
+ hideWhen(schema.node('mortgage'), () => model.loanType !== 'mortgage');
8264
+ ```
8265
+
8266
+ _Source: src/form/behaviors/schema-operators.ts_
8267
+
8268
+ ### I18N_CONTEXT_MARKER
8269
+
8270
+ **Kind:** `const`
8271
+
8272
+ Имя контекста и одновременно маркер, по которому страж ищет его копии в сборках.
8273
+
8274
+ **Signature:**
8275
+ ```typescript
8276
+ export const I18N_CONTEXT_MARKER
8277
+ ```
8278
+
8279
+ _Source: src/platforms/react/i18n/context.ts_
8280
+
8281
+ ### I18nContextValue
8282
+
8283
+ **Kind:** `interface`
8284
+
8285
+ Что видит компонент: ручка активной локали и состояние её загрузки.
8286
+
8287
+ **Signature:**
8288
+ ```typescript
8289
+ export interface I18nContextValue extends I18nHandle {
8290
+ /** Идёт загрузка другого языка; на экране пока прежний. Без загрузчика всегда `false`. */
8291
+ readonly pending: boolean;
8292
+ /** Ошибка последней загрузки языка. */
8293
+ readonly error: unknown;
8294
+ }
8295
+ ```
8296
+
8297
+ _Source: src/platforms/react/i18n/context.ts_
8298
+
8299
+ ### I18nHandle
8300
+
8301
+ **Kind:** `interface`
8302
+
8303
+ Ручка локали: перевод и форматирование по одному языку.
8304
+
8305
+ **Signature:**
8306
+ ```typescript
8307
+ export interface I18nHandle {
8308
+ /** Локаль, по которой работает ручка. */
8309
+ readonly locale: FormLocale;
8310
+ /** Код языка — то же, что `locale.code`. */
8311
+ readonly code: string;
8312
+ /** Есть ли ключ в словаре локали (встроенные английские таблицы не учитываются). */
8313
+ has(key: string): boolean;
8314
+ /**
8315
+ * Перевод ключа приложения: словарь локали → `defaultMessage` → встроенная таблица ядра → ключ.
8316
+ *
8317
+ * @param key - Ключ сообщения.
8318
+ * @param values - Значения подстановок.
8319
+ * @param defaultMessage - Английский текст на случай, если ключа в локали нет.
8320
+ */
8321
+ t(key: string, values?: MessageValues, defaultMessage?: string): string;
8322
+ /** Число по правилам языка. */
8323
+ number(value: number, options?: Intl.NumberFormatOptions): string;
8324
+ /** Дата по правилам языка. */
8325
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
8326
+ /** Размер файла: `1536` → `1.5 KB` (`1,5 КБ` в русской локали). База — 1024. */
8327
+ fileSize(bytes: number): string;
8328
+ }
8329
+ ```
8330
+
8331
+ _Source: src/i18n/translator.ts_
8332
+
8333
+ ### I18nLoadingProviderProps
8334
+
8335
+ **Kind:** `interface`
8336
+
8337
+ Режим «провайдер грузит сам».
8338
+
8339
+ **Signature:**
8340
+ ```typescript
8341
+ export interface I18nLoadingProviderProps {
8342
+ /** Код языка. Смена кода запускает загрузку; до её конца на экране прежний язык. */
8343
+ lang: string;
8344
+ /** Загрузчик локалей — обычно результат `createLocaleLoader`. */
8345
+ load: LocaleLoad;
8346
+ /**
8347
+ * Что показать, пока не загружена самая первая локаль. По умолчанию — ничего: иначе на кадр
8348
+ * мелькнул бы встроенный английский. Если локаль уже в кэше загрузчика (`load.preload(lang)`
8349
+ * до монтирования), первый рендер синхронный и `fallback` не показывается.
8350
+ */
8351
+ fallback?: ReactNode;
8352
+ /** Отказ загрузки. Язык на экране при этом не меняется. */
8353
+ onError?: (error: unknown, lang: string) => void;
8354
+ children?: ReactNode;
8355
+ }
8356
+ ```
8357
+
8358
+ _Source: src/platforms/react/i18n/provider.ts_
8359
+
8360
+ ### I18nProvider
8361
+
8362
+ **Kind:** `function`
8363
+
8364
+ Провайдер локализации для подписей компонентов, ошибок валидации и авторских подписей формы.
8365
+ Без провайдера компоненты говорят встроенным английским.
8366
+
8367
+ При смене `lang` на экране остаётся прежний язык, пока новый не загрузится (`useI18n().pending`
8368
+ — для индикатора). При быстром переключении применяется только последний запрос. Вложенный
8369
+ провайдер заменяет локаль для своего поддерева целиком.
8370
+
8371
+ **Signature:**
8372
+ ```typescript
8373
+ export function I18nProvider(props: I18nProviderProps): ReactElement | null
8374
+ ```
8375
+
8376
+ **Examples:**
8377
+
8378
+ Провайдер грузит локаль сам
8379
+ ```tsx
8380
+ const [lang, setLang] = useState('ru');
8381
+
8382
+ <I18nProvider lang={lang} load={loadLocale}>
8383
+ <LangSwitch value={lang} onChange={setLang} />
8384
+ <CreditForm />
8385
+ </I18nProvider>
8386
+ ```
8387
+
8388
+ Готовая локаль — SSR и тесты
8389
+ ```tsx
8390
+ import { ru } from '@reformer/ui-kit/locale/ru';
8391
+
8392
+ <I18nProvider locale={ru}><CreditForm /></I18nProvider>
8393
+ ```
8394
+
8395
+ _Source: src/platforms/react/i18n/provider.ts_
8396
+
8397
+ ### I18nProviderProps
8398
+
8399
+ **Kind:** `type`
8400
+
8401
+ **Signature:**
8402
+ ```typescript
8403
+ export type I18nProviderProps = I18nStaticProviderProps | I18nLoadingProviderProps;
8404
+ ```
8405
+
8406
+ _Source: src/platforms/react/i18n/provider.ts_
8407
+
8408
+ ### I18nStaticProviderProps
8409
+
8410
+ **Kind:** `interface`
8411
+
8412
+ Режим «готовая локаль».
8413
+
8414
+ **Signature:**
8415
+ ```typescript
8416
+ export interface I18nStaticProviderProps {
8417
+ /** Локаль. Смена объекта переключает язык сразу. */
8418
+ locale: FormLocale;
8419
+ children?: ReactNode;
8420
+ }
8421
+ ```
8422
+
8423
+ _Source: src/platforms/react/i18n/provider.ts_
8424
+
6991
8425
  ### integer
6992
8426
 
6993
8427
  **Kind:** `function`
@@ -7233,6 +8667,19 @@ if (isGroupNode(node)) {
7233
8667
 
7234
8668
  _Source: src/form/type-guards.ts_
7235
8669
 
8670
+ ### isMessageDescriptor
8671
+
8672
+ **Kind:** `function`
8673
+
8674
+ Является ли значение описателем сообщения.
8675
+
8676
+ **Signature:**
8677
+ ```typescript
8678
+ export function isMessageDescriptor(value: unknown): value is MessageDescriptor
8679
+ ```
8680
+
8681
+ _Source: src/i18n/descriptor.ts_
8682
+
7236
8683
  ### isModelContainerSignal
7237
8684
 
7238
8685
  **Kind:** `function`
@@ -7293,6 +8740,207 @@ validate(model.$.amount, [required(), isNumber({ message: 'Введите чис
7293
8740
 
7294
8741
  _Source: src/form/validators/is-number.ts_
7295
8742
 
8743
+ ### isValueSignal
8744
+
8745
+ **Kind:** `function`
8746
+
8747
+ «Ручка значения», к которой можно привязать поле: лист (`instanceof Signal`) либо узел-массив
8748
+ дерева `model.$` (значение — массив целиком, `.value` читается и пишется).
8749
+
8750
+ Группа сюда не входит: она читается целиком, но не пишется и полем не бывает. Этой проверкой
8751
+ `createForm` и рендереры отличают узел-поле схемы (`{ value, component }`) от контейнера —
8752
+ вместо голого `value instanceof Signal`, который узел-массив не проходит.
8753
+
8754
+ **Signature:**
8755
+ ```typescript
8756
+ export function isValueSignal(value: unknown): value is Signal<unknown>
8757
+ ```
8758
+
8759
+ **Examples:**
8760
+
8761
+ ```typescript
8762
+ const model = createModel({ name: '', tags: [] as string[], address: { city: '' } });
8763
+ isValueSignal(model.$.name); // true — лист
8764
+ isValueSignal(model.$.tags); // true — массив как значение
8765
+ isValueSignal(model.$.address); // false — группа
8766
+ ```
8767
+
8768
+ _Source: src/model/model-signals-proxy.ts_
8769
+
8770
+ ### loadCoreLocale
8771
+
8772
+ **Kind:** `const`
8773
+
8774
+ Источник локалей ядра для `createLocaleLoader`: чанк нужного языка по запросу.
8775
+
8776
+ **Signature:**
8777
+ ```typescript
8778
+ export const loadCoreLocale
8779
+ ```
8780
+
8781
+ **Examples:**
8782
+
8783
+ ```ts
8784
+ const loadLocale = createLocaleLoader([loadCoreLocale, fetchMessages((code) => `/locales/${code}.json`)]);
8785
+ ```
8786
+
8787
+ _Source: src/locale/index.ts_
8788
+
8789
+ ### LocaleIssue
8790
+
8791
+ **Kind:** `interface`
8792
+
8793
+ Что не так со словарём.
8794
+
8795
+ **Signature:**
8796
+ ```typescript
8797
+ export interface LocaleIssue {
8798
+ readonly key: string;
8799
+ /**
8800
+ * - `syntax` — сообщение не разбирается;
8801
+ * - `missing` — ключ есть в образце, но его нет в словаре;
8802
+ * - `extra` — ключа нет в образце;
8803
+ * - `arguments` — набор подстановок не совпадает с образцом.
8804
+ */
8805
+ readonly kind: 'syntax' | 'missing' | 'extra' | 'arguments';
8806
+ readonly message: string;
8807
+ }
8808
+ ```
8809
+
8810
+ _Source: src/i18n/locale.ts_
8811
+
8812
+ ### LocaleLoad
8813
+
8814
+ **Kind:** `interface`
8815
+
8816
+ Функция загрузки локали. `peek` — необязательный синхронный кэш (есть у `createLocaleLoader`).
8817
+
8818
+ **Signature:**
8819
+ ```typescript
8820
+ export interface LocaleLoad {
8821
+ (code: string): Promise<FormLocale>;
8822
+ peek?(code: string): FormLocale | undefined;
8823
+ }
8824
+ ```
8825
+
8826
+ _Source: src/i18n/locale-store.ts_
8827
+
8828
+ ### LocaleLoader
8829
+
8830
+ **Kind:** `interface`
8831
+
8832
+ Загрузчик локалей с кэшем.
8833
+
8834
+ **Signature:**
8835
+ ```typescript
8836
+ export interface LocaleLoader {
8837
+ /** Загрузить локаль языка. Повторный вызов возвращает тот же промис и ту же локаль. */
8838
+ (code: string): Promise<FormLocale>;
8839
+ /** Уже загруженная локаль или `undefined`. Позволяет провайдеру отрисоваться синхронно. */
8840
+ peek(code: string): FormLocale | undefined;
8841
+ /** То же, что вызов загрузчика; имя для чтения в коде старта приложения. */
8842
+ preload(code: string): Promise<FormLocale>;
8843
+ }
8844
+ ```
8845
+
8846
+ _Source: src/i18n/loader.ts_
8847
+
8848
+ ### LocaleSource
8849
+
8850
+ **Kind:** `type`
8851
+
8852
+ Источник локали. Получает код языка, отдаёт локаль или словарь — сразу или промисом.
8853
+ `null`/`undefined` означает «для этого языка у меня ничего нет» и ошибкой не считается.
8854
+
8855
+ **Signature:**
8856
+ ```typescript
8857
+ export type LocaleSource = (code: string) => LocaleSourceResult | Promise<LocaleSourceResult>;
8858
+ ```
8859
+
8860
+ _Source: src/i18n/loader.ts_
8861
+
8862
+ ### LocaleSourceResult
8863
+
8864
+ **Kind:** `type`
8865
+
8866
+ Что может вернуть источник: локаль целиком, её часть, голый словарь или ничего.
8867
+
8868
+ **Signature:**
8869
+ ```typescript
8870
+ export type LocaleSourceResult = Partial<FormLocale> | Messages | null | undefined;
8871
+ ```
8872
+
8873
+ _Source: src/i18n/loader.ts_
8874
+
8875
+ ### LocaleStore
8876
+
8877
+ **Kind:** `interface`
8878
+
8879
+ Хранилище активной локали.
8880
+
8881
+ **Signature:**
8882
+ ```typescript
8883
+ export interface LocaleStore {
8884
+ getState(): LocaleStoreState;
8885
+ subscribe(listener: () => void): () => void;
8886
+ /** Запросить язык. Повторный запрос того же языка, пока он грузится или уже показан, — no-op. */
8887
+ setLang(lang: string): void;
8888
+ }
8889
+ ```
8890
+
8891
+ _Source: src/i18n/locale-store.ts_
8892
+
8893
+ ### LocaleStoreOptions
8894
+
8895
+ **Kind:** `interface`
8896
+
8897
+ Настройки хранилища.
8898
+
8899
+ **Signature:**
8900
+ ```typescript
8901
+ export interface LocaleStoreOptions {
8902
+ /** Вызывается при отказе загрузки языка, который всё ещё запрошен. */
8903
+ readonly onError?: (error: unknown, lang: string) => void;
8904
+ }
8905
+ ```
8906
+
8907
+ _Source: src/i18n/locale-store.ts_
8908
+
8909
+ ### LocaleStoreState
8910
+
8911
+ **Kind:** `interface`
8912
+
8913
+ Состояние хранилища. Объект неизменяемый: новая ссылка — новое состояние.
8914
+
8915
+ **Signature:**
8916
+ ```typescript
8917
+ export interface LocaleStoreState {
8918
+ /** Запрошенный язык. Пока `pending`, может отличаться от языка `locale`. */
8919
+ readonly lang: string;
8920
+ /** Локаль, которая сейчас на экране; `null` — ни одна ещё не загружена. */
8921
+ readonly locale: FormLocale | null;
8922
+ /** Идёт загрузка запрошенного языка. */
8923
+ readonly pending: boolean;
8924
+ /** Ошибка последней загрузки; сбрасывается следующей удачной. */
8925
+ readonly error: unknown;
8926
+ }
8927
+ ```
8928
+
8929
+ _Source: src/i18n/locale-store.ts_
8930
+
8931
+ ### LocalizableText
8932
+
8933
+ **Kind:** `type`
8934
+
8935
+ Текст, который можно задать строкой или описателем.
8936
+
8937
+ **Signature:**
8938
+ ```typescript
8939
+ export type LocalizableText = string | MessageDescriptor;
8940
+ ```
8941
+
8942
+ _Source: src/i18n/descriptor.ts_
8943
+
7296
8944
  ### makeElementFieldHandle
7297
8945
 
7298
8946
  **Kind:** `function`
@@ -7530,122 +9178,280 @@ _Source: src/form/validators/max-files.ts_
7530
9178
 
7531
9179
  Фабрика валидатора максимального размера файла.
7532
9180
 
7533
- Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
7534
- `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
7535
- Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
7536
- (например, дескриптор загруженного файла без размера) пропускаются
7537
- (используйте {@link required} для обязательности).
9181
+ Работает с одиночным файлом или массивом (`File` либо файлоподобный дескриптор с
9182
+ `name`/`size`). Проверяется каждый файл; в ошибку попадает первый нарушивший.
9183
+ Пустые значения (`null`/`undefined`/`''`/`[]`) и элементы без числового `size`
9184
+ (например, дескриптор загруженного файла без размера) пропускаются
9185
+ (используйте {@link required} для обязательности).
9186
+
9187
+ **Signature:**
9188
+ ```typescript
9189
+ export function maxFileSize<TForm = unknown, TField = unknown>(
9190
+ maxSize: number,
9191
+ options?: ValidateOptions
9192
+ ): Validator<TForm, TField>
9193
+ ```
9194
+
9195
+ **Parameters:**
9196
+ - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
9197
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
9198
+ попадают `maxFileSize`, `fileName` и `actualSize`.
9199
+
9200
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
9201
+
9202
+ **Examples:**
9203
+
9204
+ Ограничение размера вложений
9205
+ ```typescript
9206
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
9207
+ import { maxFileSize } from '@reformer/core/validators';
9208
+
9209
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
9210
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
9211
+ validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
9212
+ });
9213
+ ```
9214
+
9215
+ _Source: src/form/validators/max-file-size.ts_
9216
+
9217
+ ### maxLength
9218
+
9219
+ **Kind:** `function`
9220
+
9221
+ Фабрика валидатора максимальной длины строки или массива.
9222
+
9223
+ Работает со строкой или массивом (проверяется `value.length`). Пустые значения
9224
+ (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
9225
+ (используйте {@link required} для обязательности).
9226
+
9227
+ **Signature:**
9228
+ ```typescript
9229
+ export function maxLength<TForm = unknown, TField = unknown>(
9230
+ maxLen: number,
9231
+ options?: ValidateOptions
9232
+ ): Validator<TForm, TField>
9233
+ ```
9234
+
9235
+ **Parameters:**
9236
+ - `maxLen` — - Максимально допустимая длина (включительно)
9237
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
9238
+ попадают `maxLength` и `actualLength`.
9239
+
9240
+ **Returns:** Чистый валидатор {@link Validator} для строки или массива
9241
+
9242
+ **Examples:**
9243
+
9244
+ Максимальная длина строки
9245
+ ```typescript
9246
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
9247
+ import { maxLength } from '@reformer/core/validators';
9248
+
9249
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
9250
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
9251
+ validate(model.$.name, [maxLength(50)]);
9252
+ validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
9253
+ });
9254
+ ```
9255
+
9256
+ _Source: src/form/validators/max-length.ts_
9257
+
9258
+ ### maxTotalFileSize
9259
+
9260
+ **Kind:** `function`
9261
+
9262
+ Фабрика валидатора суммарного размера всех файлов.
9263
+
9264
+ Складывает `size` всех файлов значения (элементы без числового `size` в сумму
9265
+ не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
9266
+ (используйте {@link required} для обязательности).
9267
+
9268
+ **Signature:**
9269
+ ```typescript
9270
+ export function maxTotalFileSize<TForm = unknown, TField = unknown>(
9271
+ maxTotal: number,
9272
+ options?: ValidateOptions
9273
+ ): Validator<TForm, TField>
9274
+ ```
9275
+
9276
+ **Parameters:**
9277
+ - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
9278
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
9279
+ попадают `maxTotalFileSize` и `actualTotal`.
9280
+
9281
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
9282
+
9283
+ **Examples:**
9284
+
9285
+ Суммарно не более 20 МБ
9286
+ ```typescript
9287
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
9288
+ import { maxTotalFileSize } from '@reformer/core/validators';
9289
+
9290
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
9291
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
9292
+ validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
9293
+ });
9294
+ ```
9295
+
9296
+ _Source: src/form/validators/max-total-file-size.ts_
9297
+
9298
+ ### MESSAGE_DESCRIPTOR
9299
+
9300
+ **Kind:** `const`
9301
+
9302
+ Бренд описателя. `Symbol.for` — чтобы описатель узнавался и через границу двух копий модуля
9303
+ (разные чанки, разные версии пакета на странице).
9304
+
9305
+ **Signature:**
9306
+ ```typescript
9307
+ export const MESSAGE_DESCRIPTOR: unique symbol
9308
+ ```
9309
+
9310
+ _Source: src/i18n/descriptor.ts_
9311
+
9312
+ ### messageArguments
9313
+
9314
+ **Kind:** `function`
9315
+
9316
+ Имена аргументов сообщения, включая те, что встречаются только внутри веток `plural`/`select`.
9317
+
9318
+ **Signature:**
9319
+ ```typescript
9320
+ export function messageArguments(
9321
+ pattern: MessagePattern,
9322
+ out: Set<string> = new Set()
9323
+ ): Set<string>
9324
+ ```
9325
+
9326
+ **Parameters:**
9327
+ - `pattern` — - Результат `parseMessage`.
9328
+
9329
+ _Source: src/i18n/locale.ts_
9330
+
9331
+ ### MessageBranches
9332
+
9333
+ **Kind:** `type`
9334
+
9335
+ Ветки `plural`/`select`: категория CLDR (`one`, `few`, …), точное совпадение (`=0`) или значение `select`.
9336
+
9337
+ **Signature:**
9338
+ ```typescript
9339
+ export type MessageBranches = ReadonlyMap<string, MessagePattern>;
9340
+ ```
9341
+
9342
+ _Source: src/i18n/message-format.ts_
9343
+
9344
+ ### MessageDescriptor
9345
+
9346
+ **Kind:** `interface`
9347
+
9348
+ Описатель сообщения. Намеренно без ключей `value`, `array`, `item`, `component`: обход схемы в
9349
+ `createForm` узнаёт по ним узел, а описатель узлом не является.
9350
+
9351
+ **Signature:**
9352
+ ```typescript
9353
+ export interface MessageDescriptor {
9354
+ readonly [MESSAGE_DESCRIPTOR]: true;
9355
+ /** Ключ сообщения в словаре приложения. */
9356
+ readonly key: string;
9357
+ /** Значения подстановок. */
9358
+ readonly values?: Readonly<Record<string, unknown>>;
9359
+ /** Английский текст на случай, если ключа в локали нет. */
9360
+ readonly defaultMessage?: string;
9361
+ }
9362
+ ```
9363
+
9364
+ _Source: src/i18n/descriptor.ts_
9365
+
9366
+ ### MessageFormatOptions
9367
+
9368
+ **Kind:** `interface`
9369
+
9370
+ Настройки форматирования.
7538
9371
 
7539
9372
  **Signature:**
7540
9373
  ```typescript
7541
- export function maxFileSize<TForm = unknown, TField = unknown>(
7542
- maxSize: number,
7543
- options?: ValidateOptions
7544
- ): Validator<TForm, TField>
9374
+ export interface MessageFormatOptions {
9375
+ /**
9376
+ * Что подставить вместо аргумента, которого нет в значениях (или чьё значение не число там, где
9377
+ * ожидалось число). По умолчанию — заметный маркер `⟦name⟧`, тот же приём, что и для пропущенного
9378
+ * ключа: пропуск должен попадаться разработчику, а не пользователю.
9379
+ */
9380
+ readonly onMissingArgument?: (name: string) => string;
9381
+ }
7545
9382
  ```
7546
9383
 
7547
- **Parameters:**
7548
- - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
7549
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7550
- попадают `maxFileSize`, `fileName` и `actualSize`.
9384
+ _Source: src/i18n/message-format.ts_
7551
9385
 
7552
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
9386
+ ### MessageNode
7553
9387
 
7554
- **Examples:**
9388
+ **Kind:** `type`
7555
9389
 
7556
- Ограничение размера вложений
7557
- ```typescript
7558
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7559
- import { maxFileSize } from '@reformer/core/validators';
9390
+ Узел разобранного сообщения.
7560
9391
 
7561
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7562
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7563
- validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
7564
- });
9392
+ **Signature:**
9393
+ ```typescript
9394
+ export type MessageNode =
9395
+ | { readonly kind: 'text'; readonly value: string }
9396
+ | { readonly kind: 'arg'; readonly name: string }
9397
+ | { readonly kind: 'number'; readonly name: string }
9398
+ | { readonly kind: 'plural'; readonly name: string; readonly branches: MessageBranches }
9399
+ | { readonly kind: 'select'; readonly name: string; readonly branches: MessageBranches };
7565
9400
  ```
7566
9401
 
7567
- _Source: src/form/validators/max-file-size.ts_
7568
-
7569
- ### maxLength
9402
+ _Source: src/i18n/message-format.ts_
7570
9403
 
7571
- **Kind:** `function`
9404
+ ### MessagePattern
7572
9405
 
7573
- Фабрика валидатора максимальной длины строки или массива.
9406
+ **Kind:** `type`
7574
9407
 
7575
- Работает со строкой или массивом (проверяется `value.length`). Пустые значения
7576
- (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
7577
- (используйте {@link required} для обязательности).
9408
+ Разобранное сообщение — плоская последовательность узлов.
7578
9409
 
7579
9410
  **Signature:**
7580
9411
  ```typescript
7581
- export function maxLength<TForm = unknown, TField = unknown>(
7582
- maxLen: number,
7583
- options?: ValidateOptions
7584
- ): Validator<TForm, TField>
9412
+ export type MessagePattern = readonly MessageNode[];
7585
9413
  ```
7586
9414
 
7587
- **Parameters:**
7588
- - `maxLen` — - Максимально допустимая длина (включительно)
7589
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7590
- попадают `maxLength` и `actualLength`.
9415
+ _Source: src/i18n/message-format.ts_
7591
9416
 
7592
- **Returns:** Чистый валидатор {@link Validator} для строки или массива
9417
+ ### Messages
7593
9418
 
7594
- **Examples:**
9419
+ **Kind:** `type`
7595
9420
 
7596
- Максимальная длина строки
7597
- ```typescript
7598
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7599
- import { maxLength } from '@reformer/core/validators';
9421
+ Плоский словарь: ключ → сообщение в формате ICU (см. `message-format.ts`).
7600
9422
 
7601
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7602
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7603
- validate(model.$.name, [maxLength(50)]);
7604
- validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
7605
- });
9423
+ **Signature:**
9424
+ ```typescript
9425
+ export type Messages = Readonly<Record<string, string>>;
7606
9426
  ```
7607
9427
 
7608
- _Source: src/form/validators/max-length.ts_
7609
-
7610
- ### maxTotalFileSize
9428
+ _Source: src/i18n/locale.ts_
7611
9429
 
7612
- **Kind:** `function`
9430
+ ### MessageSyntaxError
7613
9431
 
7614
- Фабрика валидатора суммарного размера всех файлов.
9432
+ **Kind:** `class`
7615
9433
 
7616
- Складывает `size` всех файлов значения (элементы без числового `size` в сумму
7617
- не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
7618
- (используйте {@link required} для обязательности).
9434
+ Отказ разбора сообщения: синтаксис или отсутствие обязательной ветки `other`.
7619
9435
 
7620
9436
  **Signature:**
7621
9437
  ```typescript
7622
- export function maxTotalFileSize<TForm = unknown, TField = unknown>(
7623
- maxTotal: number,
7624
- options?: ValidateOptions
7625
- ): Validator<TForm, TField>
9438
+ export class MessageSyntaxError extends Error { /* … */ }
7626
9439
  ```
7627
9440
 
7628
- **Parameters:**
7629
- - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
7630
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7631
- попадают `maxTotalFileSize` и `actualTotal`.
9441
+ _Source: src/i18n/message-format.ts_
7632
9442
 
7633
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
9443
+ ### MessageValues
7634
9444
 
7635
- **Examples:**
9445
+ **Kind:** `type`
7636
9446
 
7637
- Суммарно не более 20 МБ
7638
- ```typescript
7639
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7640
- import { maxTotalFileSize } from '@reformer/core/validators';
9447
+ Значения аргументов сообщения.
7641
9448
 
7642
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7643
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7644
- validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
7645
- });
9449
+ **Signature:**
9450
+ ```typescript
9451
+ export type MessageValues = Readonly<Record<string, unknown>>;
7646
9452
  ```
7647
9453
 
7648
- _Source: src/form/validators/max-total-file-size.ts_
9454
+ _Source: src/i18n/message-format.ts_
7649
9455
 
7650
9456
  ### min
7651
9457
 
@@ -7923,7 +9729,10 @@ export interface ModelApi<T> {
7923
9729
  reset(): void;
7924
9730
  /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
7925
9731
  captureInitial(): void;
7926
- /** Резолв строкового пути в сигнал (для error-routing/мостов). */
9732
+ /**
9733
+ * Резолв строкового пути в сигнал (для error-routing/мостов): лист либо массив целиком
9734
+ * (ручка значения массива — см. {@link ModelArraySignals}). Для пути группы — `undefined`.
9735
+ */
7927
9736
  signalAt(path: string): PathAwareSignal<unknown> | undefined;
7928
9737
  }
7929
9738
  ```
@@ -7947,10 +9756,13 @@ export interface ModelArray<U> {
7947
9756
  readonly __path: string;
7948
9757
  /** Реактивная длина. */
7949
9758
  readonly length: number;
7950
- /** Добавить элемент в конец (значение элемента целиком). */
7951
- push(item: U): void;
7952
- /** Вставить элемент по индексу. */
7953
- insertAt(index: number, item: U): void;
9759
+ /**
9760
+ * Добавить элемент в конец (значение элемента целиком). Без значения — элемент по шаблону
9761
+ * массива (`arrayOf(blank)` в модели); шаблона нет — ошибка.
9762
+ */
9763
+ push(item?: U): void;
9764
+ /** Вставить элемент по индексу. Без значения — элемент по шаблону, как у {@link push}. */
9765
+ insertAt(index: number, item?: U): void;
7954
9766
  /** Удалить элемент по индексу. */
7955
9767
  removeAt(index: number): void;
7956
9768
  /** Переместить элемент. */
@@ -7987,8 +9799,8 @@ _Source: src/model/types.ts_
7987
9799
  export interface ModelArrayControl<TItem extends object> {
7988
9800
  readonly length: number;
7989
9801
  at(index: number): FormModel<TItem> | undefined;
7990
- push(item: TItem): void;
7991
- insertAt(index: number, item: TItem): void;
9802
+ push(item?: TItem): void;
9803
+ insertAt(index: number, item?: TItem): void;
7992
9804
  removeAt(index: number): void;
7993
9805
  move(from: number, to: number): void;
7994
9806
  swap(a: number, b: number): void;
@@ -8012,7 +9824,7 @@ _Source: src/form/nodes/model-array-node.ts_
8012
9824
  reorder/повторном рендере не пересоздаются (состояние и валидация сохраняются). Реализует тот же
8013
9825
  контракт, что ждут секции массива и `useFormControl` (`length`/`value`/`valid`/`errors`/`at`/`push`/…).
8014
9826
 
8015
- Обычно создаётся не напрямую, а `createForm({ model, schema })`: когда в схеме встречается узел
9827
+ Обычно создаётся не напрямую, а `createFormFromModel({ model, schema })`: когда в схеме встречается узел
8016
9828
  массива `{ array: model.<field>, item: (item) => itemSchema }`, форма материализует его как
8017
9829
  `ModelArrayNode` и кладёт под `form.<field>` (совместим с `FormArraySection`).
8018
9830
 
@@ -8032,7 +9844,7 @@ name: { value: item.$.name, component: Input },
8032
9844
  qty: { value: item.$.qty, component: Input },
8033
9845
  });
8034
9846
 
8035
- const form = createForm({
9847
+ const form = createFormFromModel({
8036
9848
  model,
8037
9849
  schema: {
8038
9850
  children: [{ array: model.rows, item: rowItem }],
@@ -8052,12 +9864,23 @@ _Source: src/form/nodes/model-array-node.ts_
8052
9864
  **Kind:** `type`
8053
9865
 
8054
9866
  Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
8055
- и {@link ReadonlySignal} значения массива целиком (реагирует и на правку элемента, и на изменение
8056
- состава — push/removeAt/move).
9867
+ и сигнал значения массива целиком (реагирует и на правку элемента, и на изменение состава —
9868
+ push/removeAt/move).
9869
+
9870
+ В отличие от группы, узел ЗАПИСЫВАЕМ: `model.$.tags.value = ['a']` заменяет массив целиком. Это
9871
+ делает его {@link PathAwareSignal} — к нему привязывают поле (`{ value: model.$.tags, component }`),
9872
+ правило (`validate(model.$.tags, …)`) и поведение, как к листу. Так массив служит ОДНИМ значением
9873
+ поля: мультивыбор, теги, список файлов.
9874
+
9875
+ `V` — объявленный тип поля, включая `null`/`undefined`: поле `string[] | null` с начальным `null`
9876
+ в рантайме — лист с тем же `.value`/`peek`/`subscribe`, поэтому общая часть честна для обоих
9877
+ случаев. `length` и индексы есть только у настоящего узла-массива (начальное значение — массив).
8057
9878
 
8058
9879
  **Signature:**
8059
9880
  ```typescript
8060
- export type ModelArraySignals<U, V> = ContainerSignal<V, 'length'> & {
9881
+ export type ModelArraySignals<U, V> = Signal<V> & {
9882
+ /** Путь массива в модели (dot-нотация). */
9883
+ readonly __path: string;
8061
9884
  readonly length: number;
8062
9885
  readonly [index: number]: ModelSignalNode<U>;
8063
9886
  };
@@ -8099,6 +9922,57 @@ export type ModelObject<T> = {
8099
9922
 
8100
9923
  _Source: src/model/types.ts_
8101
9924
 
9925
+ ### modelOf
9926
+
9927
+ **Kind:** `function`
9928
+
9929
+ Value-фасад по ручке дерева `model.$`: группа → под-модель {@link FormModel}, массив →
9930
+ {@link ModelArray}. Обратная операция к `model.$`: `modelOf(model.$.address) === model.address`.
9931
+
9932
+ Ручка находит фасад по ИДЕНТИЧНОСТИ, а не по пути. Путь для этого не годится: он абсолютный
9933
+ (`items.0.phones`) и меняется при перестановке строк, а области поведения и формы строк
9934
+ вложенные — поиск по пути от под-модели строки ничего не находит.
9935
+
9936
+ Лист ручкой контейнера не является — функция бросает. В частности, группа или массив,
9937
+ созданные из начального `null`, в модели — лист: вид узла фиксируется при создании.
9938
+
9939
+ **Signature:**
9940
+ ```typescript
9941
+ export function modelOf<V extends object | null | undefined>(handle: { peek(): V }): ModelOf<V>
9942
+ ```
9943
+
9944
+ **Parameters:**
9945
+ - `handle` — - Узел-группа или узел-массив дерева `model.$`.
9946
+
9947
+ **Returns:** Под-модель группы либо фасад массива.
9948
+
9949
+ **Examples:**
9950
+
9951
+ ```typescript
9952
+ const model = createModel({ address: { city: '' }, phones: [{ number: '' }] });
9953
+
9954
+ modelOf(model.$.address).city = 'Казань'; // под-модель: чтение, запись, get/set/patch
9955
+ modelOf(model.$.phones).push({ number: '+7' }); // фасад массива: мутации и обход
9956
+ modelOf(model.$.phones[0]).number; // строка массива — тоже группа
9957
+ ```
9958
+
9959
+ _Source: src/model/model-value-proxy.ts_
9960
+
9961
+ ### ModelOf
9962
+
9963
+ **Kind:** `type`
9964
+
9965
+ Value-фасад для ручки {@link modelOf}: массив → {@link ModelArray}, объект → под-модель
9966
+ {@link FormModel}.
9967
+
9968
+ **Signature:**
9969
+ ```typescript
9970
+ export type ModelOf<V> =
9971
+ NonNullable<V> extends ReadonlyArray<infer U> ? ModelArray<U> : FormModel<NonNullable<V>>;
9972
+ ```
9973
+
9974
+ _Source: src/model/model-value-proxy.ts_
9975
+
8102
9976
  ### ModelSignals
8103
9977
 
8104
9978
  **Kind:** `type`
@@ -8140,6 +10014,34 @@ export type ModelValue<V> =
8140
10014
 
8141
10015
  _Source: src/model/types.ts_
8142
10016
 
10017
+ ### msg
10018
+
10019
+ **Kind:** `function`
10020
+
10021
+ Создаёт описатель сообщения.
10022
+
10023
+ **Signature:**
10024
+ ```typescript
10025
+ export function msg(
10026
+ key: string,
10027
+ values?: Readonly<Record<string, unknown>>,
10028
+ defaultMessage?: string
10029
+ ): MessageDescriptor
10030
+ ```
10031
+
10032
+ **Parameters:**
10033
+ - `key` — - Ключ в словаре приложения.
10034
+ - `values` — - Значения подстановок.
10035
+ - `defaultMessage` — - Английский текст на случай, если ключа в локали нет.
10036
+
10037
+ **Examples:**
10038
+
10039
+ ```ts
10040
+ { value: model.$.email, component: Input, componentProps: { label: msg('profile.email.label') } }
10041
+ ```
10042
+
10043
+ _Source: src/i18n/descriptor.ts_
10044
+
8143
10045
  ### multipleOf
8144
10046
 
8145
10047
  **Kind:** `function`
@@ -8249,6 +10151,25 @@ const node = factory.createNode({ value: '', component: Input }); // → FieldNo
8249
10151
 
8250
10152
  _Source: src/form/factories/node-factory.ts_
8251
10153
 
10154
+ ### NodeLifecycleHooks
10155
+
10156
+ **Kind:** `interface`
10157
+
10158
+ Хуки жизненного цикла узла схемы. Каждый хук опционален; повторная регистрация перезаписывает
10159
+ предыдущее значение.
10160
+
10161
+ **Signature:**
10162
+ ```typescript
10163
+ export interface NodeLifecycleHooks {
10164
+ /** Срабатывает один раз при mount узла. Может вернуть cleanup-функцию. */
10165
+ onMount?: () => void | (() => void);
10166
+ /** Срабатывает один раз при unmount узла. */
10167
+ onUnmount?: () => void;
10168
+ }
10169
+ ```
10170
+
10171
+ _Source: src/form/schema-controller.ts_
10172
+
8252
10173
  ### nonNegative
8253
10174
 
8254
10175
  **Kind:** `function`
@@ -8343,6 +10264,34 @@ export function onChange<T>(
8343
10264
 
8344
10265
  _Source: src/form/behaviors/operators.ts_
8345
10266
 
10267
+ ### onComponentEvent
10268
+
10269
+ **Kind:** `function`
10270
+
10271
+ Повесить обработчик на проп-событие компонента узла (`onSubmit`, `onChange`, …).
10272
+
10273
+ Обработчик получает те же аргументы, что и исходный проп компонента, и перекрывает одноимённый
10274
+ проп из `componentProps`.
10275
+
10276
+ **Signature:**
10277
+ ```typescript
10278
+ export function onComponentEvent(
10279
+ node: SchemaNodeControl,
10280
+ event: string,
10281
+ handler: (...args: any[]) => any
10282
+ ): void
10283
+ ```
10284
+
10285
+ **Examples:**
10286
+
10287
+ ```typescript
10288
+ onComponentEvent(schema.node('wizard'), 'onSubmit', async () => {
10289
+ await submitApplication(model.get());
10290
+ });
10291
+ ```
10292
+
10293
+ _Source: src/form/behaviors/schema-operators.ts_
10294
+
8346
10295
  ### onDispose
8347
10296
 
8348
10297
  **Kind:** `function`
@@ -8356,6 +10305,95 @@ export function onDispose(cleanup: BehaviorCleanup): void
8356
10305
 
8357
10306
  _Source: src/form/behaviors/context.ts_
8358
10307
 
10308
+ ### onInit
10309
+
10310
+ **Kind:** `function`
10311
+
10312
+ Синхронный хук времени сборки: `fn` вызывается сразу, до первого рендера узла. Единственный
10313
+ хук, способный повлиять на первый рендер, — внутри можно править пропсы через
10314
+ `schema.node(selector).patchProps({ … })`.
10315
+
10316
+ К монтированию узла не привязан; аргумент `node` принимается ради симметрии с
10317
+ {@link onMount} / {@link onUnmount}.
10318
+
10319
+ **Signature:**
10320
+ ```typescript
10321
+ export function onInit(_node: SchemaNodeControl, fn: () => void): void
10322
+ ```
10323
+
10324
+ **Examples:**
10325
+
10326
+ ```typescript
10327
+ onInit(schema.node('summary'), () => {
10328
+ schema.node('summary').patchProps({ currency: detectCurrency() });
10329
+ });
10330
+ ```
10331
+
10332
+ _Source: src/form/behaviors/schema-operators.ts_
10333
+
10334
+ ### onMount
10335
+
10336
+ **Kind:** `function`
10337
+
10338
+ Хук после первого монтирования узла. Может вернуть очистку — она выполнится при размонтировании.
10339
+
10340
+ **Signature:**
10341
+ ```typescript
10342
+ export function onMount(node: SchemaNodeControl, fn: () => void | (() => void)): void
10343
+ ```
10344
+
10345
+ **Examples:**
10346
+
10347
+ ```typescript
10348
+ onMount(schema.node('data-boundary'), () => void loadApplication());
10349
+ ```
10350
+
10351
+ _Source: src/form/behaviors/schema-operators.ts_
10352
+
10353
+ ### onUnmount
10354
+
10355
+ **Kind:** `function`
10356
+
10357
+ Хук перед размонтированием узла.
10358
+
10359
+ **Signature:**
10360
+ ```typescript
10361
+ export function onUnmount(node: SchemaNodeControl, fn: () => void): void
10362
+ ```
10363
+
10364
+ **Examples:**
10365
+
10366
+ ```typescript
10367
+ onUnmount(schema.node('wizard'), () => saveDraft(model.get()));
10368
+ ```
10369
+
10370
+ _Source: src/form/behaviors/schema-operators.ts_
10371
+
10372
+ ### parseMessage
10373
+
10374
+ **Kind:** `function`
10375
+
10376
+ Разбирает сообщение в дерево. Бросает {@link MessageSyntaxError} на ошибке синтаксиса и на
10377
+ `plural`/`select` без ветки `other`.
10378
+
10379
+ **Signature:**
10380
+ ```typescript
10381
+ export function parseMessage(source: string): MessagePattern
10382
+ ```
10383
+
10384
+ **Parameters:**
10385
+ - `source` — - Текст сообщения из словаря.
10386
+
10387
+ **Returns:** Разобранное сообщение для {@link formatPattern}.
10388
+
10389
+ **Examples:**
10390
+
10391
+ ```ts
10392
+ parseMessage('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}');
10393
+ ```
10394
+
10395
+ _Source: src/i18n/message-format.ts_
10396
+
8359
10397
  ### pastDate
8360
10398
 
8361
10399
  **Kind:** `function`
@@ -8526,35 +10564,60 @@ _Source: src/form/validators/phone.ts_
8526
10564
 
8527
10565
  **Kind:** `function`
8528
10566
 
8529
- Связать сигнал модели с его нодой формы.
10567
+ Связать ручку модели с её нодой формы.
8530
10568
 
8531
- Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого
8532
- листового поля. Прикладной код обычно этот реестр не заполняет напрямую.
10569
+ Вызывается движком при сборке формы (`createForm`/{@link createFormFromModel}) для каждого поля,
10570
+ группы и массива под-форм. Прикладной код обычно этот реестр не заполняет напрямую.
8533
10571
 
8534
10572
  **Signature:**
8535
10573
  ```typescript
8536
- export function registerSignalNode(signal: Signal<any>, node: FormNode<any>): void
10574
+ export function registerSignalNode(signal: object, node: FormNode<any>): void
8537
10575
  ```
8538
10576
 
8539
10577
  **Parameters:**
8540
- - `signal` — - Сигнал значения из {@link FormModel} (ручка поля, `model.$.path`)
8541
- - `node` — - Нода формы, отвечающая за это поле
10578
+ - `signal` — - Ручка из {@link FormModel}: сигнал листа, узел-массив или узел-группа дерева `model.$`
10579
+ - `node` — - Нода формы, отвечающая за эту часть модели
10580
+
10581
+ **Examples:**
10582
+
10583
+ Привязка листовых полей при построении формы
10584
+ ```typescript
10585
+ import { registerSignalNode } from '@reformer/core';
10586
+
10587
+ const sig = model.signalAt('profile.email');
10588
+ const node = group.getFieldByPath('profile.email');
10589
+ if (sig && node) registerSignalNode(sig, node);
10590
+ ```
10591
+
10592
+ **See also:**
10593
+ - {@link getNodeForSignal} - обратный поиск ноды по ручке
10594
+
10595
+ _Source: src/form/signal-node-registry.ts_
10596
+
10597
+ ### renderEffect
10598
+
10599
+ **Kind:** `function`
10600
+
10601
+ Реактивный эффект, живущий, пока схема области смонтирована рендерером.
10602
+
10603
+ В отличие от `effect` поведения, запускается после монтирования — внутри доступны ref узлов
10604
+ (`schema.node(selector).getRef().current`). Может вернуть функцию очистки.
10605
+
10606
+ **Signature:**
10607
+ ```typescript
10608
+ export function renderEffect(schema: SchemaScope, effectFn: () => void | (() => void)): void
10609
+ ```
8542
10610
 
8543
10611
  **Examples:**
8544
10612
 
8545
- Привязка листовых полей при построении формы
8546
10613
  ```typescript
8547
- import { registerSignalNode } from '@reformer/core';
8548
-
8549
- const sig = model.signalAt('profile.email');
8550
- const node = group.getFieldByPath('profile.email');
8551
- if (sig && node) registerSignalNode(sig, node);
10614
+ const wizard = schema.node('wizard').getRef<FormWizardHandle>();
10615
+ renderEffect(schema, () => {
10616
+ if (model.loanType === 'mortgage') wizard.current?.goToStep(1);
10617
+ });
8552
10618
  ```
8553
10619
 
8554
- **See also:**
8555
- - {@link getNodeForSignal} - обратный поиск ноды по сигналу
8556
-
8557
- _Source: src/form/signal-node-registry.ts_
10620
+ _Source: src/form/behaviors/schema-operators.ts_
8558
10621
 
8559
10622
  ### required
8560
10623
 
@@ -8620,6 +10683,81 @@ resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue:
8620
10683
 
8621
10684
  _Source: src/model/behaviors-value.ts_
8622
10685
 
10686
+ ### ResolvedSchema
10687
+
10688
+ **Kind:** `interface`
10689
+
10690
+ Дерево, собранное реестром из документа схемы (JSON), и то, что реестр хочет донести до
10691
+ рендерера вместе с ним.
10692
+
10693
+ **Signature:**
10694
+ ```typescript
10695
+ export interface ResolvedSchema {
10696
+ /** Корневой узел дерева. */
10697
+ tree: FormSchemaNode;
10698
+ /** Обёртка поля — компонент, которым рендерер оборачивает каждое поле. */
10699
+ fieldWrapper?: unknown;
10700
+ /** Граница ошибок — компонент, которым рендерер оборачивает дерево. */
10701
+ errorBoundary?: unknown;
10702
+ }
10703
+ ```
10704
+
10705
+ _Source: src/form/form-bundle.ts_
10706
+
10707
+ ### resolveLocalized
10708
+
10709
+ **Kind:** `function`
10710
+
10711
+ Заменяет описатели сообщений на строки по активной локали. Обходит массивы и простые
10712
+ объекты-литералы — подписи опций, заголовки шагов; сигналы, React-элементы, функции и
10713
+ экземпляры классов (узлы формы) возвращает как есть. Поддерево без описателей сохраняет ссылку:
10714
+ если раскрывать было нечего, возвращается тот же объект.
10715
+
10716
+ **Signature:**
10717
+ ```typescript
10718
+ export function resolveLocalized<T>(value: T, i18n: I18nHandle): T
10719
+ ```
10720
+
10721
+ **Parameters:**
10722
+ - `value` — - Пропсы, массив или одиночное значение.
10723
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
10724
+
10725
+ **Examples:**
10726
+
10727
+ ```ts
10728
+ resolveLocalized({ label: msg('profile.email.label'), options: [{ value: 'a', label: msg('opt.a') }] }, i18n);
10729
+ // { label: 'Почта', options: [{ value: 'a', label: 'Вариант А' }] }
10730
+ ```
10731
+
10732
+ _Source: src/i18n/resolve-localized.ts_
10733
+
10734
+ ### resolveValidationError
10735
+
10736
+ **Kind:** `function`
10737
+
10738
+ Отображаемый текст ошибки валидации. Порядок источников — в шапке модуля.
10739
+
10740
+ **Signature:**
10741
+ ```typescript
10742
+ export function resolveValidationError(error: ValidationError, i18n: I18nHandle): string
10743
+ ```
10744
+
10745
+ **Parameters:**
10746
+ - `error` — - Ошибка от правила: `code`, `message`, `messageKey`, `params`.
10747
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
10748
+
10749
+ **Examples:**
10750
+
10751
+ ```ts
10752
+ resolveValidationError({ code: 'minLength', message: '', params: { minLength: 8 } }, i18n);
10753
+ // 'Enter at least 8 characters' · в русской локали — 'Не меньше 8 символов'
10754
+
10755
+ resolveValidationError({ code: 'required', message: 'Укажите телефон' }, i18n);
10756
+ // 'Укажите телефон' — явное сообщение автора важнее словаря
10757
+ ```
10758
+
10759
+ _Source: src/i18n/validation-message.ts_
10760
+
8623
10761
  ### revalidateWhen
8624
10762
 
8625
10763
  **Kind:** `function`
@@ -8653,7 +10791,8 @@ _Source: src/model/behaviors-value.ts_
8653
10791
  (`required()`/`min()`/… — они `(value, model, root) => …`) и inline-правила `(value) => …`
8654
10792
  ОБА присваиваются в `Rule<TField>[]` без `any`, при этом сохраняется проверка типа поля
8655
10793
  (`validate(model.$.age, [email()])` подсветится — `email` ждёт `string`, поле `number`).
8656
- На вызове раннер приводит правило к callable и передаёт `(value, model, model)`.
10794
+ На вызове раннер приводит правило к callable и передаёт `(value, scope, root)`: модель текущей
10795
+ области (в корне прогона — вся модель) и корень прогона.
8657
10796
 
8658
10797
  **Signature:**
8659
10798
  ```typescript
@@ -8789,13 +10928,155 @@ export interface SchemaArrayControl {
8789
10928
  /** Реактивная длина. */
8790
10929
  readonly length: number;
8791
10930
  at(index: number): unknown;
8792
- push(item: unknown): void;
10931
+ push(item?: unknown): void;
8793
10932
  removeAt(index: number): void;
8794
10933
  }
8795
10934
  ```
8796
10935
 
8797
10936
  _Source: src/form/types/schema-node.ts_
8798
10937
 
10938
+ ### SchemaController
10939
+
10940
+ **Kind:** `interface`
10941
+
10942
+ Схема-контроллер сборки: области по под-моделям.
10943
+
10944
+ **Signature:**
10945
+ ```typescript
10946
+ export interface SchemaController {
10947
+ /**
10948
+ * Область под-модели: корня сборки, строки массива или группы подформы. Создаётся при первом
10949
+ * обращении; повторное обращение отдаёт ту же область.
10950
+ */
10951
+ scopeOf(subModel: object): SchemaScope;
10952
+ }
10953
+ ```
10954
+
10955
+ _Source: src/form/schema-controller.ts_
10956
+
10957
+ ### SchemaNodeControl
10958
+
10959
+ **Kind:** `interface`
10960
+
10961
+ Управление одним узлом схемы. Получается через `schema.node(selector)`.
10962
+
10963
+ **Signature:**
10964
+ ```typescript
10965
+ export interface SchemaNodeControl {
10966
+ /** Принудительно скрыть или показать узел, минуя условие `hideWhen`. */
10967
+ setHidden(value: boolean): this;
10968
+ /** Убрать принудительное значение — снова действует условие `hideWhen`. */
10969
+ resetHidden(): this;
10970
+ /** Подмешать объект в `componentProps` узла. */
10971
+ patchProps(partial: Record<string, unknown>): this;
10972
+ /** Убрать подмешанные пропсы. */
10973
+ resetProps(): this;
10974
+ /**
10975
+ * Ref на компонент узла. Создаётся один раз на селектор; рендерер вешает его на компонент.
10976
+ * Компонент обязан принимать `ref`.
10977
+ */
10978
+ getRef<H>(): { current: H | null };
10979
+ /** @internal Селектор узла. */
10980
+ __selector: string;
10981
+ /** @internal Хранилище области. */
10982
+ __overrideMaps: SchemaOverrideMaps;
10983
+ }
10984
+ ```
10985
+
10986
+ _Source: src/form/schema-controller.ts_
10987
+
10988
+ ### SchemaOverrideMaps
10989
+
10990
+ **Kind:** `interface`
10991
+
10992
+ Хранилище переопределений одной области схемы. Ключ каждой карты — `selector` узла.
10993
+
10994
+ **Signature:**
10995
+ ```typescript
10996
+ export interface SchemaOverrideMaps {
10997
+ hiddenOverrides: Map<string, boolean | null>;
10998
+ propsOverrides: Map<string, Record<string, unknown> | null>;
10999
+ refRegistry: Map<string, { current: any }>;
11000
+ /** Условия скрытия узлов: selector → реактивное условие. */
11001
+ conditionRegistry: Map<string, () => boolean>;
11002
+ /** Реактивные эффекты, живущие, пока область смонтирована (`renderEffect`). */
11003
+ effectRegistry: Array<() => void | (() => void)>;
11004
+ /** Обработчики событий компонентов: selector → { имя пропа → обработчик }. */
11005
+ callbackRegistry: Map<string, Map<string, (...args: any[]) => any>>;
11006
+ /** Хуки жизненного цикла узла: selector → { onMount, onUnmount }. */
11007
+ lifecycleRegistry: Map<string, NodeLifecycleHooks>;
11008
+ /** Общий счётчик изменений области. Узлы на него не подписываются — см. `versionFor`. */
11009
+ version: Signal<number>;
11010
+ /**
11011
+ * Версия-сигнал одного селектора (создаётся лениво). Узел подписывается только на сигнал СВОЕГО
11012
+ * селектора, поэтому `setHidden` / `patchProps` уведомляют один узел, а не всё дерево.
11013
+ */
11014
+ versionFor(selector: string): Signal<number>;
11015
+ }
11016
+ ```
11017
+
11018
+ _Source: src/form/schema-controller.ts_
11019
+
11020
+ ### SchemaResolver
11021
+
11022
+ **Kind:** `interface`
11023
+
11024
+ Реестр, умеющий превратить документ схемы в дерево. Метод необязателен: у интерфейса реестра
11025
+ есть сторонние реализации, и ядро документ само не разбирает.
11026
+
11027
+ **Signature:**
11028
+ ```typescript
11029
+ export interface SchemaResolver {
11030
+ resolveSchema?(document: unknown, model: FormModel<never>): ResolvedSchema;
11031
+ }
11032
+ ```
11033
+
11034
+ _Source: src/form/form-bundle.ts_
11035
+
11036
+ ### SchemaScope
11037
+
11038
+ **Kind:** `interface`
11039
+
11040
+ Схема одной области: адресует узлы по `selector`.
11041
+
11042
+ **Signature:**
11043
+ ```typescript
11044
+ export interface SchemaScope {
11045
+ /** Управление узлом области по его `selector`. */
11046
+ node(selector: string): SchemaNodeControl;
11047
+ /** @internal Хранилище области — его читает рендерер. */
11048
+ __overrideMaps: SchemaOverrideMaps;
11049
+ }
11050
+ ```
11051
+
11052
+ _Source: src/form/schema-controller.ts_
11053
+
11054
+ ### schemaSubtree
11055
+
11056
+ **Kind:** `function`
11057
+
11058
+ Поддерево схемы для под-модели — результат `builder(subModel)`, один на пару.
11059
+
11060
+ **Signature:**
11061
+ ```typescript
11062
+ export function schemaSubtree<N>(builder: (subModel: any) => N, subModel: object): N
11063
+ ```
11064
+
11065
+ **Parameters:**
11066
+ - `builder` — - Билдер поддерева: `item` узла-массива или `part` узла-подформы.
11067
+ - `subModel` — - Под-модель строки массива или группы.
11068
+
11069
+ **Returns:** Узел поддерева; повторный вызов с той же парой отдаёт тот же объект.
11070
+
11071
+ **Examples:**
11072
+
11073
+ ```typescript
11074
+ const row = model.items.at(0);
11075
+ schemaSubtree(itemSchema, row) === schemaSubtree(itemSchema, row); // true
11076
+ ```
11077
+
11078
+ _Source: src/form/schema-subtree.ts_
11079
+
8799
11080
  ### SetValueOptions
8800
11081
 
8801
11082
  **Kind:** `interface`
@@ -8984,6 +11265,32 @@ transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
8984
11265
 
8985
11266
  _Source: src/model/behaviors-value.ts_
8986
11267
 
11268
+ ### translateBuiltin
11269
+
11270
+ **Kind:** `function`
11271
+
11272
+ Перевод ключа пакета: словарь локали, затем встроенная английская таблица пакета, затем сам
11273
+ ключ. Английская таблица форматируется по английским правилам — её множественные формы и
11274
+ группировка чисел не зависят от активного языка.
11275
+
11276
+ **Signature:**
11277
+ ```typescript
11278
+ export function translateBuiltin(
11279
+ locale: FormLocale,
11280
+ builtin: Messages,
11281
+ key: string,
11282
+ values?: MessageValues
11283
+ ): string
11284
+ ```
11285
+
11286
+ **Parameters:**
11287
+ - `locale` — - Активная локаль.
11288
+ - `builtin` — - Встроенная английская таблица пакета-владельца ключа.
11289
+ - `key` — - Ключ сообщения.
11290
+ - `values` — - Значения подстановок.
11291
+
11292
+ _Source: src/i18n/translator.ts_
11293
+
8987
11294
  ### UnknownRecord
8988
11295
 
8989
11296
  **Kind:** `type`
@@ -8998,6 +11305,36 @@ export type UnknownRecord = Record<string, unknown>;
8998
11305
 
8999
11306
  _Source: src/form/types/index.ts_
9000
11307
 
11308
+ ### unknownSchemaSelectors
11309
+
11310
+ **Kind:** `function`
11311
+
11312
+ Селекторы, на которые в области записаны правила, но которых нет в её дереве. Такое правило
11313
+ ничего не делает — сборка и рендерер в dev предупреждают о нём.
11314
+
11315
+ Ref в проверку не входит: его разрешено брать по абсолютному пути модели
11316
+ (`phones.0.number`), а такой строки при сборке может ещё не быть.
11317
+
11318
+ **Signature:**
11319
+ ```typescript
11320
+ export function unknownSchemaSelectors(maps: SchemaOverrideMaps, tree: unknown): string[]
11321
+ ```
11322
+
11323
+ **Parameters:**
11324
+ - `maps` — - Хранилище области.
11325
+ - `tree` — - Дерево (поддерево) области.
11326
+
11327
+ **Returns:** Селекторы с правилами, не найденные в дереве.
11328
+
11329
+ **Examples:**
11330
+
11331
+ ```typescript
11332
+ const scope = controller.scopeOf(model);
11333
+ unknownSchemaSelectors(scope.__overrideMaps, tree); // ['mortgage'] — узла с таким selector нет
11334
+ ```
11335
+
11336
+ _Source: src/form/schema-controller.ts_
11337
+
9001
11338
  ### unmarkDerived
9002
11339
 
9003
11340
  **Kind:** `function`
@@ -9185,6 +11522,29 @@ const credit = useFormBundle(() => createCoreForm<CreditForm>({ model: createCre
9185
11522
 
9186
11523
  _Source: src/platforms/react/hooks/use-form-bundle.ts_
9187
11524
 
11525
+ ### useFormBundleContext
11526
+
11527
+ **Kind:** `function`
11528
+
11529
+ Бандл сборки, внутри которой отрисован компонент; `null` — компонент стоит вне рендерера
11530
+ (например, визард в разметке React-руками получает форму и валидацию пропсами).
11531
+
11532
+ **Signature:**
11533
+ ```typescript
11534
+ export function useFormBundleContext<T = unknown>(): FormBundle<T> | null
11535
+ ```
11536
+
11537
+ **Examples:**
11538
+
11539
+ ```tsx
11540
+ function StepFooter() {
11541
+ const bundle = useFormBundleContext<CreditForm>();
11542
+ return <button onClick={() => bundle?.validation?.validateAll()}>Проверить</button>;
11543
+ }
11544
+ ```
11545
+
11546
+ _Source: src/platforms/react/form-bundle-context.ts_
11547
+
9188
11548
  ### useFormControl
9189
11549
 
9190
11550
  **Kind:** `function`
@@ -9227,6 +11587,7 @@ export function useFormControl(
9227
11587
  Текстовое поле с валидацией
9228
11588
  ```tsx
9229
11589
  import { useFormControl } from '@reformer/core';
11590
+ import { useValidationMessage } from '@reformer/core/i18n';
9230
11591
  import type { FieldNode } from '@reformer/core';
9231
11592
 
9232
11593
  interface TextFieldProps {
@@ -9242,6 +11603,8 @@ shouldShowError,
9242
11603
  errors,
9243
11604
  pending
9244
11605
  } = useFormControl(control);
11606
+ // Текст ошибки даёт резолвер: у правил без своего сообщения `message` пуст.
11607
+ const message = useValidationMessage();
9245
11608
 
9246
11609
  return (
9247
11610
  <div className="field">
@@ -9261,7 +11624,7 @@ return (
9261
11624
 
9262
11625
  {shouldShowError && errors[0] && (
9263
11626
  <span className="error" role="alert">
9264
- {errors[0].message}
11627
+ {message(errors[0])}
9265
11628
  </span>
9266
11629
  )}
9267
11630
  </div>
@@ -9309,6 +11672,7 @@ control: FieldNode<string>;
9309
11672
 
9310
11673
  function Select({ control }: SelectProps) {
9311
11674
  const { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);
11675
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9312
11676
  const options = componentProps.options as Array<{ value: string; label: string }>;
9313
11677
 
9314
11678
  return (
@@ -9326,7 +11690,7 @@ onBlur={() => control.markAsTouched()}
9326
11690
  </option>
9327
11691
  ))}
9328
11692
  </select>
9329
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
11693
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9330
11694
  </div>
9331
11695
  );
9332
11696
  }
@@ -9564,6 +11928,7 @@ dangerouslySetInnerHTML={{ __html: html }}
9564
11928
  // Основной редактор использует useFormControl для полного состояния
9565
11929
  function MarkdownEditor({ control }: MarkdownEditorProps) {
9566
11930
  const { value, shouldShowError, errors } = useFormControl(control);
11931
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9567
11932
 
9568
11933
  return (
9569
11934
  <div className="editor-container">
@@ -9571,7 +11936,7 @@ return (
9571
11936
  value={value}
9572
11937
  onChange={e => control.setValue(e.target.value)}
9573
11938
  />
9574
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
11939
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9575
11940
 
9576
11941
  {/* Preview обновляется только при изменении value *}
9577
11942
  <MarkdownPreview control={control} />
@@ -9675,12 +12040,104 @@ export interface UseFormValidationResult {
9675
12040
 
9676
12041
  _Source: src/platforms/react/hooks/use-form-validation.ts_
9677
12042
 
12043
+ ### useI18n
12044
+
12045
+ **Kind:** `function`
12046
+
12047
+ Активная локаль: перевод ключей приложения, форматирование чисел, дат и размеров, состояние
12048
+ загрузки языка. Без провайдера — английский с пустым словарём.
12049
+
12050
+ **Signature:**
12051
+ ```typescript
12052
+ export function useI18n(): I18nContextValue
12053
+ ```
12054
+
12055
+ **Examples:**
12056
+
12057
+ ```tsx
12058
+ function LangBadge() {
12059
+ const { code, pending, t } = useI18n();
12060
+ return <span aria-busy={pending}>{t('app.language')}: {code}</span>;
12061
+ }
12062
+ ```
12063
+
12064
+ _Source: src/platforms/react/i18n/hooks.ts_
12065
+
12066
+ ### useMessages
12067
+
12068
+ **Kind:** `function`
12069
+
12070
+ Переводчик пакета: ищет ключ в локали провайдера, а при промахе берёт встроенную английскую
12071
+ таблицу этого пакета. Так пакет говорит по-английски без провайдера и не зависит от того, знает
12072
+ ли локаль его ключи. Тип ключа выводится из таблицы — опечатка в ключе не скомпилируется.
12073
+
12074
+ `defaults` должен быть стабильной ссылкой (импорт JSON-модуля), иначе переводчик будет
12075
+ пересоздаваться на каждый рендер.
12076
+
12077
+ **Signature:**
12078
+ ```typescript
12079
+ export function useMessages<D extends Messages>(
12080
+ defaults: D
12081
+ ): (key: keyof D & string, values?: MessageValues) => string
12082
+ ```
12083
+
12084
+ **Parameters:**
12085
+ - `defaults` — - Встроенная английская таблица пакета.
12086
+
12087
+ **Returns:** Функция `(ключ, значения?) => строка`; её идентичность меняется вместе с локалью.
12088
+
12089
+ **Examples:**
12090
+
12091
+ ```tsx
12092
+ import en from './locale/en.json';
12093
+
12094
+ const useKitMessages = () => useMessages(en);
12095
+
12096
+ function ClearButton() {
12097
+ const t = useKitMessages();
12098
+ return <button aria-label={t('kit.select.clear')} />;
12099
+ }
12100
+ ```
12101
+
12102
+ _Source: src/platforms/react/i18n/hooks.ts_
12103
+
12104
+ ### useValidationMessage
12105
+
12106
+ **Kind:** `function`
12107
+
12108
+ Резолвер текста ошибки валидации по активной локали: `messageKey` автора → явное `message` →
12109
+ словарь локали по коду → встроенный английский → код. Идентичность функции меняется вместе с
12110
+ локалью, поэтому показанные ошибки переводятся при смене языка без повторной валидации.
12111
+
12112
+ `FormField` из `@reformer/cdk` использует его сам; вызывайте напрямую, если рисуете ошибки
12113
+ своим компонентом.
12114
+
12115
+ **Signature:**
12116
+ ```typescript
12117
+ export function useValidationMessage(): (error: ValidationError) => string
12118
+ ```
12119
+
12120
+ **Examples:**
12121
+
12122
+ ```tsx
12123
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
12124
+ const message = useValidationMessage();
12125
+ const { errors } = useFormControl(control);
12126
+ return <>{errors.map((error) => <span key={error.code}>{message(error)}</span>)}</>;
12127
+ }
12128
+ ```
12129
+
12130
+ _Source: src/platforms/react/i18n/hooks.ts_
12131
+
9678
12132
  ### validate
9679
12133
 
9680
12134
  **Kind:** `function`
9681
12135
 
9682
12136
  Синхронные правила поля.
9683
12137
 
12138
+ Поле — лист либо массив целиком: `model.$.<массив>` — такая же ручка значения, и правило
12139
+ получает массив (мультивыбор, теги, список файлов).
12140
+
9684
12141
  **Signature:**
9685
12142
  ```typescript
9686
12143
  export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void
@@ -9690,6 +12147,7 @@ export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TFiel
9690
12147
 
9691
12148
  ```ts
9692
12149
  validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
12150
+ validate(model.$.tags, [required(), maxLength(3)]); // tags: string[] — массив как значение
9693
12151
  ```
9694
12152
 
9695
12153
  _Source: src/form/validation/operators.ts_
@@ -9720,6 +12178,34 @@ validateAsync(model.$.username, [async (v, { signal }) => {
9720
12178
 
9721
12179
  _Source: src/form/validation/operators.ts_
9722
12180
 
12181
+ ### validateLocale
12182
+
12183
+ **Kind:** `function`
12184
+
12185
+ Проверяет словарь: каждое сообщение разбирается, а при заданном образце — набор ключей и имена
12186
+ подстановок совпадают с ним. Нужна там, где компилятор не поможет: словарь пришёл по сети или
12187
+ написан вне репозитория. В рантайме вызывать не обязательно — битое или пропущенное сообщение
12188
+ и так откатится на встроенный английский; проверка нужна тестам и CI.
12189
+
12190
+ **Signature:**
12191
+ ```typescript
12192
+ export function validateLocale(messages: Messages, reference?: Messages): LocaleIssue[]
12193
+ ```
12194
+
12195
+ **Parameters:**
12196
+ - `messages` — - Проверяемый словарь.
12197
+ - `reference` — - Образец, обычно английский словарь того же пакета.
12198
+
12199
+ **Returns:** Список расхождений; пустой — словарь в порядке.
12200
+
12201
+ **Examples:**
12202
+
12203
+ ```ts
12204
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
12205
+ ```
12206
+
12207
+ _Source: src/i18n/locale.ts_
12208
+
9723
12209
  ### validateModel
9724
12210
 
9725
12211
  **Kind:** `function`
@@ -9776,8 +12262,20 @@ _Source: src/form/validation/run.ts_
9776
12262
  ```typescript
9777
12263
  export interface ValidateOptions {
9778
12264
  /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
9779
- * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
12265
+ * берётся из словаря локали по `code` (`validation.<code>`; см. `resolveValidationError` в
12266
+ * `@reformer/core/i18n`). Явное сообщение важнее словаря, но не переключается вместе с языком —
12267
+ * для локализуемого текста используйте {@link ValidateOptions.messageKey}. */
9780
12268
  message?: string;
12269
+ /**
12270
+ * Ключ сообщения в словаре приложения — локализуемый текст автора правила. Если ключ есть в
12271
+ * активной локали, он важнее и `message`, и текста по `code`; если нет — в ход идёт `message`.
12272
+ *
12273
+ * @example
12274
+ * ```ts
12275
+ * required({ messageKey: 'profile.name.required', message: 'Name is required' });
12276
+ * ```
12277
+ */
12278
+ messageKey?: string;
9781
12279
  /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
9782
12280
  params?: Record<string, FormValue>;
9783
12281
  }
@@ -9809,7 +12307,13 @@ _Source: src/form/validation/operators.ts_
9809
12307
  ```typescript
9810
12308
  export interface ValidationError {
9811
12309
  code: string;
12310
+ /**
12311
+ * Готовый текст ошибки. Пустая строка — «автор правила текста не задавал»: отображаемую строку
12312
+ * тогда даёт резолвер по `code` (словарь локали `validation.<code>`).
12313
+ */
9812
12314
  message: string;
12315
+ /** Ключ сообщения в словаре приложения; при наличии в активной локали важнее `message`. */
12316
+ messageKey?: string;
9813
12317
  params?: Record<string, FormValue>;
9814
12318
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
9815
12319
  severity?: 'error' | 'warning';
@@ -9929,3 +12433,16 @@ watchField(model.$.country, async (country) => {
9929
12433
  ```
9930
12434
 
9931
12435
  _Source: src/model/behaviors-value.ts_
12436
+
12437
+ ### WeekStart
12438
+
12439
+ **Kind:** `type`
12440
+
12441
+ День недели, с которого начинается календарь: `0` — воскресенье, `1` — понедельник.
12442
+
12443
+ **Signature:**
12444
+ ```typescript
12445
+ export type WeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
12446
+ ```
12447
+
12448
+ _Source: src/i18n/locale.ts_