@reformer/core 11.0.0-beta.6 → 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 (76) 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/types/contracts.d.ts +6 -0
  6. package/dist/form/types/validation-schema.d.ts +13 -1
  7. package/dist/form/validation/operators.d.ts +6 -1
  8. package/dist/form/validators/validation-error.d.ts +8 -0
  9. package/dist/i18n/descriptor.d.ts +54 -0
  10. package/dist/i18n/en.json.d.ts +45 -0
  11. package/dist/i18n/loader.d.ts +71 -0
  12. package/dist/i18n/locale-store.d.ts +39 -0
  13. package/dist/i18n/locale.d.ts +90 -0
  14. package/dist/i18n/message-format.d.ts +116 -0
  15. package/dist/i18n/resolve-localized.d.ts +17 -0
  16. package/dist/i18n/ru.json.d.ts +45 -0
  17. package/dist/i18n/translator.d.ts +50 -0
  18. package/dist/i18n/validation-message.d.ts +18 -0
  19. package/dist/i18n.d.ts +31 -0
  20. package/dist/i18n.js +303 -0
  21. package/dist/index-BrNkQj1y.js +105 -0
  22. package/dist/index.js +639 -736
  23. package/dist/loader-Dv_Hl7JC.js +216 -0
  24. package/dist/locale/en.d.ts +2 -0
  25. package/dist/locale/en.js +5 -0
  26. package/dist/locale/en.json +42 -0
  27. package/dist/locale/index.d.ts +18 -0
  28. package/dist/locale/ru.d.ts +2 -0
  29. package/dist/locale/ru.js +41 -0
  30. package/dist/locale/ru.json +42 -0
  31. package/dist/locale.d.ts +2 -0
  32. package/dist/locale.js +9 -0
  33. package/dist/model/create-model.d.ts +17 -3
  34. package/dist/model/index.d.ts +3 -3
  35. package/dist/model/model-signals-proxy.d.ts +25 -0
  36. package/dist/model/types.d.ts +19 -5
  37. package/dist/model-signals-proxy-2vqS243J.js +250 -0
  38. package/dist/model.js +18 -16
  39. package/dist/platforms/react/hooks/types.d.ts +6 -3
  40. package/dist/platforms/react/hooks/useFormControlValue.d.ts +2 -1
  41. package/dist/platforms/react/i18n/context.d.ts +13 -0
  42. package/dist/platforms/react/i18n/hooks.d.ts +59 -0
  43. package/dist/platforms/react/i18n/provider.d.ts +52 -0
  44. package/dist/{strategy-DA3JwxsR.js → strategy-BXS_gBfz.js} +7 -7
  45. package/dist/validation-error-B-U3RsV0.js +11 -0
  46. package/dist/validation.js +1 -1
  47. package/dist/validators/email.js +5 -8
  48. package/dist/validators/future-date.js +8 -11
  49. package/dist/validators/integer.js +4 -7
  50. package/dist/validators/is-date.js +5 -8
  51. package/dist/validators/is-number.js +4 -7
  52. package/dist/validators/max-age.js +8 -11
  53. package/dist/validators/max-date.js +8 -11
  54. package/dist/validators/max-length.js +5 -8
  55. package/dist/validators/max.js +4 -7
  56. package/dist/validators/min-age.js +8 -11
  57. package/dist/validators/min-date.js +8 -11
  58. package/dist/validators/min-length.js +5 -8
  59. package/dist/validators/min.js +4 -7
  60. package/dist/validators/multiple-of.js +5 -8
  61. package/dist/validators/non-negative.js +4 -7
  62. package/dist/validators/non-zero.js +4 -7
  63. package/dist/validators/past-date.js +8 -11
  64. package/dist/validators/pattern.js +5 -8
  65. package/dist/validators/phone.js +6 -9
  66. package/dist/validators/required.js +4 -7
  67. package/dist/validators/url.js +9 -14
  68. package/dist/validators.js +84 -106
  69. package/docs/llms/03-api-signatures.md +6 -0
  70. package/docs/llms/10-arrays.md +30 -0
  71. package/docs/llms/34-i18n.md +272 -0
  72. package/llms-index.json +1 -1
  73. package/llms.txt +1589 -98
  74. package/package.json +22 -3
  75. package/dist/create-model-Dxh9cfLp.js +0 -97
  76. package/dist/model-signals-proxy-mV3WGdlO.js +0 -233
package/llms.txt CHANGED
@@ -38,6 +38,7 @@
38
38
  - 31-async-validator-debounce.md — async-validator-debounce
39
39
  - 32-async-options-loading.md — async-options-loading
40
40
  - 33-validation-strategy.md — useFormValidation — Единый выбор стратегии валидации
41
+ - 34-i18n.md — i18n — локаль, загрузка словарей и провайдер
41
42
  - API Reference (auto-generated from JSDoc)
42
43
 
43
44
  ## 1. 1. API Reference
@@ -320,6 +321,12 @@ minAge(years: number, options?: { message?: string })
320
321
  maxAge(years: number, options?: { message?: string })
