[MM-33112] Add support for multiple custom spellchecking languages (#1743)

* [MM-33112] Add support for multiple custom spellchecking languages

* Styles and other formatting

* Type and lint fixes

* Update wording
This commit is contained in:
Devin Binnie 2021-09-20 14:10:01 -04:00 committed by GitHub
parent c3963424f1
commit 6dc84b3e5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 732 additions and 174 deletions

493
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -73,6 +73,7 @@
"@types/react": "^17.0.11",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.8",
"@types/react-select": "^4.0.17",
"@types/underscore": "^1.11.2",
"@types/valid-url": "^1.0.3",
"@types/winreg": "^1.2.30",
@ -138,6 +139,7 @@
"react-beautiful-dnd": "^13.1.0",
"react-bootstrap": "^1.6.1",
"react-dom": "^16.14.0",
"react-select": "4.3.1",
"react-transition-group": "^2.5.0",
"sass": "^1.35.1",
"semver": "^5.5.0",

View file

@ -98,3 +98,5 @@ export const SEND_DROPDOWN_MENU_SIZE = 'send-dropdown-menu-size';
export const BROWSER_HISTORY_PUSH = 'browser-history-push';
export const APP_LOGGED_IN = 'app-logged-in';
export const GET_AVAILABLE_SPELL_CHECKER_LANGUAGES = 'get-available-spell-checker-languages';

View file

@ -31,7 +31,7 @@ const defaultPreferences: ConfigV3 = {
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
spellCheckerLocales: [],
darkMode: false,
downloadLocation: getDefaultDownloadLocation(),
};

View file

@ -235,8 +235,8 @@ export default class Config extends EventEmitter {
return this.combinedData?.spellCheckerURL;
}
get spellCheckerLocale() {
return this.combinedData?.spellCheckerLocale ?? defaultPreferences.spellCheckerLocale;
get spellCheckerLocales() {
return this.combinedData?.spellCheckerLocales ?? defaultPreferences.spellCheckerLocales;
}
get showTrayIcon() {
return this.combinedData?.showTrayIcon ?? defaultPreferences.showTrayIcon;

View file

@ -43,6 +43,7 @@ function upgradeV2toV3(configV2: ConfigV2) {
};
});
config.lastActiveTeam = 0;
config.spellCheckerLocales = [];
return config;
}

View file

