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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/behaviors.js +124 -124
  2. package/dist/create-model-B8U-COv-.js +103 -0
  3. package/dist/en-CjAqn7h0.js +41 -0
  4. package/dist/form/behaviors/internals.d.ts +9 -0
  5. package/dist/form/create-form.d.ts +6 -6
  6. package/dist/form/factories/node-factory.d.ts +16 -16
  7. package/dist/form/nodes/array-node.d.ts +3 -3
  8. package/dist/form/nodes/field-node.d.ts +3 -3
  9. package/dist/form/nodes/group-node.d.ts +7 -7
  10. package/dist/form/nodes/model-array-node.d.ts +2 -2
  11. package/dist/form/types/contracts.d.ts +6 -0
  12. package/dist/form/types/deep-schema.d.ts +5 -5
  13. package/dist/form/types/validation-schema.d.ts +13 -1
  14. package/dist/form/validation/operators.d.ts +6 -1
  15. package/dist/form/validators/validation-error.d.ts +8 -0
  16. package/dist/i18n/descriptor.d.ts +54 -0
  17. package/dist/i18n/en.json.d.ts +45 -0
  18. package/dist/i18n/loader.d.ts +71 -0
  19. package/dist/i18n/locale-store.d.ts +39 -0
  20. package/dist/i18n/locale.d.ts +90 -0
  21. package/dist/i18n/message-format.d.ts +116 -0
  22. package/dist/i18n/resolve-localized.d.ts +17 -0
  23. package/dist/i18n/ru.json.d.ts +45 -0
  24. package/dist/i18n/translator.d.ts +50 -0
  25. package/dist/i18n/validation-message.d.ts +18 -0
  26. package/dist/i18n.d.ts +31 -0
  27. package/dist/i18n.js +303 -0
  28. package/dist/index-BrNkQj1y.js +105 -0
  29. package/dist/index.js +548 -601
  30. package/dist/loader-Dv_Hl7JC.js +216 -0
  31. package/dist/locale/en.d.ts +2 -0
  32. package/dist/locale/en.js +5 -0
  33. package/dist/locale/en.json +42 -0
  34. package/dist/locale/index.d.ts +18 -0
  35. package/dist/locale/ru.d.ts +2 -0
  36. package/dist/locale/ru.js +41 -0
  37. package/dist/locale/ru.json +42 -0
  38. package/dist/locale.d.ts +2 -0
  39. package/dist/locale.js +9 -0
  40. package/dist/model/create-model.d.ts +17 -3
  41. package/dist/model/index.d.ts +3 -3
  42. package/dist/model/model-signals-proxy.d.ts +25 -0
  43. package/dist/model/types.d.ts +19 -5
  44. package/dist/model-signals-proxy-2vqS243J.js +250 -0
  45. package/dist/model.js +18 -16
  46. package/dist/platforms/react/field-binding/field-adapter.d.ts +87 -0
  47. package/dist/platforms/react/field-binding/field-handle.d.ts +50 -0
  48. package/dist/platforms/react/hooks/types.d.ts +6 -3
  49. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  50. package/dist/platforms/react/i18n/context.d.ts +13 -0
  51. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  52. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  53. package/dist/platforms/react/index.d.ts +4 -0
  54. package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
  55. package/dist/validation-error-B-U3RsV0.js +11 -0
  56. package/dist/validation.js +1 -1
  57. package/dist/validators/email.js +5 -8
  58. package/dist/validators/future-date.js +8 -11
  59. package/dist/validators/integer.js +4 -7
  60. package/dist/validators/is-date.js +5 -8
  61. package/dist/validators/is-number.js +4 -7
  62. package/dist/validators/max-age.js +8 -11
  63. package/dist/validators/max-date.js +8 -11
  64. package/dist/validators/max-length.js +5 -8
  65. package/dist/validators/max.js +4 -7
  66. package/dist/validators/min-age.js +8 -11
  67. package/dist/validators/min-date.js +8 -11
  68. package/dist/validators/min-length.js +5 -8
  69. package/dist/validators/min.js +4 -7
  70. package/dist/validators/multiple-of.js +5 -8
  71. package/dist/validators/non-negative.js +4 -7
  72. package/dist/validators/non-zero.js +4 -7
  73. package/dist/validators/past-date.js +8 -11
  74. package/dist/validators/pattern.js +5 -8
  75. package/dist/validators/phone.js +6 -9
  76. package/dist/validators/required.js +4 -7
  77. package/dist/validators/url.js +9 -14
  78. package/dist/validators.js +84 -106
  79. package/docs/llms/02-quick-start.md +7 -7
  80. package/docs/llms/03-api-signatures.md +11 -5
  81. package/docs/llms/05-common-mistakes.md +1 -1
  82. package/docs/llms/09-formschema.md +12 -12
  83. package/docs/llms/10-arrays.md +34 -4
  84. package/docs/llms/14-extended-mistakes.md +1 -1
  85. package/docs/llms/15-project-structure.md +15 -13
  86. package/docs/llms/16-ui-components.md +36 -9
  87. package/docs/llms/17-nonexistent-api.md +16 -2
  88. package/docs/llms/29-async-preload.md +4 -4
  89. package/docs/llms/30-type-safety-recipes.md +1 -1
  90. package/docs/llms/34-i18n.md +272 -0
  91. package/llms-index.json +1 -1
  92. package/llms.txt +1925 -175
  93. package/package.json +22 -3
  94. package/dist/create-model-Dxh9cfLp.js +0 -97
  95. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