321
322
  ```
322
323
 
324
+ Кроме `message`, каждое правило принимает `messageKey` (ключ словаря приложения) и `params`.
325
+ Без `message` правило кладёт в ошибку пустую строку, а текст при отображении берётся из словаря
326
+ локали по коду (`validation.<code>`): «Обязательное поле» под русской локалью, английская фраза без
327
+ провайдера. Явное `message` важнее словаря, но языком не переключается; для локализуемого текста —
328
+ `messageKey`. Подробно — [34-i18n.md](./34-i18n.md).
329
+
323
330
  Использование: layout-дерево `createForm` привязывает поля к сигналам, правила живут отдельной
324
331
  `ValidationSchema` и гоняются раннером `validateModel` по требованию.
325
332
 
@@ -1258,6 +1265,36 @@ const form = createForm<MyForm>({ model, schema });
1258
1265
  > он не совместим с `Record<string, FormValue>` и `ArrayNode<Item>` его отвергнет.
1259
1266
  > См. `30-type-safety-recipes.md`.
1260
1267
 
1268
+ ### Массив как ОДНО значение поля
1269
+
1270
+ Массив бывает не набором под-форм, а одним значением: мультивыбор, теги, список файлов. Чем он
1271
+ является, решает **схема**, а не данные: `{ array, item }` — набор под-форм, а узел поля
1272
+ `{ value: model.$.<путь>, component }` — одно значение. Массив, к которому в схеме ничего не
1273
+ привязано, в форму не попадает.
1274
+
1275
+ ```typescript
1276
+ type MyForm = { tags: string[] };
1277
+
1278
+ const model = createModel<MyForm>({ tags: [] });
1279
+
1280
+ const schema = {
1281
+ tags: { value: model.$.tags, component: SelectMulti }, // поле над массивом целиком
1282
+ };
1283
+ const form = createForm<MyForm>({ model, schema });
1284
+
1285
+ form.tags.setValue(['a', 'b']); // FieldNode<string[]>
1286
+ model.$.tags.value = ['c']; // ручка значения: заменяет массив целиком
1287
+ validate(model.$.tags, [required(), maxLength(3)]); // правило получает массив
1288
+ ```
1289
+
1290
+ - `model.$.<массив>` — записываемая ручка значения (`PathAwareSignal`): подходит для `value:` в
1291
+ схеме, `validate`/`cross`, `enableWhen`/`copyFrom` и для `model.signalAt(path)`.
1292
+ - Узел-массив хранит только массив: запись `null`/`undefined` даёт `[]`. Нужно отличать «не
1293
+ выбирали» от «выбрали ничего» — объявляй поле `T[] | null` с начальным `null`: в рантайме это
1294
+ лист, и привязывается он так же.
1295
+ - Запись в узел-ГРУППУ (`model.$.<группа>.value = …`) — ошибка: группа пишется по полям или через
1296
+ `model.patch(...)`.
1297
+
1261
1298
  ### Один массив — три слоя (три разных движка)
1262
1299
 
1263
1300
  Одна и та же коллекция описывается **тремя разными формами** — по одной на движок. Их легко
@@ -4465,7 +4502,280 @@ if (typeof window !== 'undefined') ctrl.start();
4465
4502
  - [31-async-validator-debounce.md](./31-async-validator-debounce.md) — async-правила и отмена устаревших прогонов
4466
4503
  - [13-multi-step.md](./13-multi-step.md) — per-step / полная валидация wizard (`validateStep` / `validateAll`)
4467
4504
 
4468
- ## 82. API Reference
4505
+ ## 82. Purpose
4506
+
4507
+ **i18n — локаль, загрузка словарей и провайдер**
4508
+
4509
+ Подпуть `@reformer/core/i18n` — общий рантайм локализации для всех пакетов `@reformer/*`: объект
4510
+ локали, загрузка словарей, React-провайдер, перевод и форматирование. Язык задаётся провайдером,
4511
+ подгружается динамически и переключается на лету; узлы формы при этом не пересоздаются.
4512
+
4513
+ Без провайдера всё работает на английском: у каждого пакета есть встроенная английская таблица.
4514
+
4515
+ > Импорт всегда из `@reformer/core/i18n`. Из главной бочки `@reformer/core` этот модуль не
4516
+ > реэкспортируется: React-контекст локализации обязан существовать в одном экземпляре.
4517
+
4518
+ ## 83. Локаль — это данные
4519
+
4520
+ ```typescript
4521
+ interface FormLocale {
4522
+ code: string; // BCP 47: 'en', 'ru', 'pt-BR' — по нему работают Intl и plural
4523
+ messages: Record<string, string>; // плоский словарь: ключ → сообщение ICU
4524
+ weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
4525
+ dateLocale?: unknown; // необязательно: объект локали react-day-picker / date-fns
4526
+ dateFormat?: string; // необязательно: токены date-fns для DatePicker
4527
+ }
4528
+ ```
4529
+
4530
+ Функций в локали нет, поэтому её можно положить JSON-файлом на сервер и получить по сети:
4531
+
4532
+ ```json
4533
+ { "code": "ru", "weekStartsOn": 1, "messages": { "profile.title": "Профиль" } }
4534
+ ```
4535
+
4536
+ `extendLocale(base, patch)` возвращает новую локаль: словари сливаются (ключи из `patch` важнее),
4537
+ остальные поля заменяются.
4538
+
4539
+ ## 84. Формат сообщений — подмножество ICU
4540
+
4541
+ | Конструкция | Пример |
4542
+ | ------------------- | ------------------------------------------------------------------------- |
4543
+ | подстановка | `Открыт файл {name}` |
4544
+ | множественные формы | `{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}` |
4545
+ | точное значение | `{count, plural, =0{файлов нет} other{# файлов}}` |
4546
+ | выбор | `{gender, select, male{он} female{она} other{оно}}` |
4547
+ | экранирование | `'{'` → `{`, `'}'` → `}`, `''` → `'` |
4548
+
4549
+ - Ветка `other` обязательна. `#` внутри `plural` — число, отформатированное по языку.
4550
+ - Типы `number`, `date`, `time`, `selectordinal` не поддерживаются: числа и даты форматируйте
4551
+ отдельно (`useI18n().number`, `.date`) и передавайте готовой строкой.
4552
+ - Категории (`one`/`few`/`many`) берутся из `Intl.PluralRules` языка локали.
4553
+
4554
+ ## 85. Загрузка локалей
4555
+
4556
+ Откуда брать словарь, решает приложение: источник — функция `(code) => локаль | словарь`.
4557
+
4558
+ ```typescript
4559
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
4560
+ import { loadCoreLocale } from '@reformer/core/locale';
4561
+
4562
+ export const loadLocale = createLocaleLoader([
4563
+ loadCoreLocale, // встроенные словари ядра — чанк нужного языка
4564
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
4565
+ ]);
4566
+ ```
4567
+
4568
+ - Источники опрашиваются параллельно, сливаются по порядку: поздний перекрывает ранний.
4569
+ - Источник отдаёт локаль целиком (`{ code, messages, … }`) или голый словарь (`{ "a.b": "…" }`);
4570
+ `null` означает «для этого языка у меня ничего нет» и ошибкой не считается.
4571
+ - Результат кэшируется по коду языка, идущий запрос не дублируется.
4572
+ - Отказ любого источника — отказ загрузки. Источник, которому можно промахнуться, оборачивайте:
4573
+ `(code) => source(code).catch(() => null)`.
4574
+ - `loadLocale.peek(code)` — уже загруженная локаль или `undefined`; `loadLocale.preload(code)` — то
4575
+ же, что вызов загрузчика.
4576
+
4577
+ Ядро отдаёт свои локали в трёх видах:
4578
+
4579
+ | Вид | Импорт | Для чего |
4580
+ | ----------------- | ------------------------------------------- | ----------------------------------------------- |
4581
+ | загрузчик | `loadCoreLocale` из `@reformer/core/locale` | основной путь: чанк языка по запросу |
4582
+ | JSON-файл | `@reformer/core/locale/ru.json` | выложить на свой сервер, править без пересборки |
4583
+ | синхронный модуль | `ru` из `@reformer/core/locale/ru` | SSR и тесты |
4584
+
4585
+ Встроенные языки ядра — `en` и `ru`. Словарь ядра несёт тексты ошибок валидации
4586
+ (`validation.<code>`) и единицы размера файла (`format.fileSize.b|kb|mb|gb|tb`).
4587
+
4588
+ Пакеты выше отдают локаль накопительно и в тех же трёх видах, поэтому источник встроенных словарей
4589
+ в загрузчике всегда один — самого верхнего из используемых пакетов:
4590
+
4591
+ | Пакеты приложения | Источник | Что в локали |
4592
+ | ------------------------------- | -------------------------------------------- | --------------------------------- |
4593
+ | только ядро | `loadCoreLocale` из `@reformer/core/locale` | ошибки валидации, размер файла |
4594
+ | ядро + `@reformer/cdk` | `loadCdkLocale` из `@reformer/cdk/locale` | то же плюс строки cdk (`cdk.*`) |
4595
+ | ядро + cdk + `@reformer/ui-kit` | `loadKitLocale` из `@reformer/ui-kit/locale` | то же плюс подписи кита (`kit.*`) |
4596
+
4597
+ ## 86. Провайдер
4598
+
4599
+ ```tsx
4600
+ import { I18nProvider } from '@reformer/core/i18n';
4601
+
4602
+ function App() {
4603
+ const [lang, setLang] = useState('ru');
4604
+ return (
4605
+ <I18nProvider lang={lang} load={loadLocale}>
4606
+ <LangSwitch value={lang} onChange={setLang} />
4607
+ <CreditForm />
4608
+ </I18nProvider>
4609
+ );
4610
+ }
4611
+ ```
4612
+
4613
+ | Проп | Назначение |
4614
+ | --------------- | ------------------------------------------------------------------------ |
4615
+ | `lang` + `load` | провайдер грузит локаль сам |
4616
+ | `fallback` | что показать, пока не загружена самая первая локаль; по умолчанию ничего |
4617
+ | `onError` | отказ загрузки `(error, lang)` |
4618
+ | `locale` | готовый объект вместо `lang` + `load` — SSR, тесты |
4619
+
4620
+ Поведение при `lang` + `load`:
4621
+
4622
+ - при смене `lang` на экране остаётся прежний язык, пока новый не загрузится;
4623
+ `useI18n().pending` — для индикатора;
4624
+ - при быстром переключении применяется только последний запрос;
4625
+ - ошибка загрузки язык не меняет и попадает в `useI18n().error` и `onError`;
4626
+ - если локаль уже в кэше (`await loadLocale.preload(lang)` до монтирования), первый рендер
4627
+ синхронный и `fallback` не показывается.
4628
+
4629
+ SSR: эффекты на сервере не выполняются, поэтому там передавайте готовый объект —
4630
+ `<I18nProvider locale={ru}>` — либо заранее вызовите `preload`.
4631
+
4632
+ Вложенный провайдер заменяет локаль для своего поддерева целиком.
4633
+
4634
+ ## 87. Хуки
4635
+
4636
+ ```typescript
4637
+ const { code, locale, pending, error, t, has, number, date, fileSize } = useI18n();
4638
+
4639
+ t('profile.title'); // ключ приложения
4640
+ t('cart.items', { count: 3 }); // с подстановками
4641
+ t('cart.empty', undefined, 'Your cart is empty'); // английский текст на случай промаха
4642
+ number(1234.5); // '1 234,5' в ru
4643
+ date(new Date(), { dateStyle: 'long' }); // '15 января 2024 г.' в ru
4644
+ fileSize(1536); // '1,5 КБ' в ru, '1.5 KB' без провайдера
4645
+ ```
4646
+
4647
+ `useMessages(builtinEn)` — для авторов пакетов и китов: ищет ключ в локали провайдера, а при
4648
+ промахе берёт встроенную английскую таблицу пакета. Тип ключа выводится из таблицы.
4649
+
4650
+ ```tsx
4651
+ import en from './locale/en.json';
4652
+
4653
+ function ClearButton() {
4654
+ const t = useMessages(en);
4655
+ return <button aria-label={t('kit.select.clear')} />;
4656
+ }
4657
+ ```
4658
+
4659
+ ## 88. Тексты ошибок валидации
4660
+
4661
+ Правила из `@reformer/core/validators` несут только `code` и `params`; готового текста у них нет
4662
+ (`message` по умолчанию — пустая строка). Строку даёт резолвер при отображении ошибки:
4663
+
4664
+ 1. `messageKey` автора правила — если такой ключ есть в активной локали;
4665
+ 2. явное `message` автора правила;
4666
+ 3. `validation.<code>` из словаря локали;
4667
+ 4. `validation.<code>` из встроенной английской таблицы ядра;
4668
+ 5. сам код — для своих кодов, которых нет ни в одном словаре.
4669
+
4670
+ ```typescript
4671
+ validate(model.$.email, [
4672
+ required(), // «Обязательное поле» в ru, английская фраза без провайдера
4673
+ minLength(5, { message: 'Слишком коротко' }), // явный текст: важнее словаря, но языком не переключается
4674
+ email({ messageKey: 'profile.email.invalid' }), // ключ словаря приложения: переключается вместе с языком
4675
+ ]);
4676
+ ```
4677
+
4678
+ - Текст получается при отображении, а не при проверке: смена языка переводит уже показанные
4679
+ ошибки без повторной валидации.
4680
+ - `FormField` (`@reformer/cdk`, `@reformer/ui-kit`) резолвит ошибку сам. В своём компоненте —
4681
+ `useValidationMessage()`; вне React — `resolveValidationError(error, createI18n(locale))`.
4682
+ - Свой код ошибки локализуется ключом `validation.<свой_код>` в словаре приложения либо
4683
+ `messageKey` в самом правиле.
4684
+ - Встроенный текст меняется тем же ключом в словаре приложения:
4685
+ `extendLocale(ru, { messages: { 'validation.required': 'Заполните поле' } })`.
4686
+ - Параметры готовятся до подстановки: `Date` — по языку (`15 янв. 2030 г.`), массив — перечнем,
4687
+ размеры в байтах (`maxFileSize`, `minFileSize`, `actualSize`, `maxTotalFileSize`, `actualTotal`) —
4688
+ строкой с единицей (`5 МБ`). Числа остаются числами, поэтому в сообщении работает `plural`.
4689
+
4690
+ ```tsx
4691
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
4692
+ const message = useValidationMessage();
4693
+ const { errors, shouldShowError } = useFormControl(control);
4694
+ return shouldShowError ? <span role="alert">{message(errors[0])}</span> : null;
4695
+ }
4696
+ ```
4697
+
4698
+ Смонтированный `ValidationMessagesProvider` из `@reformer/cdk` заменяет этот резолвер целиком:
4699
+ локаль для ошибок в его поддереве не участвует.
4700
+
4701
+ ## 89. Что показывается, когда ключа нет
4702
+
4703
+ | Ключ | Нет в локали | Сообщение не разбирается |
4704
+ | -------------------------- | --------------------------------------------------------------- | ----------------------------------------------------- |
4705
+ | пакета (`useMessages`) | встроенный английский пакета | встроенный английский, `console.error` вне production |
4706
+ | приложения (`useI18n().t`) | `defaultMessage`, затем сам ключ; `console.warn` вне production | то же, `console.error` |
4707
+
4708
+ Пропущенная подстановка выводится как `{name}`. Форма не падает ни в одном из случаев: неполный
4709
+ словарь, пришедший по сети, — законный сценарий.
4710
+
4711
+ ## 90. Проверка словаря в тестах
4712
+
4713
+ ```typescript
4714
+ import { validateLocale } from '@reformer/core/i18n';
4715
+
4716
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
4717
+ // находит: syntax (не разбирается), missing / extra (ключи), arguments (потерянная подстановка)
4718
+ ```
4719
+
4720
+ ## 91. Описатели сообщений (низкий уровень)
4721
+
4722
+ `msg(key, values?, defaultMessage?)` создаёт описатель сообщения, `defineMessages(appEn)` — `msg` с
4723
+ типизированными ключами, `resolveLocalized(value, i18n)` раскрывает описатели в объекте пропсов.
4724
+
4725
+ > ⚠️ Рендереры и `FormField` описатели сами пока НЕ раскрывают. Не кладите `msg(...)` в
4726
+ > `componentProps` схемы: компонент получит объект вместо строки. До появления раскрытия в
4727
+ > рендерерах вызывайте `resolveLocalized` вручную или используйте `useI18n().t`.
4728
+
4729
+ ## 92. Anti-patterns
4730
+
4731
+ ```typescript
4732
+ // ❌ Импорт из главной бочки — i18n там нет, а второй путь импорта дал бы вторую копию контекста
4733
+ import { I18nProvider } from '@reformer/core';
4734
+
4735
+ // ✅ Единственный путь импорта — подпуть i18n
4736
+ import { I18nProvider } from '@reformer/core/i18n';
4737
+ ```
4738
+
4739
+ ```tsx
4740
+ // ❌ Новый загрузчик на каждый рендер: кэш теряется, локаль грузится заново, первый кадр — fallback
4741
+ <I18nProvider lang={lang} load={createLocaleLoader([loadCoreLocale])}>
4742
+
4743
+ // ✅ Загрузчик — константа модуля: один кэш на приложение
4744
+ const loadLocale = createLocaleLoader([loadCoreLocale]);
4745
+ <I18nProvider lang={lang} load={loadLocale}>
4746
+ ```
4747
+
4748
+ ```typescript
4749
+ // ❌ Число или дата типами ICU внутри сообщения — не поддерживается, сообщение не разберётся
4750
+ const message = 'Итого: {total, number}';
4751
+
4752
+ // ✅ Отформатировать отдельно и передать готовой строкой
4753
+ t('cart.total', { total: number(total, { style: 'currency', currency: 'RUB' }) });
4754
+ ```
4755
+
4756
+ ```tsx
4757
+ // ❌ Ошибка выводится полем message: у правил без своего текста оно пустое — на экране ничего
4758
+ <span>{errors[0].message}</span>;
4759
+
4760
+ // ✅ Текст ошибки — через резолвер: он знает словарь локали, messageKey и явное сообщение
4761
+ const message = useValidationMessage();
4762
+ <span>{message(errors[0])}</span>;
4763
+ ```
4764
+
4765
+ ```typescript
4766
+ // ❌ Настройки локали без словаря: объект без поля messages принимается за словарь сообщений
4767
+ const source = () => ({ weekStartsOn: 1 });
4768
+
4769
+ // ✅ Часть локали узнаётся по полю messages — отдавайте его хотя бы пустым
4770
+ const source = () => ({ weekStartsOn: 1, messages: {} });
4771
+ ```
4772
+
4773
+ ## 93. See also
4774
+
4775
+ - [28-submit-and-reset.md](./28-submit-and-reset.md) — жизненный цикл формы; смена языка его не затрагивает
4776
+ - [33-validation-strategy.md](./33-validation-strategy.md) — когда запускается валидация
4777
+
4778
+ ## 94. API Reference
4469
4779
 
4470
4780
  _Auto-generated from JSDoc on public exports._
4471
4781
 
@@ -4932,6 +5242,35 @@ await v!.validateAll(); // все шаги + extras
4932
5242
 
4933
5243
  _Source: src/form/validation/config.ts_
4934
5244
 
5245
+ ### bundledLocaleSource
5246
+
5247
+ **Kind:** `function`
5248
+
5249
+ Источник встроенных локалей пакета: по чанку на язык. Язык ищется точно, затем по основной части
5250
+ кода (`ru-RU` → `ru`). Языка нет — источник отдаёт `null`: подписи пакета останутся английскими,
5251
+ пока приложение не даст их своим источником.
5252
+
5253
+ **Signature:**
5254
+ ```typescript
5255
+ export function bundledLocaleSource(
5256
+ loaders: Readonly<Record<string, () => Promise<FormLocale>>>
5257
+ ): LocaleSource
5258
+ ```
5259
+
5260
+ **Parameters:**
5261
+ - `loaders` — - Код языка → ленивый импорт его локали.
5262
+
5263
+ **Examples:**
5264
+
5265
+ ```ts
5266
+ export const loadKitLocale = bundledLocaleSource({
5267
+ en: () => import('./en').then((m) => m.en),
5268
+ ru: () => import('./ru').then((m) => m.ru),
5269
+ });
5270
+ ```
5271
+
5272
+ _Source: src/i18n/loader.ts_
5273
+
4935
5274
  ### ChangeContext
4936
5275
 
4937
5276
  **Kind:** `interface`
@@ -5049,6 +5388,19 @@ copyFrom(model.$.email, model.$.emailAdditional, { when: () => model.sameEmail }
5049
5388
 
5050
5389
  _Source: src/model/behaviors-value.ts_
5051
5390
 
5391
+ ### CORE_LOCALES
5392
+
5393
+ **Kind:** `const`
5394
+
5395
+ Языки, которые ядро поставляет само.
5396
+
5397
+ **Signature:**
5398
+ ```typescript
5399
+ export const CORE_LOCALES: readonly string[]
5400
+ ```
5401
+
5402
+ _Source: src/locale/index.ts_
5403
+
5052
5404
  ### CORE_RUNTIME_TOKEN
5053
5405
 
5054
5406
  **Kind:** `const`
@@ -5359,6 +5711,122 @@ dispose();
5359
5711
 
5360
5712
  _Source: src/form/validation/strategy.ts_
5361
5713
 
5714
+ ### createI18n
5715
+
5716
+ **Kind:** `function`
5717
+
5718
+ Ручка для локали. Для одного и того же объекта локали возвращается один и тот же объект —
5719
+ ручку можно держать в зависимостях хуков.
5720
+
5721
+ **Signature:**
5722
+ ```typescript
5723
+ export function createI18n(locale: FormLocale): I18nHandle
5724
+ ```
5725
+
5726
+ **Parameters:**
5727
+ - `locale` — - Локаль; см. {@link FormLocale}.
5728
+
5729
+ **Examples:**
5730
+
5731
+ ```ts
5732
+ const i18n = createI18n({ code: 'ru', messages: { 'cart.items': '{count, plural, one{# товар} few{# товара} other{# товаров}}' } });
5733
+ i18n.t('cart.items', { count: 3 }); // '3 товара'
5734
+ i18n.fileSize(1536); // '1,5 КБ'
5735
+ ```
5736
+
5737
+ _Source: src/i18n/translator.ts_
5738
+
5739
+ ### createLocaleLoader
5740
+
5741
+ **Kind:** `function`
5742
+
5743
+ Собирает загрузчик из источников. Источники опрашиваются параллельно, а сливаются по порядку:
5744
+ поздний перекрывает ранний. Результат кэшируется по коду языка, идущий запрос не дублируется.
5745
+ Отказ любого источника — отказ загрузки; из кэша такой запрос удаляется, и следующая попытка
5746
+ пойдёт заново. Источник, которому можно промахнуться, приложение оборачивает само:
5747
+ `(code) => source(code).catch(() => null)`.
5748
+
5749
+ Код языка у результата — всегда запрошенный: по нему работает `Intl`, и он не должен зависеть
5750
+ от того, что написано внутри файла.
5751
+
5752
+ **Signature:**
5753
+ ```typescript
5754
+ export function createLocaleLoader(sources: readonly LocaleSource[]): LocaleLoader
5755
+ ```
5756
+
5757
+ **Parameters:**
5758
+ - `sources` — - Источники в порядке возрастания приоритета.
5759
+
5760
+ **Examples:**
5761
+
5762
+ ```ts
5763
+ import { createLocaleLoader, fetchMessages } from '@reformer/core/i18n';
5764
+ import { loadKitLocale } from '@reformer/ui-kit/locale';
5765
+
5766
+ export const loadLocale = createLocaleLoader([
5767
+ loadKitLocale, // подписи кита — чанк нужного языка
5768
+ fetchMessages((code) => `/locales/${code}.json`), // ключи приложения и правки — с сервера
5769
+ ]);
5770
+ ```
5771
+
5772
+ _Source: src/i18n/loader.ts_
5773
+
5774
+ ### createLocaleStore
5775
+
5776
+ **Kind:** `function`
5777
+
5778
+ Создаёт хранилище. Загрузка при создании НЕ начинается: начальное состояние считается чисто —
5779
+ из кэша загрузчика, если локаль там уже есть, — а запрос уходит на первый {@link LocaleStore.setLang}.
5780
+ Это позволяет создавать хранилище прямо в рендере.
5781
+
5782
+ **Signature:**
5783
+ ```typescript
5784
+ export function createLocaleStore(
5785
+ load: LocaleLoad,
5786
+ lang: string,
5787
+ options: LocaleStoreOptions = {}
5788
+ ): LocaleStore
5789
+ ```
5790
+
5791
+ **Parameters:**
5792
+ - `load` — - Загрузчик локалей.
5793
+ - `lang` — - Начальный язык.
5794
+ - `options` — - См. {@link LocaleStoreOptions}.
5795
+
5796
+ _Source: src/i18n/locale-store.ts_
5797
+
5798
+ ### createMessageFormatter
5799
+
5800
+ **Kind:** `function`
5801
+
5802
+ Форматтер одной локали с кэшем разбора: сообщение разбирается один раз, на первый вызов.
5803
+ Это внутренность `I18nService.t()` — сам `t()` добавляет сверху поиск ключа в словаре, маркер
5804
+ пропущенного ключа и откат на резервную локаль.
5805
+
5806
+ **Signature:**
5807
+ ```typescript
5808
+ export function createMessageFormatter(
5809
+ locale: string,
5810
+ options: MessageFormatOptions = {}
5811
+ ): (source: string, values?: MessageValues) => string
5812
+ ```
5813
+
5814
+ **Parameters:**
5815
+ - `locale` — - Локаль форматтера.
5816
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
5817
+
5818
+ **Returns:** Функция `(сообщение, значения) => строка`.
5819
+
5820
+ **Examples:**
5821
+
5822
+ ```ts
5823
+ const format = createMessageFormatter('ru');
5824
+ format('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}', { count: 5 });
5825
+ // '5 файлов'
5826
+ ```
5827
+
5828
+ _Source: src/i18n/message-format.ts_
5829
+
5362
5830
  ### createModel
5363
5831
 
5364
5832
  **Kind:** `function`
@@ -5413,6 +5881,20 @@ export function cross<TSnapshot>(
5413
5881
 
5414
5882
  _Source: src/form/validation/operators.ts_
5415
5883
 
5884
+ ### DEFAULT_LOCALE
5885
+
5886
+ **Kind:** `const`
5887
+
5888
+ Локаль «провайдера нет»: английский, пустой словарь. Каждый пакет при промахе по словарю берёт
5889
+ свою встроенную английскую таблицу, поэтому пустой словарь означает «всё по-английски».
5890
+
5891
+ **Signature:**
5892
+ ```typescript
5893
+ export const DEFAULT_LOCALE: FormLocale
5894
+ ```
5895
+
5896
+ _Source: src/i18n/locale.ts_
5897
+
5416
5898
  ### defer
5417
5899
 
5418
5900
  **Kind:** `function`
@@ -5449,6 +5931,37 @@ export const myBehavior = defineFormBehavior<MyForm>(({ model, form }) => {
5449
5931
 
5450
5932
  _Source: src/form/behaviors/context.ts_
5451
5933
 
5934
+ ### defineMessages
5935
+
5936
+ **Kind:** `function`
5937
+
5938
+ `msg` с типизированными ключами: принимает английский словарь приложения, проверяет ключ при
5939
+ компиляции и подставляет английский текст как `defaultMessage` — без провайдера и при промахе
5940
+ по локали форма покажет его, а не ключ.
5941
+
5942
+ **Signature:**
5943
+ ```typescript
5944
+ export function defineMessages<D extends Messages>(
5945
+ defaults: D
5946
+ ): {
5947
+ msg(key: keyof D & string, values?: Readonly<Record<string, unknown>>): MessageDescriptor;
5948
+ }
5949
+ ```
5950
+
5951
+ **Parameters:**
5952
+ - `defaults` — - Английский словарь приложения.
5953
+
5954
+ **Examples:**
5955
+
5956
+ ```ts
5957
+ import appEn from './locales/en.json';
5958
+
5959
+ export const { msg } = defineMessages(appEn);
5960
+ msg('profile.email.label'); // опечатка в ключе — ошибка компиляции
5961
+ ```
5962
+
5963
+ _Source: src/i18n/descriptor.ts_
5964
+
5452
5965
  ### defineValidationSchema
5453
5966
 
5454
5967
  **Kind:** `function`
@@ -5507,7 +6020,8 @@ _Source: src/form/behaviors/node.ts_
5507
6020
  **Kind:** `function`
5508
6021
 
5509
6022
  Применить под-правила к КАЖДОМУ элементу текущего массива модели.
5510
- `U extends object` — элементы должны быть под-моделями (объектами); для массива примитивов валидируйте лист напрямую.
6023
+ `U extends object` — элементы должны быть под-моделями (объектами); массив примитивов — одно
6024
+ значение поля, его проверяют целиком: `validate(model.$.<массив>, [...])`.
5511
6025
 
5512
6026
  **Signature:**
5513
6027
  ```typescript
@@ -5526,9 +6040,11 @@ _Source: src/form/validation/operators.ts_
5526
6040
  Обойти ВСЕ листовые сигналы модели (включая элементы массивов), вызвав `visit` на каждом.
5527
6041
 
5528
6042
  Внутри реактивного `effect` служит подпиской «любое поле изменилось»: `visit(sig => void sig.value)`
5529
- подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Так строятся
5530
- триггеры `change`/`blur` стратегий валидации (см. `createFormValidation`), тем же паттерном, что
5531
- `revalidateWhen`, но без ручного перечисления зависимостей.
6043
+ подписывает на значения листьев, а обход массивов через `items.value` — на их состав. Тот же
6044
+ паттерн, что `revalidateWhen`, но без ручного перечисления зависимостей.
6045
+
6046
+ Сами массивы обход не посещает — только их элементы. Поле, привязанное к массиву целиком
6047
+ (мультивыбор), живёт на сигнале массива; чтобы увидеть и его, берите {@link eachValueSignal}.
5532
6048
 
5533
6049
  **Signature:**
5534
6050
  ```typescript
@@ -5540,6 +6056,28 @@ export function eachLeafSignal<T>(
5540
6056
 
5541
6057
  _Source: src/model/create-model.ts_
5542
6058
 
6059
+ ### eachValueSignal
6060
+
6061
+ **Kind:** `function`
6062
+
6063
+ Обойти ВСЕ ручки значений модели: листья (включая элементы массивов) И сами массивы.
6064
+
6065
+ Отличие от {@link eachLeafSignal} — узлы-массивы: массив бывает одним значением поля
6066
+ (`{ value: model.$.tags, component }`), и нода такого поля привязана к сигналу массива, а не
6067
+ к сигналам элементов. Обходчику, которому нужна именно НОДА поля (стратегия `blur` подписывается
6068
+ на её `touched`), одних листьев мало — он не увидел бы мультивыбор. Так строятся триггеры
6069
+ `change`/`blur` стратегий валидации (см. `createFormValidation`).
6070
+
6071
+ **Signature:**
6072
+ ```typescript
6073
+ export function eachValueSignal<T>(
6074
+ model: FormModel<T>,
6075
+ visit: (signal: PathAwareSignal<unknown>) => void
6076
+ ): void
6077
+ ```
6078
+
6079
+ _Source: src/model/create-model.ts_
6080
+
5543
6081
  ### effect
5544
6082
 
5545
6083
  **Kind:** `function`
@@ -5713,26 +6251,79 @@ exclusiveFlag(model.$.contacts, (row) => row.$.primary);
5713
6251
 
5714
6252
  _Source: src/form/behaviors/collections.ts_
5715
6253
 
5716
- ### FIELD_WRAPPER_ONLY_PROPS
6254
+ ### extendLocale
5717
6255
 
5718
- **Kind:** `const`
6256
+ **Kind:** `function`
5719
6257
 
5720
- Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
5721
- подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
6258
+ Новая локаль на основе существующей: словари сливаются (ключи из `patch` важнее), остальные
6259
+ поля заменяются. Исходный объект не меняется.
5722
6260
 
5723
6261
  **Signature:**
5724
6262
  ```typescript
5725
- export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
6263
+ export function extendLocale(base: FormLocale, patch: Partial<FormLocale>): FormLocale
5726
6264
  ```
5727
6265
 
5728
- _Source: src/platforms/react/field-binding/field-adapter.ts_
6266
+ **Parameters:**
6267
+ - `base` — - Локаль-основа, например поставляемая китом.
6268
+ - `patch` — - Свои ключи, переопределения китовых подписей и поля локали.
5729
6269
 
5730
- ### FieldAdapter
6270
+ **Examples:**
5731
6271
 
5732
- **Kind:** `interface`
6272
+ ```ts
6273
+ import { ru } from '@reformer/ui-kit/locale/ru';
5733
6274
 
5734
- Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
5735
- DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
6275
+ const appRu = extendLocale(ru, {
6276
+ messages: { 'profile.title': 'Профиль', 'kit.formWizard.submit': 'Отправить' },
6277
+ });
6278
+ ```
6279
+
6280
+ _Source: src/i18n/locale.ts_
6281
+
6282
+ ### fetchMessages
6283
+
6284
+ **Kind:** `function`
6285
+
6286
+ Источник локали по сети: забирает JSON по адресу, построенному из кода языка. Файл может быть
6287
+ словарём (`{ "profile.title": "Профиль" }`) или локалью целиком (`{ "code": "ru", "messages": … }`).
6288
+ Ответ не `2xx` — ошибка загрузки.
6289
+
6290
+ **Signature:**
6291
+ ```typescript
6292
+ export function fetchMessages(url: (code: string) => string, init?: RequestInit): LocaleSource
6293
+ ```
6294
+
6295
+ **Parameters:**
6296
+ - `url` — - Адрес файла для языка.
6297
+ - `init` — - Параметры запроса, передаются в `fetch` как есть.
6298
+
6299
+ **Examples:**
6300
+
6301
+ ```ts
6302
+ fetchMessages((code) => `/locales/${code}.json`);
6303
+ ```
6304
+
6305
+ _Source: src/i18n/loader.ts_
6306
+
6307
+ ### FIELD_WRAPPER_ONLY_PROPS
6308
+
6309
+ **Kind:** `const`
6310
+
6311
+ Props, которые адресованы обёртке поля, а не контролу: `labelTooltip` рисует `FormField` у
6312
+ подписи. Они приходят в общем мешке `componentProps`, и без среза утекли бы в DOM контрола.
6313
+
6314
+ **Signature:**
6315
+ ```typescript
6316
+ export const FIELD_WRAPPER_ONLY_PROPS: readonly string[]
6317
+ ```
6318
+
6319
+ _Source: src/platforms/react/field-binding/field-adapter.ts_
6320
+
6321
+ ### FieldAdapter
6322
+
6323
+ **Kind:** `interface`
6324
+
6325
+ Адаптер поля: описывает диалект контрола (`checked`/`onCheckedChange`, `value`/`onValueChange`,
6326
+ DOM-событие в `onChange`, …). Все ключи необязательны — по умолчанию seam проходит как есть.
5736
6327
 
5737
6328
  **Signature:**
5738
6329
  ```typescript
@@ -5895,11 +6486,12 @@ export interface FieldControlState<T> {
5895
6486
  * @example
5896
6487
  * ```tsx
5897
6488
  * const { errors } = useFormControl(field);
6489
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5898
6490
  *
5899
6491
  * return (
5900
6492
  * <ul className="error-list">
5901
6493
  * {errors.map((error, i) => (
5902
- * <li key={i}>{error.message}</li>
6494
+ * <li key={i}>{message(error)}</li>
5903
6495
  * ))}
5904
6496
  * </ul>
5905
6497
  * );
@@ -5962,12 +6554,13 @@ export interface FieldControlState<T> {
5962
6554
  * @example
5963
6555
  * ```tsx
5964
6556
  * const { shouldShowError, errors } = useFormControl(field);
6557
+ * const message = useValidationMessage(); // из '@reformer/core/i18n'
5965
6558
  *
5966
6559
  * return (
5967
6560
  * <div>
5968
6561
  * <input {...props} />
5969
6562
  * {shouldShowError && (
5970
- * <span className="error">{errors[0]?.message}</span>
6563
+ * <span className="error">{message(errors[0])}</span>
5971
6564
  * )}
5972
6565
  * </div>
5973
6566
  * );
@@ -6030,6 +6623,7 @@ control: FieldNode<string>;
6030
6623
 
6031
6624
  function TextField({ control }: Props) {
6032
6625
  const state = useFormControl(control);
6626
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
6033
6627
 
6034
6628
  return (
6035
6629
  <div>
@@ -6039,7 +6633,7 @@ disabled={state.disabled}
6039
6633
  onChange={e => control.setValue(e.target.value)}
6040
6634
  />
6041
6635
  {state.shouldShowError && state.errors[0] && (
6042
- <span className="error">{state.errors[0].message}</span>
6636
+ <span className="error">{message(state.errors[0])}</span>
6043
6637
  )}
6044
6638
  </div>
6045
6639
  );
@@ -6262,6 +6856,33 @@ todos.forEach((item, i) => {
6262
6856
 
6263
6857
  _Source: src/form/types/form-proxy.ts_
6264
6858
 
6859
+ ### formatPattern
6860
+
6861
+ **Kind:** `function`
6862
+
6863
+ Собирает строку из разобранного сообщения. Значения подставляются как текст и повторно
6864
+ не разбираются.
6865
+
6866
+ **Signature:**
6867
+ ```typescript
6868
+ export function formatPattern(
6869
+ pattern: MessagePattern,
6870
+ values: MessageValues,
6871
+ locale: string,
6872
+ options: MessageFormatOptions = {}
6873
+ ): string
6874
+ ```
6875
+
6876
+ **Parameters:**
6877
+ - `pattern` — - Результат {@link parseMessage}.
6878
+ - `values` — - Значения аргументов.
6879
+ - `locale` — - Локаль для {@link Intl.PluralRules} и {@link Intl.NumberFormat} (`ru`, `en`, …).
6880
+ - `options` — - Настройки; см. {@link MessageFormatOptions}.
6881
+
6882
+ **Returns:** Готовая строка интерфейса.
6883
+
6884
+ _Source: src/i18n/message-format.ts_
6885
+
6265
6886
  ### FormBehavior
6266
6887
 
6267
6888
  **Kind:** `interface`
@@ -6407,6 +7028,43 @@ try {
6407
7028
 
6408
7029
  _Source: src/form/validation/error-handler.ts_
6409
7030
 
7031
+ ### FormLocale
7032
+
7033
+ **Kind:** `interface`
7034
+
7035
+ Локаль формы.
7036
+
7037
+ **Signature:**
7038
+ ```typescript
7039
+ export interface FormLocale {
7040
+ /** Код языка BCP 47 (`en`, `ru`, `pt-BR`): по нему работают `Intl` и множественные формы. */
7041
+ readonly code: string;
7042
+ /** Словарь сообщений. Ключа нет — пакет подставит встроенный английский. */
7043
+ readonly messages: Messages;
7044
+ /** Первый день недели календаря. По умолчанию — воскресенье. */
7045
+ readonly weekStartsOn?: WeekStart;
7046
+ /**
7047
+ * Необязательное переопределение: объект локали `react-day-picker` / `date-fns`. Для ядра
7048
+ * непрозрачен. Без него даты форматируются через `Intl` по {@link FormLocale.code}.
7049
+ */
7050
+ readonly dateLocale?: unknown;
7051
+ /** Необязательное переопределение: формат даты в токенах `date-fns` для DatePicker. */
7052
+ readonly dateFormat?: string;
7053
+ }
7054
+ ```
7055
+
7056
+ **Examples:**
7057
+
7058
+ ```ts
7059
+ const ru: FormLocale = {
7060
+ code: 'ru',
7061
+ weekStartsOn: 1,
7062
+ messages: { 'kit.select.selected': 'Выбрано: {count}' },
7063
+ };
7064
+ ```
7065
+
7066
+ _Source: src/i18n/locale.ts_
7067
+
6410
7068
  ### FormModel
6411
7069
 
6412
7070
  **Kind:** `type`
@@ -6988,6 +7646,163 @@ export interface GroupNodeConfig<T> {
6988
7646
 
6989
7647
  _Source: src/form/types/index.ts_
6990
7648
 
7649
+ ### I18N_CONTEXT_MARKER
7650
+
7651
+ **Kind:** `const`
7652
+
7653
+ Имя контекста и одновременно маркер, по которому страж ищет его копии в сборках.
7654
+
7655
+ **Signature:**
7656
+ ```typescript
7657
+ export const I18N_CONTEXT_MARKER
7658
+ ```
7659
+
7660
+ _Source: src/platforms/react/i18n/context.ts_
7661
+
7662
+ ### I18nContextValue
7663
+
7664
+ **Kind:** `interface`
7665
+
7666
+ Что видит компонент: ручка активной локали и состояние её загрузки.
7667
+
7668
+ **Signature:**
7669
+ ```typescript
7670
+ export interface I18nContextValue extends I18nHandle {
7671
+ /** Идёт загрузка другого языка; на экране пока прежний. Без загрузчика всегда `false`. */
7672
+ readonly pending: boolean;
7673
+ /** Ошибка последней загрузки языка. */
7674
+ readonly error: unknown;
7675
+ }
7676
+ ```
7677
+
7678
+ _Source: src/platforms/react/i18n/context.ts_
7679
+
7680
+ ### I18nHandle
7681
+
7682
+ **Kind:** `interface`
7683
+
7684
+ Ручка локали: перевод и форматирование по одному языку.
7685
+
7686
+ **Signature:**
7687
+ ```typescript
7688
+ export interface I18nHandle {
7689
+ /** Локаль, по которой работает ручка. */
7690
+ readonly locale: FormLocale;
7691
+ /** Код языка — то же, что `locale.code`. */
7692
+ readonly code: string;
7693
+ /** Есть ли ключ в словаре локали (встроенные английские таблицы не учитываются). */
7694
+ has(key: string): boolean;
7695
+ /**
7696
+ * Перевод ключа приложения: словарь локали → `defaultMessage` → встроенная таблица ядра → ключ.
7697
+ *
7698
+ * @param key - Ключ сообщения.
7699
+ * @param values - Значения подстановок.
7700
+ * @param defaultMessage - Английский текст на случай, если ключа в локали нет.
7701
+ */
7702
+ t(key: string, values?: MessageValues, defaultMessage?: string): string;
7703
+ /** Число по правилам языка. */
7704
+ number(value: number, options?: Intl.NumberFormatOptions): string;
7705
+ /** Дата по правилам языка. */
7706
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
7707
+ /** Размер файла: `1536` → `1.5 KB` (`1,5 КБ` в русской локали). База — 1024. */
7708
+ fileSize(bytes: number): string;
7709
+ }
7710
+ ```
7711
+
7712
+ _Source: src/i18n/translator.ts_
7713
+
7714
+ ### I18nLoadingProviderProps
7715
+
7716
+ **Kind:** `interface`
7717
+
7718
+ Режим «провайдер грузит сам».
7719
+
7720
+ **Signature:**
7721
+ ```typescript
7722
+ export interface I18nLoadingProviderProps {
7723
+ /** Код языка. Смена кода запускает загрузку; до её конца на экране прежний язык. */
7724
+ lang: string;
7725
+ /** Загрузчик локалей — обычно результат `createLocaleLoader`. */
7726
+ load: LocaleLoad;
7727
+ /**
7728
+ * Что показать, пока не загружена самая первая локаль. По умолчанию — ничего: иначе на кадр
7729
+ * мелькнул бы встроенный английский. Если локаль уже в кэше загрузчика (`load.preload(lang)`
7730
+ * до монтирования), первый рендер синхронный и `fallback` не показывается.
7731
+ */
7732
+ fallback?: ReactNode;
7733
+ /** Отказ загрузки. Язык на экране при этом не меняется. */
7734
+ onError?: (error: unknown, lang: string) => void;
7735
+ children?: ReactNode;
7736
+ }
7737
+ ```
7738
+
7739
+ _Source: src/platforms/react/i18n/provider.ts_
7740
+
7741
+ ### I18nProvider
7742
+
7743
+ **Kind:** `function`
7744
+
7745
+ Провайдер локализации для подписей компонентов, ошибок валидации и авторских подписей формы.
7746
+ Без провайдера компоненты говорят встроенным английским.
7747
+
7748
+ При смене `lang` на экране остаётся прежний язык, пока новый не загрузится (`useI18n().pending`
7749
+ — для индикатора). При быстром переключении применяется только последний запрос. Вложенный
7750
+ провайдер заменяет локаль для своего поддерева целиком.
7751
+
7752
+ **Signature:**
7753
+ ```typescript
7754
+ export function I18nProvider(props: I18nProviderProps): ReactElement | null
7755
+ ```
7756
+
7757
+ **Examples:**
7758
+
7759
+ Провайдер грузит локаль сам
7760
+ ```tsx
7761
+ const [lang, setLang] = useState('ru');
7762
+
7763
+ <I18nProvider lang={lang} load={loadLocale}>
7764
+ <LangSwitch value={lang} onChange={setLang} />
7765
+ <CreditForm />
7766
+ </I18nProvider>
7767
+ ```
7768
+
7769
+ Готовая локаль — SSR и тесты
7770
+ ```tsx
7771
+ import { ru } from '@reformer/ui-kit/locale/ru';
7772
+
7773
+ <I18nProvider locale={ru}><CreditForm /></I18nProvider>
7774
+ ```
7775
+
7776
+ _Source: src/platforms/react/i18n/provider.ts_
7777
+
7778
+ ### I18nProviderProps
7779
+
7780
+ **Kind:** `type`
7781
+
7782
+ **Signature:**
7783
+ ```typescript
7784
+ export type I18nProviderProps = I18nStaticProviderProps | I18nLoadingProviderProps;
7785
+ ```
7786
+
7787
+ _Source: src/platforms/react/i18n/provider.ts_
7788
+
7789
+ ### I18nStaticProviderProps
7790
+
7791
+ **Kind:** `interface`
7792
+
7793
+ Режим «готовая локаль».
7794
+
7795
+ **Signature:**
7796
+ ```typescript
7797
+ export interface I18nStaticProviderProps {
7798
+ /** Локаль. Смена объекта переключает язык сразу. */
7799
+ locale: FormLocale;
7800
+ children?: ReactNode;
7801
+ }
7802
+ ```
7803
+
7804
+ _Source: src/platforms/react/i18n/provider.ts_
7805
+
6991
7806
  ### integer
6992
7807
 
6993
7808
  **Kind:** `function`
@@ -7233,6 +8048,19 @@ if (isGroupNode(node)) {
7233
8048
 
7234
8049
  _Source: src/form/type-guards.ts_
7235
8050
 
8051
+ ### isMessageDescriptor
8052
+
8053
+ **Kind:** `function`
8054
+
8055
+ Является ли значение описателем сообщения.
8056
+
8057
+ **Signature:**
8058
+ ```typescript
8059
+ export function isMessageDescriptor(value: unknown): value is MessageDescriptor
8060
+ ```
8061
+
8062
+ _Source: src/i18n/descriptor.ts_
8063
+
7236
8064
  ### isModelContainerSignal
7237
8065
 
7238
8066
  **Kind:** `function`
@@ -7293,6 +8121,207 @@ validate(model.$.amount, [required(), isNumber({ message: 'Введите чис
7293
8121
 
7294
8122
  _Source: src/form/validators/is-number.ts_
7295
8123
 
8124
+ ### isValueSignal
8125
+
8126
+ **Kind:** `function`
8127
+
8128
+ «Ручка значения», к которой можно привязать поле: лист (`instanceof Signal`) либо узел-массив
8129
+ дерева `model.$` (значение — массив целиком, `.value` читается и пишется).
8130
+
8131
+ Группа сюда не входит: она читается целиком, но не пишется и полем не бывает. Этой проверкой
8132
+ `createForm` и рендереры отличают узел-поле схемы (`{ value, component }`) от контейнера —
8133
+ вместо голого `value instanceof Signal`, который узел-массив не проходит.
8134
+
8135
+ **Signature:**
8136
+ ```typescript
8137
+ export function isValueSignal(value: unknown): value is Signal<unknown>
8138
+ ```
8139
+
8140
+ **Examples:**
8141
+
8142
+ ```typescript
8143
+ const model = createModel({ name: '', tags: [] as string[], address: { city: '' } });
8144
+ isValueSignal(model.$.name); // true — лист
8145
+ isValueSignal(model.$.tags); // true — массив как значение
8146
+ isValueSignal(model.$.address); // false — группа
8147
+ ```
8148
+
8149
+ _Source: src/model/model-signals-proxy.ts_
8150
+
8151
+ ### loadCoreLocale
8152
+
8153
+ **Kind:** `const`
8154
+
8155
+ Источник локалей ядра для `createLocaleLoader`: чанк нужного языка по запросу.
8156
+
8157
+ **Signature:**
8158
+ ```typescript
8159
+ export const loadCoreLocale
8160
+ ```
8161
+
8162
+ **Examples:**
8163
+
8164
+ ```ts
8165
+ const loadLocale = createLocaleLoader([loadCoreLocale, fetchMessages((code) => `/locales/${code}.json`)]);
8166
+ ```
8167
+
8168
+ _Source: src/locale/index.ts_
8169
+
8170
+ ### LocaleIssue
8171
+
8172
+ **Kind:** `interface`
8173
+
8174
+ Что не так со словарём.
8175
+
8176
+ **Signature:**
8177
+ ```typescript
8178
+ export interface LocaleIssue {
8179
+ readonly key: string;
8180
+ /**
8181
+ * - `syntax` — сообщение не разбирается;
8182
+ * - `missing` — ключ есть в образце, но его нет в словаре;
8183
+ * - `extra` — ключа нет в образце;
8184
+ * - `arguments` — набор подстановок не совпадает с образцом.
8185
+ */
8186
+ readonly kind: 'syntax' | 'missing' | 'extra' | 'arguments';
8187
+ readonly message: string;
8188
+ }
8189
+ ```
8190
+
8191
+ _Source: src/i18n/locale.ts_
8192
+
8193
+ ### LocaleLoad
8194
+
8195
+ **Kind:** `interface`
8196
+
8197
+ Функция загрузки локали. `peek` — необязательный синхронный кэш (есть у `createLocaleLoader`).
8198
+
8199
+ **Signature:**
8200
+ ```typescript
8201
+ export interface LocaleLoad {
8202
+ (code: string): Promise<FormLocale>;
8203
+ peek?(code: string): FormLocale | undefined;
8204
+ }
8205
+ ```
8206
+
8207
+ _Source: src/i18n/locale-store.ts_
8208
+
8209
+ ### LocaleLoader
8210
+
8211
+ **Kind:** `interface`
8212
+
8213
+ Загрузчик локалей с кэшем.
8214
+
8215
+ **Signature:**
8216
+ ```typescript
8217
+ export interface LocaleLoader {
8218
+ /** Загрузить локаль языка. Повторный вызов возвращает тот же промис и ту же локаль. */
8219
+ (code: string): Promise<FormLocale>;
8220
+ /** Уже загруженная локаль или `undefined`. Позволяет провайдеру отрисоваться синхронно. */
8221
+ peek(code: string): FormLocale | undefined;
8222
+ /** То же, что вызов загрузчика; имя для чтения в коде старта приложения. */
8223
+ preload(code: string): Promise<FormLocale>;
8224
+ }
8225
+ ```
8226
+
8227
+ _Source: src/i18n/loader.ts_
8228
+
8229
+ ### LocaleSource
8230
+
8231
+ **Kind:** `type`
8232
+
8233
+ Источник локали. Получает код языка, отдаёт локаль или словарь — сразу или промисом.
8234
+ `null`/`undefined` означает «для этого языка у меня ничего нет» и ошибкой не считается.
8235
+
8236
+ **Signature:**
8237
+ ```typescript
8238
+ export type LocaleSource = (code: string) => LocaleSourceResult | Promise<LocaleSourceResult>;
8239
+ ```
8240
+
8241
+ _Source: src/i18n/loader.ts_
8242
+
8243
+ ### LocaleSourceResult
8244
+
8245
+ **Kind:** `type`
8246
+
8247
+ Что может вернуть источник: локаль целиком, её часть, голый словарь или ничего.
8248
+
8249
+ **Signature:**
8250
+ ```typescript
8251
+ export type LocaleSourceResult = Partial<FormLocale> | Messages | null | undefined;
8252
+ ```
8253
+
8254
+ _Source: src/i18n/loader.ts_
8255
+
8256
+ ### LocaleStore
8257
+
8258
+ **Kind:** `interface`
8259
+
8260
+ Хранилище активной локали.
8261
+
8262
+ **Signature:**
8263
+ ```typescript
8264
+ export interface LocaleStore {
8265
+ getState(): LocaleStoreState;
8266
+ subscribe(listener: () => void): () => void;
8267
+ /** Запросить язык. Повторный запрос того же языка, пока он грузится или уже показан, — no-op. */
8268
+ setLang(lang: string): void;
8269
+ }
8270
+ ```
8271
+
8272
+ _Source: src/i18n/locale-store.ts_
8273
+
8274
+ ### LocaleStoreOptions
8275
+
8276
+ **Kind:** `interface`
8277
+
8278
+ Настройки хранилища.
8279
+
8280
+ **Signature:**
8281
+ ```typescript
8282
+ export interface LocaleStoreOptions {
8283
+ /** Вызывается при отказе загрузки языка, который всё ещё запрошен. */
8284
+ readonly onError?: (error: unknown, lang: string) => void;
8285
+ }
8286
+ ```
8287
+
8288
+ _Source: src/i18n/locale-store.ts_
8289
+
8290
+ ### LocaleStoreState
8291
+
8292
+ **Kind:** `interface`
8293
+
8294
+ Состояние хранилища. Объект неизменяемый: новая ссылка — новое состояние.
8295
+
8296
+ **Signature:**
8297
+ ```typescript
8298
+ export interface LocaleStoreState {
8299
+ /** Запрошенный язык. Пока `pending`, может отличаться от языка `locale`. */
8300
+ readonly lang: string;
8301
+ /** Локаль, которая сейчас на экране; `null` — ни одна ещё не загружена. */
8302
+ readonly locale: FormLocale | null;
8303
+ /** Идёт загрузка запрошенного языка. */
8304
+ readonly pending: boolean;
8305
+ /** Ошибка последней загрузки; сбрасывается следующей удачной. */
8306
+ readonly error: unknown;
8307
+ }
8308
+ ```
8309
+
8310
+ _Source: src/i18n/locale-store.ts_
8311
+
8312
+ ### LocalizableText
8313
+
8314
+ **Kind:** `type`
8315
+
8316
+ Текст, который можно задать строкой или описателем.
8317
+
8318
+ **Signature:**
8319
+ ```typescript
8320
+ export type LocalizableText = string | MessageDescriptor;
8321
+ ```
8322
+
8323
+ _Source: src/i18n/descriptor.ts_
8324
+
7296
8325
  ### makeElementFieldHandle
7297
8326
 
7298
8327
  **Kind:** `function`
@@ -7538,114 +8567,272 @@ _Source: src/form/validators/max-files.ts_
7538
8567
 
7539
8568
  **Signature:**
7540
8569
  ```typescript
