From 8ca192a48d94b8a419a82448cf94ad4e7b213f18 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:41:14 -0700 Subject: [PATCH] Update i18n eslint rule to validate missing/extra icu params --- .eslint/rules/valid-i18n-keys.js | 232 +++++++++++++++++- .eslint/rules/valid-i18n-keys.test.js | 185 +++++++++++++- _locales/en/messages.json | 4 +- ts/components/StoriesAddStoryButton.tsx | 5 +- ts/components/StoryViewer.tsx | 5 +- ts/components/ToastManager.tsx | 19 +- ts/components/WhatsNewModal.tsx | 7 +- .../conversation/DeliveryIssueDialog.tsx | 8 +- .../MandatoryProfileSharingActions.tsx | 2 +- ts/components/conversation/Message.tsx | 2 +- .../conversation/MessageRequestActions.tsx | 12 +- .../MessageRequestActionsConfirmation.tsx | 3 - .../RemoveGroupMemberConfirmationDialog.tsx | 8 +- .../conversation/SafetyNumberNotification.tsx | 6 +- .../conversation/UnsupportedMessage.tsx | 15 +- ts/services/LinkPreview.ts | 2 +- 16 files changed, 449 insertions(+), 66 deletions(-) diff --git a/.eslint/rules/valid-i18n-keys.js b/.eslint/rules/valid-i18n-keys.js index 73007a331b3..6013d0650c0 100644 --- a/.eslint/rules/valid-i18n-keys.js +++ b/.eslint/rules/valid-i18n-keys.js @@ -2,14 +2,25 @@ // SPDX-License-Identifier: AGPL-3.0-only const crypto = require('crypto'); +const icuParser = require('@formatjs/icu-messageformat-parser'); const globalMessages = require('../../_locales/en/messages.json'); const messageKeys = Object.keys(globalMessages).sort((a, b) => { return a.localeCompare(b); }); +const allIcuParams = messageKeys + .filter(key => { + return isIcuMessageKey(globalMessages, key); + }) + .map(key => { + return Array.from( + getIcuMessageParams(globalMessages[key].messageformat) + ).join('\n'); + }); const hashSum = crypto.createHash('sha256'); hashSum.update(messageKeys.join('\n')); +hashSum.update(allIcuParams.join('\n')); const messagesCacheKey = hashSum.digest('hex'); function isI18nCall(node) { @@ -54,6 +65,14 @@ function getI18nCallMessageKey(node) { return valueToMessageKey(arg1); } +function getI18nCallValues(node) { + // babel-eslint messes with elements arrays in some cases because of TS + if (node.arguments.length < 2) { + return null; + } + return node.arguments[1]; +} + function getIntlElementMessageKey(node) { let idAttribute = node.attributes.find(attribute => { return ( @@ -72,6 +91,27 @@ function getIntlElementMessageKey(node) { return valueToMessageKey(value); } +function getIntlElementComponents(node) { + let componentsAttribute = node.attributes.find(attribute => { + return ( + attribute.type === 'JSXAttribute' && + attribute.name.type === 'JSXIdentifier' && + attribute.name.name === 'components' + ); + }); + + if (componentsAttribute == null) { + return null; + } + + let value = componentsAttribute.value; + if (value?.type !== 'JSXExpressionContainer') { + return null; + } + + return value.expression; +} + function isValidMessageKey(messages, key) { return Object.hasOwn(messages, key); } @@ -89,6 +129,67 @@ function isDeletedMessageKey(messages, key) { return description?.toLowerCase().startsWith('(deleted '); } +function getIcuMessageParams(message) { + const params = new Set(); + + function visitOptions(options) { + for (const option of Object.values(options)) { + visit(option.value); + } + } + + function visit(elements) { + for (const element of elements) { + switch (element.type) { + case icuParser.TYPE.argument: + params.add(element.value); + break; + case icuParser.TYPE.date: + params.add(element.value); + break; + case icuParser.TYPE.literal: + break; + case icuParser.TYPE.number: + params.add(element.value); + break; + case icuParser.TYPE.plural: + params.add(element.value); + visitOptions(element.options); + break; + case icuParser.TYPE.pound: + break; + case icuParser.TYPE.select: + params.add(element.value); + visitOptions(element.options); + break; + case icuParser.TYPE.tag: + params.add(element.value); + visit(element.children); + break; + case icuParser.TYPE.time: + params.add(element.value); + break; + default: + throw new Error(`Unknown element type: ${element.type}`); + } + } + } + + visit(icuParser.parse(message)); + + return params; +} + +function getMissingFromSet(expected, actual) { + const result = new Set(); + for (const item of expected) { + if (!actual.has(item)) { + result.add(item); + } + } + return result; +} + module.exports = { messagesCacheKey, meta: { @@ -150,7 +251,7 @@ module.exports = { return; } - let key = getIntlElementMessageKey(node); + const key = getIntlElementMessageKey(node); if (key == null) { context.report({ @@ -184,13 +285,76 @@ module.exports = { }); return; } + + const params = getIcuMessageParams(messages[key].messageformat); + const components = getIntlElementComponents(node); + + if (params.size === 0) { + if (components != null) { + context.report({ + node, + message: `<Intl> message "${key}" does not have any params, but has a "components" attribute`, + }); + } + return; + } + + if (components == null) { + context.report({ + node, + message: `<Intl> message "${key}" has params, but is missing a "components" attribute`, + }); + return; + } + + if (components.type !== 'ObjectExpression') { + context.report({ + node: components, + message: `<Intl> "components" attribute must be an object literal`, + }); + return; + } + + const props = new Set(); + for (const property of components.properties) { + if (property.type !== 'Property' || property.computed) { + context.report({ + node: property, + message: `<Intl> "components" attribute must only contain literal keys`, + }); + return; + } + props.add(property.key.name); + } + + const missingParams = getMissingFromSet(params, props); + if (missingParams.size > 0) { + for (const param of missingParams) { + context.report({ + node: components, + message: `<Intl> message "${key}" has a param "${param}", but no corresponding component`, + }); + } + return; + } + + const extraComponents = getMissingFromSet(props, params); + if (extraComponents.size > 0) { + for (const prop of extraComponents) { + context.report({ + node: components, + message: `<Intl> message "${key}" has a component "${prop}", but no corresponding param`, + }); + } + return; + } }, CallExpression(node) { if (!isI18nCall(node)) { return; } - let key = getI18nCallMessageKey(node); + const key = getI18nCallMessageKey(node); if (key == null) { context.report({ @@ -222,6 +386,70 @@ module.exports = { node, message: `i18n() key "${key}" is marked as deleted in _locales/en/messages.json`, }); + return; + } + + const params = getIcuMessageParams(messages[key].messageformat); + const values = getI18nCallValues(node); + + if (params.size === 0) { + if (values != null) { + context.report({ + node, + message: `i18n() message "${key}" does not have any params, but has a "values" argument`, + }); + } + return; + } + + if (values == null) { + context.report({ + node, + message: `i18n() message "${key}" has params, but is missing a "values" argument`, + }); + return; + } + + if (values.type !== 'ObjectExpression') { + context.report({ + node: values, + message: `i18n() "values" argument must be an object literal`, + }); + return; + } + + const props = new Set(); + for (const property of values.properties) { + if (property.type !== 'Property' || property.computed) { + context.report({ + node: property, + message: `i18n() "values" argument must only contain literal keys`, + }); + return; + } + props.add(property.key.name); + } + + const missingParams = getMissingFromSet(params, props); + if (missingParams.size > 0) { + for (const param of missingParams) { + context.report({ + node: values, + message: `i18n() message "${key}" has a param "${param}", but no corresponding value`, + }); + } + return; + } + + const extraProps = getMissingFromSet(props, params); + if (extraProps.size > 0) { + for (const prop of extraProps) { + context.report({ + node: values, + message: `i18n() message "${key}" has a value "${prop}", but no corresponding param`, + }); + } + return; } }, }; diff --git a/.eslint/rules/valid-i18n-keys.test.js b/.eslint/rules/valid-i18n-keys.test.js index 0661a8d9f0d..ae60794705e 100644 --- a/.eslint/rules/valid-i18n-keys.test.js +++ b/.eslint/rules/valid-i18n-keys.test.js @@ -17,6 +17,12 @@ const __mockMessages__ = { messageformat: 'shouldnt use me anymore', description: '(deleted 01/01/1970)', }, + 'icu:no_params': { + messageformat: 'ICU message', + }, + 'icu:nested': { + messageformat: '{one, select, other {{two, plural, other {{three}}}}}}', + }, }; // Need to load so mocha doesn't complain about polluting the global namespace @@ -36,15 +42,27 @@ const ruleTester = new RuleTester({ ruleTester.run('valid-i18n-keys', rule, { valid: [ { - code: `i18n("icu:real_message")`, + code: `i18n("icu:real_message", { message: "foo" })`, options: [{ messagesCacheKey, __mockMessages__ }], }, { - code: `window.i18n("icu:real_message")`, + code: `window.i18n("icu:real_message", { message: "foo" })`, options: [{ messagesCacheKey, __mockMessages__ }], }, { - code: `let jsx = <Intl id="icu:real_message"/>`, + code: `let jsx = <Intl id="icu:real_message" components={{ message: "foo" }}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + }, + { + code: `i18n("icu:no_params")`, + options: [{ messagesCacheKey, __mockMessages__ }], + }, + { + code: `let jsx = <Intl id="icu:no_params"/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + }, + { + code: `i18n("icu:nested", { one: "1", two: "2", three: "3" })`, options: [{ messagesCacheKey, __mockMessages__ }], }, ], @@ -221,5 +239,166 @@ ruleTester.run('valid-i18n-keys', rule, { }, ], }, + { + code: `i18n("icu:no_params", { message: "foo" })`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + 'i18n() message "icu:no_params" does not have any params, but has a "values" argument', + type: 'CallExpression', + }, + ], + }, + { + code: `i18n("icu:real_message")`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + 'i18n() message "icu:real_message" has params, but is missing a "values" argument', + type: 'CallExpression', + }, + ], + }, + { + code: `i18n("icu:real_message", null)`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: 'i18n() "values" argument must be an object literal', + type: 'Literal', + }, + ], + }, + { + code: `i18n("icu:real_message", { [foo]: "foo" })`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: 'i18n() "values" argument must only contain literal keys', + type: 'Property', + }, + ], + }, + { + code: `i18n("icu:real_message", { ...props })`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: 'i18n() "values" argument must only contain literal keys', + type: 'SpreadElement', + }, + ], + }, + { + code: `i18n("icu:real_message", {})`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + 'i18n() message "icu:real_message" has a param "message", but no corresponding value', + type: 'ObjectExpression', + }, + ], + }, + { + code: `i18n("icu:real_message", { message: "foo", foo: "bar" })`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + 'i18n() message "icu:real_message" has a value "foo", but no corresponding param', + type: 'ObjectExpression', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:no_params" components={{ message: "foo" }}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> message "icu:no_params" does not have any params, but has a "components" attribute', + type: 'JSXOpeningElement', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message"/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> message "icu:real_message" has params, but is missing a "components" attribute', + type: 'JSXOpeningElement', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message" components={null}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: '<Intl> "components" attribute must be an object literal', + type: 'Literal', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message" components={{ [foo]: "foo" }}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> "components" attribute must only contain literal keys', + type: 'Property', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message" components={{ ...props }}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> "components" attribute must only contain literal keys', + type: 'SpreadElement', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message" components={{}}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> message "icu:real_message" has a param "message", but no corresponding component', + type: 'ObjectExpression', + }, + ], + }, + { + code: `let jsx = <Intl id="icu:real_message" components={{ message: "foo", foo: "bar" }}/>`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + '<Intl> message "icu:real_message" has a component "foo", but no corresponding param', + type: 'ObjectExpression', + }, + ], + }, + { + code: `i18n("icu:nested", { one: "1", two: "2" })`, + options: [{ messagesCacheKey, __mockMessages__ }], + errors: [ + { + message: + 'i18n() message "icu:nested" has a param "three", but no corresponding value', + type: 'ObjectExpression', + }, + ], + }, ], }); diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f0b5fb0f678..34a451862e9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -10084,7 +10084,7 @@ "description": "(deleted 03/29/2023) Information shown at the bottom of the profile editor section" }, "icu:ProfileEditor--info--link": { - "messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>{learnMore}</learnMoreLink>", + "messageformat": "Your profile is encrypted. Your profile and changes to it will be visible to your contacts and when you start or accept new chats. <learnMoreLink>Learn More</learnMoreLink>", "description": "Information shown at the bottom of the profile editor section" }, "ProfileEditor--learnMore": { @@ -10093,7 +10093,7 @@ }, "icu:ProfileEditor--learnMore": { "messageformat": "Learn More", - "description": "Text that links to a support article" + "description": "(deleted 04/03/2023) Text that links to a support article" }, "Bio--speak-freely": { "message": "Speak Freely", diff --git a/ts/components/StoriesAddStoryButton.tsx b/ts/components/StoriesAddStoryButton.tsx index 12a8cce952a..20daac2cdda 100644 --- a/ts/components/StoriesAddStoryButton.tsx +++ b/ts/components/StoriesAddStoryButton.tsx @@ -70,7 +70,10 @@ export function StoriesAddStoryButton({ if (result.reason === ReasonVideoNotGood.TooBig) { setError( - i18n('icu:StoryCreator__error--video-too-big', result.renderDetails) + i18n('icu:StoryCreator__error--video-too-big', { + limit: result.renderDetails.limit, + units: result.renderDetails.units, + }) ); return; } diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 7f909b919d3..002d4928958 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -861,7 +861,10 @@ export function StoryViewer({ <Intl i18n={i18n} id="icu:MyStories__views--strong" - components={{ viewCount, strong: renderStrong }} + components={{ + views: viewCount, + strong: renderStrong, + }} /> )} {(isSent || viewCount > 0) && replyCount > 0 && ' '} diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index e158c9230a1..3aea4c4ab63 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import { get } from 'lodash'; import type { LocalizerType, ReplacementValuesType } from '../types/Util'; import { SECOND } from '../util/durations'; import { Toast } from './Toast'; @@ -41,7 +40,7 @@ export function ToastManager({ return ( <Toast onClose={hideToast} timeout={SHORT_TIMEOUT}> {i18n('icu:AddUserToAnotherGroupModal__toast--adding-user-to-group', { - ...toast.parameters, + contact: toast.parameters?.contact, })} </Toast> ); @@ -106,9 +105,7 @@ export function ToastManager({ if (toastType === ToastType.CannotStartGroupCall) { return ( <Toast onClose={hideToast}> - {i18n('icu:GroupV2--cannot-start-group-call', { - ...toast.parameters, - })} + {i18n('icu:GroupV2--cannot-start-group-call')} </Toast> ); } @@ -219,7 +216,10 @@ export function ToastManager({ if (toastType === ToastType.FileSize) { return ( <Toast onClose={hideToast}> - {i18n('icu:fileSizeWarning', toast?.parameters)} + {i18n('icu:fileSizeWarning', { + limit: toast.parameters?.limit, + units: toast.parameters?.units, + })} </Toast> ); } @@ -323,9 +323,7 @@ export function ToastManager({ if (toastType === ToastType.TooManyMessagesToForward) { return ( <Toast onClose={hideToast}> - {i18n('icu:SelectModeActions__toast--TooManyMessagesToForward', { - count: get(toast.parameters, 'count'), - })} + {i18n('icu:SelectModeActions__toast--TooManyMessagesToForward')} </Toast> ); } @@ -356,7 +354,8 @@ export function ToastManager({ return ( <Toast onClose={hideToast}> {i18n('icu:AddUserToAnotherGroupModal__toast--user-added-to-group', { - ...toast.parameters, + contact: toast.parameters?.contact, + group: toast.parameters?.group, })} </Toast> ); diff --git a/ts/components/WhatsNewModal.tsx b/ts/components/WhatsNewModal.tsx index d21c3ef6280..d92cbe4a1f7 100644 --- a/ts/components/WhatsNewModal.tsx +++ b/ts/components/WhatsNewModal.tsx @@ -35,12 +35,7 @@ export function WhatsNewModal({ date: new Date(window.getBuildCreation?.() || Date.now()), version: window.getVersion?.(), features: [ - <Intl - i18n={i18n} - id="icu:WhatsNew__v6.13--0" - renderText={renderText} - components={{}} - />, + <Intl i18n={i18n} id="icu:WhatsNew__v6.13--0" renderText={renderText} />, <Intl i18n={i18n} id="icu:WhatsNew__v6.13--1" diff --git a/ts/components/conversation/DeliveryIssueDialog.tsx b/ts/components/conversation/DeliveryIssueDialog.tsx index 111db674b38..445b12a50f9 100644 --- a/ts/components/conversation/DeliveryIssueDialog.tsx +++ b/ts/components/conversation/DeliveryIssueDialog.tsx @@ -52,9 +52,7 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement { </> ); - const intlComponents = { - sender: <Emojify text={sender.title} />, - }; + const senderTitle = <Emojify text={sender.title} />; return ( <Modal @@ -80,13 +78,13 @@ export function DeliveryIssueDialog(props: PropsType): React.ReactElement { {inGroup ? ( <Intl id="icu:DeliveryIssue--summary--group" - components={intlComponents} + components={{ sender: senderTitle }} i18n={i18n} /> ) : ( <Intl id="icu:DeliveryIssue--summary" - components={intlComponents} + components={{ sender: senderTitle }} i18n={i18n} /> )} diff --git a/ts/components/conversation/MandatoryProfileSharingActions.tsx b/ts/components/conversation/MandatoryProfileSharingActions.tsx index cf92ba6fd1f..ddfef7df11b 100644 --- a/ts/components/conversation/MandatoryProfileSharingActions.tsx +++ b/ts/components/conversation/MandatoryProfileSharingActions.tsx @@ -92,7 +92,7 @@ export function MandatoryProfileSharingActions({ <Intl i18n={i18n} id="icu:MessageRequests--profile-sharing--group--link" - components={{ firstName: firstNameContact, learnMoreLink }} + components={{ learnMoreLink }} /> )} </p> diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index c508c9b679f..d912607871b 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -2555,7 +2555,7 @@ export class Message extends React.PureComponent<Props, State> { <span className="module-message__alt-accessibility-tree"> <span id={`message-accessibility-label:${id}`}> {author.isMe - ? i18n('icu:messageAccessibilityLabel--outgoing', {}) + ? i18n('icu:messageAccessibilityLabel--outgoing') : i18n('icu:messageAccessibilityLabel--incoming', { author: author.title, })} diff --git a/ts/components/conversation/MessageRequestActions.tsx b/ts/components/conversation/MessageRequestActions.tsx index 5c8e969f603..a7dd60c8877 100644 --- a/ts/components/conversation/MessageRequestActions.tsx +++ b/ts/components/conversation/MessageRequestActions.tsx @@ -77,18 +77,10 @@ export function MessageRequestActions({ /> )} {conversationType === 'group' && isBlocked && ( - <Intl - i18n={i18n} - id="icu:MessageRequests--message-group-blocked" - components={{ name }} - /> + <Intl i18n={i18n} id="icu:MessageRequests--message-group-blocked" /> )} {conversationType === 'group' && !isBlocked && ( - <Intl - i18n={i18n} - id="icu:MessageRequests--message-group" - components={{ name }} - /> + <Intl i18n={i18n} id="icu:MessageRequests--message-group" /> )} </p> <div className="module-message-request-actions__buttons"> diff --git a/ts/components/conversation/MessageRequestActionsConfirmation.tsx b/ts/components/conversation/MessageRequestActionsConfirmation.tsx index c4a992f8c28..1d5192f6268 100644 --- a/ts/components/conversation/MessageRequestActionsConfirmation.tsx +++ b/ts/components/conversation/MessageRequestActionsConfirmation.tsx @@ -136,9 +136,6 @@ export function MessageRequestActionsConfirmation({ <Intl i18n={i18n} id="icu:MessageRequests--delete-direct-confirm-title" - components={{ - title: <ContactName key="name" title={title} />, - }} /> ) : ( <Intl diff --git a/ts/components/conversation/RemoveGroupMemberConfirmationDialog.tsx b/ts/components/conversation/RemoveGroupMemberConfirmationDialog.tsx index 0ba29518da5..5c0e67c0705 100644 --- a/ts/components/conversation/RemoveGroupMemberConfirmationDialog.tsx +++ b/ts/components/conversation/RemoveGroupMemberConfirmationDialog.tsx @@ -30,9 +30,7 @@ export function RemoveGroupMemberConfirmationDialog({ group.accessControlAddFromInviteLink ); - const intlComponents = { - name: <ContactName title={conversation.title} />, - }; + const contactName = <ContactName title={conversation.title} />; return ( <ConfirmationDialog @@ -51,13 +49,13 @@ export function RemoveGroupMemberConfirmationDialog({ <Intl i18n={i18n} id="icu:RemoveGroupMemberConfirmation__description__with-link" - components={intlComponents} + components={{ name: contactName }} /> ) : ( <Intl i18n={i18n} id="icu:RemoveGroupMemberConfirmation__description" - components={intlComponents} + components={{ name: contactName }} /> ) } diff --git a/ts/components/conversation/SafetyNumberNotification.tsx b/ts/components/conversation/SafetyNumberNotification.tsx index 6eddc5fe153..7ac6e1bee8c 100644 --- a/ts/components/conversation/SafetyNumberNotification.tsx +++ b/ts/components/conversation/SafetyNumberNotification.tsx @@ -57,11 +57,7 @@ export function SafetyNumberNotification({ i18n={i18n} /> ) : ( - <Intl - id="icu:safetyNumberChanged" - components={{ name }} - i18n={i18n} - /> + <Intl id="icu:safetyNumberChanged" i18n={i18n} /> ) } button={ diff --git a/ts/components/conversation/UnsupportedMessage.tsx b/ts/components/conversation/UnsupportedMessage.tsx index dae17b4201a..2581b4ec756 100644 --- a/ts/components/conversation/UnsupportedMessage.tsx +++ b/ts/components/conversation/UnsupportedMessage.tsx @@ -50,28 +50,23 @@ function UnsupportedMessageContents({ canProcessNow, contact, i18n }: Props) { /> ); } - return ( - <Intl - id="icu:Message--from-me-unsupported-message" - components={{ contact: contactName }} - i18n={i18n} - /> - ); + return <Intl id="icu:Message--from-me-unsupported-message" i18n={i18n} />; } if (canProcessNow) { return ( <Intl id="icu:Message--from-me-unsupported-message-ask-to-resend" - components={{ contact: contactName }} i18n={i18n} /> ); } return ( <Intl - id="icu:Message--from-me-unsupported-message" - components={{ contact: contactName }} + id="icu:Message--unsupported-message" i18n={i18n} + components={{ + contact: contactName, + }} /> ); } diff --git a/ts/services/LinkPreview.ts b/ts/services/LinkPreview.ts index 2dc348c5436..9c176b76f14 100644 --- a/ts/services/LinkPreview.ts +++ b/ts/services/LinkPreview.ts @@ -519,7 +519,7 @@ async function getGroupPreview( window.Signal.Groups.decryptGroupTitle(result.title, secretParams) || window.i18n('icu:unknownGroup'); const description = window.i18n('icu:GroupV2--join--group-metadata--full', { - count: result?.memberCount ?? 0, + memberCount: result?.memberCount ?? 0, }); let image: undefined | LinkPreviewImage;