Multilanguage posting

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-05-28 00:11:28 +02:00
parent a20e57e062
commit ed9dc9eee3
25 changed files with 323 additions and 136 deletions

View File

@@ -87,6 +87,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
schedule: scheduledAt,
group_id: groupId,
text,
modified_language: modifiedLanguage,
} = compose;
const prevSpoiler = usePrevious(spoiler);
@@ -282,6 +283,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<div>
<Suspense>
<ComposeEditor
key={modifiedLanguage}
ref={editorRef}
className='mt-2'
composeId={id}

View File

@@ -7,7 +7,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { createSelector } from 'reselect';
import { addComposeLanguage, changeComposeLanguage, deleteComposeLanguage } from 'soapbox/actions/compose';
import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'soapbox/actions/compose';
import { Button, Icon, Input, Portal } from 'soapbox/components/ui';
import { type Language, languages as languagesObject } from 'soapbox/features/preferences';
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'soapbox/hooks';
@@ -66,7 +66,12 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
],
});
const { language, suggested_language: suggestedLanguage, textMap } = useCompose(composeId);
const {
language,
modified_language: modifiedLanguage,
suggested_language: suggestedLanguage,
textMap,
} = useCompose(composeId);
const handleClick: React.EventHandler<
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
@@ -87,8 +92,6 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
}
};
const handleChange = (language: Language | null) => dispatch(changeComposeLanguage(composeId, language));
const handleOptionKeyDown: React.KeyboardEventHandler = e => {
const value = e.currentTarget.getAttribute('data-index');
const index = results.findIndex(([key]) => key === value);
@@ -125,10 +128,17 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
const handleOptionClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
const value = (e.currentTarget as HTMLElement)?.getAttribute('data-index') as Language;
if (textMap.size) {
if (!(textMap.has(value) || language === value)) return;
dispatch(changeComposeModifiedLanguage(composeId, value));
} else {
dispatch(changeComposeLanguage(composeId, value));
}
e.preventDefault();
setIsOpen(false);
handleChange(value);
};
const handleAddLanguageClick: React.EventHandler<any> = (e: MouseEvent | KeyboardEvent) => {
@@ -288,7 +298,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
let buttonLabel = intl.formatMessage(messages.languagePrompt);
if (language) {
const list: string[] = [languagesObject[language]];
const list: string[] = [languagesObject[modifiedLanguage || language]];
if (textMap.size) list.push(intl.formatMessage(messages.multipleLanguages, {
count: textMap.size,
}));
@@ -347,6 +357,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
<div className='h-96 w-full overflow-scroll' ref={node} tabIndex={-1}>
{results.map(([code, name]) => {
const active = code === language;
const modified = code === modifiedLanguage;
return (
<div
@@ -357,15 +368,20 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
onKeyDown={handleOptionKeyDown}
onClick={handleOptionClick}
className={clsx(
'flex cursor-pointer gap-2 p-2.5 text-sm text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
{ 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active },
'flex gap-2 p-2.5 text-sm text-gray-700 dark:text-gray-400',
{
'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700': modified,
'cursor-pointer hover:bg-gray-100 black:hover:bg-gray-900 dark:hover:bg-gray-800': !textMap.size || textMap.has(code),
'cursor-pointer': active,
'cursor-default': !active && !(!textMap.size || textMap.has(code)),
},
)}
aria-selected={active}
ref={active ? focusedItem : null}
>
<div
className={clsx('flex-auto grow text-primary-600 dark:text-primary-400', {
'text-black dark:text-white': active,
'text-black dark:text-white': modified,
})}
>
{name}

View File

@@ -48,7 +48,7 @@ const Option: React.FC<IOption> = ({
const dispatch = useAppDispatch();
const intl = useIntl();
const suggestions = useCompose(composeId).suggestions;
const { suggestions } = useCompose(composeId);
const handleOptionTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => onChange(index, event.target.value);
@@ -112,11 +112,11 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
const intl = useIntl();
const { configuration } = useInstance();
const compose = useCompose(composeId);
const { poll, language, modified_language: modifiedLanguage } = useCompose(composeId);
const options = compose.poll?.options;
const expiresIn = compose.poll?.expires_in;
const isMultiple = compose.poll?.multiple;
const options = !modifiedLanguage || modifiedLanguage === language ? poll?.options : poll?.options_map.map((option, key) => option.get(modifiedLanguage, poll.options.get(key)!));
const expiresIn = poll?.expires_in;
const isMultiple = poll?.multiple;
const {
max_options: maxOptions,

View File

@@ -26,7 +26,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
}, ref) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const compose = useCompose(composeId);
const { language, modified_language, spoiler, spoiler_text: spoilerText, spoilerTextMap, suggestions } = useCompose(composeId);
const handleChangeSpoilerText: React.ChangeEventHandler<HTMLInputElement> = (e) => {
dispatch(changeComposeSpoilerText(composeId, e.target.value));
@@ -36,12 +36,14 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
dispatch(changeComposeSpoilerness(composeId));
};
const value = !modified_language || modified_language === language ? spoilerText : spoilerTextMap.get(modified_language, '');
return (
<Stack
space={4}
className={clsx({
'relative transition-height': true,
'hidden': !compose.spoiler,
'hidden': !spoiler,
})}
>
<Divider />
@@ -53,10 +55,10 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
<AutosuggestInput
placeholder={intl.formatMessage(messages.placeholder)}
value={compose.spoiler_text}
value={value}
onChange={handleChangeSpoilerText}
disabled={!compose.spoiler}
suggestions={compose.suggestions}
disabled={!spoiler}
suggestions={suggestions}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
onSuggestionSelected={onSuggestionSelected}

View File

@@ -90,7 +90,8 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
placeholder,
}, ref) => {
const dispatch = useAppDispatch();
const isWysiwyg = useCompose(composeId).content_type === 'wysiwyg';
const { content_type: contentType } = useCompose(composeId);
const isWysiwyg = contentType === 'wysiwyg';
const nodes = useNodes(isWysiwyg);
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
@@ -106,20 +107,28 @@ const ComposeEditor = React.forwardRef<LexicalEditor, IComposeEditor>(({
if (!compose) return;
if (compose.editorState) {
return compose.editorState;
const editorState = !compose.modified_language || compose.modified_language === compose.language
? compose.editorState
: compose.editorStateMap.get(compose.modified_language, '');
if (editorState) {
return editorState;
}
return () => {
const text = !compose.modified_language || compose.modified_language === compose.language
? compose.text
: compose.textMap.get(compose.modified_language, '');
if (isWysiwyg) {
$createRemarkImport({
handlers: {
image: importImage,
},
})(compose.text);
})(text);
} else {
const paragraph = $createParagraphNode();
const textNode = $createTextNode(compose.text);
const textNode = $createTextNode(text);
paragraph.append(textNode);