7541
- export function maxFileSize<TForm = unknown, TField = unknown>(
7542
- maxSize: number,
7543
- options?: ValidateOptions
7544
- ): Validator<TForm, TField>
8570
+ export function maxFileSize<TForm = unknown, TField = unknown>(
8571
+ maxSize: number,
8572
+ options?: ValidateOptions
8573
+ ): Validator<TForm, TField>
8574
+ ```
8575
+
8576
+ **Parameters:**
8577
+ - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
8578
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8579
+ попадают `maxFileSize`, `fileName` и `actualSize`.
8580
+
8581
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8582
+
8583
+ **Examples:**
8584
+
8585
+ Ограничение размера вложений
8586
+ ```typescript
8587
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8588
+ import { maxFileSize } from '@reformer/core/validators';
8589
+
8590
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8591
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8592
+ validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
8593
+ });
8594
+ ```
8595
+
8596
+ _Source: src/form/validators/max-file-size.ts_
8597
+
8598
+ ### maxLength
8599
+
8600
+ **Kind:** `function`
8601
+
8602
+ Фабрика валидатора максимальной длины строки или массива.
8603
+
8604
+ Работает со строкой или массивом (проверяется `value.length`). Пустые значения
8605
+ (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
8606
+ (используйте {@link required} для обязательности).
8607
+
8608
+ **Signature:**
8609
+ ```typescript
8610
+ export function maxLength<TForm = unknown, TField = unknown>(
8611
+ maxLen: number,
8612
+ options?: ValidateOptions
8613
+ ): Validator<TForm, TField>
8614
+ ```
8615
+
8616
+ **Parameters:**
8617
+ - `maxLen` — - Максимально допустимая длина (включительно)
8618
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8619
+ попадают `maxLength` и `actualLength`.
8620
+
8621
+ **Returns:** Чистый валидатор {@link Validator} для строки или массива
8622
+
8623
+ **Examples:**
8624
+
8625
+ Максимальная длина строки
8626
+ ```typescript
8627
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8628
+ import { maxLength } from '@reformer/core/validators';
8629
+
8630
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8631
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8632
+ validate(model.$.name, [maxLength(50)]);
8633
+ validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
8634
+ });
8635
+ ```
8636
+
8637
+ _Source: src/form/validators/max-length.ts_
8638
+
8639
+ ### maxTotalFileSize
8640
+
8641
+ **Kind:** `function`
8642
+
8643
+ Фабрика валидатора суммарного размера всех файлов.
8644
+
8645
+ Складывает `size` всех файлов значения (элементы без числового `size` в сумму
8646
+ не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
8647
+ (используйте {@link required} для обязательности).
8648
+
8649
+ **Signature:**
8650
+ ```typescript
8651
+ export function maxTotalFileSize<TForm = unknown, TField = unknown>(
8652
+ maxTotal: number,
8653
+ options?: ValidateOptions
8654
+ ): Validator<TForm, TField>
8655
+ ```
8656
+
8657
+ **Parameters:**
8658
+ - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
8659
+ - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
8660
+ попадают `maxTotalFileSize` и `actualTotal`.
8661
+
8662
+ **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8663
+
8664
+ **Examples:**
8665
+
8666
+ Суммарно не более 20 МБ
8667
+ ```typescript
8668
+ import { defineValidationSchema, validate } from '@reformer/core/validation';
8669
+ import { maxTotalFileSize } from '@reformer/core/validators';
8670
+
8671
+ // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
8672
+ const validation = defineValidationSchema<MyForm>(({ model }) => {
8673
+ validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
8674
+ });
8675
+ ```
8676
+
8677
+ _Source: src/form/validators/max-total-file-size.ts_
8678
+
8679
+ ### MESSAGE_DESCRIPTOR
8680
+
8681
+ **Kind:** `const`
8682
+
8683
+ Бренд описателя. `Symbol.for` — чтобы описатель узнавался и через границу двух копий модуля
8684
+ (разные чанки, разные версии пакета на странице).
8685
+
8686
+ **Signature:**
8687
+ ```typescript
8688
+ export const MESSAGE_DESCRIPTOR: unique symbol
8689
+ ```
8690
+
8691
+ _Source: src/i18n/descriptor.ts_
8692
+
8693
+ ### messageArguments
8694
+
8695
+ **Kind:** `function`
8696
+
8697
+ Имена аргументов сообщения, включая те, что встречаются только внутри веток `plural`/`select`.
8698
+
8699
+ **Signature:**
8700
+ ```typescript
8701
+ export function messageArguments(
8702
+ pattern: MessagePattern,
8703
+ out: Set<string> = new Set()
8704
+ ): Set<string>
8705
+ ```
8706
+
8707
+ **Parameters:**
8708
+ - `pattern` — - Результат `parseMessage`.
8709
+
8710
+ _Source: src/i18n/locale.ts_
8711
+
8712
+ ### MessageBranches
8713
+
8714
+ **Kind:** `type`
8715
+
8716
+ Ветки `plural`/`select`: категория CLDR (`one`, `few`, …), точное совпадение (`=0`) или значение `select`.
8717
+
8718
+ **Signature:**
8719
+ ```typescript
8720
+ export type MessageBranches = ReadonlyMap<string, MessagePattern>;
8721
+ ```
8722
+
8723
+ _Source: src/i18n/message-format.ts_
8724
+
8725
+ ### MessageDescriptor
8726
+
8727
+ **Kind:** `interface`
8728
+
8729
+ Описатель сообщения. Намеренно без ключей `value`, `array`, `item`, `component`: обход схемы в
8730
+ `createForm` узнаёт по ним узел, а описатель узлом не является.
8731
+
8732
+ **Signature:**
8733
+ ```typescript
8734
+ export interface MessageDescriptor {
8735
+ readonly [MESSAGE_DESCRIPTOR]: true;
8736
+ /** Ключ сообщения в словаре приложения. */
8737
+ readonly key: string;
8738
+ /** Значения подстановок. */
8739
+ readonly values?: Readonly<Record<string, unknown>>;
8740
+ /** Английский текст на случай, если ключа в локали нет. */
8741
+ readonly defaultMessage?: string;
8742
+ }
8743
+ ```
8744
+
8745
+ _Source: src/i18n/descriptor.ts_
8746
+
8747
+ ### MessageFormatOptions
8748
+
8749
+ **Kind:** `interface`
8750
+
8751
+ Настройки форматирования.
8752
+
8753
+ **Signature:**
8754
+ ```typescript
8755
+ export interface MessageFormatOptions {
8756
+ /**
8757
+ * Что подставить вместо аргумента, которого нет в значениях (или чьё значение не число там, где
8758
+ * ожидалось число). По умолчанию — заметный маркер `⟦name⟧`, тот же приём, что и для пропущенного
8759
+ * ключа: пропуск должен попадаться разработчику, а не пользователю.
8760
+ */
8761
+ readonly onMissingArgument?: (name: string) => string;
8762
+ }
7545
8763
  ```
7546
8764
 
7547
- **Parameters:**
7548
- - `maxSize` — - Максимально допустимый размер файла в байтах (включительно)
7549
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7550
- попадают `maxFileSize`, `fileName` и `actualSize`.
8765
+ _Source: src/i18n/message-format.ts_
7551
8766
 
7552
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8767
+ ### MessageNode
7553
8768
 
7554
- **Examples:**
8769
+ **Kind:** `type`
7555
8770
 
7556
- Ограничение размера вложений
7557
- ```typescript
7558
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7559
- import { maxFileSize } from '@reformer/core/validators';
8771
+ Узел разобранного сообщения.
7560
8772
 
