Client-side language detection
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -27,6 +27,7 @@ const languages = Object.entries(languagesObject) as Array<[Language, string]>;
|
||||
|
||||
const messages = defineMessages({
|
||||
languagePrompt: { id: 'compose.language_dropdown.prompt', defaultMessage: 'Select language' },
|
||||
languageSuggestion: { id: 'compose.language_dropdown.suggestion', defaultMessage: '{language} (detected)' },
|
||||
search: { id: 'compose.language_dropdown.search', defaultMessage: 'Search language…' },
|
||||
});
|
||||
|
||||
@ -61,7 +62,7 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
],
|
||||
});
|
||||
|
||||
const language = useCompose(composeId).language;
|
||||
const { language, suggested_language: suggestedLanguage } = useCompose(composeId);
|
||||
|
||||
const handleClick: React.EventHandler<
|
||||
React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLButtonElement>
|
||||
@ -253,12 +254,18 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
|
||||
const isSearching = searchValue !== '';
|
||||
const results = search();
|
||||
|
||||
let buttonLabel = intl.formatMessage(messages.languagePrompt);
|
||||
if (language) buttonLabel = languagesObject[language];
|
||||
else if (suggestedLanguage) buttonLabel = intl.formatMessage(messages.languageSuggestion, {
|
||||
language: languagesObject[suggestedLanguage as Language] || suggestedLanguage,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
text={language ? languagesObject[language] : intl.formatMessage(messages.languagePrompt)}
|
||||
text={buttonLabel}
|
||||
icon={require('@tabler/icons/outline/language.svg')}
|
||||
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
title={intl.formatMessage(messages.languagePrompt)}
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { $createRemarkExport } from '@mkljczk/lexical-remark';
|
||||
import { type LanguageIdentificationModel } from 'fasttext.wasm.js/dist/models/language-identification/common.js';
|
||||
import { $getRoot } from 'lexical';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { addSuggestedQuote, setEditorState } from 'soapbox/actions/compose';
|
||||
import { addSuggestedLanguage, addSuggestedQuote, setEditorState } from 'soapbox/actions/compose';
|
||||
import { fetchStatus } from 'soapbox/actions/statuses';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
import { getStatusIdsFromLinksInContent } from 'soapbox/utils/status';
|
||||
|
||||
let lidModel: LanguageIdentificationModel;
|
||||
|
||||
interface IStatePlugin {
|
||||
composeId: string;
|
||||
isWysiwyg?: boolean;
|
||||
@ -50,9 +53,35 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
||||
});
|
||||
}, 2000), []);
|
||||
|
||||
const detectLanguage = useCallback(debounce(async (text: string) => {
|
||||
dispatch(async (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const compose = state.compose.get(composeId);
|
||||
|
||||
if (!features.postLanguages || features.languageDetection || compose?.language) return;
|
||||
|
||||
const wordsLength = text.split(/\s+/).length;
|
||||
|
||||
if (wordsLength < 4) return;
|
||||
|
||||
if (!lidModel) {
|
||||
// eslint-disable-next-line import/extensions
|
||||
const { getLIDModel } = await import('fasttext.wasm.js/common');
|
||||
lidModel = await getLIDModel();
|
||||
}
|
||||
if (!lidModel.model) await lidModel.load();
|
||||
const { alpha2, possibility } = await lidModel.identify(text.replace(/\s+/i, ' '));
|
||||
|
||||
if (alpha2 && possibility > 0.5) {
|
||||
dispatch(addSuggestedLanguage(composeId, alpha2));
|
||||
}
|
||||
});
|
||||
}, 750), []);
|
||||
|
||||
useEffect(() => {
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
let text;
|
||||
const plainText = editorState.read(() => $getRoot().getTextContent());
|
||||
let text = plainText;
|
||||
if (isWysiwyg) {
|
||||
text = editorState.read($createRemarkExport({
|
||||
handlers: {
|
||||
@ -60,13 +89,12 @@ const StatePlugin: React.FC<IStatePlugin> = ({ composeId, isWysiwyg }) => {
|
||||
mention: (node) => ({ type: 'text', value: node.getTextContent() }),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
text = editorState.read(() => $getRoot().getTextContent());
|
||||
}
|
||||
const isEmpty = text === '';
|
||||
const data = isEmpty ? null : JSON.stringify(editorState.toJSON());
|
||||
dispatch(setEditorState(composeId, data, text));
|
||||
getQuoteSuggestions(text);
|
||||
getQuoteSuggestions(plainText);
|
||||
detectLanguage(plainText);
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user