> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Translate the UI text

Replace every string the comment components render with the labels on the provider and on each component.

Every string the default components render can be replaced, except one, so a comments surface in another language is configured rather than forked. This guide covers the `labels` object the provider takes, the `labels` each default component takes on top of it, which props carry copy on the primitives, and the one string still out of reach.

Prerequisites:

- A comments surface under `CommentsProvider`, or under `SDKCommentsProvider` rendered inside `SanityApp` with a `Suspense` boundary and an error boundary around it. See [Get started with comments](https://www.sanity.io/docs/collaboration/get-started).

## Translate the whole surface

Pass `labels` to `CommentsProvider`, or to `SDKCommentsProvider`, which hands it on. It reaches every component below: the panel, the thread cards, the comments with their menus and dialogs, every composer including the inline one, the list, and the reactions.

**Comments.tsx**

```tsx
import {SDKCommentsProvider, SDKCommentsPanel} from '@sanity/collaboration/sdk'
import type {CommentsLabels} from '@sanity/collaboration/ui'
import {Suspense} from 'react'

const REACTION_NAMES: Record<string, string> = {
  ':+1:': 'Tommel opp',
  ':-1:': 'Tommel ned',
  ':eyes:': 'Øyne',
  ':heart:': 'Hjerte',
  ':heavy_plus_sign:': 'Pluss',
  ':rocket:': 'Rakett',
}

const norwegian: CommentsLabels = {
  panel: {
    panel: 'Kommentarer',
    open: 'Åpne',
    resolved: 'Løste',
    addThread: 'Start en ny tråd',
    composerPlaceholder: (target) =>
      target.fieldPath === ''
        ? 'Kommenter dokumentet…'
        : `Kommenter ${target.fieldPath}…`,
    empty: (status) =>
      status === 'open'
        ? 'Ingen åpne kommentarer.'
        : 'Ingen løste kommentarer.',
  },
  thread: {
    showMoreReplies: (count) => `Vis ${count} til`,
    showFewerReplies: 'Skjul',
    replyPlaceholder: 'Svar eller nevn med @',
  },
  comment: {
    actions: {
      resolve: 'Løs kommentaren',
      reopen: 'Gjenåpne kommentaren',
      edit: 'Rediger',
      delete: 'Slett',
    },
    deleteTitle: 'Slette kommentaren?',
    deleteConfirm: 'Slett',
    deleteCancel: 'Avbryt',
    unknownAuthor: 'Ukjent',
    edited: '(redigert)',
  },
  input: {
    placeholder: 'Skriv en kommentar…',
    submit: 'Kommenter',
    cancel: 'Avbryt',
    mentionsEmpty: 'Ingen brukere funnet',
  },
  list: {loading: 'Laster kommentarer…'},
  inline: {button: 'Kommenter'},
  reactions: {name: (shortName) => REACTION_NAMES[shortName] ?? shortName},
}

export function Comments() {
  return (
    <Suspense fallback={<p>Resolving your identity…</p>}>
      <SDKCommentsProvider labels={norwegian}>
        <Suspense fallback={<p>Laster kommentarer…</p>}>
          <SDKCommentsPanel
            documentId="post-123"
            documentType="post"
            compose={{fieldPath: 'title'}}
          />
        </Suspense>
      </SDKCommentsProvider>
    </Suspense>
  )
}
```

This is the `Comments` component from Get started with `labels` added. `SDKCommentsProvider` suspends while it resolves who you are, so it stays under the same `Suspense` boundary. Every section, and every entry in it, is optional. What you leave out keeps the English default, so a single string can be replaced without restating the rest. Define the object once, outside render, as the example does. Each section reaches these components:

| Section | Reaches |
| --- | --- |
| `panel` | `CommentsPanel`'s own strings |
| `thread` | `CommentThread`'s name, replies toggle, and reply prompt |
| `comment` | Every `Comment`, and the menu and dialog it opens |
| `input` | Every composer: reply, edit, the panel's, and the inline one |
| `list` | `CommentList`'s loading text, and `document`: the heading over document-level threads |
| `inline` | `InlineCommentInput`'s affordance |
| `reactions` | Reaction names, in the accessible names of pills and picker |

A count or a field path is a function rather than a template, since a language that pluralizes differently from English cannot be served by substitution alone. So is a reaction name: shortnames are an open set, so return the shortname itself for one you do not know.

Some of these strings are never drawn: `thread.thread` names a thread card for a screen reader, and `panel.statusFilter` names the panel's status tabs. They are worth translating even though no sighted reader sees them. `panel.panel` is both the panel's accessible name and the title in its header.

## Override one component

`Comment`, `CommentInput`, `CommentThread`, and `CommentsPanel` each take a `labels` prop of their own, with the same entries as the provider's section. It wins over the provider entry by entry, so it is for wording one surface differently, not for translating the app:

```tsx
<CommentThread
  thread={thread}
  labels={{
    showMoreReplies: (count) => `Vis alle ${count}`,
    comment: {actions: {delete: 'Fjern'}},
  }}
/>
```

From lowest to highest: the English defaults, the provider's `labels`, and the component's own `labels`. A component nested inside another gets its own `labels` from the outer one: a card passes its `labels.comment` to every comment it holds, and a comment passes its `labels.input` to its edit composer. `comment.actions` merges a level down, so a provider that renames Edit and a card that renames Delete both stick.

A composer's prompt and submit text are labels too. They come from three places, and each wins over the one before it:

- Provider defaults: `input.placeholder` and `input.submit` set them for every composer.
- Per-composer overrides: `thread.replyPlaceholder` for the reply composers, `panel.composerPlaceholder` for the panel's composers, and `comment.editPlaceholder` and `comment.editSubmit` for the edit composer. `panel.composerPlaceholder` is given the composer's `CommentTarget`, whose field path is `''` for a thread on the whole document.
- Props, for one composer worded differently from the rest: `placeholder` and `submitLabel` on `CommentInput` and `InlineCommentInput`, and `replyPlaceholder` on `CommentThread`.

`inline.button` names the inline affordance, and `label` on `InlineCommentInput` wins over it. `CommentBadge` takes `label` for its accessible name, and `CommentList` takes `empty` for its empty state.

The defaults are exported as `DEFAULT_COMMENT_LABELS`, `DEFAULT_COMMENT_INPUT_LABELS`, `DEFAULT_COMMENT_THREAD_CARD_LABELS`, and `DEFAULT_COMMENTS_PANEL_LABELS`.

## Translate the text inside a comment

`Comment` takes the strings of everything it renders or opens as one `labels` object: its actions menu, the delete confirmation, the failed-create notice, the edit composer, the edited marker, the "you" on a self-mention, the reaction picker, and the name of an author there is no user for. The provider's `labels.comment` is that same object less `input`, which the provider's own `labels.input` covers. For one comment worded differently:

```tsx
import type {CommentData} from '@sanity/collaboration'
import {Comment} from '@sanity/collaboration/ui'

export function RemovableComment({comment}: {comment: CommentData}) {
  return (
    <Comment
      comment={comment}
      labels={{
        actions: {delete: 'Fjern'},
        deleteConfirm: 'Fjern',
        input: {cancel: 'Lukk'},
      }}
    />
  )
}
```

`labels.actions` is the same object `commentActions` takes, and `labels.input` the composer's own `CommentInputLabels`.

`CommentThread` passes `labels.comment` to every comment it holds, parent and replies. A `CommentsPanel` or `SDKCommentsPanel` reaches its cards through `threadProps`, which is how one panel's comments can read differently from the rest of the app:

```tsx
import {SDKCommentsPanel} from '@sanity/collaboration/sdk'
import type {CommentLabels} from '@sanity/collaboration/ui'
import type {DocumentHandle} from '@sanity/sdk'

const reviewLabels: Partial<CommentLabels> = {
  actions: {resolve: 'Godkjenn'},
}

export function ReviewPanel({handle}: {handle: DocumentHandle}) {
  return (
    <SDKCommentsPanel
      {...handle}
      // Keeps this panel's unsent drafts apart from another panel's.
      draftScope="review"
      threadProps={() => ({labels: {comment: reviewLabels}})}
    />
  )
}
```

Two panels over the same document share their unsent drafts. `draftScope` keeps this panel's drafts apart from the other's.

With `CommentsPanel`, return the labels beside the callbacks you already pass:

```tsx
const threadCallbacks = useSDKThreadCallbacks(handle)

<CommentsPanel
  threads={threads}
  threadProps={(thread) => ({
    ...threadCallbacks(thread),
    labels: {comment: reviewLabels},
  })}
/>
```

## Pass the strings as props

The primitives a comment is built from take every string they render as a prop. A layout composed from them, as in [Build a custom comment](https://www.sanity.io/docs/collaboration/build-a-custom-comment), passes them directly.

Anything that ends up as a button or an accessible name is named `xxxLabel`. The rest keeps a descriptive name.

```tsx
<CommentHeader
  author={author.user}
  authorLoading={author.loading}
  createdAt={comment.createdAt}
  lastEditedAt={comment.lastEditedAt}
  authorFallback="Ukjent"
  editedLabel="(redigert)"
/>
```

These are the strings each primitive renders, and what it falls back to:

| Primitive | Prop | Default |
| --- | --- | --- |
| `CommentHeader` | `authorFallback` | `Unknown` |
| `CommentHeader` | `editedLabel` | `(edited)` |
| `CommentTimestamp` | `editedLabel` | `(edited)` |
| `CommentMessageContent` | `youLabel` | `you` |
| `MentionChip` | `youLabel` | `you` |
| `UserName` | `fallback` | `Unknown` |
| `CommentReactions` | `addReactionLabel` | `Add reaction` |
| `AddReactionButton` | `label` | `Add reaction` |
| `CommentActionBar` | `menuLabel` | `Comment actions` |
| `CommentErrorNotice` | `retryLabel` | `Try again` |
| `DeleteCommentDialog` | `title` | `Delete comment?` |
| `DeleteCommentDialog` | `confirmLabel` | `Delete` |
| `DeleteCommentDialog` | `cancelLabel` | `Cancel` |
| `CommentInput` | `placeholder` | `Add a comment…` |
| `CommentInput` | `submitLabel` | `Comment` |
| `CommentBadge` | `label` | `N comments` |

Reaction names are the exception: `CommentReactions` and `AddReactionButton` take them from the provider's `labels.reactions.name`, since a primitive that renders reactions is always under a provider.

## Rename the default actions

Under the default components, the provider's `labels.comment.actions` renames them. A layout of your own that calls `commentActions` passes the names itself, as one object. Each entry it is not given keeps the English default:

```typescript
commentActions({
  comment,
  capabilities,
  onEdit,
  onDelete,
  labels: {edit: 'Rediger', delete: 'Slett'},
})
```

The defaults are `Resolve comment`, `Reopen comment`, `Retry`, `Edit`, `Copy link`, and `Delete`. A seventh, `discard`, names the one button `Comment` shows on a comment that failed to post; it defaults to `Discard`, and `commentActions` does not use it.

> [!WARNING]
> Once the names are translated, do not filter the action list on `action.name`. Build the list without the callback you do not want, or filter before the labels are applied. See [Customize comment actions](https://www.sanity.io/docs/collaboration/customize-comment-actions).

## Format timestamps

Timestamps need nothing. The provider's default `formatTime` uses `Intl.RelativeTimeFormat` with no explicit locale, so it already follows the runtime's. Pass `formatTime` to `CommentsProvider` or `SDKCommentsProvider` to override the format itself:

```tsx
<CommentsProvider
  currentUser={me}
  getUser={getUser}
  formatTime={(isoDate) => myFormatter.format(new Date(isoDate))}
>
```

## What cannot be translated yet

**DeleteCommentDialog's close button** takes its accessible name from Sanity UI, which does not expose it. It stays `Close dialog`. The discard confirmation that `CommentInput` opens with `confirmDiscard` has the same close button.

## Next steps

- The primitives these props belong to, with every other prop: [Primitives](https://collaboration-platform.sanity.dev/?path=/docs/introduction--docs).
- Assembling a card that takes them: [Build a custom comment](https://www.sanity.io/docs/collaboration/build-a-custom-comment).
- A panel with a language picker, as an example you can run: [translate](https://github.com/sanity-io/collaboration-platform-examples/tree/main/examples/translate).



## Related changelog entries

Entries are listed newest first; each link points to the full entry as markdown. Follow one when you need to know what changed, when, or why — for example, to summarize recent updates, explain behavior that differs from older documentation, or check whether a fix has shipped.

- [@sanity/collaboration v0.3.0: New send shortcut, Cancel that clears the draft, and a discard action for failed comments](https://www.sanity.io/docs/changelog/e3fbc3a3-2d35-40e6-9309-786ec3e24b49.md) — October 29, 2026
- [@sanity/collaboration v0.1.0: First beta release of the comment components](https://www.sanity.io/docs/changelog/88be4643-749f-4954-8493-689b43490d18.md) — October 26, 2026