Init Language Picker
This commit is contained in:
parent
754bb02c06
commit
89e66da351
25 changed files with 610 additions and 61 deletions
|
@ -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"
|
||||
|
|
|
@ -26,6 +26,7 @@ function getLocaleMessages(locale: string): LocaleMessagesType {
|
|||
export type LocaleDirection = 'ltr' | 'rtl';
|
||||
|
||||
export type LocaleType = {
|
||||
availableLocales: Array<string>;
|
||||
i18n: LocalizerType;
|
||||
name: string;
|
||||
direction: LocaleDirection;
|
||||
|
@ -65,6 +66,7 @@ function getLocaleDirection(
|
|||
}
|
||||
|
||||
function finalize(
|
||||
availableLocales: Array<string>,
|
||||
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<string> {
|
|||
|
||||
export function load({
|
||||
preferredSystemLocales,
|
||||
localeOverride,
|
||||
hourCyclePreference,
|
||||
logger,
|
||||
}: {
|
||||
preferredSystemLocales: Array<string>;
|
||||
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,
|
||||
|
|
28
app/main.ts
28
app/main.ts
|
@ -359,6 +359,26 @@ async function getBackgroundColor(
|
|||
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;
|
||||
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,
|
||||
|
|
1
images/icons/v3/globe/globe.svg
Normal file
1
images/icons/v3/globe/globe.svg
Normal 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 |
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -972,7 +972,7 @@ export async function startApp(): Promise<void> {
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 && (
|
||||
<button
|
||||
aria-label={i18n('icu:back')}
|
||||
className={getClassName('__back-button')}
|
||||
onClick={onBackButtonClick}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
{title && (
|
||||
<h1
|
||||
className={classNames(
|
||||
getClassName('__title'),
|
||||
hasXButton ? getClassName('__title--with-x-button') : null
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
{hasXButton && !title && (
|
||||
<div className={getClassName('__title')} />
|
||||
)}
|
||||
{hasXButton && (
|
||||
<button
|
||||
aria-label={i18n('icu:close')}
|
||||
className={getClassName('__close-button')}
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
<div className={getClassName('__headerTitle')}>
|
||||
{onBackButtonClick && (
|
||||
<button
|
||||
aria-label={i18n('icu:back')}
|
||||
className={getClassName('__back-button')}
|
||||
onClick={onBackButtonClick}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
{title && (
|
||||
<h1
|
||||
className={classNames(
|
||||
getClassName('__title'),
|
||||
hasXButton ? getClassName('__title--with-x-button') : null
|
||||
)}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
{hasXButton && !title && (
|
||||
<div className={getClassName('__title')} />
|
||||
)}
|
||||
{hasXButton && (
|
||||
<button
|
||||
aria-label={i18n('icu:close')}
|
||||
className={getClassName('__close-button')}
|
||||
onClick={onClose}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{modalHeaderChildren}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
|
|
|
@ -68,6 +68,7 @@ export default {
|
|||
label: 'Logitech Webcam (4e72:9058)',
|
||||
},
|
||||
],
|
||||
availableLocales: ['en'],
|
||||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
blockedCount: 0,
|
||||
|
@ -108,7 +109,10 @@ export default {
|
|||
isSystemTraySupported: true,
|
||||
isMinimizeToAndStartInSystemTraySupported: true,
|
||||
lastSyncTime: Date.now(),
|
||||
localeOverride: null,
|
||||
notificationContent: 'name',
|
||||
preferredSystemLocales: ['en'],
|
||||
resolvedLocale: 'en',
|
||||
selectedCamera:
|
||||
'dfbe6effe70b0611ba0fdc2a9ea3f39f6cb110e6687948f7e5f016c111b7329c',
|
||||
selectedMicrophone: availableMicrophones[0],
|
||||
|
@ -143,6 +147,7 @@ export default {
|
|||
onIncomingCallNotificationsChange: action(
|
||||
'onIncomingCallNotificationsChange'
|
||||
),
|
||||
onLocaleChange: action('onLocaleChange'),
|
||||
onLastSyncTimeChange: action('onLastSyncTimeChange'),
|
||||
onMediaCameraPermissionsChange: action('onMediaCameraPermissionsChange'),
|
||||
onMediaPermissionsChange: action('onMediaPermissionsChange'),
|
||||
|
|
|
@ -10,9 +10,10 @@ import React, {
|
|||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { noop, partition } from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import uuid from 'uuid';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
|
||||
import type { MediaDeviceSettings } from '../types/Calling';
|
||||
import type {
|
||||
|
@ -59,6 +60,9 @@ import { useEscapeHandling } from '../hooks/useEscapeHandling';
|
|||
import { useUniqueId } from '../hooks/useUniqueId';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { focusableSelectors } from '../util/focusableSelectors';
|
||||
import { Modal } from './Modal';
|
||||
import { SearchInput } from './SearchInput';
|
||||
import { removeDiacritics } from '../util/removeDiacritics';
|
||||
|
||||
type CheckboxChangeHandlerType = (value: boolean) => unknown;
|
||||
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
|
||||
|
@ -103,6 +107,12 @@ export type PropsDataType = {
|
|||
whoCanSeeMe: PhoneNumberSharingMode;
|
||||
zoomFactor: ZoomFactorType;
|
||||
|
||||
// Localization
|
||||
availableLocales: ReadonlyArray<string>;
|
||||
localeOverride: string | null;
|
||||
preferredSystemLocales: ReadonlyArray<string>;
|
||||
resolvedLocale: string;
|
||||
|
||||
// Other props
|
||||
hasCustomTitleBar: boolean;
|
||||
initialSpellCheckSetting: boolean;
|
||||
|
@ -161,6 +171,7 @@ type PropsFunctionType = {
|
|||
onHideMenuBarChange: CheckboxChangeHandlerType;
|
||||
onIncomingCallNotificationsChange: CheckboxChangeHandlerType;
|
||||
onLastSyncTimeChange: (time: number) => unknown;
|
||||
onLocaleChange: (locale: string | null) => void;
|
||||
onMediaCameraPermissionsChange: CheckboxChangeHandlerType;
|
||||
onMediaPermissionsChange: CheckboxChangeHandlerType;
|
||||
onMessageAudioChange: CheckboxChangeHandlerType;
|
||||
|
@ -204,6 +215,46 @@ enum Page {
|
|||
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 = [
|
||||
{
|
||||
text: '75%',
|
||||
|
@ -230,6 +281,7 @@ const DEFAULT_ZOOM_FACTORS = [
|
|||
export function Preferences({
|
||||
addCustomColor,
|
||||
availableCameras,
|
||||
availableLocales,
|
||||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
blockedCount,
|
||||
|
@ -289,6 +341,7 @@ export function Preferences({
|
|||
onHideMenuBarChange,
|
||||
onIncomingCallNotificationsChange,
|
||||
onLastSyncTimeChange,
|
||||
onLocaleChange,
|
||||
onMediaCameraPermissionsChange,
|
||||
onMediaPermissionsChange,
|
||||
onMessageAudioChange,
|
||||
|
@ -309,16 +362,19 @@ export function Preferences({
|
|||
onWhoCanSeeMeChange,
|
||||
onWhoCanFindMeChange,
|
||||
onZoomFactorChange,
|
||||
preferredSystemLocales,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
resetAllChatColors,
|
||||
resetDefaultChatColor,
|
||||
resolvedLocale,
|
||||
selectedCamera,
|
||||
selectedMicrophone,
|
||||
selectedSpeaker,
|
||||
sentMediaQualitySetting,
|
||||
setGlobalDefaultConversationColor,
|
||||
shouldShowStoriesSettings,
|
||||
localeOverride,
|
||||
themeSetting,
|
||||
universalExpireTimer = DurationInSeconds.ZERO,
|
||||
whoCanFindMe,
|
||||
|
@ -328,6 +384,7 @@ export function Preferences({
|
|||
const storiesId = useUniqueId();
|
||||
const themeSelectId = useUniqueId();
|
||||
const zoomSelectId = useUniqueId();
|
||||
const languageId = useUniqueId();
|
||||
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
|
||||
|
@ -336,13 +393,31 @@ export function Preferences({
|
|||
const [nowSyncing, setNowSyncing] = useState(false);
|
||||
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
|
||||
useState(false);
|
||||
const [languageDialog, setLanguageDialog] = useState<LanguageDialog | null>(
|
||||
null
|
||||
);
|
||||
const [selectedLanguageLocale, setSelectedLanguageLocale] = useState<
|
||||
string | null
|
||||
>(localeOverride);
|
||||
const [languageSearchInput, setLanguageSearchInput] = useState('');
|
||||
const theme = useTheme();
|
||||
|
||||
function closeLanguageDialog() {
|
||||
setLanguageDialog(null);
|
||||
setSelectedLanguageLocale(localeOverride);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
doneRendering();
|
||||
}, [doneRendering]);
|
||||
|
||||
useEscapeHandling(closeSettings);
|
||||
useEscapeHandling(() => {
|
||||
if (languageDialog != null) {
|
||||
closeLanguageDialog();
|
||||
} else {
|
||||
closeSettings();
|
||||
}
|
||||
});
|
||||
|
||||
const onZoomSelectChange = useCallback(
|
||||
(value: string) => {
|
||||
|
@ -395,6 +470,82 @@ export function Preferences({
|
|||
[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;
|
||||
if (page === Page.General) {
|
||||
settings = (
|
||||
|
@ -504,6 +655,129 @@ export function Preferences({
|
|||
</div>
|
||||
<SettingsRow>
|
||||
<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={
|
||||
<label htmlFor={themeSelectId}>
|
||||
{i18n('icu:Preferences--theme')}
|
||||
|
@ -532,6 +806,7 @@ export function Preferences({
|
|||
}
|
||||
/>
|
||||
<Control
|
||||
icon
|
||||
left={i18n('icu:showChatColorEditor')}
|
||||
onClick={() => {
|
||||
setPage(Page.ChatColor);
|
||||
|
@ -548,6 +823,7 @@ export function Preferences({
|
|||
}
|
||||
/>
|
||||
<Control
|
||||
icon
|
||||
left={
|
||||
<label htmlFor={zoomSelectId}>
|
||||
{i18n('icu:Preferences--zoom')}
|
||||
|
@ -1307,6 +1583,7 @@ export function Preferences({
|
|||
>
|
||||
{i18n('icu:Preferences__button--notifications')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={classNames({
|
||||
|
@ -1346,16 +1623,27 @@ function SettingsRow({
|
|||
}
|
||||
|
||||
function Control({
|
||||
icon,
|
||||
left,
|
||||
onClick,
|
||||
right,
|
||||
}: {
|
||||
/** A className or `true` to leave room for icon */
|
||||
icon?: string | true;
|
||||
left: ReactNode;
|
||||
onClick?: () => unknown;
|
||||
right: ReactNode;
|
||||
}): JSX.Element {
|
||||
const content = (
|
||||
<>
|
||||
{icon && (
|
||||
<div
|
||||
className={classNames(
|
||||
'Preferences__control--icon',
|
||||
icon === true ? null : icon
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="Preferences__control--key">{left}</div>
|
||||
<div className="Preferences__control--value">{right}</div>
|
||||
</>
|
||||
|
|
|
@ -19,6 +19,7 @@ const EPHEMERAL_NAME_MAP = new Map([
|
|||
['spellCheck', 'spell-check'],
|
||||
['systemTraySetting', 'system-tray-setting'],
|
||||
['themeSetting', 'theme-setting'],
|
||||
['localeOverride', 'localeOverride'],
|
||||
]);
|
||||
|
||||
type ResponseQueueEntry = Readonly<{
|
||||
|
@ -79,6 +80,9 @@ export class SettingsChannel extends EventEmitter {
|
|||
isEphemeral: true,
|
||||
});
|
||||
|
||||
this.installSetting('localeOverride', {
|
||||
isEphemeral: true,
|
||||
});
|
||||
this.installSetting('notificationSetting');
|
||||
this.installSetting('notificationDrawAttention');
|
||||
this.installSetting('audioMessage');
|
||||
|
|
|
@ -32,5 +32,5 @@ export async function deleteAllData(): Promise<void> {
|
|||
Errors.toLogFormat(error)
|
||||
);
|
||||
}
|
||||
window.IPC.restart();
|
||||
window.SignalContext.restartApp();
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ describe('locale', async () => {
|
|||
) {
|
||||
const actualLocale = await load({
|
||||
preferredSystemLocales,
|
||||
localeOverride: null,
|
||||
hourCyclePreference: HourCyclePreference.UnknownPreference,
|
||||
logger,
|
||||
});
|
||||
|
|
|
@ -199,6 +199,7 @@ const PLATFORMS = [
|
|||
describe('createTemplate', () => {
|
||||
const { i18n } = loadLocale({
|
||||
preferredSystemLocales: ['en'],
|
||||
localeOverride: null,
|
||||
hourCyclePreference: HourCyclePreference.UnknownPreference,
|
||||
logger: {
|
||||
fatal: stub().throwsArg(0),
|
||||
|
|
|
@ -46,6 +46,7 @@ export const rendererConfigSchema = z.object({
|
|||
installPath: configRequiredStringSchema,
|
||||
osRelease: configRequiredStringSchema,
|
||||
osVersion: configRequiredStringSchema,
|
||||
availableLocales: z.array(configRequiredStringSchema),
|
||||
resolvedTranslationsLocale: configRequiredStringSchema,
|
||||
resolvedTranslationsLocaleDirection: z.enum(['ltr', 'rtl']),
|
||||
hourCyclePreference: HourCyclePreferenceSchema,
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -56,6 +56,7 @@ export type StorageAccessType = {
|
|||
'call-system-notification': boolean;
|
||||
'hide-menu-bar': boolean;
|
||||
'incoming-call-notification': boolean;
|
||||
localeOverride: string | null;
|
||||
'notification-draw-attention': boolean;
|
||||
'notification-setting': NotificationSettingType;
|
||||
'read-receipt-setting': boolean;
|
||||
|
|
|
@ -61,6 +61,7 @@ export type IPCEventsValuesType = {
|
|||
hideMenuBar: boolean | undefined;
|
||||
incomingCallNotification: boolean;
|
||||
lastSyncTime: number | undefined;
|
||||
localeOverride: string | null;
|
||||
notificationDrawAttention: boolean;
|
||||
notificationSetting: NotificationSettingType;
|
||||
preferredAudioInputDevice: AudioDevice | undefined;
|
||||
|
@ -372,6 +373,12 @@ export function createIPCEvents(
|
|||
return promise;
|
||||
},
|
||||
|
||||
getLocaleOverride: () => {
|
||||
return window.storage.get('localeOverride') ?? null;
|
||||
},
|
||||
setLocaleOverride: async (locale: string | null) => {
|
||||
await window.storage.put('localeOverride', locale);
|
||||
},
|
||||
getNotificationSetting: () =>
|
||||
window.storage.get('notification-setting', 'message'),
|
||||
setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') =>
|
||||
|
|
1
ts/window.d.ts
vendored
1
ts/window.d.ts
vendored
|
@ -74,7 +74,6 @@ export type IPCType = {
|
|||
logAppLoadedEvent?: (options: { processedCount?: number }) => void;
|
||||
readyForUpdates: () => void;
|
||||
removeSetupMenuItems: () => unknown;
|
||||
restart: () => void;
|
||||
setAutoHideMenuBar: (value: boolean) => void;
|
||||
setAutoLaunch: (value: boolean) => Promise<void>;
|
||||
setBadge: (badge: number | 'marked-unread') => void;
|
||||
|
|
|
@ -41,6 +41,7 @@ export type MinimalSignalContextType = {
|
|||
executeMenuRole: (role: MenuItemConstructorOptions['role']) => Promise<void>;
|
||||
getAppInstance: () => string | undefined;
|
||||
getEnvironment: () => string;
|
||||
getI18nAvailableLocales: () => ReadonlyArray<string>;
|
||||
getI18nLocale: LocalizerType['getLocale'];
|
||||
getI18nLocaleMessages: LocalizerType['getLocaleMessages'];
|
||||
getResolvedMessagesLocaleDirection: () => LocaleDirection;
|
||||
|
@ -53,6 +54,7 @@ export type MinimalSignalContextType = {
|
|||
getPath: (name: 'userData' | 'home' | 'install') => string;
|
||||
getVersion: () => string;
|
||||
nativeThemeListener: NativeThemeType;
|
||||
restartApp: () => void;
|
||||
Settings: {
|
||||
themeSetting: SettingType<IPCEventsValuesType['themeSetting']>;
|
||||
waitForChange: () => Promise<void>;
|
||||
|
|
|
@ -99,10 +99,6 @@ const IPC: IPCType = {
|
|||
}),
|
||||
readyForUpdates: () => ipc.send('ready-for-updates'),
|
||||
removeSetupMenuItems: () => ipc.send('remove-setup-menu-items'),
|
||||
restart: () => {
|
||||
log.info('restart');
|
||||
ipc.send('restart');
|
||||
},
|
||||
setAutoHideMenuBar: autoHide => ipc.send('set-auto-hide-menu-bar', autoHide),
|
||||
setAutoLaunch: value => ipc.invoke('set-auto-launch', value),
|
||||
setBadge: badge => ipc.send('set-badge', badge),
|
||||
|
|
|
@ -40,6 +40,7 @@ export const MinimalSignalContext: MinimalSignalContextType = {
|
|||
async getMenuOptions(): Promise<MenuOptionsType> {
|
||||
return ipcRenderer.invoke('getMenuOptions');
|
||||
},
|
||||
getI18nAvailableLocales: () => config.availableLocales,
|
||||
getI18nLocale: () => config.resolvedTranslationsLocale,
|
||||
getI18nLocaleMessages: () => localeMessages,
|
||||
|
||||
|
@ -48,8 +49,8 @@ export const MinimalSignalContext: MinimalSignalContextType = {
|
|||
config.resolvedTranslationsLocaleDirection,
|
||||
getHourCyclePreference: () => config.hourCyclePreference,
|
||||
getPreferredSystemLocales: () => config.preferredSystemLocales,
|
||||
|
||||
nativeThemeListener: createNativeThemeListener(ipcRenderer, window),
|
||||
restartApp: () => ipcRenderer.send('restart'),
|
||||
OS: {
|
||||
getClassName: () => ipcRenderer.sendSync('OS.getClassName'),
|
||||
hasCustomTitleBar: () => hasCustomTitleBar,
|
||||
|
|
|
@ -50,6 +50,7 @@ installSetting('hasStoriesDisabled');
|
|||
installSetting('hideMenuBar');
|
||||
installSetting('incomingCallNotification');
|
||||
installSetting('lastSyncTime');
|
||||
installSetting('localeOverride');
|
||||
installSetting('notificationDrawAttention');
|
||||
installSetting('notificationSetting');
|
||||
installSetting('spellCheck');
|
||||
|
|
|
@ -20,6 +20,7 @@ SettingsWindowProps.onRender(
|
|||
({
|
||||
addCustomColor,
|
||||
availableCameras,
|
||||
availableLocales,
|
||||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
blockedCount,
|
||||
|
@ -78,6 +79,7 @@ SettingsWindowProps.onRender(
|
|||
onHideMenuBarChange,
|
||||
onIncomingCallNotificationsChange,
|
||||
onLastSyncTimeChange,
|
||||
onLocaleChange,
|
||||
onMediaCameraPermissionsChange,
|
||||
onMediaPermissionsChange,
|
||||
onMessageAudioChange,
|
||||
|
@ -98,16 +100,19 @@ SettingsWindowProps.onRender(
|
|||
onWhoCanFindMeChange,
|
||||
onWhoCanSeeMeChange,
|
||||
onZoomFactorChange,
|
||||
preferredSystemLocales,
|
||||
removeCustomColor,
|
||||
removeCustomColorOnConversations,
|
||||
resetAllChatColors,
|
||||
resetDefaultChatColor,
|
||||
resolvedLocale,
|
||||
selectedCamera,
|
||||
selectedMicrophone,
|
||||
selectedSpeaker,
|
||||
sentMediaQualitySetting,
|
||||
setGlobalDefaultConversationColor,
|
||||
shouldShowStoriesSettings,
|
||||
localeOverride,
|
||||
themeSetting,
|
||||
universalExpireTimer,
|
||||
whoCanFindMe,
|
||||
|
@ -118,6 +123,7 @@ SettingsWindowProps.onRender(
|
|||
<Preferences
|
||||
addCustomColor={addCustomColor}
|
||||
availableCameras={availableCameras}
|
||||
availableLocales={availableLocales}
|
||||
availableMicrophones={availableMicrophones}
|
||||
availableSpeakers={availableSpeakers}
|
||||
blockedCount={blockedCount}
|
||||
|
@ -167,6 +173,7 @@ SettingsWindowProps.onRender(
|
|||
isSyncSupported={isSyncSupported}
|
||||
isSystemTraySupported={isSystemTraySupported}
|
||||
lastSyncTime={lastSyncTime}
|
||||
localeOverride={localeOverride}
|
||||
makeSyncRequest={makeSyncRequest}
|
||||
notificationContent={notificationContent}
|
||||
onAudioNotificationsChange={onAudioNotificationsChange}
|
||||
|
@ -179,6 +186,7 @@ SettingsWindowProps.onRender(
|
|||
onHideMenuBarChange={onHideMenuBarChange}
|
||||
onIncomingCallNotificationsChange={onIncomingCallNotificationsChange}
|
||||
onLastSyncTimeChange={onLastSyncTimeChange}
|
||||
onLocaleChange={onLocaleChange}
|
||||
onMediaCameraPermissionsChange={onMediaCameraPermissionsChange}
|
||||
onMediaPermissionsChange={onMediaPermissionsChange}
|
||||
onMessageAudioChange={onMessageAudioChange}
|
||||
|
@ -201,10 +209,12 @@ SettingsWindowProps.onRender(
|
|||
onWhoCanFindMeChange={onWhoCanFindMeChange}
|
||||
onWhoCanSeeMeChange={onWhoCanSeeMeChange}
|
||||
onZoomFactorChange={onZoomFactorChange}
|
||||
preferredSystemLocales={preferredSystemLocales}
|
||||
removeCustomColorOnConversations={removeCustomColorOnConversations}
|
||||
removeCustomColor={removeCustomColor}
|
||||
resetAllChatColors={resetAllChatColors}
|
||||
resetDefaultChatColor={resetDefaultChatColor}
|
||||
resolvedLocale={resolvedLocale}
|
||||
selectedCamera={selectedCamera}
|
||||
selectedMicrophone={selectedMicrophone}
|
||||
selectedSpeaker={selectedSpeaker}
|
||||
|
|
|
@ -46,6 +46,7 @@ const settingSpellCheck = createSetting('spellCheck');
|
|||
const settingTextFormatting = createSetting('textFormatting');
|
||||
const settingTheme = createSetting('themeSetting');
|
||||
const settingSystemTraySetting = createSetting('systemTraySetting');
|
||||
const settingLocaleOverride = createSetting('localeOverride');
|
||||
|
||||
const settingLastSyncTime = createSetting('lastSyncTime');
|
||||
|
||||
|
@ -169,6 +170,7 @@ async function renderPreferences() {
|
|||
selectedMicrophone,
|
||||
selectedSpeaker,
|
||||
sentMediaQualitySetting,
|
||||
localeOverride,
|
||||
systemTraySetting,
|
||||
themeSetting,
|
||||
universalExpireTimer,
|
||||
|
@ -210,6 +212,7 @@ async function renderPreferences() {
|
|||
selectedMicrophone: settingAudioInput.getValue(),
|
||||
selectedSpeaker: settingAudioOutput.getValue(),
|
||||
sentMediaQualitySetting: settingSentMediaQuality.getValue(),
|
||||
localeOverride: settingLocaleOverride.getValue(),
|
||||
systemTraySetting: settingSystemTraySetting.getValue(),
|
||||
themeSetting: settingTheme.getValue(),
|
||||
universalExpireTimer: settingUniversalExpireTimer.getValue(),
|
||||
|
@ -236,9 +239,15 @@ async function renderPreferences() {
|
|||
settingUniversalExpireTimer.setValue
|
||||
);
|
||||
|
||||
const availableLocales = MinimalSignalContext.getI18nAvailableLocales();
|
||||
const resolvedLocale = MinimalSignalContext.getI18nLocale();
|
||||
const preferredSystemLocales =
|
||||
MinimalSignalContext.getPreferredSystemLocales();
|
||||
|
||||
const props = {
|
||||
// Settings
|
||||
availableCameras,
|
||||
availableLocales,
|
||||
availableMicrophones,
|
||||
availableSpeakers,
|
||||
blockedCount,
|
||||
|
@ -268,7 +277,10 @@ async function renderPreferences() {
|
|||
hasTextFormatting,
|
||||
hasTypingIndicators,
|
||||
lastSyncTime,
|
||||
localeOverride,
|
||||
notificationContent,
|
||||
preferredSystemLocales,
|
||||
resolvedLocale,
|
||||
selectedCamera,
|
||||
selectedMicrophone,
|
||||
selectedSpeaker,
|
||||
|
@ -347,6 +359,10 @@ async function renderPreferences() {
|
|||
settingIncomingCallNotification.setValue
|
||||
),
|
||||
onLastSyncTimeChange: attachRenderCallback(settingLastSyncTime.setValue),
|
||||
onLocaleChange: async (locale: string | null) => {
|
||||
await settingLocaleOverride.setValue(locale);
|
||||
MinimalSignalContext.restartApp();
|
||||
},
|
||||
onMediaCameraPermissionsChange: attachRenderCallback(
|
||||
settingMediaCameraPermissions.setValue
|
||||
),
|
||||
|
|
43
yarn.lock
43
yarn.lock
|
@ -1629,6 +1629,14 @@
|
|||
"@formatjs/intl-localematcher" "0.2.32"
|
||||
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":
|
||||
version "1.2.1"
|
||||
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"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/icu-messageformat-parser@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.1.tgz#953080ea5c053bc73bdf55d0a524a3c3c133ae6b"
|
||||
integrity sha512-knF2AkAKN4Upv4oIiKY4Wd/dLH68TNMPgV/tJMu/T6FP9aQwbv8fpj7U3lkyniPaNVxvia56Gxax8MKOjtxLSQ==
|
||||
"@formatjs/icu-messageformat-parser@2.4.0":
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.4.0.tgz#e165f3594c68416ce15f63793768251de2a85f88"
|
||||
integrity sha512-6Dh5Z/gp4F/HovXXu/vmd0If5NbYLB5dZrmhWVNb+BOGOEU3wt7Z/83KY1dtd7IDhAnYHasbmKE1RbTE0J+3hw==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.14.3"
|
||||
"@formatjs/icu-skeleton-parser" "1.3.18"
|
||||
"@formatjs/ecma402-abstract" "1.15.0"
|
||||
"@formatjs/icu-skeleton-parser" "1.4.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@formatjs/icu-skeleton-parser@1.3.13":
|
||||
|
@ -1717,6 +1725,14 @@
|
|||
"@formatjs/ecma402-abstract" "1.11.4"
|
||||
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":
|
||||
version "6.1.3"
|
||||
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"
|
||||
|
||||
intl-messageformat@^10.1.0:
|
||||
version "10.3.4"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.4.tgz#20f064c28b46fa6d352a4c4ba5e9bfc597af3eba"
|
||||
integrity sha512-/FxUIrlbPtuykSNX85CB5sp2FjLVeTmdD7TfRkVFPft2n4FgcSlAcilFytYiFAEmPHc+0PvpLCIPXeaGFzIvOg==
|
||||
version "10.3.5"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.5.tgz#f55684fc663e62616ad59d3a504ea0cac3f267b7"
|
||||
integrity sha512-6kPkftF8Jg3XJCkGKa5OD+nYQ+qcSxF4ZkuDdXZ6KGG0VXn+iblJqRFyDdm9VvKcMyC0Km2+JlVQffFM52D0YA==
|
||||
dependencies:
|
||||
"@formatjs/ecma402-abstract" "1.14.3"
|
||||
"@formatjs/ecma402-abstract" "1.15.0"
|
||||
"@formatjs/fast-memoize" "2.0.1"
|
||||
"@formatjs/icu-messageformat-parser" "2.3.1"
|
||||
"@formatjs/icu-messageformat-parser" "2.4.0"
|
||||
tslib "^2.4.0"
|
||||
|
||||
intl-messageformat@^9.3.19:
|
||||
|
@ -18836,6 +18852,11 @@ thread-stream@^2.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254"
|
||||
|
|
Loading…
Reference in a new issue