# @json-render/directives

Pre-built custom directives for `@json-render/core`. Drop them into your catalog and renderer to add formatting, math, string manipulation, and i18n.

## Install

```bash
npm install @json-render/directives
```

## Quick Start

```typescript

// Wire into prompt generation
const prompt = catalog.prompt({ directives: standardDirectives });

// Wire into the renderer
<JSONUIProvider spec={spec} directives={standardDirectives}>
  ...
</JSONUIProvider>
```

To add factory directives like `createI18nDirective`, spread the array:

```typescript

const directives = [...standardDirectives, createI18nDirective(config)];
```

## Directives

### `$format` — Locale-aware value formatting

Formats values using `Intl` formatters. Supports `date`, `currency`, `number`, and `percent`.

```json
{ "$format": "currency", "value": { "$state": "/cart/total" }, "currency": "USD" }
```

```json
{ "$format": "date", "value": { "$state": "/user/createdAt" } }
```

```json
{ "$format": "number", "value": 1234567, "notation": "compact" }
```

```json
{ "$format": "percent", "value": 0.75 }
```

Relative dates are also supported:

```json
{ "$format": "date", "value": { "$state": "/post/createdAt" }, "style": "relative" }
```

This returns strings like `"3h ago"`, `"2d from now"`, or `"just now"`.

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$format</code></td>
      <td><code>{'\"date\" | \"currency\" | \"number\" | \"percent\"'}</code></td>
      <td>Format type.</td>
    </tr>
    <tr>
      <td><code>value</code></td>
      <td><code>unknown</code></td>
      <td>Value to format. Accepts any dynamic expression.</td>
    </tr>
    <tr>
      <td><code>locale</code></td>
      <td><code>string</code></td>
      <td>Optional. Locale for formatting (e.g. <code>"en-US"</code>).</td>
    </tr>
    <tr>
      <td><code>currency</code></td>
      <td><code>string</code></td>
      <td>Optional. Currency code for <code>"currency"</code> format. Default: <code>"USD"</code>.</td>
    </tr>
    <tr>
      <td><code>notation</code></td>
      <td><code>string</code></td>
      <td>Optional. Notation for <code>"number"</code> format (e.g. <code>"compact"</code>).</td>
    </tr>
    <tr>
      <td><code>style</code></td>
      <td><code>string</code></td>
      <td>Optional. Set to <code>"relative"</code> for relative date formatting.</td>
    </tr>
    <tr>
      <td><code>options</code></td>
      <td><code>{'Record<string, unknown>'}</code></td>
      <td>Optional. Extra <code>Intl</code> formatter options.</td>
    </tr>
  </tbody>
</table>

### `$math` — Arithmetic operations

Performs arithmetic on one or two operands. Operands accept any dynamic expression.

```json
{ "$math": "add", "a": { "$state": "/subtotal" }, "b": { "$state": "/tax" } }
```

```json
{ "$math": "round", "a": 3.7 }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$math</code></td>
      <td><code>{'\"add\" | \"subtract\" | \"multiply\" | \"divide\" | \"mod\" | \"min\" | \"max\" | \"round\" | \"floor\" | \"ceil\" | \"abs\"'}</code></td>
      <td>Operation to perform.</td>
    </tr>
    <tr>
      <td><code>a</code></td>
      <td><code>unknown</code></td>
      <td>First operand. Defaults to <code>0</code> if missing.</td>
    </tr>
    <tr>
      <td><code>b</code></td>
      <td><code>unknown</code></td>
      <td>Second operand (binary ops only). Defaults to <code>0</code> if missing.</td>
    </tr>
  </tbody>
</table>

Unary operations (`round`, `floor`, `ceil`, `abs`) only use `a`. Division by zero returns `0`.

### `$concat` — String concatenation

Concatenates multiple values into a single string. Each element is resolved then joined.

```json
{ "$concat": [{ "$state": "/user/firstName" }, " ", { "$state": "/user/lastName" }] }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$concat</code></td>
      <td><code>{'unknown[]'}</code></td>
      <td>Array of values to concatenate. Each is resolved, converted to string, and joined.</td>
    </tr>
  </tbody>
</table>

### `$count` — Array/string length

Returns the length of an array or string. Returns `0` for other types.

