@inshapardaz/likhari-react 0.1.6-dev.60 → 0.1.6-dev.62

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.
@@ -3,6 +3,7 @@ import type { SerializedEditorState } from 'lexical';
3
3
  import { type FormatId } from '@inshapardaz/likhari-converters';
4
4
  import { type EditorFeatureConfig, type FeatureConfigPresetName } from '@inshapardaz/likhari-core';
5
5
  import { type FontOption } from './fonts/index.js';
6
+ import { type Locale } from './i18n/index.js';
6
7
  export interface EditorInitialContent {
7
8
  format: Extract<FormatId, 'lexical-json' | 'plain-text'>;
8
9
  value: string;
@@ -14,7 +15,7 @@ export interface EditorRootProps {
14
15
  featurePreset?: FeatureConfigPresetName;
15
16
  theme?: MantineThemeOverride;
16
17
  colorScheme?: 'light' | 'dark';
17
- locale?: 'en' | 'ur' | 'pa-shahmukhi';
18
+ locale?: Locale;
18
19
  placeholder?: string;
19
20
  /**
20
21
  * CSS height for the editor box (toolbar + canvas together) — accepts any
@@ -1 +1 @@
1
- {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAU1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAIjE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGA8IrB,CAAC"}
1
+ {"version":3,"file":"EditorRoot.d.ts","sourceRoot":"","sources":["../src/EditorRoot.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAU1D,OAAO,KAAK,EAAe,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,EAAwB,KAAK,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAKzH,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,SAAS,CAAC;AAGjE,OAAO,EAAgC,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEnE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC;IACzD,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,WAAW,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;IAClD;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC;IACrD;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,CAAC;IACrC,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAG,IAAI,CAAC;IAClD,iBAAiB,IAAI,OAAO,CAAC;IAC7B,cAAc,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,KAAK,IAAI,IAAI,CAAC;CACf;AAQD,eAAO,MAAM,UAAU,uGAkJrB,CAAC"}
@@ -18,14 +18,17 @@ import { Toolbar } from './components/Toolbar.js';
18
18
  import { injectUrduWebFontsCss } from './fonts/index.js';
19
19
  import { LinkPastePlugin } from './plugins/LinkPastePlugin.js';
20
20
  import { ImageOptionsContext } from './image/ImageOptionsContext.js';
21
+ import { UiStringsContext, getStrings } from './i18n/index.js';
21
22
  function initialEditorStateJson(initialContent) {
22
23
  if (!initialContent)
23
24
  return undefined;
24
25
  const state = defaultFormatRegistry.parse(initialContent.format, initialContent.value);
25
26
  return JSON.stringify(state);
26
27
  }
27
- export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder = 'Start writing…', height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, }, ref) {
28
+ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialContent, featureConfig, featurePreset, theme, colorScheme, locale = 'en', placeholder, height = '480px', onChange, onSave, showSave = Boolean(onSave), onImageUpload, fetchImage, fontOptions, }, ref) {
28
29
  const config = useMemo(() => resolveFeatureConfig(featureConfig, featurePreset), [featureConfig, featurePreset]);
30
+ const strings = useMemo(() => getStrings(locale), [locale]);
31
+ const resolvedPlaceholder = placeholder ?? strings.editor.placeholder;
29
32
  // The urdu-web-fonts stylesheets are needed for the font dropdown and for
30
33
  // RTL content, whose canvas font (editor.css) is one of those families.
31
34
  useEffect(() => {
@@ -78,12 +81,12 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
78
81
  return true;
79
82
  if (typeof window === 'undefined')
80
83
  return true;
81
- return window.confirm('You have unsaved changes. Discard them?');
84
+ return window.confirm(strings.editor.confirmDiscard);
82
85
  },
83
86
  focus() {
84
87
  rootElementRef.current?.focus();
85
88
  },
86
- }), [isDirty]);
89
+ }), [isDirty, strings]);
87
90
  const rootElementRef = useRef(null);
88
91
  const handleChange = (state) => {
89
92
  editorStateRef.current = state;
@@ -102,5 +105,5 @@ export const EditorRoot = forwardRef(function EditorRoot({ documentId, initialCo
102
105
  onSave?.(content, format);
103
106
  };
104
107
  const dir = locale === 'en' ? 'ltr' : 'rtl';
105
- return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": "Editor content" }), placeholder: _jsx("div", { className: "likhari-placeholder", children: placeholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }) }));
108
+ return (_jsx(EditorThemeProvider, { theme: theme, colorScheme: colorScheme, children: _jsx("div", { className: "likhari-root", dir: dir, ref: rootElementRef, "data-document-id": documentId, style: { height: typeof height === 'number' ? `${height}px` : height }, children: _jsx(UiStringsContext.Provider, { value: strings, children: _jsx(ImageOptionsContext.Provider, { value: imageOptions, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsx(Toolbar, { config: config, onSave: handleSave, isDirty: isDirty, showSave: showSave, fontOptions: fontOptions, direction: dir, locale: locale }), _jsx("div", { className: "likhari-canvas", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "likhari-content-editable", dir: dir, "aria-label": strings.editor.contentLabel }), placeholder: _jsx("div", { className: "likhari-placeholder", children: resolvedPlaceholder }), ErrorBoundary: LexicalErrorBoundary }) }), config.history && _jsx(HistoryPlugin, {}), (config.lists.bullet || config.lists.numbered || config.lists.check) && _jsx(ListPlugin, {}), config.lists.check && _jsx(CheckListPlugin, {}), config.links && _jsx(LinkPlugin, {}), config.links && _jsx(LinkPastePlugin, {}), _jsx(OnChangePlugin, { onChange: handleChange })] }) }) }) }) }));
106
109
  });
@@ -1 +1 @@
1
- {"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,+BAgE7G"}
1
+ {"version":3,"file":"LinkDialog.d.ts","sourceRoot":"","sources":["../../src/components/LinkDialog.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;gDACgD;AAChD,wBAAgB,UAAU,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,eAAe,+BAiE7G"}
@@ -2,9 +2,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Button, Group, Modal, TextInput } from '@mantine/core';
4
4
  import { normalizeLinkUrl } from '../utils/linkUrl.js';
5
+ import { useUiStrings } from '../i18n/useStrings.js';
5
6
  /** Insert/edit-link dialog (UI spec §6). Mantine Modal supplies focus
6
7
  * trapping, Escape-to-close and the overlay. */
7
8
  export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemove, onClose }) {
9
+ const strings = useUiStrings();
8
10
  const [url, setUrl] = useState(initialUrl);
9
11
  const [text, setText] = useState('');
10
12
  const [error, setError] = useState(null);
@@ -19,13 +21,13 @@ export function LinkDialog({ opened, initialUrl, showTextField, onSubmit, onRemo
19
21
  e.preventDefault();
20
22
  const normalized = normalizeLinkUrl(url);
21
23
  if (!normalized) {
22
- setError('Enter a valid http(s), mailto, tel or relative URL');
24
+ setError(strings.linkDialog.invalidUrl);
23
25
  return;
24
26
  }
25
27
  onSubmit({ url: normalized, text: text.trim() || normalized });
26
28
  };
27
- return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ? 'Edit link' : 'Insert link', centered: true, size: "sm", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: "URL", placeholder: "https://example.com", value: url, onChange: (e) => {
29
+ return (_jsx(Modal, { opened: opened, onClose: onClose, title: onRemove ? strings.linkDialog.titleEdit : strings.linkDialog.titleInsert, centered: true, size: "sm", children: _jsxs("form", { onSubmit: handleSubmit, children: [_jsx(TextInput, { label: strings.linkDialog.urlLabel, placeholder: strings.linkDialog.urlPlaceholder, value: url, onChange: (e) => {
28
30
  setUrl(e.currentTarget.value);
29
31
  setError(null);
30
- }, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label: "Text", placeholder: "Link text (defaults to the URL)", value: text, onChange: (e) => setText(e.currentTarget.value) })), _jsxs(Group, { justify: "space-between", mt: "md", children: [onRemove ? (_jsx(Button, { variant: "subtle", color: "red", onClick: onRemove, children: "Remove link" })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", children: "Apply" })] })] })] }) }));
32
+ }, error: error, "data-autofocus": true }), showTextField && (_jsx(TextInput, { mt: "sm", label: strings.linkDialog.textLabel, placeholder: strings.linkDialog.textPlaceholder, value: text, onChange: (e) => setText(e.currentTarget.value) })), _jsxs(Group, { justify: "space-between", mt: "md", children: [onRemove ? (_jsx(Button, { variant: "subtle", color: "red", onClick: onRemove, children: strings.link.removeLink })) : (_jsx("span", {})), _jsxs(Group, { gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", children: strings.linkDialog.apply })] })] })] }) }));
31
33
  }
@@ -1,5 +1,6 @@
1
1
  import type { ResolvedEditorFeatureConfig } from '@inshapardaz/likhari-core';
2
2
  import { type FontOption } from '../fonts/index.js';
3
+ import type { Locale } from '../i18n/strings.js';
3
4
  export interface ToolbarProps {
4
5
  config: ResolvedEditorFeatureConfig;
5
6
  onSave?: () => void;
@@ -9,6 +10,8 @@ export interface ToolbarProps {
9
10
  fontOptions?: FontOption[];
10
11
  /** Text direction of the canvas; picks which default font and size are pre-selected. */
11
12
  direction?: 'ltr' | 'rtl';
13
+ /** UI locale — picks the translated strings for labels, tooltips and menu items. */
14
+ locale?: Locale;
12
15
  }
13
- export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction }: ToolbarProps): import("react").JSX.Element;
16
+ export declare function Toolbar({ config, onSave, isDirty, showSave, fontOptions, direction, locale }: ToolbarProps): import("react").JSX.Element;
14
17
  //# sourceMappingURL=Toolbar.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAmPtG,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CAC3B;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,EAAE,YAAY,+BAgoBjI"}
1
+ {"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/components/Toolbar.tsx"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAA6D,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAGtG,OAAO,KAAK,EAAE,MAAM,EAAW,MAAM,iBAAiB,CAAC;AAoPvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,2BAA2B,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,wFAAwF;IACxF,SAAS,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;IAC1B,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAkC,EAAE,SAAiB,EAAE,MAAa,EAAE,EAAE,YAAY,+BAqoBhJ"}
@@ -15,6 +15,7 @@ import { LinkDialog } from './LinkDialog.js';
15
15
  import { normalizeLinkUrl } from '../utils/linkUrl.js';
16
16
  import { CANVAS_FONT_DEFAULTS, DEFAULT_FONT_OPTIONS, FONT_SIZES_PX } from '../fonts/index.js';
17
17
  import { setStyleProperty } from '../utils/style.js';
18
+ import { useStrings } from '../i18n/useStrings.js';
18
19
  import { IconAbc, IconAlignCenter, IconAlignJustified, IconAlignLeft, IconAlignRight, IconArrowBackUp, IconArrowForwardUp, IconBold, IconClearFormatting, IconDeviceFloppy, IconDots, IconExternalLink, IconFeather, IconIndentDecrease, IconIndentIncrease, IconItalic, IconLetterCase, IconLetterCaseLower, IconLetterCaseUpper, IconLink, IconLinkOff, IconPencil, IconPhoto, IconPilcrow, IconSparkles, IconStrikethrough, IconSubscript, IconSuperscript, IconTextSize, IconTypography, IconUnderline, IconWand, } from '@tabler/icons-react';
19
20
  /** Sentinel for "no inline style": Lexical returns the default it is given when
20
21
  * the selection has none, so this tells "unstyled" (show the canvas default)
@@ -49,15 +50,15 @@ function ToolbarButton({ icon: Icon, title, active, dirty, disabled, onClick, })
49
50
  * isn't implemented yet (link/image/poetry/font/language-tooling — see
50
51
  * docs/lexical-editor-spec.md §13's phasing). Renders so the toolbar's
51
52
  * layout is final now and only needs its onClick wired up later. */
52
- function StubButton({ icon, title }) {
53
- return _jsx(ToolbarButton, { icon: icon, title: `${title} (coming soon)`, disabled: true, onClick: undefined });
53
+ function StubButton({ icon, title, comingSoon }) {
54
+ return _jsx(ToolbarButton, { icon: icon, title: comingSoon(title), disabled: true, onClick: undefined });
54
55
  }
55
56
  /** Mantine Select (combobox) for the toolbar's dropdown controls. The icon
56
57
  * labels the control itself, since a dropdown can't show one per option in
57
58
  * its closed state. The dropdown portals to <body>, so it is not clipped by
58
59
  * the editor's `overflow: hidden` frame. */
59
- function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange }) {
60
- return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled ? `${label} (coming soon)` : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? 'No match' : undefined, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
60
+ function ToolbarSelect({ icon: Icon, label, value, data, width, disabled, placeholder, searchable, renderOption, onChange, comingSoon, noMatchMessage }) {
61
+ return (_jsx(Select, { size: "xs", w: width, "aria-label": label, title: disabled && comingSoon ? comingSoon(label) : label, data: data, value: value, placeholder: placeholder, disabled: disabled, searchable: searchable, renderOption: renderOption, nothingFoundMessage: searchable ? noMatchMessage : undefined, allowDeselect: false, leftSection: _jsx(Icon, { size: 15, stroke: ICON_STROKE }), comboboxProps: { withinPortal: true, position: 'bottom-start', middlewares: { flip: true, shift: true } }, classNames: { input: 'likhari-toolbar-select-input', dropdown: 'likhari-toolbar-select-dropdown' }, onChange: (v) => v && onChange?.(v) }));
61
62
  }
62
63
  function OverflowItem({ icon: Icon, label, active, onClick, }) {
63
64
  return (_jsx(Menu.Item, { leftSection: _jsx(Icon, { size: ICON_SIZE, stroke: ICON_STROKE }), "data-active": active ? 'true' : 'false', className: "likhari-overflow-item",
@@ -66,16 +67,17 @@ function OverflowItem({ icon: Icon, label, active, onClick, }) {
66
67
  }
67
68
  /** The link menu's contents — shared by the toolbar button and the right-click
68
69
  * menu: the URL (opens in a new tab), Edit link, Remove link. */
69
- function LinkMenuItems({ url, onEdit, onRemove }) {
70
+ function LinkMenuItems({ url, onEdit, onRemove, strings }) {
70
71
  // Only offer the link as clickable if it's a URL the editor would have accepted.
71
72
  const safeUrl = normalizeLinkUrl(url);
72
- return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: "Link" }), safeUrl ? (_jsx(Menu.Item, { component: "a", href: safeUrl, target: "_blank", rel: "noopener noreferrer", leftSection: _jsx(IconExternalLink, { size: ICON_SIZE, stroke: ICON_STROKE }), title: url, className: "likhari-link-menu-url", children: url })) : (
73
+ return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { children: strings.link.menuLabel }), safeUrl ? (_jsx(Menu.Item, { component: "a", href: safeUrl, target: "_blank", rel: "noopener noreferrer", leftSection: _jsx(IconExternalLink, { size: ICON_SIZE, stroke: ICON_STROKE }), title: url, className: "likhari-link-menu-url", children: url })) : (
73
74
  // Not a URL the editor would accept (e.g. loaded from a document): show it, don't make it clickable.
74
- _jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url || '(no URL)' })), _jsx(Menu.Item, { leftSection: _jsx(IconPencil, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: "Edit link" }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconLinkOff, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onRemove, children: "Remove link" })] }));
75
+ _jsx(Menu.Item, { disabled: true, title: url, className: "likhari-link-menu-url", children: url || strings.link.noUrl })), _jsx(Menu.Item, { leftSection: _jsx(IconPencil, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onEdit, children: strings.link.editLink }), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconLinkOff, { size: ICON_SIZE, stroke: ICON_STROKE }), onMouseDown: (e) => e.preventDefault(), onClick: onRemove, children: strings.link.removeLink })] }));
75
76
  }
76
- export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr' }) {
77
+ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAULT_FONT_OPTIONS, direction = 'ltr', locale = 'en' }) {
77
78
  const [editor] = useLexicalComposerContext();
78
79
  const [state, setState] = useState(INITIAL_STATE);
80
+ const strings = useStrings(locale);
79
81
  const updateToolbar = useCallback(() => {
80
82
  editor.getEditorState().read(() => {
81
83
  const selection = $getSelection();
@@ -397,33 +399,33 @@ export function Toolbar({ config, onSave, isDirty, showSave, fontOptions = DEFAU
397
399
  const shownFontFamily = state.fontFamily === UNSET_STYLE ? canvasDefaults.family : state.fontFamily;
398
400
  const shownFontSize = state.fontSize === UNSET_STYLE ? canvasDefaults.size : state.fontSize;
399
401
  const formattingOptions = [
400
- { value: 'paragraph', label: 'Paragraph' },
401
- ...headingLevels.map((level) => ({ value: `h${level}`, label: `Heading ${level}` })),
402
- ...(config.lists.numbered ? [{ value: 'number', label: 'Numbered list' }] : []),
403
- ...(config.lists.bullet ? [{ value: 'bullet', label: 'Bullet list' }] : []),
404
- ...(config.lists.check ? [{ value: 'check', label: 'Task list' }] : []),
405
- ...(config.blocks.quote ? [{ value: 'quote', label: 'Quote' }] : []),
402
+ { value: 'paragraph', label: strings.toolbar.formattingOptions.paragraph },
403
+ ...headingLevels.map((level) => ({ value: `h${level}`, label: strings.toolbar.formattingOptions.heading(level) })),
404
+ ...(config.lists.numbered ? [{ value: 'number', label: strings.toolbar.formattingOptions.numberedList }] : []),
405
+ ...(config.lists.bullet ? [{ value: 'bullet', label: strings.toolbar.formattingOptions.bulletList }] : []),
406
+ ...(config.lists.check ? [{ value: 'check', label: strings.toolbar.formattingOptions.taskList }] : []),
407
+ ...(config.blocks.quote ? [{ value: 'quote', label: strings.toolbar.formattingOptions.quote }] : []),
406
408
  ];
407
409
  const alignOptions = [
408
- ...(config.alignment.start ? [{ value: 'start', label: 'Align start' }] : []),
409
- ...(config.alignment.center ? [{ value: 'center', label: 'Align center' }] : []),
410
- ...(config.alignment.start ? [{ value: 'end', label: 'Align end' }] : []),
411
- ...(config.alignment.justify ? [{ value: 'justify', label: 'Justify' }] : []),
412
- ...(config.alignment.left ? [{ value: 'left', label: 'Align left' }] : []),
413
- ...(config.alignment.right ? [{ value: 'right', label: 'Align right' }] : []),
410
+ ...(config.alignment.start ? [{ value: 'start', label: strings.toolbar.alignOptions.start }] : []),
411
+ ...(config.alignment.center ? [{ value: 'center', label: strings.toolbar.alignOptions.center }] : []),
412
+ ...(config.alignment.start ? [{ value: 'end', label: strings.toolbar.alignOptions.end }] : []),
413
+ ...(config.alignment.justify ? [{ value: 'justify', label: strings.toolbar.alignOptions.justify }] : []),
414
+ ...(config.alignment.left ? [{ value: 'left', label: strings.toolbar.alignOptions.left }] : []),
415
+ ...(config.alignment.right ? [{ value: 'right', label: strings.toolbar.alignOptions.right }] : []),
414
416
  ];
415
417
  const formattingValue = state.listType ?? state.blockType;
416
418
  const AlignIcon = ALIGN_ICONS[state.elementFormat] ?? IconAlignLeft;
417
- return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": "Formatting", children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: "Save", dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: "Undo", disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: "Redo", disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: "Formatting", width: 148, value: formattingValue, data: formattingOptions, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: "Bold", active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: "Italic", active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: "Underline", active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: "Font family", width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: "Font", data: fontData, searchable: true, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: "Font size", width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: "Size", data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: "Alignment", width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
419
+ return (_jsxs("div", { className: "likhari-toolbar", role: "toolbar", "aria-label": strings.toolbar.ariaLabel, children: [showSave && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarButton, { icon: IconDeviceFloppy, title: strings.toolbar.save, dirty: Boolean(isDirty), onClick: onSave }) })), config.history && (_jsxs("div", { className: "likhari-toolbar-group", children: [_jsx(ToolbarButton, { icon: IconArrowBackUp, title: strings.toolbar.undo, disabled: !state.canUndo, onClick: () => editor.dispatchCommand(UNDO_COMMAND, undefined) }), _jsx(ToolbarButton, { icon: IconArrowForwardUp, title: strings.toolbar.redo, disabled: !state.canRedo, onClick: () => editor.dispatchCommand(REDO_COMMAND, undefined) })] })), showFormattingGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: IconPilcrow, label: strings.toolbar.formattingLabel, width: 148, value: formattingValue, data: formattingOptions, comingSoon: strings.toolbar.comingSoon, onChange: withRefocus((v) => applyFormatting(v)) }) })), showInlineGroup && (_jsxs("div", { className: "likhari-toolbar-group", children: [fmt.bold && (_jsx(ToolbarButton, { icon: IconBold, title: strings.toolbar.bold, active: state.activeFormats.has('bold'), onClick: () => formatText('bold') })), fmt.italic && (_jsx(ToolbarButton, { icon: IconItalic, title: strings.toolbar.italic, active: state.activeFormats.has('italic'), onClick: () => formatText('italic') })), fmt.underline && (_jsx(ToolbarButton, { icon: IconUnderline, title: strings.toolbar.underline, active: state.activeFormats.has('underline'), onClick: () => formatText('underline') }))] })), (config.font.family || config.font.size) && (_jsxs("div", { className: "likhari-toolbar-group", children: [config.font.family && (_jsx(ToolbarSelect, { icon: IconTypography, label: strings.toolbar.fontFamily, width: 150, value: fontOptions.some((f) => f.family === shownFontFamily) ? shownFontFamily : null, placeholder: strings.toolbar.fontFamilyPlaceholder, data: fontData, searchable: true, noMatchMessage: strings.toolbar.noMatch, renderOption: ({ option }) => _jsx("span", { style: { fontFamily: option.value }, children: option.label }), onChange: withRefocus((v) => applyFont('font-family', v)) })), config.font.size && (_jsx(ToolbarSelect, { icon: IconTextSize, label: strings.toolbar.fontSize, width: 84, value: FONT_SIZES_PX.some((px) => `${px}px` === shownFontSize) ? shownFontSize : null, placeholder: strings.toolbar.fontSizePlaceholder, data: FONT_SIZES_PX.map((px) => ({ value: `${px}px`, label: String(px) })), onChange: withRefocus((v) => applyFont('font-size', v)) }))] })), showAlignGroup && (_jsx("div", { className: "likhari-toolbar-group", children: _jsx(ToolbarSelect, { icon: AlignIcon, label: strings.toolbar.alignment, width: 138, value: state.elementFormat || 'start', data: alignOptions, onChange: withRefocus((v) => formatElement(v)) }) })), config.links && (_jsx("div", { className: "likhari-toolbar-group", children: state.isLink ? (
418
420
  // Caret is in a link: the button opens a menu to see, edit or remove it.
419
- _jsxs(Menu, { position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": "Link options", title: "Link options", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
421
+ _jsxs(Menu, { position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "data-active": "true", "aria-pressed": "true", "aria-haspopup": "menu", "aria-label": strings.toolbar.linkOptions, title: strings.toolbar.linkOptions, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconLink, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: state.linkUrl, onEdit: () => {
420
422
  restoreSelection();
421
423
  openLinkDialog();
422
- }, onRemove: removeLink }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: "Insert link (Ctrl+K)", onClick: openLinkDialog })) })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
424
+ }, onRemove: removeLink, strings: strings }) })] })) : (_jsx(ToolbarButton, { icon: IconLink, title: strings.toolbar.insertLink, onClick: openLinkDialog })) })), config.links && (_jsxs(Menu, { opened: linkContext !== null, onChange: (opened) => {
423
425
  if (!opened)
424
426
  setLinkContext(null);
425
427
  }, position: "bottom-start", withinPortal: true, shadow: "sm", width: 240, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: linkContext?.x ?? -9999, top: linkContext?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsx(Menu.Dropdown, { children: _jsx(LinkMenuItems, { url: linkContext?.url ?? '', onEdit: () => {
426
428
  restoreSelection();
427
429
  openLinkDialog();
428
- }, onRemove: removeLink }) })] })), showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: "Insert image", onClick: openImageDialog })), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: "Poetry blocks" })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: "Auto-correct" }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: "Text cleanup" }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: "Spell-checker" })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": "More formatting", title: "More formatting", onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: "Strikethrough", active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: "Superscript", active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: "Subscript", active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: "UPPERCASE", onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: "lowercase", onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: "Capitalize", onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: "Clear formatting", onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: "Outdent", onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: "Indent", onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] })), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
430
+ }, onRemove: removeLink, strings: strings }) })] })), showStubInsertGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [(config.images.linked || config.images.embedded) && (_jsx(ToolbarButton, { icon: IconPhoto, title: strings.toolbar.insertImage, onClick: openImageDialog })), config.poetry.enabled && _jsx(StubButton, { icon: IconFeather, title: strings.toolbar.poetryBlocks, comingSoon: strings.toolbar.comingSoon })] })), showLanguageGroup && (_jsxs("div", { className: "likhari-toolbar-group likhari-toolbar-group--collapse-tablet", children: [config.language.autocorrect && _jsx(StubButton, { icon: IconWand, title: strings.toolbar.autocorrect, comingSoon: strings.toolbar.comingSoon }), config.language.textCleanup && _jsx(StubButton, { icon: IconSparkles, title: strings.toolbar.textCleanup, comingSoon: strings.toolbar.comingSoon }), config.language.spellCheck && _jsx(StubButton, { icon: IconAbc, title: strings.toolbar.spellChecker, comingSoon: strings.toolbar.comingSoon })] })), showOverflowMenu && (_jsxs(Menu, { position: "bottom-end", withinPortal: true, shadow: "sm", width: 210, closeOnItemClick: true, onOpen: snapshotSelection, children: [_jsx(Menu.Target, { children: _jsx("button", { type: "button", className: "likhari-toolbar-button", "aria-label": strings.toolbar.moreFormatting, title: strings.toolbar.moreFormatting, onMouseDown: (e) => e.preventDefault(), children: _jsx(IconDots, { size: ICON_SIZE, stroke: ICON_STROKE }) }) }), _jsxs(Menu.Dropdown, { children: [fmt.strikethrough && (_jsx(OverflowItem, { icon: IconStrikethrough, label: strings.toolbar.strikethrough, active: state.activeFormats.has('strikethrough'), onClick: runOverflowAction(() => formatText('strikethrough')) })), fmt.superscript && (_jsx(OverflowItem, { icon: IconSuperscript, label: strings.toolbar.superscript, active: state.activeFormats.has('superscript'), onClick: runOverflowAction(() => formatText('superscript')) })), fmt.subscript && (_jsx(OverflowItem, { icon: IconSubscript, label: strings.toolbar.subscript, active: state.activeFormats.has('subscript'), onClick: runOverflowAction(() => formatText('subscript')) })), fmt.caseTransforms && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconLetterCaseUpper, label: strings.toolbar.uppercase, onClick: runOverflowAction(() => applyCaseTransform('upper')) }), _jsx(OverflowItem, { icon: IconLetterCaseLower, label: strings.toolbar.lowercase, onClick: runOverflowAction(() => applyCaseTransform('lower')) }), _jsx(OverflowItem, { icon: IconLetterCase, label: strings.toolbar.capitalize, onClick: runOverflowAction(() => applyCaseTransform('capitalize')) })] })), fmt.clearFormatting && _jsx(OverflowItem, { icon: IconClearFormatting, label: strings.toolbar.clearFormatting, onClick: runOverflowAction(clearFormatting) }), config.indent && (_jsxs(_Fragment, { children: [_jsx(OverflowItem, { icon: IconIndentDecrease, label: strings.toolbar.outdent, onClick: runOverflowAction(() => editor.dispatchCommand(OUTDENT_CONTENT_COMMAND, undefined)) }), _jsx(OverflowItem, { icon: IconIndentIncrease, label: strings.toolbar.indent, onClick: runOverflowAction(() => editor.dispatchCommand(INDENT_CONTENT_COMMAND, undefined)) })] }))] })] })), (config.images.linked || config.images.embedded) && (_jsx(ImageDialog, { mode: "insert", opened: imageDialogOpen, onSubmit: insertImage, onClose: closeImageDialog })), config.links && (_jsx(LinkDialog, { opened: linkDialogOpen, initialUrl: state.linkUrl, showTextField: linkNeedsText, onSubmit: applyLink, onRemove: state.isLink ? removeLink : undefined, onClose: closeLinkDialog }))] }));
429
431
  }
