diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx
index b164a07cbd..cb4662dfc9 100644
--- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx
+++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx
@@ -238,6 +238,7 @@ class LanguageDropdown extends PureComponent {
static propTypes = {
value: PropTypes.string,
frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string),
+ guess: PropTypes.string,
intl: PropTypes.object.isRequired,
onChange: PropTypes.func,
};
@@ -281,7 +282,7 @@ class LanguageDropdown extends PureComponent {
};
render () {
- const { value, intl, frequentlyUsedLanguages } = this.props;
+ const { value, guess, intl, frequentlyUsedLanguages } = this.props;
const { open, placement } = this.state;
const current = preloadedLanguages.find(lang => lang[0] === value) ?? [];
@@ -294,7 +295,7 @@ class LanguageDropdown extends PureComponent {
onClick={this.handleToggle}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
- className={classNames('dropdown-button', { active: open })}
+ className={classNames('dropdown-button', { active: open, warning: guess !== '' && guess !== value })}
>
{current[2] ?? value}
diff --git a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js b/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
index 64c90afa92..43907c7b49 100644
--- a/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
+++ b/app/javascript/mastodon/features/compose/containers/language_dropdown_container.js
@@ -2,6 +2,8 @@ import { createSelector } from '@reduxjs/toolkit';
import { Map as ImmutableMap } from 'immutable';
import { connect } from 'react-redux';
+import lande from 'lande';
+import { debounce } from 'lodash';
import { changeComposeLanguage } from 'mastodon/actions/compose';
@@ -16,9 +18,80 @@ const getFrequentlyUsedLanguages = createSelector([
.toArray()
));
+const ISO_639_MAP = {
+ afr: 'af', // Afrikaans
+ ara: 'ar', // Arabic
+ aze: 'az', // Azerbaijani
+ bel: 'be', // Belarusian
+ ben: 'bn', // Bengali
+ bul: 'bg', // Bulgarian
+ cat: 'ca', // Catalan
+ ces: 'cs', // Czech
+ ckb: 'ku', // Kurdish
+ cmn: 'zh', // Mandarin
+ dan: 'da', // Danish
+ deu: 'de', // German
+ ell: 'el', // Greek
+ eng: 'en', // English
+ est: 'et', // Estonian
+ eus: 'eu', // Basque
+ fin: 'fi', // Finnish
+ fra: 'fr', // French
+ hau: 'ha', // Hausa
+ heb: 'he', // Hebrew
+ hin: 'hi', // Hindi
+ hrv: 'hr', // Croatian
+ hun: 'hu', // Hungarian
+ hye: 'hy', // Armenian
+ ind: 'id', // Indonesian
+ isl: 'is', // Icelandic
+ ita: 'it', // Italian
+ jpn: 'ja', // Japanese
+ kat: 'ka', // Georgian
+ kaz: 'kk', // Kazakh
+ kor: 'ko', // Korean
+ lit: 'lt', // Lithuanian
+ mar: 'mr', // Marathi
+ mkd: 'mk', // Macedonian
+ nld: 'nl', // Dutch
+ nob: 'no', // Norwegian
+ pes: 'fa', // Persian
+ pol: 'pl', // Polish
+ por: 'pt', // Portuguese
+ ron: 'ro', // Romanian
+ run: 'rn', // Rundi
+ rus: 'ru', // Russian
+ slk: 'sk', // Slovak
+ spa: 'es', // Spanish
+ srp: 'sr', // Serbian
+ swe: 'sv', // Swedish
+ tgl: 'tl', // Tagalog
+ tur: 'tr', // Turkish
+ ukr: 'uk', // Ukrainian
+ vie: 'vi', // Vietnamese
+};
+
+const debouncedLande = debounce((text) => lande(text), 500, { trailing: true });
+
+const detectedLanguage = createSelector([
+ state => state.getIn(['compose', 'text']),
+], text => {
+ if (text.length > 20) {
+ const guesses = debouncedLande(text);
+ const [lang, confidence] = guesses[0];
+
+ if (confidence > 0.8) {
+ return ISO_639_MAP[lang];
+ }
+ }
+
+ return '';
+});
+
const mapStateToProps = state => ({
frequentlyUsedLanguages: getFrequentlyUsedLanguages(state),
value: state.getIn(['compose', 'language']),
+ guess: detectedLanguage(state),
});
const mapDispatchToProps = dispatch => ({
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 2c6efb71b4..ee1861ff5d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -935,6 +935,16 @@ body > [data-popper-placement] {
border-color: $ui-highlight-color;
color: $primary-text-color;
}
+
+ &.warning {
+ border-color: var(--goldenrod-2);
+ color: var(--goldenrod-2);
+
+ &.active {
+ background-color: var(--goldenrod-2);
+ color: var(--indigo-1);
+ }
+ }
}
.character-counter {
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index 902b823e1f..f1b53c3606 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -4,7 +4,7 @@ const { env, settings } = require('../configuration');
// Those modules contain modern ES code that need to be transpiled for Webpack to process it
const nodeModulesToProcess = [
- '@reduxjs', 'fuzzysort'
+ '@reduxjs', 'fuzzysort', 'toygrad'
];
module.exports = {
diff --git a/package.json b/package.json
index 4eb95d4389..d9c0e81601 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
"imports-loader": "^1.2.0",
"intl-messageformat": "^10.3.5",
"js-yaml": "^4.1.0",
+ "lande": "^1.0.10",
"lodash": "^4.17.21",
"mark-loader": "^0.1.6",
"marky": "^1.2.5",
diff --git a/yarn.lock b/yarn.lock
index 20d00347b8..f0d63c1ea9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2930,6 +2930,7 @@ __metadata:
jest: "npm:^29.5.0"
jest-environment-jsdom: "npm:^29.5.0"
js-yaml: "npm:^4.1.0"
+ lande: "npm:^1.0.10"
lint-staged: "npm:^15.0.0"
lodash: "npm:^4.17.21"
mark-loader: "npm:^0.1.6"
@@ -11322,6 +11323,15 @@ __metadata:
languageName: node
linkType: hard
+"lande@npm:^1.0.10":
+ version: 1.0.10
+ resolution: "lande@npm:1.0.10"
+ dependencies:
+ toygrad: "npm:^2.6.0"
+ checksum: 10c0/27300be5937b6b9e245a7ea7a8216a0dcf5286a3b7ae38886c10c5c75b83fbfa1a69cd6754ab26bb38c6bd18aa8a2dcb62dea873506accb245cf82084acfee71
+ languageName: node
+ linkType: hard
+
"language-subtag-registry@npm:^0.3.20":
version: 0.3.22
resolution: "language-subtag-registry@npm:0.3.22"
@@ -17127,6 +17137,13 @@ __metadata:
languageName: node
linkType: hard
+"toygrad@npm:^2.6.0":
+ version: 2.6.0
+ resolution: "toygrad@npm:2.6.0"
+ checksum: 10c0/96e42ced87431e99cec7d9b446c7827fe7782c2fd82bb5fc8c4a0855679011d809f9967096a60b4c8ceca867a29f1aadd62af447bdb652cb6f7fee279ae743ed
+ languageName: node
+ linkType: hard
+
"tr46@npm:^1.0.1":
version: 1.0.1
resolution: "tr46@npm:1.0.1"