# Helpers (https://v2.s3.dimah.dev/docs/react/helpers)

Helpers come from three places:

- **`@dimah-s3/react`** — formatter hooks that follow the `S3Provider` `locale` and translations.
- **`@dimah-s3/react/engine`** — plain formatters with no React import, such as `formatFileSize`.
- **`@dimah-s3/core`** — protocol helpers: validation, checksums, and errors.

## Formatter hooks [#formatter-hooks]

Each hook returns a formatter. Digits and unit labels follow the
`S3Provider` `locale` and your translations; see
[Internationalization](https://v2.s3.dimah.dev/docs/i18n). The built-in UI uses the same hooks.

```tsx
"use client";

import {
  useFormatDate,
  useFormatEta,
  useFormatFileSize,
  useFormatNumber,
  useFormatSpeed,
  type UploadFileState,
} from "@dimah-s3/react";

export function FileLine({ file }: { file: UploadFileState }) {
  const formatFileSize = useFormatFileSize();
  const formatSpeed = useFormatSpeed();
  const formatEta = useFormatEta();
  const formatNumber = useFormatNumber();
  const formatDate = useFormatDate();
  const { speed } = file.progress;

  return (
    <p>
      {file.fileName} · {formatFileSize(file.fileSize)} ·{" "}
      {formatNumber(file.progress.percent)}%
      {speed ? ` · ${formatSpeed(speed)}` : null}
      {formatEta(file.progress) ? ` · ${formatEta(file.progress)}` : null}
      {file.result?.lastModified
        ? ` · ${formatDate(file.result.lastModified)}`
        : null}
    </p>
  );
}
```

- **`useFormatFileSize()`** — `12.4 MB`.
- **`useFormatNumber()`** — counts in the locale's digits.
- **`useFormatSpeed()`** — `1.2 MB/s`.
- **`useFormatEta()`** — `9s left`, `4m left`, `1h 30m left`. It takes a `TransferProgress` and returns `null` until the speed is known.
- **`useFormatDate()`** — a `Date`, an ISO string, or epoch milliseconds, with optional `Intl.DateTimeFormat` options. Invalid input returns `""`.
- **`useFormatAcceptLabels()`** — short labels for `accept` tokens, such as `["Images", "PDF"]` for `upload.policy.accept`. Group labels such as `Images` are translated (`"Images(file type group)"`).

## Errors [#errors]

Every error a hook reports is an `APIError` from `@dimah-s3/core` with a
stable `code`. `useFormatDimahError()` turns it into a translated message,
with sizes and counts in the provider's locale:

```tsx
"use client";

import { useFormatDimahError, useUpload } from "@dimah-s3/react";
import { toast } from "@dimah-s3/ui";

export function AttachmentUploader() {
  const formatError = useFormatDimahError();
  const upload = useUpload({
    route: "attachments",
    onError: (error) => {
      toast.add({ type: "error", title: formatError(error) });
    },
  });

  return (
    <>
      <input {...upload.getInputProps()} />
      <button type="button" onClick={() => upload.open()}>
        Attach files
      </button>
    </>
  );
}
```

Codes it does not know, and plain `Error`s, return their `message`. To branch
on a code, use `isS3ErrorCode` from core:

```ts
import { isS3ErrorCode } from "@dimah-s3/core";

if (isS3ErrorCode(error, "FILE_TOO_LARGE")) {
  console.log(error.params); // { size, maxFileSize }
}
```

The full list of codes is in [Errors](https://v2.s3.dimah.dev/docs/server/errors). See also
[Outcomes and errors](https://v2.s3.dimah.dev/docs/react/guides/outcomes).

---

## Plain formatters [#plain-formatters]

These ship from the engine entry, so they work outside React and on the
server. The size formatters take a `locale` and unit labels; the hooks above
pass the provider's for you.

```ts
import {
  formatAcceptLabels,
  formatFileSize,
  formatSpeed,
  formatUploadProgress,
  toTimestamp,
  truncateFileName,
} from "@dimah-s3/react/engine";

formatFileSize(1_500_000); // "1.4 MB"
formatFileSize(1_500_000, { locale: "fa", units: { MB: "مگابایت" } }); // "۱٫۴ مگابایت"
formatUploadProgress(1_200_000, 2_000_000, 60); // "1.1 MB / 1.9 MB (60%)"
formatSpeed(1_200_000); // "1.1 MB/s"
formatAcceptLabels(["image/*", ".pdf", "application/zip"]); // ["Images", "PDF", "ZIP"]
formatAcceptLabels(["image/*"], { groups: { image: "Bilder" } }); // ["Bilder"]
truncateFileName("my-avatar-profile-picture-long-name.png", 20); // "my-avatar-profi….png"
toTimestamp("2026-10-31T12:00:00Z"); // 1793448000000, or null when invalid
```

`truncateFileName` is for plain text, such as a document title. In markup,
the UI's `FileName` shortens a name to the width it has and keeps the
extension visible.

---

## Core helpers [#core-helpers]

```ts
import { validateFile } from "@dimah-s3/core";

// null when valid, otherwise an APIError:
// FILE_TYPE_NOT_ALLOWED, FILE_EMPTY, or FILE_TOO_LARGE
const error = validateFile(file, {
  accept: ["image/*"],
  maxFileSize: 2 * 1024 * 1024,
});
```

### Checksums and public URLs [#checksums-and-public-urls]

```ts
import { buildPublicObjectUrl, sha256Blob } from "@dimah-s3/core";

// SHA-256 as unpadded base64, read in 1 MiB chunks
const hash = await sha256Blob(file);

// Public CDN URL. It does not sign; use it for public-read objects only.
const url = buildPublicObjectUrl({
  baseUrl: "https://cdn.example.com",
  key: "avatar/uuid/avatar.png",
});
```

`useUpload` already validates files and sends checksums when the route asks
for them. Use these helpers in your own pickers or server code.
