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"