Make valid-i18n-keys rule strict and fix most exceptions

This commit is contained in:
Jamie Kyle
2023-03-29 10:15:54 -07:00
committed by GitHub
parent 18a6da310f
commit 11cfcb4e32
36 changed files with 796 additions and 687 deletions

View File

@@ -60,16 +60,8 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
i18n('GroupV1--Migration--invited--you')
) : (
<>
{renderUsers(
invitedMembers,
i18n,
'GroupV1--Migration--invited'
)}
{renderUsers(
droppedMembers,
i18n,
'GroupV1--Migration--removed'
)}
{renderUsers(invitedMembers, i18n, 'invited')}
{renderUsers(droppedMembers, i18n, 'removed')}
</>
)}
</p>
@@ -106,31 +98,52 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
function renderUsers(
members: Array<ConversationType>,
i18n: LocalizerType,
keyPrefix: string
kind: 'invited' | 'removed'
): React.ReactElement | null {
if (!members || members.length === 0) {
return null;
}
if (members.length === 1) {
const contact = <ContactName title={members[0].title} />;
return (
<p>
<Intl
i18n={i18n}
id={`${keyPrefix}--one`}
components={{
contact: <ContactName title={members[0].title} />,
}}
/>
{kind === 'invited' && (
<Intl
i18n={i18n}
id="GroupV1--Migration--invited--one"
components={{ contact }}
/>
)}
{kind === 'removed' && (
<Intl
i18n={i18n}
id="GroupV1--Migration--removed--one"
components={{ contact }}
/>
)}
</p>
);
}
const count = members.length.toString();
return (
<p>
{i18n(`${keyPrefix}--many`, {
count: members.length.toString(),
})}
{kind === 'invited' && members.length > 1 && (
<Intl
i18n={i18n}
id="GroupV1--Migration--invited--many"
components={{ count }}
/>
)}
{kind === 'removed' && members.length > 1 && (
<Intl
i18n={i18n}
id="GroupV1--Migration--removed--many"
components={{ count }}
/>
)}
</p>
);
}

View File

