---
title: Localization
description: Translate the words the package shows or says, from the kinds' names and the placeholders to the slash keywords and the screen-reader announcements, with one labels dictionary per view; your chrome's words are your markup's.
icon: languages
---

Two kinds of words show in an editor:

- **The package's own words**: what the package itself renders or says. The kinds' and commands' names (the slash menu, Turn into and the block menu search them; a handle's payload names its block by them), the marks' toolbar buttons, the placeholders, the to-do checkbox, an empty toggle's or callout's hint, the callout's icon button, the page, table of contents, page link and equation blocks, a table's and a column band's accessible names, the block menu's actions and colours (its search matches them), the file size of a file block, and what screen readers hear when blocks move or go. They come from one dictionary, `Labels`, one section per plugin; English is the default.
- **Your chrome's words**: the menus, toolbars, handles, panels, bars and cards are your markup ([Menus and handles](/docs/customization/menus)), so their words are yours, in your markup: the controllers hand your snippets state and actions, never words, and name none of your elements (give your menus and toolbars their `aria-label`).

To show another language, give the view its `labels`: any entries of any section. The entries you leave out stay English, and so does an entry of a nested group (`kinds`, `placeholders`, `colors`) you leave out. A label that says something about a value is a function of it, so each language orders and inflects its own words: `movedUp: (what) => ...`, `columns: (count) => ...`, `fileSize: (bytes) => ...`.

| Section of `Labels` | Its words |
| --- | --- |
| `editor` | Block move and delete announcements, a suggestion's default name |
| `richText` | Kinds and marks (menu rows, toolbar buttons, handle names), the to-do checkbox, an empty body's hint, the callout's icon button, the placeholders `richTextPlaceholder` shows |
| `blockMenu` | The block menu's actions and colour rows (and the `/red` colour commands) |
| `image` | The image preset |
| `media` | The embed, bookmark, file, video and audio presets, a file's size |
| `code` | The code preset (a language's name is its `languages` row's) |
| `columns` | The layout commands, the column resize bands |
| `page`, `toc`, `pageLink` | The page block and its command, the table of contents, an untitled page link |
| `comments` | The toolbar's Comment button, the sidebar's and a card's accessible names |
| `table` | The table command and accessible name, its block menu rows and switches, a column band, a padded cell |
| `equation` | The equation presets, an empty or invalid equation |

```svelte title="Hints.svelte" check
<script lang="ts">
	import { Edytor, richTextPlaceholder } from 'edytor';
</script>

<!-- One word changed: the rest stays English. -->
<Edytor
	plugins={[() => ({ placeholder: richTextPlaceholder })]}
	labels={{ richText: { placeholders: { empty: 'Write, or press / for blocks' } } }}
/>
```

## A French editor

Type the whole dictionary as `Labels`: when a release adds a label, your build fails until you translate it. `englishLabels` is the English one, to start from.

