diff --git a/.eslint/rules/valid-i18n-keys.js b/.eslint/rules/valid-i18n-keys.js
index 5d6f30659..7aa4ceebd 100644
--- a/.eslint/rules/valid-i18n-keys.js
+++ b/.eslint/rules/valid-i18n-keys.js
@@ -14,9 +14,11 @@ const messagesCacheKey = hashSum.digest('hex');
function isI18nCall(node) {
return (
- node.type === 'CallExpression' &&
- node.callee.type === 'Identifier' &&
- node.callee.name === 'i18n'
+ (node.type === 'CallExpression' &&
+ node.callee.type === 'Identifier' &&
+ node.callee.name === 'i18n') ||
+ (node.callee.type === 'MemberExpression' &&
+ node.callee.property.name === 'i18n')
);
}
@@ -36,20 +38,7 @@ function valueToMessageKey(node) {
if (isStringLiteral(node)) {
return node.value;
}
-
- if (node.type !== 'TemplateLiteral') {
- return null;
- }
-
- if (node.quasis.length === 1) {
- return node.quasis[0].value.cooked;
- }
-
- const parts = node.quasis.map(element => {
- return element.value.cooked;
- });
-
- return new RegExp(`^${parts.join('(.*)')}$`);
+ return null;
}
function getI18nCallMessageKey(node) {
@@ -80,24 +69,11 @@ function getIntlElementMessageKey(node) {
let value = idAttribute.value;
- if (value.type === 'JSXExpressionContainer') {
- value = value.expression;
- }
-
return valueToMessageKey(value);
}
function isValidMessageKey(key) {
- if (typeof key === 'string') {
- if (Object.hasOwn(messages, key)) {
- return true;
- }
- } else if (key instanceof RegExp) {
- if (messageKeys.some(k => key.test(k))) {
- return true;
- }
- }
- return false;
+ return Object.hasOwn(messages, key);
}
module.exports = {
diff --git a/.eslint/rules/valid-i18n-keys.test.js b/.eslint/rules/valid-i18n-keys.test.js
index 7b957d216..b59757782 100644
--- a/.eslint/rules/valid-i18n-keys.test.js
+++ b/.eslint/rules/valid-i18n-keys.test.js
@@ -27,27 +27,68 @@ ruleTester.run('valid-i18n-keys', rule, {
options: [{ messagesCacheKey }],
},
{
- code: 'i18n(`AddCaptionModal__${title}`)',
+ code: `window.i18n("AddCaptionModal__title")`,
options: [{ messagesCacheKey }],
},
{
code: `let jsx = `,
options: [{ messagesCacheKey }],
},
+ ],
+ invalid: [
+ {
+ code: 'i18n(`AddCaptionModal__${title}`)',
+ options: [{ messagesCacheKey }],
+ errors: [
+ {
+ message: "i18n()'s first argument should always be a literal string",
+ type: 'CallExpression',
+ },
+ ],
+ },
+ {
+ code: 'window.i18n(`AddCaptionModal__${title}`)',
+ options: [{ messagesCacheKey }],
+ errors: [
+ {
+ message: "i18n()'s first argument should always be a literal string",
+ type: 'CallExpression',
+ },
+ ],
+ },
{
code: `let jsx = `,
options: [{ messagesCacheKey }],
+ errors: [
+ {
+ message:
+ " must always be provided an 'id' attribute with a literal string",
+ type: 'JSXOpeningElement',
+ },
+ ],
},
{
code: 'let jsx = ',
options: [{ messagesCacheKey }],
+ errors: [
+ {
+ message:
+ " must always be provided an 'id' attribute with a literal string",
+ type: 'JSXOpeningElement',
+ },
+ ],
},
{
code: 'let jsx = ',
options: [{ messagesCacheKey }],
+ errors: [
+ {
+ message:
+ " must always be provided an 'id' attribute with a literal string",
+ type: 'JSXOpeningElement',
+ },
+ ],
},
- ],
- invalid: [
{
code: `i18n("THIS_KEY_SHOULD_NEVER_EXIST")`,
options: [{ messagesCacheKey }],
diff --git a/ts/components/ConversationList.stories.tsx b/ts/components/ConversationList.stories.tsx
index ec2412c6a..86ef40991 100644
--- a/ts/components/ConversationList.stories.tsx
+++ b/ts/components/ConversationList.stories.tsx
@@ -604,19 +604,23 @@ export function Headers(): JSX.Element {
rows={[
{
type: RowType.Header,
- i18nKey: 'conversationsHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('conversationsHeader'),
},
{
type: RowType.Header,
- i18nKey: 'messagesHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('messagesHeader'),
},
{
type: RowType.Header,
- i18nKey: 'findByUsernameHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('findByUsernameHeader'),
},
{
type: RowType.Header,
- i18nKey: 'findByPhoneNumberHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
},
]}
/>
@@ -629,7 +633,8 @@ export function FindByPhoneNumber(): JSX.Element {
rows={[
{
type: RowType.Header,
- i18nKey: 'findByPhoneNumberHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
},
{
type: RowType.StartNewConversation,
@@ -673,7 +678,8 @@ export function FindByUsername(): JSX.Element {
rows={[
{
type: RowType.Header,
- i18nKey: 'findByUsernameHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('findByUsernameHeader'),
},
{
type: RowType.UsernameSearchResult,
@@ -745,7 +751,8 @@ export function KitchenSink(): JSX.Element {
},
{
type: RowType.Header,
- i18nKey: 'contactsHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('contactsHeader'),
},
{
type: RowType.Contact,
@@ -753,7 +760,8 @@ export function KitchenSink(): JSX.Element {
},
{
type: RowType.Header,
- i18nKey: 'messagesHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('messagesHeader'),
},
{
type: RowType.Conversation,
@@ -765,7 +773,8 @@ export function KitchenSink(): JSX.Element {
},
{
type: RowType.Header,
- i18nKey: 'findByUsernameHeader',
+ // eslint-disable-next-line @typescript-eslint/no-shadow
+ getHeaderText: i18n => i18n('findByUsernameHeader'),
},
{
type: RowType.UsernameSearchResult,
diff --git a/ts/components/ConversationList.tsx b/ts/components/ConversationList.tsx
index b94b264fb..8d61c1394 100644
--- a/ts/components/ConversationList.tsx
+++ b/ts/components/ConversationList.tsx
@@ -103,9 +103,17 @@ type MessageRowType = {
type HeaderRowType = {
type: RowType.Header;
- i18nKey: string;
+ getHeaderText: (i18n: LocalizerType) => string;
};
+// Exported for tests across multiple files
+export function _testHeaderText(row: Row | void): string | null {
+ if (row?.type === RowType.Header) {
+ return row.getHeaderText(((key: string) => key) as LocalizerType);
+ }
+ return null;
+}
+
type SearchResultsLoadingFakeHeaderType = {
type: RowType.SearchResultsLoadingFakeHeader;
};
@@ -375,18 +383,18 @@ export function ConversationList({
/>
);
break;
- case RowType.Header:
+ case RowType.Header: {
+ const headerText = row.getHeaderText(i18n);
result = (
- {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
- {i18n(row.i18nKey)}
+ {headerText}
);
break;
+ }
case RowType.MessageSearchResult:
result = <>{renderMessageSearchResult?.(row.messageId)}>;
break;
diff --git a/ts/components/GroupV1MigrationDialog.tsx b/ts/components/GroupV1MigrationDialog.tsx
index 8f6d77506..9d223d604 100644
--- a/ts/components/GroupV1MigrationDialog.tsx
+++ b/ts/components/GroupV1MigrationDialog.tsx
@@ -7,6 +7,7 @@ import type { ConversationType } from '../state/ducks/conversations';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import { GroupDialog } from './GroupDialog';
import { sortByTitle } from '../util/sortByTitle';
+import { missingCaseError } from '../util';
export type DataPropsType = {
conversationId: string;
@@ -70,8 +71,6 @@ export const GroupV1MigrationDialog: React.FunctionComponent =
const keepHistory = hasMigrated
? i18n('GroupV1--Migration--info--keep-history')
: i18n('GroupV1--Migration--migrate--keep-history');
- const migrationKey = hasMigrated ? 'after' : 'before';
- const droppedMembersKey = `GroupV1--Migration--info--removed--${migrationKey}`;
let primaryButtonText: string;
let onClickPrimaryButton: () => void;
@@ -116,14 +115,16 @@ export const GroupV1MigrationDialog: React.FunctionComponent =
getPreferredBadge,
i18n,
members: invitedMembers,
- prefix: 'GroupV1--Migration--info--invited',
+ hasMigrated,
+ kind: 'invited',
theme,
})}
{renderMembers({
getPreferredBadge,
i18n,
members: droppedMembers,
- prefix: droppedMembersKey,
+ hasMigrated,
+ kind: 'dropped',
theme,
})}
>
@@ -136,26 +137,49 @@ function renderMembers({
getPreferredBadge,
i18n,
members,
- prefix,
+ hasMigrated,
+ kind,
theme,
}: Readonly<{
getPreferredBadge: PreferredBadgeSelectorType;
i18n: LocalizerType;
members: Array;
- prefix: string;
+ hasMigrated: boolean;
+ kind: 'invited' | 'dropped';
theme: ThemeType;
}>): React.ReactNode {
if (!members.length) {
return null;
}
- const postfix = members.length === 1 ? '--one' : '--many';
- const key = `${prefix}${postfix}`;
+ let text: string;
+ switch (kind) {
+ case 'invited':
+ text =
+ members.length === 1
+ ? i18n('GroupV1--Migration--info--invited--one')
+ : i18n('GroupV1--Migration--info--invited--many');
+ break;
+ case 'dropped':
+ if (hasMigrated) {
+ text =
+ members.length === 1
+ ? i18n('GroupV1--Migration--info--removed--before--one')
+ : i18n('GroupV1--Migration--info--removed--before--many');
+ } else {
+ text =
+ members.length === 1
+ ? i18n('GroupV1--Migration--info--removed--after--one')
+ : i18n('GroupV1--Migration--info--removed--after--many');
+ }
+ break;
+ default:
+ throw missingCaseError(kind);
+ }
return (
<>
- {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
- {i18n(key)}
+ {text}
{
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
public override render() {
- const { components, id, i18n, renderText = defaultRenderText } = this.props;
+ const {
+ components,
+ id,
+ // Indirection for linter/migration tooling
+ i18n: localizer,
+ renderText = defaultRenderText,
+ } = this.props;
if (!id) {
log.error('Error: Intl id prop not provided');
return null;
}
- if (!i18n.isLegacyFormat(id)) {
+ if (!localizer.isLegacyFormat(id)) {
strictAssert(
!Array.isArray(components),
`components cannot be an array for ICU message ${id}`
);
- const intl = i18n.getIntl();
+ const intl = localizer.getIntl();
return intl.formatMessage({ id }, components);
}
- // eslint-disable-next-line local-rules/valid-i18n-keys
- const text = i18n(id);
+ const text = localizer(id);
const results: Array<
string | JSX.Element | Array | null
> = [];
diff --git a/ts/components/SendStoryModal.tsx b/ts/components/SendStoryModal.tsx
index 5288973ff..08bcd1bbe 100644
--- a/ts/components/SendStoryModal.tsx
+++ b/ts/components/SendStoryModal.tsx
@@ -339,7 +339,7 @@ export function SendStoryModal({
{
diff --git a/ts/components/StoriesSettingsModal.tsx b/ts/components/StoriesSettingsModal.tsx
index 2e98fd3e5..3dbe56b6c 100644
--- a/ts/components/StoriesSettingsModal.tsx
+++ b/ts/components/StoriesSettingsModal.tsx
@@ -627,7 +627,7 @@ export function DistributionListSettingsModal({
{isMyStory && (
{
setPage(Page.HideStoryFrom);
@@ -791,7 +791,7 @@ function CheckboxRender({
type EditMyStoryPrivacyPropsType = {
hasDisclaimerAbove?: boolean;
i18n: LocalizerType;
- learnMore: string;
+ kind: 'privacy' | 'mine';
myStories: StoryDistributionListWithMembersDataType;
onClickExclude: () => unknown;
onClickOnlyShareWith: () => unknown;
@@ -805,7 +805,7 @@ type EditMyStoryPrivacyPropsType = {
export function EditMyStoryPrivacy({
hasDisclaimerAbove,
i18n,
- learnMore,
+ kind,
myStories,
onClickExclude,
onClickOnlyShareWith,
@@ -814,24 +814,30 @@ export function EditMyStoryPrivacy({
toggleSignalConnectionsModal,
signalConnectionsCount,
}: EditMyStoryPrivacyPropsType): JSX.Element {
+ const learnMore = (
+
+ );
const disclaimerElement = (
- {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
-
- {i18n('StoriesSettings__mine__disclaimer--learn-more')}
-
- ),
- }}
- i18n={i18n}
- id={learnMore}
- />
+ {kind === 'mine' ? (
+
+ ) : (
+
+ )}
);
diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx
index 73c2e72cc..e61c198cf 100644
--- a/ts/components/ToastManager.tsx
+++ b/ts/components/ToastManager.tsx
@@ -40,10 +40,9 @@ export function ToastManager({
if (toastType === ToastType.AddingUserToGroup) {
return (
- {i18n(
- 'AddUserToAnotherGroupModal__toast--adding-user-to-group',
- toast.parameters
- )}
+ {i18n('AddUserToAnotherGroupModal__toast--adding-user-to-group', {
+ ...toast.parameters,
+ })}
);
}
@@ -107,7 +106,9 @@ export function ToastManager({
if (toastType === ToastType.CannotStartGroupCall) {
return (
- {i18n('GroupV2--cannot-start-group-call', toast.parameters)}
+ {i18n('GroupV2--cannot-start-group-call', {
+ ...toast.parameters,
+ })}
);
}
@@ -344,10 +345,9 @@ export function ToastManager({
if (toastType === ToastType.UserAddedToGroup) {
return (
- {i18n(
- 'AddUserToAnotherGroupModal__toast--user-added-to-group',
- toast.parameters
- )}
+ {i18n('AddUserToAnotherGroupModal__toast--user-added-to-group', {
+ ...toast.parameters,
+ })}
);
}
diff --git a/ts/components/WhatsNewModal.tsx b/ts/components/WhatsNewModal.tsx
index 501796552..3b3c799a6 100644
--- a/ts/components/WhatsNewModal.tsx
+++ b/ts/components/WhatsNewModal.tsx
@@ -6,7 +6,6 @@ import React from 'react';
import moment from 'moment';
import { Modal } from './Modal';
-import type { IntlComponentsType } from './Intl';
import { Intl } from './Intl';
import { Emojify } from './conversation/Emojify';
import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
@@ -19,61 +18,46 @@ export type PropsType = {
type ReleaseNotesType = {
date: Date;
version: string;
- features: Array<{ key: string; components: IntlComponentsType }>;
+ features: Array;
};
const renderText: RenderTextCallbackType = ({ key, text }) => (
);
-const releaseNotes: ReleaseNotesType = {
- date: new Date(window.getBuildCreation?.() || Date.now()),
- version: window.getVersion?.(),
- features: [
- {
- key: 'icu:WhatsNew__v6.12--0',
- components: {},
- },
- {
- key: 'icu:WhatsNew__v6.12--1',
- components: {},
- },
- ],
-};
-
export function WhatsNewModal({
i18n,
hideWhatsNewModal,
}: PropsType): JSX.Element {
let contentNode: ReactChild;
+ const releaseNotes: ReleaseNotesType = {
+ date: new Date(window.getBuildCreation?.() || Date.now()),
+ version: window.getVersion?.(),
+ features: [
+ ,
+ ,
+ ],
+ };
+
if (releaseNotes.features.length === 1) {
- const { key, components } = releaseNotes.features[0];
- contentNode = (
-
- {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
-
-
- );
+ contentNode = {releaseNotes.features[0]}
;
} else {
contentNode = (
- {releaseNotes.features.map(({ key, components }) => (
- -
- {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
-
-
- ))}
+ {releaseNotes.features.map(element => {
+ return - {element}
;
+ })}
);
}
diff --git a/ts/components/conversation/GroupV1Migration.tsx b/ts/components/conversation/GroupV1Migration.tsx
index 0e4478201..6c7bbbdac 100644
--- a/ts/components/conversation/GroupV1Migration.tsx
+++ b/ts/components/conversation/GroupV1Migration.tsx
@@ -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')}
>
)}
@@ -106,31 +98,52 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
function renderUsers(
members: Array,
i18n: LocalizerType,
- keyPrefix: string
+ kind: 'invited' | 'removed'
): React.ReactElement | null {
if (!members || members.length === 0) {
return null;
}
if (members.length === 1) {
+ const contact = ;
return (
- ,
- }}
- />
+ {kind === 'invited' && (
+
+ )}
+ {kind === 'removed' && (
+
+ )}
);
}
+ const count = members.length.toString();
+
return (
- {i18n(`${keyPrefix}--many`, {
- count: members.length.toString(),
- })}
+ {kind === 'invited' && members.length > 1 && (
+
+ )}
+ {kind === 'removed' && members.length > 1 && (
+
+ )}
);
}
diff --git a/ts/components/conversation/MandatoryProfileSharingActions.tsx b/ts/components/conversation/MandatoryProfileSharingActions.tsx
index f03aa7651..e6c3eec9f 100644
--- a/ts/components/conversation/MandatoryProfileSharingActions.tsx
+++ b/ts/components/conversation/MandatoryProfileSharingActions.tsx
@@ -40,6 +40,26 @@ export function MandatoryProfileSharingActions({
}: Props): JSX.Element {
const [mrState, setMrState] = React.useState(MessageRequestState.default);
+ const firstNameContact = (
+
+
+
+ );
+
+ const learnMore = (
+
+ {i18n('MessageRequests--learn-more')}
+
+ );
+
return (
<>
{mrState !== MessageRequestState.default ? (
@@ -62,34 +82,19 @@ export function MandatoryProfileSharingActions({
) : null}
-
-
-
- ),
- learnMore: (
-
- {i18n('MessageRequests--learn-more')}
-
- ),
- }}
- />
+ {conversationType === 'direct' ? (
+
+ ) : (
+
+ )}