7561
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7562
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7563
- validate(model.$.documents, [maxFileSize(5 * 1024 * 1024, { message: 'Файл больше 5 МБ' })]);
7564
- });
8773
+ **Signature:**
8774
+ ```typescript
8775
+ export type MessageNode =
8776
+ | { readonly kind: 'text'; readonly value: string }
8777
+ | { readonly kind: 'arg'; readonly name: string }
8778
+ | { readonly kind: 'number'; readonly name: string }
8779
+ | { readonly kind: 'plural'; readonly name: string; readonly branches: MessageBranches }
8780
+ | { readonly kind: 'select'; readonly name: string; readonly branches: MessageBranches };
7565
8781
  ```
7566
8782
 
7567
- _Source: src/form/validators/max-file-size.ts_
7568
-
7569
- ### maxLength
8783
+ _Source: src/i18n/message-format.ts_
7570
8784
 
7571
- **Kind:** `function`
8785
+ ### MessagePattern
7572
8786
 
7573
- Фабрика валидатора максимальной длины строки или массива.
8787
+ **Kind:** `type`
7574
8788
 
7575
- Работает со строкой или массивом (проверяется `value.length`). Пустые значения
7576
- (`null`/`undefined`/`''`) и значения без числового `length` пропускаются
7577
- (используйте {@link required} для обязательности).
8789
+ Разобранное сообщение — плоская последовательность узлов.
7578
8790
 