@@ -2,12 +2,13 @@
2
2
 
3
3
  > **Default rule (read first)**: для UI используй `FormField` из
4
4
  > [`@reformer/ui-kit`](../../reformer-ui-kit/) — он покрывает 95% случаев одной
5
- > строкой `<FormField control={form.x} />`. Свои field-обёртки пиши ТОЛЬКО если
5
+ > строкой `<FormField control={form.x} />`. Свои обёртки поля пиши ТОЛЬКО если
6
6
  > ui-kit не подходит (другая design system, особый low-level input).
7
7
  >
8
8
  > Канонический schema-driven подход:
9
9
  >
10
- > - **компонент** объявляется в схеме как `component: InputField` (или `Select`, `Checkbox`, etc.)
10
+ > - **компонент** объявляется в схеме как `component: Input` (или `SelectAsync`, `CheckboxWithLabel`, …) —
11
+ > сам компонент кита, отдельных «field-версий» нет: связывает его с полем обёртка (`FormField`)
11
12
  > - **пропсы** компонента — в `componentProps: { label, placeholder, options, type, ... }`
12
13
  > - **JSX рендерит**: `<FormField control={form.x} />` БЕЗ дополнительных props
13
14
  >
@@ -19,7 +20,7 @@
19
20
  ```tsx
20
21
  import { useMemo } from 'react';
21
22
  import { createModel, createForm } from '@reformer/core';
22
- import { FormField, InputField, SelectField, CheckboxField, Button } from '@reformer/ui-kit';
23
+ import { FormField, Input, SelectAsync, CheckboxWithLabel, Button } from '@reformer/ui-kit';
23
24
 
24
25
  type RegistrationForm = {
25
26
  email: string;
@@ -33,12 +34,12 @@ function RegistrationPage() {
33
34
  const schema = {
34
35
  email: {
35
36
  value: model.$.email,
36
- component: InputField,
37
+ component: Input,
37
38
  componentProps: { label: 'Email', type: 'email', placeholder: 'you@example.com' },
38
39
  },
39
40
  country: {
40
41
  value: model.$.country,
41
- component: SelectField,
42
+ component: SelectAsync,
42
43
  componentProps: {
43
44
  label: 'Country',
44
45
  options: [
@@ -49,7 +50,7 @@ function RegistrationPage() {
49
50
  },
50
51
  agree: {
51
52
  value: model.$.agree,
52
- component: CheckboxField,
53
+ component: CheckboxWithLabel,
53
54
  componentProps: { label: 'I agree to terms' },
54
55
  },
55
56
  };
@@ -92,7 +93,7 @@ function RegistrationPage() {
92
93
  ✅ Всё это в схеме:
93
94
 
94
95
  ```ts
95
- { email: { component: InputField, componentProps: { label: 'Email' } } }
96
+ { email: { component: Input, componentProps: { label: 'Email' } } }
96
97
  ```
97
98
 
98
99
  ```tsx