@ -1,5 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable quote-props */
export const PRODUCTION = 'production';
export const DEVELOPMENT = 'development';
@ -21,3 +22,234 @@ export const DEFAULT_WINDOW_WIDTH = 1000;
export const DEFAULT_WINDOW_HEIGHT = 700;
export const MINIMUM_WINDOW_WIDTH = 700;
export const MINIMUM_WINDOW_HEIGHT = 240;
export const localeTranslations: Record<string, string> = {
'af': 'Afrikaans',
'af-ZA': 'Afrikaans (South Africa)',
'ar': 'Arabic',
'ar-AE': 'Arabic (U.A.E.)',
'ar-BH': 'Arabic (Bahrain)',
'ar-DZ': 'Arabic (Algeria)',
'ar-EG': 'Arabic (Egypt)',
'ar-IQ': 'Arabic (Iraq)',
'ar-JO': 'Arabic (Jordan)',
'ar-KW': 'Arabic (Kuwait)',
'ar-LB': 'Arabic (Lebanon)',
'ar-LY': 'Arabic (Libya)',
'ar-MA': 'Arabic (Morocco)',
'ar-OM': 'Arabic (Oman)',
'ar-QA': 'Arabic (Qatar)',
'ar-SA': 'Arabic (Saudi Arabia)',
'ar-SY': 'Arabic (Syria)',
'ar-TN': 'Arabic (Tunisia)',
'ar-YE': 'Arabic (Yemen)',
'az': 'Azeri (Latin)',
'az-AZ': 'Azeri (Azerbaijan)',
'be': 'Belarusian',
'be-BY': 'Belarusian (Belarus)',
'bg': 'Bulgarian',
'bg-BG': 'Bulgarian (Bulgaria)',
'bs-BA': 'Bosnian (Bosnia and Herzegovina)',
'ca': 'Catalan',
'ca-ES': 'Catalan (Spain)',
'cs': 'Czech',
'cs-CZ': 'Czech (Czech Republic)',
'cy': 'Welsh',
'cy-GB': 'Welsh (United Kingdom)',
'da': 'Danish',
'da-DK': 'Danish (Denmark)',
'de': 'German',
'de-AT': 'German (Austria)',
'de-CH': 'German (Switzerland)',
'de-DE': 'German (Germany)',
'de-LI': 'German (Liechtenstein)',
'de-LU': 'German (Luxembourg)',
'dv': 'Divehi',
'dv-MV': 'Divehi (Maldives)',
'el': 'Greek',
'el-GR': 'Greek (Greece)',
'en': 'English',
'en-AU': 'English (Australia)',
'en-BZ': 'English (Belize)',
'en-CA': 'English (Canada)',
'en-CB': 'English (Caribbean)',
'en-GB': 'English (United Kingdom)',
'en-IE': 'English (Ireland)',
'en-JM': 'English (Jamaica)',
'en-NZ': 'English (New Zealand)',
'en-PH': 'English (Republic of the Philippines)',
'en-TT': 'English (Trinidad and Tobago)',
'en-US': 'English (United States)',
'en-ZA': 'English (South Africa)',
'en-ZW': 'English (Zimbabwe)',
'eo': 'Esperanto',
'es': 'Spanish',
'es-AR': 'Spanish (Argentina)',
'es-BO': 'Spanish (Bolivia)',
'es-CL': 'Spanish (Chile)',
'es-CO': 'Spanish (Colombia)',
'es-CR': 'Spanish (Costa Rica)',
'es-DO': 'Spanish (Dominican Republic)',
'es-EC': 'Spanish (Ecuador)',
'es-ES': 'Spanish (Spain)',
'es-GT': 'Spanish (Guatemala)',
'es-HN': 'Spanish (Honduras)',
'es-MX': 'Spanish (Mexico)',
'es-NI': 'Spanish (Nicaragua)',
'es-PA': 'Spanish (Panama)',
'es-PE': 'Spanish (Peru)',
'es-PR': 'Spanish (Puerto Rico)',
'es-PY': 'Spanish (Paraguay)',
'es-SV': 'Spanish (El Salvador)',
'es-UY': 'Spanish (Uruguay)',
'es-VE': 'Spanish (Venezuela)',
'et': 'Estonian',
'et-EE': 'Estonian (Estonia)',
'eu': 'Basque',
'eu-ES': 'Basque (Spain)',
'fa': 'Farsi',
'fa-IR': 'Farsi (Iran)',
'fi': 'Finnish',
'fi-FI': 'Finnish (Finland)',
'fo': 'Faroese',
'fo-FO': 'Faroese (Faroe Islands)',
'fr': 'French',
'fr-BE': 'French (Belgium)',
'fr-CA': 'French (Canada)',
'fr-CH': 'French (Switzerland)',
'fr-FR': 'French (France)',
'fr-LU': 'French (Luxembourg)',
'fr-MC': 'French (Principality of Monaco)',
'gl': 'Galician',
'gl-ES': 'Galician (Spain)',
'gu': 'Gujarati',
'gu-IN': 'Gujarati (India)',
'he': 'Hebrew',
'he-IL': 'Hebrew (Israel)',
'hi': 'Hindi',
'hi-IN': 'Hindi (India)',
'hr': 'Croatian',
'hr-BA': 'Croatian (Bosnia and Herzegovina)',
'hr-HR': 'Croatian (Croatia)',
'hu': 'Hungarian',
'hu-HU': 'Hungarian (Hungary)',
'hy': 'Armenian',
'hy-AM': 'Armenian (Armenia)',
'id': 'Indonesian',
'id-ID': 'Indonesian (Indonesia)',
'is': 'Icelandic',
'is-IS': 'Icelandic (Iceland)',
'it': 'Italian',
'it-CH': 'Italian (Switzerland)',
'it-IT': 'Italian (Italy)',
'ja': 'Japanese',
'ja-JP': 'Japanese (Japan)',
'ka': 'Georgian',
'ka-GE': 'Georgian (Georgia)',
'kk': 'Kazakh',
'kk-KZ': 'Kazakh (Kazakhstan)',
'kn': 'Kannada',
'kn-IN': 'Kannada (India)',
'ko': 'Korean',
'ko-KR': 'Korean (Korea)',
'kok': 'Konkani',
'kok-IN': 'Konkani (India)',
'ky': 'Kyrgyz',
'ky-KG': 'Kyrgyz (Kyrgyzstan)',
'lt': 'Lithuanian',
'lt-LT': 'Lithuanian (Lithuania)',
'lv': 'Latvian',
'lv-LV': 'Latvian (Latvia)',
'mi': 'Maori',
'mi-NZ': 'Maori (New Zealand)',
'mk': 'FYRO Macedonian',
'mk-MK': 'FYRO Macedonian (Former Yugoslav Republic of Macedonia)',
'mn': 'Mongolian',
'mn-MN': 'Mongolian (Mongolia)',
'mr': 'Marathi',
'mr-IN': 'Marathi (India)',
'ms': 'Malay',
'ms-BN': 'Malay (Brunei Darussalam)',
'ms-MY': 'Malay (Malaysia)',
'mt': 'Maltese',
'mt-MT': 'Maltese (Malta)',
'nb': 'Norwegian (Bokm?l)',
'nb-NO': 'Norwegian (Bokm?l) (Norway)',
'nl': 'Dutch',
'nl-BE': 'Dutch (Belgium)',
'nl-NL': 'Dutch (Netherlands)',
'nn-NO': 'Norwegian (Nynorsk) (Norway)',
'ns': 'Northern Sotho',
'ns-ZA': 'Northern Sotho (South Africa)',
'pa': 'Punjabi',
'pa-IN': 'Punjabi (India)',
'pl': 'Polish',
'pl-PL': 'Polish (Poland)',
'ps': 'Pashto',
'ps-AR': 'Pashto (Afghanistan)',
'pt': 'Portuguese',
'pt-BR': 'Portuguese (Brazil)',
'pt-PT': 'Portuguese (Portugal)',
'qu': 'Quechua',
'qu-BO': 'Quechua (Bolivia)',
'qu-EC': 'Quechua (Ecuador)',
'qu-PE': 'Quechua (Peru)',
'ro': 'Romanian',
'ro-RO': 'Romanian (Romania)',
'ru': 'Russian',
'ru-RU': 'Russian (Russia)',
'sa': 'Sanskrit',
'sa-IN': 'Sanskrit (India)',
'se': 'Sami (Northern)',
'se-FI': 'Sami (Finland)',
'se-NO': 'Sami (Norway)',
'se-SE': 'Sami (Sweden)',
'sk': 'Slovak',
'sk-SK': 'Slovak (Slovakia)',
'sl': 'Slovenian',
'sl-SI': 'Slovenian (Slovenia)',
'sq': 'Albanian',
'sq-AL': 'Albanian (Albania)',
'sr-BA': 'Serbian (Bosnia and Herzegovina)',
'sr-SP': 'Serbian (Serbia and Montenegro)',
'sv': 'Swedish',
'sv-FI': 'Swedish (Finland)',
'sv-SE': 'Swedish (Sweden)',
'sw': 'Swahili',
'sw-KE': 'Swahili (Kenya)',
'syr': 'Syriac',
'syr-SY': 'Syriac (Syria)',
'ta': 'Tamil',
'ta-IN': 'Tamil (India)',
'te': 'Telugu',
'te-IN': 'Telugu (India)',
'th': 'Thai',
'th-TH': 'Thai (Thailand)',
'tl': 'Tagalog',
'tl-PH': 'Tagalog (Philippines)',
'tn': 'Tswana',
'tn-ZA': 'Tswana (South Africa)',
'tr': 'Turkish',
'tr-TR': 'Turkish (Turkey)',
'tt': 'Tatar',
'tt-RU': 'Tatar (Russia)',
'ts': 'Tsonga',
'uk': 'Ukrainian',
'uk-UA': 'Ukrainian (Ukraine)',
'ur': 'Urdu',
'ur-PK': 'Urdu (Islamic Republic of Pakistan)',
'uz': 'Uzbek (Latin)',
'uz-UZ': 'Uzbek (Uzbekistan)',
'vi': 'Vietnamese',
'vi-VN': 'Vietnamese (Viet Nam)',
'xh': 'Xhosa',
'xh-ZA': 'Xhosa (South Africa)',
'zh': 'Chinese',
'zh-CN': 'Chinese (S)',
'zh-HK': 'Chinese (Hong Kong)',
'zh-MO': 'Chinese (Macau)',
'zh-SG': 'Chinese (Singapore)',
'zh-TW': 'Chinese (T)',
'zu': 'Zulu',
'zu-ZA': 'Zulu (South Africa)',
};