@@ -0,0 +1,4 @@
1
+ export type { Locale, Strings } from './strings.js';
2
+ export { STRINGS, getStrings } from './strings.js';
3
+ export { useStrings, useUiStrings, UiStringsContext } from './useStrings.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { STRINGS, getStrings } from './strings.js';
2
+ export { useStrings, useUiStrings, UiStringsContext } from './useStrings.js';
@@ -0,0 +1,158 @@
1
+ /**
2
+ * UI localization strings for the editor's own chrome (toolbar, dialogs,
3
+ * menus, placeholders) — see docs/lexical-editor-spec.md §3.2. This is
4
+ * separate from content direction/fonts (handled in EditorRoot) and from
5
+ * Markdown/HTML/plain-text format translation, which is out of scope here.
6
+ */
7
+ /** Re-exported from EditorRoot's existing prop type so there is a single
8
+ * source of truth for which locales the editor knows about. */
9
+ export type Locale = 'en' | 'ur' | 'pa-shahmukhi';
10
+ export interface Strings {
11
+ editor: {
12
+ /** Default placeholder shown in an empty editor (overridden by an explicit `placeholder` prop). */
13
+ placeholder: string;
14
+ /** aria-label on the contenteditable itself. */
15
+ contentLabel: string;
16
+ /** window.confirm() prompt shown by EditorRef.confirmDiscard() when there are unsaved changes. */
17
+ confirmDiscard: string;
18
+ };
19
+ common: {
20
+ cancel: string;
21
+ save: string;
22
+ insert: string;
23
+ };
24
+ toolbar: {
25
+ /** aria-label on the toolbar's own role="toolbar" container. */
26
+ ariaLabel: string;
27
+ save: string;
28
+ undo: string;
29
+ redo: string;
30
+ formattingLabel: string;
31
+ bold: string;
32
+ italic: string;
33
+ underline: string;
34
+ fontFamily: string;
35
+ fontFamilyPlaceholder: string;
36
+ fontSize: string;
37
+ fontSizePlaceholder: string;
38
+ noMatch: string;
39
+ alignment: string;
40
+ linkOptions: string;
41
+ insertLink: string;
42
+ insertImage: string;
43
+ poetryBlocks: string;
44
+ comingSoon: (label: string) => string;
45
+ autocorrect: string;
46
+ textCleanup: string;
47
+ spellChecker: string;
48
+ moreFormatting: string;
49
+ strikethrough: string;
50
+ superscript: string;
51
+ subscript: string;
52
+ uppercase: string;
53
+ lowercase: string;
54
+ capitalize: string;
55
+ clearFormatting: string;
56
+ outdent: string;
57
+ indent: string;
58
+ formattingOptions: {
59
+ paragraph: string;
60
+ heading: (level: number) => string;
61
+ numberedList: string;
62
+ bulletList: string;
63
+ taskList: string;
64
+ quote: string;
65
+ };
66
+ alignOptions: {
67
+ start: string;
68
+ center: string;
69
+ end: string;
70
+ justify: string;
71
+ left: string;
72
+ right: string;
73
+ };
74
+ };
75
+ link: {
76
+ menuLabel: string;
77
+ noUrl: string;
78
+ editLink: string;
79
+ removeLink: string;
80
+ };
81
+ linkDialog: {
82
+ titleInsert: string;
83
+ titleEdit: string;
84
+ urlLabel: string;
85
+ urlPlaceholder: string;
86
+ textLabel: string;
87
+ textPlaceholder: string;
88
+ invalidUrl: string;
89
+ apply: string;
90
+ };
91
+ imageDialog: {
92
+ titleInsert: string;
93
+ titleEdit: string;
94
+ currentImage: string;
95
+ fromUrl: string;
96
+ upload: string;
97
+ embeddedInDocument: string;
98
+ linkedFrom: (src: string) => string;
99
+ convertButton: string;
100
+ convertHint: string;
101
+ imageUrlLabel: string;
102
+ imageUrlPlaceholder: string;
103
+ embedCopyLabel: string;
104
+ embedCopyDescription: string;
105
+ convertedFromUrl: string;
106
+ fileLabel: string;
107
+ filePlaceholder: string;
108
+ fileDescription: (maxSizeMB: number, storedInDocument: boolean) => string;
109
+ altTextLabel: string;
110
+ altTextDescription: string;
111
+ captionLabel: string;
112
+ captionDescription: string;
113
+ errors: {
114
+ enterUrl: string;
115
+ chooseFile: string;
116
+ noImage: string;
117
+ uploadUnusable: string;
118
+ couldNotSave: string;
119
+ couldNotConvert: string;
120
+ couldNotReadFile: string;
121
+ largerThan: (maxSizeMB: number) => string;
122
+ };
123
+ };
124
+ imageCropDialog: {
125
+ title: string;
126
+ sizeLabel: string;
127
+ widthLabel: string;
128
+ heightLabel: string;
129
+ autoPlaceholder: string;
130
+ keepProportions: string;
131
+ originalSize: string;
132
+ dragHint: (natural: {
133
+ width: number;
134
+ height: number;
135
+ } | null) => string;
136
+ errors: {
137
+ uploadUnusable: string;
138
+ couldNotSave: string;
139
+ largerThan: (maxSizeMB: number) => string;
140
+ };
141
+ };
142
+ imageMenu: {
143
+ editImage: string;
144
+ convertToEmbedded: string;
145
+ cropResize: string;
146
+ addCaption: string;
147
+ editCaption: string;
148
+ removeCaption: string;
149
+ deleteImage: string;
150
+ resizeImage: string;
151
+ };
152
+ }
153
+ export declare const STRINGS: Record<Locale, Strings>;
154
+ /** Returns the strings dictionary for `locale`, defensively falling back to
155
+ * `en` for any locale not present in `STRINGS` (e.g. a future locale value
156
+ * passed by a caller on an older type definition). */
157
+ export declare function getStrings(locale: Locale): Strings;
158
+ //# sourceMappingURL=strings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strings.d.ts","sourceRoot":"","sources":["../../src/i18n/strings.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;+DAC+D;AAC/D,MAAM,MAAM,MAAM,GAAG,IAAI,GAAG,IAAI,GAAG,cAAc,CAAC;AAElD,MAAM,WAAW,OAAO;IACtB,MAAM,EAAE;QACN,mGAAmG;QACnG,WAAW,EAAE,MAAM,CAAC;QACpB,gDAAgD;QAChD,YAAY,EAAE,MAAM,CAAC;QACrB,kGAAkG;QAClG,cAAc,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,OAAO,EAAE;QACP,gEAAgE;QAChE,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,QAAQ,EAAE,MAAM,CAAC;QACjB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;QACtC,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;QACxB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,EAAE;YACjB,SAAS,EAAE,MAAM,CAAC;YAClB,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;YACnC,YAAY,EAAE,MAAM,CAAC;YACrB,UAAU,EAAE,MAAM,CAAC;YACnB,QAAQ,EAAE,MAAM,CAAC;YACjB,KAAK,EAAE,MAAM,CAAC;SACf,CAAC;QACF,YAAY,EAAE;YACZ,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,GAAG,EAAE,MAAM,CAAC;YACZ,OAAO,EAAE,MAAM,CAAC;YAChB,IAAI,EAAE,MAAM,CAAC;YACb,KAAK,EAAE,MAAM,CAAC;SACf,CAAC;KACH,CAAC;IACF,IAAI,EAAE;QACJ,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,UAAU,EAAE;QACV,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,cAAc,EAAE,MAAM,CAAC;QACvB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;KACf,CAAC;IACF,WAAW,EAAE;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,kBAAkB,EAAE,MAAM,CAAC;QAC3B,UAAU,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;QACpC,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,cAAc,EAAE,MAAM,CAAC;QACvB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,gBAAgB,EAAE,OAAO,KAAK,MAAM,CAAC;QAC1E,YAAY,EAAE,MAAM,CAAC;QACrB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,YAAY,EAAE,MAAM,CAAC;QACrB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,MAAM,EAAE;YACN,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,OAAO,EAAE,MAAM,CAAC;YAChB,cAAc,EAAE,MAAM,CAAC;YACvB,YAAY,EAAE,MAAM,CAAC;YACrB,eAAe,EAAE,MAAM,CAAC;YACxB,gBAAgB,EAAE,MAAM,CAAC;YACzB,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,eAAe,EAAE;QACf,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,YAAY,EAAE,MAAM,CAAC;QACrB,QAAQ,EAAE,CAAC,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,KAAK,MAAM,CAAC;QACxE,MAAM,EAAE;YACN,cAAc,EAAE,MAAM,CAAC;YACvB,YAAY,EAAE,MAAM,CAAC;YACrB,UAAU,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;SAC3C,CAAC;KACH,CAAC;IACF,SAAS,EAAE;QACT,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,aAAa,EAAE,MAAM,CAAC;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;CACH;AAgaD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAI3C,CAAC;AAEF;;sDAEsD;AACtD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD"}
@@ -0,0 +1,425 @@
1
+ /**
2
+ * UI localization strings for the editor's own chrome (toolbar, dialogs,
3
+ * menus, placeholders) — see docs/lexical-editor-spec.md §3.2. This is
4
+ * separate from content direction/fonts (handled in EditorRoot) and from
5
+ * Markdown/HTML/plain-text format translation, which is out of scope here.
6
+ */
7
+ const en = {
8
+ editor: {
9
+ placeholder: 'Start writing…',
10
+ contentLabel: 'Editor content',
11
+ confirmDiscard: 'You have unsaved changes. Discard them?',
12
+ },
13
+ common: {
14
+ cancel: 'Cancel',
15
+ save: 'Save',
16
+ insert: 'Insert',
17
+ },
18
+ toolbar: {
19
+ ariaLabel: 'Formatting',
20
+ save: 'Save',
21
+ undo: 'Undo',
22
+ redo: 'Redo',
23
+ formattingLabel: 'Formatting',
24
+ bold: 'Bold',
25
+ italic: 'Italic',
26
+ underline: 'Underline',
27
+ fontFamily: 'Font family',
28
+ fontFamilyPlaceholder: 'Font',
29
+ fontSize: 'Font size',
30
+ fontSizePlaceholder: 'Size',
31
+ noMatch: 'No match',
32
+ alignment: 'Alignment',
33
+ linkOptions: 'Link options',
34
+ insertLink: 'Insert link (Ctrl+K)',
35
+ insertImage: 'Insert image',
36
+ poetryBlocks: 'Poetry blocks',
37
+ comingSoon: (label) => `${label} (coming soon)`,
38
+ autocorrect: 'Auto-correct',
39
+ textCleanup: 'Text cleanup',
40
+ spellChecker: 'Spell-checker',
41
+ moreFormatting: 'More formatting',
42
+ strikethrough: 'Strikethrough',
43
+ superscript: 'Superscript',
44
+ subscript: 'Subscript',
45
+ uppercase: 'UPPERCASE',
46
+ lowercase: 'lowercase',
47
+ capitalize: 'Capitalize',
48
+ clearFormatting: 'Clear formatting',
49
+ outdent: 'Outdent',
50
+ indent: 'Indent',
51
+ formattingOptions: {
52
+ paragraph: 'Paragraph',
53
+ heading: (level) => `Heading ${level}`,
54
+ numberedList: 'Numbered list',
55
+ bulletList: 'Bullet list',
56
+ taskList: 'Task list',
57
+ quote: 'Quote',
58
+ },
59
+ alignOptions: {
60
+ start: 'Align start',
61
+ center: 'Align center',
62
+ end: 'Align end',
63
+ justify: 'Justify',
64
+ left: 'Align left',
65
+ right: 'Align right',
66
+ },
67
+ },
68
+ link: {
69
+ menuLabel: 'Link',
70
+ noUrl: '(no URL)',
71
+ editLink: 'Edit link',
72
+ removeLink: 'Remove link',
73
+ },
74
+ linkDialog: {
75
+ titleInsert: 'Insert link',
76
+ titleEdit: 'Edit link',
77
+ urlLabel: 'URL',
78
+ urlPlaceholder: 'https://example.com',
79
+ textLabel: 'Text',
80
+ textPlaceholder: 'Link text (defaults to the URL)',
81
+ invalidUrl: 'Enter a valid http(s), mailto, tel or relative URL',
82
+ apply: 'Apply',
83
+ },
84
+ imageDialog: {
85
+ titleInsert: 'Insert image',
86
+ titleEdit: 'Edit image',
87
+ currentImage: 'Current image',
88
+ fromUrl: 'From URL',
89
+ upload: 'Upload',
90
+ embeddedInDocument: 'Embedded in the document',
91
+ linkedFrom: (src) => `Linked from ${src}`,
92
+ convertButton: 'Convert to embedded image',
93
+ convertHint: 'Downloads a copy into the document, so it can be cropped, rotated and resized.',
94
+ imageUrlLabel: 'Image URL',
95
+ imageUrlPlaceholder: 'https://example.com/photo.jpg',
96
+ embedCopyLabel: 'Embed a copy in the document',
97
+ embedCopyDescription: 'Downloads the image so it can be edited and no longer depends on the URL.',
98
+ convertedFromUrl: 'Converted from the URL: a copy will be embedded in the document.',
99
+ fileLabel: 'Image file',
100
+ filePlaceholder: 'Choose an image',
101
+ fileDescription: (maxSizeMB, storedInDocument) => `Up to ${maxSizeMB} MB${storedInDocument ? '; stored inside the document' : ''}`,
102
+ altTextLabel: 'Alt text',
103
+ altTextDescription: 'Describes the image for screen readers',
104
+ captionLabel: 'Caption',
105
+ captionDescription: 'Shown under the image. Leave empty for none.',
106
+ errors: {
107
+ enterUrl: 'Enter an http(s) or relative image URL',
108
+ chooseFile: 'Choose an image file',
109
+ noImage: 'No image',
110
+ uploadUnusable: 'The upload handler returned an unusable image URL',
111
+ couldNotSave: 'Could not save the image',
112
+ couldNotConvert: 'Could not convert the image',
113
+ couldNotReadFile: 'Could not read the file',
114
+ largerThan: (maxSizeMB) => `Image is larger than ${maxSizeMB} MB`,
115
+ },
116
+ },
117
+ imageCropDialog: {
118
+ title: 'Crop & resize',
119
+ sizeLabel: 'Size',
120
+ widthLabel: 'Width (px)',
121
+ heightLabel: 'Height (px)',
122
+ autoPlaceholder: 'Auto',
123
+ keepProportions: 'Keep proportions',
124
+ originalSize: 'Original size',
125
+ dragHint: (natural) => `You can also drag the corner handle on a selected image. ${natural ? `Original: ${natural.width} × ${natural.height} px.` : ''}`,
126
+ errors: {
127
+ uploadUnusable: 'The upload handler returned an unusable image URL',
128
+ couldNotSave: 'Could not save the image',
129
+ largerThan: (maxSizeMB) => `Image is larger than ${maxSizeMB} MB`,
130
+ },
131
+ },
132
+ imageMenu: {
133
+ editImage: 'Edit image…',
134
+ convertToEmbedded: 'Convert to embedded image',
135
+ cropResize: 'Crop & resize…',
136
+ addCaption: 'Add caption…',
137
+ editCaption: 'Edit caption…',
138
+ removeCaption: 'Remove caption',
139
+ deleteImage: 'Delete image',
140
+ resizeImage: 'Resize image',
141
+ },
142
+ };
143
+ const ur = {
144
+ editor: {
145
+ placeholder: 'لکھنا شروع کریں…',
146
+ contentLabel: 'ایڈیٹر کا مواد',
147
+ confirmDiscard: 'آپ کی غیر محفوظ شدہ تبدیلیاں ہیں۔ کیا انہیں رد کر دیا جائے؟',
148
+ },
149
+ common: {
150
+ cancel: 'منسوخ کریں',
151
+ save: 'محفوظ کریں',
152
+ insert: 'شامل کریں',
153
+ },
154
+ toolbar: {
155
+ ariaLabel: 'فارمیٹنگ',
156
+ save: 'محفوظ کریں',
157
+ undo: 'کالعدم کریں',
158
+ redo: 'دوبارہ کریں',
159
+ formattingLabel: 'فارمیٹنگ',
160
+ bold: 'موٹا',
161
+ italic: 'ترچھا',
162
+ underline: 'زیرِ خط',
163
+ fontFamily: 'فونٹ کا خاندان',
164
+ fontFamilyPlaceholder: 'فونٹ',
165
+ fontSize: 'فونٹ کا سائز',
166
+ fontSizePlaceholder: 'سائز',
167
+ noMatch: 'کوئی مماثلت نہیں',
168
+ alignment: 'سیدھ',
169
+ linkOptions: 'لنک کے اختیارات',
170
+ insertLink: 'لنک شامل کریں (Ctrl+K)',
171
+ insertImage: 'تصویر شامل کریں',
172
+ poetryBlocks: 'شاعری کے بلاکس',
173
+ comingSoon: (label) => `${label} (جلد آ رہا ہے)`,
174
+ autocorrect: 'خودکار تصحیح',
175
+ textCleanup: 'متن کی صفائی',
176
+ spellChecker: 'ہجے چیک کرنے والا',
177
+ moreFormatting: 'مزید فارمیٹنگ',
178
+ strikethrough: 'خط زدہ',
179
+ superscript: 'بالا نویس',
180
+ subscript: 'زیریں نویس',
181
+ uppercase: 'بڑے حروف',
182
+ lowercase: 'چھوٹے حروف',
183
+ capitalize: 'پہلا حرف بڑا',
184
+ clearFormatting: 'فارمیٹنگ ختم کریں',
185
+ outdent: 'حاشیہ کم کریں',
186
+ indent: 'حاشیہ بڑھائیں',
187
+ formattingOptions: {
188
+ paragraph: 'پیراگراف',
189
+ heading: (level) => `عنوان ${level}`,
190
+ numberedList: 'نمبردار فہرست',
191
+ bulletList: 'بلٹ فہرست',
192
+ taskList: 'ٹاسک فہرست',
193
+ quote: 'اقتباس',
194
+ },
195
+ alignOptions: {
196
+ start: 'ابتدائی سیدھ',
197
+ center: 'درمیانی سیدھ',
198
+ end: 'اختتامی سیدھ',
199
+ justify: 'دو طرفہ سیدھ',
200
+ left: 'بائیں سیدھ',
201
+ right: 'دائیں سیدھ',
202
+ },
203
+ },
204
+ link: {
205
+ menuLabel: 'لنک',
206
+ noUrl: '(کوئی یو آر ایل نہیں)',
207
+ editLink: 'لنک میں ترمیم کریں',
208
+ removeLink: 'لنک ہٹائیں',
209
+ },
210
+ linkDialog: {
211
+ titleInsert: 'لنک شامل کریں',
212
+ titleEdit: 'لنک میں ترمیم کریں',
213
+ urlLabel: 'یو آر ایل',
214
+ urlPlaceholder: 'https://example.com',
215
+ textLabel: 'عبارت',
216
+ textPlaceholder: 'لنک کی عبارت (نہ دینے پر یو آر ایل ہی استعمال ہوگا)',
217
+ invalidUrl: 'براہِ کرم ایک درست http(s)، mailto، tel یا رشتہ دار یو آر ایل درج کریں',
218
+ apply: 'لاگو کریں',
219
+ },
220
+ imageDialog: {
221
+ titleInsert: 'تصویر شامل کریں',
222
+ titleEdit: 'تصویر میں ترمیم کریں',
223
+ currentImage: 'موجودہ تصویر',
224
+ fromUrl: 'یو آر ایل سے',
225
+ upload: 'اپ لوڈ',
226
+ embeddedInDocument: 'دستاویز میں شامل',
227
+ linkedFrom: (src) => `${src} سے منسلک`,
228
+ convertButton: 'شامل شدہ تصویر میں تبدیل کریں',
229
+ convertHint: 'دستاویز میں ایک نقل ڈاؤن لوڈ کرتا ہے، تاکہ اسے کاٹا، گھمایا اور سائز تبدیل کیا جا سکے۔',
230
+ imageUrlLabel: 'تصویر کا یو آر ایل',
231
+ imageUrlPlaceholder: 'https://example.com/photo.jpg',
232
+ embedCopyLabel: 'دستاویز میں ایک نقل شامل کریں',
233
+ embedCopyDescription: 'تصویر ڈاؤن لوڈ کرتا ہے تاکہ اس میں ترمیم کی جا سکے اور یہ یو آر ایل پر منحصر نہ رہے۔',
234
+ convertedFromUrl: 'یو آر ایل سے تبدیل شدہ: ایک نقل دستاویز میں شامل کی جائے گی۔',
235
+ fileLabel: 'تصویر کی فائل',
236
+ filePlaceholder: 'ایک تصویر منتخب کریں',
237
+ fileDescription: (maxSizeMB, storedInDocument) => `${maxSizeMB} ایم بی تک${storedInDocument ? '؛ دستاویز کے اندر محفوظ ہوتی ہے' : ''}`,
238
+ altTextLabel: 'متبادل متن',
239
+ altTextDescription: 'سکرین ریڈرز کے لیے تصویر کی تفصیل بیان کرتا ہے',
240
+ captionLabel: 'کیپشن',
241
+ captionDescription: 'تصویر کے نیچے دکھایا جاتا ہے۔ نہ چاہیں تو خالی چھوڑ دیں۔',
242
+ errors: {
243
+ enterUrl: 'ایک http(s) یا رشتہ دار تصویر یو آر ایل درج کریں',
244
+ chooseFile: 'ایک تصویر فائل منتخب کریں',
245
+ noImage: 'کوئی تصویر نہیں',
246
+ uploadUnusable: 'اپ لوڈ ہینڈلر نے ناقابلِ استعمال یو آر ایل واپس کیا',
247
+ couldNotSave: 'تصویر محفوظ نہیں ہو سکی',
248
+ couldNotConvert: 'تصویر تبدیل نہیں ہو سکی',
249
+ couldNotReadFile: 'فائل پڑھی نہیں جا سکی',
250
+ largerThan: (maxSizeMB) => `تصویر ${maxSizeMB} ایم بی سے بڑی ہے`,
251
+ },
252
+ },
253
+ imageCropDialog: {
254
+ title: 'کاٹیں اور سائز تبدیل کریں',
255
+ sizeLabel: 'سائز',
256
+ widthLabel: 'چوڑائی (px)',
257
+ heightLabel: 'اونچائی (px)',
258
+ autoPlaceholder: 'خودکار',
259
+ keepProportions: 'تناسب برقرار رکھیں',
260
+ originalSize: 'اصل سائز',
261
+ dragHint: (natural) => `منتخب شدہ تصویر پر کارنر ہینڈل کھینچ کر بھی سائز تبدیل کیا جا سکتا ہے۔ ${natural ? `اصل: ${natural.width} × ${natural.height} پکسل۔` : ''}`,
262
+ errors: {
263
+ uploadUnusable: 'اپ لوڈ ہینڈلر نے ناقابلِ استعمال یو آر ایل واپس کیا',
264
+ couldNotSave: 'تصویر محفوظ نہیں ہو سکی',
265
+ largerThan: (maxSizeMB) => `تصویر ${maxSizeMB} ایم بی سے بڑی ہے`,
266
+ },
267
+ },
268
+ imageMenu: {
269
+ editImage: 'تصویر میں ترمیم کریں…',
270
+ convertToEmbedded: 'شامل شدہ تصویر میں تبدیل کریں',
271
+ cropResize: 'کاٹیں اور سائز تبدیل کریں…',
272
+ addCaption: 'کیپشن شامل کریں…',
273
+ editCaption: 'کیپشن میں ترمیم کریں…',
274
+ removeCaption: 'کیپشن ہٹائیں',
275
+ deleteImage: 'تصویر حذف کریں',
276
+ resizeImage: 'تصویر کا سائز تبدیل کریں',
277
+ },
278
+ };
279
+ const paShahmukhi = {
280
+ editor: {
281
+ placeholder: 'لکھنا شروع کرو…',
282
+ contentLabel: 'ایڈیٹر دی سامگری',
283
+ confirmDiscard: 'تہاڈیاں کجھ تبدیلیاں سنبھالیاں نئیں گئیاں۔ کی ایہناں نوں رد کر دیئے؟',
284
+ },
285
+ common: {
286
+ cancel: 'رد کرو',
287
+ save: 'سنبھالو',
288
+ insert: 'پاؤ',
289
+ },
290
+ toolbar: {
291
+ ariaLabel: 'فارمیٹنگ',
292
+ save: 'سنبھالو',
293
+ undo: 'پہلاں جیہا کرو',
294
+ redo: 'مُڑ کرو',
295
+ formattingLabel: 'فارمیٹنگ',
296
+ bold: 'گہرا',
297
+ italic: 'ترچھا',
298
+ underline: 'تھلے لکیر',
299
+ fontFamily: 'فونٹ خاندان',
300
+ fontFamilyPlaceholder: 'فونٹ',
301
+ fontSize: 'فونٹ دا سائز',
302
+ fontSizePlaceholder: 'سائز',
303
+ noMatch: 'کوئی نئیں ملیا',
304
+ alignment: 'سیدھ',
305
+ linkOptions: 'لنک دے اختیار',
306
+ insertLink: 'لنک پاؤ (Ctrl+K)',
307
+ insertImage: 'تصویر پاؤ',
308
+ poetryBlocks: 'شاعری بلاک',
309
+ comingSoon: (label) => `${label} (چھیتی آ رہا اے)`,
310
+ autocorrect: 'خودکار درستی',
311
+ textCleanup: 'متن دی صفائی',
312
+ spellChecker: 'ہجے چیکر',
313
+ moreFormatting: 'ہور فارمیٹنگ',
314
+ strikethrough: 'کٹی لکیر',
315
+ superscript: 'اُتلا لکھت',
316
+ subscript: 'تلا لکھت',
317
+ uppercase: 'وڈے حرف',
318
+ lowercase: 'نکے حرف',
319
+ capitalize: 'پہلا حرف وڈا',
320
+ clearFormatting: 'فارمیٹنگ ہٹاؤ',
321
+ outdent: 'حاشیہ گھٹاؤ',
322
+ indent: 'حاشیہ ودھاؤ',
323
+ formattingOptions: {
324
+ paragraph: 'پیراگراف',
325
+ heading: (level) => `سرلیکھ ${level}`,
326
+ numberedList: 'نمبردار لسٹ',
327
+ bulletList: 'بلٹ لسٹ',
328
+ taskList: 'ٹاسک لسٹ',
329
+ quote: 'حوالہ',
330
+ },
331
+ alignOptions: {
332
+ start: 'شروع دی سیدھ',
333
+ center: 'وچکار دی سیدھ',
334
+ end: 'آخری سیدھ',
335
+ justify: 'دوویں پاسے سیدھ',
336
+ left: 'بائیں سیدھ',
337
+ right: 'سجے سیدھ',
338
+ },
339
+ },
340
+ link: {
341
+ menuLabel: 'لنک',
342
+ noUrl: '(کوئی یو آر ایل نئیں)',
343
+ editLink: 'لنک وچ تبدیلی کرو',
344
+ removeLink: 'لنک ہٹاؤ',
345
+ },
346
+ linkDialog: {
347
+ titleInsert: 'لنک پاؤ',
348
+ titleEdit: 'لنک وچ تبدیلی کرو',
349
+ urlLabel: 'یو آر ایل',
350
+ urlPlaceholder: 'https://example.com',
351
+ textLabel: 'لکھت',
352
+ textPlaceholder: 'لنک دی لکھت (نہ دین تے یو آر ایل ای ورتیا جاوے گا)',
353
+ invalidUrl: 'کِرپا کر کے صحیح http(s)، mailto، tel یا نسبتی یو آر ایل پاؤ',
354
+ apply: 'لاگو کرو',
355
+ },
356
+ imageDialog: {
357
+ titleInsert: 'تصویر پاؤ',
358
+ titleEdit: 'تصویر وچ تبدیلی کرو',
359
+ currentImage: 'موجودہ تصویر',
360
+ fromUrl: 'یو آر ایل توں',
361
+ upload: 'اپ لوڈ',
362
+ embeddedInDocument: 'دستاویز وچ شامل',
363
+ linkedFrom: (src) => `${src} توں لنک کیتا گیا`,
364
+ convertButton: 'شامل تصویر وچ بدلو',
365
+ convertHint: 'دستاویز وچ اک کاپی ڈاؤن لوڈ کردا اے، تاں جو ایہنوں کٹیا، گھمایا تے سائز بدلیا جا سکے۔',
366
+ imageUrlLabel: 'تصویر دا یو آر ایل',
367
+ imageUrlPlaceholder: 'https://example.com/photo.jpg',
368
+ embedCopyLabel: 'دستاویز وچ اک کاپی شامل کرو',
369
+ embedCopyDescription: 'تصویر ڈاؤن لوڈ کردا اے تاں جو ایہنوں تبدیل کیتا جا سکے تے ایہہ یو آر ایل تے منحصر نہ رہے۔',
370
+ convertedFromUrl: 'یو آر ایل توں بدلیا گیا: اک کاپی دستاویز وچ شامل کیتی جاوے گی۔',
371
+ fileLabel: 'تصویر دی فائل',
372
+ filePlaceholder: 'اک تصویر چُنو',
373
+ fileDescription: (maxSizeMB, storedInDocument) => `${maxSizeMB} ایم بی تیک${storedInDocument ? '؛ دستاویز دے اندر سنبھالی جاندی اے' : ''}`,
374
+ altTextLabel: 'بدل متن',
375
+ altTextDescription: 'سکرین ریڈرز لئی تصویر دی وضاحت کردا اے',
376
+ captionLabel: 'کیپشن',
377
+ captionDescription: 'تصویر دے تھلے دکھایا جاندا اے۔ نہ چاہیدا تے خالی چھڈ دیو۔',
378
+ errors: {
379
+ enterUrl: 'اک http(s) یا نسبتی تصویر یو آر ایل پاؤ',
380
+ chooseFile: 'اک تصویر فائل چُنو',
381
+ noImage: 'کوئی تصویر نئیں',
382
+ uploadUnusable: 'اپ لوڈ ہینڈلر نے ناکارآمد یو آر ایل موڑیا',
383
+ couldNotSave: 'تصویر سنبھالی نئیں جا سکی',
384
+ couldNotConvert: 'تصویر بدلی نئیں جا سکی',
385
+ couldNotReadFile: 'فائل پڑھی نئیں جا سکی',
386
+ largerThan: (maxSizeMB) => `تصویر ${maxSizeMB} ایم بی توں وڈی اے`,
387
+ },
388
+ },
389
+ imageCropDialog: {
390
+ title: 'کٹو تے سائز بدلو',
391
+ sizeLabel: 'سائز',
392
+ widthLabel: 'چوڑائی (px)',
393
+ heightLabel: 'اُچائی (px)',
394
+ autoPlaceholder: 'خودکار',
395
+ keepProportions: 'تناسب رکھو',
396
+ originalSize: 'اصلی سائز',
397
+ dragHint: (natural) => `چُنی ہوئی تصویر تے کارنر ہینڈل گھسیٹ کے وی سائز بدلیا جا سکدا اے۔ ${natural ? `اصلی: ${natural.width} × ${natural.height} پکسل۔` : ''}`,
398
+ errors: {
399
+ uploadUnusable: 'اپ لوڈ ہینڈلر نے ناکارآمد یو آر ایل موڑیا',
400
+ couldNotSave: 'تصویر سنبھالی نئیں جا سکی',
401
+ largerThan: (maxSizeMB) => `تصویر ${maxSizeMB} ایم بی توں وڈی اے`,
402
+ },
403
+ },
404
+ imageMenu: {
405
+ editImage: 'تصویر وچ تبدیلی کرو…',
406
+ convertToEmbedded: 'شامل تصویر وچ بدلو',
407
+ cropResize: 'کٹو تے سائز بدلو…',
408
+ addCaption: 'کیپشن شامل کرو…',
409
+ editCaption: 'کیپشن وچ تبدیلی کرو…',
410
+ removeCaption: 'کیپشن ہٹاؤ',
411
+ deleteImage: 'تصویر مٹاؤ',
412
+ resizeImage: 'تصویر دا سائز بدلو',
413
+ },
414
+ };
415
+ export const STRINGS = {
416
+ en,
417
+ ur,
418
+ 'pa-shahmukhi': paShahmukhi,
419
+ };
420
+ /** Returns the strings dictionary for `locale`, defensively falling back to
421
+ * `en` for any locale not present in `STRINGS` (e.g. a future locale value
422
+ * passed by a caller on an older type definition). */
423
+ export function getStrings(locale) {
424
+ return STRINGS[locale] ?? STRINGS.en;
425
+ }
@@ -0,0 +1,10 @@
1
+ import { type Locale, type Strings } from './strings.js';
2
+ /** Returns the translated strings dictionary for `locale`. */
3
+ export declare function useStrings(locale: Locale): Strings;
4
+ /** Provided once at `EditorRoot`'s top level so components that aren't
5
+ * direct children (dialogs, context menus) can reach the current locale's
6
+ * strings without threading a `locale` prop through every intermediate
7
+ * component. */
8
+ export declare const UiStringsContext: import("react").Context<Strings>;
9
+ export declare function useUiStrings(): Strings;
10
+ //# sourceMappingURL=useStrings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useStrings.d.ts","sourceRoot":"","sources":["../../src/i18n/useStrings.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,WAAW,CAAC;AAElE,8DAA8D;AAC9D,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;gBAGgB;AAChB,eAAO,MAAM,gBAAgB,kCAA2C,CAAC;AAEzE,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
@@ -0,0 +1,14 @@
1
+ import { createContext, useContext, useMemo } from 'react';
2
+ import { getStrings } from './strings.js';
3
+ /** Returns the translated strings dictionary for `locale`. */
4
+ export function useStrings(locale) {
5
+ return useMemo(() => getStrings(locale), [locale]);
6
+ }
7
+ /** Provided once at `EditorRoot`'s top level so components that aren't
8
+ * direct children (dialogs, context menus) can reach the current locale's
9
+ * strings without threading a `locale` prop through every intermediate
10
+ * component. */
11
+ export const UiStringsContext = createContext(getStrings('en'));
12
+ export function useUiStrings() {
13
+ return useContext(UiStringsContext);
14
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ImageComponent.d.ts","sourceRoot":"","sources":["../../src/image/ImageComponent.tsx"],"names":[],"mappings":"AAKA,OAAO,EASL,KAAK,OAAO,EACb,MAAM,SAAS,CAAC;AAKjB,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,aAAa,CAAC;AAU5D,UAAU,mBAAmB;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB,yFAAyF;IACzF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,+BAyP9G"}
1
+ {"version":3,"file":"ImageComponent.d.ts","sourceRoot":"","sources":["../../src/image/ImageComponent.tsx"],"names":[],"mappings":"AAKA,OAAO,EASL,KAAK,OAAO,EACb,MAAM,SAAS,CAAC;AAMjB,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,aAAa,CAAC;AAU5D,UAAU,mBAAmB;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB,yFAAyF;IACzF,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,+BA0P9G"}
@@ -8,6 +8,7 @@ import { $getNodeByKey, $getSelection, $isNodeSelection, CLICK_COMMAND, COMMAND_
8
8
  import { ImageCropDialog } from './ImageCropDialog.js';
9
9
  import { ImageDialog } from './ImageDialog.js';
10
10
  import { useImageOptions } from './ImageOptionsContext.js';
11
+ import { useUiStrings } from '../i18n/useStrings.js';
11
12
  import { dataUrlBytes, fetchImageAsDataUrl } from './imageEdit.js';
12
13
  // A type-only import of ImageNode (ImageNode itself imports this file), so the
13
14
  // node is recognised by its type string rather than `instanceof`.
@@ -26,6 +27,7 @@ const MIN_WIDTH = 32;
26
27
  export function ImageComponent({ nodeKey, src, altText, caption, linkType, width, height }) {
27
28
  const [editor] = useLexicalComposerContext();
28
29
  const options = useImageOptions();
30
+ const strings = useUiStrings();
29
31
  const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
30
32
  const imageRef = useRef(null);
31
33
  const figureRef = useRef(null);
@@ -82,7 +84,7 @@ export function ImageComponent({ nodeKey, src, altText, caption, linkType, width
82
84
  try {
83
85
  const dataUrl = await fetchImageAsDataUrl(src, options.fetchImage);
84
86
  if (dataUrlBytes(dataUrl) > options.maxSizeMB * 1024 * 1024) {
85
- throw new Error(`Image is larger than ${options.maxSizeMB} MB`);
87
+ throw new Error(strings.imageDialog.errors.largerThan(options.maxSizeMB));
86
88
  }
87
89
  updateNode((node) => node.setSource(dataUrl, 'embedded'));
88
90
  }
@@ -154,10 +156,10 @@ export function ImageComponent({ nodeKey, src, altText, caption, linkType, width
154
156
  clearSelection();
155
157
  setSelected(true);
156
158
  setMenu({ x: e.clientX, y: e.clientY });
157
- }, children: [_jsxs("span", { className: "likhari-image-wrap", children: [_jsx("img", { ref: imageRef, className: "likhari-image", src: src, alt: altText, width: shownWidth, draggable: false }), isSelected && linkType === 'embedded' && (_jsx("span", { className: "likhari-image-resize-handle", role: "separator", "aria-label": "Resize image", onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }))] }), caption && _jsx("figcaption", { className: "likhari-image-caption", children: caption }), _jsxs(Menu, { opened: menu !== null, onChange: (opened) => {
159
+ }, children: [_jsxs("span", { className: "likhari-image-wrap", children: [_jsx("img", { ref: imageRef, className: "likhari-image", src: src, alt: altText, width: shownWidth, draggable: false }), isSelected && linkType === 'embedded' && (_jsx("span", { className: "likhari-image-resize-handle", role: "separator", "aria-label": strings.imageMenu.resizeImage, onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp, onPointerCancel: onResizeUp }))] }), caption && _jsx("figcaption", { className: "likhari-image-caption", children: caption }), _jsxs(Menu, { opened: menu !== null, onChange: (opened) => {
158
160
  if (!opened)
159
161
  setMenu(null);
160
- }, position: "bottom-start", withinPortal: true, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: menu?.x ?? -9999, top: menu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconPhotoEdit, { size: 16, stroke: 1.75 }), onClick: () => setDialog({}), children: "Edit image\u2026" }), linkType === 'linked' && options.allowEmbedded && (_jsx(Menu.Item, { leftSection: _jsx(IconPhotoDown, { size: 16, stroke: 1.75 }), onClick: () => void convertToEmbedded(), children: "Convert to embedded image" })), linkType === 'embedded' && (_jsx(Menu.Item, { leftSection: _jsx(IconCrop, { size: 16, stroke: 1.75 }), onClick: () => setCropDialogOpen(true), children: "Crop & resize\u2026" })), options.allowCaption && (_jsx(Menu.Item, { leftSection: _jsx(IconTextCaption, { size: 16, stroke: 1.75 }), onClick: () => setDialog({ intent: 'caption' }), children: caption ? 'Edit caption…' : 'Add caption…' })), options.allowCaption && caption && (_jsx(Menu.Item, { leftSection: _jsx(IconX, { size: 16, stroke: 1.75 }), onClick: () => updateNode((node) => node.setCaption(null)), children: "Remove caption" })), _jsx(Menu.Divider, {}), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconTrash, { size: 16, stroke: 1.75 }), onClick: removeImage, children: "Delete image" })] })] }), _jsx(ImageDialog, { mode: "edit", opened: dialog !== null, initial: { src, altText, caption, linkType }, intent: dialog?.intent, onSubmit: applyEdit, onClose: () => {
162
+ }, position: "bottom-start", withinPortal: true, shadow: "sm", width: 200, children: [_jsx(Menu.Target, { children: _jsx("span", { "aria-hidden": "true", style: { position: 'fixed', left: menu?.x ?? -9999, top: menu?.y ?? -9999, width: 1, height: 1, pointerEvents: 'none' } }) }), _jsxs(Menu.Dropdown, { children: [_jsx(Menu.Item, { leftSection: _jsx(IconPhotoEdit, { size: 16, stroke: 1.75 }), onClick: () => setDialog({}), children: strings.imageMenu.editImage }), linkType === 'linked' && options.allowEmbedded && (_jsx(Menu.Item, { leftSection: _jsx(IconPhotoDown, { size: 16, stroke: 1.75 }), onClick: () => void convertToEmbedded(), children: strings.imageMenu.convertToEmbedded })), linkType === 'embedded' && (_jsx(Menu.Item, { leftSection: _jsx(IconCrop, { size: 16, stroke: 1.75 }), onClick: () => setCropDialogOpen(true), children: strings.imageMenu.cropResize })), options.allowCaption && (_jsx(Menu.Item, { leftSection: _jsx(IconTextCaption, { size: 16, stroke: 1.75 }), onClick: () => setDialog({ intent: 'caption' }), children: caption ? strings.imageMenu.editCaption : strings.imageMenu.addCaption })), options.allowCaption && caption && (_jsx(Menu.Item, { leftSection: _jsx(IconX, { size: 16, stroke: 1.75 }), onClick: () => updateNode((node) => node.setCaption(null)), children: strings.imageMenu.removeCaption })), _jsx(Menu.Divider, {}), _jsx(Menu.Item, { color: "red", leftSection: _jsx(IconTrash, { size: 16, stroke: 1.75 }), onClick: removeImage, children: strings.imageMenu.deleteImage })] })] }), _jsx(ImageDialog, { mode: "edit", opened: dialog !== null, initial: { src, altText, caption, linkType }, intent: dialog?.intent, onSubmit: applyEdit, onClose: () => {
161
163
  setDialog(null);
162
164
  editor.focus();
163
165
  } }), linkType === 'embedded' && (_jsx(ImageCropDialog, { opened: cropDialogOpen, initial: { src, width, height }, onSubmit: applyCrop, onClose: () => {
@@ -1 +1 @@
1
- {"version":3,"file":"ImageCropDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropDialog.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAqBD;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,oBAAoB,+BAyJ3F"}
1
+ {"version":3,"file":"ImageCropDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageCropDialog.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACtE,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAChD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAqBD;;qEAEqE;AACrE,wBAAgB,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,oBAAoB,+BA6J3F"}
@@ -5,6 +5,7 @@ import { normalizeImageUrl } from './imageUrl.js';
5
5
  import { dataUrlBytes, dataUrlToFile, fitDimension, mimeFromSrc } from './imageEdit.js';
6
6
  import { ImageCropper } from './ImageCropper.js';
7
7
  import { useImageOptions } from './ImageOptionsContext.js';
8
+ import { useUiStrings } from '../i18n/useStrings.js';
8
9
  /** Natural pixel size of an image URL (for the aspect ratio and size presets). */
9
10
  function useNaturalSize(src) {
10
11
  const [size, setSize] = useState(null);
@@ -29,6 +30,7 @@ function useNaturalSize(src) {
29
30
  * its context menu. Edits that change pixels are re-embedded as a data URI,
30
31
  * or stored through the host's `onImageUpload` when there is one. */
31
32
  export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
33
+ const strings = useUiStrings();
32
34
  const { maxSizeMB, onImageUpload } = useImageOptions();
33
35
  const [pixelSrc, setPixelSrc] = useState(null);
34
36
  const [width, setWidth] = useState(null);
@@ -75,7 +77,7 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
75
77
  let src = effectiveSrc;
76
78
  if (pixelSrc) {
77
79
  if (dataUrlBytes(pixelSrc) > maxSizeMB * 1024 * 1024) {
78
- setError(`Image is larger than ${maxSizeMB} MB`);
80
+ setError(strings.imageCropDialog.errors.largerThan(maxSizeMB));
79
81
  return;
80
82
  }
81
83
  if (onImageUpload) {
@@ -83,7 +85,7 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
83
85
  const ext = mimeFromSrc(pixelSrc).split('/')[1];
84
86
  src = await onImageUpload(dataUrlToFile(pixelSrc, `image.${ext}`));
85
87
  if (!normalizeImageUrl(src)) {
86
- setError('The upload handler returned an unusable image URL');
88
+ setError(strings.imageCropDialog.errors.uploadUnusable);
87
89
  return;
88
90
  }
89
91
  }
@@ -91,23 +93,26 @@ export function ImageCropDialog({ opened, initial, onSubmit, onClose }) {
91
93
  onSubmit({ src, width, height });
92
94
  }
93
95
  catch (err) {
94
- setError(err instanceof Error ? err.message : 'Could not save the image');
96
+ // ImageCropper's own pixel-edit error message (imageEdit.ts,
97
+ // applyPixelOp) is shown as-is when present — see the note in
98
+ // ImageDialog.tsx for why that pure utility's messages stay English.
99
+ setError(err instanceof Error ? err.message : strings.imageCropDialog.errors.couldNotSave);
95
100
  }
96
101
  finally {
97
102
  setBusy(false);
98
103
  }
99
104
  };
100
- return (_jsx(Modal, { opened: opened, onClose: onClose, title: "Crop & resize", centered: true, size: "lg", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "md", children: [_jsx(ImageCropper, { src: effectiveSrc, canReset: pixelSrc !== null, onApply: (edited) => {
105
+ return (_jsx(Modal, { opened: opened, onClose: onClose, title: strings.imageCropDialog.title, centered: true, size: "lg", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "md", children: [_jsx(ImageCropper, { src: effectiveSrc, canReset: pixelSrc !== null, onApply: (edited) => {
101
106
  setPixelSrc(edited);
102
107
  resetSize();
103
108
  }, onReset: () => {
104
109
  setPixelSrc(null);
105
110
  setWidth(initial.width);
106
111
  setHeight(initial.height);
107
- } }), _jsx(Divider, { label: "Size", labelPosition: "left" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { grow: true, align: "flex-end", children: [_jsx(NumberInput, { label: "Width (px)", min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.width) : 'Auto', value: width ?? '', onChange: (v) => onSizeChange('width', v) }), _jsx(NumberInput, { label: "Height (px)", min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.height) : 'Auto', value: height ?? '', onChange: (v) => onSizeChange('height', v) })] }), _jsx(Checkbox, { label: "Keep proportions", checked: lockAspect, onChange: (e) => setLockAspect(e.currentTarget.checked) }), _jsxs(Group, { gap: "xs", children: [[25, 50, 75, 100].map((pct) => (_jsxs(Button, { size: "xs", variant: "default", disabled: !natural, onClick: () => {
112
+ } }), _jsx(Divider, { label: strings.imageCropDialog.sizeLabel, labelPosition: "left" }), _jsxs(Stack, { gap: "sm", children: [_jsxs(Group, { grow: true, align: "flex-end", children: [_jsx(NumberInput, { label: strings.imageCropDialog.widthLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.width) : strings.imageCropDialog.autoPlaceholder, value: width ?? '', onChange: (v) => onSizeChange('width', v) }), _jsx(NumberInput, { label: strings.imageCropDialog.heightLabel, min: 1, max: 10000, hideControls: true, placeholder: natural ? String(natural.height) : strings.imageCropDialog.autoPlaceholder, value: height ?? '', onChange: (v) => onSizeChange('height', v) })] }), _jsx(Checkbox, { label: strings.imageCropDialog.keepProportions, checked: lockAspect, onChange: (e) => setLockAspect(e.currentTarget.checked) }), _jsxs(Group, { gap: "xs", children: [[25, 50, 75, 100].map((pct) => (_jsxs(Button, { size: "xs", variant: "default", disabled: !natural, onClick: () => {
108
113
  if (!natural)
109
114
  return;
110
115
  setWidth(Math.round((natural.width * pct) / 100));
111
116
  setHeight(Math.round((natural.height * pct) / 100));
112
- }, children: [pct, "%"] }, pct))), _jsx(Button, { size: "xs", variant: "subtle", onClick: resetSize, children: "Original size" })] }), _jsxs(Text, { size: "xs", c: "dimmed", children: ["You can also drag the corner handle on a selected image. ", natural ? `Original: ${natural.width} × ${natural.height} px.` : ''] })] }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", loading: busy, children: "Save" })] })] }) }) }));
117
+ }, children: [pct, "%"] }, pct))), _jsx(Button, { size: "xs", variant: "subtle", onClick: resetSize, children: strings.imageCropDialog.originalSize })] }), _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageCropDialog.dragHint(natural) })] }), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: strings.common.save })] })] }) }) }));
113
118
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB;gFAC4E;IAC5E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5F;4CACwC;IACxC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAaD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAmPjG"}
1
+ {"version":3,"file":"ImageDialog.d.ts","sourceRoot":"","sources":["../../src/image/ImageDialog.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,wBAAwB;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB;gFAC4E;IAC5E,aAAa,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,0CAA0C;IAC1C,OAAO,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,aAAa,CAAA;KAAE,CAAC;IAC5F;4CACwC;IACxC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC5C,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAaD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,gBAAgB,+BAgQjG"}
@@ -5,11 +5,12 @@ import { IconPhotoDown } from '@tabler/icons-react';
5
5
  import { ACCEPTED_IMAGE_TYPES, normalizeImageUrl, validateImageFile } from './imageUrl.js';
6
6
  import { dataUrlBytes, dataUrlToFile, fetchImageAsDataUrl, mimeFromSrc } from './imageEdit.js';
7
7
  import { useImageOptions } from './ImageOptionsContext.js';
8
+ import { useUiStrings } from '../i18n/useStrings.js';
8
9
  function readAsDataUrl(file) {
9
10
  return new Promise((resolve, reject) => {
10
11
  const reader = new FileReader();
11
12
  reader.onload = () => resolve(String(reader.result));
12
- reader.onerror = () => reject(reader.error ?? new Error('Could not read the file'));
13
+ reader.onerror = () => reject(new Error('READ_FAILED'));
13
14
  reader.readAsDataURL(file);
14
15
  });
15
16
  }
@@ -21,6 +22,7 @@ function readAsDataUrl(file) {
21
22
  * image's context menu once it has pixels of its own to edit.
22
23
  */
23
24
  export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }) {
25
+ const strings = useUiStrings();
24
26
  const { allowLinked, allowEmbedded, allowCaption, maxSizeMB, onImageUpload, fetchImage } = useImageOptions();
25
27
  const editing = mode === 'edit' && initial !== undefined;
26
28
  const defaultSource = editing ? 'keep' : allowLinked ? 'linked' : 'embedded';
@@ -74,8 +76,8 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
74
76
  try {
75
77
  setFileSrc(await readAsDataUrl(next));
76
78
  }
77
- catch (err) {
78
- setError(err instanceof Error ? err.message : 'Could not read the file');
79
+ catch {
80
+ setError(strings.imageDialog.errors.couldNotReadFile);
79
81
  }
80
82
  };
81
83
  /** Downloads a linked image and switches the dialog to its embedded copy. */
@@ -87,13 +89,17 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
87
89
  try {
88
90
  const dataUrl = await fetchImageAsDataUrl(src, fetchImage);
89
91
  if (dataUrlBytes(dataUrl) > maxSizeMB * 1024 * 1024)
90
- throw new Error(`Image is larger than ${maxSizeMB} MB`);
92
+ throw new Error(strings.imageDialog.errors.largerThan(maxSizeMB));
91
93
  setSource('embedded');
92
94
  setFile(null);
93
95
  setFileSrc(dataUrl);
94
96
  }
95
97
  catch (err) {
96
- setError(err instanceof Error ? err.message : 'Could not convert the image');
98
+ // fetchImageAsDataUrl's own errors (network/CORS failures, unsupported
99
+ // type) are English (packages/react/src/image/imageEdit.ts) — shown
100
+ // as-is since threading locale into that pure utility isn't worth the
101
+ // refactor here; only the size-limit message above is ours to localize.
102
+ setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotConvert);
97
103
  }