@@ -113,7 +114,8 @@ import { InputMask } from 'react-input-mask';
113
114
  ```
114
115
 
115
116
  `children` оборачивается в `CdkFormField.Control asChild` и получает все нужные
116
- props (`value`, `onChange`, `onBlur`, `aria-invalid`).
117
+ props (`value`, `onChange`, `onBlur`, `aria-invalid`) — в диалекте ребёнка, если тот объявил
118
+ статику `reformerAdapter` (`defineFieldControl` из `@reformer/ui-kit/fields`), иначе value-based.
117
119
 
118
120
  ### Advanced — write your own from scratch (rare)
119
121
 
@@ -155,12 +157,37 @@ function MyFormField<T>({ control }: MyFormFieldProps<T>) {
155
157
  <MyFormField control={form.email} /> // ← без label-prop
156
158
  ```
157
159
 
160
+ ### Связывание поля с контролом — `FieldAdapter` (`@reformer/core`)
161
+
162
+ Контролы говорят на разных диалектах (`checked` + `onCheckedChange`, `onValueChange`, DOM-событие
163
+ в `onChange`), форма — на value-based seam (`value` + `onChange(value)` + `onBlur`). Перевод
164
+ описывает `FieldAdapter` (`valueProp`, `changeProp`, `fromEmit`, `toValue`, `bindBlur`, `strip`,
165
+ `passControl` — все необязательны). Компонент объявляет его статикой `reformerAdapter`, а обёртка
166
+ поля (`FormField.Control` из `@reformer/cdk`, рендерер `@reformer/renderer-react`) читает статику
167
+ и связывает поле сама — отдельные «field-версии» компонентов не нужны.
168
+
169
+ | Символ | Назначение |
170
+ | ----------------------------- | --------------------------------------------------------------------------- |
171
+ | `FieldAdapter` | Тип адаптера: диалект контрола |
172
+ | `getFieldAdapter(component)` | Статика `component.reformerAdapter` либо `undefined` |
173
+ | `bindFieldProps(adapter, seam, props)` | Props контрола под адаптер (без адаптера — seam как есть) |
174
+ | `FieldHandle` | Императивный handle поля: `focus` / `blur` / `scrollIntoView` / `getElement` |
175
+ | `makeElementFieldHandle(ref)` | Базовый `FieldHandle` из DOM-узла |
176
+ | `useFieldHandleRef(outerRef)` | Ref для контрола + публикация handle потребителю (строит ОБЁРТКА поля) |
177
+
178
+ В приложении обычно достаточно `defineFieldControl(MyControl, { adapter })` из
179
+ `@reformer/ui-kit/fields` (плюс готовые пресеты `checkedAdapter`, `valueChangeAdapter`, …); для
180
+ компонентов чужой библиотеки без статики — `RendererSettings.resolveFieldAdapter`.
181
+
158
182
  ### Integration with UI libraries (shadcn etc.)
159
183
 
160
184
  > **Через рендерер — без обёртки.** Если ты рендеришь форму через `@reformer/renderer-react` /
161
185
  > `@reformer/renderer-json`, вместо ручной обёртки на каждый контрол можно зарегистрировать сырой
162
186
  > компонент и передать `settings.resolveFieldAdapter` — рендерер сам сведёт value-seam к диалекту
163
- > контрола. Ручная обёртка на `useFormControl` (ниже) нужна для прямого JSX без рендерера.
187
+ > контрола. Свой компонент можно подготовить и один раз — статикой
188
+ > `defineFieldControl(MyInput, { adapter })` из `@reformer/ui-kit/fields`: его поймут и рендерер,
189
+ > и `FormField.Control` из `@reformer/cdk`. Ручная обёртка на `useFormControl` (ниже) нужна для
190
+ > прямого JSX без `FormField` и рендерера.
164
191
 
165
192
  Если есть существующая design system — оборачивай её компоненты в один
166
193
  `MyFormField` (как выше) и используй один прop `control`. Не множь обёртки на
@@ -26,6 +26,20 @@
26
26
  | `FormProvider`, `control` prop, `register()` | `<Component form={form} />`, `useFormControl(form.field)` | Форма передаётся через props |
27
27
  | `getFieldValue()` | `model.field` / `useFormControlValue(form.field)`| Не существует |