View file

@ -88,7 +88,7 @@ const configDataSchemaV2 = Joi.object<ConfigV2>({
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
spellCheckerLocale: Joi.string().default('en-US'),
spellCheckerURL: Joi.string().allow(null),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
@ -119,7 +119,7 @@ const configDataSchemaV3 = Joi.object<ConfigV3>({
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
spellCheckerLocales: Joi.array().items(Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/)).default([]),
spellCheckerURL: Joi.string().allow(null),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),

View file

@ -43,6 +43,7 @@ import {
UPDATE_SHORTCUT_MENU,
OPEN_TEAMS_DROPDOWN,
UPDATE_LAST_ACTIVE,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
} from 'common/communication';
import Config from 'common/config';
import {MattermostServer} from 'common/servers/MattermostServer';
@ -265,6 +266,7 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, handleGetAvailableSpellCheckerLanguages);
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
}
@ -289,6 +291,7 @@ function handleConfigUpdate(newConfig: CombinedConfig) {
authManager.handleConfigUpdate(newConfig);
}
setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge);
updateSpellCheckerLocales();
}
ipcMain.emit('update-menu', true, config);
@ -639,6 +642,12 @@ function handleRemoveServerModal(e: IpcMainEvent, name: string) {
}
}
function updateSpellCheckerLocales() {
if (config.data?.spellCheckerLocales.length && app.isReady()) {
session.defaultSession.setSpellCheckerLanguages(config.data?.spellCheckerLocales);
}
}
function initializeAfterAppReady() {
updateServerInfos(config.teams);
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
@ -662,6 +671,7 @@ function initializeAfterAppReady() {
log.info(`Dictionary definitions downloaded successfully for ${lang}`);
});
}
updateSpellCheckerLocales();
}
const appVersionJson = path.join(app.getPath('userData'), 'app-state.json');
@ -975,3 +985,6 @@ function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewNam
config.set('lastActiveTeam', teams.find((team) => team.name === serverName)?.order || 0);
}
function handleGetAvailableSpellCheckerLanguages() {
return session.defaultSession.availableSpellCheckerLanguages;
}

