Creating Wrapper Components with Auto ID Generation#
A common pattern in large applications is to create wrapper components that encapsulate react-intl functionality with custom styling, validation, or business logic. This guide shows you how to create these components while still supporting automatic message extraction and ID generation.
The Challenge#
When you create wrapper components that accept text as props, the babel plugin and other extraction tools won't automatically detect them as translation candidates. For example:
// ❌ This won't be extracted by default
<CustomButton text="Click me" />
// ✅ This will be extracted
<FormattedMessage defaultMessage="Click me" />
The solution is to configure your extraction tool to recognize your custom component names.
Critical Requirement#
Danger
Your wrapper components MUST use the exact same prop names as FormattedMessage:
id- The message ID (optional, auto-generated if not provided)defaultMessage- The default message textdescription- Context for translatorsvalues- Variables to interpolate into the message
The extraction tools look for these specific prop names. Using different names (like message, text, or content) will not work.
Solution: Using additionalComponentNames#
All FormatJS extraction tools support additionalComponentNames and additionalFunctionNames options to recognize custom components and functions.
Step 1: Create Your Wrapper Component#
Create a wrapper component that internally uses react-intl and accepts the same props as FormattedMessage:
// components/CustomButton.tsx
import {useIntl} from 'react-intl'
interface CustomButtonProps {
id?: string
defaultMessage?: string
description?: string | object
values?: Record<string, any>
onClick?: () => void
variant?: 'primary' | 'secondary'
}
export function CustomButton({
id,
defaultMessage,
description,
values,
onClick,
variant = 'primary',
}: CustomButtonProps) {
const intl = useIntl()
const text = intl.formatMessage({id, defaultMessage, description}, values)
return (
<button className={`btn btn-${variant}`} onClick={onClick}>
{text}
</button>
)
}
Step 2: Use Your Component with Message Descriptor Props#
When using your component, pass the message descriptor props directly:
// ✅ Correct: Pass message descriptor props directly
<CustomButton
defaultMessage="Click me"
description="Button to submit the form"
onClick={handleSubmit}
/>
// ✅ With explicit ID
<CustomButton
id="submit.button"
defaultMessage="Submit"
description="Submit button text"
onClick={handleSubmit}
/>
// ✅ With variables
<CustomButton
defaultMessage="Hello, {name}!"
description="Greeting with user name"
values={{name: 'World'}}
onClick={handleSubmit}
/>
Step 3: Configure Your Extraction Tool#
Configure your extraction tool to recognize CustomButton as a component that contains translatable messages.
See the tool-specific configuration below:
Pattern: Component with Inline Message Descriptor#
For simpler cases where you want to pass the message directly as a prop, you can create a component that accepts message descriptor props:
// components/TranslatableButton.tsx
import {FormattedMessage} from 'react-intl'
interface TranslatableButtonProps {
id?: string
defaultMessage: string
description?: string
values?: Record<string, any>
onClick?: () => void
}
export function TranslatableButton({
id,
defaultMessage,
description,
values,
onClick,
}: TranslatableButtonProps) {
return (
<button className="btn" onClick={onClick}>
<FormattedMessage
id={id}
defaultMessage={defaultMessage}
description={description}
values={values}
/>
</button>
)
}
Usage:
// ✅ This will be extracted when you configure additionalComponentNames
<TranslatableButton
defaultMessage="Click me"
description="Submit button"
onClick={handleSubmit}
/>
Configuration:
{
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"additionalComponentNames": ["TranslatableButton"]
}
]
]
}
Pattern: Function Wrapper for formatMessage#
You can also create function wrappers for formatMessage. Note that the wrapper must accept the same signature as intl.formatMessage:
// utils/i18n.ts
import {useIntl, type MessageDescriptor} from 'react-intl'
export function useTranslation() {
const intl = useIntl()
// Create a shorter alias - must match formatMessage signature
const t = (descriptor: MessageDescriptor, values?: Record<string, any>) =>
intl.formatMessage(descriptor, values)
return {t}
}
Usage:
function MyComponent() {
const {t} = useTranslation()
return (
<div>
{t(
{
defaultMessage: 'Hello, {name}!',
description: 'Greeting message',
},
{name: 'World'}
)}
</div>
)
}
To extract messages from the t function:
{
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"additionalFunctionNames": ["t"]
}
]
]
}
Babel Plugin Configuration#
For babel-plugin-formatjs, add the additionalComponentNames option to your babel configuration:
babel.config.json:
{
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"additionalComponentNames": ["CustomButton", "TranslatableButton"],
"additionalFunctionNames": ["t", "$formatMessage"]
}
]
]
}
TypeScript Transformer Configuration#
For @formatjs/ts-transformer, configure it in your build tool:
webpack with ts-loader:
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: {
getCustomTransformers() {
return {
before: [
transform({
overrideIdFn: '[sha512:contenthash:base64:6]',
additionalComponentNames: [
'CustomButton',
'TranslatableButton',
],
additionalFunctionNames: ['t', '$formatMessage'],
}),
],
}
},
},
},
],
},
],
},
}
ts-patch in tsconfig.json:
{
"compilerOptions": {
"plugins": [
{
"transform": "@formatjs/ts-transformer",
"import": "transform",
"type": "config",
"overrideIdFn": "[sha512:contenthash:base64:6]",
"additionalComponentNames": ["CustomButton", "TranslatableButton"],
"additionalFunctionNames": ["t", "$formatMessage"]
}
]
}
}
CLI Configuration#
For @formatjs/cli, use command-line flags:
formatjs extract "src/**/*.{ts,tsx}" \
--out-file messages.json \
--id-interpolation-pattern '[sha512:contenthash:base64:6]' \
--additional-component-names CustomButton,TranslatableButton \
--additional-function-names t,$formatMessage
Or create a configuration file and reference it in your package.json scripts:
package.json:
{
"scripts": {
"extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file messages.json --additional-component-names CustomButton,TranslatableButton --additional-function-names t,$formatMessage"
}
}
Important Considerations#
Type Safety#
The additionalComponentNames and additionalFunctionNames options are less safe than the default extraction because they don't verify imports. This means:
// ⚠️ This will be extracted even if CustomButton is not from react-intl
import {CustomButton} from 'some-other-library'
;<CustomButton defaultMessage="Hello" />
To maintain type safety:
- Use TypeScript to enforce correct prop types
- Establish naming conventions (e.g., all translatable components start with
Translatable) - Document your wrapper components clearly
Message Descriptor Structure#
Required Prop Names
Your wrapper components must accept props that match the
MessageDescriptor interface. The extraction tools look for these exact prop
names - they will not work with custom prop names.
interface MessageDescriptor {
id?: string
defaultMessage?: string
description?: string | object
}
Example of what works vs. what doesn't:
// ✅ CORRECT - Uses standard prop names
<CustomButton defaultMessage="Click me" description="Button text" />
// ❌ WRONG - Custom prop names won't be extracted
<CustomButton message="Click me" tooltip="Button text" />
<CustomButton text="Click me" />
<CustomButton label={{defaultMessage: "Click me"}} />
Auto ID Generation#
When using idInterpolationPattern or overrideIdFn, IDs will be automatically generated based on the defaultMessage content. This works seamlessly with custom components that use the same prop names as FormattedMessage:
// Without explicit ID - auto-generates ID from content hash
<CustomButton defaultMessage="Click me" description="Submit button" />
// Generates ID like: "2Hd9f0" (hash of "Click me")
// With explicit ID - ID is preserved
<CustomButton id="submit.button" defaultMessage="Click me" description="Submit button" />
// Uses ID: "submit.button"
Complete Example#
Here's a complete example showing a design system with wrapper components:
// components/design-system/Button.tsx
import {useIntl} from 'react-intl'
interface ButtonProps {
id?: string
defaultMessage?: string
description?: string | object
values?: Record<string, any>
variant?: 'primary' | 'secondary' | 'danger'
size?: 'small' | 'medium' | 'large'
onClick?: () => void
disabled?: boolean
}
export function Button({
id,
defaultMessage,
description,
values,
variant = 'primary',
size = 'medium',
onClick,
disabled,
}: ButtonProps) {
const intl = useIntl()
const text = intl.formatMessage({id, defaultMessage, description}, values)
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
aria-label={text}
>
{text}
</button>
)
}
// components/design-system/Alert.tsx
import {FormattedMessage} from 'react-intl'
interface AlertProps {
titleId?: string
titleDefaultMessage?: string
titleDescription?: string | object
messageId?: string
messageDefaultMessage?: string
messageDescription?: string | object
type?: 'info' | 'warning' | 'error' | 'success'
}
export function Alert({
titleId,
titleDefaultMessage,
titleDescription,
messageId,
messageDefaultMessage,
messageDescription,
type = 'info',
}: AlertProps) {
return (
<div className={`alert alert-${type}`}>
<h4>
<FormattedMessage
id={titleId}
defaultMessage={titleDefaultMessage}
description={titleDescription}
/>
</h4>
<p>
<FormattedMessage
id={messageId}
defaultMessage={messageDefaultMessage}
description={messageDescription}
/>
</p>
</div>
)
}
// App.tsx
import {Button, Alert} from './components/design-system'
export function MyForm() {
const [showSuccess, setShowSuccess] = useState(false)
return (
<div>
{showSuccess && (
<Alert
titleDefaultMessage="Success!"
titleDescription="Success alert title"
messageDefaultMessage="Your changes have been saved."
messageDescription="Success alert message"
type="success"
/>
)}
<Button
defaultMessage="Save changes"
description="Button to save form data"
variant="primary"
onClick={() => setShowSuccess(true)}
/>
<Button
defaultMessage="Cancel"
description="Button to cancel the operation"
variant="secondary"
onClick={() => {}}
/>
</div>
)
}
babel.config.json:
{
"plugins": [
[
"formatjs",
{
"idInterpolationPattern": "[sha512:contenthash:base64:6]",
"additionalComponentNames": ["Button", "Alert"],
"ast": true
}
]
]
}
With this configuration:
- All
ButtonandAlertcomponents will have their messages extracted - IDs will be auto-generated based on
defaultMessage - Messages will be pre-compiled to AST for better runtime performance
- Type safety is maintained through TypeScript
Troubleshooting#
Messages not being extracted#
- Verify your component is listed in
additionalComponentNames - Check that prop names match
MessageDescriptorinterface (defaultMessage,id,description) - Ensure the component is used with JSX syntax (not dynamically created)
- Run extraction with
--throwsflag to see detailed errors
IDs not being generated#
- Verify
idInterpolationPatternoroverrideIdFnis configured - Check that you're not providing an empty
idprop (useundefinedor omit it) - Ensure
defaultMessageis provided (required for ID generation)
Type errors#
- Import
MessageDescriptortype fromreact-intl - Ensure your component props extend or match
MessageDescriptor - Use TypeScript strict mode to catch mismatches early