98
104
  finally {
99
105
  setConverting(false);
@@ -103,7 +109,11 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
103
109
  e.preventDefault();
104
110
  setError(null);
105
111
  if (!baseSrc) {
106
- setError(source === 'linked' ? 'Enter an http(s) or relative image URL' : source === 'embedded' ? 'Choose an image file' : 'No image');
112
+ setError(source === 'linked'
113
+ ? strings.imageDialog.errors.enterUrl
114
+ : source === 'embedded'
115
+ ? strings.imageDialog.errors.chooseFile
116
+ : strings.imageDialog.errors.noImage);
107
117
  return;
108
118
  }
109
119
  try {
@@ -119,7 +129,7 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
119
129
  }
120
130
  if (newPixels) {
121
131
  if (dataUrlBytes(newPixels) > maxSizeMB * 1024 * 1024) {
122
- setError(`Image is larger than ${maxSizeMB} MB`);
132
+ setError(strings.imageDialog.errors.largerThan(maxSizeMB));
123
133
  return;
124
134
  }
125
135
  linkType = 'embedded';
@@ -129,7 +139,7 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
129
139
  const upload = !file ? dataUrlToFile(newPixels, `image.${ext}`) : file;
130
140
  src = await onImageUpload(upload);
131
141
  if (!normalizeImageUrl(src)) {
132
- setError('The upload handler returned an unusable image URL');
142
+ setError(strings.imageDialog.errors.uploadUnusable);
133
143
  return;
134
144
  }
135
145
  }
@@ -146,17 +156,19 @@ export function ImageDialog({ mode, opened, initial, intent, onSubmit, onClose }
146
156
  });
