<FormattedMessage> is a component; it renders the formatted text into a React.Fragment or the element named in tagName, so it cannot be assigned to an attribute. placeholder, title, alt and aria-label take plain strings, and the react-intl docs list exactly this case, "setting text attributes such as title, aria-label", as the reason the imperative API exists.
Get the intl object with the useIntl hook and call formatMessage:
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
searchPlaceholder: {
id: 'search.placeholder',
defaultMessage: 'Search',
description: 'Placeholder text of the search box',
},
});
function SearchBox() {
const intl = useIntl();
const label = intl.formatMessage(messages.searchPlaceholder);
return <input type="search" placeholder={label} aria-label={label} />;
}
An inline descriptor works the same: intl.formatMessage({ id: 'search.placeholder', defaultMessage: 'Search' }). defineMessages returns the descriptors unchanged; its job is to keep them in a shape the formatjs CLI and Babel plugin can extract, so prefer it over building ids at runtime.
formatMessage returns a string as long as every value is a primitive, as in { count } or { name: 'Ana' }. Pass a React element or a rich-text function and you get an array of nodes instead, which is again unusable in an attribute. Variables in attribute text otherwise work like anywhere else; see How to use dynamic strings in a localization message in React?.
useIntl only works in function components. The injectIntl higher-order component that used to supply this.props.intl to class components was removed in react-intl 10 (upgrade guide). A class you cannot convert can read the exported IntlContext through static contextType and call this.context.formatMessage(...), or be wrapped by a small function component that calls useIntl and passes intl down as a prop.
Outside React, for example in a validation helper, build the same object with createIntl, as shown in How to use localization messages in non-component code in React?.
With react-i18next there is nothing to work around: t returns a string, so placeholder={t('search.placeholder')} is all it takes.
Published Oct 9, 2026 · Markdown version