From 89e66da351aa82eba896d8bf2380e07ea2490810 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Mon, 6 Nov 2023 13:19:23 -0800 Subject: [PATCH] Init Language Picker --- _locales/en/messages.json | 36 +++ app/locale.ts | 11 +- app/main.ts | 28 ++- images/icons/v3/globe/globe.svg | 1 + stylesheets/components/Modal.scss | 10 +- stylesheets/components/Preferences.scss | 116 ++++++++++ ts/background.ts | 2 +- ts/components/Modal.tsx | 73 +++--- ts/components/Preferences.stories.tsx | 5 + ts/components/Preferences.tsx | 292 +++++++++++++++++++++++- ts/main/settingsChannel.ts | 4 + ts/shims/deleteAllData.ts | 2 +- ts/test-node/app/locale_test.ts | 1 + ts/test-node/app/menu_test.ts | 1 + ts/types/RendererConfig.ts | 1 + ts/types/Storage.d.ts | 1 + ts/util/createIPCEvents.ts | 7 + ts/window.d.ts | 1 - ts/windows/context.ts | 2 + ts/windows/main/phase1-ipc.ts | 4 - ts/windows/minimalContext.ts | 3 +- ts/windows/preload.ts | 1 + ts/windows/settings/app.tsx | 10 + ts/windows/settings/preload.ts | 16 ++ yarn.lock | 43 +++- 25 files changed, 610 insertions(+), 61 deletions(-) create mode 100644 images/icons/v3/globe/globe.svg diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 416d56c59087..06be4c71a6eb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5823,6 +5823,42 @@ "messageformat": "You will no longer be able to share or view stories. Story updates you have recently shared will also be deleted.", "description": "Confirmation modal body for disabling stories" }, + "icu:Preferences__Language__Label": { + "messageformat": "Language", + "description": "Language setting label" + }, + "icu:Preferences__Language__ModalTitle": { + "messageformat": "Language", + "description": "Language setting modal title" + }, + "icu:Preferences__Language__SystemLanguage": { + "messageformat": "System Language", + "description": "Option for system language" + }, + "icu:Preferences__Language__SearchLanguages": { + "messageformat": "Search languages", + "description": "Placholder for language preference search box" + }, + "icu:Preferences__Language__NoResults": { + "messageformat": "No results for “{searchTerm}”", + "description": "When no results are found for language preference search" + }, + "icu:Preferences__LanguageModal__Set": { + "messageformat": "Set", + "description": "Button to set language preference" + }, + "icu:Preferences__LanguageModal__Restart__Title": { + "messageformat": "Restart Signal to apply", + "description": "Title for restart Signal modal to apply language changes" + }, + "icu:Preferences__LanguageModal__Restart__Description": { + "messageformat": "To change the language, the app needs to restart.", + "description": "Description for restart Signal modal to apply language changes" + }, + "icu:Preferences__LanguageModal__Restart__Button": { + "messageformat": "Restart", + "description": "Button to restart Signal to apply language changes" + }, "icu:DialogUpdate--version-available": { "messageformat": "Update to version {version} available", "description": "Tooltip for new update available" diff --git a/app/locale.ts b/app/locale.ts index 59697ea5b0d1..8c14ee49cc68 100644 --- a/app/locale.ts +++ b/app/locale.ts @@ -26,6 +26,7 @@ function getLocaleMessages(locale: string): LocaleMessagesType { export type LocaleDirection = 'ltr' | 'rtl'; export type LocaleType = { + availableLocales: Array; i18n: LocalizerType; name: string; direction: LocaleDirection; @@ -65,6 +66,7 @@ function getLocaleDirection( } function finalize( + availableLocales: Array, messages: LocaleMessagesType, backupMessages: LocaleMessagesType, localeName: string, @@ -80,6 +82,7 @@ function finalize( logger.info(`locale: Text info direction for ${localeName}: ${direction}`); return { + availableLocales, i18n, name: localeName, direction, @@ -99,10 +102,12 @@ export function _getAvailableLocales(): Array { export function load({ preferredSystemLocales, + localeOverride, hourCyclePreference, logger, }: { preferredSystemLocales: Array; + localeOverride: string | null; hourCyclePreference: HourCyclePreference; logger: LoggerType; }): LocaleType { @@ -117,10 +122,11 @@ export function load({ const availableLocales = _getAvailableLocales(); logger.info('locale: Supported locales:', availableLocales.join(', ')); - logger.info('locale: Preferred locales: ', preferredSystemLocales.join(', ')); + logger.info('locale: Preferred locales:', preferredSystemLocales.join(', ')); + logger.info('locale: Locale Override:', localeOverride); const matchedLocale = LocaleMatcher.match( - preferredSystemLocales, + localeOverride != null ? [localeOverride] : preferredSystemLocales, availableLocales, 'en', { algorithm: 'best fit' } @@ -132,6 +138,7 @@ export function load({ const englishMessages = getLocaleMessages('en'); return finalize( + availableLocales, matchedLocaleMessages, englishMessages, matchedLocale, diff --git a/app/main.ts b/app/main.ts index bd5141cdd2de..f64eb288e3d9 100644 --- a/app/main.ts +++ b/app/main.ts @@ -359,6 +359,26 @@ async function getBackgroundColor( throw missingCaseError(theme); } +async function getLocaleOverrideSetting(): Promise { + const fastValue = ephemeralConfig.get('localeOverride'); + // eslint-disable-next-line eqeqeq -- Checking for null explicitly + if (typeof fastValue === 'string' || fastValue === null) { + getLogger().info('got fast localeOverride setting', fastValue); + return fastValue; + } + + const json = await sql.sqlCall('getItemById', 'localeOverride'); + + // Default to `null` if setting doesn't exist yet + const slowValue = typeof json?.value === 'string' ? json.value : null; + + ephemeralConfig.set('localeOverride', slowValue); + + getLogger().info('got slow localeOverride setting', slowValue); + + return slowValue; +} + let systemTrayService: SystemTrayService | undefined; const systemTraySettingCache = new SystemTraySettingCache( sql, @@ -1782,11 +1802,15 @@ app.on('ready', async () => { // Write buffered information into newly created logger. consoleLogger.writeBufferInto(logger); + sqlInitPromise = initializeSQL(userDataPath); + if (!resolvedTranslationsLocale) { preferredSystemLocales = resolveCanonicalLocales( loadPreferredSystemLocales() ); + const localeOverride = await getLocaleOverrideSetting(); + const hourCyclePreference = getHourCyclePreference(); logger.info(`app.ready: hour cycle preference: ${hourCyclePreference}`); @@ -1797,13 +1821,12 @@ app.on('ready', async () => { ); resolvedTranslationsLocale = loadLocale({ preferredSystemLocales, + localeOverride, hourCyclePreference, logger: getLogger(), }); } - sqlInitPromise = initializeSQL(userDataPath); - // First run: configure Signal to minimize to tray. Additionally, on Windows // enable auto-start with start-in-tray so that starting from a Desktop icon // would still show the window. @@ -2372,6 +2395,7 @@ ipc.on('get-config', async event => { const parsed = rendererConfigSchema.safeParse({ name: packageJson.productName, + availableLocales: getResolvedMessagesLocale().availableLocales, resolvedTranslationsLocale: getResolvedMessagesLocale().name, resolvedTranslationsLocaleDirection: getResolvedMessagesLocale().direction, hourCyclePreference: getResolvedMessagesLocale().hourCyclePreference, diff --git a/images/icons/v3/globe/globe.svg b/images/icons/v3/globe/globe.svg new file mode 100644 index 000000000000..e67b012d6d55 --- /dev/null +++ b/images/icons/v3/globe/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/stylesheets/components/Modal.scss b/stylesheets/components/Modal.scss index e427cba2a97e..e3c3a9955b73 100644 --- a/stylesheets/components/Modal.scss +++ b/stylesheets/components/Modal.scss @@ -21,15 +21,17 @@ } &__header { + &--with-back-button .module-Modal__title { + text-align: center; + } + } + + &__headerTitle { align-items: center; display: flex; justify-content: space-between; padding-block: 16px 1em; padding-inline: 16px; - - &--with-back-button .module-Modal__title { - text-align: center; - } } &__title { diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss index 19cf2577a6dc..9367d4806c9c 100644 --- a/stylesheets/components/Preferences.scss +++ b/stylesheets/components/Preferences.scss @@ -194,6 +194,13 @@ padding-block: 4px; padding-inline: 24px; + &--icon { + width: 20px; + height: 20px; + flex-shrink: 0; + margin-inline-end: 12px; + } + &--key { flex-grow: 1; padding-inline-end: 20px; @@ -292,3 +299,112 @@ } } } + +.Preferences__LanguageIcon { + @include light-theme { + @include color-svg('../images/icons/v3/globe/globe.svg', $color-gray-75); + } + @include dark-theme { + @include color-svg('../images/icons/v3/globe/globe.svg', $color-gray-15); + } +} + +.Preferences__LanguageButton { + text-transform: capitalize; + @include button-reset; +} + +.Preferences__LanguageModal { + height: 560px; + .module-Modal__body { + flex-grow: 1; + } +} + +.Preferences__LanguageModal__Title { + @include font-body-1-bold; + margin-inline: 8px; +} + +.Preferences__LanguageModal__NoResults { + @include font-body-1; + margin: 16px; + text-align: center; +} + +.Preferences__LanguageModal__Item { + @include button-reset; + width: 100%; + padding-block: 2px; + padding-inline: 8px; + &:hover { + .Preferences__LanguageModal__Item__Inner { + @include light-theme { + background-color: $color-black-alpha-06; + } + @include dark-theme { + background-color: $color-white-alpha-06; + } + } + } + &:focus { + outline: none; + .Preferences__LanguageModal__Item__Inner { + @include keyboard-mode { + background-color: $color-black-alpha-06; + box-shadow: 0 0 0 2px $color-ultramarine; + } + @include dark-keyboard-mode { + background-color: $color-white-alpha-06; + } + } + } +} + +.Preferences__LanguageModal__Item__Inner { + display: flex; + gap: 8px; + align-items: center; + justify-content: center; + padding-block: 5px; + padding-inline: 16px; + border-radius: 8px; +} + +.Preferences__LanguageModal__Item__Label { + flex-grow: 1; +} + +.Preferences__LanguageModal__Item__Current { + display: block; + text-transform: capitalize; + @include font-body-1; +} + +.Preferences__LanguageModal__Item__Check { + display: flex; + height: 20px; + width: 20px; + align-items: center; + justify-content: center; + background: $color-ultramarine; + @include rounded-corners; + &::after { + @include color-svg('../images/icons/v3/check/check.svg', $color-white); + content: ''; + height: 14px; + width: 14px; + } +} + +.Preferences__LanguageModal__Item__Matching { + display: block; + text-transform: capitalize; + @include font-body-2; + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } +} diff --git a/ts/background.ts b/ts/background.ts index 769577c72b14..5b943fc29936 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -972,7 +972,7 @@ export async function startApp(): Promise { // This one should always be last - it could restart the app if (window.isBeforeVersion(lastVersion, 'v5.30.0-alpha')) { await deleteAllLogs(); - window.IPC.restart(); + window.SignalContext.restartApp(); return; } } diff --git a/ts/components/Modal.tsx b/ts/components/Modal.tsx index f8c3794181b2..6271836138b0 100644 --- a/ts/components/Modal.tsx +++ b/ts/components/Modal.tsx @@ -30,6 +30,7 @@ type PropsType = { hasFooterDivider?: boolean; i18n: LocalizerType; modalFooter?: JSX.Element; + modalHeaderChildren?: ReactNode; moduleClassName?: string; onBackButtonClick?: () => unknown; onClose?: () => void; @@ -52,6 +53,7 @@ export function Modal({ hasXButton, i18n, modalFooter, + modalHeaderChildren, moduleClassName, noMouseClose, onBackButtonClick, @@ -122,6 +124,7 @@ export function Modal({ hasXButton={hasXButton} i18n={i18n} modalFooter={modalFooter} + modalHeaderChildren={modalHeaderChildren} moduleClassName={moduleClassName} onBackButtonClick={onBackButtonClick} onClose={close} @@ -162,6 +165,7 @@ export function ModalPage({ hasXButton, i18n, modalFooter, + modalHeaderChildren, moduleClassName, onBackButtonClick, onClose, @@ -179,7 +183,9 @@ export function ModalPage({ const [scrolledToBottom, setScrolledToBottom] = useState(false); const [hasOverflow, setHasOverflow] = useState(false); - const hasHeader = Boolean(hasXButton || title || onBackButtonClick); + const hasHeader = Boolean( + hasXButton || title || modalHeaderChildren || onBackButtonClick + ); const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName); useScrollObserver(bodyRef, bodyInnerRef, scroll => { @@ -216,37 +222,40 @@ export function ModalPage({ : null )} > - {onBackButtonClick && ( - + + + } + > + {localeSearchResults.length === 0 && ( +
+ {i18n('icu:Preferences__Language__NoResults', { + searchTerm: languageSearchInput.trim(), + })} +
+ )} + {localeSearchResults.map(option => { + const id = `${languageId}:${option.locale ?? 'system'}`; + const isSelected = option.locale === selectedLanguageLocale; + return ( + + ); + })} + + )} + {languageDialog === LanguageDialog.Confirmation && ( + { + onLocaleChange(selectedLanguageLocale); + }, + }, + ]} + > + {i18n('icu:Preferences__LanguageModal__Restart__Description')} + + )} + {i18n('icu:Preferences--theme')} @@ -532,6 +806,7 @@ export function Preferences({ } /> { setPage(Page.ChatColor); @@ -548,6 +823,7 @@ export function Preferences({ } /> {i18n('icu:Preferences--zoom')} @@ -1307,6 +1583,7 @@ export function Preferences({ > {i18n('icu:Preferences__button--notifications')} +