147
157
  }
148
158
  catch (err) {
149
- setError(err instanceof Error ? err.message : 'Could not save the image');
159
+ // fetchImageAsDataUrl's own network/CORS error message (imageEdit.ts)
160
+ // is shown as-is when present — see note in `convert` above.
161
+ setError(err instanceof Error ? err.message : strings.imageDialog.errors.couldNotSave);
150
162
  }
151
163
  finally {
152
164
  setBusy(false);
153
165
  }
154
166
  };
155
- const convertButton = (_jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPhotoDown, { size: 14 }), loading: converting, disabled: !baseSrc, onClick: () => convert(baseSrc), children: "Convert to embedded image" }));
156
- return (_jsx(Modal, { opened: opened, onClose: onClose, title: editing ? 'Edit image' : 'Insert image', centered: true, size: "sm", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [(editing || (allowLinked && allowEmbedded)) && (_jsx(SegmentedControl, { fullWidth: true, value: source, onChange: (v) => changeSource(v), data: [
157
- ...(editing ? [{ value: 'keep', label: 'Current image' }] : []),
158
- ...(allowLinked ? [{ value: 'linked', label: 'From URL' }] : []),
159
- ...(allowEmbedded ? [{ value: 'embedded', label: 'Upload' }] : []),
160
- ] })), source === 'keep' && (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", c: "dimmed", lineClamp: 2, style: { wordBreak: 'break-all' }, children: initial?.linkType === 'embedded' ? 'Embedded in the document' : `Linked from ${initial?.src}` }), initial?.linkType === 'linked' && allowEmbedded && (_jsxs(Group, { gap: "xs", children: [convertButton, _jsx(Text, { size: "xs", c: "dimmed", children: "Downloads a copy into the document, so it can be cropped, rotated and resized." })] }))] })), source === 'linked' && (_jsxs(_Fragment, { children: [_jsx(TextInput, { label: "Image URL", placeholder: "https://example.com/photo.jpg", value: url, onChange: (e) => setUrl(e.currentTarget.value), "data-autofocus": intent !== 'caption' ? true : undefined }), allowEmbedded && (_jsx(Checkbox, { label: "Embed a copy in the document", description: "Downloads the image so it can be edited and no longer depends on the URL.", checked: embedCopy, onChange: (e) => setEmbedCopy(e.currentTarget.checked) }))] })), source === 'embedded' &&
161
- (convertedFromUrl ? (_jsx(Text, { size: "sm", c: "dimmed", children: "Converted from the URL: a copy will be embedded in the document." })) : (_jsx(FileInput, { label: "Image file", placeholder: "Choose an image", accept: ACCEPTED_IMAGE_TYPES.join(','), value: file, onChange: changeFile, description: `Up to ${maxSizeMB} MB${onImageUpload ? '' : '; stored inside the document'}` }))), _jsx(TextInput, { label: "Alt text", description: "Describes the image for screen readers", value: altText, onChange: (e) => setAltText(e.currentTarget.value) }), allowCaption && (_jsx(TextInput, { label: "Caption", description: "Shown under the image. Leave empty for none.", value: caption, onChange: (e) => setCaption(e.currentTarget.value), "data-autofocus": intent === 'caption' ? true : undefined })), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: "Cancel" }), _jsx(Button, { type: "submit", loading: busy, children: editing ? 'Save' : 'Insert' })] })] }) }) }));
167
+ const convertButton = (_jsx(Button, { size: "xs", variant: "light", leftSection: _jsx(IconPhotoDown, { size: 14 }), loading: converting, disabled: !baseSrc, onClick: () => convert(baseSrc), children: strings.imageDialog.convertButton }));
168
+ return (_jsx(Modal, { opened: opened, onClose: onClose, title: editing ? strings.imageDialog.titleEdit : strings.imageDialog.titleInsert, centered: true, size: "sm", children: _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [(editing || (allowLinked && allowEmbedded)) && (_jsx(SegmentedControl, { fullWidth: true, value: source, onChange: (v) => changeSource(v), data: [
169
+ ...(editing ? [{ value: 'keep', label: strings.imageDialog.currentImage }] : []),
170
+ ...(allowLinked ? [{ value: 'linked', label: strings.imageDialog.fromUrl }] : []),
171
+ ...(allowEmbedded ? [{ value: 'embedded', label: strings.imageDialog.upload }] : []),
172
+ ] })), source === 'keep' && (_jsxs(Stack, { gap: 6, children: [_jsx(Text, { size: "sm", c: "dimmed", lineClamp: 2, style: { wordBreak: 'break-all' }, children: initial?.linkType === 'embedded' ? strings.imageDialog.embeddedInDocument : strings.imageDialog.linkedFrom(initial?.src ?? '') }), initial?.linkType === 'linked' && allowEmbedded && (_jsxs(Group, { gap: "xs", children: [convertButton, _jsx(Text, { size: "xs", c: "dimmed", children: strings.imageDialog.convertHint })] }))] })), source === 'linked' && (_jsxs(_Fragment, { children: [_jsx(TextInput, { label: strings.imageDialog.imageUrlLabel, placeholder: strings.imageDialog.imageUrlPlaceholder, value: url, onChange: (e) => setUrl(e.currentTarget.value), "data-autofocus": intent !== 'caption' ? true : undefined }), allowEmbedded && (_jsx(Checkbox, { label: strings.imageDialog.embedCopyLabel, description: strings.imageDialog.embedCopyDescription, checked: embedCopy, onChange: (e) => setEmbedCopy(e.currentTarget.checked) }))] })), source === 'embedded' &&
173
+ (convertedFromUrl ? (_jsx(Text, { size: "sm", c: "dimmed", children: strings.imageDialog.convertedFromUrl })) : (_jsx(FileInput, { label: strings.imageDialog.fileLabel, placeholder: strings.imageDialog.filePlaceholder, accept: ACCEPTED_IMAGE_TYPES.join(','), value: file, onChange: changeFile, description: strings.imageDialog.fileDescription(maxSizeMB, !onImageUpload) }))), _jsx(TextInput, { label: strings.imageDialog.altTextLabel, description: strings.imageDialog.altTextDescription, value: altText, onChange: (e) => setAltText(e.currentTarget.value) }), allowCaption && (_jsx(TextInput, { label: strings.imageDialog.captionLabel, description: strings.imageDialog.captionDescription, value: caption, onChange: (e) => setCaption(e.currentTarget.value), "data-autofocus": intent === 'caption' ? true : undefined })), error && (_jsx(Alert, { color: "red", variant: "light", p: "xs", children: error })), _jsxs(Group, { justify: "flex-end", gap: "xs", children: [_jsx(Button, { variant: "default", onClick: onClose, children: strings.common.cancel }), _jsx(Button, { type: "submit", loading: busy, children: editing ? strings.common.save : strings.common.insert })] })] }) }) }));
162
174
  }