```ts title="src/lib/fr.ts" check
import { CODE_LANGUAGES, type Labels } from 'edytor';

const octets = (count: number) => {
	const units = ['o', 'Ko', 'Mo', 'Go', 'To'];
	let value = count;
	let unit = 0;
	while (value >= 1024 && unit < units.length - 1) {
		value /= 1024;
		unit++;
	}
	const shown = unit === 0 ? value : value.toFixed(value < 10 ? 1 : 0).replace('.', ',');
	return `${shown} ${units[unit]}`;
};

export const fr: Labels = {
	editor: {
		suggestion: 'Proposition',
		block: (kind) => `bloc ${kind}`,
		blocks: (count) => `${count} blocs`,
		movedUp: (what) => `${what} déplacé vers le haut`,
		movedDown: (what) => `${what} déplacé vers le bas`,
		indented: (what) => `${what} indenté`,
		outdented: (what) => `${what} désindenté`,
		moved: (what) => `${what} déplacé`,
		deleted: (what) => `${what} supprimé`
	},
	richText: {
		kinds: {
			paragraph: 'Texte',
			heading1: 'Titre 1',
			heading2: 'Titre 2',
			heading3: 'Titre 3',
			toggleHeading1: 'Titre dépliant 1',
			toggleHeading2: 'Titre dépliant 2',
			toggleHeading3: 'Titre dépliant 3',
			bulletedList: 'Liste à puces',
			numberedList: 'Liste numérotée',
			todoList: 'Liste de tâches',
			toggleList: 'Liste dépliante',
			callout: 'Encadré',
			quote: 'Citation',
			divider: 'Séparateur'
		},
		marks: {
			bold: 'Gras',
			italic: 'Italique',
			underline: 'Souligné',
			strike: 'Barré',
			code: 'Code'
		},
		checkbox: 'Fait',
		emptyBody: {
			toggle: 'Bloc dépliant vide. Cliquez ou déposez des blocs ici.',
			callout: 'Encadré vide. Cliquez ou déposez des blocs ici.'
		},
		calloutIcon: {
			change: 'Changer l’icône',
			add: 'Ajouter une icône'
		},
		placeholders: {
			heading: (level) => `Titre ${level}`,
			toggleHeading: (level) => `Titre dépliant ${level}`,
			list: 'Liste',
			todo: 'Tâche',
			toggle: 'Liste dépliante',
			quote: 'Citation vide',
			callout: 'Titre de l’encadré',
			caption: 'Ajoutez une légende…',
			empty: 'Tapez « / » pour les commandes'
		}
	},
	blockMenu: {
		turnInto: 'Transformer en',
		color: 'Couleur',
		colors: {
			default: 'Par défaut',
			gray: 'Gris',
			brown: 'Marron',
			orange: 'Orange',
			yellow: 'Jaune',
			green: 'Vert',
			blue: 'Bleu',
			purple: 'Violet',
			pink: 'Rose',
			red: 'Rouge'
		},
		colorText: (color) => `Texte ${color.toLowerCase()}`,
		colorBackground: (color) => `Fond ${color.toLowerCase()}`,
		copyLink: 'Copier le lien du bloc',
		duplicate: 'Dupliquer',
		moveUp: 'Monter',
		moveDown: 'Descendre',
		delete: 'Supprimer',
		ctrl: 'Ctrl',
		shift: 'Maj',
		deleteKey: 'Suppr'
	},
	image: {
		image: 'Image'
	},
	media: {
		embed: 'Intégration',
		bookmark: 'Signet web',
		file: 'Fichier',
		video: 'Vidéo',
		audio: 'Audio',
		fileSize: octets
	},
	code: {
		code: 'Code'
	},
	columns: {
		columns: (count) => `${count} colonnes`,
		resize: 'Redimensionner les colonnes'
	},
	page: {
		page: 'Page',
		untitled: 'Sans titre'
	},
	toc: {
		toc: 'Table des matières',
		untitled: 'Sans titre',
		empty: 'Ajoutez des titres pour créer une table des matières.'
	},
	pageLink: {
		untitled: 'Sans titre'
	},
	comments: {
		comment: 'Commenter',
		sidebar: 'Commentaires'
	},
	table: {
		table: 'Tableau',
		headerRow: 'Ligne d’en-tête',
		headerColumn: 'Colonne d’en-tête',
		insertRowAbove: 'Insérer une ligne au-dessus',
		insertRowBelow: 'Insérer une ligne en dessous',
		insertColumnLeft: 'Insérer une colonne à gauche',
		insertColumnRight: 'Insérer une colonne à droite',
		deleteRow: 'Supprimer la ligne',
		deleteColumn: 'Supprimer la colonne',
		resize: 'Redimensionner la colonne',
		emptyCell: 'Cellule vide'
	},
	equation: {
		block: 'Équation en bloc',
		inline: 'Équation en ligne',
		blockPlaceholder: 'Ajouter une équation TeX',
		inlinePlaceholder: 'Nouvelle équation',
		invalid: 'Équation invalide'
	}
};

/** The code languages, named in French where the name differs. */
export const frLanguages = CODE_LANGUAGES.map((language) =>
	language.id === 'plaintext' ? { ...language, label: 'Texte brut' } : language
);

/** The French slash keywords, by command id (they replace the English ones: the English names are kept in them). */
export const frKeywords: Record<string, string[]> = {
	'block.paragraph': ['texte', 'paragraphe', 'paragraph'],
	'block.heading1': ['titre', 'h1'],
	'block.heading2': ['sous-titre', 'h2'],
	'block.heading3': ['h3'],
	'block.bulleted-list-item': ['puce', 'ul'],
	'block.numbered-list-item': ['numéro', 'ol'],
	'block.todo-item': ['tâche', 'case', 'todo'],
	'block.toggle': ['dépliant', 'details'],
	'block.toggle-heading1': ['dépliant', 'replier'],
	'block.toggle-heading2': ['dépliant', 'replier'],
	'block.toggle-heading3': ['dépliant', 'replier'],
	'block.callout': ['note', 'astuce'],
	'block.quote': ['citation', 'quote'],
	'block.divider': ['séparateur', 'hr'],
	'block.toc': ['sommaire', 'plan', 'toc'],
	'page.new': ['sous-page', 'nouvelle page', 'document', 'lien']
};
```

