Remove t from utils and most components (#27274)

* Remove t from utils/utils

* Remove t from utils/url

* Remove t from utils/constants

* Remove t from components/widgets

* Remove t from components/copy_button

* Remove t from components/searchable_channel_list

* Fix mocking of React in some tests
Этот коммит содержится в:
Harrison Healey
2024-06-17 13:40:37 -04:00
коммит произвёл GitHub
родитель 9187c772b6
Коммит 0da473b9f8
17 изменённых файлов: 331 добавлений и 166 удалений

Просмотреть файл

@@ -8,6 +8,7 @@ import useGetHighestThresholdCloudLimit from './useGetHighestThresholdCloudLimit
import type {LimitSummary} from './useGetHighestThresholdCloudLimit';
jest.mock('react', () => ({
...jest.requireActual('react'),
useMemo: (fn: () => LimitSummary) => fn(),
}));

Просмотреть файл

@@ -8,6 +8,7 @@ import useGetMultiplesExceededCloudLimit from './useGetMultiplesExceededCloudLim
import type {LimitSummary} from './useGetMultiplesExceededCloudLimit';
jest.mock('react', () => ({
...jest.requireActual('react'),
useMemo: (fn: () => LimitSummary) => fn(),
}));

Просмотреть файл

@@ -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: 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 = (
<Tooltip id='copyButton'>
<FormattedMessage
id={getId()}
defaultMessage={getDefaultMessage()}
/>
<FormattedMessage {...tooltipMessage}/>
</Tooltip>
);
@@ -70,13 +60,13 @@ const CopyButton: React.FC<Props> = (props: Props) => {
<OverlayTrigger
shouldUpdatePosition={true}
delayShow={Constants.OVERLAY_TIME_DELAY}
placement={props.placement}
placement={props.placement ?? 'top'}
overlay={tooltip}
>
<span
className={spanClassName}
onClick={copyText}
aria-label={intl.formatMessage({id: getId(), defaultMessage: getDefaultMessage()})}
aria-label={intl.formatMessage(tooltipMessage)}
role='button'
>
{!isCopied &&
@@ -94,9 +84,19 @@ const CopyButton: React.FC<Props> = (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;

Просмотреть файл

@@ -145,15 +145,11 @@ const FilePreviewModalMainActions: React.FC<Props> = (props: Props) => {
</ExternalLink>
</OverlayTrigger>
);
const getBeforeCopyText = () => {
const fileType = getFileType(props.fileInfo.extension);
return fileType === FileTypes.TEXT ? 'Copy text' : undefined;
};
const copy = (
<CopyButton
className='file-preview-modal-main-actions__action-item'
beforeCopyText={getBeforeCopyText()}
isForText={getFileType(props.fileInfo.extension) === FileTypes.TEXT}
placement={tooltipPlacement}
content={props.content}
/>

Просмотреть файл

@@ -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<Props, State> {
) : 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<Props, State> {
listContent = (
<div
className='no-channel-message'
aria-label={this.state.channelSearchValue.length > 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')
}
>
<MagnifyingGlassSVG/>
@@ -547,7 +546,7 @@ export class SearchableChannelList extends React.PureComponent<Props, State> {
} 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<Props, State> {
}
}
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);

Просмотреть файл

@@ -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',
},
});

Просмотреть файл

@@ -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',
},
});

Просмотреть файл

@@ -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',
},
});

Просмотреть файл

@@ -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<HTMLDivElement, SuggestionProps<EmojiItem>>((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',
},
});

Просмотреть файл

@@ -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',
},
});

Просмотреть файл