7579
8791
  **Signature:**
7580
8792
  ```typescript
7581
- export function maxLength<TForm = unknown, TField = unknown>(
7582
- maxLen: number,
7583
- options?: ValidateOptions
7584
- ): Validator<TForm, TField>
8793
+ export type MessagePattern = readonly MessageNode[];
7585
8794
  ```
7586
8795
 
7587
- **Parameters:**
7588
- - `maxLen` — - Максимально допустимая длина (включительно)
7589
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7590
- попадают `maxLength` и `actualLength`.
8796
+ _Source: src/i18n/message-format.ts_
7591
8797
 
7592
- **Returns:** Чистый валидатор {@link Validator} для строки или массива
8798
+ ### Messages
7593
8799
 
7594
- **Examples:**
8800
+ **Kind:** `type`
7595
8801
 
7596
- Максимальная длина строки
7597
- ```typescript
7598
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7599
- import { maxLength } from '@reformer/core/validators';
8802
+ Плоский словарь: ключ → сообщение в формате ICU (см. `message-format.ts`).
7600
8803
 
7601
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7602
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7603
- validate(model.$.name, [maxLength(50)]);
7604
- validate(model.$.bio, [maxLength(500, { message: 'Максимум 500 символов' })]);
7605
- });
8804
+ **Signature:**
8805
+ ```typescript
8806
+ export type Messages = Readonly<Record<string, string>>;
7606
8807
  ```
7607
8808
 
7608
- _Source: src/form/validators/max-length.ts_
7609
-
7610
- ### maxTotalFileSize
8809
+ _Source: src/i18n/locale.ts_
7611
8810
 
7612
- **Kind:** `function`
8811
+ ### MessageSyntaxError
7613
8812
 
7614
- Фабрика валидатора суммарного размера всех файлов.
8813
+ **Kind:** `class`
7615
8814
 
7616
- Складывает `size` всех файлов значения (элементы без числового `size` в сумму
7617
- не входят). Пустые значения (`null`/`undefined`/`''`/`[]`) пропускаются
7618
- (используйте {@link required} для обязательности).
8815
+ Отказ разбора сообщения: синтаксис или отсутствие обязательной ветки `other`.
7619
8816
 
7620
8817
  **Signature:**
7621
8818
  ```typescript
