@inkbrick/react 0.0.0-stage → 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FormulaPalette-WFRPRHS5.js +320 -0
- package/dist/FormulaPalette-WFRPRHS5.js.map +1 -0
- package/dist/chunk-TPUX4C45.js +2309 -0
- package/dist/chunk-TPUX4C45.js.map +1 -0
- package/dist/index.d.ts +1431 -0
- package/dist/index.js +4880 -0
- package/dist/index.js.map +1 -0
- package/dist/mathCss-FLPE6QBM.js +361 -0
- package/dist/mathCss-FLPE6QBM.js.map +1 -0
- package/package.json +46 -3
- package/src/block-handle/BlockHandleController.ts +93 -0
- package/src/block-handle/BlockHandleHost.tsx +108 -0
- package/src/block-handle/DefaultBlockHandle.tsx +71 -0
- package/src/block-handle/DefaultBlockMenu.tsx +114 -0
- package/src/block-handle/DropIndicatorHost.tsx +58 -0
- package/src/block-handle/actions.tsx +146 -0
- package/src/block-handle/blockDrag.ts +215 -0
- package/src/block-handle/geometry.ts +64 -0
- package/src/block-handle/gripGesture.ts +108 -0
- package/src/block-handle/types.ts +65 -0
- package/src/block-handle/useBlockDrag.ts +21 -0
- package/src/block-handle/useBlockHandles.ts +125 -0
- package/src/blocks/BlockChildren.tsx +17 -0
- package/src/blocks/BlockProps.ts +5 -0
- package/src/blocks/CalloutBlock.tsx +61 -0
- package/src/blocks/CodeBlock.tsx +96 -0
- package/src/blocks/DividerBlock.tsx +15 -0
- package/src/blocks/Editable.tsx +84 -0
- package/src/blocks/FormulaBlock.tsx +173 -0
- package/src/blocks/HeadingBlock.tsx +22 -0
- package/src/blocks/ImageBlock.tsx +362 -0
- package/src/blocks/ListBlock.tsx +86 -0
- package/src/blocks/NoteBlock.tsx +10 -0
- package/src/blocks/PageBlock.tsx +10 -0
- package/src/blocks/ParagraphBlock.tsx +20 -0
- package/src/blocks/QuoteBlock.tsx +18 -0
- package/src/blocks/UnknownBlockView.tsx +27 -0
- package/src/blocks/defineBlock.ts +49 -0
- package/src/clipboard/html/blocks.ts +379 -0
- package/src/clipboard/html/inline.ts +92 -0
- package/src/clipboard/html/style.ts +92 -0
- package/src/clipboard/html/tags.ts +77 -0
- package/src/clipboard/htmlToFragment.ts +41 -0
- package/src/clipboard/useClipboard.ts +296 -0
- package/src/code/highlight.ts +56 -0
- package/src/code/languages.ts +236 -0
- package/src/code/scan/css.ts +164 -0
- package/src/code/scan/generic.ts +328 -0
- package/src/code/scan/html.ts +129 -0
- package/src/code/scan/markdown.ts +58 -0
- package/src/code/scan/shared.ts +81 -0
- package/src/code/scan/yaml.ts +34 -0
- package/src/code/types.ts +34 -0
- package/src/context/EditorContext.ts +9 -0
- package/src/context/EditorProvider.tsx +172 -0
- package/src/controllers/adapters.tsx +63 -0
- package/src/editing/BlockSelectionController.ts +27 -0
- package/src/editing/CaretController.ts +27 -0
- package/src/editing/EditorView.ts +108 -0
- package/src/editing/ViewportController.ts +63 -0
- package/src/editing/beforeInput.ts +276 -0
- package/src/editing/deleteAtBoundary.ts +16 -0
- package/src/editing/inputActions.ts +109 -0
- package/src/editing/renderCode.ts +69 -0
- package/src/editing/renderInline.ts +144 -0
- package/src/editing/selection.ts +262 -0
- package/src/editing/useEditorInput.ts +223 -0
- package/src/editing/useSelectionState.ts +46 -0
- package/src/formula/FormulaController.ts +51 -0
- package/src/formula/FormulaPalette.tsx +79 -0
- package/src/formula/FormulaPopover.tsx +181 -0
- package/src/formula/LazyFormulaPalette.tsx +42 -0
- package/src/formula/actions.ts +138 -0
- package/src/formula/snippet.ts +70 -0
- package/src/formula/symbols.ts +264 -0
- package/src/hooks/useBlock.ts +21 -0
- package/src/hooks/useBlockController.ts +30 -0
- package/src/hooks/useChildren.ts +16 -0
- package/src/hooks/useDocument.ts +16 -0
- package/src/hooks/useEditor.ts +13 -0
- package/src/hooks/useEditorView.ts +13 -0
- package/src/hooks/useHistory.ts +46 -0
- package/src/hooks/useReadOnly.ts +21 -0
- package/src/hooks/useT.ts +10 -0
- package/src/i18n/en.ts +182 -0
- package/src/i18n/ru.ts +174 -0
- package/src/i18n/translate.ts +77 -0
- package/src/icons/ICONS_LICENSE.md +50 -0
- package/src/icons/Icon.tsx +41 -0
- package/src/icons/lucide.ts +234 -0
- package/src/images/ImageError.ts +39 -0
- package/src/images/UploadTracker.ts +49 -0
- package/src/images/insertImages.ts +142 -0
- package/src/images/types.ts +22 -0
- package/src/images/upload.ts +62 -0
- package/src/index.ts +102 -0
- package/src/keymap/defaultKeymap.ts +189 -0
- package/src/keymap/keymap.ts +42 -0
- package/src/keymap/shortcut.ts +105 -0
- package/src/keymap/types.ts +40 -0
- package/src/math/FormulaView.tsx +62 -0
- package/src/math/mathCss.ts +4 -0
- package/src/math/renderer.ts +198 -0
- package/src/registry/defaultBlockComponents.ts +26 -0
- package/src/registry/resolveBlockComponent.ts +14 -0
- package/src/renderer/BlockRenderer.tsx +63 -0
- package/src/renderer/DocumentRenderer.tsx +33 -0
- package/src/renderer/DocumentView.tsx +94 -0
- package/src/renderer/virtual/VirtualChildList.tsx +355 -0
- package/src/renderer/virtual/estimate.ts +62 -0
- package/src/renderer/virtual/virtualRenderer.tsx +35 -0
- package/src/slash/DefaultSlashMenu.tsx +119 -0
- package/src/slash/SlashMenuController.ts +66 -0
- package/src/slash/SlashMenuHost.tsx +48 -0
- package/src/slash/applySlashItem.ts +57 -0
- package/src/slash/defaultSlashItems.tsx +186 -0
- package/src/slash/filter.ts +29 -0
- package/src/slash/items.ts +30 -0
- package/src/slash/syncSlashMenu.ts +67 -0
- package/src/slash/types.ts +21 -0
- package/src/toolbar/DefaultToolbar.tsx +281 -0
- package/src/toolbar/ToolbarController.ts +100 -0
- package/src/toolbar/ToolbarHost.tsx +81 -0
- package/src/toolbar/formatting.ts +75 -0
- package/src/toolbar/rich.ts +58 -0
- package/src/toolbar/syncToolbar.ts +39 -0
- package/src/toolbar/types.ts +21 -0
- package/src/ui/EditorUIContext.ts +26 -0
- package/src/ui/presentation.ts +24 -0
- package/src/ui/types.ts +70 -0
- package/README.md +0 -3
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { BlockId } from '@inkbrick/document';
|
|
4
|
+
|
|
5
|
+
import { useEditor } from './useEditor';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Подписывается на один блок. Блоки в документе иммутабельны (каждое изменение
|
|
9
|
+
* создаёт новый объект), поэтому ссылка — корректный snapshot для React.
|
|
10
|
+
*/
|
|
11
|
+
export function useBlock(id: BlockId) {
|
|
12
|
+
const { store } = useEditor();
|
|
13
|
+
|
|
14
|
+
const subscribe = useCallback(
|
|
15
|
+
(listener: () => void) => store.subscribeBlock(id, listener),
|
|
16
|
+
[store, id],
|
|
17
|
+
);
|
|
18
|
+
const getSnapshot = useCallback(() => store.getBlock(id), [store, id]);
|
|
19
|
+
|
|
20
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
21
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { BaseBlock, BlockProps } from '@inkbrick/document';
|
|
4
|
+
import type { BlockContext, BlockController } from '@inkbrick/engine';
|
|
5
|
+
|
|
6
|
+
import { useEditor } from './useEditor';
|
|
7
|
+
|
|
8
|
+
export interface BlockControllerHandle<
|
|
9
|
+
TProps extends BlockProps = BlockProps,
|
|
10
|
+
> extends BlockContext<TProps> {
|
|
11
|
+
readonly controller: BlockController<TProps> | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Контроллер блока для его React-компонента: `execute('counter.increment')` и `can(...)` уже знают `blockId`.
|
|
16
|
+
* Команды уважают режим только чтения (возвращают undefined); для разметки используйте prop `readOnly`.
|
|
17
|
+
*/
|
|
18
|
+
export function useBlockController<TProps extends BlockProps = BlockProps>(
|
|
19
|
+
block: BaseBlock,
|
|
20
|
+
): BlockControllerHandle<TProps> {
|
|
21
|
+
const editor = useEditor();
|
|
22
|
+
|
|
23
|
+
return useMemo(() => {
|
|
24
|
+
const context = editor.controllers.contextOf<TProps>(block);
|
|
25
|
+
|
|
26
|
+
return Object.assign(Object.create(context) as BlockContext<TProps>, {
|
|
27
|
+
controller: editor.controllers.get(block.flavour) as BlockController<TProps> | undefined,
|
|
28
|
+
});
|
|
29
|
+
}, [editor, block]);
|
|
30
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BlockId } from '@inkbrick/document';
|
|
2
|
+
|
|
3
|
+
import { useBlock } from './useBlock';
|
|
4
|
+
|
|
5
|
+
const NO_CHILDREN: readonly BlockId[] = [];
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Идентификаторы дочерних блоков. Возвращает стабильную ссылку, пока children
|
|
9
|
+
* не изменились (создавать новый массив в getSnapshot нельзя — это приводит
|
|
10
|
+
* к бесконечному циклу рендера в useSyncExternalStore).
|
|
11
|
+
*/
|
|
12
|
+
export function useChildren(parentId: BlockId): readonly BlockId[] {
|
|
13
|
+
const parent = useBlock(parentId);
|
|
14
|
+
|
|
15
|
+
return parent?.children ?? NO_CHILDREN;
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useEditor } from './useEditor';
|
|
4
|
+
|
|
5
|
+
export function useDocument() {
|
|
6
|
+
const editor = useEditor();
|
|
7
|
+
const { store } = editor;
|
|
8
|
+
|
|
9
|
+
useSyncExternalStore(
|
|
10
|
+
(listener) => store.subscribe(listener),
|
|
11
|
+
() => store.getVersion(),
|
|
12
|
+
() => store.getVersion(),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return editor.document;
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { EditorContext } from '../context/EditorContext';
|
|
4
|
+
|
|
5
|
+
export function useEditor() {
|
|
6
|
+
const editor = useContext(EditorContext);
|
|
7
|
+
|
|
8
|
+
if (!editor) {
|
|
9
|
+
throw new Error('useEditor must be used inside <EditorProvider>');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return editor;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import { EditorViewContext } from '../context/EditorContext';
|
|
4
|
+
|
|
5
|
+
export function useEditorView() {
|
|
6
|
+
const view = useContext(EditorViewContext);
|
|
7
|
+
|
|
8
|
+
if (!view) {
|
|
9
|
+
throw new Error('useEditorView must be used inside <EditorProvider>');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return view;
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useCallback, useSyncExternalStore } from 'react';
|
|
2
|
+
import { flushSync } from 'react-dom';
|
|
3
|
+
|
|
4
|
+
import type { HistoryResult } from '@inkbrick/engine';
|
|
5
|
+
|
|
6
|
+
import { useEditor } from './useEditor';
|
|
7
|
+
import { useEditorView } from './useEditorView';
|
|
8
|
+
|
|
9
|
+
export interface HistoryControls {
|
|
10
|
+
readonly canUndo: boolean;
|
|
11
|
+
readonly canRedo: boolean;
|
|
12
|
+
undo(): boolean;
|
|
13
|
+
redo(): boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function useHistory(): HistoryControls {
|
|
17
|
+
const editor = useEditor();
|
|
18
|
+
const view = useEditorView();
|
|
19
|
+
const { store } = editor;
|
|
20
|
+
|
|
21
|
+
useSyncExternalStore(
|
|
22
|
+
(listener) => store.subscribe(listener),
|
|
23
|
+
() => store.getHistoryVersion(),
|
|
24
|
+
() => store.getHistoryVersion(),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const apply = useCallback(
|
|
28
|
+
(action: () => HistoryResult | undefined) => {
|
|
29
|
+
let result: HistoryResult | undefined;
|
|
30
|
+
|
|
31
|
+
flushSync(() => {
|
|
32
|
+
result = action();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result?.caret) view.placeCaret(result.caret.blockId, result.caret.offset);
|
|
36
|
+
|
|
37
|
+
return result !== undefined;
|
|
38
|
+
},
|
|
39
|
+
[view],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const undo = useCallback(() => apply(() => editor.undo()), [apply, editor]);
|
|
43
|
+
const redo = useCallback(() => apply(() => editor.redo()), [apply, editor]);
|
|
44
|
+
|
|
45
|
+
return { canUndo: editor.canUndo, canRedo: editor.canRedo, undo, redo };
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useEditorUI } from '../ui/EditorUIContext';
|
|
4
|
+
import { useEditor } from './useEditor';
|
|
5
|
+
|
|
6
|
+
export function useReadOnly(): boolean {
|
|
7
|
+
const editor = useEditor();
|
|
8
|
+
const { readOnly } = useEditorUI();
|
|
9
|
+
|
|
10
|
+
const state = useSyncExternalStore(
|
|
11
|
+
(listener) => editor.subscribeReadOnly(listener),
|
|
12
|
+
() => editor.readOnly,
|
|
13
|
+
() => editor.readOnly,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* Prop провайдера действует сразу (в том числе при серверном рендере, где эффекты не выполняются),
|
|
18
|
+
* а движок синхронизируется эффектом.
|
|
19
|
+
*/
|
|
20
|
+
return readOnly ?? state;
|
|
21
|
+
}
|
package/src/i18n/en.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Английский словарь — эталон: набор его ключей определяет `MessageKey`, а недостающие ключи любого другого
|
|
3
|
+
* словаря берутся отсюда. В строках можно использовать подстановки `{name}`.
|
|
4
|
+
*/
|
|
5
|
+
export const en = {
|
|
6
|
+
'editor.hint': 'Press / to open the menu',
|
|
7
|
+
'editor.label': 'Document',
|
|
8
|
+
|
|
9
|
+
'block.list.done': 'Done',
|
|
10
|
+
'block.code.language': 'Code language',
|
|
11
|
+
'block.code.copy': 'Copy code',
|
|
12
|
+
'block.code.copied': 'Copied',
|
|
13
|
+
'block.callout.label': 'Callout style: {tone}. Click to change',
|
|
14
|
+
'block.callout.change': 'Change style',
|
|
15
|
+
'block.callout.tone.info': 'info',
|
|
16
|
+
'block.callout.tone.warning': 'warning',
|
|
17
|
+
'block.callout.tone.success': 'success',
|
|
18
|
+
'block.callout.tone.danger': 'danger',
|
|
19
|
+
'block.unknown.title': 'Unknown block type:',
|
|
20
|
+
'block.unknown.hint': 'Register a component for it via',
|
|
21
|
+
|
|
22
|
+
'image.options': 'Image options',
|
|
23
|
+
'image.align.left': 'Align left',
|
|
24
|
+
'image.align.center': 'Align center',
|
|
25
|
+
'image.align.right': 'Align right',
|
|
26
|
+
'image.width': 'Image width',
|
|
27
|
+
'image.alt': 'Alt text',
|
|
28
|
+
'image.caption.label': 'Caption',
|
|
29
|
+
'image.caption.placeholder': 'Write a caption…',
|
|
30
|
+
'image.replace': 'Replace image',
|
|
31
|
+
'image.url.label': 'Image URL',
|
|
32
|
+
'image.url.placeholder': 'Paste an image link…',
|
|
33
|
+
'image.embed': 'Embed',
|
|
34
|
+
'image.upload': 'Upload',
|
|
35
|
+
'image.status.uploading': 'Uploading…',
|
|
36
|
+
'image.status.none': 'No image',
|
|
37
|
+
'image.status.unsupported': 'This image address is not supported',
|
|
38
|
+
'image.status.failed': 'The image could not be loaded',
|
|
39
|
+
'image.status.retry': 'Replace',
|
|
40
|
+
'image.error.invalidLink': 'Enter a valid http(s) image link',
|
|
41
|
+
'image.error.unsupportedAddress': 'This image address is not supported',
|
|
42
|
+
'image.error.type': 'Unsupported image type "{type}"',
|
|
43
|
+
'image.error.size':
|
|
44
|
+
'The image is larger than {size} KB. Provide "images.upload" to store larger images.',
|
|
45
|
+
'image.error.uploadAddress': 'The upload returned an unsupported image address',
|
|
46
|
+
'image.error.read': 'Could not read the file',
|
|
47
|
+
'image.error.generic': 'Could not add the image',
|
|
48
|
+
|
|
49
|
+
'formula.source': 'LaTeX formula',
|
|
50
|
+
'formula.placeholder': 'Type LaTeX, e.g. \\frac{a}{b}',
|
|
51
|
+
'formula.hint': 'Ctrl+Enter to apply, Esc to cancel',
|
|
52
|
+
'formula.empty': 'Add a formula',
|
|
53
|
+
'formula.edit': 'Edit formula',
|
|
54
|
+
'formula.inline.title': 'Inline formula',
|
|
55
|
+
'formula.apply': 'Apply',
|
|
56
|
+
'formula.remove': 'Remove formula',
|
|
57
|
+
'formula.cancel': 'Cancel',
|
|
58
|
+
'formula.palette.label': 'Math symbols',
|
|
59
|
+
'formula.palette.functions': 'Functions',
|
|
60
|
+
'formula.palette.structures': 'Structures',
|
|
61
|
+
'formula.palette.greek': 'Greek letters',
|
|
62
|
+
'formula.palette.operators': 'Operators',
|
|
63
|
+
'formula.palette.relations': 'Relations',
|
|
64
|
+
'formula.palette.arrows': 'Arrows',
|
|
65
|
+
'formula.palette.sets': 'Sets and logic',
|
|
66
|
+
|
|
67
|
+
'color.black': 'Black',
|
|
68
|
+
'color.white': 'White',
|
|
69
|
+
'color.gray': 'Gray',
|
|
70
|
+
'color.brown': 'Brown',
|
|
71
|
+
'color.orange': 'Orange',
|
|
72
|
+
'color.yellow': 'Yellow',
|
|
73
|
+
'color.green': 'Green',
|
|
74
|
+
'color.blue': 'Blue',
|
|
75
|
+
'color.purple': 'Purple',
|
|
76
|
+
'color.pink': 'Pink',
|
|
77
|
+
'color.red': 'Red',
|
|
78
|
+
|
|
79
|
+
'toolbar.label': 'Text formatting',
|
|
80
|
+
'toolbar.bold.label': 'Bold',
|
|
81
|
+
'toolbar.bold.title': 'Bold (Ctrl+B)',
|
|
82
|
+
'toolbar.italic.label': 'Italic',
|
|
83
|
+
'toolbar.italic.title': 'Italic (Ctrl+I)',
|
|
84
|
+
'toolbar.underline.label': 'Underline',
|
|
85
|
+
'toolbar.underline.title': 'Underline (Ctrl+U)',
|
|
86
|
+
'toolbar.strike.label': 'Strikethrough',
|
|
87
|
+
'toolbar.strike.title': 'Strikethrough (Ctrl+Shift+X)',
|
|
88
|
+
'toolbar.code.label': 'Code',
|
|
89
|
+
'toolbar.code.title': 'Code (Ctrl+E)',
|
|
90
|
+
'toolbar.formula.label': 'Formula',
|
|
91
|
+
'toolbar.formula.title': 'Formula (Ctrl+Shift+M)',
|
|
92
|
+
'toolbar.color.label': 'Text color',
|
|
93
|
+
'toolbar.color.title': 'Text color',
|
|
94
|
+
'toolbar.color.default': 'Default color',
|
|
95
|
+
'toolbar.highlight.label': 'Highlight color',
|
|
96
|
+
'toolbar.highlight.title': 'Highlight',
|
|
97
|
+
'toolbar.highlight.none': 'No highlight',
|
|
98
|
+
'toolbar.align.left.label': 'Align left',
|
|
99
|
+
'toolbar.align.left.title': 'Align left (Ctrl+Shift+L)',
|
|
100
|
+
'toolbar.align.center.label': 'Align center',
|
|
101
|
+
'toolbar.align.center.title': 'Align center (Ctrl+Shift+E)',
|
|
102
|
+
'toolbar.align.right.label': 'Align right',
|
|
103
|
+
'toolbar.align.right.title': 'Align right (Ctrl+Shift+R)',
|
|
104
|
+
'toolbar.align.justify.label': 'Justify',
|
|
105
|
+
'toolbar.align.justify.title': 'Justify (Ctrl+Shift+J)',
|
|
106
|
+
'toolbar.link.label': 'Link',
|
|
107
|
+
'toolbar.link.title': 'Link (Ctrl+K)',
|
|
108
|
+
'toolbar.link.url': 'Link URL',
|
|
109
|
+
'toolbar.link.placeholder': 'Paste a link…',
|
|
110
|
+
'toolbar.link.apply': 'Apply link',
|
|
111
|
+
'toolbar.link.remove': 'Remove link',
|
|
112
|
+
|
|
113
|
+
'handle.add': 'Add block below',
|
|
114
|
+
'handle.grip': 'Drag to move, click for options',
|
|
115
|
+
'menu.label': 'Block options',
|
|
116
|
+
'menu.duplicate': 'Duplicate',
|
|
117
|
+
'menu.moveUp': 'Move up',
|
|
118
|
+
'menu.moveDown': 'Move down',
|
|
119
|
+
'menu.delete': 'Delete',
|
|
120
|
+
'menu.turnInto': 'Turn into',
|
|
121
|
+
'menu.empty': 'No actions',
|
|
122
|
+
|
|
123
|
+
'history.undo': 'Undo',
|
|
124
|
+
'history.redo': 'Redo',
|
|
125
|
+
|
|
126
|
+
'slash.label': 'Insert block',
|
|
127
|
+
'slash.empty': 'No results',
|
|
128
|
+
'slash.item.text.title': 'Text',
|
|
129
|
+
'slash.item.text.description': 'Plain paragraph',
|
|
130
|
+
'slash.item.text.keywords': 'paragraph, p',
|
|
131
|
+
'slash.item.heading-1.title': 'Heading 1',
|
|
132
|
+
'slash.item.heading-1.description': 'Large section heading',
|
|
133
|
+
'slash.item.heading-1.keywords': 'h1, title',
|
|
134
|
+
'slash.item.heading-2.title': 'Heading 2',
|
|
135
|
+
'slash.item.heading-2.description': 'Medium section heading',
|
|
136
|
+
'slash.item.heading-2.keywords': 'h2, subtitle',
|
|
137
|
+
'slash.item.heading-3.title': 'Heading 3',
|
|
138
|
+
'slash.item.heading-3.description': 'Small section heading',
|
|
139
|
+
'slash.item.heading-3.keywords': 'h3',
|
|
140
|
+
'slash.item.heading-4.title': 'Heading 4',
|
|
141
|
+
'slash.item.heading-4.description': 'Subsection heading',
|
|
142
|
+
'slash.item.heading-4.keywords': 'h4',
|
|
143
|
+
'slash.item.heading-5.title': 'Heading 5',
|
|
144
|
+
'slash.item.heading-5.description': 'Minor subsection heading',
|
|
145
|
+
'slash.item.heading-5.keywords': 'h5',
|
|
146
|
+
'slash.item.heading-6.title': 'Heading 6',
|
|
147
|
+
'slash.item.heading-6.description': 'Smallest heading',
|
|
148
|
+
'slash.item.heading-6.keywords': 'h6',
|
|
149
|
+
'slash.item.bullet-list.title': 'Bulleted list',
|
|
150
|
+
'slash.item.bullet-list.description': 'Simple bulleted list',
|
|
151
|
+
'slash.item.bullet-list.keywords': 'ul, unordered, list',
|
|
152
|
+
'slash.item.numbered-list.title': 'Numbered list',
|
|
153
|
+
'slash.item.numbered-list.description': 'List with numbering',
|
|
154
|
+
'slash.item.numbered-list.keywords': 'ol, ordered, list',
|
|
155
|
+
'slash.item.todo-list.title': 'To-do list',
|
|
156
|
+
'slash.item.todo-list.description': 'Track tasks with checkboxes',
|
|
157
|
+
'slash.item.todo-list.keywords': 'todo, task, checkbox, check',
|
|
158
|
+
'slash.item.quote.title': 'Quote',
|
|
159
|
+
'slash.item.quote.description': 'Capture a quote',
|
|
160
|
+
'slash.item.quote.keywords': 'blockquote, citation',
|
|
161
|
+
'slash.item.code.title': 'Code',
|
|
162
|
+
'slash.item.code.description': 'Code snippet with monospace font',
|
|
163
|
+
'slash.item.code.keywords': 'snippet, pre, program',
|
|
164
|
+
'slash.item.callout.title': 'Callout',
|
|
165
|
+
'slash.item.callout.description': 'Highlight important information',
|
|
166
|
+
'slash.item.callout.keywords': 'info, note, warning, tip, alert',
|
|
167
|
+
'slash.item.image.title': 'Image',
|
|
168
|
+
'slash.item.image.description': 'Upload or embed an image',
|
|
169
|
+
'slash.item.image.keywords': 'picture, photo, img, upload',
|
|
170
|
+
'slash.item.formula.title': 'Formula',
|
|
171
|
+
'slash.item.formula.description': 'Display a LaTeX formula',
|
|
172
|
+
'slash.item.formula.keywords': 'math, equation, latex, tex',
|
|
173
|
+
'slash.item.divider.title': 'Divider',
|
|
174
|
+
'slash.item.divider.description': 'Visually separate sections',
|
|
175
|
+
'slash.item.divider.keywords': 'hr, line, separator, rule',
|
|
176
|
+
} as const;
|
|
177
|
+
|
|
178
|
+
export type MessageKey = keyof typeof en;
|
|
179
|
+
|
|
180
|
+
export type Messages = Readonly<Record<MessageKey, string>>;
|
|
181
|
+
|
|
182
|
+
export type PartialMessages = Readonly<Partial<Record<MessageKey, string>>>;
|
package/src/i18n/ru.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Messages } from './en';
|
|
2
|
+
|
|
3
|
+
export const ru: Messages = {
|
|
4
|
+
'editor.hint': 'Нажмите «/», чтобы открыть меню',
|
|
5
|
+
'editor.label': 'Документ',
|
|
6
|
+
|
|
7
|
+
'block.list.done': 'Выполнено',
|
|
8
|
+
'block.code.language': 'Язык кода',
|
|
9
|
+
'block.code.copy': 'Копировать код',
|
|
10
|
+
'block.code.copied': 'Скопировано',
|
|
11
|
+
'block.callout.label': 'Стиль заметки: {tone}. Нажмите, чтобы изменить',
|
|
12
|
+
'block.callout.change': 'Изменить стиль',
|
|
13
|
+
'block.callout.tone.info': 'информация',
|
|
14
|
+
'block.callout.tone.warning': 'предупреждение',
|
|
15
|
+
'block.callout.tone.success': 'успех',
|
|
16
|
+
'block.callout.tone.danger': 'опасность',
|
|
17
|
+
'block.unknown.title': 'Неизвестный тип блока:',
|
|
18
|
+
'block.unknown.hint': 'Зарегистрируйте для него компонент через',
|
|
19
|
+
|
|
20
|
+
'image.options': 'Параметры изображения',
|
|
21
|
+
'image.align.left': 'По левому краю',
|
|
22
|
+
'image.align.center': 'По центру',
|
|
23
|
+
'image.align.right': 'По правому краю',
|
|
24
|
+
'image.width': 'Ширина изображения',
|
|
25
|
+
'image.alt': 'Альтернативный текст',
|
|
26
|
+
'image.caption.label': 'Подпись',
|
|
27
|
+
'image.caption.placeholder': 'Добавьте подпись…',
|
|
28
|
+
'image.replace': 'Заменить изображение',
|
|
29
|
+
'image.url.label': 'Адрес изображения',
|
|
30
|
+
'image.url.placeholder': 'Вставьте ссылку на изображение…',
|
|
31
|
+
'image.embed': 'Вставить',
|
|
32
|
+
'image.upload': 'Загрузить',
|
|
33
|
+
'image.status.uploading': 'Загрузка…',
|
|
34
|
+
'image.status.none': 'Нет изображения',
|
|
35
|
+
'image.status.unsupported': 'Этот адрес изображения не поддерживается',
|
|
36
|
+
'image.status.failed': 'Не удалось загрузить изображение',
|
|
37
|
+
'image.status.retry': 'Заменить',
|
|
38
|
+
'image.error.invalidLink': 'Введите корректную ссылку на изображение (http или https)',
|
|
39
|
+
'image.error.unsupportedAddress': 'Этот адрес изображения не поддерживается',
|
|
40
|
+
'image.error.type': 'Неподдерживаемый тип изображения «{type}»',
|
|
41
|
+
'image.error.size':
|
|
42
|
+
'Изображение больше {size} КБ. Чтобы хранить большие изображения, задайте «images.upload».',
|
|
43
|
+
'image.error.uploadAddress': 'Загрузчик вернул неподдерживаемый адрес изображения',
|
|
44
|
+
'image.error.read': 'Не удалось прочитать файл',
|
|
45
|
+
'image.error.generic': 'Не удалось добавить изображение',
|
|
46
|
+
|
|
47
|
+
'formula.source': 'Формула LaTeX',
|
|
48
|
+
'formula.placeholder': 'Введите LaTeX, например \\frac{a}{b}',
|
|
49
|
+
'formula.hint': 'Ctrl+Enter — применить, Esc — отмена',
|
|
50
|
+
'formula.empty': 'Добавить формулу',
|
|
51
|
+
'formula.edit': 'Изменить формулу',
|
|
52
|
+
'formula.inline.title': 'Встроенная формула',
|
|
53
|
+
'formula.apply': 'Применить',
|
|
54
|
+
'formula.remove': 'Удалить формулу',
|
|
55
|
+
'formula.cancel': 'Отмена',
|
|
56
|
+
'formula.palette.label': 'Математические символы',
|
|
57
|
+
'formula.palette.functions': 'Функции',
|
|
58
|
+
'formula.palette.structures': 'Конструкции',
|
|
59
|
+
'formula.palette.greek': 'Греческие буквы',
|
|
60
|
+
'formula.palette.operators': 'Операторы',
|
|
61
|
+
'formula.palette.relations': 'Отношения',
|
|
62
|
+
'formula.palette.arrows': 'Стрелки',
|
|
63
|
+
'formula.palette.sets': 'Множества и логика',
|
|
64
|
+
|
|
65
|
+
'color.black': 'Чёрный',
|
|
66
|
+
'color.white': 'Белый',
|
|
67
|
+
'color.gray': 'Серый',
|
|
68
|
+
'color.brown': 'Коричневый',
|
|
69
|
+
'color.orange': 'Оранжевый',
|
|
70
|
+
'color.yellow': 'Жёлтый',
|
|
71
|
+
'color.green': 'Зелёный',
|
|
72
|
+
'color.blue': 'Синий',
|
|
73
|
+
'color.purple': 'Фиолетовый',
|
|
74
|
+
'color.pink': 'Розовый',
|
|
75
|
+
'color.red': 'Красный',
|
|
76
|
+
|
|
77
|
+
'toolbar.label': 'Форматирование текста',
|
|
78
|
+
'toolbar.bold.label': 'Жирный',
|
|
79
|
+
'toolbar.bold.title': 'Жирный (Ctrl+B)',
|
|
80
|
+
'toolbar.italic.label': 'Курсив',
|
|
81
|
+
'toolbar.italic.title': 'Курсив (Ctrl+I)',
|
|
82
|
+
'toolbar.underline.label': 'Подчёркнутый',
|
|
83
|
+
'toolbar.underline.title': 'Подчёркнутый (Ctrl+U)',
|
|
84
|
+
'toolbar.strike.label': 'Зачёркнутый',
|
|
85
|
+
'toolbar.strike.title': 'Зачёркнутый (Ctrl+Shift+X)',
|
|
86
|
+
'toolbar.code.label': 'Код',
|
|
87
|
+
'toolbar.code.title': 'Код (Ctrl+E)',
|
|
88
|
+
'toolbar.formula.label': 'Формула',
|
|
89
|
+
'toolbar.formula.title': 'Формула (Ctrl+Shift+M)',
|
|
90
|
+
'toolbar.color.label': 'Цвет текста',
|
|
91
|
+
'toolbar.color.title': 'Цвет текста',
|
|
92
|
+
'toolbar.color.default': 'Цвет по умолчанию',
|
|
93
|
+
'toolbar.highlight.label': 'Цвет фона',
|
|
94
|
+
'toolbar.highlight.title': 'Выделение цветом',
|
|
95
|
+
'toolbar.highlight.none': 'Без выделения',
|
|
96
|
+
'toolbar.align.left.label': 'По левому краю',
|
|
97
|
+
'toolbar.align.left.title': 'По левому краю (Ctrl+Shift+L)',
|
|
98
|
+
'toolbar.align.center.label': 'По центру',
|
|
99
|
+
'toolbar.align.center.title': 'По центру (Ctrl+Shift+E)',
|
|
100
|
+
'toolbar.align.right.label': 'По правому краю',
|
|
101
|
+
'toolbar.align.right.title': 'По правому краю (Ctrl+Shift+R)',
|
|
102
|
+
'toolbar.align.justify.label': 'По ширине',
|
|
103
|
+
'toolbar.align.justify.title': 'По ширине (Ctrl+Shift+J)',
|
|
104
|
+
'toolbar.link.label': 'Ссылка',
|
|
105
|
+
'toolbar.link.title': 'Ссылка (Ctrl+K)',
|
|
106
|
+
'toolbar.link.url': 'Адрес ссылки',
|
|
107
|
+
'toolbar.link.placeholder': 'Вставьте ссылку…',
|
|
108
|
+
'toolbar.link.apply': 'Применить ссылку',
|
|
109
|
+
'toolbar.link.remove': 'Удалить ссылку',
|
|
110
|
+
|
|
111
|
+
'handle.add': 'Добавить блок ниже',
|
|
112
|
+
'handle.grip': 'Перетащите, чтобы переместить, или нажмите для параметров',
|
|
113
|
+
'menu.label': 'Параметры блока',
|
|
114
|
+
'menu.duplicate': 'Дублировать',
|
|
115
|
+
'menu.moveUp': 'Переместить вверх',
|
|
116
|
+
'menu.moveDown': 'Переместить вниз',
|
|
117
|
+
'menu.delete': 'Удалить',
|
|
118
|
+
'menu.turnInto': 'Превратить в',
|
|
119
|
+
'menu.empty': 'Нет действий',
|
|
120
|
+
|
|
121
|
+
'history.undo': 'Отменить',
|
|
122
|
+
'history.redo': 'Повторить',
|
|
123
|
+
|
|
124
|
+
'slash.label': 'Вставить блок',
|
|
125
|
+
'slash.empty': 'Ничего не найдено',
|
|
126
|
+
'slash.item.text.title': 'Текст',
|
|
127
|
+
'slash.item.text.description': 'Обычный абзац',
|
|
128
|
+
'slash.item.text.keywords': 'абзац, параграф, текст, paragraph',
|
|
129
|
+
'slash.item.heading-1.title': 'Заголовок 1',
|
|
130
|
+
'slash.item.heading-1.description': 'Крупный заголовок раздела',
|
|
131
|
+
'slash.item.heading-1.keywords': 'заголовок, h1, title',
|
|
132
|
+
'slash.item.heading-2.title': 'Заголовок 2',
|
|
133
|
+
'slash.item.heading-2.description': 'Средний заголовок раздела',
|
|
134
|
+
'slash.item.heading-2.keywords': 'заголовок, подзаголовок, h2',
|
|
135
|
+
'slash.item.heading-3.title': 'Заголовок 3',
|
|
136
|
+
'slash.item.heading-3.description': 'Небольшой заголовок раздела',
|
|
137
|
+
'slash.item.heading-3.keywords': 'заголовок, h3',
|
|
138
|
+
'slash.item.heading-4.title': 'Заголовок 4',
|
|
139
|
+
'slash.item.heading-4.description': 'Заголовок подраздела',
|
|
140
|
+
'slash.item.heading-4.keywords': 'заголовок, h4',
|
|
141
|
+
'slash.item.heading-5.title': 'Заголовок 5',
|
|
142
|
+
'slash.item.heading-5.description': 'Мелкий заголовок подраздела',
|
|
143
|
+
'slash.item.heading-5.keywords': 'заголовок, h5',
|
|
144
|
+
'slash.item.heading-6.title': 'Заголовок 6',
|
|
145
|
+
'slash.item.heading-6.description': 'Самый мелкий заголовок',
|
|
146
|
+
'slash.item.heading-6.keywords': 'заголовок, h6',
|
|
147
|
+
'slash.item.bullet-list.title': 'Маркированный список',
|
|
148
|
+
'slash.item.bullet-list.description': 'Простой список с маркерами',
|
|
149
|
+
'slash.item.bullet-list.keywords': 'список, маркер, ul, list',
|
|
150
|
+
'slash.item.numbered-list.title': 'Нумерованный список',
|
|
151
|
+
'slash.item.numbered-list.description': 'Список с нумерацией',
|
|
152
|
+
'slash.item.numbered-list.keywords': 'список, нумерация, ol, list',
|
|
153
|
+
'slash.item.todo-list.title': 'Список задач',
|
|
154
|
+
'slash.item.todo-list.description': 'Задачи с флажками',
|
|
155
|
+
'slash.item.todo-list.keywords': 'задачи, чекбокс, флажок, todo, task',
|
|
156
|
+
'slash.item.quote.title': 'Цитата',
|
|
157
|
+
'slash.item.quote.description': 'Выделить цитату',
|
|
158
|
+
'slash.item.quote.keywords': 'цитата, blockquote',
|
|
159
|
+
'slash.item.code.title': 'Код',
|
|
160
|
+
'slash.item.code.description': 'Фрагмент кода моноширинным шрифтом',
|
|
161
|
+
'slash.item.code.keywords': 'код, сниппет, программа, snippet',
|
|
162
|
+
'slash.item.callout.title': 'Заметка',
|
|
163
|
+
'slash.item.callout.description': 'Выделить важную информацию',
|
|
164
|
+
'slash.item.callout.keywords': 'заметка, важно, предупреждение, подсказка, callout, info',
|
|
165
|
+
'slash.item.image.title': 'Изображение',
|
|
166
|
+
'slash.item.image.description': 'Загрузить или вставить изображение',
|
|
167
|
+
'slash.item.image.keywords': 'картинка, фото, изображение, image, img',
|
|
168
|
+
'slash.item.formula.title': 'Формула',
|
|
169
|
+
'slash.item.formula.description': 'Формула LaTeX отдельной строкой',
|
|
170
|
+
'slash.item.formula.keywords': 'формула, математика, уравнение, latex',
|
|
171
|
+
'slash.item.divider.title': 'Разделитель',
|
|
172
|
+
'slash.item.divider.description': 'Визуально разделить разделы',
|
|
173
|
+
'slash.item.divider.keywords': 'линия, разделитель, черта, hr',
|
|
174
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { en, type MessageKey, type Messages, type PartialMessages } from './en';
|
|
2
|
+
import { ru } from './ru';
|
|
3
|
+
|
|
4
|
+
export type MessageParams = Readonly<Record<string, string | number>>;
|
|
5
|
+
|
|
6
|
+
export type TFunction = (key: MessageKey, params?: MessageParams) => string;
|
|
7
|
+
|
|
8
|
+
const registry = new Map<string, PartialMessages>([
|
|
9
|
+
['en', en],
|
|
10
|
+
['ru', ru],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Подключает словарь под кодом языка (`'de'`, `'uk'`, `'pt-BR'`). Словарь может быть неполным: недостающие
|
|
15
|
+
* ключи берутся из английского. Повторная регистрация того же кода заменяет словарь (в том числе встроенный).
|
|
16
|
+
*/
|
|
17
|
+
export function registerLocale(code: string, messages: PartialMessages): void {
|
|
18
|
+
registry.set(normalize(code), messages);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getLocales(): string[] {
|
|
22
|
+
return [...registry.keys()];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const normalize = (code: string) => code.trim().toLowerCase();
|
|
26
|
+
|
|
27
|
+
function lookup(
|
|
28
|
+
code: string,
|
|
29
|
+
locales: Readonly<Record<string, PartialMessages>> | undefined,
|
|
30
|
+
): PartialMessages | undefined {
|
|
31
|
+
const wanted = normalize(code);
|
|
32
|
+
const own = locales
|
|
33
|
+
? Object.fromEntries(Object.entries(locales).map(([key, value]) => [normalize(key), value]))
|
|
34
|
+
: undefined;
|
|
35
|
+
|
|
36
|
+
for (const candidate of [wanted, wanted.split('-')[0]]) {
|
|
37
|
+
const found = own?.[candidate] ?? registry.get(candidate);
|
|
38
|
+
|
|
39
|
+
if (found) return found;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ResolveMessagesOptions {
|
|
46
|
+
readonly locale?: string;
|
|
47
|
+
readonly locales?: Readonly<Record<string, PartialMessages>>;
|
|
48
|
+
readonly messages?: PartialMessages;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveMessages(options: ResolveMessagesOptions = {}): Messages {
|
|
52
|
+
const base = options.locale ? lookup(options.locale, options.locales) : undefined;
|
|
53
|
+
|
|
54
|
+
return { ...en, ...stripEmpty(base), ...stripEmpty(options.messages) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripEmpty(messages: PartialMessages | undefined): PartialMessages {
|
|
58
|
+
if (!messages) return {};
|
|
59
|
+
|
|
60
|
+
return Object.fromEntries(
|
|
61
|
+
Object.entries(messages).filter(([, value]) => typeof value === 'string'),
|
|
62
|
+
) as PartialMessages;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function interpolate(template: string, params?: MessageParams): string {
|
|
66
|
+
if (!params) return template;
|
|
67
|
+
|
|
68
|
+
return template.replace(/\{(\w+)\}/g, (match, name: string) =>
|
|
69
|
+
name in params ? String(params[name]) : match,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createTranslator(messages: Messages): TFunction {
|
|
74
|
+
return (key, params) => interpolate(messages[key] ?? en[key], params);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const defaultT: TFunction = createTranslator(en);
|