@@ -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<HTMLSpanElement>) {
const {formatMessage} = useIntl();
return (
@@ -15,7 +13,7 @@ export default function CloseIcon(props: React.HTMLAttributes<HTMLSpanElement>)
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'})}
>
<path
fillRule='nonzero'

Просмотреть файл

@@ -4,7 +4,8 @@
import classNames from 'classnames';
import React from 'react';
import type {RefObject} from 'react';
import {FormattedMessage} from 'react-intl';
import type {MessageDescriptor} from 'react-intl';
import {FormattedMessage, defineMessages} from 'react-intl';
import {components} from 'react-select';
import type {ValueType, ActionMeta, InputActionMeta} from 'react-select';
import type {Async} from 'react-select/async';
@@ -19,7 +20,6 @@ import PrivateChannelIcon from 'components/widgets/icons/lock_icon';
import LoadingSpinner from 'components/widgets/loading/loading_spinner';
import {Constants} from 'utils/constants';
import {t} from 'utils/i18n';
import './channels_input.scss';
@@ -31,22 +31,29 @@ type Props = {
value: Channel[];
onInputChange: (change: string) => 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<Props, State> {
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<Async<Channel> & {handleInputChange: (newValue: string, actionMeta: InputActionMeta | {action: 'custom'}) => string}>;
@@ -89,8 +96,7 @@ export default class ChannelsInput extends React.PureComponent<Props, State> {
loadingMessage = () => {
const text = (
<FormattedMessage
id={this.props.loadingMessageId}
defaultMessage={this.props.loadingMessageDefault}
{...this.props.loadingMessage}
/>
);
@@ -108,8 +114,7 @@ export default class ChannelsInput extends React.PureComponent<Props, State> {
<div className='channels-input__option channels-input__option--no-matches'>
<Msg {...props}>
<FormattedMarkdownMessage
id={this.props.noOptionsMessageId}
defaultMessage={this.props.noOptionsMessageDefault}
{...this.props.noOptionsMessage}
values={{text: inputValue}}
/>
</Msg>

Просмотреть файл

@@ -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 = 'Youre getting closer to the free {limit} message limit. <a>{callToAction}</a>';
let description = defineMessage({
id: 'workspace_limits.menu_limit.warn.messages_history',
defaultMessage: 'Youre getting closer to the free {limit} message limit. <a>{callToAction}</a>',
});
values.limit = intl.formatNumber(highestLimit.limit);
if (usageRatio >= limitThresholds.danger) {
if (isAdminUser) {
id = t('workspace_limits.menu_limit.critical.messages_history');
defaultMessage = 'Youre close to hitting the free {limit} message history limit <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.critical.messages_history',
defaultMessage: 'Youre close to hitting the free {limit} message history limit <a>{callToAction}</a>',
});
} 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. <a>{callToAction}</a>';
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. <a>{callToAction}</a>',
});
}
}
if (usageRatio >= limitThresholds.reached) {
if (isAdminUser) {
id = t('workspace_limits.menu_limit.reached.messages_history');
defaultMessage = 'Youve reached the free message history limit. You can only view up to the last {limit} messages in your history. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.reached.messages_history',
defaultMessage: 'Youve reached the free message history limit. You can only view up to the last {limit} messages in your history. <a>{callToAction}</a>',
});
values.limit = inK(highestLimit.limit);
} else {
id = t('workspace_limits.menu_limit.reached.messages_history_non_admin');
defaultMessage = 'Youve reached your message limit. Your admin can upgrade your plan for unlimited messages. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.reached.messages_history_non_admin',
defaultMessage: 'Youve reached your message limit. Your admin can upgrade your plan for unlimited messages. <a>{callToAction}</a>',
});
}
}
if (usageRatio >= limitThresholds.exceeded) {
if (isAdminUser) {
id = t('workspace_limits.menu_limit.over.messages_history');
defaultMessage = 'Youre over the free message history limit. You can only view up to the last {limit} messages in your history. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.over.messages_history',
defaultMessage: 'Youre over the free message history limit. You can only view up to the last {limit} messages in your history. <a>{callToAction}</a>',
});
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. <a>{callToAction}</a>';
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. <a>{callToAction}</a>',
});
}
}
return {
@@ -120,30 +133,35 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser
defaultMessage: 'Total messages',
}),
description: intl.formatMessage<ReactNode>(
{
id,
defaultMessage,
},
description,
values,
),
status: inK(highestLimit.usage),
};
}
case LimitTypes.fileStorage: {
let id = t('workspace_limits.menu_limit.warn.files_storage');
let defaultMessage = 'Youre getting closer to the {limit} file storage limit. <a>{callToAction}</a>';
let description = defineMessage({
id: 'workspace_limits.menu_limit.warn.files_storage',
defaultMessage: 'Youre getting closer to the {limit} file storage limit. <a>{callToAction}</a>',
});
values.limit = asGBString(highestLimit.limit, intl.formatNumber);
if (usageRatio >= limitThresholds.danger) {
id = t('workspace_limits.menu_limit.critical.files_storage');
defaultMessage = 'Youre getting closer to the {limit} file storage limit. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.critical.files_storage',
defaultMessage: 'Youre getting closer to the {limit} file storage limit. <a>{callToAction}</a>',
});
}
if (usageRatio >= limitThresholds.reached) {
id = t('workspace_limits.menu_limit.reached.files_storage');
defaultMessage = 'Youve reached the {limit} file storage limit. You can only access the most recent {limit} worth of files. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.reached.files_storage',
defaultMessage: 'Youve reached the {limit} file storage limit. You can only access the most recent {limit} worth of files. <a>{callToAction}</a>',
});
}
if (usageRatio >= limitThresholds.exceeded) {
id = t('workspace_limits.menu_limit.over.files_storage');
defaultMessage = 'Youre over the {limit} file storage limit. You can only access the most recent {limit} worth of files. <a>{callToAction}</a>';
description = defineMessage({
id: 'workspace_limits.menu_limit.over.files_storage',
defaultMessage: 'Youre over the {limit} file storage limit. You can only access the most recent {limit} worth of files. <a>{callToAction}</a>',
});
}
return {
@@ -152,10 +170,7 @@ export default function useWords(highestLimit: LimitSummary | false, isAdminUser
defaultMessage: 'File storage limit',
}),
description: intl.formatMessage<ReactNode>(
{
id,
defaultMessage,
},
description,
values,
),
status: asGBString(highestLimit.usage, intl.formatNumber),