28
28
 
29
+ ### UI: удалённые field-версии компонентов (`@reformer/ui-kit`)
30
+
31
+ Отдельных «field-версий» у компонентов кита больше нет: в `component` поля кладётся сам
32
+ компонент, диалект он объявляет статикой `reformerAdapter`, связывает поле обёртка
33
+ (`FormField.Control` / рендерер).
34
+
35
+ | Wrong | Correct |
36
+ | ---------------------------------------------- | --------------------------------------------------------- |
37
+ | `InputField`, `SelectField`, `CheckboxField`, … (любой `*Field`) | `Input`, `SelectAsync`, `CheckboxWithLabel`, … — таблица в ui-kit `01-overview.md` |
38
+ | `InputField` + `type: 'number'` | `InputNumber` |
39
+ | `InputField` + `suggestions` | `InputSuggest` |
40
+ | `FileUploadField` + `variant: 'dropzone' \| 'input'` | `FileUploadDropzone` / `FileUploadInput` (без `variant`) |
41
+ | `withFormControl(MyControl, adapter)` | `defineFieldControl(MyControl, { adapter })` из `@reformer/ui-kit/fields` |
42
+
29
43
  ### Common Import Errors
30
44
 
31
45
  ```typescript
@@ -64,8 +78,8 @@ const schema = {
64
78
 
65
79
  // CORRECT - layout только связывает поле с сигналом модели...
66
80
  const layout = {
67
- name: { value: model.$.name, component: InputField, componentProps: { label: 'Name' } },
68
- email: { value: model.$.email, component: InputField, componentProps: { label: 'Email' } },
81
+ name: { value: model.$.name, component: Input, componentProps: { label: 'Name' } },
82
+ email: { value: model.$.email, component: Input, componentProps: { label: 'Email' } },
69
83
  };
70
84
 
71
85
  // ...а правила — отдельная ambient-схема (прогоняется раннером validateModel):
@@ -31,16 +31,16 @@ Initial values задаются в `createModel(initial)`; `reset()` возвр
31
31
 
32
32
  ```typescript
33
33
  import { createModel, createForm } from '@reformer/core';
34
- import { InputField, SelectField } from '@reformer/ui-kit';
34
+ import { Input, SelectAsync, CheckboxWithLabel } from '@reformer/ui-kit';
35
35
 
36
36
  type ProfileForm = { username: string; language: 'ru' | 'en'; marketing: boolean };
37
37
 
38
38
  const model = createModel<ProfileForm>({ username: '', language: 'ru', marketing: true });
