From 0900a0139ebeac3f66b185c9f4271c93b498eb16 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 7 Feb 2019 20:48:57 +0100 Subject: Spellcheck language autodetection First prototype is based on slack --- package-lock.json | 99 ++++++++++++++++++++++++--------------------- package.json | 1 + src/webview/contextMenu.js | 11 +++++ src/webview/recipe.js | 44 +++++++++++++++++++- src/webview/spellchecker.js | 11 +++++ 5 files changed, 119 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea3fc58c9..4b8cee089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -351,23 +351,23 @@ } }, "@babel/helpers": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.2.0.tgz", - "integrity": "sha512-Fr07N+ea0dMcMN8nFpuK6dUIT7/ivt9yKQdEEnjVS83tG2pHwPi03gYmk/tyuwONnZ+sY+GFFPlWGgCtW1hF9A==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.3.1.tgz", + "integrity": "sha512-Q82R3jKsVpUV99mgX50gOPCWwco9Ec5Iln/8Vyu4osNIOQgSrd9RFrQeUvmvddFNoLwMyOUWU+5ckioEKpDoGA==", "dev": true, "requires": { "@babel/template": "^7.1.2", "@babel/traverse": "^7.1.5", - "@babel/types": "^7.2.0" + "@babel/types": "^7.3.0" }, "dependencies": { "@babel/generator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.2.0.tgz", - "integrity": "sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.3.2.tgz", + "integrity": "sha512-f3QCuPppXxtZOEm5GWPra/uYUjmNQlu9pbAD8D/9jze4pTY83rTtB1igTBSwvkeNlC5gR24zFFkz+2WHLFQhqQ==", "dev": true, "requires": { - "@babel/types": "^7.2.0", + "@babel/types": "^7.3.2", "jsesc": "^2.5.1", "lodash": "^4.17.10", "source-map": "^0.5.0", @@ -375,32 +375,32 @@ } }, "@babel/parser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.2.0.tgz", - "integrity": "sha512-M74+GvK4hn1eejD9lZ7967qAwvqTZayQa3g10ag4s9uewgR7TKjeaT0YMyoq+gVfKYABiWZ4MQD701/t5e1Jhg==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.3.2.tgz", + "integrity": "sha512-QzNUC2RO1gadg+fs21fi0Uu0OuGNzRKEmgCxoLNzbCdoprLwjfmZwzUrpUNfJPaVRwBpDY47A17yYEGWyRelnQ==", "dev": true }, "@babel/traverse": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.6.tgz", - "integrity": "sha512-CXedit6GpISz3sC2k2FsGCUpOhUqKdyL0lqNrImQojagnUMXf8hex4AxYFRuMkNGcvJX5QAFGzB5WJQmSv8SiQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.2.3.tgz", + "integrity": "sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.1.6", + "@babel/generator": "^7.2.2", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.1.6", - "@babel/types": "^7.1.6", + "@babel/parser": "^7.2.3", + "@babel/types": "^7.2.2", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.10" } }, "@babel/types": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.2.0.tgz", - "integrity": "sha512-b4v7dyfApuKDvmPb+O488UlGuR1WbwMXFsO/cyqMrnfvRAChZKJAYeeglWTjUO1b9UghKKgepAQM5tsvBJca6A==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.3.2.tgz", + "integrity": "sha512-3Y6H8xlUlpbGR+XvawiH0UXehqydTmNmEpozWcXymqwcrwYAl5KMvKtQ+TF6f6E08V6Jur7v/ykdDSF+WDEIXQ==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -409,31 +409,19 @@ } }, "debug": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz", - "integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, - "globals": { - "version": "11.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.9.0.tgz", - "integrity": "sha512-5cJVtyXWH8PiJPVLZzzoIizXx944O4OmRro5MWKx5fT4MgcN7OfaMutPeaTdJCCURwbWdhhcCWcKIffPnmTzBg==", - "dev": true - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true } } }, @@ -1203,9 +1191,9 @@ } }, "@types/node": { - "version": "10.12.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", - "integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==", + "version": "10.12.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.21.tgz", + "integrity": "sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ==", "dev": true }, "JSONStream": { @@ -2487,6 +2475,16 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" }, + "cld3-asm": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cld3-asm/-/cld3-asm-1.0.1.tgz", + "integrity": "sha512-wuAqZd44Rk164TLKSYyLFZGSObhf82udOR+M/CnOkeEShq9+Tpxb/9RAE0m/KUVD1DnR5gMkZFeYExt51PCAbg==", + "requires": { + "emscripten-wasm-loader": "^1.0.0", + "tslib": "^1.9.0", + "utf8": "^3.0.0" + } + }, "cli-boxes": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", @@ -7841,9 +7839,9 @@ } }, "handlebars": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", - "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", + "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", "dev": true, "requires": { "async": "^2.5.0", @@ -13324,7 +13322,7 @@ "dependencies": { "pretty-bytes": { "version": "1.0.4", - "resolved": "http://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", "dev": true, "requires": { @@ -14035,7 +14033,7 @@ }, "readable-stream": { "version": "1.1.14", - "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", "dev": true, "requires": { @@ -14047,13 +14045,13 @@ }, "string_decoder": { "version": "0.10.31", - "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", "dev": true }, "through2": { "version": "0.2.3", - "resolved": "http://registry.npmjs.org/through2/-/through2-0.2.3.tgz", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", "dev": true, "requires": { @@ -15892,6 +15890,12 @@ "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=", "dev": true }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -16351,6 +16355,11 @@ "resolved": "https://registry.npmjs.org/user-home/-/user-home-1.1.1.tgz", "integrity": "sha1-K1viOjK2Onyd640PKNSFcko98ZA=" }, + "utf8": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/utf8/-/utf8-3.0.0.tgz", + "integrity": "sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==" + }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", diff --git a/package.json b/package.json index fba56eb3e..ba141e036 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "address-rfc2822": "^2.0.1", "auto-launch": "https://github.com/meetfranz/node-auto-launch.git", "classnames": "2.2.6", + "cld3-asm": "1.0.1", "debug-electron": "^0.0.4", "du": "^0.1.0", "electron-dl": "1.12.0", diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js index a76c03e5a..7fadfa72b 100644 --- a/src/webview/contextMenu.js +++ b/src/webview/contextMenu.js @@ -237,6 +237,17 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck type: 'separator', visible: defaultSpellcheckerLanguage !== spellcheckerLanguage, }, + { + id: 'automaticDetection', + label: 'Automatic language detection', + type: 'radio', + visible: defaultSpellcheckerLanguage !== spellcheckerLanguage, + checked: spellcheckerLanguage === 'automatic', + click() { + debug('Detect spellchecker language automatically'); + ipcRenderer.sendToHost('set-service-spellchecker-language', 'automatic'); + }, + }, ...spellcheckingLanguages], }); diff --git a/src/webview/recipe.js b/src/webview/recipe.js index c718b348e..38a65276e 100644 --- a/src/webview/recipe.js +++ b/src/webview/recipe.js @@ -1,10 +1,12 @@ import { ipcRenderer } from 'electron'; import path from 'path'; import { autorun, computed, observable } from 'mobx'; +import { loadModule } from 'cld3-asm'; +import { debounce } from 'lodash'; import RecipeWebview from './lib/RecipeWebview'; -import spellchecker, { switchDict, disable as disableSpellchecker } from './spellchecker'; +import spellchecker, { switchDict, disable as disableSpellchecker, getSpellcheckerLocaleByFuzzyIdentifier } from './spellchecker'; import { injectDarkModeStyle, isDarkModeStyleInjected, removeDarkModeStyle } from './darkmode'; import contextMenu from './contextMenu'; import './notifications'; @@ -60,6 +62,39 @@ class RecipeController { ); autorun(() => this.update()); + + console.log(JSON.parse(JSON.stringify(this.settings))); + + const cldFactory = await loadModule(); + const identifier = cldFactory.create(0, 1000); + + window.addEventListener('keyup', debounce((e) => { + const elem = e.target; + + let value = ''; + if (elem.isContentEditable) { + value = elem.textContent; + } else { + // + } + + // Force a minimum length to get better detection results + if (value.length < 30) return; + + debug('Detecting language for', value); + const findResult = identifier.findLanguage(value); + + debug('Language detection result', findResult); + + if (findResult.is_reliable) { + debug('Language detected reliably, setting spellchecker language to', findResult.language); + const spellcheckerLocale = getSpellcheckerLocaleByFuzzyIdentifier(findResult.language); + debug('reported back', spellcheckerLocale); + if (spellcheckerLocale) { + switchDict(spellcheckerLocale); + } + } + }, 200)); } loadRecipeModule(event, config, recipe) { @@ -87,7 +122,12 @@ class RecipeController { if (this.settings.app.enableSpellchecking) { debug('Setting spellchecker language to', this.spellcheckerLanguage); - switchDict(this.spellcheckerLanguage); + let { spellcheckerLanguage } = this; + if (spellcheckerLanguage === 'automatic') { + debug('Found `automatic` locale, falling back to user locale until detected', this.settings.app.locale); + spellcheckerLanguage = this.settings.app.locale; + } + switchDict(spellcheckerLanguage); } else { debug('Disable spellchecker'); disableSpellchecker(); diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js index becaed449..c711382be 100644 --- a/src/webview/spellchecker.js +++ b/src/webview/spellchecker.js @@ -3,6 +3,7 @@ import { SpellCheckerProvider } from 'electron-hunspell'; import path from 'path'; import { DICTIONARY_PATH } from '../config'; +import { SPELLCHECKER_LOCALES } from '../i18n/languages'; const debug = require('debug')('Franz:spellchecker'); @@ -82,3 +83,13 @@ export function disable() { currentDict = null; } } + +export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) { + const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key.split('-')[0] === identifier); + + if (locales.length >= 1) { + return locales[0]; + } + + return null; +} -- cgit v1.2.3-70-g09d2 From 75fc8f0867f222e1918c1ea430aeb26be9390fda Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 7 Feb 2019 20:49:22 +0100 Subject: improved fuzzy search --- src/webview/spellchecker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/webview/spellchecker.js b/src/webview/spellchecker.js index c711382be..9158b3b94 100644 --- a/src/webview/spellchecker.js +++ b/src/webview/spellchecker.js @@ -85,7 +85,7 @@ export function disable() { } export function getSpellcheckerLocaleByFuzzyIdentifier(identifier) { - const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key.split('-')[0] === identifier); + const locales = Object.keys(SPELLCHECKER_LOCALES).filter(key => key === identifier.toLowerCase() || key.split('-')[0] === identifier.toLowerCase()); if (locales.length >= 1) { return locales[0]; -- cgit v1.2.3-70-g09d2 From 3e0e220908f137344f423a80958ca8672fbf64c1 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 7 Feb 2019 20:51:48 +0100 Subject: set trigger threshhold to 40 characters --- src/webview/recipe.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/webview/recipe.js b/src/webview/recipe.js index 38a65276e..fa107ba1a 100644 --- a/src/webview/recipe.js +++ b/src/webview/recipe.js @@ -63,8 +63,6 @@ class RecipeController { autorun(() => this.update()); - console.log(JSON.parse(JSON.stringify(this.settings))); - const cldFactory = await loadModule(); const identifier = cldFactory.create(0, 1000); @@ -79,7 +77,7 @@ class RecipeController { } // Force a minimum length to get better detection results - if (value.length < 30) return; + if (value.length < 40) return; debug('Detecting language for', value); const findResult = identifier.findLanguage(value); -- cgit v1.2.3-70-g09d2 From b56673e93b162addf34f037a9db2b396ec2644b0 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Thu, 7 Feb 2019 20:51:53 +0100 Subject: Remove debugging output --- src/webview/contextMenu.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/webview/contextMenu.js b/src/webview/contextMenu.js index 7fadfa72b..75915851d 100644 --- a/src/webview/contextMenu.js +++ b/src/webview/contextMenu.js @@ -207,8 +207,6 @@ const buildMenuTpl = (props, suggestions, isSpellcheckEnabled, defaultSpellcheck }); }); - console.log('isSpellcheckEnabled', isSpellcheckEnabled); - menuTpl.push({ type: 'separator', }, { -- cgit v1.2.3-70-g09d2 From e2437f27ccd1c7646accc75b819fff3295c7a2e2 Mon Sep 17 00:00:00 2001 From: Stefan Malzner Date: Fri, 8 Feb 2019 11:32:20 +0100 Subject: feat(Spell checking): Add option to automatically detect language --- .../settings/services/EditServiceForm.js | 10 +-- src/containers/settings/EditServiceScreen.js | 40 +++++++----- src/containers/settings/EditSettingsScreen.js | 16 ++--- src/features/spellchecker/index.js | 8 ++- src/helpers/i18n-helpers.js | 27 +++++--- src/i18n/globalMessages.js | 16 +++++ src/i18n/locales/en-US.json | 4 ++ src/webview/contextMenu.js | 11 ++-- src/webview/recipe.js | 76 +++++++++++++--------- 9 files changed, 132 insertions(+), 76 deletions(-) diff --git a/src/components/settings/services/EditServiceForm.js b/src/components/settings/services/EditServiceForm.js index 468d85c45..21616b5de 100644 --- a/src/components/settings/services/EditServiceForm.js +++ b/src/components/settings/services/EditServiceForm.js @@ -128,7 +128,8 @@ export default @observer class EditServiceForm extends Component { isSaving: PropTypes.bool.isRequired, isDeleting: PropTypes.bool.isRequired, isProxyFeatureEnabled: PropTypes.bool.isRequired, - isProxyFeaturePremiumFeature: PropTypes.bool.isRequired, + isProxyPremiumFeature: PropTypes.bool.isRequired, + isSpellcheckerPremiumFeature: PropTypes.bool.isRequired, }; static defaultProps = { @@ -191,7 +192,8 @@ export default @observer class EditServiceForm extends Component { isDeleting, onDelete, isProxyFeatureEnabled, - isProxyFeaturePremiumFeature, + isProxyPremiumFeature, + isSpellcheckerPremiumFeature, } = this.props; const { intl } = this.context; @@ -339,14 +341,14 @@ export default @observer class EditServiceForm extends Component { - +