From 6655bfc57601a8a048b7574b9f09c622152184ef Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Wed, 15 May 2024 14:48:02 -0700 Subject: [PATCH] Make ICU types stricter for inline JSX --- _locales/en/messages.json | 2 +- .../AnnouncementsOnlyGroupBanner.tsx | 4 +- ts/components/CallNeedPermissionScreen.tsx | 4 +- ts/components/CallsList.tsx | 4 +- ts/components/CallsNewCall.tsx | 4 +- ts/components/Checkbox.tsx | 8 +-- ts/components/DialogUpdate.tsx | 8 +-- ts/components/GroupCallRemoteParticipant.tsx | 6 +- .../{Intl.stories.tsx => I18n.stories.tsx} | 20 +++---- ts/components/{Intl.tsx => I18n.tsx} | 4 +- ts/components/IncomingCallBar.tsx | 12 ++-- ...NewlyCreatedGroupInvitedContactsDialog.tsx | 4 +- ts/components/Preferences.tsx | 10 +++- ts/components/SafetyNumberNotReady.tsx | 8 +-- ts/components/SafetyNumberViewer.tsx | 6 +- ts/components/SharedGroupNames.tsx | 12 ++-- ts/components/SignalConnectionsModal.tsx | 4 +- ts/components/StoriesSettingsModal.tsx | 6 +- ts/components/StoryDetailsModal.tsx | 8 +-- ts/components/StoryViewer.tsx | 6 +- ts/components/UnsupportedOSDialog.tsx | 6 +- ts/components/WhatsNewLink.tsx | 4 +- ts/components/WhatsNewModal.tsx | 4 +- .../conversation/AboutContactModal.tsx | 6 +- .../conversation/ChangeNumberNotification.tsx | 4 +- .../ContactSpoofingReviewDialogPerson.tsx | 4 +- .../ConversationMergeNotification.tsx | 6 +- .../conversation/DeliveryIssueDialog.tsx | 6 +- .../DeliveryIssueNotification.tsx | 4 +- .../conversation/GroupNotification.tsx | 20 +++---- .../conversation/GroupV1DisabledActions.tsx | 4 +- .../conversation/GroupV1Migration.tsx | 10 ++-- ts/components/conversation/GroupV2Change.tsx | 6 +- .../MandatoryProfileSharingActions.tsx | 6 +- .../conversation/MessageRequestActions.tsx | 12 ++-- .../MessageRequestActionsConfirmation.tsx | 16 ++--- .../RemoveGroupMemberConfirmationDialog.tsx | 6 +- .../conversation/SafetyNumberNotification.tsx | 6 +- ts/components/conversation/Timeline.tsx | 8 +-- .../conversation/TimerNotification.tsx | 6 +- .../TitleTransitionNotification.tsx | 4 +- .../conversation/UnsupportedMessage.tsx | 10 ++-- .../conversation/VerificationNotification.tsx | 10 ++-- .../ConfirmAdditionsModal.tsx | 6 +- .../conversationList/ContactListItem.tsx | 8 +-- .../conversationList/MessageSearchResult.tsx | 12 ++-- .../InstallScreenQrCodeNotScannedStep.tsx | 8 +-- .../InstallScreenUpdateDialog.tsx | 10 ++-- .../leftPane/LeftPaneInboxHelper.tsx | 4 +- .../leftPane/LeftPaneSearchHelper.tsx | 4 +- ts/scripts/generate-icu-types.ts | 58 ++++++++++++------- ts/scripts/remove-strings.ts | 2 +- 52 files changed, 220 insertions(+), 200 deletions(-) rename ts/components/{Intl.stories.tsx => I18n.stories.tsx} (86%) rename ts/components/{Intl.tsx => I18n.tsx} (87%) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 86b2f94705d..2959e85f9b9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5775,7 +5775,7 @@ "description": "Title for the auto convert emoji setting" }, "icu:Preferences__auto-convert-emoji--description": { - "messageformat": "For example, :-) will be converted to 🙂", + "messageformat": "For example, :-) will be converted to 🙂", "description": "Description for the auto convert emoji setting" }, "icu:Preferences--advanced": { diff --git a/ts/components/AnnouncementsOnlyGroupBanner.tsx b/ts/components/AnnouncementsOnlyGroupBanner.tsx index 2103791253c..f40e7cccb4c 100644 --- a/ts/components/AnnouncementsOnlyGroupBanner.tsx +++ b/ts/components/AnnouncementsOnlyGroupBanner.tsx @@ -6,7 +6,7 @@ import type { ConversationType, ShowConversationType, } from '../state/ducks/conversations'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import type { LocalizerType, ThemeType } from '../types/Util'; import { Modal } from './Modal'; import { ConversationListItem } from './conversationList/ConversationListItem'; @@ -51,7 +51,7 @@ export function AnnouncementsOnlyGroupBanner({ )}
-

- JSX.Element; - description?: string; + description?: ReactNode; disabled?: boolean; isRadio?: boolean; label: string; @@ -62,9 +62,7 @@ export const Checkbox = forwardRef(function CheckboxInner(

); diff --git a/ts/components/DialogUpdate.tsx b/ts/components/DialogUpdate.tsx index e3af3b1ecf2..45626d938bf 100644 --- a/ts/components/DialogUpdate.tsx +++ b/ts/components/DialogUpdate.tsx @@ -7,7 +7,7 @@ import { isBeta } from '../util/version'; import { DialogType } from '../types/Dialogs'; import type { LocalizerType } from '../types/Util'; import { PRODUCTION_DOWNLOAD_URL, BETA_DOWNLOAD_URL } from '../types/support'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { LeftPaneDialog } from './LeftPaneDialog'; import type { WidthBreakpoint } from './_util'; import { formatFileSize } from '../util/formatFileSize'; @@ -48,7 +48,7 @@ export function DialogUpdate({ title={i18n('icu:cannotUpdate')} > - - - Signal.app, folder: /Applications, diff --git a/ts/components/GroupCallRemoteParticipant.tsx b/ts/components/GroupCallRemoteParticipant.tsx index a669968fd4f..433e715520f 100644 --- a/ts/components/GroupCallRemoteParticipant.tsx +++ b/ts/components/GroupCallRemoteParticipant.tsx @@ -22,7 +22,7 @@ import { } from './CallingAudioIndicator'; import { Avatar, AvatarSize } from './Avatar'; import { ConfirmationDialog } from './ConfirmationDialog'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { ContactName } from './conversation/ContactName'; import { useIntersectionObserver } from '../hooks/useIntersectionObserver'; import { MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH } from '../calling/constants'; @@ -408,7 +408,7 @@ export const GroupCallRemoteParticipant: React.FC = React.memo( if (isBlocked) { setErrorDialogTitle(
- = React.memo( } else if (showMissingMediaKeys) { setErrorDialogTitle(
- ): JSX.Element { - return ; + return ; } export function SingleStringReplacement( args: Props<'icu:leftTheGroup'> ): JSX.Element { return ( - + ); } @@ -39,7 +39,7 @@ export function SingleTagReplacement( args: Props<'icu:leftTheGroup'> ): JSX.Element { return ( - ): JSX.Element { return ( - ): JSX.Element { return ( - Fred, name2: The Fredster }} @@ -81,7 +81,7 @@ export function Emoji( args: Props<'icu:Message__reaction-emoji-label--you'> ): JSX.Element { return ( - = { components: ICUJSXMessageParamsByKeyType[Key]; }); -export function Intl({ +export function I18n({ components, id, // Indirection for linter/migration tooling i18n: localizer, }: Props): JSX.Element | null { if (!id) { - log.error('Error: Intl id prop not provided'); + log.error('Error: id prop not provided'); return null; } diff --git a/ts/components/IncomingCallBar.tsx b/ts/components/IncomingCallBar.tsx index d9431a90e4e..388e71ddc51 100644 --- a/ts/components/IncomingCallBar.tsx +++ b/ts/components/IncomingCallBar.tsx @@ -5,7 +5,7 @@ import type { ReactChild } from 'react'; import React, { useCallback, useEffect, useRef } from 'react'; import { Avatar, AvatarSize } from './Avatar'; import { Tooltip } from './Tooltip'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { Theme } from '../util/theme'; import { getParticipantName } from '../util/callingGetParticipantName'; import { ContactName } from './conversation/ContactName'; @@ -124,7 +124,7 @@ function GroupCallMessage({ switch (otherMembersRung.length) { case 0: return ( - - }} diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index 4bc28b98293..0cc03bc0966 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -66,6 +66,7 @@ import { Modal } from './Modal'; import { SearchInput } from './SearchInput'; import { removeDiacritics } from '../util/removeDiacritics'; import { assertDev } from '../util/assert'; +import { I18n } from './I18n'; type CheckboxChangeHandlerType = (value: boolean) => unknown; type SelectChangeHandlerType = (value: T) => unknown; @@ -865,9 +866,12 @@ export function Preferences({ /> + } label={i18n('icu:Preferences__auto-convert-emoji--title')} moduleClassName="Preferences__checkbox" name="autoConvertEmoji" diff --git a/ts/components/SafetyNumberNotReady.tsx b/ts/components/SafetyNumberNotReady.tsx index 5558520e693..01fb3646d4c 100644 --- a/ts/components/SafetyNumberNotReady.tsx +++ b/ts/components/SafetyNumberNotReady.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { Button, ButtonVariant } from './Button'; import { Modal } from './Modal'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import type { LocalizerType } from '../types/Util'; import { SAFETY_NUMBER_URL } from '../types/support'; @@ -26,15 +26,15 @@ export function SafetyNumberNotReady({ return (
- +
diff --git a/ts/components/SafetyNumberViewer.tsx b/ts/components/SafetyNumberViewer.tsx index 76aedb35abb..109c1d01418 100644 --- a/ts/components/SafetyNumberViewer.tsx +++ b/ts/components/SafetyNumberViewer.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { Button, ButtonVariant } from './Button'; import { QrCode } from './QrCode'; import type { ConversationType } from '../state/ducks/conversations'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { Emojify } from './conversation/Emojify'; import type { LocalizerType } from '../types/Util'; import type { SafetyNumberType } from '../types/safetyNumber'; @@ -95,14 +95,14 @@ export function SafetyNumberViewer({ {safetyNumberCard} diff --git a/ts/components/SharedGroupNames.tsx b/ts/components/SharedGroupNames.tsx index 5b284e9d8d2..16be25470dd 100644 --- a/ts/components/SharedGroupNames.tsx +++ b/ts/components/SharedGroupNames.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { take } from 'lodash'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import type { LocalizerType } from '../types/Util'; import { UserText } from './UserText'; @@ -30,7 +30,7 @@ export function SharedGroupNames({ if (sharedGroupNames.length >= 5) { const remainingCount = sharedGroupNames.length - 3; return ( - = 2) { return ( - = 1) { return ( -
- {i18n('icu:SignalConnectionsModal__title')} diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx index b709e4b2a91..8ec096ce25d 100644 --- a/ts/components/StoriesSettingsModal.tsx +++ b/ts/components/StoriesSettingsModal.tsx @@ -22,7 +22,7 @@ import { ContactPills } from './ContactPills'; import { ContactPill } from './ContactPill'; import { ConversationList, RowType } from './ConversationList'; import { Input } from './Input'; -import { Intl } from './Intl'; +import { I18n } from './I18n'; import { MY_STORY_ID, getStoryDistributionListName } from '../types/Stories'; import { PagedModal, ModalPage } from './Modal'; import { SearchInput } from './SearchInput'; @@ -836,13 +836,13 @@ export function EditMyStoryPrivacy({ const disclaimerElement = (
{kind === 'mine' ? ( - ) : ( -
- {attachment && (
- 0 && (
- {i18n('icu:StoryViewer__views-off')} )} {isSent && hasViewReceiptSetting && ( - 0) && replyCount > 0 && ' '} {replyCount > 0 && ( - ], + features: [], }; if (releaseNotes.features.length === 1 && !releaseNotes.header) { diff --git a/ts/components/conversation/AboutContactModal.tsx b/ts/components/conversation/AboutContactModal.tsx index 8ce1762b3f8..97f9c6f2e5d 100644 --- a/ts/components/conversation/AboutContactModal.tsx +++ b/ts/components/conversation/AboutContactModal.tsx @@ -11,7 +11,7 @@ import { Modal } from '../Modal'; import { UserText } from '../UserText'; import { SharedGroupNames } from '../SharedGroupNames'; import { About } from './About'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import { areNicknamesEnabled, canHaveNicknameAndNote, @@ -154,7 +154,7 @@ export function AboutContactModal({ (conversation.nicknameGivenName || conversation.nicknameFamilyName) && conversation.titleNoNickname ? ( - -
-
-
- diff --git a/ts/components/conversation/DeliveryIssueDialog.tsx b/ts/components/conversation/DeliveryIssueDialog.tsx index 445b12a50f9..e3804a491b0 100644 --- a/ts/components/conversation/DeliveryIssueDialog.tsx +++ b/ts/components/conversation/DeliveryIssueDialog.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { Button, ButtonSize, ButtonVariant } from '../Button'; import type { ConversationType } from '../../state/ducks/conversations'; import { Modal } from '../Modal'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import { Emojify } from './Emojify'; import { useRestoreFocus } from '../../hooks/useRestoreFocus'; @@ -76,13 +76,13 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement {
{inGroup ? ( - ) : ( - , diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index aa4b6a3c165..7e25f6bf041 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -7,7 +7,7 @@ import { compact, flatten } from 'lodash'; import { ContactName } from './ContactName'; import { SystemMessage } from './SystemMessage'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import type { LocalizerType } from '../../types/Util'; import { missingCaseError } from '../../util/missingCaseError'; @@ -69,14 +69,14 @@ function GroupNotificationChange({ switch (type) { case 'name': return ( - ); case 'avatar': - return ; + return ; case 'add': if (!contacts || !contacts.length) { throw new Error('Group update is missing contacts'); @@ -87,13 +87,13 @@ function GroupNotificationChange({ {otherPeople.length > 0 && ( <> {otherPeople.length === 1 ? ( - ) : ( - - +
)} @@ -118,13 +118,13 @@ function GroupNotificationChange({ } return contacts.length > 1 ? ( - ) : ( - + ) : ( - }} diff --git a/ts/components/conversation/GroupV1DisabledActions.tsx b/ts/components/conversation/GroupV1DisabledActions.tsx index cb8f0a5a9cf..19f73aec843 100644 --- a/ts/components/conversation/GroupV1DisabledActions.tsx +++ b/ts/components/conversation/GroupV1DisabledActions.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import type { LocalizerType } from '../../types/Util'; export type PropsType = { @@ -19,7 +19,7 @@ export function GroupV1DisabledActions({ return (

- {kind === 'invited' && ( - )} {kind === 'removed' && ( - {kind === 'invited' && ( - )} {kind === 'removed' && ( - ( i18n: LocalizerType, components: ICUJSXMessageParamsByKeyType[Key] ): JSX.Element { - return ; + return ; } enum ModalState { @@ -229,7 +229,7 @@ function GroupV2Detail({ i18n={i18n} onClose={() => setModalState(ModalState.None)} > -

{conversationType === 'direct' ? ( - ) : ( - + ); } else { - message = ; + message = ; } } diff --git a/ts/components/conversation/MessageRequestActionsConfirmation.tsx b/ts/components/conversation/MessageRequestActionsConfirmation.tsx index af4ff793047..deedebc876a 100644 --- a/ts/components/conversation/MessageRequestActionsConfirmation.tsx +++ b/ts/components/conversation/MessageRequestActionsConfirmation.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import type { ContactNameData } from './ContactName'; import { ContactName } from './ContactName'; import { ConfirmationDialog } from '../ConfirmationDialog'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import type { LocalizerType } from '../../types/Util'; export enum MessageRequestState { @@ -65,7 +65,7 @@ export function MessageRequestActionsConfirmation({ }} title={ conversationType === 'direct' ? ( - ) : ( - ) : ( - - ) : ( - ) : ( - + ) } button={ diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index 065aad14a2f..7efef17f9ff 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -18,7 +18,7 @@ import { clearTimeoutIfNecessary } from '../../util/clearTimeoutIfNecessary'; import { WidthBreakpoint } from '../_util'; import { ErrorBoundary } from './ErrorBoundary'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import { TimelineWarning } from './TimelineWarning'; import { TimelineWarnings } from './TimelineWarnings'; import { NewlyCreatedGroupInvitedContactsDialog } from '../NewlyCreatedGroupInvitedContactsDialog'; @@ -995,7 +995,7 @@ export class Timeline extends React.Component< switch (warning.type) { case ContactSpoofingType.DirectConversationWithSameTitle: text = ( - ) : ( - , diff --git a/ts/components/conversation/UnsupportedMessage.tsx b/ts/components/conversation/UnsupportedMessage.tsx index 2581b4ec756..5998446edab 100644 --- a/ts/components/conversation/UnsupportedMessage.tsx +++ b/ts/components/conversation/UnsupportedMessage.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { SystemMessage } from './SystemMessage'; import { Button, ButtonSize, ButtonVariant } from '../Button'; import { ContactName } from './ContactName'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import type { LocalizerType } from '../../types/Util'; import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser'; @@ -43,25 +43,25 @@ function UnsupportedMessageContents({ canProcessNow, contact, i18n }: Props) { if (isMe) { if (canProcessNow) { return ( - ); } - return ; + return ; } if (canProcessNow) { return ( - ); } return ( - + ) : ( - ) : ( - = React.memo( i18n={i18n} onClose={() => setConfirmingBlocking(false)} title={ - = React.memo( i18n={i18n} onClose={() => setConfirmingRemoving(false)} title={ - = React.memo( i18n={i18n} onClose={() => setConfirmingRemoving(false)} title={ - ): JSX.Element => person.isMe ? ( - + ) : ( ); @@ -112,7 +112,7 @@ export const MessageSearchResult: FunctionComponent = React.memo( if (to.type === 'group') { headerName = ( - = React.memo( } else { headerName = ( - = React.memo( if (to.type === 'group') { headerName = ( - = React.memo( } else { headerName = ( -

  • {i18n('icu:Install__instructions__1')}
  • -
  • - - - - Signal.app, folder: /Applications, diff --git a/ts/components/leftPane/LeftPaneInboxHelper.tsx b/ts/components/leftPane/LeftPaneInboxHelper.tsx index edc7797a1f8..f748cb1d5c0 100644 --- a/ts/components/leftPane/LeftPaneInboxHelper.tsx +++ b/ts/components/leftPane/LeftPaneInboxHelper.tsx @@ -5,7 +5,7 @@ import { last } from 'lodash'; import type { ReactChild } from 'react'; import React from 'react'; -import { Intl } from '../Intl'; +import { I18n } from '../I18n'; import type { ToFindType } from './LeftPaneHelper'; import type { ConversationType, @@ -118,7 +118,7 @@ export class LeftPaneInboxHelper extends LeftPaneHelper return (
    - { return a.localeCompare(b); }) as Array; -function generateType( - name: string, - stringType: ts.TypeNode, - componentType: ts.TypeNode -): ts.Statement { +function filterDefaultParams(params: Map) { + const filteredParams = new Map(); + + for (const [key, value] of params) { + if (key === 'emojify') { + continue; + } + + filteredParams.set(key, value); + } + + return filteredParams; +} + +const ComponentOrStringNode = + ts.factory.createTypeReferenceNode('ComponentOrString'); +const ComponentNode = ts.factory.createTypeReferenceNode('Component'); +const StringToken = ts.factory.createToken(ts.SyntaxKind.StringKeyword); +const NeverToken = ts.factory.createToken(ts.SyntaxKind.NeverKeyword); + +function generateType(name: string, supportsComponents: boolean): ts.Statement { const props = new Array(); for (const key of messageKeys) { if (key === 'smartling') { @@ -70,7 +86,21 @@ function generateType( const { messageformat } = message; - const params = getICUMessageParams(messageformat); + const rawParams = getICUMessageParams(messageformat); + const params = filterDefaultParams(rawParams); + + if (!supportsComponents) { + const needsComponents = Array.from(rawParams.values()).some(value => { + return value.type === 'jsx'; + }); + + if (needsComponents) { + continue; + } + } + + const stringType = supportsComponents ? ComponentOrStringNode : StringToken; + const componentType = supportsComponents ? ComponentNode : NeverToken; let paramType: ts.TypeNode; if (params.size === 0) { @@ -193,21 +223,9 @@ statements.push( ) ); -statements.push( - generateType( - 'ICUJSXMessageParamsByKeyType', - ts.factory.createTypeReferenceNode('ComponentOrString'), - ts.factory.createTypeReferenceNode('Component') - ) -); +statements.push(generateType('ICUJSXMessageParamsByKeyType', true)); -statements.push( - generateType( - 'ICUStringMessageParamsByKeyType', - ts.factory.createToken(ts.SyntaxKind.StringKeyword), - ts.factory.createToken(ts.SyntaxKind.NeverKeyword) - ) -); +statements.push(generateType('ICUStringMessageParamsByKeyType', false)); const root = ts.factory.createSourceFile( statements, diff --git a/ts/scripts/remove-strings.ts b/ts/scripts/remove-strings.ts index b7b26d9908a..213a2155cd1 100644 --- a/ts/scripts/remove-strings.ts +++ b/ts/scripts/remove-strings.ts @@ -40,7 +40,7 @@ async function main() { // Find uses in either: // - `i18n('key')` - // - `` + // - `` try { const result = await execa(