This commit is contained in:
Thomas Steiner 2025-07-11 10:29:05 -07:00 committed by GitHub
commit ec386eba6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 122 additions and 26 deletions

View File

@ -15,7 +15,6 @@ import { missingAltTextModal } from 'mastodon/initial_state';
import AutosuggestInput from 'mastodon/components/autosuggest_input'; import AutosuggestInput from 'mastodon/components/autosuggest_input';
import AutosuggestTextarea from 'mastodon/components/autosuggest_textarea'; import AutosuggestTextarea from 'mastodon/components/autosuggest_textarea';
import { Button } from 'mastodon/components/button'; import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container'; import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
@ -310,7 +309,7 @@ class ComposeForm extends ImmutablePureComponent {
> >
{intl.formatMessage( {intl.formatMessage(
this.props.isEditing ? this.props.isEditing ?
messages.saveChanges : messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish) (this.props.isInReply ? messages.reply : messages.publish)
)} )}
</Button> </Button>

View File

@ -20,7 +20,7 @@ import { languages as preloadedLanguages } from 'mastodon/initial_state';
import type { RootState } from 'mastodon/store'; import type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { debouncedGuess } from '../util/language_detection'; import { debouncedGuess, countLetters } from '../util/language_detection';
const messages = defineMessages({ const messages = defineMessages({
changeLanguage: { changeLanguage: {
@ -375,12 +375,25 @@ export const LanguageDropdown: React.FC = () => {
); );
useEffect(() => { useEffect(() => {
if (text.length > 20) { let canceled = false;
debouncedGuess(text, setGuess);
if (countLetters(text) >= 5) {
debouncedGuess(text)
.then((lang) => {
if (!canceled) {
setGuess(lang ?? '');
}
})
.catch(() => {
setGuess('');
});
} else { } else {
debouncedGuess.cancel();
setGuess(''); setGuess('');
} }
return () => {
canceled = true;
};
}, [text, setGuess]); }, [text, setGuess]);
return ( return (

View File

@ -0,0 +1,5 @@
export declare const debouncedGuess: (
text: string,
) => Promise<string | undefined>;
export declare const countLetters: (text: string) => number;

View File

@ -1,7 +1,5 @@
import lande from 'lande'; const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis;
import { debounce } from 'lodash'; let languageDetectorSupportedAndReady = languageDetectorInGlobalThis && await globalThis.LanguageDetector.availability() === 'available';
import { urlRegex } from './url_regex';
const ISO_639_MAP = { const ISO_639_MAP = {
afr: 'af', // Afrikaans afr: 'af', // Afrikaans
@ -56,21 +54,23 @@ const ISO_639_MAP = {
vie: 'vi', // Vietnamese vie: 'vi', // Vietnamese
}; };
const guessLanguage = (text) => { const countLetters = (text) => {
text = text const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
.replace(urlRegex, '') const letters = [...segmenter.segment(text)]
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, ''); return letters.length
if (text.length > 20) {
const [lang, confidence] = lande(text)[0];
if (confidence > 0.8)
return ISO_639_MAP[lang];
}
return '';
}; };
export const debouncedGuess = debounce((text, setGuess) => { let module;
setGuess(guessLanguage(text)); // If the API is supported, but the model not loaded yet…
}, 500, { maxWait: 1500, leading: true, trailing: true }); if (languageDetectorInGlobalThis) {
if (!languageDetectorSupportedAndReady) {
// …trigger the model download
globalThis.LanguageDetector.create();
}
module = await import('./language_detection_with_languagedetector');
} else {
module = await import('./language_detection_with_laude');
}
const debouncedGuess = module.debouncedGuess;
export { debouncedGuess, countLetters, ISO_639_MAP };

View File

@ -0,0 +1,41 @@
import { debounce } from 'lodash';
import { urlRegex } from './url_regex';
const guessLanguage = async (text) => {
text = text
.replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '');
try {
const languageDetector = await globalThis.LanguageDetector.create();
let {detectedLanguage, confidence} = (await languageDetector.detect(text))[0];
if (confidence > 0.8) {
detectedLanguage = detectedLanguage.split('-')[0];
return detectedLanguage;
}
} catch {
return '';
}
return '';
};
const debouncedGuess = (() => {
let resolver = null;
const debounced = debounce((text) => {
const result = guessLanguage(text);
if (resolver) {
resolver(result);
resolver = null;
}
}, 500, { maxWait: 1500, leading: true, trailing: true });
return (text) => new Promise((resolve) => {
resolver = resolve;
debounced(text);
});
})();
export { debouncedGuess };

View File

@ -0,0 +1,38 @@
import lande from 'lande';
import { debounce } from 'lodash';
import { countLetters, ISO_639_MAP } from './language_detection';
import { urlRegex } from './url_regex';
const guessLanguage = (text) => {
text = text
.replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '');
if (countLetters(text) > 20) {
const [lang, confidence] = lande(text)[0];
if (confidence > 0.8)
return ISO_639_MAP[lang];
}
return '';
};
const debouncedGuess = (() => {
let resolver = null;
const debounced = debounce((text) => {
const result = guessLanguage(text);
if (resolver) {
resolver(result);
resolver = null;
}
}, 500, { maxWait: 1500, leading: true, trailing: true });
return (text) => new Promise((resolve) => {
resolver = resolve;
debounced(text);
});
})();
export { debouncedGuess };