diff --git a/.eslintignore b/.eslintignore index cb3053eaef7..3751428909d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -244,7 +244,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils. packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js @@ -895,11 +894,13 @@ packages/editor/CodeMirror/testUtil/createEditorControl.js packages/editor/CodeMirror/testUtil/createEditorSettings.js packages/editor/CodeMirror/testUtil/createTestEditor.js packages/editor/CodeMirror/testUtil/forceFullParse.js +packages/editor/CodeMirror/testUtil/getLastAnnouncement.js packages/editor/CodeMirror/testUtil/loadLanguages.js packages/editor/CodeMirror/testUtil/pressReleaseKey.js packages/editor/CodeMirror/testUtil/typeText.js packages/editor/CodeMirror/theme.js packages/editor/CodeMirror/utils/biDirectionalTextExtension.js +packages/editor/CodeMirror/utils/editorLocalizationExtension.js packages/editor/CodeMirror/utils/formatting/RegionSpec.js packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js packages/editor/CodeMirror/utils/formatting/findInlineMatch.js @@ -924,6 +925,9 @@ packages/editor/CodeMirror/utils/searchExtension.js packages/editor/CodeMirror/utils/setupVim.js packages/editor/SelectionFormatting.js packages/editor/events.js +packages/editor/localization.js +packages/editor/localizationPatterns.js +packages/editor/tools/buildLocalizations.js packages/editor/types.js packages/fork-htmlparser2/src/CollectingHandler.js packages/fork-htmlparser2/src/FeedHandler.spec.js diff --git a/.gitignore b/.gitignore index dcb47fa5384..5c33f7e81b8 100644 --- a/.gitignore +++ b/.gitignore @@ -220,7 +220,6 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v5/utils/useScrollUtils. packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/CodeMirror.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.js -packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useKeymap.js packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/useRefocusOnVisiblePaneChange.js packages/app-desktop/gui/NoteEditor/NoteBody/PlainEditor/PlainEditor.js @@ -871,11 +870,13 @@ packages/editor/CodeMirror/testUtil/createEditorControl.js packages/editor/CodeMirror/testUtil/createEditorSettings.js packages/editor/CodeMirror/testUtil/createTestEditor.js packages/editor/CodeMirror/testUtil/forceFullParse.js +packages/editor/CodeMirror/testUtil/getLastAnnouncement.js packages/editor/CodeMirror/testUtil/loadLanguages.js packages/editor/CodeMirror/testUtil/pressReleaseKey.js packages/editor/CodeMirror/testUtil/typeText.js packages/editor/CodeMirror/theme.js packages/editor/CodeMirror/utils/biDirectionalTextExtension.js +packages/editor/CodeMirror/utils/editorLocalizationExtension.js packages/editor/CodeMirror/utils/formatting/RegionSpec.js packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.js packages/editor/CodeMirror/utils/formatting/findInlineMatch.js @@ -900,6 +901,9 @@ packages/editor/CodeMirror/utils/searchExtension.js packages/editor/CodeMirror/utils/setupVim.js packages/editor/SelectionFormatting.js packages/editor/events.js +packages/editor/localization.js +packages/editor/localizationPatterns.js +packages/editor/tools/buildLocalizations.js packages/editor/types.js packages/fork-htmlparser2/src/CollectingHandler.js packages/fork-htmlparser2/src/FeedHandler.spec.js diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx index 2464132172d..f2814f81479 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/Editor.tsx @@ -14,7 +14,8 @@ import useKeymap from './utils/useKeymap'; import useEditorSearch from '../utils/useEditorSearchExtension'; import CommandService from '@joplin/lib/services/CommandService'; import { SearchMarkers } from '../../../utils/useSearchMarkers'; -import localisation from './utils/localisation'; +import { makeLocalizations, setLocalizations } from '@joplin/editor/localization'; +import { rawStringInCurrentLocale } from '@joplin/lib/locale'; interface Props extends EditorProps { style: React.CSSProperties; @@ -99,11 +100,12 @@ const Editor = (props: Props, ref: ForwardedRef) => { const editorProps: EditorProps = { ...props, - localisations: localisation(), onEvent: event => onEventRef.current(event), onLogMessage: message => onLogMessageRef.current(message), }; + setLocalizations(makeLocalizations(rawStringInCurrentLocale)); + const editor = createEditor(editorContainerRef.current, editorProps); editor.addStyles({ '.cm-scroller': { overflow: 'auto' }, diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts index 0eb9cc96953..4b51ea5c43b 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/useEditorCommands.ts @@ -84,7 +84,7 @@ const useEditorCommands = (props: Props) => { const url = await dialogs.prompt(_('Insert Hyperlink')); focus('useEditorCommands::textLink', editorRef.current); if (url) { - editorRef.current.wrapSelections('[', `](${url})`); + editorRef.current.wrapSelections('[', `](${url})`, _('Hyperlink')); } }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.ts b/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.ts index a3ca1943f04..69561251ace 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.ts +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.ts @@ -15,14 +15,17 @@ import CodeMirrorControl from '@joplin/editor/CodeMirror/CodeMirrorControl'; import WebViewToRNMessenger from '../../../utils/ipc/WebViewToRNMessenger'; import { WebViewToEditorApi } from '../types'; import { focus } from '@joplin/lib/utils/focusHandler'; +import { Localizations, setLocalizations } from '@joplin/editor/localization'; export const initCodeMirror = ( parentElement: HTMLElement, initialText: string, + localizations: Localizations, settings: EditorSettings, ): CodeMirrorControl => { const messenger = new WebViewToRNMessenger('editor', null); + setLocalizations(localizations); const control = createEditor(parentElement, { initialText, settings, diff --git a/packages/app-mobile/components/NoteEditor/CodeMirror/demo.html b/packages/app-mobile/components/NoteEditor/CodeMirror/demo.html index f1a4dd6c6a0..dc21c76bb2a 100644 --- a/packages/app-mobile/components/NoteEditor/CodeMirror/demo.html +++ b/packages/app-mobile/components/NoteEditor/CodeMirror/demo.html @@ -54,7 +54,7 @@ indentWithTabs: false, }; - window.cm = codeMirrorBundle.initCodeMirror(parent, initialText, settings); + window.cm = codeMirrorBundle.initCodeMirror(parent, initialText, {}, settings); \ No newline at end of file diff --git a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts index 87ba65007de..4999d1f2570 100644 --- a/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts +++ b/packages/app-mobile/components/NoteEditor/MarkdownToolbar/buttons/useHeaderButtons.ts @@ -14,9 +14,6 @@ const useHeaderButtons = ({ selectionState, editorControl, readOnly }: ButtonRow description: _('Header %d', level), active, - // We only call addHeaderButton 5 times and in the same order, so - // the linter error is safe to ignore. - // eslint-disable-next-line @seiyab/react-hooks/rules-of-hooks onPress: () => { editorControl.toggleHeaderLevel(level); }, diff --git a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx index 6f987348fe6..bd2b25ec20b 100644 --- a/packages/app-mobile/components/NoteEditor/NoteEditor.tsx +++ b/packages/app-mobile/components/NoteEditor/NoteEditor.tsx @@ -15,7 +15,7 @@ import { editorFont } from '../global-style'; import { EditorControl as EditorBodyControl, ContentScriptData } from '@joplin/editor/types'; import { EditorControl, EditorSettings, SelectionRange, WebViewToEditorApi } from './types'; -import { _ } from '@joplin/lib/locale'; +import { _, rawStringInCurrentLocale } from '@joplin/lib/locale'; import MarkdownToolbar from './MarkdownToolbar/MarkdownToolbar'; import { ChangeEvent, EditorEvent, EditorEventType, SelectionRangeChangeEvent, UndoRedoDepthChangeEvent } from '@joplin/editor/events'; import { EditorCommandType, EditorKeymap, EditorLanguageType, SearchState } from '@joplin/editor/types'; @@ -30,6 +30,7 @@ import { OnMessageEvent } from '../ExtendedWebView/types'; import { join, dirname } from 'path'; import * as mimeUtils from '@joplin/lib/mime-utils'; import uuid from '@joplin/lib/uuid'; +import { makeLocalizations } from '@joplin/editor/localization'; type ChangeEventHandler = (event: ChangeEvent)=> void; type UndoRedoDepthChangeHandler = (event: UndoRedoDepthChangeEvent)=> void; @@ -376,8 +377,9 @@ function NoteEditor(props: Props, ref: any) { const parentElement = document.getElementsByClassName('CodeMirror')[0]; const initialText = ${JSON.stringify(props.initialText)}; const settings = ${JSON.stringify(editorSettings)}; + const localizations = ${JSON.stringify(makeLocalizations(rawStringInCurrentLocale))}; - window.cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, settings); + window.cm = codeMirrorBundle.initCodeMirror(parentElement, initialText, localizations, settings); ${setInitialSelectionJS} diff --git a/packages/app-mobile/components/screens/SearchScreen/index.tsx b/packages/app-mobile/components/screens/SearchScreen/index.tsx index 137ccb3c017..a4c424f9823 100644 --- a/packages/app-mobile/components/screens/SearchScreen/index.tsx +++ b/packages/app-mobile/components/screens/SearchScreen/index.tsx @@ -97,6 +97,7 @@ const SearchScreenComponent: React.FC = props => { { diff --git a/packages/editor/CodeMirror/createEditor.ts b/packages/editor/CodeMirror/createEditor.ts index cd06f8089ac..78d74fbe125 100644 --- a/packages/editor/CodeMirror/createEditor.ts +++ b/packages/editor/CodeMirror/createEditor.ts @@ -34,6 +34,7 @@ import biDirectionalTextExtension from './utils/biDirectionalTextExtension'; import searchExtension from './utils/searchExtension'; import isCursorAtBeginning from './utils/isCursorAtBeginning'; import overwriteModeExtension from './utils/overwriteModeExtension'; +import editorLocalizationExtension from './utils/editorLocalizationExtension'; // Newer versions of CodeMirror by default use Chrome's EditContext API. // While this might be stable enough for desktop use, it causes significant @@ -276,7 +277,7 @@ const createEditor = ( biDirectionalTextExtension, overwriteModeExtension, - props.localisations ? EditorState.phrases.of(props.localisations) : [], + editorLocalizationExtension(), // Adds additional CSS classes to tokens (the default CSS classes are // auto-generated and thus unstable). diff --git a/packages/editor/CodeMirror/markdown/markdownCommands.test.ts b/packages/editor/CodeMirror/markdown/markdownCommands.test.ts index 05c5d2322a8..45b241073b5 100644 --- a/packages/editor/CodeMirror/markdown/markdownCommands.test.ts +++ b/packages/editor/CodeMirror/markdown/markdownCommands.test.ts @@ -6,6 +6,8 @@ import { } from './markdownCommands'; import createTestEditor from '../testUtil/createTestEditor'; import { blockMathTagName } from './markdownMathParser'; +import getLastAnnouncement from '../testUtil/getLastAnnouncement'; +import typeText from '../testUtil/typeText'; describe('markdownCommands', () => { @@ -38,6 +40,23 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...'); }); + it('should announce for accessibility after bolding or removing bold formatting from content', async () => { + const initialDocText = 'Test'; + const editor = await createTestEditor( + initialDocText, EditorSelection.range(0, initialDocText.length), [], + ); + + toggleBolded(editor); + + expect(editor.state.doc.toString()).toBe('**Test**'); + expect(getLastAnnouncement(editor)).toBe('Added Bold markup'); + + toggleBolded(editor); + + expect(editor.state.doc.toString()).toBe('Test'); + expect(getLastAnnouncement(editor)).toBe('Removed Bold markup'); + }); + it('for a cursor, bolding, then italicizing, should produce a bold-italic region', async () => { const initialDocText = ''; const editor = await createTestEditor( @@ -84,6 +103,19 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('Testing...\n\n`f(x) = ...` is a function.'); }); + it('should announce for accessibility when navigating out of an inline code region', async () => { + const editor = await createTestEditor('', EditorSelection.cursor(0), []); + + toggleCode(editor); + expect(getLastAnnouncement(editor)).toBe('Added Inline code markup'); + typeText(editor, 'test'); + toggleCode(editor); + expect(getLastAnnouncement(editor)).toBe('Moved cursor out of Inline code markup'); + + expect(editor.state.doc.toString()).toBe('`test`'); + expect(editor.state.selection.main.head).toBe(editor.state.doc.length); + }); + it('should set headers to the proper levels (when toggling)', async () => { const initialDocText = 'Testing...\nThis is a test.'; const editor = await createTestEditor(initialDocText, EditorSelection.cursor('Testing...'.length), []); @@ -254,6 +286,20 @@ describe('markdownCommands', () => { expect(editor.state.doc.toString()).toBe('> \tTesting...\n> \tTest.'); }); + it('insertOrIncreaseIndent announce the change when text is selected', async () => { + const initialText = 'Test.'; + const editor = await createTestEditor( + initialText, + EditorSelection.range(0, initialText.length), + [], + ); + + insertOrIncreaseIndent(editor); + + expect(getLastAnnouncement(editor)).toBe('Added Indent markup to line start'); + expect(editor.state.doc.toString()).toBe('\tTest.'); + }); + it('insertOrIncreaseIndent should insert tabs when selection is empty, in a paragraph', async () => { const initialText = 'This is a test\nof indentation.'; const editor = await createTestEditor( diff --git a/packages/editor/CodeMirror/markdown/markdownCommands.ts b/packages/editor/CodeMirror/markdown/markdownCommands.ts index a230d50c32d..95d649ab4fd 100644 --- a/packages/editor/CodeMirror/markdown/markdownCommands.ts +++ b/packages/editor/CodeMirror/markdown/markdownCommands.ts @@ -17,11 +17,16 @@ import growSelectionToNode from '../utils/growSelectionToNode'; import tabsToSpaces from '../utils/formatting/tabsToSpaces'; import renumberSelectedLists from './utils/renumberSelectedLists'; import toggleSelectedLinesStartWith from '../utils/formatting/toggleSelectedLinesStartWith'; +import { _ } from '../../localization'; const startingSpaceRegex = /^(\s*)/; export const toggleBolded: Command = (view: EditorView): boolean => { - const spec = RegionSpec.of({ template: '**', nodeName: 'StrongEmphasis' }); + const spec = RegionSpec.of({ + template: '**', + nodeName: 'StrongEmphasis', + accessibleName: _('Bold'), + }); const changes = toggleInlineFormatGlobally(view.state, spec); view.dispatch(changes); @@ -78,8 +83,13 @@ export const toggleItalicized: Command = (view: EditorView): boolean => { template: { start: '*', end: '*' }, matcher: { start: /[_*]/g, end: /[_*]/g }, + + accessibleName: _('Italic'), }); - view.dispatch(changes); + + view.dispatch( + changes, + ); } return true; @@ -89,11 +99,16 @@ export const toggleItalicized: Command = (view: EditorView): boolean => { // a block (fenced) code block. export const toggleCode: Command = (view: EditorView): boolean => { const codeFenceRegex = /^```\w*\s*$/; - const inlineRegionSpec = RegionSpec.of({ template: '`', nodeName: 'InlineCode' }); + const inlineRegionSpec = RegionSpec.of({ + template: '`', + nodeName: 'InlineCode', + accessibleName: _('Inline code'), + }); const blockRegionSpec: RegionSpec = { nodeName: 'FencedCode', template: { start: '```', end: '```' }, matcher: { start: codeFenceRegex, end: codeFenceRegex }, + accessibleName: _('Block code'), }; const changes = toggleRegionFormatGlobally(view.state, inlineRegionSpec, blockRegionSpec); @@ -105,7 +120,11 @@ export const toggleCode: Command = (view: EditorView): boolean => { export const toggleMath: Command = (view: EditorView): boolean => { const blockStartRegex = /^\$\$/; const blockEndRegex = /\$\$\s*$/; - const inlineRegionSpec = RegionSpec.of({ nodeName: 'InlineMath', template: '$' }); + const inlineRegionSpec = RegionSpec.of({ + nodeName: 'InlineMath', + template: '$', + accessibleName: _('Inline math'), + }); const blockRegionSpec = RegionSpec.of({ nodeName: 'BlockMath', template: '$$', @@ -113,6 +132,7 @@ export const toggleMath: Command = (view: EditorView): boolean => { start: blockStartRegex, end: blockEndRegex, }, + accessibleName: _('Block math'), }); const changes = toggleRegionFormatGlobally(view.state, inlineRegionSpec, blockRegionSpec); @@ -161,6 +181,40 @@ export const toggleList = (listType: ListType): Command => { return null; }; + const getAnnouncementForChange = (itemAddedCount: number, itemReplacedCount: number, itemRemovedCount: number) => { + if (itemAddedCount === 0 && itemRemovedCount === 0 && itemReplacedCount === 0) { + // No changes to announce + return ''; + } + + let listTypeDescription = ''; + if (listType === ListType.CheckList) { + listTypeDescription = _('Checklist'); + } else if (listType === ListType.OrderedList) { + listTypeDescription = _('Numbered list'); + } else if (listType === ListType.UnorderedList) { + listTypeDescription = _('Bullet list'); + } else { + const exhaustivenessCheck: never = listType; + throw new Error(`Unknown list type ${exhaustivenessCheck}`); + } + + const announcement = []; + if (itemAddedCount) { + announcement.push(_('Added %d %s items', itemAddedCount, listTypeDescription)); + } + + if (itemReplacedCount) { + announcement.push(_('Replaced %d items with %s items', itemReplacedCount, listTypeDescription)); + } + + if (itemRemovedCount) { + announcement.push(_('Removed %d %s items', itemRemovedCount, listTypeDescription)); + } + + return announcement.join(' , '); + }; + const changes: TransactionSpec = state.changeByRange((sel: SelectionRange) => { const changes: ChangeSpec[] = []; let containerType: ListType|null = null; @@ -273,6 +327,11 @@ export const toggleList = (listType: ListType): Command => { sel = EditorSelection.range(fromLine.from, toLine.to); } + // Number of list items removed and replaced with non-list items + let numberOfItemsRemoved = 0; + let numberOfItemsReplaced = 0; + let numberOfItemsAdded = 0; + // Number of the item in the list (e.g. 2 for the 2nd item in the list) let listItemCounter = 1; for (let lineNum = fromLine.number; lineNum <= toLine.number; lineNum ++) { @@ -327,6 +386,15 @@ export const toggleList = (listType: ListType): Command => { replacementString = `${firstLineIndentation}- `; } + // Store information required for accessibility announcements + if (replacementString.length === 0) { + numberOfItemsRemoved ++; + } else if (deleteTo > deleteFrom) { + numberOfItemsReplaced ++; + } else { + numberOfItemsAdded ++; + } + changes.push({ from: deleteFrom, to: deleteTo, @@ -348,9 +416,18 @@ export const toggleList = (listType: ListType): Command => { ); } + const announcementText = getAnnouncementForChange( + numberOfItemsAdded, numberOfItemsReplaced, numberOfItemsRemoved, + ); + return { changes, range: sel, + effects: [ + announcementText ? ( + EditorView.announce.of(announcementText) + ) : [], + ].flat(), }; }); view.dispatch(changes); @@ -371,29 +448,34 @@ export const toggleHeaderLevel = (level: number): Command => { headerStr += '#'; } - const matchEmpty = true; // Remove header formatting for any other level let changes = toggleSelectedLinesStartWith( view.state, - new RegExp( - // Check all numbers of #s lower than [level] - `${level - 1 >= 1 ? `(?:^[#]{1,${level - 1}}\\s)|` : '' - - // Check all number of #s higher than [level] - }(?:^[#]{${level + 1},}\\s)`, - ), - '', - matchEmpty, + { + regex: new RegExp( + // Check all numbers of #s lower than [level] + `${level - 1 >= 1 ? `(?:^[#]{1,${level - 1}}\\s)|` : '' + + // Check all number of #s higher than [level] + }(?:^[#]{${level + 1},}\\s)`, + ), + template: '', + matchEmpty: true, + accessibleName: _('Header'), + }, ); view.dispatch(changes); // Set to the proper header level changes = toggleSelectedLinesStartWith( view.state, - // We want exactly [level] '#' characters. - new RegExp(`^[#]{${level}} `), - `${headerStr} `, - matchEmpty, + { + // We want exactly [level] '#' characters. + regex: new RegExp(`^[#]{${level}} `), + template: `${headerStr} `, + matchEmpty: true, + accessibleName: _('Header level %d', level), + }, ); view.dispatch(changes); @@ -428,17 +510,20 @@ export const insertHorizontalRule: Command = (view: EditorView) => { // Prepends the given editor's indentUnit to all lines of the current selection // and re-numbers modified ordered lists (if any). export const increaseIndent: Command = (view: EditorView): boolean => { - const matchEmpty = true; const matchNothing = /$ ^/; const indentUnit = indentString(view.state, getIndentUnit(view.state)); const changes = toggleSelectedLinesStartWith( view.state, - // Delete nothing - matchNothing, - // ...and thus always add indentUnit. - indentUnit, - matchEmpty, + { + // Delete nothing + regex: matchNothing, + // ...and thus always add indentUnit. + template: indentUnit, + matchEmpty: true, + + accessibleName: _('Indent'), + }, ); view.dispatch(changes); @@ -478,15 +563,19 @@ export const insertOrIncreaseIndent: Command = (view: EditorView): boolean => { }; export const decreaseIndent: Command = (view: EditorView): boolean => { - const matchEmpty = true; const changes = toggleSelectedLinesStartWith( view.state, - // Assume indentation is either a tab or in units - // of n spaces. - new RegExp(`^(?:[\\t]|[ ]{1,${getIndentUnit(view.state)}})`), - // Don't add new text - '', - matchEmpty, + { + // Assume indentation is either a tab or in units + // of n spaces. + regex: new RegExp(`^(?:[\\t]|[ ]{1,${getIndentUnit(view.state)}})`), + + // Don't add new text + template: '', + matchEmpty: true, + + accessibleName: _('Indent'), + }, ); view.dispatch(changes); diff --git a/packages/editor/CodeMirror/testUtil/getLastAnnouncement.ts b/packages/editor/CodeMirror/testUtil/getLastAnnouncement.ts new file mode 100644 index 00000000000..7a6eddd5fca --- /dev/null +++ b/packages/editor/CodeMirror/testUtil/getLastAnnouncement.ts @@ -0,0 +1,10 @@ +import { EditorView } from '@codemirror/view'; + +// Returns the most recent accessibility announcement made by +// EditorView.announce.of. +const getLastAnnouncement = (view: EditorView) => { + const announcementContainer = view.dom.querySelector('.cm-announced'); + return announcementContainer.textContent; +}; + +export default getLastAnnouncement; diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.ts b/packages/editor/CodeMirror/utils/editorLocalizationExtension.ts similarity index 74% rename from packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.ts rename to packages/editor/CodeMirror/utils/editorLocalizationExtension.ts index a17cf3571ae..c1d8067a26d 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/v6/utils/localisation.ts +++ b/packages/editor/CodeMirror/utils/editorLocalizationExtension.ts @@ -1,7 +1,9 @@ -import { _ } from '@joplin/lib/locale'; +import { EditorState } from '@codemirror/state'; +import { _ } from '../../localization'; +// Translates built-in editor strings // See https://codemirror.net/examples/translate/ -export default () => ({ +const editorLocalizationExtension = () => EditorState.phrases.of({ // @codemirror/view 'Control character': _('Control character'), @@ -26,3 +28,5 @@ export default () => ({ 'replaced match on line $': _('replaced match on line $'), 'on line': _('on line'), }); + +export default editorLocalizationExtension; diff --git a/packages/editor/CodeMirror/utils/formatting/RegionSpec.ts b/packages/editor/CodeMirror/utils/formatting/RegionSpec.ts index 1064f8663f3..b65fce27a1a 100644 --- a/packages/editor/CodeMirror/utils/formatting/RegionSpec.ts +++ b/packages/editor/CodeMirror/utils/formatting/RegionSpec.ts @@ -16,12 +16,15 @@ export interface RegionSpec { // How to identify the region matcher: RegionMatchSpec; + + accessibleName: string; } export namespace RegionSpec { // eslint-disable-line no-redeclare interface RegionSpecConfig { nodeName?: string; template: string | { start: string; end: string }; + accessibleName: string; matcher?: RegionMatchSpec; } @@ -47,6 +50,7 @@ export namespace RegionSpec { // eslint-disable-line no-redeclare nodeName: config.nodeName, template: { start: templateStart, end: templateEnd }, matcher, + accessibleName: config.accessibleName, }; }; diff --git a/packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.ts b/packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.ts index 1e1f49de435..e5e23c375b6 100644 --- a/packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.ts +++ b/packages/editor/CodeMirror/utils/formatting/findInlineMatch.test.ts @@ -7,6 +7,7 @@ describe('findInlineMatch', () => { const boldSpec: RegionSpec = RegionSpec.of({ template: '**', + accessibleName: 'Bold', }); it('matching a bolded region: should return the length of the match', () => { @@ -33,6 +34,7 @@ describe('findInlineMatch', () => { const spec: RegionSpec = { template: { start: '*', end: '*' }, matcher: { start: /[*_]/g, end: /[*_]/g }, + accessibleName: 'Italic', }; const testString = 'This is a _test_'; const testDoc = DocumentText.of([testString]); @@ -51,6 +53,7 @@ describe('findInlineMatch', () => { start: /^\s*[-*]\s/g, end: /$/g, }, + accessibleName: 'List', }; it('matching a custom list: should not match a list if not within the selection', () => { diff --git a/packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.ts b/packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.ts index 992b4bf91f5..1c1512b1d61 100644 --- a/packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.ts +++ b/packages/editor/CodeMirror/utils/formatting/toggleInlineRegionSurrounded.ts @@ -1,7 +1,9 @@ -import { Text as DocumentText, EditorSelection, SelectionRange } from '@codemirror/state'; +import { EditorSelection, EditorState, SelectionRange } from '@codemirror/state'; import { RegionSpec } from './RegionSpec'; import findInlineMatch, { MatchSide } from './findInlineMatch'; import { SelectionUpdate } from './types'; +import { EditorView } from '@codemirror/view'; +import { _ } from '../../../localization'; // Toggles whether the given selection matches the inline region specified by [spec]. // @@ -10,8 +12,9 @@ import { SelectionUpdate } from './types'; // If the selection is already surrounded by these characters, they are // removed. const toggleInlineRegionSurrounded = ( - doc: DocumentText, sel: SelectionRange, spec: RegionSpec, + state: EditorState, sel: SelectionRange, spec: RegionSpec, ): SelectionUpdate => { + const doc = state.doc; let content = doc.sliceString(sel.from, sel.to); const startMatchLen = findInlineMatch(doc, spec, sel, MatchSide.Start); const endMatchLen = findInlineMatch(doc, spec, sel, MatchSide.End); @@ -22,6 +25,7 @@ const toggleInlineRegionSurrounded = ( const changes = []; let finalSelStart = sel.from; let finalSelEnd = sel.to; + let announcement; if (startsWithBefore && endsWithAfter) { // Remove the before and after. @@ -35,6 +39,8 @@ const toggleInlineRegionSurrounded = ( to: sel.to, insert: content, }); + + announcement = _('Removed %s markup', spec.accessibleName); } else { changes.push({ from: sel.from, @@ -55,11 +61,15 @@ const toggleInlineRegionSurrounded = ( finalSelStart = sel.from + spec.template.start.length; finalSelEnd = finalSelStart; } + announcement = _('Added %s markup', spec.accessibleName); } return { changes, range: EditorSelection.range(finalSelStart, finalSelEnd), + effects: [ + EditorView.announce.of(announcement), + ], }; }; diff --git a/packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.ts b/packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.ts index 95aeba72b73..d2576cdda5c 100644 --- a/packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.ts +++ b/packages/editor/CodeMirror/utils/formatting/toggleInlineSelectionFormat.ts @@ -4,6 +4,8 @@ import { SelectionUpdate } from './types'; import findInlineMatch, { MatchSide } from './findInlineMatch'; import growSelectionToNode from '../growSelectionToNode'; import toggleInlineRegionSurrounded from './toggleInlineRegionSurrounded'; +import { EditorView } from '@codemirror/view'; +import { _ } from '@joplin/lib/locale'; // Returns updated selections: For all selections in the given `EditorState`, toggles // whether each is contained in an inline region of type [spec]. @@ -22,12 +24,17 @@ const toggleInlineSelectionFormat = ( return { range: EditorSelection.cursor(newCursorPos), + effects: [ + EditorView.announce.of( + _('Moved cursor out of %s markup', spec.accessibleName), + ), + ], }; } // Grow the selection to encompass the entire node. const newRange = growSelectionToNode(state, sel, spec.nodeName); - return toggleInlineRegionSurrounded(state.doc, newRange, spec); + return toggleInlineRegionSurrounded(state, newRange, spec); }; export default toggleInlineSelectionFormat; diff --git a/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.ts b/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.ts index 1054fa83998..f8bca8d7bd0 100644 --- a/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.ts +++ b/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.test.ts @@ -12,10 +12,12 @@ describe('toggleRegionFormatGlobally', () => { const inlineCodeRegionSpec = RegionSpec.of({ template: '`', nodeName: 'InlineCode', + accessibleName: 'Inline code', }); const blockCodeRegionSpec: RegionSpec = { template: { start: '``````', end: '``````' }, matcher: { start: codeFenceRegex, end: codeFenceRegex }, + accessibleName: 'Block code', }; it('should create an empty inline region around the cursor, if given an empty selection', () => { diff --git a/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.ts b/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.ts index a310be60888..176abe75b4c 100644 --- a/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.ts +++ b/packages/editor/CodeMirror/utils/formatting/toggleRegionFormatGlobally.ts @@ -3,6 +3,8 @@ import { RegionSpec } from './RegionSpec'; import findInlineMatch, { MatchSide } from './findInlineMatch'; import growSelectionToNode from '../growSelectionToNode'; import toggleInlineSelectionFormat from './toggleInlineSelectionFormat'; +import { EditorView } from '@codemirror/view'; +import { _ } from '../../../localization'; const blockQuoteStartLen = '> '.length; const blockQuoteRegex = /^>\s/; @@ -98,6 +100,11 @@ const toggleRegionFormatGlobally = ( ], range: EditorSelection.cursor(inlineStart + blockStart.length), + effects: [ + EditorView.announce.of( + _('Converted %s to %s', inlineSpec.accessibleName, blockSpec.accessibleName), + ), + ], }; } @@ -146,6 +153,7 @@ const toggleRegionFormatGlobally = ( // Otherwise, we're toggling the block version const startMatch = blockSpec.matcher.start.exec(fromLineText); const stopMatch = blockSpec.matcher.end.exec(toLineText); + let announcement; if (startMatch && stopMatch) { // Get start and stop indices for the starting and ending matches const [fromMatchFrom, fromMatchTo] = getMatchEndPoints(startMatch, fromLine, inBlockQuote); @@ -164,6 +172,8 @@ const toggleRegionFormatGlobally = ( to: toMatchTo, }); charsAdded -= toMatchTo - toMatchFrom; + + announcement = _('Removed %s markup', blockSpec.accessibleName); } else { let insertBefore, insertAfter; @@ -185,15 +195,27 @@ const toggleRegionFormatGlobally = ( insert: insertAfter, }); charsAdded += insertBefore.length + insertAfter.length; + + announcement = _('Added %s markup', blockSpec.accessibleName); + } + + const range = EditorSelection.range( + fromLine.from, toLine.to + charsAdded, + ); + + if (!range.empty) { + announcement += `\n${_('Selected changed content')}`; } return { changes, // Selection should now encompass all lines that were changed. - range: EditorSelection.range( - fromLine.from, toLine.to + charsAdded, - ), + range, + + effects: [ + EditorView.announce.of(announcement), + ], }; }); diff --git a/packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.ts b/packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.ts index 2637312d45c..804136861b6 100644 --- a/packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.ts +++ b/packages/editor/CodeMirror/utils/formatting/toggleSelectedLinesStartWith.ts @@ -1,19 +1,26 @@ import { EditorSelection, EditorState, Line, SelectionRange, TransactionSpec } from '@codemirror/state'; import growSelectionToNode from '../growSelectionToNode'; +import { EditorView } from '@codemirror/view'; +import { _ } from '../../../localization'; -// Toggles whether all lines in the user's selection start with [regex]. -const toggleSelectedLinesStartWith = ( - state: EditorState, - regex: RegExp, - template: string, - matchEmpty: boolean, +interface FormattingSpec { + regex: RegExp; + template: string; + matchEmpty: boolean; // Determines where this formatting can begin on a line. // Defaults to after a block quote marker - lineContentStartRegex = /^>\s/, + lineContentStartRegex?: RegExp; + // Syntax name associated with what [regex] matches (e.g. FencedCode) + nodeName?: string; - // Name associated with what [regex] matches (e.g. FencedCode) - nodeName?: string, + accessibleName: string; +} + +// Toggles whether all lines in the user's selection start with [regex]. +const toggleSelectedLinesStartWith = ( + state: EditorState, + { regex, template, matchEmpty, lineContentStartRegex = /^>\s/, nodeName, accessibleName }: FormattingSpec, ): TransactionSpec => { const getLineContentStart = (line: Line): number => { const blockQuoteMatch = line.text.match(lineContentStartRegex); @@ -38,7 +45,7 @@ const toggleSelectedLinesStartWith = ( const doc = state.doc; const fromLine = doc.lineAt(sel.from); const toLine = doc.lineAt(sel.to); - let hasProp = false; + let alreadyHasFormatting = false; let charsAdded = 0; let charsAddedBefore = 0; @@ -51,12 +58,13 @@ const toggleSelectedLinesStartWith = ( // If already matching [regex], if (text.search(regex) === 0) { - hasProp = true; + alreadyHasFormatting = true; } lines.push(line); } + let changedLineCount = 0; for (const line of lines) { const text = getLineContent(line); const contentFrom = getLineContentStart(line); @@ -68,24 +76,24 @@ const toggleSelectedLinesStartWith = ( continue; } - if (hasProp) { + changedLineCount ++; + if (alreadyHasFormatting) { const match = text.match(regex); - if (!match) { - continue; + if (match) { + changes.push({ + from: contentFrom, + to: contentFrom + match[0].length, + insert: '', + }); + + const deletedSize = match[0].length; + if (contentFrom <= sel.from) { + // Math.min: Handles the case where some deleted characters are before sel.from + // and others are after. + charsAddedBefore -= Math.min(sel.from - contentFrom, deletedSize); + } + charsAdded -= deletedSize; } - changes.push({ - from: contentFrom, - to: contentFrom + match[0].length, - insert: '', - }); - - const deletedSize = match[0].length; - if (contentFrom <= sel.from) { - // Math.min: Handles the case where some deleted characters are before sel.from - // and others are after. - charsAddedBefore -= Math.min(sel.from - contentFrom, deletedSize); - } - charsAdded -= deletedSize; } else { changes.push({ from: contentFrom, @@ -109,11 +117,28 @@ const toggleSelectedLinesStartWith = ( newSel = EditorSelection.range(fromLine.from, toLine.to + charsAdded); } + let announcement = ''; + if (charsAdded > 0) { + announcement = _('Added %s markup', accessibleName); + } else if (charsAdded < 0) { + announcement = _('Removed %s markup', accessibleName); + } + + if (changedLineCount > 1) { + announcement += ` ${_('on %d lines', changedLineCount)}`; + } else { + announcement += ` ${_('to line start')}`; + } + return { changes, // Selection should now encompass all lines that were changed. range: newSel, + + effects: announcement ? [ + EditorView.announce.of(announcement), + ] : [], }; }); diff --git a/packages/editor/CodeMirror/utils/formatting/types.ts b/packages/editor/CodeMirror/utils/formatting/types.ts index f330f84b469..a64ec847376 100644 --- a/packages/editor/CodeMirror/utils/formatting/types.ts +++ b/packages/editor/CodeMirror/utils/formatting/types.ts @@ -1,5 +1,5 @@ -import { ChangeSpec, SelectionRange } from '@codemirror/state'; +import { ChangeSpec, SelectionRange, StateEffect } from '@codemirror/state'; // Specifies the update of a single selection region and its contents -export type SelectionUpdate = { range: SelectionRange; changes?: ChangeSpec }; +export type SelectionUpdate = { range: SelectionRange; changes?: ChangeSpec; effects?: StateEffect[] }; diff --git a/packages/editor/CodeMirror/utils/overwriteModeExtension.test.ts b/packages/editor/CodeMirror/utils/overwriteModeExtension.test.ts index 2be4212a5ce..8bf019c37af 100644 --- a/packages/editor/CodeMirror/utils/overwriteModeExtension.test.ts +++ b/packages/editor/CodeMirror/utils/overwriteModeExtension.test.ts @@ -3,6 +3,7 @@ import createTestEditor from '../testUtil/createTestEditor'; import overwriteModeExtension, { toggleOverwrite } from './overwriteModeExtension'; import typeText from '../testUtil/typeText'; import pressReleaseKey from '../testUtil/pressReleaseKey'; +import getLastAnnouncement from '../testUtil/getLastAnnouncement'; const createEditor = async (initialText: string, defaultEnabled = false) => { const editor = await createTestEditor(initialText, EditorSelection.cursor(0), [], [ @@ -54,4 +55,14 @@ describe('overwriteModeExtension', () => { expect(editor.state.doc.toString()).toBe('Test! This is a test! test\nTest'); }); + + test('should announce when toggling overwrite', async () => { + const editor = await createEditor('\nTest'); + + pressReleaseKey(editor, { key: 'Insert', code: 'Insert' }); + expect(getLastAnnouncement(editor)).toBe('Overwrite mode enabled'); + + pressReleaseKey(editor, { key: 'Insert', code: 'Insert' }); + expect(getLastAnnouncement(editor)).toBe('Overwrite mode disabled'); + }); }); diff --git a/packages/editor/CodeMirror/utils/overwriteModeExtension.ts b/packages/editor/CodeMirror/utils/overwriteModeExtension.ts index 762bd0e00ac..8e2a3fba791 100644 --- a/packages/editor/CodeMirror/utils/overwriteModeExtension.ts +++ b/packages/editor/CodeMirror/utils/overwriteModeExtension.ts @@ -1,5 +1,6 @@ import { keymap, EditorView } from '@codemirror/view'; import { StateField, Facet, StateEffect } from '@codemirror/state'; +import { _ } from '../../localization'; const overwriteModeFacet = Facet.define({ combine: values => values[0] ?? false, @@ -67,8 +68,14 @@ const overwriteModeExtension = [ keymap.of([{ key: 'Insert', run: (view) => { + const newEnabled = !view.state.field(overwriteModeState); view.dispatch({ - effects: toggleOverwrite.of(!view.state.field(overwriteModeState)), + effects: [ + toggleOverwrite.of(newEnabled), + EditorView.announce.of( + newEnabled ? _('Overwrite mode enabled') : _('Overwrite mode disabled'), + ), + ], }); return false; }, diff --git a/packages/editor/localization.ts b/packages/editor/localization.ts new file mode 100644 index 00000000000..36e1e0cf2a5 --- /dev/null +++ b/packages/editor/localization.ts @@ -0,0 +1,34 @@ +import localizationPatterns from './localizationPatterns'; +const { sprintf } = require('sprintf-js'); + +export type Localizations = Record; +const localizations: Localizations = Object.create(null); + +// For mobile, using @joplin/lib/locale directly significantly +// increases bundle size (as of Nov 2024, 2 MB -> 5 MB). +// +// Instead, localized strings should be transferred to the editor library +// just before creating the editor. +export const _ = (pattern: string, ...formatArgs: (string|number)[]) => { + const localizedPattern = localizations[pattern]; + if (localizedPattern !== undefined) { + pattern = localizedPattern; + } + return sprintf(pattern, ...formatArgs); +}; + +type LocalizeNoFormat = (original: string)=> string; +export const makeLocalizations = (localizeNoFormat: LocalizeNoFormat) => { + const result: Localizations = {}; + for (const key of localizationPatterns) { + result[key] = localizeNoFormat(key); + } + return result; +}; + +export const setLocalizations = (newLocalizations: Localizations) => { + for (const [key, translated] of Object.entries(newLocalizations)) { + localizations[key] = translated; + } +}; + diff --git a/packages/editor/localizationPatterns.ts b/packages/editor/localizationPatterns.ts new file mode 100644 index 00000000000..54fa6008665 --- /dev/null +++ b/packages/editor/localizationPatterns.ts @@ -0,0 +1,46 @@ +// AUTO-GENERATED by buildLocalizations.ts +export default [ + 'Removed %s markup', + 'Added %s markup', + 'Moved cursor out of %s markup', + 'Converted %s to %s', + 'Selected changed content', + 'on %d lines', + 'to line start', + 'Bold', + 'Italic', + 'Inline code', + 'Block code', + 'Inline math', + 'Block math', + 'Checklist', + 'Numbered list', + 'Bullet list', + 'Added %d %s items', + 'Replaced %d items with %s items', + 'Removed %d %s items', + 'Header', + 'Header level %d', + 'Indent', + 'Overwrite mode enabled', + 'Overwrite mode disabled', + 'Control character', + 'Selection deleted', + 'Go to line', + 'go', + 'Find', + 'Replace', + 'next', + 'previous', + 'all', + 'match case', + 'by word', + 'replace', + 'replace all', + 'close', + 'current match', + 'replaced $ matches', + 'replaced match on line $', + 'on line', +]; +// AUTO-GENERATED by buildLocalizations.ts diff --git a/packages/editor/package.json b/packages/editor/package.json index 91c1d495cf1..ce8a3620af7 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -6,6 +6,7 @@ "scripts": { "tsc": "tsc --project tsconfig.json", "watch": "tsc --watch --preserveWatchOutput --project tsconfig.json", + "build": "ts-node tools/buildLocalizations.ts", "test": "jest", "test-ci": "yarn test" }, @@ -20,9 +21,11 @@ "@types/react": "18.3.3", "@types/react-redux": "7.1.33", "@types/styled-components": "5.1.32", + "glob": "11.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "ts-jest": "29.1.5", + "ts-node": "10.9.2", "typescript": "5.4.5" }, "dependencies": { diff --git a/packages/editor/tools/buildLocalizations.ts b/packages/editor/tools/buildLocalizations.ts new file mode 100644 index 00000000000..4d1bdbc166f --- /dev/null +++ b/packages/editor/tools/buildLocalizations.ts @@ -0,0 +1,57 @@ +import * as ts from 'typescript'; +import { glob } from 'glob'; +import { basename, dirname, join, resolve } from 'node:path'; +import { writeFile } from 'node:fs/promises'; + +// Finds all strings localized with _(...) in packages/editor so that they can be +// transferred to the editor from the main process. +const buildLocalizations = async () => { + const configPath = ts.findConfigFile('../', ts.sys.fileExists, 'tsconfig.json'); + const config = ts.readConfigFile(configPath, ts.sys.readFile); + + const editorPackagePath = resolve(dirname(__dirname)); + const fileNames = await glob('**/*.ts', { + absolute: true, + cwd: editorPackagePath, + ignore: 'node_modules/**', + }); + const program = ts.createProgram(fileNames, config.config); + + const localizationPatterns = new Set(); + + const visit = (node: ts.Node, sourceFile: ts.SourceFile) => { + if (ts.isCallExpression(node) && node.expression.getText(sourceFile) === '_') { + const phrase = node.arguments[0].getText(sourceFile); + localizationPatterns.add(phrase); + } + + ts.forEachChild(node, otherNode => visit(otherNode, sourceFile)); + }; + + for (const sourceFile of program.getSourceFiles()) { + const sourceFilePath = resolve(sourceFile.fileName); + if (sourceFilePath.includes('node_modules') || !sourceFilePath.startsWith(editorPackagePath)) { + continue; + } + + ts.forEachChild(sourceFile, (node) => visit(node, sourceFile)); + } + + const autoGeneratedComment = `// AUTO-GENERATED by ${basename(__filename)}`; + await writeFile( + join(editorPackagePath, 'localizationPatterns.ts'), + [ + autoGeneratedComment, + 'export default [', + // Each pattern already includes ''s. + ...[ + ...localizationPatterns.values(), + ].map(pattern => `\t${pattern},`), + '];', + autoGeneratedComment, + '', + ].join('\n'), + ); +}; + +void buildLocalizations(); diff --git a/packages/editor/types.ts b/packages/editor/types.ts index 12a15f02c62..7a2750b6898 100644 --- a/packages/editor/types.ts +++ b/packages/editor/types.ts @@ -177,15 +177,9 @@ export type OnEventCallback = (event: EditorEvent)=> void; export type PasteFileCallback = (data: File)=> Promise; type OnScrollPastBeginningCallback = ()=> void; -interface Localisations { - [editorString: string]: string; -} - export interface EditorProps { settings: EditorSettings; initialText: string; - // Used mostly for internal editor library strings - localisations?: Localisations; // If null, paste and drag-and-drop will not work for resources unless handled elsewhere. onPasteFile: PasteFileCallback|null; diff --git a/packages/lib/locale.ts b/packages/lib/locale.ts index 11083dd9e55..eb014cdedc9 100644 --- a/packages/lib/locale.ts +++ b/packages/lib/locale.ts @@ -683,6 +683,24 @@ export const toIso639Alpha3 = (code: string) => { return info.alpha3; }; +const rawStringByLocale = (locale: string, source: string) => { + const strings = localeStrings(locale); + const lookUpResult = strings[source]; + let translatedString = ''; + + if (lookUpResult === undefined || !lookUpResult.join('')) { + translatedString = source; + } else { + translatedString = lookUpResult[0]; + } + + return translatedString; +}; + +export const rawStringInCurrentLocale = (source: string) => { + return rawStringByLocale(currentLocale_, source); +}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied function _(s: string, ...args: any[]): string { return stringByLocale(currentLocale_, s, ...args); @@ -714,17 +732,9 @@ function _n(singular: string, plural: string, n: number, ...args: any[]) { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied -const stringByLocale = (locale: string, s: string, ...args: any[]): string => { - const strings = localeStrings(locale); - const result = strings[s]; - let translatedString = ''; - - if (result === undefined || !result.join('')) { - translatedString = s; - } else { - translatedString = result[0]; - } +// Like lookUpStringInLocale, but applies format args. +const stringByLocale = (locale: string, s: string, ...args: unknown[]): string => { + const translatedString = rawStringByLocale(locale, s); try { return sprintf(translatedString, ...args); diff --git a/yarn.lock b/yarn.lock index adb3eca8dbf..5a2aa8cbd25 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8525,9 +8525,11 @@ __metadata: "@types/react": 18.3.3 "@types/react-redux": 7.1.33 "@types/styled-components": 5.1.32 + glob: 11.0.0 jest: 29.7.0 jest-environment-jsdom: 29.7.0 ts-jest: 29.1.5 + ts-node: 10.9.2 typescript: 5.4.5 languageName: unknown linkType: soft @@ -26482,6 +26484,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:11.0.0": + version: 11.0.0 + resolution: "glob@npm:11.0.0" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^4.0.1 + minimatch: ^10.0.0 + minipass: ^7.1.2 + package-json-from-dist: ^1.0.0 + path-scurry: ^2.0.0 + bin: + glob: dist/esm/bin.mjs + checksum: 8a2dd914d5776987be5244624d9491bbcaf19f2387e06783737003ff696ebfd2264190c47014f8709c1c02d8bc892f17660cf986c587b107e194c0a3151ab333 + languageName: node + linkType: hard + "glob@npm:5 - 7, glob@npm:^7.0.0, glob@npm:^7.0.5, glob@npm:^7.1.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6": version: 7.2.0 resolution: "glob@npm:7.2.0" @@ -30053,6 +30071,15 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^4.0.1": + version: 4.0.2 + resolution: "jackspeak@npm:4.0.2" + dependencies: + "@isaacs/cliui": ^8.0.2 + checksum: 210030029edfa1658328799ad88c3d0fc057c4cb8a069fc4137cc8d2cc4b65c9721c6e749e890f9ca77a954bb54f200f715b8896e50d330e5f3e902e72b40974 + languageName: node + linkType: hard + "jake@npm:^10.8.5": version: 10.8.5 resolution: "jake@npm:10.8.5" @@ -32620,6 +32647,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.0.0": + version: 11.0.2 + resolution: "lru-cache@npm:11.0.2" + checksum: f9c27c58919a30f42834de9444de9f75bcbbb802c459239f96dd449ad880d8f9a42f51556d13659864dc94ab2dbded9c4a4f42a3e25a45b6da01bb86111224df + languageName: node + linkType: hard + "lru-cache@npm:^5.1.1": version: 5.1.1 resolution: "lru-cache@npm:5.1.1" @@ -34272,6 +34306,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^10.0.0": + version: 10.0.1 + resolution: "minimatch@npm:10.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: f5b63c2f30606091a057c5f679b067f84a2cd0ffbd2dbc9143bda850afd353c7be81949ff11ae0c86988f07390eeca64efd7143ee05a0dab37f6c6b38a2ebb6c + languageName: node + linkType: hard + "minimatch@npm:^3.0.2, minimatch@npm:^3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" @@ -37257,6 +37300,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^2.0.0": + version: 2.0.0 + resolution: "path-scurry@npm:2.0.0" + dependencies: + lru-cache: ^11.0.0 + minipass: ^7.1.2 + checksum: 9953ce3857f7e0796b187a7066eede63864b7e1dfc14bf0484249801a5ab9afb90d9a58fc533ebb1b552d23767df8aa6a2c6c62caf3f8a65f6ce336a97bbb484 + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7"