package/dist/index.d.ts CHANGED
@@ -5,4 +5,6 @@ export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js'
5
5
  export type { ImagePayload, ImageLinkType, SerializedImageNode } from './image/ImageNode.js';
6
6
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
7
7
  export type { FontOption } from './fonts/index.js';
8
+ export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
9
+ export type { Locale, Strings } from './i18n/index.js';
8
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAC9E,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrF,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AACzF,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC"}
package/dist/index.js CHANGED
@@ -2,3 +2,4 @@ export { EditorRoot } from './EditorRoot.js';
2
2
  export { defaultMantineTheme } from './theme/mantineTheme.js';
3
3
  export { ImageNode, $createImageNode, $isImageNode } from './image/ImageNode.js';
4
4
  export { DEFAULT_FONT_OPTIONS, URDU_WEB_FONT_OPTIONS, FONT_SIZES_PX } from './fonts/index.js';
5
+ export { STRINGS, getStrings, useStrings, useUiStrings, UiStringsContext } from './i18n/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inshapardaz/likhari-react",
3
- "version": "0.1.6-dev.60",
3
+ "version": "0.1.6-dev.62",
4
4
  "description": "React implementation of the Likhari editor, built on @lexical/react.",
5
5
  "license": "MIT",
6
6
  "repository": {