Then give the view its words, and the code plugin its languages. Your chrome's snippets write their own words, in French here:

```svelte title="src/lib/Editor.svelte" check
<script lang="ts">
	import {
		Edytor,
		createCodePlugin,
		createColumnsPlugin,
		createPagePlugin,
		createTocPlugin,
		richTextPlaceholder
	} from 'edytor';
	import { fr, frKeywords, frLanguages } from '$lib/fr';

	const plugins = [
		// Notion's hints in empty blocks.
		() => ({ placeholder: richTextPlaceholder }),
		createCodePlugin({ languages: frLanguages }),
		createColumnsPlugin(),
		createPagePlugin(),
		createTocPlugin()
	];
</script>

<Edytor
	{plugins}
	labels={{ ...fr, keywords: frKeywords }}
	aria-label="Corps de la page"
/>
```

The view's own accessible name is yours to give (`aria-label`, `aria-labelledby`): the editor has none of its own.

## Slash keywords

The slash menu finds a kind by its label and by its keywords. A translated label is found as it is: `/titre` finds "Titre 1". The English keywords stay unless you replace them: the view's `labels.keywords` takes, by command id, the keywords that replace a command's own (list the English ones in it to keep them). The rich text plugin's command ids are `block.paragraph`, `block.heading1` to `block.heading3`, `block.toggle-heading1` to `block.toggle-heading3`, `block.bulleted-list-item`, `block.numbered-list-item`, `block.todo-item`, `block.toggle`, `block.callout`, `block.quote` and `block.divider`; the other plugins' are `block.image`, `block.code`, `block.embed`, `block.bookmark`, `block.file`, `block.video`, `block.audio`, `columns.2` to `columns.5`, `page.new`, `block.toc`, `block.table`, `block.equation` and `equation.inline`.

## Your chrome's words

Your markup writes its own words: the slash menu's section headings (a preset's `group`, such as `Basic blocks` or `Media`, is a key your markup shows in your words), your toolbar's colours, a table grip's menu rows (named by their `item.id`, `insert-above` …), the pasted-link menu's rows (`link`, `embed`, `bookmark`), an empty media block's texts (by its `kind`), the version history's statuses (`VersionHistory.status` is a code: `restored`, `denied` …). The repository's demo chrome keeps its words in one module, [`words.svelte.ts`](https://github.com/beynar/edytor/tree/master/src/routes/chrome/words.svelte.ts), which a French view replaces with `setWords(frWords)`: a pattern to copy, not a package API.

## What is not localized

- **Your document.** Labels translate the package's words, never the content: a document keeps the text people typed.
- **Your own kinds, marks and commands.** Their `label`, `keywords` and `group` are what you write in their records: write them in your language, or build them from your own dictionary. So are your own [triggers](/docs/plugins/input-rules#triggers)' menus.
- **Key names on a Mac.** The block menu's shortcuts use the Mac's symbols (⌘, ⇧) there; elsewhere the `ctrl`, `shift` and `deleteKey` labels.
- **Number and date formats** beyond what a label function formats: `fileSize` is a function, so use `Intl.NumberFormat` there if you need it.
- **Right-to-left chrome.** Blocks take their text's direction; your menus and toolbars are laid out as your markup lays them out.