@@ -40,6 +40,26 @@ export function MandatoryProfileSharingActions({
}: Props): JSX.Element {
const [mrState, setMrState] = React.useState(MessageRequestState.default);
const firstNameContact = (
<strong
key="name"
className="module-message-request-actions__message__name"
>
<ContactName firstName={firstName} title={title} preferFirstName />
</strong>
);
const learnMore = (
<a
href="https://support.signal.org/hc/articles/360007459591"
target="_blank"
rel="noreferrer"
className="module-message-request-actions__message__learn-more"
>
{i18n('MessageRequests--learn-more')}
</a>
);
return (
<>
{mrState !== MessageRequestState.default ? (
@@ -62,34 +82,19 @@ export function MandatoryProfileSharingActions({
) : null}
<div className="module-message-request-actions">
<p className="module-message-request-actions__message">
<Intl
i18n={i18n}
id={`MessageRequests--profile-sharing--${conversationType}`}
components={{
firstName: (
<strong
key="name"
className="module-message-request-actions__message__name"
>
<ContactName
firstName={firstName}
title={title}
preferFirstName
/>
</strong>
),
learnMore: (
<a
href="https://support.signal.org/hc/articles/360007459591"
target="_blank"
rel="noreferrer"
className="module-message-request-actions__message__learn-more"
>
{i18n('MessageRequests--learn-more')}
</a>
),
}}
/>
{conversationType === 'direct' ? (
<Intl
i18n={i18n}
id="MessageRequests--profile-sharing--direct"
components={{ firstName: firstNameContact, learnMore }}
/>
) : (
<Intl
i18n={i18n}
id="MessageRequests--profile-sharing--group"
components={{ firstName: firstNameContact, learnMore }}
/>
)}
</p>
<div className="module-message-request-actions__buttons">
<Button

View File

@@ -1251,7 +1251,10 @@ export class Message extends React.PureComponent<Props, State> {
}
if (giftBadge.state === GiftBadgeStates.Unopened) {
const description = i18n(`icu:message--donation--unopened--${direction}`);
const description =
direction === 'incoming'
? i18n('icu:message--donation--unopened--incoming')
: i18n('icu:message--donation--unopened--outgoing');
const isRTL = getDirection(description) === 'rtl';
const { metadataWidth } = this.state;
@@ -1931,26 +1934,23 @@ export class Message extends React.PureComponent<Props, State> {
isTapToViewError,
} = this.props;
const incomingString = isTapToViewExpired
? i18n('Message--tap-to-view-expired')
: i18n(
`Message--tap-to-view--incoming${
isVideo(attachments) ? '-video' : ''
}`
);
const outgoingString = i18n('Message--tap-to-view--outgoing');
const isDownloadPending = this.isAttachmentPending();
if (isDownloadPending) {
return;
}
// eslint-disable-next-line no-nested-ternary
return isTapToViewError
? i18n('incomingError')
: direction === 'outgoing'
? outgoingString
: incomingString;
if (isTapToViewError) {
return i18n('incomingError');
}
if (direction === 'outgoing') {
return i18n('Message--tap-to-view--outgoing');
}
if (isTapToViewExpired) {
return i18n('Message--tap-to-view-expired');
}
if (isVideo(attachments)) {
return i18n('Message--tap-to-view--incoming-video');
}
return i18n('Message--tap-to-view--incoming');
}
public renderTapToView(): JSX.Element {

View File

@@ -26,6 +26,7 @@ import * as log from '../../logging/log';
import { formatDateTimeLong } from '../../util/timestamp';
import { DurationInSeconds } from '../../util/durations';
import { format as formatRelativeTime } from '../../util/expirationTimer';
import { missingCaseError } from '../../util';
export type Contact = Pick<
ConversationType,
@@ -200,24 +201,49 @@ export class MessageDetail extends React.Component<Props> {
);
}
private renderContactGroupHeaderText(
sendStatus: undefined | SendStatus
): string {
const { i18n } = this.props;
if (sendStatus === undefined) {
return i18n('from');
}
switch (sendStatus) {
case SendStatus.Failed:
return i18n('MessageDetailsHeader--Failed');
case SendStatus.Pending:
return i18n('MessageDetailsHeader--Pending');
case SendStatus.Sent:
return i18n('MessageDetailsHeader--Sent');
case SendStatus.Delivered:
return i18n('MessageDetailsHeader--Delivered');
case SendStatus.Read:
return i18n('MessageDetailsHeader--Read');
case SendStatus.Viewed:
return i18n('MessageDetailsHeader--Viewed');
default:
throw missingCaseError(sendStatus);
}
}
private renderContactGroup(
sendStatus: undefined | SendStatus,
contacts: undefined | ReadonlyArray<Contact>
): ReactNode {
const { i18n } = this.props;
if (!contacts || !contacts.length) {
return null;
}
const i18nKey =
sendStatus === undefined ? 'from' : `MessageDetailsHeader--${sendStatus}`;
const sortedContacts = [...contacts].sort((a, b) =>
contactSortCollator.compare(a.title, b.title)
);
const headerText = this.renderContactGroupHeaderText(sendStatus);
return (
<div key={i18nKey} className="module-message-detail__contact-group">
<div key={headerText} className="module-message-detail__contact-group">
<div
className={classNames(
'module-message-detail__contact-group__header',
@@ -225,8 +251,7 @@ export class MessageDetail extends React.Component<Props> {
`module-message-detail__contact-group__header--${sendStatus}`
)}
>
{/* eslint-disable-next-line local-rules/valid-i18n-keys */}
{i18n(i18nKey)}
{headerText}
</div>
{sortedContacts.map(contact => this.renderContact(contact))}
</div>

View File

@@ -35,6 +35,15 @@ export function MessageRequestActions({
}: Props): JSX.Element {
const [mrState, setMrState] = React.useState(MessageRequestState.default);
const name = (
<strong
key="name"
className="module-message-request-actions__message__name"
>
<ContactName firstName={firstName} title={title} preferFirstName />
</strong>
);
return (
<>
{mrState !== MessageRequestState.default ? (
@@ -53,26 +62,34 @@ export function MessageRequestActions({
) : null}
<div className="module-message-request-actions">
<p className="module-message-request-actions__message">
<Intl
i18n={i18n}
id={`MessageRequests--message-${conversationType}${
isBlocked ? '-blocked' : ''
}`}
components={{
name: (
<strong
key="name"
className="module-message-request-actions__message__name"
>
<ContactName
firstName={firstName}
title={title}
preferFirstName
/>
</strong>
),
}}
/>
{conversationType === 'direct' && isBlocked && (
<Intl
i18n={i18n}
id="MessageRequests--message-direct-blocked"
components={{ name }}
/>
)}
{conversationType === 'direct' && !isBlocked && (
<Intl
i18n={i18n}
id="MessageRequests--message-direct"
components={{ name }}
/>
)}
{conversationType === 'group' && isBlocked && (
<Intl
i18n={i18n}
id="MessageRequests--message-group-blocked"
components={{ name }}
/>
)}
{conversationType === 'group' && !isBlocked && (
<Intl
i18n={i18n}
id="MessageRequests--message-group"
components={{ name }}
/>
)}
</p>
<div className="module-message-request-actions__buttons">
<Button

View File

@@ -132,13 +132,23 @@ export function MessageRequestActionsConfirmation({
onChangeState(MessageRequestState.default);
}}
title={
<Intl
i18n={i18n}
id={`MessageRequests--delete-${conversationType}-confirm-title`}
components={{
title: <ContactName key="name" title={title} />,
}}
/>
conversationType === 'direct' ? (
<Intl
i18n={i18n}
id="MessageRequests--delete-direct-confirm-title"
components={{
title: <ContactName key="name" title={title} />,
}}
/>
) : (
<Intl
i18n={i18n}
id="MessageRequests--delete-group-confirm-title"
components={{
title: <ContactName key="name" title={title} />,
}}
/>
)
}
actions={[
{

View File

@@ -35,32 +35,30 @@ export function SafetyNumberNotification({
i18n,
toggleSafetyNumberModal,
}: Props): JSX.Element {
const changeKey = isGroup
? 'safetyNumberChangedGroup'
: 'safetyNumberChanged';
const name = (
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
title={contact.title}
module="module-safety-number-notification__contact"
/>
</span>
);
return (
<SystemMessage
icon="safety-number"
contents={
// eslint-disable-next-line local-rules/valid-i18n-keys
<Intl
id={changeKey}
components={{
name: (
<span
key="external-1"
className="module-safety-number-notification__contact"
>
<ContactName
title={contact.title}
module="module-safety-number-notification__contact"
/>
</span>
),
}}
i18n={i18n}
/>
isGroup ? (
<Intl
id="safetyNumberChangedGroup"
components={{ name }}
i18n={i18n}
/>
) : (
<Intl id="safetyNumberChanged" components={{ name }} i18n={i18n} />
)
}
button={
<Button

View File

@@ -41,51 +41,46 @@ export type Props = PropsData & PropsHousekeeping;
export function TimerNotification(props: Props): JSX.Element {
const { disabled, i18n, title, type } = props;
let changeKey: string;
let timespan: string;
if (props.disabled) {
changeKey = 'disabledDisappearingMessages';
timespan = ''; // Set to the empty string to satisfy types
} else {
changeKey = 'theyChangedTheTimer';
timespan = expirationTimer.format(i18n, props.expireTimer);
}
const name = <ContactName key="external-1" title={title} />;
let message: ReactNode;
switch (type) {
case 'fromOther':
message = (
// eslint-disable-next-line local-rules/valid-i18n-keys
message = props.disabled ? (
<Intl
i18n={i18n}
id={changeKey}
components={{
name: <ContactName key="external-1" title={title} />,
time: timespan,
}}
id="disabledDisappearingMessages"
components={{ name }}
/>
) : (
<Intl
i18n={i18n}
id="theyChangedTheTimer"
components={{ name, time: timespan }}
/>
);
break;
case 'fromMe':
message = disabled
? i18n('youDisabledDisappearingMessages')
: i18n('youChangedTheTimer', {
time: timespan,
});
: i18n('youChangedTheTimer', { time: timespan });
break;
case 'fromSync':
message = disabled
? i18n('disappearingMessagesDisabled')
: i18n('timerSetOnSync', {
time: timespan,
});
: i18n('timerSetOnSync', { time: timespan });
break;
case 'fromMember':
message = disabled
? i18n('disappearingMessagesDisabledByMember')
: i18n('timerSetByMember', {
time: timespan,
});
: i18n('timerSetByMember', { time: timespan });
break;
default:
log.warn('TimerNotification: unsupported type provided:', type);

View File

@@ -30,42 +30,64 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
function UnsupportedMessageContents({ canProcessNow, contact, i18n }: Props) {
const { isMe } = contact;
const contactName = (
<span key="external-1" className="module-unsupported-message__contact">
<ContactName
title={contact.title}
module="module-unsupported-message__contact"
/>
</span>
);
if (isMe) {
if (canProcessNow) {
return (
<Intl
id="Message--unsupported-message-ask-to-resend"
components={{ contact: contactName }}
i18n={i18n}
/>
);
}
return (
<Intl
id="Message--from-me-unsupported-message"
components={{ contact: contactName }}
i18n={i18n}
/>
);
}
if (canProcessNow) {
return (
<Intl
id="Message--from-me-unsupported-message-ask-to-resend"
components={{ contact: contactName }}
i18n={i18n}
/>
);
}
return (
<Intl
id="Message--from-me-unsupported-message"
components={{ contact: contactName }}
i18n={i18n}
/>
);
}
export function UnsupportedMessage({
canProcessNow,
contact,
i18n,
}: Props): JSX.Element {
const { isMe } = contact;
const otherStringId = canProcessNow
? 'Message--unsupported-message-ask-to-resend'
: 'Message--unsupported-message';
const meStringId = canProcessNow
? 'Message--from-me-unsupported-message-ask-to-resend'
: 'Message--from-me-unsupported-message';
const stringId = isMe ? meStringId : otherStringId;
const icon = canProcessNow ? 'unsupported--can-process' : 'unsupported';
return (
<SystemMessage
icon={icon}
icon={canProcessNow ? 'unsupported--can-process' : 'unsupported'}
contents={
// eslint-disable-next-line local-rules/valid-i18n-keys
<Intl
id={stringId}
components={{
contact: (
<span
key="external-1"
className="module-unsupported-message__contact"
>
<ContactName
title={contact.title}
module="module-unsupported-message__contact"
/>
</span>
),
}}
<UnsupportedMessageContents
canProcessNow={canProcessNow}
contact={contact}
i18n={i18n}
/>
}

View File

@@ -25,45 +25,43 @@ type PropsHousekeeping = {
export type Props = PropsData & PropsHousekeeping;
export class VerificationNotification extends React.Component<Props> {
public getStringId(): string {
const { isLocal, type } = this.props;
public renderContents(): JSX.Element {
const { contact, isLocal, type, i18n } = this.props;
const name = (
<ContactName
key="external-1"
title={contact.title}
module="module-verification-notification__contact"
/>
);
switch (type) {
case 'markVerified':
return isLocal
? 'youMarkedAsVerified'
: 'youMarkedAsVerifiedOtherDevice';
return isLocal ? (
<Intl id="youMarkedAsVerified" components={{ name }} i18n={i18n} />
) : (
<Intl
id="youMarkedAsVerifiedOtherDevice"
components={{ name }}
i18n={i18n}
/>
);
case 'markNotVerified':
return isLocal
? 'youMarkedAsNotVerified'
: 'youMarkedAsNotVerifiedOtherDevice';
return isLocal ? (
<Intl id="youMarkedAsNotVerified" components={{ name }} i18n={i18n} />
) : (
<Intl
id="youMarkedAsNotVerifiedOtherDevice"
components={{ name }}
i18n={i18n}
/>
);
default:
throw missingCaseError(type);
}
}
public renderContents(): JSX.Element {
const { contact, i18n } = this.props;
const id = this.getStringId();
return (
// eslint-disable-next-line local-rules/valid-i18n-keys
<Intl
id={id}
components={{
name: (
<ContactName
key="external-1"
title={contact.title}
module="module-verification-notification__contact"
/>
),
}}
i18n={i18n}
/>
);
}
public override render(): JSX.Element {
const { type } = this.props;
const icon = type === 'markVerified' ? 'verified' : 'verified-not';

View File

@@ -1,8 +1,6 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable local-rules/valid-i18n-keys */
import React, {
useEffect,
useMemo,
@@ -212,7 +210,8 @@ export function ChooseGroupMembersModal({
if (virtualIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'contactsHeader',
// eslint-disable-next-line @typescript-eslint/no-shadow
getHeaderText: i18n => i18n('contactsHeader'),
};
}
@@ -250,7 +249,8 @@ export function ChooseGroupMembersModal({
if (virtualIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'findByPhoneNumberHeader',
// eslint-disable-next-line @typescript-eslint/no-shadow
getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
};
}
if (virtualIndex === 1) {
@@ -268,7 +268,8 @@ export function ChooseGroupMembersModal({
if (virtualIndex === 0) {
return {
type: RowType.Header,
i18nKey: 'findByUsernameHeader',
// eslint-disable-next-line @typescript-eslint/no-shadow
getHeaderText: i18n => i18n('findByUsernameHeader'),
};
}
if (virtualIndex === 1) {
@@ -307,16 +308,18 @@ export function ChooseGroupMembersModal({
let item;
switch (row?.type) {
case RowType.Header:
case RowType.Header: {
const headerText = row.getHeaderText(i18n);
item = (
<div
className="module-conversation-list__item--header"
aria-label={i18n(row.i18nKey)}
aria-label={headerText}
>
{i18n(row.i18nKey)}
{headerText}
</div>
);
break;
}
case RowType.ContactCheckbox:
item = (
<ContactCheckbox

View File

@@ -618,8 +618,7 @@ function ConversationDetailsCallButton({
onClick={onClick}
variant={ButtonVariant.Details}
>
{/* eslint-disable-next-line local-rules/valid-i18n-keys */}
{i18n(type)}
{type === 'audio' ? i18n('audio') : i18n('video')}
</Button>
);

View File

@@ -71,11 +71,25 @@ function MediaSection({
const first = section.mediaItems[0];
const { message } = first;
const date = moment(getMessageTimestamp(message));
const header =
section.type === 'yearMonth'
? date.format(MONTH_FORMAT)
: // eslint-disable-next-line local-rules/valid-i18n-keys
i18n(section.type);
function getHeader(): string {
switch (section.type) {
case 'yearMonth':
return date.format(MONTH_FORMAT);
case 'today':
return i18n('today');
case 'yesterday':
return i18n('yesterday');
case 'thisWeek':
return i18n('thisWeek');
case 'thisMonth':
return i18n('thisMonth');
default:
throw missingCaseError(section);
}
}
const header = getHeader();
return (
<AttachmentSection