7622
- export function maxTotalFileSize<TForm = unknown, TField = unknown>(
7623
- maxTotal: number,
7624
- options?: ValidateOptions
7625
- ): Validator<TForm, TField>
8819
+ export class MessageSyntaxError extends Error { /* … */ }
7626
8820
  ```
7627
8821
 
7628
- **Parameters:**
7629
- - `maxTotal` — - Максимально допустимый суммарный размер в байтах (включительно)
7630
- - `options` — - Опции валидатора ({@link ValidateOptions}). В `params` ошибки автоматически
7631
- попадают `maxTotalFileSize` и `actualTotal`.
8822
+ _Source: src/i18n/message-format.ts_
7632
8823
 
7633
- **Returns:** Чистый валидатор {@link Validator} для файла или массива файлов
8824
+ ### MessageValues
7634
8825
 
7635
- **Examples:**
8826
+ **Kind:** `type`
7636
8827
 
7637
- Суммарно не более 20 МБ
7638
- ```typescript
7639
- import { defineValidationSchema, validate } from '@reformer/core/validation';
7640
- import { maxTotalFileSize } from '@reformer/core/validators';
8828
+ Значения аргументов сообщения.
7641
8829
 
7642
- // Правила живут в отдельной схеме над МОДЕЛЬЮ — layout-схема валидаторов не несёт.
7643
- const validation = defineValidationSchema<MyForm>(({ model }) => {
7644
- validate(model.$.documents, [maxTotalFileSize(20 * 1024 * 1024, { message: 'Суммарно не более 20 МБ' })]);
7645
- });
8830
+ **Signature:**
8831
+ ```typescript
8832
+ export type MessageValues = Readonly<Record<string, unknown>>;
7646
8833
  ```
