Init Language Picker

This commit is contained in:
Jamie Kyle 2023-11-06 13:19:23 -08:00 committed by GitHub
parent 754bb02c06
commit 89e66da351
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 610 additions and 61 deletions

View file

@ -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.", "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" "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": { "icu:DialogUpdate--version-available": {
"messageformat": "Update to version {version} available", "messageformat": "Update to version {version} available",
"description": "Tooltip for new update available" "description": "Tooltip for new update available"

View file

@ -26,6 +26,7 @@ function getLocaleMessages(locale: string): LocaleMessagesType {
export type LocaleDirection = 'ltr' | 'rtl'; export type LocaleDirection = 'ltr' | 'rtl';
export type LocaleType = { export type LocaleType = {
availableLocales: Array<string>;
i18n: LocalizerType; i18n: LocalizerType;
name: string; name: string;
direction: LocaleDirection; direction: LocaleDirection;
@ -65,6 +66,7 @@ function getLocaleDirection(
} }
function finalize( function finalize(
availableLocales: Array<string>,
messages: LocaleMessagesType, messages: LocaleMessagesType,
backupMessages: LocaleMessagesType, backupMessages: LocaleMessagesType,
localeName: string, localeName: string,
@ -80,6 +82,7 @@ function finalize(
logger.info(`locale: Text info direction for ${localeName}: ${direction}`); logger.info(`locale: Text info direction for ${localeName}: ${direction}`);
return { return {
availableLocales,
i18n, i18n,
name: localeName, name: localeName,
direction, direction,
@ -99,10 +102,12 @@ export function _getAvailableLocales(): Array<string> {
export function load({ export function load({
preferredSystemLocales, preferredSystemLocales,
localeOverride,
hourCyclePreference, hourCyclePreference,
logger, logger,
}: { }: {
preferredSystemLocales: Array<string>; preferredSystemLocales: Array<string>;
localeOverride: string | null;
hourCyclePreference: HourCyclePreference; hourCyclePreference: HourCyclePreference;
logger: LoggerType; logger: LoggerType;
}): LocaleType { }): LocaleType {
@ -117,10 +122,11 @@ export function load({
const availableLocales = _getAvailableLocales(); const availableLocales = _getAvailableLocales();
logger.info('locale: Supported locales:', availableLocales.join(', ')); 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( const matchedLocale = LocaleMatcher.match(
preferredSystemLocales, localeOverride != null ? [localeOverride] : preferredSystemLocales,
availableLocales, availableLocales,
'en', 'en',
{ algorithm: 'best fit' } { algorithm: 'best fit' }
@ -132,6 +138,7 @@ export function load({
const englishMessages = getLocaleMessages('en'); const englishMessages = getLocaleMessages('en');
return finalize( return finalize(
availableLocales,
matchedLocaleMessages, matchedLocaleMessages,
englishMessages, englishMessages,
matchedLocale, matchedLocale,

View file

@ -359,6 +359,26 @@ async function getBackgroundColor(
throw missingCaseError(theme); throw missingCaseError(theme);
} }
async function getLocaleOverrideSetting(): Promise<string | null> {
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; let systemTrayService: SystemTrayService | undefined;
const systemTraySettingCache = new SystemTraySettingCache( const systemTraySettingCache = new SystemTraySettingCache(
sql, sql,
@ -1782,11 +1802,15 @@ app.on('ready', async () => {
// Write buffered information into newly created logger. // Write buffered information into newly created logger.
consoleLogger.writeBufferInto(logger); consoleLogger.writeBufferInto(logger);
sqlInitPromise = initializeSQL(userDataPath);
if (!resolvedTranslationsLocale) { if (!resolvedTranslationsLocale) {
preferredSystemLocales = resolveCanonicalLocales( preferredSystemLocales = resolveCanonicalLocales(
loadPreferredSystemLocales() loadPreferredSystemLocales()
); );
const localeOverride = await getLocaleOverrideSetting();
const hourCyclePreference = getHourCyclePreference(); const hourCyclePreference = getHourCyclePreference();
logger.info(`app.ready: hour cycle preference: ${hourCyclePreference}`); logger.info(`app.ready: hour cycle preference: ${hourCyclePreference}`);
@ -1797,13 +1821,12 @@ app.on('ready', async () => {
); );
resolvedTranslationsLocale = loadLocale({ resolvedTranslationsLocale = loadLocale({
preferredSystemLocales, preferredSystemLocales,
localeOverride,
hourCyclePreference, hourCyclePreference,
logger: getLogger(), logger: getLogger(),
}); });
} }
sqlInitPromise = initializeSQL(userDataPath);
// First run: configure Signal to minimize to tray. Additionally, on Windows // 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 // enable auto-start with start-in-tray so that starting from a Desktop icon
// would still show the window. // would still show the window.
@ -2372,6 +2395,7 @@ ipc.on('get-config', async event => {
const parsed = rendererConfigSchema.safeParse({ const parsed = rendererConfigSchema.safeParse({
name: packageJson.productName, name: packageJson.productName,
availableLocales: getResolvedMessagesLocale().availableLocales,
resolvedTranslationsLocale: getResolvedMessagesLocale().name, resolvedTranslationsLocale: getResolvedMessagesLocale().name,
resolvedTranslationsLocaleDirection: getResolvedMessagesLocale().direction, resolvedTranslationsLocaleDirection: getResolvedMessagesLocale().direction,
hourCyclePreference: getResolvedMessagesLocale().hourCyclePreference, hourCyclePreference: getResolvedMessagesLocale().hourCyclePreference,

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="none"><g clip-path="url(#a)"><path fill="#000" fill-rule="evenodd" d="M10 19.063A9.062 9.062 0 1 0 10 .938a9.062 9.062 0 0 0 0 18.125Zm0-16.667c-.148 0-.376.077-.67.41-.294.33-.592.851-.857 1.557-.235.627-.433 1.37-.58 2.2h4.215a12.632 12.632 0 0 0-.581-2.2c-.265-.706-.563-1.227-.856-1.558-.295-.332-.523-.41-.671-.41Zm2.304 5.625H7.696c-.06.63-.092 1.293-.092 1.979 0 .686.032 1.349.092 1.98h4.608c.06-.631.092-1.294.092-1.98 0-.686-.032-1.349-.092-1.98Zm1.464 3.958a22.564 22.564 0 0 0 0-3.958h3.576c.17.63.26 1.294.26 1.979s-.09 1.348-.26 1.98h-3.576Zm-1.66 1.459H7.893c.147.829.345 1.572.58 2.198.265.707.563 1.227.856 1.559.295.332.523.41.671.41.148 0 .376-.078.67-.41.294-.332.592-.852.857-1.559.235-.626.433-1.37.58-2.198Zm.304 3.775c.178-.325.337-.683.48-1.064a14.4 14.4 0 0 0 .695-2.712h3.198a7.628 7.628 0 0 1-4.373 3.777Zm0-14.427a7.627 7.627 0 0 1 4.373 3.776h-3.198a14.4 14.4 0 0 0-.695-2.71 8.425 8.425 0 0 0-.48-1.065ZM7.587 17.215a8.437 8.437 0 0 1-.48-1.065 14.404 14.404 0 0 1-.694-2.712H3.215a7.627 7.627 0 0 0 4.372 3.777ZM6.232 11.98a22.572 22.572 0 0 1 0-3.958H2.656A7.614 7.614 0 0 0 2.396 10c0 .685.09 1.348.26 1.98h3.576Zm.181-5.416c.165-1.01.4-1.928.695-2.712a8.2 8.2 0 0 1 .48-1.064 7.627 7.627 0 0 0-4.373 3.776h3.198Z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h20v20H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -21,15 +21,17 @@
} }
&__header { &__header {
&--with-back-button .module-Modal__title {
text-align: center;
}
}
&__headerTitle {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-block: 16px 1em; padding-block: 16px 1em;
padding-inline: 16px; padding-inline: 16px;
&--with-back-button .module-Modal__title {
text-align: center;
}
} }
&__title { &__title {

View file

@ -194,6 +194,13 @@
padding-block: 4px; padding-block: 4px;
padding-inline: 24px; padding-inline: 24px;
&--icon {
width: 20px;
height: 20px;
flex-shrink: 0;
margin-inline-end: 12px;
}
&--key { &--key {
flex-grow: 1; flex-grow: 1;
padding-inline-end: 20px; 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;
}
}

View file

@ -972,7 +972,7 @@ export async function startApp(): Promise<void> {
// This one should always be last - it could restart the app // This one should always be last - it could restart the app
if (window.isBeforeVersion(lastVersion, 'v5.30.0-alpha')) { if (window.isBeforeVersion(lastVersion, 'v5.30.0-alpha')) {
await deleteAllLogs(); await deleteAllLogs();
window.IPC.restart(); window.SignalContext.restartApp();
return; return;
} }
} }

View file

@ -30,6 +30,7 @@ type PropsType = {
hasFooterDivider?: boolean; hasFooterDivider?: boolean;
i18n: LocalizerType; i18n: LocalizerType;
modalFooter?: JSX.Element; modalFooter?: JSX.Element;
modalHeaderChildren?: ReactNode;
moduleClassName?: string; moduleClassName?: string;
onBackButtonClick?: () => unknown; onBackButtonClick?: () => unknown;
onClose?: () => void; onClose?: () => void;
@ -52,6 +53,7 @@ export function Modal({
hasXButton, hasXButton,
i18n, i18n,
modalFooter, modalFooter,
modalHeaderChildren,
moduleClassName, moduleClassName,
noMouseClose, noMouseClose,
onBackButtonClick, onBackButtonClick,
@ -122,6 +124,7 @@ export function Modal({
hasXButton={hasXButton} hasXButton={hasXButton}
i18n={i18n} i18n={i18n}
modalFooter={modalFooter} modalFooter={modalFooter}
modalHeaderChildren={modalHeaderChildren}
moduleClassName={moduleClassName} moduleClassName={moduleClassName}
onBackButtonClick={onBackButtonClick} onBackButtonClick={onBackButtonClick}
onClose={close} onClose={close}
@ -162,6 +165,7 @@ export function ModalPage({
hasXButton, hasXButton,
i18n, i18n,
modalFooter, modalFooter,
modalHeaderChildren,
moduleClassName, moduleClassName,
onBackButtonClick, onBackButtonClick,
onClose, onClose,
@ -179,7 +183,9 @@ export function ModalPage({
const [scrolledToBottom, setScrolledToBottom] = useState(false); const [scrolledToBottom, setScrolledToBottom] = useState(false);
const [hasOverflow, setHasOverflow] = 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); const getClassName = getClassNamesFor(BASE_CLASS_NAME, moduleClassName);
useScrollObserver(bodyRef, bodyInnerRef, scroll => { useScrollObserver(bodyRef, bodyInnerRef, scroll => {
@ -216,37 +222,40 @@ export function ModalPage({
: null : null
)} )}
> >
{onBackButtonClick && ( <div className={getClassName('__headerTitle')}>
<button {onBackButtonClick && (
aria-label={i18n('icu:back')} <button
className={getClassName('__back-button')} aria-label={i18n('icu:back')}
onClick={onBackButtonClick} className={getClassName('__back-button')}
tabIndex={0} onClick={onBackButtonClick}
type="button" tabIndex={0}
/> type="button"
)} />
{title && ( )}
<h1 {title && (
className={classNames( <h1
getClassName('__title'), className={classNames(
hasXButton ? getClassName('__title--with-x-button') : null getClassName('__title'),
)} hasXButton ? getClassName('__title--with-x-button') : null
> )}
{title} >
</h1> {title}
)} </h1>
{hasXButton && !title && ( )}
<div className={getClassName('__title')} /> {hasXButton && !title && (
)} <div className={getClassName('__title')} />
{hasXButton && ( )}
<button {hasXButton && (
aria-label={i18n('icu:close')} <button
className={getClassName('__close-button')} aria-label={i18n('icu:close')}
onClick={onClose} className={getClassName('__close-button')}
tabIndex={0} onClick={onClose}
type="button" tabIndex={0}
/> type="button"
)} />
)}
</div>
{modalHeaderChildren}
</div> </div>
)} )}
<div <div

View file

@ -68,6 +68,7 @@ export default {
label: 'Logitech Webcam (4e72:9058)', label: 'Logitech Webcam (4e72:9058)',
}, },
], ],
availableLocales: ['en'],
availableMicrophones, availableMicrophones,
availableSpeakers, availableSpeakers,
blockedCount: 0, blockedCount: 0,
@ -108,7 +109,10 @@ export default {
isSystemTraySupported: true, isSystemTraySupported: true,
isMinimizeToAndStartInSystemTraySupported: true, isMinimizeToAndStartInSystemTraySupported: true,
lastSyncTime: Date.now(), lastSyncTime: Date.now(),
localeOverride: null,
notificationContent: 'name', notificationContent: 'name',
preferredSystemLocales: ['en'],
resolvedLocale: 'en',
selectedCamera: selectedCamera:
'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c', 'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c',
selectedMicrophone: availableMicrophones[0], selectedMicrophone: availableMicrophones[0],
@ -143,6 +147,7 @@ export default {
onIncomingCallNotificationsChange: action( onIncomingCallNotificationsChange: action(
'onIncomingCallNotificationsChange' 'onIncomingCallNotificationsChange'
), ),
onLocaleChange: action('onLocaleChange'),
onLastSyncTimeChange: action('onLastSyncTimeChange'), onLastSyncTimeChange: action('onLastSyncTimeChange'),
onMediaCameraPermissionsChange: action('onMediaCameraPermissionsChange'), onMediaCameraPermissionsChange: action('onMediaCameraPermissionsChange'),
onMediaPermissionsChange: action('onMediaPermissionsChange'), onMediaPermissionsChange: action('onMediaPermissionsChange'),

View file

@ -10,9 +10,10 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { noop } from 'lodash'; import { noop, partition } from 'lodash';
import classNames from 'classnames'; import classNames from 'classnames';
import uuid from 'uuid'; import uuid from 'uuid';
import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling'; import type { MediaDeviceSettings } from '../types/Calling';
import type { import type {
@ -59,6 +60,9 @@ import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { useUniqueId } from '../hooks/useUniqueId'; import { useUniqueId } from '../hooks/useUniqueId';
import { useTheme } from '../hooks/useTheme'; import { useTheme } from '../hooks/useTheme';
import { focusableSelectors } from '../util/focusableSelectors'; import { focusableSelectors } from '../util/focusableSelectors';
import { Modal } from './Modal';
import { SearchInput } from './SearchInput';
import { removeDiacritics } from '../util/removeDiacritics';
type CheckboxChangeHandlerType = (value: boolean) => unknown; type CheckboxChangeHandlerType = (value: boolean) => unknown;
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown; type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
@ -103,6 +107,12 @@ export type PropsDataType = {
whoCanSeeMe: PhoneNumberSharingMode; whoCanSeeMe: PhoneNumberSharingMode;
zoomFactor: ZoomFactorType; zoomFactor: ZoomFactorType;
// Localization
availableLocales: ReadonlyArray<string>;
localeOverride: string | null;
preferredSystemLocales: ReadonlyArray<string>;
resolvedLocale: string;
// Other props // Other props
hasCustomTitleBar: boolean; hasCustomTitleBar: boolean;
initialSpellCheckSetting: boolean; initialSpellCheckSetting: boolean;
@ -161,6 +171,7 @@ type PropsFunctionType = {
onHideMenuBarChange: CheckboxChangeHandlerType; onHideMenuBarChange: CheckboxChangeHandlerType;
onIncomingCallNotificationsChange: CheckboxChangeHandlerType; onIncomingCallNotificationsChange: CheckboxChangeHandlerType;
onLastSyncTimeChange: (time: number) => unknown; onLastSyncTimeChange: (time: number) => unknown;
onLocaleChange: (locale: string | null) => void;
onMediaCameraPermissionsChange: CheckboxChangeHandlerType; onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
onMediaPermissionsChange: CheckboxChangeHandlerType; onMediaPermissionsChange: CheckboxChangeHandlerType;
onMessageAudioChange: CheckboxChangeHandlerType; onMessageAudioChange: CheckboxChangeHandlerType;
@ -204,6 +215,46 @@ enum Page {
PNP = 'PNP', PNP = 'PNP',
} }
enum LanguageDialog {
Selection,
Confirmation,
}
function getLocaleLanguagesWithMultipleRegions(
locales: ReadonlyArray<string>
): Set<string> {
const result = new Set<string>();
const seen = new Set<string>();
for (const locale of locales) {
const { language } = new Intl.Locale(locale);
if (seen.has(language)) {
result.add(language);
} else {
seen.add(language);
}
}
return result;
}
const cache = new Map<string, string>();
function getLanguageLabel(ofLocale: string, inLocale: string) {
const key = `${ofLocale}:${inLocale}`;
const cached = cache.get(key);
if (cached != null) {
return cached;
}
const value =
new Intl.DisplayNames(inLocale, {
type: 'language',
fallback: 'code',
style: 'long',
languageDisplay: 'standard',
}).of(ofLocale) ?? '';
cache.set(key, value);
return value;
}
const DEFAULT_ZOOM_FACTORS = [ const DEFAULT_ZOOM_FACTORS = [
{ {
text: '75%', text: '75%',
@ -230,6 +281,7 @@ const DEFAULT_ZOOM_FACTORS = [
export function Preferences({ export function Preferences({
addCustomColor, addCustomColor,
availableCameras, availableCameras,
availableLocales,
availableMicrophones, availableMicrophones,
availableSpeakers, availableSpeakers,
blockedCount, blockedCount,
@ -289,6 +341,7 @@ export function Preferences({
onHideMenuBarChange, onHideMenuBarChange,
onIncomingCallNotificationsChange, onIncomingCallNotificationsChange,
onLastSyncTimeChange, onLastSyncTimeChange,
onLocaleChange,
onMediaCameraPermissionsChange, onMediaCameraPermissionsChange,
onMediaPermissionsChange, onMediaPermissionsChange,
onMessageAudioChange, onMessageAudioChange,
@ -309,16 +362,19 @@ export function Preferences({
onWhoCanSeeMeChange, onWhoCanSeeMeChange,
onWhoCanFindMeChange, onWhoCanFindMeChange,
onZoomFactorChange, onZoomFactorChange,
preferredSystemLocales,
removeCustomColor, removeCustomColor,
removeCustomColorOnConversations, removeCustomColorOnConversations,
resetAllChatColors, resetAllChatColors,
resetDefaultChatColor, resetDefaultChatColor,
resolvedLocale,
selectedCamera, selectedCamera,
selectedMicrophone, selectedMicrophone,
selectedSpeaker, selectedSpeaker,
sentMediaQualitySetting, sentMediaQualitySetting,
setGlobalDefaultConversationColor, setGlobalDefaultConversationColor,
shouldShowStoriesSettings, shouldShowStoriesSettings,
localeOverride,
themeSetting, themeSetting,
universalExpireTimer = DurationInSeconds.ZERO, universalExpireTimer = DurationInSeconds.ZERO,
whoCanFindMe, whoCanFindMe,
@ -328,6 +384,7 @@ export function Preferences({
const storiesId = useUniqueId(); const storiesId = useUniqueId();
const themeSelectId = useUniqueId(); const themeSelectId = useUniqueId();
const zoomSelectId = useUniqueId(); const zoomSelectId = useUniqueId();
const languageId = useUniqueId();
const [confirmDelete, setConfirmDelete] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false);
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false); const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
@ -336,13 +393,31 @@ export function Preferences({
const [nowSyncing, setNowSyncing] = useState(false); const [nowSyncing, setNowSyncing] = useState(false);
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] = const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
useState(false); useState(false);
const [languageDialog, setLanguageDialog] = useState<LanguageDialog | null>(
null
);
const [selectedLanguageLocale, setSelectedLanguageLocale] = useState<
string | null
>(localeOverride);
const [languageSearchInput, setLanguageSearchInput] = useState('');
const theme = useTheme(); const theme = useTheme();
function closeLanguageDialog() {
setLanguageDialog(null);
setSelectedLanguageLocale(localeOverride);
}
useEffect(() => { useEffect(() => {
doneRendering(); doneRendering();
}, [doneRendering]); }, [doneRendering]);
useEscapeHandling(closeSettings); useEscapeHandling(() => {
if (languageDialog != null) {
closeLanguageDialog();
} else {
closeSettings();
}
});
const onZoomSelectChange = useCallback( const onZoomSelectChange = useCallback(
(value: string) => { (value: string) => {
@ -395,6 +470,82 @@ export function Preferences({
[onSelectedSpeakerChange, availableSpeakers] [onSelectedSpeakerChange, availableSpeakers]
); );
const localeSearchOptions = useMemo(() => {
const collator = new Intl.Collator(resolvedLocale, { usage: 'sort' });
const languagesWithMultipleRegions =
getLocaleLanguagesWithMultipleRegions(availableLocales);
const availableLocalesOptions = availableLocales
.map(locale => {
const { language } = new Intl.Locale(locale);
const displayLocale = languagesWithMultipleRegions.has(language)
? locale
: language;
const currentLocaleLabel = getLanguageLabel(
displayLocale,
resolvedLocale
);
const matchingLocaleLabel = getLanguageLabel(displayLocale, locale);
return { locale, currentLocaleLabel, matchingLocaleLabel };
})
.sort((a, b) => {
return collator.compare(a.currentLocaleLabel, b.currentLocaleLabel);
});
const [localeOverrideMatches, localeOverrideNonMatches] = partition(
availableLocalesOptions,
option => {
return option.locale === localeOverride;
}
);
const preferredSystemLocaleMatch = LocaleMatcher.match(
preferredSystemLocales as Array<string>, // bad types
availableLocales as Array<string>, // bad types
'en',
{ algorithm: 'best fit' }
);
return [
...localeOverrideMatches,
{
locale: null,
currentLocaleLabel: i18n('icu:Preferences__Language__SystemLanguage'),
matchingLocaleLabel: getLanguageLabel(
preferredSystemLocaleMatch,
preferredSystemLocaleMatch
),
},
...localeOverrideNonMatches,
];
}, [
i18n,
availableLocales,
resolvedLocale,
localeOverride,
preferredSystemLocales,
]);
const localeSearchResults = useMemo(() => {
return localeSearchOptions.filter(option => {
const input = removeDiacritics(languageSearchInput.trim().toLowerCase());
if (input === '') {
return true;
}
function isMatch(value: string) {
return removeDiacritics(value.toLowerCase()).includes(input);
}
return (
isMatch(option.currentLocaleLabel) ||
(option.matchingLocaleLabel && isMatch(option.matchingLocaleLabel))
);
});
}, [localeSearchOptions, languageSearchInput]);
let settings: JSX.Element | undefined; let settings: JSX.Element | undefined;
if (page === Page.General) { if (page === Page.General) {
settings = ( settings = (
@ -504,6 +655,129 @@ export function Preferences({
</div> </div>
<SettingsRow> <SettingsRow>
<Control <Control
icon="Preferences__LanguageIcon"
left={i18n('icu:Preferences__Language__Label')}
right={
<span
className="Preferences__LanguageButton"
lang={localeOverride ?? resolvedLocale}
>
{localeOverride != null
? getLanguageLabel(localeOverride, resolvedLocale)
: i18n('icu:Preferences__Language__SystemLanguage')}
</span>
}
onClick={() => {
setLanguageDialog(LanguageDialog.Selection);
}}
/>
{languageDialog === LanguageDialog.Selection && (
<Modal
i18n={i18n}
modalName="Preferences__LanguageModal"
moduleClassName="Preferences__LanguageModal"
padded={false}
onClose={closeLanguageDialog}
title={i18n('icu:Preferences__Language__ModalTitle')}
modalHeaderChildren={
<SearchInput
i18n={i18n}
value={languageSearchInput}
placeholder={i18n(
'icu:Preferences__Language__SearchLanguages'
)}
moduleClassName="Preferences__LanguageModal__SearchInput"
onChange={event => {
setLanguageSearchInput(event.currentTarget.value);
}}
/>
}
modalFooter={
<>
<Button
variant={ButtonVariant.Secondary}
onClick={closeLanguageDialog}
>
{i18n('icu:cancel')}
</Button>
<Button
variant={ButtonVariant.Primary}
disabled={selectedLanguageLocale === localeOverride}
onClick={() => {
setLanguageDialog(LanguageDialog.Confirmation);
}}
>
{i18n('icu:Preferences__LanguageModal__Set')}
</Button>
</>
}
>
{localeSearchResults.length === 0 && (
<div className="Preferences__LanguageModal__NoResults">
{i18n('icu:Preferences__Language__NoResults', {
searchTerm: languageSearchInput.trim(),
})}
</div>
)}
{localeSearchResults.map(option => {
const id = `${languageId}:${option.locale ?? 'system'}`;
const isSelected = option.locale === selectedLanguageLocale;
return (
<button
key={id}
type="button"
className="Preferences__LanguageModal__Item"
onClick={() => {
setSelectedLanguageLocale(option.locale);
}}
aria-pressed={isSelected}
>
<span className="Preferences__LanguageModal__Item__Inner">
<span className="Preferences__LanguageModal__Item__Label">
<span className="Preferences__LanguageModal__Item__Current">
{option.currentLocaleLabel}
</span>
{option.matchingLocaleLabel != null && (
<span
lang={option.locale ?? resolvedLocale}
className="Preferences__LanguageModal__Item__Matching"
>
{option.matchingLocaleLabel}
</span>
)}
</span>
{isSelected && (
<span className="Preferences__LanguageModal__Item__Check" />
)}
</span>
</button>
);
})}
</Modal>
)}
{languageDialog === LanguageDialog.Confirmation && (
<ConfirmationDialog
dialogName="Preferences__Language"
i18n={i18n}
title={i18n('icu:Preferences__LanguageModal__Restart__Title')}
onCancel={closeLanguageDialog}
onClose={closeLanguageDialog}
cancelText={i18n('icu:cancel')}
actions={[
{
text: i18n('icu:Preferences__LanguageModal__Restart__Button'),
style: 'affirmative',
action: () => {
onLocaleChange(selectedLanguageLocale);
},
},
]}
>
{i18n('icu:Preferences__LanguageModal__Restart__Description')}
</ConfirmationDialog>
)}
<Control
icon
left={ left={
<label htmlFor={themeSelectId}> <label htmlFor={themeSelectId}>
{i18n('icu:Preferences--theme')} {i18n('icu:Preferences--theme')}
@ -532,6 +806,7 @@ export function Preferences({
} }
/> />
<Control <Control
icon
left={i18n('icu:showChatColorEditor')} left={i18n('icu:showChatColorEditor')}
onClick={() => { onClick={() => {
setPage(Page.ChatColor); setPage(Page.ChatColor);
@ -548,6 +823,7 @@ export function Preferences({
} }
/> />
<Control <Control
icon
left={ left={
<label htmlFor={zoomSelectId}> <label htmlFor={zoomSelectId}>
{i18n('icu:Preferences--zoom')} {i18n('icu:Preferences--zoom')}
@ -1307,6 +1583,7 @@ export function Preferences({
> >
{i18n('icu:Preferences__button--notifications')} {i18n('icu:Preferences__button--notifications')}
</button> </button>
<button <button
type="button" type="button"
className={classNames({ className={classNames({
@ -1346,16 +1623,27 @@ function SettingsRow({
} }
function Control({ function Control({
icon,
left, left,
onClick, onClick,
right, right,
}: { }: {
/** A className or `true` to leave room for icon */
icon?: string | true;
left: ReactNode; left: ReactNode;
onClick?: () => unknown; onClick?: () => unknown;
right: ReactNode; right: ReactNode;
}): JSX.Element { }): JSX.Element {
const content = ( const content = (
<> <>
{icon && (
<div
className={classNames(
'Preferences__control--icon',
icon === true ? null : icon
)}
/>
)}
<div className="Preferences__control--key">{left}</div> <div className="Preferences__control--key">{left}</div>
<div className="Preferences__control--value">{right}</div> <div className="Preferences__control--value">{right}</div>
</> </>

View file

@ -19,6 +19,7 @@ const EPHEMERAL_NAME_MAP = new Map([
['spellCheck', 'spell-check'], ['spellCheck', 'spell-check'],
['systemTraySetting', 'system-tray-setting'], ['systemTraySetting', 'system-tray-setting'],
['themeSetting', 'theme-setting'], ['themeSetting', 'theme-setting'],
['localeOverride', 'localeOverride'],
]); ]);
type ResponseQueueEntry = Readonly<{ type ResponseQueueEntry = Readonly<{
@ -79,6 +80,9 @@ export class SettingsChannel extends EventEmitter {
isEphemeral: true, isEphemeral: true,
}); });
this.installSetting('localeOverride', {
isEphemeral: true,
});
this.installSetting('notificationSetting'); this.installSetting('notificationSetting');
this.installSetting('notificationDrawAttention'); this.installSetting('notificationDrawAttention');
this.installSetting('audioMessage'); this.installSetting('audioMessage');

View file

@ -32,5 +32,5 @@ export async function deleteAllData(): Promise<void> {
Errors.toLogFormat(error) Errors.toLogFormat(error)
); );
} }
window.IPC.restart(); window.SignalContext.restartApp();
} }

View file

@ -26,6 +26,7 @@ describe('locale', async () => {
) { ) {
const actualLocale = await load({ const actualLocale = await load({
preferredSystemLocales, preferredSystemLocales,
localeOverride: null,
hourCyclePreference: HourCyclePreference.UnknownPreference, hourCyclePreference: HourCyclePreference.UnknownPreference,
logger, logger,
}); });

View file

@ -199,6 +199,7 @@ const PLATFORMS = [
describe('createTemplate', () => { describe('createTemplate', () => {
const { i18n } = loadLocale({ const { i18n } = loadLocale({
preferredSystemLocales: ['en'], preferredSystemLocales: ['en'],
localeOverride: null,
hourCyclePreference: HourCyclePreference.UnknownPreference, hourCyclePreference: HourCyclePreference.UnknownPreference,
logger: { logger: {
fatal: stub().throwsArg(0), fatal: stub().throwsArg(0),

View file

@ -46,6 +46,7 @@ export const rendererConfigSchema = z.object({
installPath: configRequiredStringSchema, installPath: configRequiredStringSchema,
osRelease: configRequiredStringSchema, osRelease: configRequiredStringSchema,
osVersion: configRequiredStringSchema, osVersion: configRequiredStringSchema,
availableLocales: z.array(configRequiredStringSchema),
resolvedTranslationsLocale: configRequiredStringSchema, resolvedTranslationsLocale: configRequiredStringSchema,
resolvedTranslationsLocaleDirection: z.enum(['ltr', 'rtl']), resolvedTranslationsLocaleDirection: z.enum(['ltr', 'rtl']),
hourCyclePreference: HourCyclePreferenceSchema, hourCyclePreference: HourCyclePreferenceSchema,

View file

@ -56,6 +56,7 @@ export type StorageAccessType = {
'call-system-notification': boolean; 'call-system-notification': boolean;
'hide-menu-bar': boolean; 'hide-menu-bar': boolean;
'incoming-call-notification': boolean; 'incoming-call-notification': boolean;
localeOverride: string | null;
'notification-draw-attention': boolean; 'notification-draw-attention': boolean;
'notification-setting': NotificationSettingType; 'notification-setting': NotificationSettingType;
'read-receipt-setting': boolean; 'read-receipt-setting': boolean;

View file

@ -61,6 +61,7 @@ export type IPCEventsValuesType = {
hideMenuBar: boolean | undefined; hideMenuBar: boolean | undefined;
incomingCallNotification: boolean; incomingCallNotification: boolean;
lastSyncTime: number | undefined; lastSyncTime: number | undefined;
localeOverride: string | null;
notificationDrawAttention: boolean; notificationDrawAttention: boolean;
notificationSetting: NotificationSettingType; notificationSetting: NotificationSettingType;
preferredAudioInputDevice: AudioDevice | undefined; preferredAudioInputDevice: AudioDevice | undefined;
@ -372,6 +373,12 @@ export function createIPCEvents(
return promise; return promise;
}, },
getLocaleOverride: () => {
return window.storage.get('localeOverride') ?? null;
},
setLocaleOverride: async (locale: string | null) => {
await window.storage.put('localeOverride', locale);
},
getNotificationSetting: () => getNotificationSetting: () =>
window.storage.get('notification-setting', 'message'), window.storage.get('notification-setting', 'message'),
setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') => setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') =>

1
ts/window.d.ts vendored
View file

@ -74,7 +74,6 @@ export type IPCType = {
logAppLoadedEvent?: (options: { processedCount?: number }) => void; logAppLoadedEvent?: (options: { processedCount?: number }) => void;
readyForUpdates: () => void; readyForUpdates: () => void;
removeSetupMenuItems: () => unknown; removeSetupMenuItems: () => unknown;
restart: () => void;
setAutoHideMenuBar: (value: boolean) => void; setAutoHideMenuBar: (value: boolean) => void;
setAutoLaunch: (value: boolean) => Promise<void>; setAutoLaunch: (value: boolean) => Promise<void>;
setBadge: (badge: number | 'marked-unread') => void; setBadge: (badge: number | 'marked-unread') => void;

View file

@ -41,6 +41,7 @@ export type MinimalSignalContextType = {
executeMenuRole: (role: MenuItemConstructorOptions['role']) => Promise<void>; executeMenuRole: (role: MenuItemConstructorOptions['role']) => Promise<void>;
getAppInstance: () => string | undefined; getAppInstance: () => string | undefined;
getEnvironment: () => string; getEnvironment: () => string;
getI18nAvailableLocales: () => ReadonlyArray<string>;
getI18nLocale: LocalizerType['getLocale']; getI18nLocale: LocalizerType['getLocale'];
getI18nLocaleMessages: LocalizerType['getLocaleMessages']; getI18nLocaleMessages: LocalizerType['getLocaleMessages'];
getResolvedMessagesLocaleDirection: () => LocaleDirection; getResolvedMessagesLocaleDirection: () => LocaleDirection;
@ -53,6 +54,7 @@ export type MinimalSignalContextType = {
getPath: (name: 'userData' | 'home' | 'install') => string; getPath: (name: 'userData' | 'home' | 'install') => string;
getVersion: () => string; getVersion: () => string;
nativeThemeListener: NativeThemeType; nativeThemeListener: NativeThemeType;
restartApp: () => void;
Settings: { Settings: {
themeSetting: SettingType<IPCEventsValuesType['themeSetting']>; themeSetting: SettingType<IPCEventsValuesType['themeSetting']>;
waitForChange: () => Promise<void>; waitForChange: () => Promise<void>;

View file

@ -99,10 +99,6 @@ const IPC: IPCType = {
}), }),
readyForUpdates: () => ipc.send('ready-for-updates'), readyForUpdates: () => ipc.send('ready-for-updates'),
removeSetupMenuItems: () => ipc.send('remove-setup-menu-items'), removeSetupMenuItems: () => ipc.send('remove-setup-menu-items'),
restart: () => {
log.info('restart');
ipc.send('restart');
},
setAutoHideMenuBar: autoHide => ipc.send('set-auto-hide-menu-bar', autoHide), setAutoHideMenuBar: autoHide => ipc.send('set-auto-hide-menu-bar', autoHide),
setAutoLaunch: value => ipc.invoke('set-auto-launch', value), setAutoLaunch: value => ipc.invoke('set-auto-launch', value),
setBadge: badge => ipc.send('set-badge', badge), setBadge: badge => ipc.send('set-badge', badge),

View file

@ -40,6 +40,7 @@ export const MinimalSignalContext: MinimalSignalContextType = {
async getMenuOptions(): Promise<MenuOptionsType> { async getMenuOptions(): Promise<MenuOptionsType> {
return ipcRenderer.invoke('getMenuOptions'); return ipcRenderer.invoke('getMenuOptions');
}, },
getI18nAvailableLocales: () => config.availableLocales,
getI18nLocale: () => config.resolvedTranslationsLocale, getI18nLocale: () => config.resolvedTranslationsLocale,
getI18nLocaleMessages: () => localeMessages, getI18nLocaleMessages: () => localeMessages,
@ -48,8 +49,8 @@ export const MinimalSignalContext: MinimalSignalContextType = {
config.resolvedTranslationsLocaleDirection, config.resolvedTranslationsLocaleDirection,
getHourCyclePreference: () => config.hourCyclePreference, getHourCyclePreference: () => config.hourCyclePreference,
getPreferredSystemLocales: () => config.preferredSystemLocales, getPreferredSystemLocales: () => config.preferredSystemLocales,
nativeThemeListener: createNativeThemeListener(ipcRenderer, window), nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
restartApp: () => ipcRenderer.send('restart'),
OS: { OS: {
getClassName: () => ipcRenderer.sendSync('OS.getClassName'), getClassName: () => ipcRenderer.sendSync('OS.getClassName'),
hasCustomTitleBar: () => hasCustomTitleBar, hasCustomTitleBar: () => hasCustomTitleBar,

View file

@ -50,6 +50,7 @@ installSetting('hasStoriesDisabled');
installSetting('hideMenuBar'); installSetting('hideMenuBar');
installSetting('incomingCallNotification'); installSetting('incomingCallNotification');
installSetting('lastSyncTime'); installSetting('lastSyncTime');
installSetting('localeOverride');
installSetting('notificationDrawAttention'); installSetting('notificationDrawAttention');
installSetting('notificationSetting'); installSetting('notificationSetting');
installSetting('spellCheck'); installSetting('spellCheck');

View file

@ -20,6 +20,7 @@ SettingsWindowProps.onRender(
({ ({
addCustomColor, addCustomColor,
availableCameras, availableCameras,
availableLocales,
availableMicrophones, availableMicrophones,
availableSpeakers, availableSpeakers,
blockedCount, blockedCount,
@ -78,6 +79,7 @@ SettingsWindowProps.onRender(
onHideMenuBarChange, onHideMenuBarChange,
onIncomingCallNotificationsChange, onIncomingCallNotificationsChange,
onLastSyncTimeChange, onLastSyncTimeChange,
onLocaleChange,
onMediaCameraPermissionsChange, onMediaCameraPermissionsChange,
onMediaPermissionsChange, onMediaPermissionsChange,
onMessageAudioChange, onMessageAudioChange,
@ -98,16 +100,19 @@ SettingsWindowProps.onRender(
onWhoCanFindMeChange, onWhoCanFindMeChange,
onWhoCanSeeMeChange, onWhoCanSeeMeChange,
onZoomFactorChange, onZoomFactorChange,
preferredSystemLocales,
removeCustomColor, removeCustomColor,
removeCustomColorOnConversations, removeCustomColorOnConversations,
resetAllChatColors, resetAllChatColors,
resetDefaultChatColor, resetDefaultChatColor,
resolvedLocale,
selectedCamera, selectedCamera,
selectedMicrophone, selectedMicrophone,
selectedSpeaker, selectedSpeaker,
sentMediaQualitySetting, sentMediaQualitySetting,
setGlobalDefaultConversationColor, setGlobalDefaultConversationColor,
shouldShowStoriesSettings, shouldShowStoriesSettings,
localeOverride,
themeSetting, themeSetting,
universalExpireTimer, universalExpireTimer,
whoCanFindMe, whoCanFindMe,
@ -118,6 +123,7 @@ SettingsWindowProps.onRender(
<Preferences <Preferences
addCustomColor={addCustomColor} addCustomColor={addCustomColor}
availableCameras={availableCameras} availableCameras={availableCameras}
availableLocales={availableLocales}
availableMicrophones={availableMicrophones} availableMicrophones={availableMicrophones}
availableSpeakers={availableSpeakers} availableSpeakers={availableSpeakers}
blockedCount={blockedCount} blockedCount={blockedCount}
@ -167,6 +173,7 @@ SettingsWindowProps.onRender(
isSyncSupported={isSyncSupported} isSyncSupported={isSyncSupported}
isSystemTraySupported={isSystemTraySupported} isSystemTraySupported={isSystemTraySupported}
lastSyncTime={lastSyncTime} lastSyncTime={lastSyncTime}
localeOverride={localeOverride}
makeSyncRequest={makeSyncRequest} makeSyncRequest={makeSyncRequest}
notificationContent={notificationContent} notificationContent={notificationContent}
onAudioNotificationsChange={onAudioNotificationsChange} onAudioNotificationsChange={onAudioNotificationsChange}
@ -179,6 +186,7 @@ SettingsWindowProps.onRender(
onHideMenuBarChange={onHideMenuBarChange} onHideMenuBarChange={onHideMenuBarChange}
onIncomingCallNotificationsChange={onIncomingCallNotificationsChange} onIncomingCallNotificationsChange={onIncomingCallNotificationsChange}
onLastSyncTimeChange={onLastSyncTimeChange} onLastSyncTimeChange={onLastSyncTimeChange}
onLocaleChange={onLocaleChange}
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange} onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
onMediaPermissionsChange={onMediaPermissionsChange} onMediaPermissionsChange={onMediaPermissionsChange}
onMessageAudioChange={onMessageAudioChange} onMessageAudioChange={onMessageAudioChange}
@ -201,10 +209,12 @@ SettingsWindowProps.onRender(
onWhoCanFindMeChange={onWhoCanFindMeChange} onWhoCanFindMeChange={onWhoCanFindMeChange}
onWhoCanSeeMeChange={onWhoCanSeeMeChange} onWhoCanSeeMeChange={onWhoCanSeeMeChange}
onZoomFactorChange={onZoomFactorChange} onZoomFactorChange={onZoomFactorChange}
preferredSystemLocales={preferredSystemLocales}
removeCustomColorOnConversations={removeCustomColorOnConversations} removeCustomColorOnConversations={removeCustomColorOnConversations}
removeCustomColor={removeCustomColor} removeCustomColor={removeCustomColor}
resetAllChatColors={resetAllChatColors} resetAllChatColors={resetAllChatColors}
resetDefaultChatColor={resetDefaultChatColor} resetDefaultChatColor={resetDefaultChatColor}
resolvedLocale={resolvedLocale}
selectedCamera={selectedCamera} selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone} selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker} selectedSpeaker={selectedSpeaker}

View file

@ -46,6 +46,7 @@ const settingSpellCheck = createSetting('spellCheck');
const settingTextFormatting = createSetting('textFormatting'); const settingTextFormatting = createSetting('textFormatting');
const settingTheme = createSetting('themeSetting'); const settingTheme = createSetting('themeSetting');
const settingSystemTraySetting = createSetting('systemTraySetting'); const settingSystemTraySetting = createSetting('systemTraySetting');
const settingLocaleOverride = createSetting('localeOverride');
const settingLastSyncTime = createSetting('lastSyncTime'); const settingLastSyncTime = createSetting('lastSyncTime');
@ -169,6 +170,7 @@ async function renderPreferences() {
selectedMicrophone, selectedMicrophone,
selectedSpeaker, selectedSpeaker,
sentMediaQualitySetting, sentMediaQualitySetting,
localeOverride,
systemTraySetting, systemTraySetting,
themeSetting, themeSetting,
universalExpireTimer, universalExpireTimer,
@ -210,6 +212,7 @@ async function renderPreferences() {
selectedMicrophone: settingAudioInput.getValue(), selectedMicrophone: settingAudioInput.getValue(),
selectedSpeaker: settingAudioOutput.getValue(), selectedSpeaker: settingAudioOutput.getValue(),
sentMediaQualitySetting: settingSentMediaQuality.getValue(), sentMediaQualitySetting: settingSentMediaQuality.getValue(),
localeOverride: settingLocaleOverride.getValue(),
systemTraySetting: settingSystemTraySetting.getValue(), systemTraySetting: settingSystemTraySetting.getValue(),
themeSetting: settingTheme.getValue(), themeSetting: settingTheme.getValue(),
universalExpireTimer: settingUniversalExpireTimer.getValue(), universalExpireTimer: settingUniversalExpireTimer.getValue(),
@ -236,9 +239,15 @@ async function renderPreferences() {
settingUniversalExpireTimer.setValue settingUniversalExpireTimer.setValue
); );
const availableLocales = MinimalSignalContext.getI18nAvailableLocales();
const resolvedLocale = MinimalSignalContext.getI18nLocale();
const preferredSystemLocales =
MinimalSignalContext.getPreferredSystemLocales();
const props = { const props = {
// Settings // Settings
availableCameras, availableCameras,
availableLocales,
availableMicrophones, availableMicrophones,
availableSpeakers, availableSpeakers,
blockedCount, blockedCount,
@ -268,7 +277,10 @@ async function renderPreferences() {
hasTextFormatting, hasTextFormatting,
hasTypingIndicators, hasTypingIndicators,
lastSyncTime, lastSyncTime,
localeOverride,
notificationContent, notificationContent,
preferredSystemLocales,
resolvedLocale,
selectedCamera, selectedCamera,
selectedMicrophone, selectedMicrophone,
selectedSpeaker, selectedSpeaker,
@ -347,6 +359,10 @@ async function renderPreferences() {
settingIncomingCallNotification.setValue settingIncomingCallNotification.setValue
), ),
onLastSyncTimeChange: attachRenderCallback(settingLastSyncTime.setValue), onLastSyncTimeChange: attachRenderCallback(settingLastSyncTime.setValue),
onLocaleChange: async (locale: string | null) => {
await settingLocaleOverride.setValue(locale);
MinimalSignalContext.restartApp();
},
onMediaCameraPermissionsChange: attachRenderCallback( onMediaCameraPermissionsChange: attachRenderCallback(
settingMediaCameraPermissions.setValue settingMediaCameraPermissions.setValue
), ),

View file

@ -1629,6 +1629,14 @@
"@formatjs/intl-localematcher" "0.2.32" "@formatjs/intl-localematcher" "0.2.32"
tslib "^2.4.0" tslib "^2.4.0"
"@formatjs/ecma402-abstract@1.15.0":
version "1.15.0"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.15.0.tgz#0a285a5dc69889e15d53803bd5036272e23e5a18"
integrity sha512-7bAYAv0w4AIao9DNg0avfOLTCPE9woAgs6SpXuMq11IN3A+l+cq8ghczwqSZBM11myvPSJA7vLn72q0rJ0QK6Q==
dependencies:
"@formatjs/intl-localematcher" "0.2.32"
tslib "^2.4.0"
"@formatjs/fast-memoize@1.2.1": "@formatjs/fast-memoize@1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21" resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz#e6f5aee2e4fd0ca5edba6eba7668e2d855e0fc21"
@ -1684,13 +1692,13 @@
"@formatjs/icu-skeleton-parser" "1.3.18" "@formatjs/icu-skeleton-parser" "1.3.18"
tslib "^2.4.0" tslib "^2.4.0"
"@formatjs/icu-messageformat-parser@2.3.1": "@formatjs/icu-messageformat-parser@2.4.0":
version "2.3.1" version "2.4.0"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.1.tgz#953080ea5c053bc73bdf55d0a524a3c3c133ae6b" resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.4.0.tgz#e165f3594c68416ce15f63793768251de2a85f88"
integrity sha512-knF2AkAKN4Upv4oIiKY4Wd/dLH68TNMPgV/tJMu/T6FP9aQwbv8fpj7U3lkyniPaNVxvia56Gxax8MKOjtxLSQ== integrity sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "1.14.3" "@formatjs/ecma402-abstract" "1.15.0"
"@formatjs/icu-skeleton-parser" "1.3.18" "@formatjs/icu-skeleton-parser" "1.4.0"
tslib "^2.4.0" tslib "^2.4.0"
"@formatjs/icu-skeleton-parser@1.3.13": "@formatjs/icu-skeleton-parser@1.3.13":
@ -1717,6 +1725,14 @@
"@formatjs/ecma402-abstract" "1.11.4" "@formatjs/ecma402-abstract" "1.11.4"
tslib "^2.1.0" tslib "^2.1.0"
"@formatjs/icu-skeleton-parser@1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.4.0.tgz#96342eca7c4eef7a309875569e5da973db3465e6"
integrity sha512-Qq347VM616rVLkvN6QsKJELazRyNlbCiN47LdH0Mc5U7E2xV0vatiVhGqd3KFgbc055BvtnUXR7XX60dCGFuWg==
dependencies:
"@formatjs/ecma402-abstract" "1.15.0"
tslib "^2.4.0"
"@formatjs/intl-displaynames@6.1.3": "@formatjs/intl-displaynames@6.1.3":
version "6.1.3" version "6.1.3"
resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.1.3.tgz#c9d283db518cd721c0855e9854bfadb9ba304b6a" resolved "https://registry.yarnpkg.com/@formatjs/intl-displaynames/-/intl-displaynames-6.1.3.tgz#c9d283db518cd721c0855e9854bfadb9ba304b6a"
@ -11789,13 +11805,13 @@ intl-messageformat@10.3.1:
tslib "^2.4.0" tslib "^2.4.0"
intl-messageformat@^10.1.0: intl-messageformat@^10.1.0:
version "10.3.4" version "10.3.5"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.4.tgz#20f064c28b46fa6d352a4c4ba5e9bfc597af3eba" resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.5.tgz#f55684fc663e62616ad59d3a504ea0cac3f267b7"
integrity sha512-/FxUIrlbPtuykSNX85CB5sp2FjLVeTmdD7TfRkVFPft2n4FgcSlAcilFytYiFAEmPHc+0PvpLCIPXeaGFzIvOg== integrity sha512-6kPkftF8Jg3XJCkGKa5OD+nYQ+qcSxF4ZkuDdXZ6KGG0VXn+iblJqRFyDdm9VvKcMyC0Km2+JlVQffFM52D0YA==
dependencies: dependencies:
"@formatjs/ecma402-abstract" "1.14.3" "@formatjs/ecma402-abstract" "1.15.0"
"@formatjs/fast-memoize" "2.0.1" "@formatjs/fast-memoize" "2.0.1"
"@formatjs/icu-messageformat-parser" "2.3.1" "@formatjs/icu-messageformat-parser" "2.4.0"
tslib "^2.4.0" tslib "^2.4.0"
intl-messageformat@^9.3.19: intl-messageformat@^9.3.19:
@ -18836,6 +18852,11 @@ thread-stream@^2.0.0:
dependencies: dependencies:
real-require "^0.2.0" real-require "^0.2.0"
throttle-debounce@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-3.0.1.tgz#32f94d84dfa894f786c9a1f290e7a645b6a19abb"
integrity sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==
through2-filter@^3.0.0: through2-filter@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"