Blockleaf

Translating the chrome

Every string the editor renders comes from one labels object, and a partial translation is a valid one.

The default chrome is replaceable, but replacing all of it to change one word is not a translation story. So every string the editor and its menus put on screen comes from a single labels object.

import { BlockleafEditor, type BlockleafLabels } from '@blockleaf/editor';

// Outside the component. See "Give it a stable reference" below.
const fr: BlockleafLabels = {
	blockTypes: { 'heading-1': 'Titre 1' },
	slashMenu: { menu: 'Commandes', groups: { headings: 'Titres' } },
	link: { address: 'Adresse du lien' }
};

<BlockleafEditor labels={fr} />;

Anything left out keeps its English default, at every level — overriding one group heading does not require restating the others. A partial translation is a valid one.

What is in it

KeyCovers
blockTypesThe names of the blocks themselves
slashMenuThe menu, its group headings, item descriptions, size picker
bubbleMenuThe selection toolbar and its marks
blockMenuThe drag handle, its menu, "Turn into" and the block actions
tableRow, column and cell actions, and the menus that hold them
imageAlignment, delete, and the alt text panel
linkThe link editor's field and buttons
uploadProgress, failure, Cancel, Retry and Dismiss
codeBlockThe language picker
placeholderThe text shown in an empty block

One translation, two surfaces

blockTypes names each block once. The slash menu's items and the block menu's "Turn into" group both read from it, so Heading 1 is never translated twice — and the two can never drift apart.

Every key is typed. A misspelled id is a compile error rather than a silently English menu item:

resolveLabels({ table: { actions: { 'add-row-above': 'x' } } });
//                                  ^ not a TableActionId

Give it a stable reference

labels is one of the props the editor's extensions are rebuilt from, the same as slashItems and bubbleActions. Define it at module scope, or memoise it — an object literal written inline is a new object on every render, and the chrome would be rebuilt each time.

// Do this.
const labels: BlockleafLabels = { link: { address: 'Adresse du lien' } };

// Not this.
<BlockleafEditor labels={{ link: { address: 'Adresse du lien' } }} />;

The upload adapter

The upload extension is built by your code, because it needs your adapter, so it is not among the extensions BlockleafEditor assembles. It is wired up for you anyway: if it carries no labels of its own, it is given the ones on the editor. Pass them yourself when you build it outside an editor, or want it to differ:

import { createUploadExtension, resolveLabels } from '@blockleaf/editor';

createUploadExtension({ adapter, labels: resolveLabels(fr) });

The code block is the same: it ships from its own subpath and takes labels directly.

Building your own menus

Each default registry is a function of a label set, so your own list can start from a translated one rather than from English:

import { createSlashCommands, resolveLabels } from '@blockleaf/editor';

const labels = resolveLabels(fr);
const commands = createSlashCommands(labels.slashMenu, labels.blockTypes);

createBubbleActions, createBlockActions, createTableActions, createCellActions and createImageActions work the same way. The old defaultSlashCommands, defaultBubbleActions and friends are still exported and still English — they are those same functions called with the defaults.

What is not covered

Right-to-left layout, and date or number formatting. The editor stores text, not formatted values, so neither has come up yet; say so in an issue if it has for you.

On this page