7647
8834
 
7648
- _Source: src/form/validators/max-total-file-size.ts_
8835
+ _Source: src/i18n/message-format.ts_
7649
8836
 
7650
8837
  ### min
7651
8838
 
@@ -7923,7 +9110,10 @@ export interface ModelApi<T> {
7923
9110
  reset(): void;
7924
9111
  /** Зафиксировать текущие значения как новый initial-снимок («точка отсчёта»). */
7925
9112
  captureInitial(): void;
7926
- /** Резолв строкового пути в сигнал (для error-routing/мостов). */
9113
+ /**
9114
+ * Резолв строкового пути в сигнал (для error-routing/мостов): лист либо массив целиком
9115
+ * (ручка значения массива — см. {@link ModelArraySignals}). Для пути группы — `undefined`.
9116
+ */
7927
9117
  signalAt(path: string): PathAwareSignal<unknown> | undefined;
7928
9118
  }
7929
9119
  ```
@@ -8052,12 +9242,23 @@ _Source: src/form/nodes/model-array-node.ts_
8052
9242
  **Kind:** `type`
8053
9243
 
8054
9244
  Узел-массив дерева `model.$`: индексируемый доступ к под-сигналам элементов, реактивная `length`
8055
- и {@link ReadonlySignal} значения массива целиком (реагирует и на правку элемента, и на изменение
8056
- состава — push/removeAt/move).
9245
+ и сигнал значения массива целиком (реагирует и на правку элемента, и на изменение состава —
9246
+ push/removeAt/move).
9247
+
9248
+ В отличие от группы, узел ЗАПИСЫВАЕМ: `model.$.tags.value = ['a']` заменяет массив целиком. Это
9249
+ делает его {@link PathAwareSignal} — к нему привязывают поле (`{ value: model.$.tags, component }`),
9250
+ правило (`validate(model.$.tags, …)`) и поведение, как к листу. Так массив служит ОДНИМ значением
9251
+ поля: мультивыбор, теги, список файлов.
9252
+
9253
+ `V` — объявленный тип поля, включая `null`/`undefined`: поле `string[] | null` с начальным `null`
9254
+ в рантайме — лист с тем же `.value`/`peek`/`subscribe`, поэтому общая часть честна для обоих
9255
+ случаев. `length` и индексы есть только у настоящего узла-массива (начальное значение — массив).
8057
9256
 
8058
9257
  **Signature:**
8059
9258
  ```typescript