39
39
  const schema = {
40
- username: { value: model.$.username, component: InputField, componentProps: { label: 'Username' } },
40
+ username: { value: model.$.username, component: Input, componentProps: { label: 'Username' } },
41
41
  language: {
42
42
  value: model.$.language,
43
- component: SelectField,
43
+ component: SelectAsync,
44
44
  componentProps: {
45
45
  label: 'Язык',
46
46
  options: [
@@ -49,7 +49,7 @@ const schema = {
49
49
  ],
50
50
  },
51
51
  },
52
- marketing: { value: model.$.marketing, component: InputField, componentProps: { type: 'checkbox' } },
52
+ marketing: { value: model.$.marketing, component: CheckboxWithLabel, componentProps: { label: 'Рассылка' } },
53
53
  };
54
54
  const form = createForm({ model, schema });
55
55
  // model.get() === { username: '', language: 'ru', marketing: true }
@@ -59,7 +59,7 @@ Layout-схема БЕЗ валидаторов — правила живут в
59
59
  const model = createModel<CreditApplicationForm>(initial);
60
60
 
61
61
  const schema = {
62
- loanAmount: { value: model.$.loanAmount, component: InputField },
62
+ loanAmount: { value: model.$.loanAmount, component: InputNumber },
63
63
  // вложенная группа — builder, принимающий ModelSignals<Sub>
64
64
  personalData: personalDataNodes(model.$.personalData),
65
65
  // массив — { array, item }
@@ -0,0 +1,272 @@
1
+ # i18n — локаль, загрузка словарей и провайдер
2
+
3
+ ## Purpose
4
+
5
+ Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*`: объект
6
+ локали, загрузка словарей, React-провайдер, перевод и форматирование. Язык задаётся провайдером,
7
+ подгружается динамически и переключается на лету; узлы формы при этом не пересоздаются.
8
+
9
+ Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
10
+
11
+ > Импорт всегда из `@reformer/core/i18n`. Из главной бочки `@reformer/core` этот модуль не
12
+ > реэкспортируется: React-контекст локализации обязан существовать в одном экземпляре.
13
+
14
+ ## Локаль — это данные
15
+
16
+ ```typescript
17
+ interface FormLocale {
18
+ code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
19
+ messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
20
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
21
+ dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
22
+ dateFormat?: string; // необязательно: токены date-fns для DatePicker
23
+ }
24
+ ```
25
+
26
+ Функций в локали нет, поэтому её можно положить JSON-файлом на сервер и получить по сети:
27
+
28
+ ```json
29
+ { "code": "ru", "weekStartsOn": 1, "messages": { "profile.title": "Профиль" } }
30
+ ```
31
+
32
+ `extendLocale(base, patch)` возвращает новую локаль: словари сливаются (ключи из `patch` важнее),
33
+ остальные поля заменяются.
34
+
35
+ ## Формат сообщений — подмножество ICU
36
+
37
+ | Конструкция | Пример |
38
+ | ------------------- | ------------------------------------------------------------------------- |
39
+ | подстановка | `Открыт файл {name}` |
40
+ | множественные формы | `{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}` |
41
+ | точное значение | `{count, plural, =0{файлов нет} other{# файлов}}` |
42
+ | выбор | `{gender, select, male{он} female{она} other{оно}}` |
43
+ | экранирование | `'{'` → `{`, `'}'` → `}`, `''` → `'` |
44
+
45
+ - Ветка `other` обязательна. `#` внутри `plural` — число, отформатированное по языку.
46
+ - Типы `number`, `date`, `time`, `selectordinal` не поддерживаются: числа и даты форматируйте
47
+ отдельно (`useI18n().number`, `.date`) и передавайте готовой строкой.
48
+ - Категории (`one`/`few`/`many`) берутся из `Intl.PluralRules` языка локали.
49
+
50
+ ## Загрузка локалей
51
+
52
+ Откуда брать словарь, решает приложение: источник — функция `(code) => локаль | словарь`.
53
+
54
+ ```typescript
55
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
56
+ import { loadCoreLocale } from '@reformer/core/locale';
57
+
58
+ export const loadLocale = createLocaleLoader([
59
+ loadCoreLocale, // встроенные словари ядра — чанк нужного языка
60
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
61
+ ]);
62
+ ```
63
+
64
+ - Источники опрашиваются параллельно, сливаются по порядку: поздний перекрывает ранний.
65
+ - Источник отдаёт локаль целиком (`{ code, messages, … }`) или голый словарь (`{ "a.b": "…" }`);
66
+ `null` означает «для этого языка у меня ничего нет» и ошибкой не считается.
67
+ - Результат кэшируется по коду языка, идущий запрос не дублируется.
68
+ - Отказ любого источника — отказ загрузки. Источник, которому можно промахнуться, оборачивайте:
69
+ `(code) => source(code).catch(() => null)`.
70
+ - `loadLocale.peek(code)` — уже загруженная локаль или `undefined`; `loadLocale.preload(code)` — то
71
+ же, что вызов загрузчика.
72
+
73
+ Ядро отдаёт свои локали в трёх видах:
74
+
75
+ | Вид | Импорт | Для чего |
76
+ | ----------------- | ------------------------------------------- | ----------------------------------------------- |
77
+ | загрузчик | `loadCoreLocale` из `@reformer/core/locale` | основной путь: чанк языка по запросу |
78
+ | JSON-файл | `@reformer/core/locale/ru.json` | выложить на свой сервер, править без пересборки |
79
+ | синхронный модуль | `ru` из `@reformer/core/locale/ru` | SSR и тесты |
80
+
81
+ Встроенные языки ядра — `en` и `ru`. Словарь ядра несёт тексты ошибок валидации
82
+ (`validation.<code>`) и единицы размера файла (`format.fileSize.b|kb|mb|gb|tb`).
83
+
84
+ Пакеты выше отдают локаль накопительно и в тех же трёх видах, поэтому источник встроенных словарей
85
+ в загрузчике всегда один — самого верхнего из используемых пакетов:
86
+
87
+ | Пакеты приложения | Источник | Что в локали |
88
+ | ------------------------------- | -------------------------------------------- | --------------------------------- |
89
+ | только ядро | `loadCoreLocale` из `@reformer/core/locale` | ошибки валидации, размер файла |
90
+ | ядро + `@reformer/cdk` | `loadCdkLocale` из `@reformer/cdk/locale` | то же плюс строки cdk (`cdk.*`) |
91
+ | ядро + cdk + `@reformer/ui-kit` | `loadKitLocale` из `@reformer/ui-kit/locale` | то же плюс подписи кита (`kit.*`) |
92
+
93
+ ## Провайдер
94
+
95
+ ```tsx
96
+ import { I18nProvider } from '@reformer/core/i18n';
97
+
98
+ function App() {
99
+ const [lang, setLang] = useState('ru');
100
+ return (
101
+ <I18nProvider lang={lang} load={loadLocale}>
102
+ <LangSwitch value={lang} onChange={setLang} />
103
+ <CreditForm />
104
+ </I18nProvider>
105
+ );
106
+ }
107
+ ```
108
+
109
+ | Проп | Назначение |
110
+ | --------------- | ------------------------------------------------------------------------ |
111
+ | `lang` + `load` | провайдер грузит локаль сам |
112
+ | `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
113
+ | `onError` | отказ загрузки `(error, lang)` |
114
+ | `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
115
+
116
+ Поведение при `lang` + `load`:
117
+
118
+ - при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
119
+ `useI18n().pending` — для индикатора;
120
+ - при быстром переключении применяется только последний запрос;
121
+ - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
122
+ - если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
123
+ синхронный и `fallback` не показывается.
124
+
125
+ SSR: эффекты на сервере не выполняются, поэтому там передавайте готовый объект —
126
+ `<I18nProvider locale={ru}>` — либо заранее вызовите `preload`.
127
+
128
+ Вложенный провайдер заменяет локаль для своего поддерева целиком.
129
+
130
+ ## Хуки
131
+
132
+ ```typescript
133
+ const { code, locale, pending, error, t, has, number, date, fileSize } = useI18n();
134
+
135
+ t('profile.title'); // ключ приложения
136
+ t('cart.items', { count: 3 }); // с подстановками
137
+ t('cart.empty', undefined, 'Your cart is empty'); // английский текст на случай промаха
138
+ number(1234.5); // '1 234,5' в ru
139
+ date(new Date(), { dateStyle: 'long' }); // '15 января 2024 г.' в ru
140
+ fileSize(1536); // '1,5 КБ' в ru, '1.5 KB' без провайдера
141
+ ```
142
+
143
+ `useMessages(builtinEn)` — для авторов пакетов и китов: ищет ключ в локали провайдера, а при
144
+ промахе берёт встроенную английскую таблицу пакета. Тип ключа выводится из таблицы.
145
+
146
+ ```tsx
147
+ import en from './locale/en.json';
148
+
149
+ function ClearButton() {
150
+ const t = useMessages(en);
151
+ return <button aria-label={t('kit.select.clear')} />;
152
+ }
153
+ ```
154
+
155
+ ## Тексты ошибок валидации
156
+
157
+ Правила из `@reformer/core/validators` несут только `code` и `params`; готового текста у них нет
158
+ (`message` по умолчанию — пустая строка). Строку даёт резолвер при отображении ошибки:
159
+
160
+ 1. `messageKey` автора правила — если такой ключ есть в активной локали;
161
+ 2. явное `message` автора правила;
162
+ 3. `validation.<code>` из словаря локали;
163
+ 4. `validation.<code>` из встроенной английской таблицы ядра;
164
+ 5. сам код — для своих кодов, которых нет ни в одном словаре.
165
+
166
+ ```typescript
167
+ validate(model.$.email, [
168
+ required(), // «Обязательное поле» в ru, английская фраза без провайдера
169
+ minLength(5, { message: 'Слишком коротко' }), // явный текст: важнее словаря, но языком не переключается
170
+ email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
171
+ ]);
172
+ ```
173
+
174
+ - Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
175
+ ошибки без повторной валидации.
176
+ - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит ошибку сам. В своём компоненте —
177
+ `useValidationMessage()`; вне React — `resolveValidationError(error, createI18n(locale))`.
178
+ - Свой код ошибки локализуется ключом `validation.<свой_код>` в словаре приложения либо
179
+ `messageKey` в самом правиле.
180
+ - Встроенный текст меняется тем же ключом в словаре приложения:
181
+ `extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
182
+ - Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
183
+ размеры в байтах (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) —
184
+ строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
185
+
186
+ ```tsx
187
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
188
+ const message = useValidationMessage();
189
+ const { errors, shouldShowError } = useFormControl(control);
190
+ return shouldShowError ? <span role="alert">{message(errors[0])}</span> : null;
191
+ }
192
+ ```
193
+
194
+ Смонтированный `ValidationMessagesProvider` из `@reformer/cdk` заменяет этот резолвер целиком:
195
+ локаль для ошибок в его поддереве не участвует.
196
+
197
+ ## Что показывается, когда ключа нет
198
+
199
+ | Ключ | Нет в локали | Сообщение не разбирается |
200
+ | -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
201
+ | пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
202
+ | приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
203
+
204
+ Пропущенная подстановка выводится как `{name}`. Форма не падает ни в одном из случаев: неполный
205
+ словарь, пришедший по сети, — законный сценарий.
206
+
207
+ ## Проверка словаря в тестах
208
+
209
+ ```typescript
210
+ import { validateLocale } from '@reformer/core/i18n';
211
+
212
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
213
+ // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
214
+ ```
215
+
216
+ ## Описатели сообщений (низкий уровень)
217
+
218
+ `msg(key, values?, defaultMessage?)` создаёт описатель сообщения, `defineMessages(appEn)` — `msg` с
219
+ типизированными ключами, `resolveLocalized(value, i18n)` раскрывает описатели в объекте пропсов.
220
+
221
+ > ⚠️ Рендереры и `FormField` описатели сами пока НЕ раскрывают. Не кладите `msg(...)` в
222
+ > `componentProps` схемы: компонент получит объект вместо строки. До появления раскрытия в
223
+ > рендерерах вызывайте `resolveLocalized` вручную или используйте `useI18n().t`.
224
+
225
+ ## Anti-patterns
226
+
227
+ ```typescript
228
+ // ❌ Импорт из главной бочки — i18n там нет, а второй путь импорта дал бы вторую копию контекста
229
+ import { I18nProvider } from '@reformer/core';
230
+
231
+ // ✅ Единственный путь импорта — подпуть i18n
232
+ import { I18nProvider } from '@reformer/core/i18n';
233
+ ```
234
+
235
+ ```tsx
236
+ // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль грузится заново, первый кадр — fallback
237
+ <I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
238
+
239
+ // ✅ Загрузчик — константа модуля: один кэш на приложение
240
+ const loadLocale = createLocaleLoader([loadCoreLocale]);
241
+ <I18nProvider lang={lang} load={loadLocale}>
242
+ ```
243
+
244
+ ```typescript
245
+ // ❌ Число или дата типами ICU внутри сообщения — не поддерживается, сообщение не разберётся
246
+ const message = 'Итого: {total, number}';
247
+
248
+ // ✅ Отформатировать отдельно и передать готовой строкой
249
+ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) });
250
+ ```
251
+
252
+ ```tsx
253
+ // ❌ Ошибка выводится полем message: у правил без своего текста оно пустое — на экране ничего
254
+ <span>{errors[0].message}</span>;
255
+
256
+ // ✅ Текст ошибки — через резолвер: он знает словарь локали, messageKey и явное сообщение
257
+ const message = useValidationMessage();
258
+ <span>{message(errors[0])}</span>;
259
+ ```
260
+
261
+ ```typescript
262
+ // ❌ Настройки локали без словаря: объект без поля messages принимается за словарь сообщений
263
+ const source = () => ({ weekStartsOn: 1 });
264
+
265
+ // ✅ Часть локали узнаётся по полю messages — отдавайте его хотя бы пустым
266
+ const source = () => ({ weekStartsOn: 1, messages: {} });
267
+ ```
268
+
269
+ ## See also
270
+
271
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
272
+ - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация