@crystallize/design-system 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/index.css +15 -17
  3. package/dist/index.d.ts +8 -1
  4. package/dist/index.js +469 -442
  5. package/dist/index.mjs +407 -385
  6. package/package.json +1 -1
  7. package/src/rich-text-editor/i18n/i18n.test.ts +14 -0
  8. package/src/rich-text-editor/i18n/index.tsx +64 -0
  9. package/src/rich-text-editor/i18n/translations/en.ts +66 -0
  10. package/src/rich-text-editor/i18n/types.ts +62 -0
  11. package/src/rich-text-editor/plugins/ActionsPlugin/index.tsx +5 -22
  12. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/CopyButton/index.tsx +4 -1
  13. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/components/PrettierButton/index.tsx +11 -1
  14. package/src/rich-text-editor/plugins/CodeActionMenuPlugin/index.tsx +2 -1
  15. package/src/rich-text-editor/plugins/FloatingLinkEditorPlugin/index.tsx +23 -5
  16. package/src/rich-text-editor/plugins/FloatingTextFormatToolbarPlugin/index.tsx +21 -10
  17. package/src/rich-text-editor/plugins/TabFocusPlugin/index.tsx +4 -12
  18. package/src/rich-text-editor/plugins/TableActionMenuPlugin/index.tsx +23 -14
  19. package/src/rich-text-editor/plugins/ToolbarPlugin/index.tsx +33 -33
  20. package/src/rich-text-editor/plugins/ToolbarPlugin/insert-table.tsx +6 -4
  21. package/src/rich-text-editor/rich-text-editor.css +6 -0
  22. package/src/rich-text-editor/rich-text-editor.stories.tsx +10 -0
  23. package/src/rich-text-editor/rich-text-editor.tsx +15 -9
  24. package/src/rich-text-editor/ui/LinkPreview.tsx +3 -1
  25. package/src/rich-text-editor/ui/ContentEditable.css +0 -13
  26. package/src/rich-text-editor/ui/ContentEditable.tsx +0 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crystallize/design-system",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "types": "./dist/index.d.ts",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -0,0 +1,14 @@
1
+ import { replaceI18nVariablesInString } from '.';
2
+
3
+ describe('RichTextEditor i18n', () => {
4
+ it('replaces variable parts of a translation string correctly', async () => {
5
+ expect(replaceI18nVariablesInString('Hi {{name}}!', 'you')).toBe('Hi you!');
6
+ expect(replaceI18nVariablesInString('Hi {{whatever-goes here}}!', 'you')).toBe('Hi you!');
7
+ expect(replaceI18nVariablesInString('It can be replace {{name}} many times {{name}}', 'you')).toBe(
8
+ 'It can be replace you many times you',
9
+ );
10
+ expect(replaceI18nVariablesInString('It needs correct formatting {name}}', 'you')).toBe(
11
+ 'It needs correct formatting {name}}',
12
+ );
13
+ });
14
+ });
@@ -0,0 +1,64 @@
1
+ import { createContext, ReactNode, useContext, useEffect, useState } from 'react';
2
+
3
+ import type { I18N } from './types';
4
+
5
+ const I18nContext = createContext<I18N | null>(null);
6
+
7
+ export type SupportedLanguages = 'en';
8
+
9
+ export { default as labelTranslationEn } from './translations/en';
10
+
11
+ export function I18nProvider({
12
+ language,
13
+ labelTranslations,
14
+ children,
15
+ }: {
16
+ language: SupportedLanguages;
17
+ labelTranslations?: I18N;
18
+ children: ReactNode;
19
+ }) {
20
+ const [translations, setTranslations] = useState<I18N | null>(labelTranslations || null);
21
+
22
+ useEffect(() => {
23
+ let unmounted = false;
24
+ (async function load() {
25
+ if (!labelTranslations) {
26
+ const resource = await import(`./translations/${language}.ts`);
27
+ if (!unmounted) {
28
+ setTranslations(resource.default);
29
+ }
30
+ }
31
+ })();
32
+
33
+ return () => {
34
+ unmounted = true;
35
+ };
36
+ }, [language, labelTranslations]);
37
+
38
+ return <I18nContext.Provider value={translations}>{children}</I18nContext.Provider>;
39
+ }
40
+
41
+ export function replaceI18nVariablesInString(str: string, replaceWith: string) {
42
+ return str.replace(/({{[^}]+}})/g, replaceWith);
43
+ }
44
+
45
+ export function useTr() {
46
+ const context = useContext(I18nContext);
47
+
48
+ return (key: keyof I18N, units?: number) => {
49
+ const thereAreUnits = typeof units === 'number';
50
+ const keyToUse = thereAreUnits && units > 1 ? `${key}_plural` : key;
51
+
52
+ if (context && keyToUse in context) {
53
+ // @ts-expect-error
54
+ const tr: string = context[keyToUse];
55
+
56
+ if (thereAreUnits) {
57
+ return replaceI18nVariablesInString(tr, units.toString());
58
+ }
59
+ return tr;
60
+ }
61
+
62
+ return '';
63
+ };
64
+ }
@@ -0,0 +1,66 @@
1
+ import type { I18N } from '../types';
2
+
3
+ const translations: I18N = {
4
+ actionClearTextFormatting: 'Clear text formatting',
5
+ actionTextFormattingOptions: 'Formatting options for additional text styles',
6
+ actionFormatAsStrongLabel: 'Format text as bold',
7
+ actionFormatAsStrongTitle: 'Bold (Ctrl+B)',
8
+ actionFormatAsStrongTitleApple: 'Bold (⌘B)',
9
+ actionFormatAsEmphasizedLabel: 'Format text as italics',
10
+ actionFormatAsEmphasizedTitle: 'Italic (Ctrl+I)',
11
+ actionFormatAsEmphasizedTitleApple: 'Italic (⌘I)',
12
+ actionFormatAsUnderlinedLabel: 'Underline text',
13
+ actionFormatAsUnderlinedTitle: 'Underlined (Ctrl+U)',
14
+ actionFormatAsUnderlinedTitleApple: 'Underlined (⌘U)',
15
+ actionFormatWithStrikethroughLabel: 'Format text with a strikethrough',
16
+ actionFormatWithStrikethroughTitle: 'Strikethrough',
17
+ actionFormatWithSubscriptLabel: 'Format text as subscript',
18
+ actionFormatWithSubscriptTitle: 'Subscript',
19
+ actionFormatWithSuperscriptLabel: 'Format text as superscript',
20
+ actionFormatWithSuperscriptTitle: 'Superscript',
21
+ actionInsertCodeBlock: 'Insert code block',
22
+ actionInsertlink: 'Insert link',
23
+ actionCopyJSON: 'Copy JSON',
24
+ actionCopyCode: 'Copy code',
25
+ actionClear: 'Clear editor',
26
+ actionFormatCode: 'Format code with Prettier',
27
+ actionTableInsertRowsAbove: 'Insert row above',
28
+ actionTableInsertRowsAbove_plural: 'Insert {{rows}} rows above',
29
+ actionTableInsertRowsBelow: 'Insert row above',
30
+ actionTableInsertRowsBelow_plural: 'Insert {{rows}} rows above',
31
+ actionTableInsertColumnsBefore: 'Insert column left',
32
+ actionTableInsertColumnsBefore_plural: 'Insert {{columns}} columns left',
33
+ actionTableInsertColumnsAfter: 'Insert column right',
34
+ actionTableInsertColumnsAfter_plural: 'Insert {{columns}} columns right',
35
+ actionTableAddRowHeader: 'Add row header',
36
+ actionTableRemoveRowHeader: 'Remove row header',
37
+ actionTableAddColumnHeader: 'Add column header',
38
+ actionTableRemoveColumnHeader: 'Remove column header',
39
+ actionTableDeleteColumn: 'Delete column',
40
+ actionTableDeleteRow: 'Delete row',
41
+ actionTableDeleteTable: 'Delete table',
42
+ actionTableOpenOptions: 'Open table options',
43
+ actionUndoLabel: 'Undo',
44
+ actionUndoTitle: 'Undo (Ctrl+Z)',
45
+ actionUndoTitleApple: 'Undo (⌘Z)',
46
+ actionRedoLabel: 'Undo',
47
+ actionRedoTitle: 'Undo (Ctrl+Y)',
48
+ actionRedoTitleApple: 'Redo (⌘Y)',
49
+ codeSelectLanguage: 'Select language',
50
+ linkEditorLink: 'Link',
51
+ linkEditorRel: 'Rel',
52
+ linkEditorTarget: 'Target',
53
+ linkEditorCommit: 'Done',
54
+ linkEditorEdit: 'Edit',
55
+ linkPreviewReplaceTextWithTitle: 'Replace link text with its title',
56
+ horizontalRule: 'Horizontal rule',
57
+ table: 'Table',
58
+ insertTableTitle: 'Insert table',
59
+ insertTableDescription:
60
+ 'Define your starting point of a table, you can add and remove columns and rows after creation.',
61
+ insertTableRows: 'Rows',
62
+ insertTableColumns: 'Columns',
63
+ insertTableCommit: 'Insert table',
64
+ } as const;
65
+
66
+ export default translations;
@@ -0,0 +1,62 @@
1
+ export type I18NKeys =
2
+ | 'actionClearTextFormatting'
3
+ | 'actionTextFormattingOptions'
4
+ | 'actionFormatAsStrongLabel'
5
+ | 'actionFormatAsStrongTitle'
6
+ | 'actionFormatAsStrongTitleApple'
7
+ | 'actionFormatAsEmphasizedLabel'
8
+ | 'actionFormatAsEmphasizedTitle'
9
+ | 'actionFormatAsEmphasizedTitleApple'
10
+ | 'actionFormatAsUnderlinedLabel'
11
+ | 'actionFormatAsUnderlinedTitle'
12
+ | 'actionFormatAsUnderlinedTitleApple'
13
+ | 'actionFormatWithStrikethroughLabel'
14
+ | 'actionFormatWithStrikethroughTitle'
15
+ | 'actionFormatWithSubscriptLabel'
16
+ | 'actionFormatWithSubscriptTitle'
17
+ | 'actionFormatWithSuperscriptLabel'
18
+ | 'actionFormatWithSuperscriptTitle'
19
+ | 'actionCopyJSON'
20
+ | 'actionClear'
21
+ | 'actionFormatCode'
22
+ | 'actionCopyCode'
23
+ | 'actionInsertCodeBlock'
24
+ | 'actionInsertlink'
25
+ | 'actionTableInsertRowsAbove'
26
+ | 'actionTableInsertRowsAbove_plural'
27
+ | 'actionTableInsertRowsBelow'
28
+ | 'actionTableInsertRowsBelow_plural'
29
+ | 'actionTableInsertColumnsBefore'
30
+ | 'actionTableInsertColumnsBefore_plural'
31
+ | 'actionTableInsertColumnsAfter'
32
+ | 'actionTableInsertColumnsAfter_plural'
33
+ | 'actionTableAddRowHeader'
34
+ | 'actionTableRemoveRowHeader'
35
+ | 'actionTableAddColumnHeader'
36
+ | 'actionTableRemoveColumnHeader'
37
+ | 'actionTableDeleteColumn'
38
+ | 'actionTableDeleteRow'
39
+ | 'actionTableDeleteTable'
40
+ | 'actionTableOpenOptions'
41
+ | 'actionUndoLabel'
42
+ | 'actionUndoTitle'
43
+ | 'actionUndoTitleApple'
44
+ | 'actionRedoLabel'
45
+ | 'actionRedoTitle'
46
+ | 'actionRedoTitleApple'
47
+ | 'codeSelectLanguage'
48
+ | 'linkEditorLink'
49
+ | 'linkEditorRel'
50
+ | 'linkEditorTarget'
51
+ | 'linkEditorCommit'
52
+ | 'linkEditorEdit'
53
+ | 'linkPreviewReplaceTextWithTitle'
54
+ | 'horizontalRule'
55
+ | 'table'
56
+ | 'insertTableTitle'
57
+ | 'insertTableDescription'
58
+ | 'insertTableRows'
59
+ | 'insertTableColumns'
60
+ | 'insertTableCommit';
61
+
62
+ export type I18N = Record<I18NKeys, string>;
@@ -11,6 +11,7 @@ import { CLEAR_EDITOR_COMMAND, LexicalEditor } from 'lexical';
11
11
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
12
12
 
13
13
  import { ActionMenu } from '../../../action-menu';
14
+ import { useTr } from '../../i18n';
14
15
  import { lexicalToCrystallizeRichText } from '../../model/lexical-to-crystallize';
15
16
  import type { CrystallizeRichTextActionMenuItem } from '../../types/types';
16
17
 
@@ -24,25 +25,6 @@ async function copyJson(editor: LexicalEditor) {
24
25
  }
25
26
  }
26
27
 
27
- async function exportJson(editor: LexicalEditor) {
28
- const json = lexicalToCrystallizeRichText({ editorState: editor.getEditorState() });
29
-
30
- const blob = new Blob([JSON.stringify(json, null, 1)], {
31
- type: 'application/json',
32
- });
33
- const href = URL.createObjectURL(blob);
34
-
35
- const link = document.createElement('a');
36
- link.href = href;
37
- link.download = 'crystallizeRichText.json';
38
-
39
- document.body.appendChild(link);
40
- link.click();
41
- document.body.removeChild(link);
42
-
43
- URL.revokeObjectURL(href);
44
- }
45
-
46
28
  export default function ActionsPlugin({
47
29
  append,
48
30
  prepend,
@@ -51,6 +33,8 @@ export default function ActionsPlugin({
51
33
  prepend?: CrystallizeRichTextActionMenuItem[];
52
34
  }): JSX.Element {
53
35
  const [editor] = useLexicalComposerContext();
36
+ const tr = useTr();
37
+
54
38
  return (
55
39
  <div className="z-50 flex items-center ">
56
40
  <div></div>
@@ -66,8 +50,7 @@ export default function ActionsPlugin({
66
50
  {actionItem.title}
67
51
  </ActionMenu.Item>
68
52
  ))}
69
- <ActionMenu.Item onSelect={() => copyJson(editor)}>Copy JSON</ActionMenu.Item>
70
- <ActionMenu.Item onSelect={() => exportJson(editor)}>Export JSON</ActionMenu.Item>
53
+ <ActionMenu.Item onSelect={() => copyJson(editor)}>{tr('actionCopyJSON')}</ActionMenu.Item>
71
54
  <ActionMenu.Item
72
55
  className="danger"
73
56
  onSelect={() => {
@@ -75,7 +58,7 @@ export default function ActionsPlugin({
75
58
  editor.focus();
76
59
  }}
77
60
  >
78
- Clear paragraph
61
+ {tr('actionClear')}
79
62
  </ActionMenu.Item>
80
63
  {!append
81
64
  ? null
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, $getSelection, $setSelection, LexicalEditor
12
12
  import { useDebouncedCallback } from 'use-debounce';
13
13
  import { $isCodeNode } from '@lexical/code';
14
14
 
15
+ import { useTr } from '../../../../i18n';
16
+
15
17
  interface Props {
16
18
  editor: LexicalEditor;
17
19
  getCodeDOMNode: () => HTMLElement | null;
@@ -19,6 +21,7 @@ interface Props {
19
21
 
20
22
  export function CopyButton({ editor, getCodeDOMNode }: Props) {
21
23
  const [isCopyCompleted, setCopyCompleted] = useState<boolean>(false);
24
+ const tr = useTr();
22
25
 
23
26
  const removeSuccessIcon = useDebouncedCallback(() => {
24
27
  setCopyCompleted(false);
@@ -54,7 +57,7 @@ export function CopyButton({ editor, getCodeDOMNode }: Props) {
54
57
  }
55
58
 
56
59
  return (
57
- <button className="menu-item" onClick={handleClick} aria-label="copy">
60
+ <button className="menu-item" onClick={handleClick} aria-label={tr('actionCopyCode')}>
58
61
  {isCopyCompleted ? <i className="format success" /> : <i className="format copy" />}
59
62
  </button>
60
63
  );
@@ -12,6 +12,8 @@ import { $getNearestNodeFromDOMNode, LexicalEditor } from 'lexical';
12
12
  import type { Options } from 'prettier';
13
13
  import { $isCodeNode } from '@lexical/code';
14
14
 
15
+ import { useTr } from '../../../../i18n';
16
+
15
17
  interface Props {
16
18
  lang: string;
17
19
  editor: LexicalEditor;
@@ -69,6 +71,7 @@ function getPrettierOptions(lang: string): Options {
69
71
  export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
70
72
  const [syntaxError, setSyntaxError] = useState<string>('');
71
73
  const [tipsVisible, setTipsVisible] = useState<boolean>(false);
74
+ const tr = useTr();
72
75
 
73
76
  async function handleClick(): Promise<void> {
74
77
  const codeDOMNode = getCodeDOMNode();
@@ -92,6 +95,13 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
92
95
 
93
96
  parsed = format(content, options);
94
97
 
98
+ /**
99
+ * Remove EOF from prettier output. This is useful when
100
+ * using prettier on files, but becomes weird when the code
101
+ * is embedded within a larger portion of text.
102
+ */
103
+ parsed = parsed.replace(/[\r\n]+$/, '');
104
+
95
105
  if (parsed !== '') {
96
106
  const selection = codeNode.select(0);
97
107
  selection.insertText(parsed);
@@ -129,7 +139,7 @@ export function PrettierButton({ lang, editor, getCodeDOMNode }: Props) {
129
139
  onClick={handleClick}
130
140
  onMouseEnter={handleMouseEnter}
131
141
  onMouseLeave={handleMouseLeave}
132
- aria-label="prettier"
142
+ aria-label={tr('actionFormatCode')}
133
143
  >
134
144
  {syntaxError ? <i className="format prettier-error" /> : <i className="format prettier" />}
135
145
  </button>
@@ -29,7 +29,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
29
29
 
30
30
  const [lang, setLang] = useState('');
31
31
  const [isShown, setShown] = useState<boolean>(false);
32
- const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(false);
32
+ const [shouldListenMouseMove, setShouldListenMouseMove] = useState<boolean>(true);
33
33
  const [position, setPosition] = useState<Position>({
34
34
  right: '0',
35
35
  top: '0',
@@ -43,6 +43,7 @@ function CodeActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }): J
43
43
 
44
44
  const debouncedOnMouseMove = useDebouncedCallback((event: MouseEvent) => {
45
45
  const { codeDOMNode, isOutside } = getMouseInfo(event);
46
+
46
47
  if (isOutside) {
47
48
  setShown(false);
48
49
  return;
@@ -30,6 +30,7 @@ import { Button } from '../../../button';
30
30
  import { IconButton } from '../../../icon-button';
31
31
  import { Icon } from '../../../iconography';
32
32
  import { InputWithLabel } from '../../../input-with-label';
33
+ import { useTr } from '../../i18n';
33
34
  import LinkPreview from '../../ui/LinkPreview';
34
35
  import { getSelectedNode } from '../../utils/getSelectedNode';
35
36
  import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
@@ -53,6 +54,7 @@ function FloatingLinkEditor({
53
54
  const [target, setTarget] = useState<string | null>(null);
54
55
  const [isEditMode, setEditMode] = useState(false);
55
56
  const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null);
57
+ const tr = useTr();
56
58
 
57
59
  const updateLinkEditor = useCallback(() => {
58
60
  const selection = $getSelection();
@@ -187,14 +189,29 @@ function FloatingLinkEditor({
187
189
  {isEditMode ? (
188
190
  <div>
189
191
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
190
- <InputWithLabel label="Link" type="text" value={linkUrl} onChange={e => setLinkUrl(e.target.value)} />
192
+ <InputWithLabel
193
+ label={tr('linkEditorLink')}
194
+ type="text"
195
+ value={linkUrl}
196
+ onChange={e => setLinkUrl(e.target.value)}
197
+ />
191
198
  </div>
192
199
 
193
200
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
194
- <InputWithLabel label="Rel" type="text" value={rel ?? ''} onChange={e => setRel(e.target.value)} />
201
+ <InputWithLabel
202
+ label={tr('linkEditorRel')}
203
+ type="text"
204
+ value={rel ?? ''}
205
+ onChange={e => setRel(e.target.value)}
206
+ />
195
207
  </div>
196
208
  <div className="border-0 border-b border-gray-100-800 border-solid px-3">
197
- <InputWithLabel label="Target" type="text" value={target ?? ''} onChange={e => setTarget(e.target.value)} />
209
+ <InputWithLabel
210
+ label={tr('linkEditorTarget')}
211
+ type="text"
212
+ value={target ?? ''}
213
+ onChange={e => setTarget(e.target.value)}
214
+ />
198
215
  </div>
199
216
  <div className="flex px-6 py-2 justify-end">
200
217
  <Button
@@ -211,14 +228,14 @@ function FloatingLinkEditor({
211
228
  }
212
229
  }}
213
230
  >
214
- Done
231
+ {tr('linkEditorCommit')}
215
232
  </Button>
216
233
  </div>
217
234
  </div>
218
235
  ) : (
219
236
  <>
220
237
  <div className="link-input !flex flex-nowrap justify-between items-center max-w-full ">
221
- <div className=" grid">
238
+ <div className="grid">
222
239
  <a href={linkUrl} target="_blank" rel="noopener noreferrer">
223
240
  {linkUrl}
224
241
  </a>
@@ -241,6 +258,7 @@ function FloatingLinkEditor({
241
258
  tabIndex={0}
242
259
  onMouseDown={event => event.preventDefault()}
243
260
  onClick={() => setEditMode(true)}
261
+ aria-label={tr('linkEditorEdit')}
244
262
  >
245
263
  <Icon.Edit />
246
264
  </IconButton>
@@ -25,6 +25,8 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext
25
25
  import { mergeRegister } from '@lexical/utils';
26
26
 
27
27
  import { IconButton } from '../../../icon-button';
28
+ import { useTr } from '../../i18n';
29
+ import { IS_APPLE } from '../../utils/environment';
28
30
  import { getDOMRangeRect } from '../../utils/getDOMRangeRect';
29
31
  import { getSelectedNode } from '../../utils/getSelectedNode';
30
32
  import { setFloatingElemPosition } from '../../utils/setFloatingElemPosition';
@@ -53,6 +55,7 @@ function TextFormatFloatingToolbar({
53
55
  isUnderline: boolean;
54
56
  }): JSX.Element {
55
57
  const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
58
+ const tr = useTr();
56
59
 
57
60
  const insertLink = useCallback(() => {
58
61
  if (!isLink) {
@@ -139,7 +142,8 @@ function TextFormatFloatingToolbar({
139
142
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
140
143
  }}
141
144
  style={{ padding: 0, overflow: 'hidden' }}
142
- aria-label="Format text as bold"
145
+ title={tr(IS_APPLE ? 'actionFormatAsStrongTitleApple' : 'actionFormatAsStrongTitle')}
146
+ aria-label={tr('actionFormatAsStrongLabel')}
143
147
  >
144
148
  <i
145
149
  className={`format bold w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -152,7 +156,8 @@ function TextFormatFloatingToolbar({
152
156
  onClick={() => {
153
157
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
154
158
  }}
155
- aria-label="Format text as italics"
159
+ title={tr('actionFormatAsEmphasizedTitle')}
160
+ aria-label={tr('actionFormatAsEmphasizedLabel')}
156
161
  >
157
162
  <i
158
163
  className={`format italic w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -165,7 +170,8 @@ function TextFormatFloatingToolbar({
165
170
  onClick={() => {
166
171
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
167
172
  }}
168
- aria-label="Format text to underlined"
173
+ title={tr('actionFormatAsUnderlinedTitle')}
174
+ aria-label={tr('actionFormatAsUnderlinedLabel')}
169
175
  >
170
176
  <i
171
177
  className={`format underline w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -178,7 +184,8 @@ function TextFormatFloatingToolbar({
178
184
  onClick={() => {
179
185
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
180
186
  }}
181
- aria-label="Format text with a strikethrough"
187
+ title={tr('actionFormatWithStrikethroughTitle')}
188
+ aria-label={tr('actionFormatWithStrikethroughLabel')}
182
189
  >
183
190
  <i
184
191
  className={`format strikethrough w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -191,8 +198,8 @@ function TextFormatFloatingToolbar({
191
198
  onClick={() => {
192
199
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
193
200
  }}
194
- title="Subscript"
195
- aria-label="Format Subscript"
201
+ title={tr('actionFormatWithSubscriptTitle')}
202
+ aria-label={tr('actionFormatWithSubscriptLabel')}
196
203
  >
197
204
  <i
198
205
  className={`format subscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -205,8 +212,8 @@ function TextFormatFloatingToolbar({
205
212
  onClick={() => {
206
213
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
207
214
  }}
208
- title="Superscript"
209
- aria-label="Format Superscript"
215
+ title={tr('actionFormatWithSuperscriptTitle')}
216
+ aria-label={tr('actionFormatWithSuperscriptLabel')}
210
217
  >
211
218
  <i
212
219
  className={`format superscript w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -219,7 +226,7 @@ function TextFormatFloatingToolbar({
219
226
  onClick={() => {
220
227
  editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
221
228
  }}
222
- aria-label="Insert code block"
229
+ aria-label={tr('actionInsertCodeBlock')}
223
230
  >
224
231
  <i
225
232
  className={`format code w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
@@ -227,7 +234,11 @@ function TextFormatFloatingToolbar({
227
234
  }`}
228
235
  />
229
236
  </IconButton>
230
- <IconButton style={{ padding: 0, overflow: 'hidden' }} onClick={insertLink} aria-label="Insert link">
237
+ <IconButton
238
+ style={{ padding: 0, overflow: 'hidden' }}
239
+ onClick={insertLink}
240
+ aria-label={tr('actionInsertlink')}
241
+ >
231
242
  <i
232
243
  className={`format link w-full h-full bg-[length:18px_18px] bg-no-repeat bg-center ${
233
244
  isLink ? 'bg-purple-50-900 opacity-100' : 'opacity-60'
@@ -6,14 +6,9 @@
6
6
  *
7
7
  */
8
8
 
9
- import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
10
- import {
11
- $getSelection,
12
- $isRangeSelection,
13
- $setSelection,
14
- FOCUS_COMMAND,
15
- } from 'lexical';
16
- import {useEffect} from 'react';
9
+ import { useEffect } from 'react';
10
+ import { $getSelection, $isRangeSelection, $setSelection, FOCUS_COMMAND } from 'lexical';
11
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
17
12
 
18
13
  const COMMAND_PRIORITY_LOW = 1;
19
14
  const TAB_TO_FOCUS_INTERVAL = 100;
@@ -48,10 +43,7 @@ export default function TabFocusPlugin(): null {
48
43
  (event: FocusEvent) => {
49
44
  const selection = $getSelection();
50
45
  if ($isRangeSelection(selection)) {
51
- if (
52
- lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL >
53
- event.timeStamp
54
- ) {
46
+ if (lastTabKeyDownTimestamp + TAB_TO_FOCUS_INTERVAL > event.timeStamp) {
55
47
  $setSelection(selection.clone());
56
48
  }
57
49
  }
@@ -33,6 +33,7 @@ import {
33
33
  import { DropdownMenu } from '../../../dropdown-menu';
34
34
  import { IconButton } from '../../../icon-button';
35
35
  import { Icon } from '../../../iconography';
36
+ import { useTr } from '../../i18n';
36
37
 
37
38
  type TableStats = {
38
39
  rows: number;
@@ -51,6 +52,7 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
51
52
  columns: 1,
52
53
  rows: 1,
53
54
  });
55
+ const tr = useTr();
54
56
 
55
57
  useEffect(() => {
56
58
  return editor.registerMutationListener(TableCellNode, nodeMutations => {
@@ -254,41 +256,48 @@ function TableActionMenu({ tableCellNode: _tableCellNode, tableStats }: TableCel
254
256
  return (
255
257
  <>
256
258
  <DropdownMenu.Item onSelect={() => insertTableRowAtSelection(false)}>
257
- Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} above
259
+ {tr('actionTableInsertRowsAbove', selectionCounts.rows)}
258
260
  </DropdownMenu.Item>
259
261
  <DropdownMenu.Item onSelect={() => insertTableRowAtSelection(true)}>
260
- Insert {selectionCounts.rows === 1 ? 'row' : `${selectionCounts.rows} rows`} below
262
+ {tr('actionTableInsertRowsBelow', selectionCounts.rows)}
261
263
  </DropdownMenu.Item>
262
264
  <DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(false)}>
263
- Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} left
265
+ {tr('actionTableInsertColumnsBefore', selectionCounts.columns)}
264
266
  </DropdownMenu.Item>
265
267
  <DropdownMenu.Item onSelect={() => insertTableColumnAtSelection(true)}>
266
- Insert {selectionCounts.columns === 1 ? 'column' : `${selectionCounts.columns} columns`} right
268
+ {tr('actionTableInsertColumnsAfter', selectionCounts.columns)}
267
269
  </DropdownMenu.Item>
268
270
  <DropdownMenu.Item onSelect={() => toggleTableRowIsHeader()}>
269
- {(tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW ? 'Remove' : 'Add'} row
270
- header
271
+ {tr(
272
+ (tableCellNode.__headerState & TableCellHeaderStates.ROW) === TableCellHeaderStates.ROW
273
+ ? 'actionTableRemoveRowHeader'
274
+ : 'actionTableAddRowHeader',
275
+ )}
271
276
  </DropdownMenu.Item>
272
277
  <DropdownMenu.Item onSelect={() => toggleTableColumnIsHeader()}>
273
- {(tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
274
- ? 'Remove'
275
- : 'Add'}{' '}
276
- column header
278
+ {tr(
279
+ (tableCellNode.__headerState & TableCellHeaderStates.COLUMN) === TableCellHeaderStates.COLUMN
280
+ ? 'actionTableRemoveColumnHeader'
281
+ : 'actionTableAddColumnHeader',
282
+ )}
277
283
  </DropdownMenu.Item>
278
284
  <DropdownMenu.Separator />
279
285
  {tableStats.columns > 1 && (
280
- <DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>Delete column</DropdownMenu.Item>
286
+ <DropdownMenu.Item onSelect={() => deleteTableColumnAtSelection()}>
287
+ {tr('actionTableDeleteColumn')}
288
+ </DropdownMenu.Item>
281
289
  )}
282
290
  {tableStats.rows > 1 && (
283
- <DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>Delete row</DropdownMenu.Item>
291
+ <DropdownMenu.Item onSelect={() => deleteTableRowAtSelection()}>{tr('actionTableDeleteRow')}</DropdownMenu.Item>
284
292
  )}
285
- <DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>Delete table</DropdownMenu.Item>
293
+ <DropdownMenu.Item onSelect={() => deleteTableAtSelection()}>{tr('actionTableDeleteTable')}</DropdownMenu.Item>
286
294
  </>
287
295
  );
288
296
  }
289
297
 
290
298
  function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement }) {
291
299
  const [editor] = useLexicalComposerContext();
300
+ const tr = useTr();
292
301
 
293
302
  const menuButtonRef = useRef(null);
294
303
 
@@ -393,7 +402,7 @@ function TableCellActionMenuContainer({ anchorElem }: { anchorElem: HTMLElement
393
402
  onOpenChange={isOpen => setIsMenuOpen(isOpen)}
394
403
  content={<TableActionMenu tableCellNode={tableCellNode} tableStats={tableStats} />}
395
404
  >
396
- <IconButton size="xs" className="table-cell-action-button">
405
+ <IconButton size="xs" className="table-cell-action-button" aria-label={tr('actionTableOpenOptions')}>
397
406
  <Icon.Arrow />
398
407
  </IconButton>
399
408
  </DropdownMenu.Root>