```json
{ "$count": { "$state": "/cart/items" } }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$count</code></td>
      <td><code>unknown</code></td>
      <td>Value to count. Accepts arrays and strings.</td>
    </tr>
  </tbody>
</table>

### `$truncate` — Text truncation

Truncates text to a maximum length with a configurable suffix.

```json
{ "$truncate": { "$state": "/post/body" }, "length": 140, "suffix": "..." }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$truncate</code></td>
      <td><code>unknown</code></td>
      <td>Value to truncate.</td>
    </tr>
    <tr>
      <td><code>length</code></td>
      <td><code>number</code></td>
      <td>Optional. Max character length. Default: <code>100</code>.</td>
    </tr>
    <tr>
      <td><code>suffix</code></td>
      <td><code>string</code></td>
      <td>Optional. Suffix to append when truncated. Default: <code>"..."</code>.</td>
    </tr>
  </tbody>
</table>

### `$pluralize` — Singular/plural forms

Selects a singular, plural, or zero form based on a count.

```json
{ "$pluralize": { "$state": "/cart/itemCount" }, "one": "item", "other": "items", "zero": "no items" }
```

Output: `"3 items"`, `"1 item"`, or `"no items"`.

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$pluralize</code></td>
      <td><code>unknown</code></td>
      <td>Count value. Accepts dynamic expressions.</td>
    </tr>
    <tr>
      <td><code>one</code></td>
      <td><code>string</code></td>
      <td>Singular form label.</td>
    </tr>
    <tr>
      <td><code>other</code></td>
      <td><code>string</code></td>
      <td>Plural form label.</td>
    </tr>
    <tr>
      <td><code>zero</code></td>
      <td><code>string</code></td>
      <td>Optional. Label for count of zero. If omitted, uses <code>"0 {'<other>'}"</code>.</td>
    </tr>
  </tbody>
</table>

### `$join` — Join array elements

Joins array elements with a separator string.

```json
{ "$join": { "$state": "/tags" }, "separator": ", " }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$join</code></td>
      <td><code>unknown</code></td>
      <td>Array to join. Non-array values are converted to string.</td>
    </tr>
    <tr>
      <td><code>separator</code></td>
      <td><code>string</code></td>
      <td>Optional. Separator between elements. Default: <code>", "</code>.</td>
    </tr>
  </tbody>
</table>

### `createI18nDirective` — Internationalization

Factory function that creates a `$t` directive for translations with `{'{{param}}'}` interpolation.

```typescript

const tDirective = createI18nDirective({
  locale: 'en',
  messages: {
    en: { "greeting": "Hello, {'{{name}}'}!", "checkout.submit": "Place Order" },
    es: { "greeting": "Hola, {'{{name}}'}!", "checkout.submit": "Realizar Pedido" },
  },
  fallbackLocale: 'en',
});
```

Usage in specs:

```json
{ "$t": "checkout.submit" }
```

```json
{ "$t": "greeting", "params": { "name": { "$state": "/user/name" } } }
```

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>$t</code></td>
      <td><code>string</code></td>
      <td>Translation key.</td>
    </tr>
    <tr>
      <td><code>params</code></td>
      <td><code>{'Record<string, unknown>'}</code></td>
      <td>Optional. Interpolation parameters. Values accept dynamic expressions.</td>
    </tr>
  </tbody>
</table>

#### `I18nConfig`

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code>locale</code></td>
      <td><code>string</code></td>
      <td>Current locale (e.g. <code>"en"</code>).</td>
    </tr>
    <tr>
      <td><code>messages</code></td>
      <td><code>{'Record<string, Record<string, string>>'}</code></td>
      <td>Map of locale to key-value translation pairs.</td>
    </tr>
    <tr>
      <td><code>fallbackLocale</code></td>
      <td><code>string</code></td>
      <td>Optional. Fallback locale when a key is missing in the current locale.</td>
    </tr>
  </tbody>
</table>

## Composition

Directives compose naturally. Each resolver calls `resolvePropValue` on its inputs, so you can nest directives:

```json
{
  "$format": "currency",
  "value": { "$math": "multiply", "a": { "$state": "/price" }, "b": { "$state": "/qty" } },
  "currency": "USD"
}
```

```json
{
  "$pluralize": { "$count": { "$state": "/items" } },
  "one": "item",
  "other": "items"
}
```