View file

@ -8,13 +8,23 @@ import 'renderer/css/settings.css';
import React from 'react';
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button} from 'react-bootstrap';
import ReactSelect, {ActionMeta, OptionsType} from 'react-select';
import {debounce} from 'underscore';
import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
import {CombinedConfig, LocalConfiguration} from 'types/config';
import {DeepPartial} from 'types/utils';
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
import {localeTranslations} from 'common/utils/constants';
import {
GET_LOCAL_CONFIGURATION,
UPDATE_CONFIGURATION,
DOUBLE_CLICK_ON_WINDOW,
GET_DOWNLOAD_LOCATION,
RELOAD_CONFIGURATION,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
} from 'common/communication';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
@ -26,13 +36,11 @@ type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
type State = DeepPartial<CombinedConfig> & {
ready: boolean;
maximized?: boolean;
teams?: Team[];
showAddTeamForm: boolean;
trayWasVisible?: boolean;
firstRun?: boolean;
savingState: SavingStateItems;
userOpenedDownloadDialog: boolean;
allowSaveSpellCheckerURL: boolean;
availableLanguages: Array<{label: string; value: string}>;
}
type SavingStateItems = {
@ -61,18 +69,19 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
saveQueue: SaveQueueItem[];
selectedSpellCheckerLocales: Array<{label: string; value: string}>;
constructor(props: Record<string, never>) {
super(props);
this.state = {
ready: false,
teams: [],
showAddTeamForm: false,
savingState: {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
},
userOpenedDownloadDialog: false,
allowSaveSpellCheckerURL: false,
availableLanguages: [],
};
this.getConfig();
@ -89,19 +98,20 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
this.spellCheckerURLRef = React.createRef();
this.saveQueue = [];
this.selectedSpellCheckerLocales = [];
}
componentDidMount() {
window.ipcRenderer.on(ADD_SERVER, () => {
this.setState({
showAddTeamForm: true,
});
});
window.ipcRenderer.on(RELOAD_CONFIGURATION, () => {
this.updateSaveState();
this.getConfig();
});
window.ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES).then((languages: string[]) => {
const availableLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
this.setState({availableLanguages});
});
}
getConfig = () => {
@ -112,16 +122,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
convertConfigDataToState = (configData: Partial<LocalConfiguration>, currentState: Partial<State> = {}) => {
const newState = Object.assign({} as State, configData);
newState.showAddTeamForm = currentState.showAddTeamForm || false;
newState.trayWasVisible = currentState.trayWasVisible || false;
if (newState.teams?.length === 0 && currentState.firstRun !== false) {
newState.firstRun = false;
newState.showAddTeamForm = true;
}
newState.savingState = currentState.savingState || {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
};
this.selectedSpellCheckerLocales = configData.spellCheckerLocales?.map((language: string) => ({label: localeTranslations[language] || language, value: language})) || [];
return newState;
}
@ -172,17 +178,6 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
}
}, 2000);
handleTeamsChange = (teams: Team[]) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
showAddTeamForm: false,
teams,
});
if (teams.length === 0) {
this.setState({showAddTeamForm: true});
}
}
handleChangeShowTrayIcon = () => {
const shouldShowTrayIcon = this.showTrayIconRef.current?.checked;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
@ -220,19 +215,6 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
});
}
toggleShowTeamForm = () => {
this.setState({
showAddTeamForm: !this.state.showAddTeamForm,
});
(document.activeElement as HTMLElement).blur();
}
setShowTeamFormVisibility = (val: boolean) => {
this.setState({
showAddTeamForm: val,
});
}
handleFlashWindow = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
@ -295,6 +277,22 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
});
}
handleChangeSpellCheckerLocales = (value: OptionsType<{label: string; value: string}>, actionMeta: ActionMeta<{label: string; value: string}>) => {
switch (actionMeta.action) {
case 'select-option':
this.selectedSpellCheckerLocales = [...value];
break;
case 'remove-value':
this.selectedSpellCheckerLocales = this.selectedSpellCheckerLocales.filter((language) => language.value !== actionMeta.removedValue.value);
}
const values = this.selectedSpellCheckerLocales.map((language) => language.value);
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'spellCheckerLocales', data: values});
this.setState({
spellCheckerLocales: values,
});
}
handleChangeEnableHardwareAcceleration = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: this.enableHardwareAccelerationRef.current?.checked});
this.setState({
@ -426,6 +424,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
}
options.push(
<>
<FormCheck>
<FormCheck.Input
type='checkbox'
@ -437,10 +436,24 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
/>
{'Check spelling'}
<FormText>
{'Highlight misspelled words in your messages based on your system language configuration. '}
{'Highlight misspelled words in your messages based on your system language or language preference. '}
{'Setting takes effect after restarting the app.'}
</FormText>
</FormCheck>);
</FormCheck>
{this.state.useSpellChecker &&
<ReactSelect
className='SettingsPage__spellCheckerLocalesDropdown'
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
options={this.state.availableLanguages}
isMulti={true}
isClearable={false}
onChange={this.handleChangeSpellCheckerLocales}
value={this.selectedSpellCheckerLocales}
placeholder={'Select preferred language(s)'}
/>
}
</>,
);
if (process.platform !== 'darwin') {
if (this.state.spellCheckerURL === null || typeof this.state.spellCheckerURL === 'undefined') {
options.push(

View file

@ -3,3 +3,19 @@
.TeamListItem:hover {
background: #242a30;
}
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control {
background: #242a30;
}
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__menu {
background: #242a30;
}
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option {
background: #242a30;
}
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option:hover {
background: rgba(255, 255, 255, 0.16);
}

View file

@ -35,3 +35,9 @@ body {
background-color: #166de0;
border-color: #166de0;
}
.SettingsPage__spellCheckerLocalesDropdown {
margin-top: 8px;
margin-left: 16px;
width: 50%;
}

View file

@ -34,7 +34,7 @@ export type ConfigV3 = {
useSpellChecker: boolean;
enableHardwareAcceleration: boolean;
autostart: boolean;
spellCheckerLocale: string;
spellCheckerLocales: string[];
darkMode: boolean;
downloadLocation: string;
spellCheckerURL?: string;