@reformer/renderer-json 9.0.0-beta.1 → 9.0.0-beta.2

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.
package/llms.txt CHANGED
@@ -12,6 +12,7 @@
12
12
  - 05-cookbook.md — Cookbook
13
13
  - 06-validation.md — Validation
14
14
  - 07-form-wizard.md — FormWizard
15
+ - 08-i18n.md — i18n / Локализация
15
16
  - API Reference (auto-generated from JSDoc)
16
17
 
17
18
  ## 1. Installation
@@ -147,8 +148,10 @@ function MyFormPage() {
147
148
  - `'$model(path)'` — путь к полю/массиву модели (лист → `model.signalAt(path)`, массив → value-прокси массива).
148
149
  - `'$component(Name)'` — имя компонента в реестре (`reg.component`).
149
150
  - `'$dataSource(NAME)'` — имя registry-source (`reg.dataSource`): options, itemLabel, константы, loading-компоненты.
151
+ - `'$fn(name)'` — имя функции в реестре (`reg.fn`): форматтеры, компараторы, itemLabel, обработчики. Резолвится в саму функцию (передаётся в проп как есть). Отдельный от `$dataSource` вид — `validate` ловит перепутанные `$fn`/`$dataSource` и `reg.fn` бросает на не-функцию.
152
+ - `'$locale(key)'` — ключ строки для сервиса локализации (`reg.locale`). Резолвится в **строку на этапе конвертации** (`registry.getLocale().resolve(key)`); промах ключа / нет сервиса → сам ключ. Только `componentProps` (label/placeholder/title/aria-*), не структурные позиции. С параметрами — структурная форма `{ "$locale": "key", "params": { … } }` (объект, params — литералы). Реактивный/markdown-текст — компонент `$component(I18n)`, см. [08-i18n.md](08-i18n.md).
150
153
  - **`selector`** — plain-строка, id узла для render-behavior (`schema.node('…')`, `hideWhen`, `patchProps`). **Не** путь модели.
151
- - **`componentProps`** — что прокидывается в React-компонент. Значения могут содержать строки-операторы (`'$dataSource(NAME)'`, `'$model(...)'`, `'$component(...)'`) или вложенные `JsonNode` — конвертер резолвит их рекурсивно. Обычные значения (числа, инлайн-массивы options, `label`) идут как есть.
154
+ - **`componentProps`** — что прокидывается в React-компонент. Значения могут содержать строки-операторы (`'$dataSource(NAME)'`, `'$model(...)'`, `'$component(...)'`, `'$fn(...)'`, `'$locale(...)'`) или вложенные `JsonNode` — конвертер резолвит их рекурсивно. Обычные значения (числа, инлайн-массивы options, `label`) идут как есть.
152
155
 
153
156
  ## 8. Type Guards
154
157
 
@@ -206,7 +209,21 @@ const schema: JsonFormSchema = {
206
209
  }
207
210
  ```
208
211
 
209
- Массив с шаблоном элемента (`array` + `item.$template` + `initialValue`). Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива:
212
+ Функция из реестра (`$fn`) и локализованный текст (`$locale`) в `componentProps`:
213
+
214
+ ```typescript
215
+ {
216
+ value: '$model(email)',
217
+ component: '$component(Input)',
218
+ componentProps: {
219
+ label: '$locale(fields.email.label)', // → строка из сервиса локализации
220
+ placeholder: '$locale(fields.email.placeholder)',
221
+ format: '$fn(formatEmail)', // → сама функция из reg.fn, передаётся в проп
222
+ },
223
+ }
224
+ ```
225
+
226
+ Массив с шаблоном элемента (`array` + `item.$template` + `initialValue`). Внутри `$template` пути `$model(...)` резолвятся **относительно элемента** массива. `itemLabel` — функция `(control, index) => string`, поэтому идиоматично через `$fn`:
210
227
 
211
228
  ```typescript
212
229
  {
@@ -214,9 +231,9 @@ const schema: JsonFormSchema = {
214
231
  array: '$model(properties)',
215
232
  initialValue: { type: 'apartment', description: '', estimatedValue: 0, hasEncumbrance: false },
216
233
  componentProps: {
217
- title: 'Имущество',
218
- addButtonLabel: '+ Добавить имущество',
219
- itemLabel: '$dataSource(PROPERTY_ITEM_LABEL_SOURCE_FN)',
234
+ title: '$locale(properties.title)',
235
+ addButtonLabel: '$locale(properties.add)',
236
+ itemLabel: '$fn(propertyItemLabel)',
220
237
  },
221
238
  item: {
222
239
  $template: {
@@ -241,6 +258,10 @@ const schema: JsonFormSchema = {
241
258
  - **`initialValue` как FieldConfig** — это plain-литерал по форме элемента (`{ field: value }`), а не `{ value, component }`. Клонируется через `JSON.parse(JSON.stringify(...))`, поэтому только сериализуемые значения.
242
259
  - **Ссылаться на `$dataSource(NAME)` без регистрации** — при `validate` неизвестное имя даст ошибку; без валидации строка просто прокинется как есть (молчаливый баг).
243
260
  - **Класть `validators` в field-node** — `JsonFieldNode` несёт только layout, поля `validators` в нём нет и оператора `$validator(...)` не существует. Валидация значений живёт в отдельной TS-схеме над моделью (`validateFormModel`), а не в JSON — см. [06-validation.md](06-validation.md).
261
+ - **Путать `$fn` и `$dataSource`** — функции регистрируй через `reg.fn` и ссылайся через `$fn(name)`, данные — через `reg.dataSource`/`$dataSource(NAME)`. Перекрёстное использование (`$fn(LOAN_TYPES)`, `$dataSource(comparator)`) `validateFormSchema` отклонит, а рантайм бросит. `reg.fn` дополнительно бросает при регистрации не-функции.
262
+ - **Аргументы у `$fn`** — оператор передаёт функцию **по ссылке**; биндинга аргументов (`$fn(goToStep, 2)`) нет. Нужен предзаданный аргумент — зарегистрируй уже связанную функцию через `reg.fn`.
263
+ - **`$locale` для reactive-переключения языка** — ключ резолвится в строку **на этапе конвертации** (иначе signal уронил бы строковые компоненты). Смена языка = новый сервис в `reg.locale` + пересборка дерева, а не «живое» обновление. Для live-переключения и markdown/rich — компонент `$component(I18n)` + `LocaleProvider`, см. [08-i18n.md](08-i18n.md).
264
+ - **Динамический/составной ключ `$locale`** — ключ это статичный литерал (`$locale(fields.email.label)`); вложить в него `$model(...)`/выражение нельзя. При наличии каталога опечатка ключа ловится на `validate`; без каталога промах молча деградирует до самого ключа.
244
265
 
245
266
  ## 11. See also
246
267
 
@@ -254,6 +275,8 @@ const schema: JsonFormSchema = {
254
275
 
255
276
  - **component** — любой React-компонент, зарегистрированный под именем и доступный в схеме как `component: '$component(name)'`. Один метод `reg.component(name, Component)` регистрирует и компоненты-листья (Input/Select — узел несёт `value: '$model(path)'`), и контейнеры-обёртки (Box/Section/FormField — узел несёт `children`). Роль узла (лист vs контейнер) определяется **структурой узла в схеме** (`value` vs `children`), а не тем, как компонент зарегистрирован.
256
277
  - **dataSource value** — именованная константа, функция или React-компонент, на которые ссылаются строкой `'$dataSource(NAME)'` из `componentProps`. Регистрируется через `reg.dataSource(name, value)`.
278
+ - **fn** — функция (форматтер/компаратор/itemLabel/обработчик), на которую ссылаются строкой `'$fn(name)'` из `componentProps`. Регистрируется через `reg.fn(name, fn)`. Отдельный от `dataSource` вид: `reg.fn` бросает при регистрации не-функции, а `validateFormSchema` ловит перепутанные `$fn`/`$dataSource`. Рантайм передаёт функцию в проп по ссылке (как `$dataSource`-функцию), новизна — в статической проверке.
279
+ - **locale service** — сервис локализации для `$locale(key)` (строковый путь) и компонента `I18n` (реактивный путь). Регистрируется через `reg.locale(serviceOrResolver)` (ключ `LOCALE_SERVICE`) и/или подаётся в `LocaleProvider`. Интерфейс `LocaleService`: `resolve(key, params?) => string` (+ опциональный `render(key, params?) => ReactNode` для markdown/JSX, + `keys` для validate-проверки). Фабрики без зависимостей: `createLocaleResolver(catalog)` (плоский каталог), `createLocaleService(table)` (`ключ → (params) => строка` — параметры/склонение без ICU). ICU (`intl-messageformat`) и markdown подключает потребитель своей реализацией сервиса. Полный гид — [08-i18n.md](08-i18n.md).
257
280
  - **`FIELD_WRAPPER`** — зарезервированное имя (`'$fieldWrapper'`) для контейнера-обёртки полей (label, error, hint). Обычно регистрируется как `FormField` из `@reformer/ui-kit`.
258
281
 
259
282
  ## 13. Builder API
@@ -262,6 +285,8 @@ const schema: JsonFormSchema = {
262
285
  | -------------------------------- | ----------------------------------------------------- |
263
286
  | `reg.component(name, Component)` | Регистрирует компонент (лист или контейнер — роль решает структура узла). |
264
287
  | `reg.dataSource(name, value)` | Регистрирует dataSource-значение (константу или функцию). |
288
+ | `reg.fn(name, fn)` | Регистрирует функцию для `$fn(name)` (бросает на не-функцию). |
289
+ | `reg.locale(serviceOrResolver)` | Регистрирует единственный сервис локализации для `$locale(key)`. |
265
290
 
266
291
  ## 14. Examples
267
292
 
@@ -298,6 +323,38 @@ const registry = defineRegistry((reg) => {
298
323
  }
299
324
  ```
300
325
 
326
+ Функции (`$fn`) и сервис локализации (`$locale`):
327
+
328
+ ```typescript
329
+ import { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';
330
+
331
+ const registry = defineRegistry((reg) => {
332
+ reg.component('Input', Input);
333
+ // функции — форматтеры, компараторы, itemLabel
334
+ reg.fn('propertyItemLabel', (_control, index) => `Имущество #${index + 1}`);
335
+ reg.fn('formatCurrency', (v: number) => `${v} ₽`);
336
+ // единственный сервис локализации; каталог включает validate-time проверку ключей
337
+ reg.locale(
338
+ createLocaleResolver({
339
+ 'fields.email.label': 'Email',
340
+ 'fields.email.placeholder': 'you@example.com',
341
+ })
342
+ );
343
+ });
344
+
345
+ // В JSON-схеме:
346
+ {
347
+ value: '$model(email)',
348
+ component: '$component(Input)',
349
+ componentProps: {
350
+ label: '$locale(fields.email.label)', // → строка
351
+ format: '$fn(formatCurrency)', // → функция по ссылке
352
+ },
353
+ }
354
+ ```
355
+
356
+ Смена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale` (плюс пересборка дерева); `$locale` резолвится в строку при конвертации, «живого» переключения нет.
357
+
301
358
  ## 15. Anti-patterns
302
359
 
303
360
  - **Забыть зарегистрировать `FIELD_WRAPPER`** — поля будут рендериться без обёртки (нет label/error). В большинстве случаев это ошибка.
@@ -305,11 +362,13 @@ const registry = defineRegistry((reg) => {
305
362
  - **Ожидать наследования между вложенными `JsonRendererProvider`** — реестр сливается через `withParent`, дубли решаются по приоритету (внешний > внутренний).
306
363
  - **Использовать `$dataSource(NAME)` без регистрации** — без `validate` строка просто прокинется в проп как есть (молчаливый баг); с `validate` даст ошибку `unknown dataSource "NAME"`.
307
364
  - **Ссылаться на dataSource как на компонент** — `component: '$component(LoadingState)'`, где `LoadingState` зарегистрирован через `reg.dataSource`, бросит `Entry "..." is a 'dataSource' and cannot be used as $component(...)`. dataSource — только для значений в `componentProps`.
365
+ - **Регистрировать функцию как `dataSource` и ссылаться `$fn`** (или наоборот) — виды раздельны: `$fn(name)` резолвит только `reg.fn`-записи, `$dataSource(NAME)` — только `reg.dataSource`. Перекрёстная ссылка бросит `Entry "..." is a '...' and cannot be used as $fn(...)` и отклонится на `validate`.
366
+ - **Регистрировать несколько сервисов локализации** — сервис один (кладётся под `LOCALE_SERVICE`); повторный `reg.locale(...)` перезапишет предыдущий. Разные языки — это разные каталоги, передаваемые в `reg.locale` по одному за раз.
308
367
 
309
368
  ## 16. See also
310
369
 
311
370
  - [01-overview.md](01-overview.md) — как реестр прокидывается через `JsonRendererProvider`.
312
- - [02-json-schema.md](02-json-schema.md) — где имена компонентов появляются в схеме (операторы `$component`/`$dataSource`).
371
+ - [02-json-schema.md](02-json-schema.md) — где имена появляются в схеме (операторы `$component`/`$dataSource`/`$fn`/`$locale`).
313
372
 
314
373
  ## 17. Component "X" not found in registry
315
374
 
@@ -955,7 +1014,150 @@ Ground truth: golden `complex-multy-step-form-renderer/render-behavior.ts` — `
955
1014
  - renderer-react [03-render-behavior.md](../../../reformer-renderer-react/docs/llms/03-render-behavior.md) — семантика `hideWhen`/`renderEffect`/`onComponentEvent`/`onInit`.
956
1015
  - renderer-react [01-overview.md](../../../reformer-renderer-react/docs/llms/01-overview.md#multi-step-forms) — канонический `FormWizard`, форма шага `{ number, title, icon, body }`.
957
1016
 
958
- ## 47. API Reference
1017
+ ## 47. Два пути (важно выбрать правильный)
1018
+
1019
+ Значение попадает в один из двух приёмников, и это определяет путь:
1020
+
1021
+ | Приёмник | Что можно | Путь |
1022
+ |---|---|---|
1023
+ | **Строковый проп** (`label`, `placeholder`, `title`, `aria-*`) | только `string`, **статично** | `$locale(key)` / `{ $locale, params }` |
1024
+ | **Контент** (видимый текст, сообщения) | `string` **или** rich/JSX, **реактивно** | компонент `$component(I18n)` |
1025
+
1026
+ Причина: `componentProps` спредятся в компонент как есть — signal/ReactNode в `label` уронит его (`Objects are not valid as a React child`). Строковый путь резолвится один раз при конвертации (смена языка = пересборка дерева); реактивный путь читает сервис из React-контекста на этапе рендера (live-переключение).
1027
+
1028
+ ## 48. Сервис локализации
1029
+
1030
+ ```ts
1031
+ interface LocaleService {
1032
+ resolve(key: string, params?: Record<string, unknown>): string; // строка (обязателен)
1033
+ render?(key: string, params?: Record<string, unknown>): ReactNode; // rich/markdown (опционален)
1034
+ keys?: readonly string[]; // validate-time проверка ключей
1035
+ }
1036
+ ```
1037
+
1038
+ Фабрики (0-dependency):
1039
+
1040
+ ```ts
1041
+ import { createLocaleResolver, createLocaleService } from '@reformer/renderer-json';
1042
+
1043
+ // плоский каталог ключ→строка
1044
+ createLocaleResolver({ 'fields.email.label': 'Email' });
1045
+
1046
+ // таблица ключ→(params)=>строка — параметры и склонение без ICU (стиль cdk createMessageResolver)
1047
+ createLocaleService({
1048
+ 'fields.min': (p) => `Минимум ${p?.count} символов`,
1049
+ 'users.count': (p) => plural(Number(p?.count), ['пользователь', 'пользователя', 'пользователей']),
1050
+ });
1051
+ ```
1052
+
1053
+ ### ICU MessageFormat (подключает потребитель)
1054
+
1055
+ Стандарт для склонения по CLDR-правилам. Библиотека — `intl-messageformat`; сервис пишешь сам:
1056
+
1057
+ ```ts
1058
+ import { IntlMessageFormat } from 'intl-messageformat';
1059
+
1060
+ function createIcuLocaleService(catalog: Record<string, string>, locale: string): LocaleService {
1061
+ return {
1062
+ resolve: (key, params) =>
1063
+ key in catalog ? (new IntlMessageFormat(catalog[key], locale).format(params) as string) : key,
1064
+ keys: Object.keys(catalog),
1065
+ };
1066
+ }
1067
+ // catalog: { 'users.count': 'There {count, plural, one {is # user} other {are # users}}' }
1068
+ ```
1069
+
1070
+ ### Markdown / rich (подключает потребитель через `render`)
1071
+
1072
+ ```ts
1073
+ import ReactMarkdown from 'react-markdown';
1074
+
1075
+ const service: LocaleService = {
1076
+ resolve: (k) => catalog[k] ?? k,
1077
+ render: (k, p) => <ReactMarkdown>{format(catalog[k] ?? k, p)}</ReactMarkdown>,
1078
+ keys: Object.keys(catalog),
1079
+ };
1080
+ ```
1081
+
1082
+ ## 49. Путь 1 — строковые пропы (статично)
1083
+
1084
+ Без параметров — строковый оператор:
1085
+
1086
+ ```jsonc
1087
+ { "value": "$model(email)", "component": "$component(Input)",
1088
+ "componentProps": { "label": "$locale(fields.email.label)" } }
1089
+ ```
1090
+
1091
+ С параметрами — **структурная форма** (объект, не оператор — без парсинга аргументов):
1092
+
1093
+ ```jsonc
1094
+ { "value": "$model(password)", "component": "$component(Input)",
1095
+ "componentProps": { "label": { "$locale": "fields.min", "params": { "count": 8 } } } }
1096
+ // → resolve("fields.min", { count: 8 }) → "Минимум 8 символов"
1097
+ ```
1098
+
1099
+ Параметры — обычно **литералы** (путь статичный). Можно сослаться и на `$model(path)` в `params` — тогда берётся **снимок** текущего значения (`.peek()`, на момент конвертации) и печатается dev-warning: снимок не реактивен, при изменении модели строка не обновится. Для живого значения — `I18n` (путь 2).
1100
+
1101
+ Регистрация сервиса — в реестре (для конвертера):
1102
+
1103
+ ```ts
1104
+ const registry = defineRegistry((reg) => {
1105
+ reg.locale(createLocaleService({ 'fields.min': (p) => `Минимум ${p?.count} символов` }));
1106
+ });
1107
+ ```
1108
+
1109
+ ## 50. Путь 2 — реактивный компонент `I18n`
1110
+
1111
+ Для markdown/rich и/или реактивных параметров и live-переключения языка. Регистрируется как компонент, оборачивается `LocaleProvider`:
1112
+
1113
+ ```tsx
1114
+ import { I18n, LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';
1115
+
1116
+ const services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };
1117
+
1118
+ function App() {
1119
+ const [lang, setLang] = useState<'ru' | 'en'>('ru');
1120
+ const registry = useMemo(() => defineRegistry((reg) => {
1121
+ reg.component('I18n', I18n);
1122
+ // ...остальные компоненты
1123
+ }), []);
1124
+ return (
1125
+ <LocaleProvider service={services[lang]}>
1126
+ <JsonRendererProvider settings={{ registry, model }}>
1127
+ <JsonFormRenderer schema={schema} />
1128
+ </JsonRendererProvider>
1129
+ </LocaleProvider>
1130
+ );
1131
+ }
1132
+ ```
1133
+
1134
+ ```jsonc
1135
+ // в схеме — контейнер-узел; values.count приходит сигналом и обновляется вживую
1136
+ { "component": "$component(I18n)",
1137
+ "componentProps": { "id": "users.count", "values": { "count": "$model(userCount)" } } }
1138
+ ```
1139
+
1140
+ `I18n` рендерит `service.render?.()` (markdown/JSX) либо `service.resolve()` (строка) — формат решает сервис. Перерисовывается при смене сигнала `values.count` и при смене `service` в `LocaleProvider` (переключение языка).
1141
+
1142
+ ## 51. Валидация
1143
+
1144
+ `validateFormSchema` проверяет ключи `$locale(...)` и структурной формы `{ $locale }` против `localeKeys` (из `service.keys` или явного списка) — опечатка ключа ловится до рендера. Нет каталога → проверка мягко пропускается (ключи свободные).
1145
+
1146
+ ## 52. Anti-patterns
1147
+
1148
+ - **markdown/JSX в строковый проп** — `label: "$locale(...)"` с сервисом, возвращающим ReactNode, уронит компонент. Rich-контент только через `$component(I18n)` (или свой `$component(RichText)`).
1149
+ - **реактивный параметр в строковом `$locale`** — `{ $locale, params: { count: "$model(...)" } }` берётся снимком и **не обновляется** (путь статичный); в dev печатается warning. Для реактивных model-параметров → `I18n` с `values`.
1150
+ - **`I18n` без `LocaleProvider`** — без провайдера сервис по умолчанию отдаёт сам ключ (fallback-to-key). Оберни поддерево в `LocaleProvider`.
1151
+ - **ждать live-переключения от `$locale`** — строковый путь статичен; смена языка для label/placeholder требует пересборки (новый сервис в `reg.locale` + пере-render). Live только у `I18n`.
1152
+ - **тащить ICU/markdown в схему** — форматтер живёт в реализации `LocaleService` у потребителя, не в JSON.
1153
+
1154
+ ## 53. See also
1155
+
1156
+ - [02-json-schema.md](02-json-schema.md) — где `$locale`/`I18n` появляются в схеме.
1157
+ - [03-registry.md](03-registry.md) — `reg.locale`, `createLocaleService`, регистрация `I18n`.
1158
+ - [06-validation.md](06-validation.md) — валидация значений (отдельно от локализации).
1159
+
1160
+ ## 54. API Reference
959
1161
 
960
1162
  _Auto-generated from JSDoc on public exports._
961
1163
 
@@ -990,13 +1192,13 @@ _Source: src/schema/index.ts_
990
1192
 
991
1193
  **Kind:** `interface`
992
1194
 
993
- Запись реестра. Хранит сам компонент (или dataSource-значение) и его роль.
1195
+ Запись реестра. Хранит значение (компонент / dataSource / функцию / сервис локализации) и его роль.
994
1196
 
995
1197
  **Signature:**
996
1198
  ```typescript
997
1199
  export interface ComponentMetadata<P = any> {
998
1200
  component: ComponentType<P> | unknown;
999
- type: 'component' | 'dataSource';
1201
+ type: 'component' | 'dataSource' | 'fn' | 'locale';
1000
1202
  description?: string;
1001
1203
  }
1002
1204
  ```
@@ -1037,6 +1239,12 @@ Read-only API реестра, доступное в рантайме.
1037
1239
  export interface ComponentRegistry {
1038
1240
  get(name: string): ComponentMetadata | undefined;
1039
1241
  getDataSource<T = unknown>(name: string): T | undefined;
1242
+ /**
1243
+ * Единственный сервис локализации (для оператора `$locale(key)`), если зарегистрирован через
1244
+ * `reg.locale(...)`. Опционален — сторонние реализации `ComponentRegistry` без него продолжают
1245
+ * компилироваться; при его отсутствии `$locale` мягко деградирует до самого ключа.
1246
+ */
1247
+ getLocale?(): LocaleService | undefined;
1040
1248
  has(name: string): boolean;
1041
1249
  names(): string[];
1042
1250
  }
@@ -1091,6 +1299,85 @@ behavior,
1091
1299
 
1092
1300
  _Source: src/converter/json-to-render-schema.ts_
1093
1301
 
1302
+ ### createLocaleResolver
1303
+
1304
+ **Kind:** `function`
1305
+
1306
+ Строит {@link LocaleService} из каталога `ключ → строка`. Промах ключа → сам ключ (fallback-to-key,
1307
+ зеркало `createMessageResolver` из `@reformer/cdk`). `keys` каталога включает validate-time проверку
1308
+ опечаток. Смена языка — пересобрать сервис на другом каталоге и передать новый ref в `reg.locale`.
1309
+
1310
+ **Signature:**
1311
+ ```typescript
1312
+ export function createLocaleResolver(catalog: Record<string, string>): LocaleService
1313
+ ```
1314
+
1315
+ **Parameters:**
1316
+ - `catalog` — - Таблица `ключ → локализованная строка`.
1317
+
1318
+ **Returns:**
1319
+
1320
+ **Examples:**
1321
+
1322
+ ```ts
1323
+ import { defineRegistry } from '@reformer/renderer-json';
1324
+ import { createLocaleResolver } from '@reformer/renderer-json';
1325
+
1326
+ const registry = defineRegistry((reg) => {
1327
+ reg.locale(createLocaleResolver({
1328
+ 'fields.email.label': 'Email',
1329
+ 'fields.email.placeholder': 'you@example.com',
1330
+ }));
1331
+ });
1332
+ // В схеме: componentProps: { label: '$locale(fields.email.label)' }
1333
+ // '$locale(fields.unknown)' при наличии каталога → ошибка на validateFormSchema.
1334
+ ```
1335
+
1336
+ _Source: src/locale/locale-service.ts_
1337
+
1338
+ ### createLocaleService
1339
+
1340
+ **Kind:** `function`
1341
+
1342
+ Строит {@link LocaleService} из таблицы `ключ → (params) => строка` — 0-dependency вариант для
1343
+ параметров и склонения без ICU (стиль `createMessageResolver` из `@reformer/cdk`). Промах ключа →
1344
+ сам ключ. `keys` таблицы включает validate-time проверку опечаток.
1345
+
1346
+ Для ICU-склонения по CLDR-правилам подключи `intl-messageformat` в СВОЁМ сервисе:
1347
+ `reg.locale({ resolve: (k, p) => new IntlMessageFormat(catalog[k], lng).format(p) as string, keys })`.
1348
+ Для markdown добавь `render` (напр. через `react-markdown`).
1349
+
1350
+ **Signature:**
1351
+ ```typescript
1352
+ export function createLocaleService(
1353
+ table: Record<string, (params?: LocaleParams) => string>
1354
+ ): LocaleService
1355
+ ```
1356
+
1357
+ **Parameters:**
1358
+ - `table` — - Таблица `ключ → (params) => локализованная строка`.
1359
+
1360
+ **Returns:**
1361
+
1362
+ **Examples:**
1363
+
1364
+ ```ts
1365
+ import { createLocaleService } from '@reformer/renderer-json';
1366
+
1367
+ const ru = createLocaleService({
1368
+ 'fields.min': (p) => `Минимум ${p?.count} символов`,
1369
+ 'users.count': (p) => {
1370
+ const n = Number(p?.count);
1371
+ const forms = ['пользователь', 'пользователя', 'пользователей'];
1372
+ const idx = n % 10 === 1 && n % 100 !== 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2;
1373
+ return `${n} ${forms[idx]}`;
1374
+ },
1375
+ });
1376
+ ru.resolve('fields.min', { count: 3 }); // 'Минимум 3 символов'
1377
+ ```
1378
+
1379
+ _Source: src/locale/locale-service.ts_
1380
+
1094
1381
  ### createRenderSchemaFromJsonM1
1095
1382
 
1096
1383
  **Kind:** `function`
@@ -1143,6 +1430,19 @@ export type DataSourceOp = `$dataSource(${string})`;
1143
1430
 
1144
1431
  _Source: src/operators.ts_
1145
1432
 
1433
+ ### defaultLocaleResolver
1434
+
1435
+ **Kind:** `const`
1436
+
1437
+ Дефолтный резолвер: отдаёт сам ключ (fallback-to-key). Применяется, когда сервис не зарегистрирован.
1438
+
1439
+ **Signature:**
1440
+ ```typescript
1441
+ export const defaultLocaleResolver: LocaleResolver
1442
+ ```
1443
+
1444
+ _Source: src/locale/locale-service.ts_
1445
+
1146
1446
  ### defineRegistry
1147
1447
 
1148
1448
  **Kind:** `function`
@@ -1211,6 +1511,19 @@ const registry = defineRegistry((reg) => {
1211
1511
 
1212
1512
  _Source: src/registry/constants.ts_
1213
1513
 
1514
+ ### FnOp
1515
+
1516
+ **Kind:** `type`
1517
+
1518
+ Строка-оператор ссылки на функцию реестра: `` `$fn(${name})` ``.
1519
+
1520
+ **Signature:**
1521
+ ```typescript
1522
+ export type FnOp = `$fn(${string})`;
1523
+ ```
1524
+
1525
+ _Source: src/operators.ts_
1526
+
1214
1527
  ### formSchemaMetaSchema
1215
1528
 
1216
1529
  **Kind:** `const`
@@ -1285,6 +1598,87 @@ getDataSourceNames(registry); // ['LOAN_TYPES', 'GENDERS', 'LoadingState', ...]
1285
1598
 
1286
1599
  _Source: src/schema/index.ts_
1287
1600
 
1601
+ ### getFnNames
1602
+
1603
+ **Kind:** `function`
1604
+
1605
+ Имена функций реестра (`reg.fn`) — то, что валидно в `$fn(...)`: форматтеры/компараторы/itemLabel/
1606
+ обработчики. Отдельно от {@link getDataSourceNames}, поэтому `validateFormSchema` ловит перепутанные
1607
+ `$fn`/`$dataSource`.
1608
+
1609
+ **Signature:**
1610
+ ```typescript
1611
+ export function getFnNames(registry: ComponentRegistry): string[]
1612
+ ```
1613
+
1614
+ **Parameters:**
1615
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1616
+
1617
+ **Returns:** Массив имён функций.
1618
+
1619
+ _Source: src/schema/index.ts_
1620
+
1621
+ ### getLocaleKeys
1622
+
1623
+ **Kind:** `function`
1624
+
1625
+ Известные ключи локализации сервиса реестра (`reg.locale` с каталогом) — то, что валидно в
1626
+ `$locale(...)`. `undefined`, если сервис не зарегистрирован или задан голым резолвером без `keys`
1627
+ (тогда `validateFormSchema` мягко пропускает проверку ключей, как для `$model`-путей).
1628
+
1629
+ **Signature:**
1630
+ ```typescript
1631
+ export function getLocaleKeys(registry: ComponentRegistry): readonly string[] | undefined
1632
+ ```
1633
+
1634
+ **Parameters:**
1635
+ - `registry` — - Реестр (см. {@link defineRegistry}).
1636
+
1637
+ **Returns:** Массив ключей либо `undefined`.
1638
+
1639
+ _Source: src/schema/index.ts_
1640
+
1641
+ ### I18n
1642
+
1643
+ **Kind:** `function`
1644
+
1645
+ Реактивный локализованный текст. Читает {@link LocaleService} из {@link LocaleProvider} (live-смена
1646
+ языка) и разворачивает `values`-сигналы (live-обновление параметров). Если сервис умеет `render` —
1647
+ рендерит rich-контент (markdown/JSX), иначе строку через `resolve`.
1648
+
1649
+ **Signature:**
1650
+ ```typescript
1651
+ export function I18n({ id, values }: I18nProps): ReactNode
1652
+ ```
1653
+
1654
+ **Parameters:**
1655
+ - `props` — - {@link I18nProps} (`id` + опциональные `values`).
1656
+
1657
+ **Returns:** Локализованный `ReactNode`.
1658
+
1659
+ _Source: src/locale/i18n.tsx_
1660
+
1661
+ ### I18nProps
1662
+
1663
+ **Kind:** `interface`
1664
+
1665
+ Props {@link I18n}.
1666
+
1667
+ **Signature:**
1668
+ ```typescript
1669
+ export interface I18nProps {
1670
+ /** Ключ сообщения в сервисе локализации. */
1671
+ id: string;
1672
+ /**
1673
+ * Параметры интерполяции/склонения. Значения могут быть сигналами (`$model(...)`) — тогда текст
1674
+ * обновляется при их изменении — или литералами.
1675
+ */
1676
+ values?: LocaleParams;
1677
+ }
1678
+ ```
1679
+
1680
+ _Source: src/locale/i18n.tsx_
1681
+
1288
1682
  ### isArrayNode
1289
1683
 
1290
1684
  **Kind:** `function`
@@ -1422,6 +1816,58 @@ node.component; // ComponentOp | undefined
1422
1816
 
1423
1817
  _Source: src/types/json-schema.ts_
1424
1818
 
1819
+ ### isFnOp
1820
+
1821
+ **Kind:** `const`
1822
+
1823
+ Type-guard: строка — оператор `"$fn(...)"` (ссылка на функцию реестра).
1824
+
1825
+ **Signature:**
1826
+ ```typescript
1827
+ export const isFnOp
1828
+ ```
1829
+
1830
+ **Parameters:**
1831
+ - `v` — - Проверяемое значение.
1832
+
1833
+ **Returns:** `true`, если `v` — {@link FnOp}.
1834
+
1835
+ **Examples:**
1836
+
1837
+ ```ts
1838
+ if (isFnOp(props.itemLabel)) {
1839
+ const name = parseOperator(props.itemLabel)!.arg; // 'propertyItemLabel'
1840
+ }
1841
+ ```
1842
+
1843
+ _Source: src/operators.ts_
1844
+
1845
+ ### isLocaleOp
1846
+
1847
+ **Kind:** `const`
1848
+
1849
+ Type-guard: строка — оператор `"$locale(...)"` (ключ строки для сервиса локализации).
1850
+
1851
+ **Signature:**
1852
+ ```typescript
1853
+ export const isLocaleOp
1854
+ ```
1855
+
1856
+ **Parameters:**
1857
+ - `v` — - Проверяемое значение.
1858
+
1859
+ **Returns:** `true`, если `v` — {@link LocaleOp}.
1860
+
1861
+ **Examples:**
1862
+
1863
+ ```ts
1864
+ if (isLocaleOp(props.label)) {
1865
+ const key = parseOperator(props.label)!.arg; // 'fields.email.label'
1866
+ }
1867
+ ```
1868
+
1869
+ _Source: src/operators.ts_
1870
+
1425
1871
  ### isModelOp
1426
1872
 
1427
1873
  **Kind:** `const`
@@ -1686,7 +2132,7 @@ _Source: src/types/json-schema.ts_
1686
2132
 
1687
2133
  **Signature:**
1688
2134
  ```typescript
1689
- export type JsonOperator = ModelOp | ComponentOp | DataSourceOp;
2135
+ export type JsonOperator = ModelOp | ComponentOp | DataSourceOp | FnOp | LocaleOp;
1690
2136
  ```
1691
2137
 
1692
2138
  _Source: src/operators.ts_
@@ -1764,6 +2210,146 @@ export interface JsonRendererSettings extends RendererSettings {
1764
2210
 
1765
2211
  _Source: src/context/json-renderer-context.tsx_
1766
2212
 
2213
+ ### LOCALE_SERVICE
2214
+
2215
+ **Kind:** `const`
2216
+
2217
+ Зарезервированный ключ реестра для единственного сервиса локализации.
2218
+
2219
+ `reg.locale(...)` кладёт сервис под этот ключ; оператор `"$locale(key)"` резолвится через него
2220
+ (`registry.getLocale()`). Ключ намеренно отличается от имени оператора, чтобы `$dataSource`/`$fn`
2221
+ не могли достать сервис локализации по имени.
2222
+
2223
+ **Signature:**
2224
+ ```typescript
2225
+ export const LOCALE_SERVICE
2226
+ ```
2227
+
2228
+ **Examples:**
2229
+
2230
+ ```typescript
2231
+ import { defineRegistry, createLocaleResolver } from '@reformer/renderer-json';
2232
+
2233
+ const registry = defineRegistry((reg) => {
2234
+ reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));
2235
+ });
2236
+ ```
2237
+
2238
+ _Source: src/registry/constants.ts_
2239
+
2240
+ ### LocaleOp
2241
+
2242
+ **Kind:** `type`
2243
+
2244
+ Строка-оператор ссылки на ключ локализации: `` `$locale(${key})` ``.
2245
+
2246
+ **Signature:**
2247
+ ```typescript
2248
+ export type LocaleOp = `$locale(${string})`;
2249
+ ```
2250
+
2251
+ _Source: src/operators.ts_
2252
+
2253
+ ### LocaleParams
2254
+
2255
+ **Kind:** `type`
2256
+
2257
+ Параметры для интерполяции/склонения (ICU-values, i18next-values и т.п.).
2258
+
2259
+ **Signature:**
2260
+ ```typescript
2261
+ export type LocaleParams = Record<string, unknown>;
2262
+ ```
2263
+
2264
+ _Source: src/locale/locale-service.ts_
2265
+
2266
+ ### LocaleProvider
2267
+
2268
+ **Kind:** `function`
2269
+
2270
+ Провайдер сервиса локализации для реактивного пути. Оберни форму (или её часть) и передавай
2271
+ новый `service` при смене языка — вложенные `I18n` перерисуются без пересборки схемы.
2272
+
2273
+ **Signature:**
2274
+ ```typescript
2275
+ export function LocaleProvider({ service, children }: LocaleProviderProps): ReactNode
2276
+ ```
2277
+
2278
+ **Examples:**
2279
+
2280
+ Переключение языка
2281
+ ```tsx
2282
+ import { LocaleProvider, createLocaleService, JsonFormRenderer } from '@reformer/renderer-json';
2283
+
2284
+ const services = { ru: createLocaleService(ruTable), en: createLocaleService(enTable) };
2285
+ function App() {
2286
+ const [lang, setLang] = useState<'ru' | 'en'>('ru');
2287
+ return (
2288
+ <LocaleProvider service={services[lang]}>
2289
+ <JsonFormRenderer schema={schema} />
2290
+ </LocaleProvider>
2291
+ );
2292
+ }
2293
+ ```
2294
+
2295
+ _Source: src/locale/locale-context.tsx_
2296
+
2297
+ ### LocaleProviderProps
2298
+
2299
+ **Kind:** `interface`
2300
+
2301
+ Props {@link LocaleProvider}.
2302
+
2303
+ **Signature:**
2304
+ ```typescript
2305
+ export interface LocaleProviderProps {
2306
+ /** Текущий сервис локализации. Смена ссылки (напр. при переключении языка) перерендеривает все `I18n`. */
2307
+ service: LocaleService;
2308
+ /** Поддерево, в котором `I18n`/`useLocale` видят этот сервис. */
2309
+ children: ReactNode;
2310
+ }
2311
+ ```
2312
+
2313
+ _Source: src/locale/locale-context.tsx_
2314
+
2315
+ ### LocaleResolver
2316
+
2317
+ **Kind:** `type`
2318
+
2319
+ Резолвер ключа (+опциональные параметры) в строку.
2320
+
2321
+ **Signature:**
2322
+ ```typescript
2323
+ export type LocaleResolver = (key: string, params?: LocaleParams) => string;
2324
+ ```
2325
+
2326
+ _Source: src/locale/locale-service.ts_
2327
+
2328
+ ### LocaleService
2329
+
2330
+ **Kind:** `interface`
2331
+
2332
+ Сервис локализации в реестре / контексте. `resolve` обязателен (строковый путь); `render`
2333
+ опционален (rich/markdown-путь через `Trans`/`RichText`); `keys` (если задан — напр., из каталога)
2334
+ включает проверку опечаток ключей на этапе `validateFormSchema`.
2335
+
2336
+ **Signature:**
2337
+ ```typescript
2338
+ export interface LocaleService {
2339
+ /** Ключ (+params) → локализованная строка (при промахе — сам ключ). Для label/placeholder и fallback `Trans`. */
2340
+ resolve: LocaleResolver;
2341
+ /**
2342
+ * Ключ (+params) → rich-контент (markdown/JSX). Опционален. Используется `Trans`/`RichText`, когда
2343
+ * нужен не голый текст, а разметка. Реализуется потребителем (напр. через `react-markdown`).
2344
+ */
2345
+ render?(key: string, params?: LocaleParams): ReactNode;
2346
+ /** Множество известных ключей. Есть → `validate` ловит `unknown locale key`; нет → проверка мягко пропускается (как для `$model`). */
2347
+ keys?: readonly string[];
2348
+ }
2349
+ ```
2350
+
2351
+ _Source: src/locale/locale-service.ts_
2352
+
1767
2353
  ### ModelOp
1768
2354
 
1769
2355
  **Kind:** `type`
@@ -1781,12 +2367,12 @@ _Source: src/operators.ts_
1781
2367
 
1782
2368
  **Kind:** `interface`
1783
2369
 
1784
- Разобранный оператор: тип + аргумент (путь/имя).
2370
+ Разобранный оператор: тип + аргумент (путь/имя/ключ).
1785
2371
 
1786
2372
  **Signature:**
1787
2373
  ```typescript
1788
2374
  export interface ParsedOperator {
1789
- op: 'model' | 'component' | 'dataSource';
2375
+ op: 'model' | 'component' | 'dataSource' | 'fn' | 'locale';
1790
2376
  arg: string;
1791
2377
  }
1792
2378
  ```
@@ -1838,6 +2424,19 @@ export interface RegistryBuilder {
1838
2424
  */
1839
2425
  component<P>(name: string, component: ComponentType<P>, description?: string): void;
1840
2426
  dataSource<T>(name: string, value: T, description?: string): void;
2427
+ /**
2428
+ * Зарегистрировать функцию под именем — доступна в схеме как `$fn(name)` (форматтеры,
2429
+ * компараторы, `itemLabel`, обработчики). Отдельный от `dataSource` вид: бросает при регистрации,
2430
+ * если передана не функция, и `validateFormSchema` отклоняет перепутанные `$fn`/`$dataSource`.
2431
+ */
2432
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2433
+ fn<F extends (...args: any[]) => any>(name: string, fn: F, description?: string): void;
2434
+ /**
2435
+ * Зарегистрировать единственный сервис локализации для оператора `$locale(key)`. Принимает
2436
+ * {@link LocaleService} (каталог через `createLocaleResolver` — включает validate-time проверку
2437
+ * ключей) либо голый резолвер `(key) => string`. Кладётся под зарезервированный ключ `LOCALE_SERVICE`.
2438
+ */
2439
+ locale(service: LocaleService | LocaleResolver, description?: string): void;
1841
2440
  }
1842
2441
  ```
1843
2442
 
@@ -1848,6 +2447,8 @@ defineRegistry((reg: RegistryBuilder) => {
1848
2447
  reg.component('Input', Input);
1849
2448
  reg.component('Box', Box);
1850
2449
  reg.dataSource('LOAN_TYPES', LOAN_TYPES);
2450
+ reg.fn('formatCurrency', formatCurrency);
2451
+ reg.locale(createLocaleResolver({ 'fields.email.label': 'Email' }));
1851
2452
  });
1852
2453
  ```
1853
2454
 
@@ -1901,6 +2502,20 @@ export interface SchemaErrorPanelProps {
1901
2502
 
1902
2503
  _Source: src/components/schema-error-panel.tsx_
1903
2504
 
2505
+ ### unwrapSignalValues
2506
+
2507
+ **Kind:** `function`
2508
+
2509
+ Разворачивает значения-сигналы в обычные (читает `.value`), литералы отдаёт как есть. Если сигналов
2510
+ не было — возвращает исходный объект (стабильная ссылка). Pure — тестируется без DOM.
2511
+
2512
+ **Signature:**
2513
+ ```typescript
2514
+ export function unwrapSignalValues(values?: LocaleParams): LocaleParams | undefined
2515
+ ```
2516
+
2517
+ _Source: src/locale/use-signal-value.ts_
2518
+
1904
2519
  ### useJsonRendererSettings
1905
2520
 
1906
2521
  **Kind:** `function`
@@ -1927,3 +2542,39 @@ function MyControl() {
1927
2542
  ```
1928
2543
 
1929
2544
  _Source: src/context/json-renderer-context.tsx_
2545
+
2546
+ ### useLocale
2547
+
2548
+ **Kind:** `function`
2549
+
2550
+ Текущий сервис локализации из {@link LocaleProvider}. Без провайдера — fallback-to-key.
2551
+ Используется внутри {@link I18n}; вызывай напрямую для собственного localized-компонента.
2552
+
2553
+ **Signature:**
2554
+ ```typescript
2555
+ export function useLocale(): LocaleService
2556
+ ```
2557
+
2558
+ **Returns:** Активный {@link LocaleService}.
2559
+
2560
+ _Source: src/locale/locale-context.tsx_
2561
+
2562
+ ### useSignalValues
2563
+
2564
+ **Kind:** `function`
2565
+
2566
+ Реактивно разворачивает `values`-сигналы. Перерендер компонента при изменении любого сигнала;
2567
+ литералы проходят как есть. Снапшот кэшируется (стабильная ссылка при неизменных значениях) —
2568
+ иначе `useSyncExternalStore` зациклится.
2569
+
2570
+ **Signature:**
2571
+ ```typescript
2572
+ export function useSignalValues(values?: LocaleParams): LocaleParams | undefined
2573
+ ```
2574
+
2575
+ **Parameters:**
2576
+ - `values` — - Параметры сообщения (сигналы и/или литералы).
2577
+
2578
+ **Returns:** Развёрнутые значения для передачи в `LocaleService.resolve/render`.
2579
+
2580
+ _Source: src/locale/use-signal-value.ts_