diff --git a/webapp/channels/src/components/common/hooks/useGetHighestThresholdCloudLimit.test.ts b/webapp/channels/src/components/common/hooks/useGetHighestThresholdCloudLimit.test.ts index 8a12cdb223..b1a33dd57c 100644 --- a/webapp/channels/src/components/common/hooks/useGetHighestThresholdCloudLimit.test.ts +++ b/webapp/channels/src/components/common/hooks/useGetHighestThresholdCloudLimit.test.ts @@ -8,6 +8,7 @@ import useGetHighestThresholdCloudLimit from './useGetHighestThresholdCloudLimit import type {LimitSummary} from './useGetHighestThresholdCloudLimit'; jest.mock('react', () => ({ + ...jest.requireActual('react'), useMemo: (fn: () => LimitSummary) => fn(), })); diff --git a/webapp/channels/src/components/common/hooks/useGetMultiplesExceededCloudLimit.test.ts b/webapp/channels/src/components/common/hooks/useGetMultiplesExceededCloudLimit.test.ts index ef92428582..6d02078737 100644 --- a/webapp/channels/src/components/common/hooks/useGetMultiplesExceededCloudLimit.test.ts +++ b/webapp/channels/src/components/common/hooks/useGetMultiplesExceededCloudLimit.test.ts @@ -8,6 +8,7 @@ import useGetMultiplesExceededCloudLimit from './useGetMultiplesExceededCloudLim import type {LimitSummary} from './useGetMultiplesExceededCloudLimit'; jest.mock('react', () => ({ + ...jest.requireActual('react'), useMemo: (fn: () => LimitSummary) => fn(), })); diff --git a/webapp/channels/src/components/copy_button.tsx b/webapp/channels/src/components/copy_button.tsx index a4816becbf..4d0ba89a39 100644 --- a/webapp/channels/src/components/copy_button.tsx +++ b/webapp/channels/src/components/copy_button.tsx @@ -4,18 +4,16 @@ import classNames from 'classnames'; import React, {useRef, useState} from 'react'; import {Tooltip} from 'react-bootstrap'; -import {FormattedMessage, useIntl} from 'react-intl'; +import {FormattedMessage, defineMessages, useIntl} from 'react-intl'; import OverlayTrigger from 'components/overlay_trigger'; import Constants from 'utils/constants'; -import {t} from 'utils/i18n'; import {copyToClipboard} from 'utils/utils'; type Props = { content: string; - beforeCopyText?: string; - afterCopyText?: string; + isForText?: boolean; placement?: string; className?: string; }; @@ -41,26 +39,18 @@ const CopyButton: React.FC = (props: Props) => { copyToClipboard(props.content); }; - const getId = () => { - if (isCopied) { - return t('copied.message'); - } - return props.beforeCopyText ? t('copy.text.message') : t('copy.code.message'); - }; - - const getDefaultMessage = () => { - if (isCopied) { - return props.afterCopyText; - } - return props.beforeCopyText ?? 'Copy code'; - }; + let tooltipMessage; + if (isCopied) { + tooltipMessage = messages.copied; + } else if (props.isForText) { + tooltipMessage = messages.copyText; + } else { + tooltipMessage = messages.copyCode; + } const tooltip = ( - + ); @@ -70,13 +60,13 @@ const CopyButton: React.FC = (props: Props) => { {!isCopied && @@ -94,9 +84,19 @@ const CopyButton: React.FC = (props: Props) => { ); }; -CopyButton.defaultProps = { - afterCopyText: 'Copied', - placement: 'top', -}; +const messages = defineMessages({ + copied: { + id: 'copied.message', + defaultMessage: 'Copied', + }, + copyCode: { + id: 'copy.code.message', + defaultMessage: 'Copy code', + }, + copyText: { + id: 'copy.text.message', + defaultMessage: 'Copy text', + }, +}); export default CopyButton; diff --git a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx index 66886b1f76..dd5442c672 100644 --- a/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx +++ b/webapp/channels/src/components/file_preview_modal/file_preview_modal_main_actions/file_preview_modal_main_actions.tsx @@ -145,15 +145,11 @@ const FilePreviewModalMainActions: React.FC = (props: Props) => { ); - const getBeforeCopyText = () => { - const fileType = getFileType(props.fileInfo.extension); - return fileType === FileTypes.TEXT ? 'Copy text' : undefined; - }; const copy = ( diff --git a/webapp/channels/src/components/searchable_channel_list.tsx b/webapp/channels/src/components/searchable_channel_list.tsx index 902ceeaa32..d59e1e24a9 100644 --- a/webapp/channels/src/components/searchable_channel_list.tsx +++ b/webapp/channels/src/components/searchable_channel_list.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import React from 'react'; -import {FormattedMessage, injectIntl, type WrappedComponentProps} from 'react-intl'; +import {FormattedMessage, defineMessages, injectIntl, type WrappedComponentProps} from 'react-intl'; import {ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, AccountOutlineIcon, GlobeCheckedIcon} from '@mattermost/compass-icons/components'; import type {Channel, ChannelMembership} from '@mattermost/types/channels'; @@ -21,7 +21,6 @@ import LoadingWrapper from 'components/widgets/loading/loading_wrapper'; import {isArchivedChannel} from 'utils/channel_utils'; import Constants, {ModalIdentifiers} from 'utils/constants'; -import {t} from 'utils/i18n'; import {isKeyPressed} from 'utils/keyboard'; import * as UserAgent from 'utils/user_agent'; import {localizeMessage, localizeAndFormatMessage} from 'utils/utils'; @@ -162,8 +161,8 @@ export class SearchableChannelList extends React.PureComponent { ) : null; const channelPurposeContainerAriaLabel = localizeAndFormatMessage( - t('more_channels.channel_purpose'), - 'Channel Information: Membership Indicator: Joined, Member count {memberCount}, Purpose: {channelPurpose}', + messages.channelPurpose.id, + messages.channelPurpose.defaultMessage, {memberCount, channelPurpose: channel.purpose || ''}, ); @@ -358,7 +357,7 @@ export class SearchableChannelList extends React.PureComponent { listContent = (
0 ? localizeAndFormatMessage(t('more_channels.noMore'), 'No results for {text}', {text: this.state.channelSearchValue}) : localizeMessage('widgets.channels_input.empty', 'No channels found') + aria-label={this.state.channelSearchValue.length > 0 ? localizeAndFormatMessage(messages.noMore.id, messages.noMore.defaultMessage, {text: this.state.channelSearchValue}) : localizeMessage('widgets.channels_input.empty', 'No channels found') } > @@ -547,7 +546,7 @@ export class SearchableChannelList extends React.PureComponent { } else if (channels.length === 1) { channelCountLabel = localizeMessage('more_channels.count_one', '1 Result'); } else if (channels.length > 1) { - channelCountLabel = localizeAndFormatMessage(t('more_channels.count'), '0 Results', {count: channels.length}); + channelCountLabel = localizeAndFormatMessage(messages.channelCount.id, messages.channelCount.defaultMessage, {count: channels.length}); } else { channelCountLabel = localizeMessage('more_channels.count_zero', '0 Results'); } @@ -588,4 +587,19 @@ export class SearchableChannelList extends React.PureComponent { } } +const messages = defineMessages({ + channelCount: { + id: 'more_channels.count', + defaultMessage: '{count} Results', + }, + channelPurpose: { + id: 'more_channels.channel_purpose', + defaultMessage: 'Channel Information: Membership Indicator: Joined, Member count {memberCount} , Purpose: {channelPurpose}', + }, + noMore: { + id: 'more_channels.noMore', + defaultMessage: 'No results for {text}', + }, +}); + export default injectIntl(SearchableChannelList); diff --git a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx index 4138ffa149..ccec7d61aa 100644 --- a/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx +++ b/webapp/channels/src/components/suggestion/at_mention_provider/at_mention_provider.tsx @@ -1,6 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {defineMessages} from 'react-intl'; + import type {Group} from '@mattermost/types/groups'; import type {UserProfile} from '@mattermost/types/users'; @@ -458,3 +460,26 @@ export default class AtMentionProvider extends Provider { }; } } + +defineMessages({ + groupDivider: { + id: 'suggestion.search.group', + defaultMessage: 'Group Mentions', + }, + memberDivider: { + id: 'suggestion.mention.members', + defaultMessage: 'Channel Members', + }, + moreMembersDivider: { + id: 'suggestion.mention.moremembers', + defaultMessage: 'Other Members', + }, + nonmemberDivider: { + id: 'suggestion.mention.nonmembers', + defaultMessage: 'Not in Channel', + }, + specialDivider: { + id: 'suggestion.mention.special', + defaultMessage: 'Special Mentions', + }, +}); diff --git a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx index 1234ce44c6..53358e631b 100644 --- a/webapp/channels/src/components/suggestion/channel_mention_provider.tsx +++ b/webapp/channels/src/components/suggestion/channel_mention_provider.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import {defineMessages} from 'react-intl'; import type {Channel} from '@mattermost/types/channels'; @@ -260,3 +261,10 @@ export default class ChannelMentionProvider extends Provider { this.lastPrefixWithNoResults = ''; } } + +defineMessages({ + myChannelsDivider: { + id: 'suggestion.mention.channels', + defaultMessage: 'My Channels', + }, +}); diff --git a/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx b/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx index f2764d8639..017fff0ecb 100644 --- a/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx +++ b/webapp/channels/src/components/suggestion/command_provider/command_provider.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import {defineMessages} from 'react-intl'; import type {Store} from 'redux'; import {DockWindowIcon} from '@mattermost/compass-icons/components'; @@ -288,3 +289,10 @@ export default class CommandProvider extends Provider { return matches.findIndex((match) => match.Complete === complete) !== -1; } } + +defineMessages({ + commandsDivider: { + id: 'suggestion.commands', + defaultMessage: 'Commands', + }, +}); diff --git a/webapp/channels/src/components/suggestion/emoticon_provider.tsx b/webapp/channels/src/components/suggestion/emoticon_provider.tsx index a8ad614650..cfe94b2c1d 100644 --- a/webapp/channels/src/components/suggestion/emoticon_provider.tsx +++ b/webapp/channels/src/components/suggestion/emoticon_provider.tsx @@ -2,6 +2,7 @@ // See LICENSE.txt for license information. import React from 'react'; +import {defineMessages} from 'react-intl'; import type {Emoji} from '@mattermost/types/emojis'; @@ -11,7 +12,6 @@ import {getEmojiImageUrl, isSystemEmoji} from 'mattermost-redux/utils/emoji_util import {getEmojiMap, getRecentEmojisNames} from 'selectors/emojis'; import store from 'stores/redux_store'; -import {Preferences} from 'utils/constants'; import {compareEmojis, emojiMatchesSkin} from 'utils/emoji_utils'; import * as Emoticons from 'utils/emoticons'; @@ -29,6 +29,8 @@ type EmojiItem = { type: string; } +const suggestionTypeEmoji = 'emoji'; + const EmoticonSuggestion = React.forwardRef>((props, ref) => { const text = props.term; const emoji = props.item.emoji; @@ -131,7 +133,7 @@ export default class EmoticonProvider extends Provider { // if the emoji has skin, only add those that match with the user selected skin. if (emojiMatchesSkin(emoji, skintone)) { - matchedArray.push({name: alias, emoji, type: Preferences.CATEGORY_EMOJI}); + matchedArray.push({name: alias, emoji, type: suggestionTypeEmoji}); } break; } @@ -145,7 +147,7 @@ export default class EmoticonProvider extends Provider { const matchedArray = recentEmojis.includes(name) ? recentMatched : matched; - matchedArray.push({name, emoji, type: Preferences.CATEGORY_EMOJI}); + matchedArray.push({name, emoji, type: suggestionTypeEmoji}); } } @@ -176,3 +178,10 @@ export default class EmoticonProvider extends Provider { }); } } + +defineMessages({ + emojisDivider: { + id: 'suggestion.emoji', + defaultMessage: 'Emoji', + }, +}); diff --git a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx index c5eafd90ba..105e14887c 100644 --- a/webapp/channels/src/components/suggestion/switch_channel_provider.tsx +++ b/webapp/channels/src/components/suggestion/switch_channel_provider.tsx @@ -3,6 +3,7 @@ import classNames from 'classnames'; import React from 'react'; +import {defineMessages} from 'react-intl'; import {connect, useSelector} from 'react-redux'; import type {Channel, ChannelMembership, ChannelType} from '@mattermost/types/channels'; @@ -863,3 +864,22 @@ export default class SwitchChannelProvider extends Provider { }); } } + +defineMessages({ + moreChannels: { + id: 'suggestion.mention.morechannels', + defaultMessage: 'Other Channels', + }, + privateChannelsDivider: { + id: 'suggestion.mention.private.channels', + defaultMessage: 'Private Channels', + }, + recentChannels: { + id: 'suggestion.mention.recent.channels', + defaultMessage: 'Recent', + }, + unreadChannels: { + id: 'suggestion.mention.unread', + defaultMessage: 'Unread', + }, +}); diff --git a/webapp/channels/src/components/widgets/icons/close_icon.tsx b/webapp/channels/src/components/widgets/icons/close_icon.tsx index f60c0ac8b1..fe3ff4da5c 100644 --- a/webapp/channels/src/components/widgets/icons/close_icon.tsx +++ b/webapp/channels/src/components/widgets/icons/close_icon.tsx @@ -4,8 +4,6 @@ import React from 'react'; import {useIntl} from 'react-intl'; -import {t} from 'utils/i18n'; - export default function CloseIcon(props: React.HTMLAttributes) { const {formatMessage} = useIntl(); return ( @@ -15,7 +13,7 @@ export default function CloseIcon(props: React.HTMLAttributes) height='24px' viewBox='0 0 24 24' role='img' - aria-label={formatMessage({id: t('generic_icons.close'), defaultMessage: 'Close Icon'})} + aria-label={formatMessage({id: 'generic_icons.close', defaultMessage: 'Close Icon'})} > void; inputValue: string; - loadingMessageId?: string; - loadingMessageDefault?: string; - noOptionsMessageId?: string; - noOptionsMessageDefault?: string; + loadingMessage?: MessageDescriptor; + noOptionsMessage?: MessageDescriptor; } type State = { options: Channel[]; }; +const messages = defineMessages({ + loading: { + id: 'widgets.channels_input.loading', + defaultMessage: 'Loading', + }, + noOptions: { + id: 'widgets.channels_input.empty', + defaultMessage: 'No channels found', + }, +}); + export default class ChannelsInput extends React.PureComponent { static defaultProps = { - loadingMessageId: t('widgets.channels_input.loading'), - loadingMessageDefault: 'Loading', - noOptionsMessageId: t('widgets.channels_input.empty'), - noOptionsMessageDefault: 'No channels found', + loadingMessage: messages.loading, + noOptionsMessage: messages.noOptions, }; private selectRef: RefObject & {handleInputChange: (newValue: string, actionMeta: InputActionMeta | {action: 'custom'}) => string}>; @@ -89,8 +96,7 @@ export default class ChannelsInput extends React.PureComponent { loadingMessage = () => { const text = ( ); @@ -108,8 +114,7 @@ export default class ChannelsInput extends React.PureComponent {
diff --git a/webapp/channels/src/components/widgets/menu/menu_items/useWords.tsx b/webapp/channels/src/components/widgets/menu/menu_items/useWords.tsx index 2b41b518a6..2a4efb2430 100644 --- a/webapp/channels/src/components/widgets/menu/menu_items/useWords.tsx +++ b/webapp/channels/src/components/widgets/menu/menu_items/useWords.tsx @@ -4,14 +4,13 @@ import type {PrimitiveType, FormatXMLElementFn} from 'intl-messageformat'; import React from 'react'; import type {ReactNode} from 'react'; -import {useIntl} from 'react-intl'; +import {defineMessage, useIntl} from 'react-intl'; import type {LimitSummary} from 'components/common/hooks/useGetHighestThresholdCloudLimit'; import useOpenPricingModal from 'components/common/hooks/useOpenPricingModal'; import NotifyAdminCTA from 'components/notify_admin_cta/notify_admin_cta'; import {MattermostFeatures, LicenseSkus} from 'utils/constants'; -import {t} from 'utils/i18n'; import {limitThresholds, asGBString, inK, LimitTypes} from 'utils/limits'; interface Words { @@ -82,36 +81,50 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser switch (highestLimit.id) { case LimitTypes.messageHistory: { - let id = t('workspace_limits.menu_limit.warn.messages_history'); - let defaultMessage = 'You’re getting closer to the free {limit} message limit. {callToAction}'; + let description = defineMessage({ + id: 'workspace_limits.menu_limit.warn.messages_history', + defaultMessage: 'You’re getting closer to the free {limit} message limit. {callToAction}', + }); values.limit = intl.formatNumber(highestLimit.limit); if (usageRatio >= limitThresholds.danger) { if (isAdminUser) { - id = t('workspace_limits.menu_limit.critical.messages_history'); - defaultMessage = 'You’re close to hitting the free {limit} message history limit {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.critical.messages_history', + defaultMessage: 'You’re close to hitting the free {limit} message history limit {callToAction}', + }); } else { - id = t('workspace_limits.menu_limit.critical.messages_history_non_admin'); - defaultMessage = 'You\'re almost at the message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.critical.messages_history_non_admin', + defaultMessage: 'You\'re almost at the message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}', + }); } } if (usageRatio >= limitThresholds.reached) { if (isAdminUser) { - id = t('workspace_limits.menu_limit.reached.messages_history'); - defaultMessage = 'You’ve reached the free message history limit. You can only view up to the last {limit} messages in your history. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.reached.messages_history', + defaultMessage: 'You’ve reached the free message history limit. You can only view up to the last {limit} messages in your history. {callToAction}', + }); values.limit = inK(highestLimit.limit); } else { - id = t('workspace_limits.menu_limit.reached.messages_history_non_admin'); - defaultMessage = 'You’ve reached your message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.reached.messages_history_non_admin', + defaultMessage: 'You’ve reached your message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}', + }); } } if (usageRatio >= limitThresholds.exceeded) { if (isAdminUser) { - id = t('workspace_limits.menu_limit.over.messages_history'); - defaultMessage = 'You’re over the free message history limit. You can only view up to the last {limit} messages in your history. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.over.messages_history', + defaultMessage: 'You’re over the free message history limit. You can only view up to the last {limit} messages in your history. {callToAction}', + }); values.limit = inK(highestLimit.limit); } else { - id = t('workspace_limits.menu_limit.over.messages_history_non_admin'); - defaultMessage = 'You\'re over your message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.over.messages_history_non_admin', + defaultMessage: 'You\'re over your message limit. Your admin can upgrade your plan for unlimited messages. {callToAction}', + }); } } return { @@ -120,30 +133,35 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser defaultMessage: 'Total messages', }), description: intl.formatMessage( - { - id, - defaultMessage, - }, + description, values, ), status: inK(highestLimit.usage), }; } case LimitTypes.fileStorage: { - let id = t('workspace_limits.menu_limit.warn.files_storage'); - let defaultMessage = 'You’re getting closer to the {limit} file storage limit. {callToAction}'; + let description = defineMessage({ + id: 'workspace_limits.menu_limit.warn.files_storage', + defaultMessage: 'You’re getting closer to the {limit} file storage limit. {callToAction}', + }); values.limit = asGBString(highestLimit.limit, intl.formatNumber); if (usageRatio >= limitThresholds.danger) { - id = t('workspace_limits.menu_limit.critical.files_storage'); - defaultMessage = 'You’re getting closer to the {limit} file storage limit. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.critical.files_storage', + defaultMessage: 'You’re getting closer to the {limit} file storage limit. {callToAction}', + }); } if (usageRatio >= limitThresholds.reached) { - id = t('workspace_limits.menu_limit.reached.files_storage'); - defaultMessage = 'You’ve reached the {limit} file storage limit. You can only access the most recent {limit} worth of files. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.reached.files_storage', + defaultMessage: 'You’ve reached the {limit} file storage limit. You can only access the most recent {limit} worth of files. {callToAction}', + }); } if (usageRatio >= limitThresholds.exceeded) { - id = t('workspace_limits.menu_limit.over.files_storage'); - defaultMessage = 'You’re over the {limit} file storage limit. You can only access the most recent {limit} worth of files. {callToAction}'; + description = defineMessage({ + id: 'workspace_limits.menu_limit.over.files_storage', + defaultMessage: 'You’re over the {limit} file storage limit. You can only access the most recent {limit} worth of files. {callToAction}', + }); } return { @@ -152,10 +170,7 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser defaultMessage: 'File storage limit', }), description: intl.formatMessage( - { - id, - defaultMessage, - }, + description, values, ), status: asGBString(highestLimit.usage, intl.formatNumber), diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index a169894747..ca9ec0cc57 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -5043,7 +5043,6 @@ "suggestion.mention.all": "Notifies everyone in this channel", "suggestion.mention.channel": "Notifies everyone in this channel", "suggestion.mention.channels": "My Channels", - "suggestion.mention.groups": "Group Mentions", "suggestion.mention.here": "Notifies everyone online in this channel", "suggestion.mention.members": "Channel Members", "suggestion.mention.morechannels": "Other Channels", @@ -5053,7 +5052,6 @@ "suggestion.mention.recent.channels": "Recent", "suggestion.mention.special": "Special Mentions", "suggestion.mention.unread": "Unread", - "suggestion.mention.unread.channels": "Unread Channels", "suggestion.private": "Private channels", "suggestion.public": "Public channels", "suggestion.search.direct": "Direct Messages", diff --git a/webapp/channels/src/utils/constants.tsx b/webapp/channels/src/utils/constants.tsx index d91b9fbad0..86858219f3 100644 --- a/webapp/channels/src/utils/constants.tsx +++ b/webapp/channels/src/utils/constants.tsx @@ -8,6 +8,7 @@ import solarizedLightCSS from 'highlight.js/styles/base16/solarized-light.css'; import githubCSS from 'highlight.js/styles/github.css'; import monokaiCSS from 'highlight.js/styles/monokai.css'; import keyMirror from 'key-mirror'; +import {defineMessage, defineMessages} from 'react-intl'; import {CustomStatusDuration} from '@mattermost/types/users'; @@ -29,7 +30,6 @@ import monokaiIcon from 'images/themes/code_themes/monokai.png'; import solarizedDarkIcon from 'images/themes/code_themes/solarized-dark.png'; import solarizedLightIcon from 'images/themes/code_themes/solarized-light.png'; import logoWebhook from 'images/webhook_icon.jpg'; -import {t} from 'utils/i18n'; export const SettingsTypes = { TYPE_TEXT: 'text' as const, @@ -913,16 +913,52 @@ export const AnnouncementBarTypes = { }; export const AnnouncementBarMessages = { - EMAIL_VERIFICATION_REQUIRED: t('announcement_bar.error.email_verification_required'), - EMAIL_VERIFIED: t('announcement_bar.notification.email_verified'), - LICENSE_EXPIRED: t('announcement_bar.error.license_expired'), - LICENSE_EXPIRING: t('announcement_bar.error.license_expiring'), - LICENSE_PAST_GRACE: t('announcement_bar.error.past_grace'), - PREVIEW_MODE: t('announcement_bar.error.preview_mode'), - WEBSOCKET_PORT_ERROR: t('channel_loader.socketError'), - TRIAL_LICENSE_EXPIRING: t('announcement_bar.error.trial_license_expiring'), + EMAIL_VERIFICATION_REQUIRED: 'announcement_bar.error.email_verification_required', + EMAIL_VERIFIED: 'announcement_bar.notification.email_verified', + LICENSE_EXPIRED: 'announcement_bar.error.license_expired', + LICENSE_EXPIRING: 'announcement_bar.error.license_expiring', + LICENSE_PAST_GRACE: 'announcement_bar.error.past_grace', + PREVIEW_MODE: 'announcement_bar.error.preview_mode', + WEBSOCKET_PORT_ERROR: 'channel_loader.socketError', + TRIAL_LICENSE_EXPIRING: 'announcement_bar.error.trial_license_expiring', }; +// These messages correspond to AnnouncementBarMessages above +defineMessages({ + emailVerificationRequired: { + id: 'announcement_bar.error.email_verification_required', + defaultMessage: 'Check your email inbox to verify the address.', + }, + emailVerified: { + id: 'announcement_bar.notification.email_verified', + defaultMessage: 'Email verified', + }, + licenseExpired: { + id: 'announcement_bar.error.license_expired', + defaultMessage: '{licenseSku} license is expired and some features may be disabled.', + }, + licenseExpiring: { + id: 'announcement_bar.error.license_expiring', + defaultMessage: '{licenseSku} license expires on {date, date, long}.', + }, + pastGrace: { + id: 'announcement_bar.error.past_grace', + defaultMessage: '{licenseSku} license is expired and some features may be disabled. Please contact your System Administrator for details.', + }, + previewMode: { + id: 'announcement_bar.error.preview_mode', + defaultMessage: 'Preview Mode: Email notifications have not been configured.', + }, + socketError: { + id: 'channel_loader.socketError', + defaultMessage: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to [check WebSocket port](!https://docs.mattermost.com/install/troubleshooting.html#please-check-connection-mattermost-unreachable-if-issue-persists-ask-administrator-to-check-websocket-port).', + }, + trialLicenseExpiring: { + id: 'announcement_bar.error.trial_license_expiring', + defaultMessage: 'There are {days} days left on your free trial.', + }, +}); + export const VerifyEmailErrors = { FAILED_EMAIL_VERIFICATION: 'failed_email_verification', FAILED_USER_STATE_GET: 'failed_get_user_state', @@ -2070,43 +2106,25 @@ export const WindowSizes = { export const AcceptedProfileImageTypes = ['image/jpeg', 'image/png', 'image/bmp']; -export const searchHintOptions = [{searchTerm: 'From:', message: {id: t('search_list_option.from'), defaultMessage: 'Messages from a user'}}, - {searchTerm: 'In:', message: {id: t('search_list_option.in'), defaultMessage: 'Messages in a channel'}}, - {searchTerm: 'On:', message: {id: t('search_list_option.on'), defaultMessage: 'Messages on a date'}}, - {searchTerm: 'Before:', message: {id: t('search_list_option.before'), defaultMessage: 'Messages before a date'}}, - {searchTerm: 'After:', message: {id: t('search_list_option.after'), defaultMessage: 'Messages after a date'}}, - {searchTerm: '-', message: {id: t('search_list_option.exclude'), defaultMessage: 'Exclude search terms'}, additionalDisplay: '—'}, - {searchTerm: '""', message: {id: t('search_list_option.phrases'), defaultMessage: 'Messages with phrases'}}, +export const searchHintOptions = [{searchTerm: 'From:', message: defineMessage({id: 'search_list_option.from', defaultMessage: 'Messages from a user'})}, + {searchTerm: 'In:', message: defineMessage({id: 'search_list_option.in', defaultMessage: 'Messages in a channel'})}, + {searchTerm: 'On:', message: defineMessage({id: 'search_list_option.on', defaultMessage: 'Messages on a date'})}, + {searchTerm: 'Before:', message: defineMessage({id: 'search_list_option.before', defaultMessage: 'Messages before a date'})}, + {searchTerm: 'After:', message: defineMessage({id: 'search_list_option.after', defaultMessage: 'Messages after a date'})}, + {searchTerm: '-', message: defineMessage({id: 'search_list_option.exclude', defaultMessage: 'Exclude search terms'}), additionalDisplay: '—'}, + {searchTerm: '""', message: defineMessage({id: 'search_list_option.phrases', defaultMessage: 'Messages with phrases'})}, ]; -export const searchFilesHintOptions = [{searchTerm: 'From:', message: {id: t('search_files_list_option.from'), defaultMessage: 'Files from a user'}}, - {searchTerm: 'In:', message: {id: t('search_files_list_option.in'), defaultMessage: 'Files in a channel'}}, - {searchTerm: 'On:', message: {id: t('search_files_list_option.on'), defaultMessage: 'Files on a date'}}, - {searchTerm: 'Before:', message: {id: t('search_files_list_option.before'), defaultMessage: 'Files before a date'}}, - {searchTerm: 'After:', message: {id: t('search_files_list_option.after'), defaultMessage: 'Files after a date'}}, - {searchTerm: 'Ext:', message: {id: t('search_files_list_option.ext'), defaultMessage: 'Files with a extension'}}, - {searchTerm: '-', message: {id: t('search_files_list_option.exclude'), defaultMessage: 'Exclude search terms'}, additionalDisplay: '—'}, - {searchTerm: '""', message: {id: t('search_files_list_option.phrases'), defaultMessage: 'Files with phrases'}}, +export const searchFilesHintOptions = [{searchTerm: 'From:', message: defineMessage({id: 'search_files_list_option.from', defaultMessage: 'Files from a user'})}, + {searchTerm: 'In:', message: defineMessage({id: 'search_files_list_option.in', defaultMessage: 'Files in a channel'})}, + {searchTerm: 'On:', message: defineMessage({id: 'search_files_list_option.on', defaultMessage: 'Files on a date'})}, + {searchTerm: 'Before:', message: defineMessage({id: 'search_files_list_option.before', defaultMessage: 'Files before a date'})}, + {searchTerm: 'After:', message: defineMessage({id: 'search_files_list_option.after', defaultMessage: 'Files after a date'})}, + {searchTerm: 'Ext:', message: defineMessage({id: 'search_files_list_option.ext', defaultMessage: 'Files with a extension'})}, + {searchTerm: '-', message: defineMessage({id: 'search_files_list_option.exclude', defaultMessage: 'Exclude search terms'}), additionalDisplay: '—'}, + {searchTerm: '""', message: defineMessage({id: 'search_files_list_option.phrases', defaultMessage: 'Files with phrases'})}, ]; -// adding these rtranslations here so the weblate CI step will not fail with empty translation strings -t('suggestion.archive'); -t('suggestion.mention.channels'); -t('suggestion.mention.morechannels'); -t('suggestion.mention.unread.channels'); -t('suggestion.mention.unread'); -t('suggestion.mention.members'); -t('suggestion.mention.moremembers'); -t('suggestion.mention.nonmembers'); -t('suggestion.mention.private.channels'); -t('suggestion.mention.recent.channels'); -t('suggestion.mention.special'); -t('suggestion.mention.groups'); -t('suggestion.search.public'); -t('suggestion.search.group'); -t('suggestion.commands'); -t('suggestion.emoji'); - const { DONT_CLEAR, THIRTY_MINUTES, @@ -2118,40 +2136,41 @@ const { CUSTOM_DATE_TIME, } = CustomStatusDuration; -export const durationValues = { +export const durationValues = defineMessages({ [DONT_CLEAR]: { - id: t('custom_status.expiry_dropdown.dont_clear'), + id: 'custom_status.expiry_dropdown.dont_clear', defaultMessage: "Don't clear", }, [THIRTY_MINUTES]: { - id: t('custom_status.expiry_dropdown.thirty_minutes'), + id: 'custom_status.expiry_dropdown.thirty_minutes', defaultMessage: '30 minutes', }, [ONE_HOUR]: { - id: t('custom_status.expiry_dropdown.one_hour'), + id: 'custom_status.expiry_dropdown.one_hour', defaultMessage: '1 hour', }, [FOUR_HOURS]: { - id: t('custom_status.expiry_dropdown.four_hours'), + id: 'custom_status.expiry_dropdown.four_hours', defaultMessage: '4 hours', }, [TODAY]: { - id: t('custom_status.expiry_dropdown.today'), + id: 'custom_status.expiry_dropdown.today', defaultMessage: 'Today', }, [THIS_WEEK]: { - id: t('custom_status.expiry_dropdown.this_week'), + id: 'custom_status.expiry_dropdown.this_week', defaultMessage: 'This week', }, [DATE_AND_TIME]: { - id: t('custom_status.expiry_dropdown.date_and_time'), + id: 'custom_status.expiry_dropdown.date_and_time', defaultMessage: 'Custom Date and Time', }, [CUSTOM_DATE_TIME]: { - id: t('custom_status.expiry_dropdown.date_and_time'), + id: 'custom_status.expiry_dropdown.date_and_time', defaultMessage: 'Custom Date and Time', }, -}; +}); + export enum ClaimErrors { MFA_VALIDATE_TOKEN_AUTHENTICATE = 'mfa.validate_token.authenticate.app_error', ENT_LDAP_LOGIN_USER_NOT_REGISTERED = 'ent.ldap.do_login.user_not_registered.app_error', diff --git a/webapp/channels/src/utils/url.tsx b/webapp/channels/src/utils/url.tsx index f1989fe39d..79582b1e6e 100644 --- a/webapp/channels/src/utils/url.tsx +++ b/webapp/channels/src/utils/url.tsx @@ -2,12 +2,11 @@ // See LICENSE.txt for license information. import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import type {IntlShape} from 'react-intl'; +import {FormattedMessage, defineMessage} from 'react-intl'; +import type {IntlShape, MessageDescriptor} from 'react-intl'; import {getModule} from 'module_registry'; import Constants from 'utils/constants'; -import {t} from 'utils/i18n'; import {latinise} from 'utils/latinise'; import * as TextFormatting from 'utils/text_formatting'; @@ -106,18 +105,19 @@ export function getScheme(url: string): string | null { return match && match[1]; } -function formattedError(id: string, message: string, intl?: IntlShape): React.ReactElement | string { +function formattedError(message: MessageDescriptor, intl?: IntlShape): React.ReactElement | string { if (intl) { - return intl.formatMessage({id, defaultMessage: message}); + return intl.formatMessage(message); } - return ( - -
-
); + return ( + + +
+
+ ); } export function validateChannelUrl(url: string, intl?: IntlShape): Array { @@ -134,34 +134,82 @@ export function validateChannelUrl(url: string, intl?: IntlShape): Array 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_'); if (startsWithoutLetter && endsWithoutLetter) { - errors.push(formattedError(t('change_url.startAndEndWithLetter'), 'URLs must start and end with a lowercase letter or number.', intl)); + errors.push(formattedError( + defineMessage({ + id: 'change_url.startAndEndWithLetter', + defaultMessage: 'URLs must start and end with a lowercase letter or number.', + }), + intl, + )); } else if (startsWithoutLetter) { - errors.push(formattedError(t('change_url.startWithLetter'), 'URLs must start with a lowercase letter or number.', intl)); + errors.push(formattedError( + defineMessage({ + id: 'change_url.startWithLetter', + defaultMessage: 'URLs must start with a lowercase letter or number.', + }), + intl, + )); } else if (endsWithoutLetter) { - errors.push(formattedError(t('change_url.endWithLetter'), 'URLs must end with a lowercase letter or number.', intl)); + errors.push(formattedError( + defineMessage({ + id: 'change_url.endWithLetter', + defaultMessage: 'URLs must end with a lowercase letter or number.', + }), + intl, + )); } // In case of error we don't detect if (errors.length === 0) { - errors.push(formattedError(t('change_url.invalidUrl'), 'Invalid URL', intl)); + errors.push(formattedError( + defineMessage({ + id: 'change_url.invalidUrl', + defaultMessage: 'Invalid URL', + }), + intl, + )); } } diff --git a/webapp/channels/src/utils/utils.tsx b/webapp/channels/src/utils/utils.tsx index 0dc90735de..27e6f6f658 100644 --- a/webapp/channels/src/utils/utils.tsx +++ b/webapp/channels/src/utils/utils.tsx @@ -60,7 +60,6 @@ import type {TextboxElement} from 'components/textbox'; import {getHistory} from 'utils/browser_history'; import Constants, {FileTypes, ValidationErrors, A11yCustomEventTypes} from 'utils/constants'; import type {A11yFocusEventDetail} from 'utils/constants'; -import {t} from 'utils/i18n'; import * as Keyboard from 'utils/keyboard'; import * as UserAgent from 'utils/user_agent'; @@ -1236,7 +1235,8 @@ export function getPasswordConfig(config: Partial) { } export function isValidPassword(password: string, passwordConfig: ReturnType, intl?: IntlShape) { - let errorId = t('user.settings.security.passwordError'); + // The translation strings used by this function are defined in admin_console/password_settings + let errorId = 'user.settings.security.passwordError'; const telemetryErrorIds = []; let valid = true; const minimumLength = passwordConfig.minimumLength || Constants.MIN_PASSWORD_LENGTH;