Просмотреть файл

@@ -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",

Просмотреть файл

@@ -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',

Просмотреть файл

@@ -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 (<span key={id}>
<FormattedMessage
id={id}
defaultMessage={message}
/>
<br/>
</span>);
return (
<span key={message.id}>
<FormattedMessage
{...message}
/>
<br/>
</span>
);
}
export function validateChannelUrl(url: string, intl?: IntlShape): Array<React.ReactElement | string> {
@@ -134,34 +134,82 @@ export function validateChannelUrl(url: string, intl?: IntlShape): Array<React.R
if (cleanedURL !== url || !urlMatched || urlMatched[0] !== url || isDirectMessageFormat || urlLonger || urlShorter) {
if (urlLonger) {
errors.push(formattedError(t('change_url.longer'), 'URLs must have at least 2 characters.', intl));
errors.push(formattedError(
defineMessage({
id: 'change_url.longer',
defaultMessage: 'URLs must have at least 2 characters.',
}),
intl,
));
}
if (urlShorter) {
errors.push(formattedError(t('change_url.shorter'), 'URLs must have maximum 64 characters.', intl));
errors.push(formattedError(
defineMessage({
id: 'change_url.shorter',
defaultMessage: 'URLs must have maximum 64 characters.',
}),
intl,
));
}
if (url.match(/[^A-Za-z0-9-_]/)) {
errors.push(formattedError(t('change_url.noSpecialChars'), 'URLs cannot use special characters.', intl));
errors.push(formattedError(
defineMessage({
id: 'change_url.noSpecialChars',
defaultMessage: 'URLs cannot use special characters.',
}),
intl,
));
}
if (isDirectMessageFormat) {
errors.push(formattedError(t('change_url.invalidDirectMessage'), 'User IDs are not allowed in channel URLs.', intl));
errors.push(formattedError(
defineMessage({
id: 'change_url.invalidDirectMessage',
defaultMessage: 'User IDs are not allowed in channel URLs.',
}),
intl,
));
}
const startsWithoutLetter = url.charAt(0) === '-' || url.charAt(0) === '_';
const endsWithoutLetter = url.length > 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,
));
}
}

Просмотреть файл

@@ -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<ClientConfig>) {
}
export function isValidPassword(password: string, passwordConfig: ReturnType<typeof getPasswordConfig>, 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;