Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,76 @@ Global options apply to all editors in your app and are configured using `Lexxy.

{: .important }
When overriding configuration, call `Lexxy.configure` immediately after your import statement. Editor elements are registered after the import's call stack completes, so configuration must happen synchronously to take effect.

## Internationalization (i18n)

Lexxy ships with English as the default locale. You can register additional locales and set the active locale:

```js
import { configure } from "@37signals/lexxy"

configure({
i18n: {
locale: "ar",
ar: {
toolbar: {
bold: "عريض",
italic: "مائل",
// ... see src/config/locales/en.js for full list
},
table: {
row: "صف",
column: "عمود",
rowCount: { one: "صف واحد", two: "صفان", few: "%{count} صفوف", many: "%{count} صفًا", other: "%{count} صف" },
columnCount: { one: "عمود واحد", two: "عمودان", few: "%{count} أعمدة", many: "%{count} عمودًا", other: "%{count} عمود" },
// ...
}
}
}
})
```

### Partial translations

You only need to provide the keys you want to translate. Missing keys fall back to English:

```js
configure({
i18n: {
locale: "fr",
fr: {
toolbar: { bold: "Gras", italic: "Italique" }
}
}
})
```

### Pluralization

Table count labels support locale-aware pluralization via [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules). Provide an object with plural category keys (`zero`, `one`, `two`, `few`, `many`, `other`):

```js
{
table: {
rowCount: { one: "%{count} row", other: "%{count} rows" }
}
}
```

### Interpolation

Table labels use `%{variable}` placeholders for interpolation:

```js
{
table: {
add: "Add %{childType}", // "Add row", "Add column"
row: "row", // interpolated into %{childType}
column: "column",
}
}
```

### Available keys

See [`src/config/locales/en.js`](../src/config/locales/en.js) for the complete list of translatable keys.
55 changes: 55 additions & 0 deletions src/config/i18n.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import en from "./locales/en"

export default class I18n {
#locale
#registry

constructor(locales = {}, defaultLocale = "en") {
this.#locale = defaultLocale
this.#registry = { en, ...locales }
}

get locale() { return this.#locale }
set locale(value) { this.#locale = value }

registerLocale(name, translations) {
this.#registry[name] = translations
}

t(path, values = {}) {
let result = resolve(this.#registry[this.#locale], path)

if (result === undefined && this.#locale !== "en") {
result = resolve(this.#registry.en, path)
}

if (result === undefined) return path

if (result && typeof result === "object") {
if (!("count" in values)) return path
const rule = selectPluralForm(this.#locale, values.count)
result = result[rule] ?? result.other ?? path
}

if (typeof result === "string") {
return interpolate(result, values)
}

return path
}
}

const pluralRulesCache = {}

function resolve(tree, path) {
return path.split(".").reduce((node, key) => node?.[key], tree)
}

function selectPluralForm(locale, count) {
pluralRulesCache[locale] ??= new Intl.PluralRules(locale)
return pluralRulesCache[locale].select(count)
}

function interpolate(str, values) {
return str.replace(/%\{(\w+)\}/g, (_, key) => values[key] ?? `%{${key}}`)
}
15 changes: 14 additions & 1 deletion src/config/lexxy.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Configuration from "./configuration"
import { range } from "../helpers/array_helper.js"
import I18n from "./i18n"

const global = new Configuration({
attachmentTagName: "action-text-attachment",
Expand Down Expand Up @@ -30,13 +31,25 @@ const presets = new Configuration({
}
})

const i18n = new I18n()

export default {
global,
presets,
configure({ global: newGlobal, ...newPresets }) {
i18n,
configure({ global: newGlobal, i18n: i18nConfig, ...newPresets }) {
if (newGlobal) {
global.merge(newGlobal)
}
if (i18nConfig) {
const { locale, ...locales } = i18nConfig
for (const [ name, translations ] of Object.entries(locales)) {
i18n.registerLocale(name, translations)
}
if (locale) {
i18n.locale = locale
}
}
presets.merge(newPresets)
}
}
47 changes: 47 additions & 0 deletions src/config/locales/en.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export default {
toolbar: {
bold: "Bold",
italic: "Italic",
format: "Text formatting",
paragraph: "Normal",
paragraphTitle: "Paragraph",
headingLarge: "Large Heading",
headingLargeTitle: "Large heading",
headingMedium: "Medium Heading",
headingMediumTitle: "Medium heading",
headingSmall: "Small Heading",
headingSmallTitle: "Small heading",
strikethrough: "Strikethrough",
underline: "Underline",
highlight: "Color highlight",
removeHighlight: "Remove all coloring",
link: "Link",
linkPlaceholder: "Enter a URL\u2026",
linkSubmit: "Link",
unlink: "Unlink",
quote: "Quote",
code: "Code",
bulletList: "Bullet list",
numberedList: "Numbered list",
table: "Insert a table",
divider: "Insert a divider",
undo: "Undo",
redo: "Redo",
overflow: "Show more toolbar buttons",
overflowMenu: "More toolbar buttons",
uploadImage: "Add images and video",
uploadFile: "Upload files",
},
table: {
addBefore: "Add %{childType} before",
addAfter: "Add %{childType} after",
add: "Add %{childType}",
remove: "Remove %{childType}",
toggleStyle: "Toggle %{childType} style",
deleteTable: "Delete this table?",
row: "row",
column: "column",
rowCount: { one: "%{count} row", other: "%{count} rows" },
columnCount: { one: "%{count} column", other: "%{count} columns" },
},
}
10 changes: 6 additions & 4 deletions src/elements/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import Clipboard from "../editor/clipboard"
import Extensions from "../editor/extensions"
import { BrowserAdapter } from "../editor/adapters/browser_adapter"
import { getHighlightStyles } from "../helpers/format_helper"
import Lexxy from "../config/lexxy"

import { CustomActionTextAttachmentNode } from "../nodes/custom_action_text_attachment_node"
import { exportTextNodeDOM } from "../helpers/text_node_export_helper"
Expand All @@ -35,6 +36,7 @@ import { TablesExtension } from "../extensions/tables_extension"
import { AttachmentsExtension } from "../extensions/attachments_extension.js"
import { FormatEscapeExtension } from "../extensions/format_escape_extension.js"

function t(key, values) { return Lexxy.i18n.t(key, values) }

export class LexicalEditorElement extends HTMLElement {
static formAssociated = true
Expand Down Expand Up @@ -659,10 +661,10 @@ export class LexicalEditorElement extends HTMLElement {
if (!this.supportsRichText) return []

return [
{ label: "Normal", command: "setFormatParagraph", tag: null },
{ label: "Large heading", command: "setFormatHeadingLarge", tag: "h2" },
{ label: "Medium heading", command: "setFormatHeadingMedium", tag: "h3" },
{ label: "Small heading", command: "setFormatHeadingSmall", tag: "h4" },
{ label: t("toolbar.paragraph"), command: "setFormatParagraph", tag: null },
{ label: t("toolbar.headingLargeTitle"), command: "setFormatHeadingLarge", tag: "h2" },
{ label: t("toolbar.headingMediumTitle"), command: "setFormatHeadingMedium", tag: "h3" },
{ label: t("toolbar.headingSmallTitle"), command: "setFormatHeadingSmall", tag: "h4" },
]
}

Expand Down
25 changes: 15 additions & 10 deletions src/elements/table/table_tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import theme from "../../config/theme"
import { handleRollingTabIndex } from "../../helpers/accessibility_helper"
import { createElement } from "../../helpers/html_helper"
import { nextFrame } from "../../helpers/timing_helpers"
import Lexxy from "../../config/lexxy"

function t(key, values) { return Lexxy.i18n.t(key, values) }

export class TableTools extends HTMLElement {
connectedCallback() {
Expand Down Expand Up @@ -58,16 +61,17 @@ export class TableTools extends HTMLElement {
}

#createButtonsContainer(childType, setCountProperty, moreMenu) {
const typeName = t(`table.${childType}`)
const container = createElement("div", { className: `lexxy-floating-controls__group lexxy-table-control lexxy-table-control--${childType}` })

const plusButton = this.#createButton(`Add ${childType}`, { action: "insert", childType, direction: "after" }, "+")
const minusButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType }, "−")
const plusButton = this.#createButton(t("table.add", { childType: typeName }), { action: "insert", childType, direction: "after" }, "+")
const minusButton = this.#createButton(t("table.remove", { childType: typeName }), { action: "delete", childType }, "−")

const dropdown = createElement("details", { className: "lexxy-table-control__more-menu" })
dropdown.setAttribute("name", "lexxy-dropdown")
dropdown.tabIndex = -1

const count = createElement("summary", {}, `_ ${childType}s`)
const count = createElement("summary", {}, t(`table.${childType}Count`, { count: "_" }))
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The count parameter is passed as the string "_" instead of a number. While the placeholder is replaced with actual numeric counts later by #updateRowColumnCount(), using a non-numeric value for Intl.PluralRules.select() is technically incorrect. The initial placeholder should use a number (e.g., 0) to ensure correct plural form selection from the start.

Suggested change
const count = createElement("summary", {}, t(`table.${childType}Count`, { count: "_" }))
const count = createElement("summary", {}, t(`table.${childType}Count`, { count: 0 }))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used _ to preserve the old behavior. _ will fallback to the other plural option and produce correct output.

setCountProperty(count)
dropdown.appendChild(count)

Expand Down Expand Up @@ -97,11 +101,12 @@ export class TableTools extends HTMLElement {
}

#createMoreMenuSection(childType) {
const typeName = t(`table.${childType}`)
const section = createElement("div", { className: "lexxy-floating-controls__group lexxy-table-control__more-menu-details" })
const addBeforeButton = this.#createButton(`Add ${childType} before`, { action: "insert", childType, direction: "before" })
const addAfterButton = this.#createButton(`Add ${childType} after`, { action: "insert", childType, direction: "after" })
const toggleStyleButton = this.#createButton(`Toggle ${childType} style`, { action: "toggle", childType })
const deleteButton = this.#createButton(`Remove ${childType}`, { action: "delete", childType })
const addBeforeButton = this.#createButton(t("table.addBefore", { childType: typeName }), { action: "insert", childType, direction: "before" })
const addAfterButton = this.#createButton(t("table.addAfter", { childType: typeName }), { action: "insert", childType, direction: "after" })
const toggleStyleButton = this.#createButton(t("table.toggleStyle", { childType: typeName }), { action: "toggle", childType })
const deleteButton = this.#createButton(t("table.remove", { childType: typeName }), { action: "delete", childType })

section.appendChild(addBeforeButton)
section.appendChild(addAfterButton)
Expand All @@ -114,7 +119,7 @@ export class TableTools extends HTMLElement {
#createDeleteTableButton() {
const container = createElement("div", { className: "lexxy-table-control lexxy-floating-controls__group" })

const deleteTableButton = this.#createButton("Delete this table?", { action: "delete", childType: "table" })
const deleteTableButton = this.#createButton(t("table.deleteTable"), { action: "delete", childType: "table" })
deleteTableButton.classList.add("lexxy-table-control__button--delete-table")

container.appendChild(deleteTableButton)
Expand Down Expand Up @@ -289,8 +294,8 @@ export class TableTools extends HTMLElement {
const rowCount = tableElement.rows
const columnCount = tableElement.columns

this.rowCount.textContent = `${rowCount} row${rowCount === 1 ? "" : "s"}`
this.columnCount.textContent = `${columnCount} column${columnCount === 1 ? "" : "s"}`
this.rowCount.textContent = t("table.rowCount", { count: rowCount })
this.columnCount.textContent = t("table.columnCount", { count: columnCount })
}

#setTableCellFocus() {
Expand Down
Loading
Loading