From a4055cec401a2a666a811a5d052281f052eec22c Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Fri, 31 Mar 2023 15:36:14 -0700 Subject: [PATCH] Match multiple locales for spellchecker --- ACKNOWLEDGMENTS.md | 4 +++ app/main.ts | 6 +++- app/spell_check.ts | 54 ++++++++++++++++++---------- package.json | 1 + ts/test-node/app/spell_check_test.ts | 38 +++++++++++++------- 5 files changed, 72 insertions(+), 31 deletions(-) diff --git a/ACKNOWLEDGMENTS.md b/ACKNOWLEDGMENTS.md index d8dfb927a..948df5c47 100644 --- a/ACKNOWLEDGMENTS.md +++ b/ACKNOWLEDGMENTS.md @@ -9,6 +9,10 @@ Signal Desktop makes use of the following open source projects. License: MIT +## @formatjs/intl-localematcher + + License: MIT + ## @indutny/frameless-titlebar MIT License diff --git a/app/main.ts b/app/main.ts index 56e0379ca..301fa3e85 100644 --- a/app/main.ts +++ b/app/main.ts @@ -800,7 +800,11 @@ async function createWindow() { } mainWindowCreated = true; - setupSpellChecker(mainWindow, getResolvedMessagesLocale()); + setupSpellChecker( + mainWindow, + getPreferredSystemLocales(), + getResolvedMessagesLocale().i18n + ); if (!startInTray && windowConfig && windowConfig.maximized) { mainWindow.maximize(); } diff --git a/app/spell_check.ts b/app/spell_check.ts index ae045d24b..0820287c3 100644 --- a/app/spell_check.ts +++ b/app/spell_check.ts @@ -3,44 +3,62 @@ import type { BrowserWindow } from 'electron'; import { Menu, clipboard, nativeImage } from 'electron'; -import { uniq } from 'lodash'; import { fileURLToPath } from 'url'; +import * as LocaleMatcher from '@formatjs/intl-localematcher'; import { maybeParseUrl } from '../ts/util/url'; -import type { LocaleType } from './locale'; import type { MenuListType } from '../ts/types/menu'; +import type { LocalizerType } from '../ts/types/Util'; export function getLanguages( - userLocale: string, - availableLocales: ReadonlyArray + preferredSystemLocales: ReadonlyArray, + availableLocales: ReadonlyArray, + defaultLocale: string ): Array { - // First attempt to find the exact locale - const candidateLocales = uniq([userLocale, userLocale]).filter(l => - availableLocales.includes(l) - ); - if (candidateLocales.length > 0) { - return candidateLocales; + const matchedLocales = []; + + preferredSystemLocales.forEach(preferredSystemLocale => { + if (preferredSystemLocale === defaultLocale) { + matchedLocales.push(defaultLocale); + return; + } + const matchedLocale = LocaleMatcher.match( + [preferredSystemLocale], + availableLocales as Array, // bad types + defaultLocale, + { algorithm: 'best fit' } + ); + if (matchedLocale !== defaultLocale) { + matchedLocales.push(matchedLocale); + } + }); + + if (matchedLocales.length === 0) { + matchedLocales.push(defaultLocale); } - // If no languages were found then return all locales that start with the base - const baseLocale = userLocale.split('-')[0]; - return uniq(availableLocales.filter(l => l.startsWith(baseLocale))); + return matchedLocales; } export const setup = ( browserWindow: BrowserWindow, - { name: userLocale, i18n }: LocaleType + preferredSystemLocales: ReadonlyArray, + i18n: LocalizerType ): void => { const { session } = browserWindow.webContents; const availableLocales = session.availableSpellCheckerLanguages; - const languages = getLanguages(userLocale, availableLocales); - console.log(`spellcheck: user locale: ${userLocale}`); + const languages = getLanguages( + preferredSystemLocales, + availableLocales, + 'en' + ); + console.log('spellcheck: user locales:', preferredSystemLocales); console.log( - 'spellcheck: available spellchecker languages: ', + 'spellcheck: available spellchecker languages:', availableLocales ); - console.log('spellcheck: setting languages to: ', languages); + console.log('spellcheck: setting languages to:', languages); session.setSpellCheckerLanguages(languages); browserWindow.webContents.on('context-menu', (_event, params) => { diff --git a/package.json b/package.json index 2ef2fb311..a3cd39ee0 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ }, "dependencies": { "@formatjs/fast-memoize": "1.2.6", + "@formatjs/intl-localematcher": "0.2.32", "@indutny/frameless-titlebar": "2.3.5", "@indutny/sneequals": "4.0.0", "@popperjs/core": "2.11.6", diff --git a/ts/test-node/app/spell_check_test.ts b/ts/test-node/app/spell_check_test.ts index 0a0aff7d2..7e7f49204 100644 --- a/ts/test-node/app/spell_check_test.ts +++ b/ts/test-node/app/spell_check_test.ts @@ -8,34 +8,48 @@ import { getLanguages } from '../../../app/spell_check'; describe('SpellCheck', () => { describe('getLanguages', () => { it('works with locale and base available', () => { - assert.deepEqual(getLanguages('en-US', ['en-US', 'en-CA', 'en']), [ + assert.deepEqual(getLanguages(['en-US'], ['en-US', 'en'], 'en'), [ 'en-US', ]); }); - it('works with neither locale nor base available', () => { - assert.deepEqual(getLanguages('en-US', ['en-NZ', 'en-CA']), [ - 'en-NZ', - 'en-CA', + it('uses icu likely subtags rules to match languages', () => { + assert.deepEqual(getLanguages(['fa-FR'], ['fa-IR'], 'en'), ['fa-IR']); + assert.deepEqual(getLanguages(['zh'], ['zh-Hans-CN'], 'en'), [ + 'zh-Hans-CN', ]); + assert.deepEqual( + getLanguages(['zh-HK'], ['zh-Hans-CN', 'zh-Hant-HK'], 'en'), + ['zh-Hant-HK'] + ); + }); + + it('matches multiple locales', () => { + assert.deepEqual( + getLanguages(['fr-FR', 'es'], ['fr', 'es-ES', 'en-US'], 'en'), + ['fr', 'es-ES'] + ); }); it('works with only base locale available', () => { - assert.deepEqual(getLanguages('en-US', ['en', 'en-CA']), ['en', 'en-CA']); + assert.deepEqual(getLanguages(['en-US'], ['en'], 'en'), ['en']); }); it('works with only full locale available', () => { - assert.deepEqual(getLanguages('en-US', ['en-CA', 'en-US']), ['en-US']); + assert.deepEqual(getLanguages(['en-US'], ['en-CA', 'en-US'], 'en'), [ + 'en-US', + ]); }); it('works with base provided and base available', () => { - assert.deepEqual(getLanguages('en', ['en-CA', 'en-US', 'en']), ['en']); + assert.deepEqual(getLanguages(['en'], ['en-CA', 'en-US', 'en'], 'en'), [ + 'en', + ]); }); - it('works with base provided and base not available', () => { - assert.deepEqual(getLanguages('en', ['en-CA', 'en-US']), [ - 'en-CA', - 'en-US', + it('falls back to default', () => { + assert.deepEqual(getLanguages(['fa-IR'], ['es-ES', 'fr-FR'], 'en'), [ + 'en', ]); }); });