8060
- export type ModelArraySignals<U, V> = ContainerSignal<V, 'length'> & {
9259
+ export type ModelArraySignals<U, V> = Signal<V> & {
9260
+ /** Путь массива в модели (dot-нотация). */
9261
+ readonly __path: string;
8061
9262
  readonly length: number;
8062
9263
  readonly [index: number]: ModelSignalNode<U>;
8063
9264
  };
@@ -8140,6 +9341,34 @@ export type ModelValue<V> =
8140
9341
 
8141
9342
  _Source: src/model/types.ts_
8142
9343
 
9344
+ ### msg
9345
+
9346
+ **Kind:** `function`
9347
+
9348
+ Создаёт описатель сообщения.
9349
+
9350
+ **Signature:**
9351
+ ```typescript
9352
+ export function msg(
9353
+ key: string,
9354
+ values?: Readonly<Record<string, unknown>>,
9355
+ defaultMessage?: string
9356
+ ): MessageDescriptor
9357
+ ```
9358
+
9359
+ **Parameters:**
9360
+ - `key` — - Ключ в словаре приложения.
9361
+ - `values` — - Значения подстановок.
9362
+ - `defaultMessage` — - Английский текст на случай, если ключа в локали нет.
9363
+
9364
+ **Examples:**
9365
+
9366
+ ```ts
9367
+ { value: model.$.email, component: Input, componentProps: { label: msg('profile.email.label') } }
9368
+ ```
9369
+
9370
+ _Source: src/i18n/descriptor.ts_
9371
+
8143
9372
  ### multipleOf
8144
9373
 
8145
9374
  **Kind:** `function`
@@ -8356,6 +9585,31 @@ export function onDispose(cleanup: BehaviorCleanup): void
8356
9585
 
8357
9586
  _Source: src/form/behaviors/context.ts_
8358
9587
 
9588
+ ### parseMessage
9589
+
9590
+ **Kind:** `function`
9591
+
9592
+ Разбирает сообщение в дерево. Бросает {@link MessageSyntaxError} на ошибке синтаксиса и на
9593
+ `plural`/`select` без ветки `other`.
9594
+
9595
+ **Signature:**
9596
+ ```typescript
9597
+ export function parseMessage(source: string): MessagePattern
9598
+ ```
9599
+
9600
+ **Parameters:**
9601
+ - `source` — - Текст сообщения из словаря.
9602
+
9603
+ **Returns:** Разобранное сообщение для {@link formatPattern}.
9604
+
9605
+ **Examples:**
9606
+
9607
+ ```ts
9608
+ parseMessage('{count, plural, one{# файл} few{# файла} many{# файлов} other{# файла}}');
9609
+ ```
9610
+
9611
+ _Source: src/i18n/message-format.ts_
9612
+
8359
9613
  ### pastDate
8360
9614
 
8361
9615
  **Kind:** `function`
@@ -8620,6 +9874,60 @@ resetWhen(model.$.cardNumber, () => model.paymentType !== 'card', { resetValue:
8620
9874
 
8621
9875
  _Source: src/model/behaviors-value.ts_
8622
9876
 
9877
+ ### resolveLocalized
9878
+
9879
+ **Kind:** `function`
9880
+
9881
+ Заменяет описатели сообщений на строки по активной локали. Обходит массивы и простые
9882
+ объекты-литералы — подписи опций, заголовки шагов; сигналы, React-элементы, функции и
9883
+ экземпляры классов (узлы формы) возвращает как есть. Поддерево без описателей сохраняет ссылку:
9884
+ если раскрывать было нечего, возвращается тот же объект.
9885
+
9886
+ **Signature:**
9887
+ ```typescript
9888
+ export function resolveLocalized<T>(value: T, i18n: I18nHandle): T
9889
+ ```
9890
+
9891
+ **Parameters:**
9892
+ - `value` — - Пропсы, массив или одиночное значение.
9893
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
9894
+
9895
+ **Examples:**
9896
+
9897
+ ```ts
9898
+ resolveLocalized({ label: msg('profile.email.label'), options: [{ value: 'a', label: msg('opt.a') }] }, i18n);
9899
+ // { label: 'Почта', options: [{ value: 'a', label: 'Вариант А' }] }
9900
+ ```
9901
+
9902
+ _Source: src/i18n/resolve-localized.ts_
9903
+
9904
+ ### resolveValidationError
9905
+
9906
+ **Kind:** `function`
9907
+
9908
+ Отображаемый текст ошибки валидации. Порядок источников — в шапке модуля.
9909
+
9910
+ **Signature:**
9911
+ ```typescript
9912
+ export function resolveValidationError(error: ValidationError, i18n: I18nHandle): string
9913
+ ```
9914
+
9915
+ **Parameters:**
9916
+ - `error` — - Ошибка от правила: `code`, `message`, `messageKey`, `params`.
9917
+ - `i18n` — - Ручка активной локали (`createI18n` / `useI18n`).
9918
+
9919
+ **Examples:**
9920
+
9921
+ ```ts
9922
+ resolveValidationError({ code: 'minLength', message: '', params: { minLength: 8 } }, i18n);
9923
+ // 'Enter at least 8 characters' · в русской локали — 'Не меньше 8 символов'
9924
+
9925
+ resolveValidationError({ code: 'required', message: 'Укажите телефон' }, i18n);
9926
+ // 'Укажите телефон' — явное сообщение автора важнее словаря
9927
+ ```
9928
+
9929
+ _Source: src/i18n/validation-message.ts_
9930
+
8623
9931
  ### revalidateWhen
8624
9932
 
8625
9933
  **Kind:** `function`
@@ -8984,6 +10292,32 @@ transformValue(model.$.promoCode, (v) => (v ?? '').toUpperCase());
8984
10292
 
8985
10293
  _Source: src/model/behaviors-value.ts_
8986
10294
 
10295
+ ### translateBuiltin
10296
+
10297
+ **Kind:** `function`
10298
+
10299
+ Перевод ключа пакета: словарь локали, затем встроенная английская таблица пакета, затем сам
10300
+ ключ. Английская таблица форматируется по английским правилам — её множественные формы и
10301
+ группировка чисел не зависят от активного языка.
10302
+
10303
+ **Signature:**
10304
+ ```typescript
10305
+ export function translateBuiltin(
10306
+ locale: FormLocale,
10307
+ builtin: Messages,
10308
+ key: string,
10309
+ values?: MessageValues
10310
+ ): string
10311
+ ```
10312
+
10313
+ **Parameters:**
10314
+ - `locale` — - Активная локаль.
10315
+ - `builtin` — - Встроенная английская таблица пакета-владельца ключа.
10316
+ - `key` — - Ключ сообщения.
10317
+ - `values` — - Значения подстановок.
10318
+
10319
+ _Source: src/i18n/translator.ts_
10320
+
8987
10321
  ### UnknownRecord
8988
10322
 
8989
10323
  **Kind:** `type`
@@ -9227,6 +10561,7 @@ export function useFormControl(
9227
10561
  Текстовое поле с валидацией
9228
10562
  ```tsx
9229
10563
  import { useFormControl } from '@reformer/core';
10564
+ import { useValidationMessage } from '@reformer/core/i18n';
9230
10565
  import type { FieldNode } from '@reformer/core';
9231
10566
 
9232
10567
  interface TextFieldProps {
@@ -9242,6 +10577,8 @@ shouldShowError,
9242
10577
  errors,
9243
10578
  pending
9244
10579
  } = useFormControl(control);
10580
+ // Текст ошибки даёт резолвер: у правил без своего сообщения `message` пуст.
10581
+ const message = useValidationMessage();
9245
10582
 
9246
10583
  return (
9247
10584
  <div className="field">
@@ -9261,7 +10598,7 @@ return (
9261
10598
 
9262
10599
  {shouldShowError && errors[0] && (
9263
10600
  <span className="error" role="alert">
9264
- {errors[0].message}
10601
+ {message(errors[0])}
9265
10602
  </span>
9266
10603
  )}
9267
10604
  </div>
@@ -9309,6 +10646,7 @@ control: FieldNode<string>;
9309
10646
 
9310
10647
  function Select({ control }: SelectProps) {
9311
10648
  const { value, disabled, componentProps, shouldShowError, errors } = useFormControl(control);
10649
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9312
10650
  const options = componentProps.options as Array<{ value: string; label: string }>;
9313
10651
 
9314
10652
  return (
@@ -9326,7 +10664,7 @@ onBlur={() => control.markAsTouched()}
9326
10664
  </option>
9327
10665
  ))}
9328
10666
  </select>
9329
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
10667
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9330
10668
  </div>
9331
10669
  );
9332
10670
  }
@@ -9564,6 +10902,7 @@ dangerouslySetInnerHTML={{ __html: html }}
9564
10902
  // Основной редактор использует useFormControl для полного состояния
9565
10903
  function MarkdownEditor({ control }: MarkdownEditorProps) {
9566
10904
  const { value, shouldShowError, errors } = useFormControl(control);
10905
+ const message = useValidationMessage(); // из '@reformer/core/i18n'
9567
10906
 
9568
10907
  return (
9569
10908
  <div className="editor-container">
@@ -9571,7 +10910,7 @@ return (
9571
10910
  value={value}
9572
10911
  onChange={e => control.setValue(e.target.value)}
9573
10912
  />
9574
- {shouldShowError && <span className="error">{errors[0]?.message}</span>}
10913
+ {shouldShowError && <span className="error">{message(errors[0])}</span>}
9575
10914
 
9576
10915
  {/* Preview обновляется только при изменении value *}
9577
10916
  <MarkdownPreview control={control} />
@@ -9675,12 +11014,104 @@ export interface UseFormValidationResult {
9675
11014
 
9676
11015
  _Source: src/platforms/react/hooks/use-form-validation.ts_
9677
11016
 
11017
+ ### useI18n
11018
+
11019
+ **Kind:** `function`
11020
+
11021
+ Активная локаль: перевод ключей приложения, форматирование чисел, дат и размеров, состояние
11022
+ загрузки языка. Без провайдера — английский с пустым словарём.
11023
+
11024
+ **Signature:**
11025
+ ```typescript
11026
+ export function useI18n(): I18nContextValue
11027
+ ```
11028
+
11029
+ **Examples:**
11030
+
11031
+ ```tsx
11032
+ function LangBadge() {
11033
+ const { code, pending, t } = useI18n();
11034
+ return <span aria-busy={pending}>{t('app.language')}: {code}</span>;
11035
+ }
11036
+ ```
11037
+
11038
+ _Source: src/platforms/react/i18n/hooks.ts_
11039
+
11040
+ ### useMessages
11041
+
11042
+ **Kind:** `function`
11043
+
11044
+ Переводчик пакета: ищет ключ в локали провайдера, а при промахе берёт встроенную английскую
11045
+ таблицу этого пакета. Так пакет говорит по-английски без провайдера и не зависит от того, знает
11046
+ ли локаль его ключи. Тип ключа выводится из таблицы — опечатка в ключе не скомпилируется.
11047
+
11048
+ `defaults` должен быть стабильной ссылкой (импорт JSON-модуля), иначе переводчик будет
11049
+ пересоздаваться на каждый рендер.
11050
+
11051
+ **Signature:**
11052
+ ```typescript
11053
+ export function useMessages<D extends Messages>(
11054
+ defaults: D
11055
+ ): (key: keyof D & string, values?: MessageValues) => string
11056
+ ```
11057
+
11058
+ **Parameters:**
11059
+ - `defaults` — - Встроенная английская таблица пакета.
11060
+
11061
+ **Returns:** Функция `(ключ, значения?) => строка`; её идентичность меняется вместе с локалью.
11062
+
11063
+ **Examples:**
11064
+
11065
+ ```tsx
11066
+ import en from './locale/en.json';
11067
+
11068
+ const useKitMessages = () => useMessages(en);
11069
+
11070
+ function ClearButton() {
11071
+ const t = useKitMessages();
11072
+ return <button aria-label={t('kit.select.clear')} />;
11073
+ }
11074
+ ```
11075
+
11076
+ _Source: src/platforms/react/i18n/hooks.ts_
11077
+
11078
+ ### useValidationMessage
11079
+
11080
+ **Kind:** `function`
11081
+
11082
+ Резолвер текста ошибки валидации по активной локали: `messageKey` автора → явное `message` →
11083
+ словарь локали по коду → встроенный английский → код. Идентичность функции меняется вместе с
11084
+ локалью, поэтому показанные ошибки переводятся при смене языка без повторной валидации.
11085
+
11086
+ `FormField` из `@reformer/cdk` использует его сам; вызывайте напрямую, если рисуете ошибки
11087
+ своим компонентом.
11088
+
11089
+ **Signature:**
11090
+ ```typescript
11091
+ export function useValidationMessage(): (error: ValidationError) => string
11092
+ ```
11093
+
11094
+ **Examples:**
11095
+
11096
+ ```tsx
11097
+ function FieldErrors({ control }: { control: FieldNode<string> }) {
11098
+ const message = useValidationMessage();
11099
+ const { errors } = useFormControl(control);
11100
+ return <>{errors.map((error) => <span key={error.code}>{message(error)}</span>)}</>;
11101
+ }
11102
+ ```
11103
+
11104
+ _Source: src/platforms/react/i18n/hooks.ts_
11105
+
9678
11106
  ### validate
9679
11107
 
9680
11108
  **Kind:** `function`
9681
11109
 
9682
11110
  Синхронные правила поля.
9683
11111
 
11112
+ Поле — лист либо массив целиком: `model.$.<массив>` — такая же ручка значения, и правило
11113
+ получает массив (мультивыбор, теги, список файлов).
11114
+
9684
11115
  **Signature:**
9685
11116
  ```typescript
9686
11117
  export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TField>[]): void
@@ -9690,6 +11121,7 @@ export function validate<TField>(sig: PathAwareSignal<TField>, rules: Rule<TFiel
9690
11121
 
9691
11122
  ```ts
9692
11123
  validate(model.$.loanAmount, [required({ message: 'Сумма' }), min(50000)]);
11124
+ validate(model.$.tags, [required(), maxLength(3)]); // tags: string[] — массив как значение
9693
11125
  ```
9694
11126
 
9695
11127
  _Source: src/form/validation/operators.ts_
@@ -9720,6 +11152,34 @@ validateAsync(model.$.username, [async (v, { signal }) => {
9720
11152
 
9721
11153
  _Source: src/form/validation/operators.ts_
9722
11154
 
11155
+ ### validateLocale
11156
+
11157
+ **Kind:** `function`
11158
+
11159
+ Проверяет словарь: каждое сообщение разбирается, а при заданном образце — набор ключей и имена
11160
+ подстановок совпадают с ним. Нужна там, где компилятор не поможет: словарь пришёл по сети или
11161
+ написан вне репозитория. В рантайме вызывать не обязательно — битое или пропущенное сообщение
11162
+ и так откатится на встроенный английский; проверка нужна тестам и CI.
11163
+
11164
+ **Signature:**
11165
+ ```typescript
11166
+ export function validateLocale(messages: Messages, reference?: Messages): LocaleIssue[]
11167
+ ```
11168
+
11169
+ **Parameters:**
11170
+ - `messages` — - Проверяемый словарь.
11171
+ - `reference` — - Образец, обычно английский словарь того же пакета.
11172
+
11173
+ **Returns:** Список расхождений; пустой — словарь в порядке.
11174
+
11175
+ **Examples:**
11176
+
11177
+ ```ts
11178
+ expect(validateLocale(ruJson, enJson)).toEqual([]);
11179
+ ```
11180
+
11181
+ _Source: src/i18n/locale.ts_
11182
+
9723
11183
  ### validateModel
9724
11184
 
9725
11185
  **Kind:** `function`
@@ -9776,8 +11236,20 @@ _Source: src/form/validation/run.ts_
9776
11236
  ```typescript
9777
11237
  export interface ValidateOptions {
9778
11238
  /** Готовое сообщение об ошибке. Если не задано, валидаторы кладут `''`, и отображаемый текст
9779
- * резолвится из `code` (см. резолвер сообщений в `@reformer/cdk`). */
11239
+ * берётся из словаря локали по `code` (`validation.<code>`; см. `resolveValidationError` в
11240
+ * `@reformer/core/i18n`). Явное сообщение важнее словаря, но не переключается вместе с языком —
11241
+ * для локализуемого текста используйте {@link ValidateOptions.messageKey}. */
9780
11242
  message?: string;
11243
+ /**
11244
+ * Ключ сообщения в словаре приложения — локализуемый текст автора правила. Если ключ есть в
11245
+ * активной локали, он важнее и `message`, и текста по `code`; если нет — в ход идёт `message`.
11246
+ *
11247
+ * @example
11248
+ * ```ts
11249
+ * required({ messageKey: 'profile.name.required', message: 'Name is required' });
11250
+ * ```
11251
+ */
11252
+ messageKey?: string;
9781
11253
  /** Параметры ошибки (подстановка в шаблон сообщения / i18n). */
9782
11254
  params?: Record<string, FormValue>;
9783
11255
  }
@@ -9809,7 +11281,13 @@ _Source: src/form/validation/operators.ts_
9809
11281
  ```typescript
9810
11282
  export interface ValidationError {
9811
11283
  code: string;
11284
+ /**
11285
+ * Готовый текст ошибки. Пустая строка — «автор правила текста не задавал»: отображаемую строку
11286
+ * тогда даёт резолвер по `code` (словарь локали `validation.<code>`).
11287
+ */
9812
11288
  message: string;
11289
+ /** Ключ сообщения в словаре приложения; при наличии в активной локали важнее `message`. */
11290
+ messageKey?: string;
9813
11291
  params?: Record<string, FormValue>;
9814
11292
  /** Severity level: 'error' (default) blocks submission, 'warning' shows message but allows submission */
9815
11293
  severity?: 'error' | 'warning';
@@ -9929,3 +11407,16 @@ watchField(model.$.country, async (country) => {
9929
11407
  ```
9930
11408
 
9931
11409
  _Source: src/model/behaviors-value.ts_
11410
+
11411
+ ### WeekStart
11412
+
11413
+ **Kind:** `type`
11414
+
11415
+ День недели, с которого начинается календарь: `0` — воскресенье, `1` — понедельник.
11416
+
11417
+ **Signature:**
11418
+ ```typescript
11419
+ export type WeekStart = 0 | 1 | 2 | 3 | 4 | 5 | 6;
11420
+ ```
11421
+
11422
+ _Source: src/i18n/locale.ts_