Bad practices

Common localization mistakes to avoid and how to fix them.

Bad localization practices can result in a confusing mix of languages, inconsistent user experiences and reduced user engagement with your app. This page outlines common mistakes (bad practices) when localizing your app, and how to avoid them to make sure a consistent, high-quality experience for all users.

Bad practices when localizing app UI strings

When App UI strings aren't properly localized, it often results in unexpected untranslated content like shown in the image below. This section outlines the most common bad practices.

Example of app with untranslated content, placeholder text is in English, while everything else is in Japanese

Not setting up i18n linting

Don't skip setting up the recommended i18n linting configuration or ignore linting errors.
Do set up the recommended i18n linting and fix all linting errors. See Step 2: Configure ESLint.

Unformatted strings

Don't render strings without localizing them first.
<Text>Welcome to My App</Text>
<Select
placeholder="Select an option"
options={[
{ label: "Option 1", value: "option1" },
{ label: "Option 2", value: "option2" },
]}
/>
<Button ariaLabel="Select an option" />
JSX
Do use intl.formatMessage or <FormattedMessage> for any user-facing text.
<Text><FormattedMessage defaultMessage="Welcome to My App" /></Text>
<Select
placeholder={intl.formatMessage({ defaultMessage: "Select an option" })}
options={[
{ label: intl.formatMessage({ defaultMessage: "Option 1" }), value: "option1" },
{ label: intl.formatMessage({ defaultMessage: "Option 2" }), value: "option2" },
]}
/>
<Button ariaLabel={intl.formatMessage({ defaultMessage: "Select an option" })} />
JSX

If you need to make an exception (e.g. brand names), see Excluding text.

Use dynamic id or defaultMessage values

Don't use dynamic id values in your messages. @formatjs/cli won't extract these messages for translation.
<FormattedMessage id={menuItemKey} />
<FormattedMessage id={`menu.${menuItemKey}`} />
// OR
intl.formatMessage({ id: menuItemKey })
intl.formatMessage({ id: `menu.${menuItemKey}` })
JSX
Don't use dynamic defaultMessage values in your messages. @formatjs/cli won't extract these messages for translation.
<FormattedMessage defaultMessage={menuItemLabel} />
<FormattedMessage defaultMessage={`Menu: ${menuItemLabel}`} />
// OR
intl.formatMessage({ defaultMessage: menuItemLabel })
intl.formatMessage({ defaultMessage: `Menu: ${menuItemLabel}` })
JSX
Do use predefined messages and select the message based on a dynamic value. @formatjs/cli will correctly extract these messages for translation.
const menuMessages = defineMessages({
edit: {
defaultMessage: "Edit"
},
view: {
defaultMessage: "View"
},
// Add more menu items ...
});
function getMenuMessage(menuItemKey) {
switch (menuItemKey) {
case "edit":
return menuMessages.edit;
case "view":
return menuMessages.view;
// Add more menu items ...
default:
throw new Error(`Unknown menu item: ${menuItemKey}`);
}
}
<FormattedMessage {...getMenuMessage(selectedMenuItem)} />
JSX

Render strings returned from the backend

Avoid rendering untranslated strings returned directly from your backend. Instead, map backend responses to predefined messages on your frontend, so you can use translations provided by Canva.
<Text>{response.errorMessage}</Text>
JSX
Do map backend responses to predefined messages on the frontend.
const messages = defineMessages({
inappropriateContent: {
defaultMessage: "Inappropriate content detected."
},
// Add more messages ...
unknownError: {
defaultMessage: "An unknown error occurred. Modify your request and try again."
}
});
function getErrorMessage(errorCode) {
switch (errorCode) {
case "INAPPROPRIATE_CONTENT":
return messages.inappropriateContent;
// Add more messages ...
default:
return messages.unknownError;
}
}
<FormattedMessage {...getErrorMessage(response.errorCode)} />
JSX

See Preferred: Frontend localization for a more complete example.

Define placeholder-only messages

Avoid defining messages that only contain variable placeholders. This is problematic, because once this message is extracted into your messages_en.json file, it will only contain the variable placeholder without any other text, and as such nothing will be translated.
<FormattedMessage
defaultMessage="{selectedOption}"
values={{ selectedOption }}
/>
JSX
Do define a static message for each possible value, so that each value is translated.
const optionMessages = defineMessages({
apple: {
defaultMessage: "Apple"
},
banana: {
defaultMessage: "Banana"
}
// ...
});
function getOptionMessage(optionKey) {
switch (optionKey) {
case "apple":
return optionMessages.apple;
case "banana":
return optionMessages.banana;
// ...
default:
throw new Error(`Unknown option: ${optionKey}`);
}
}
<FormattedMessage {...getOptionMessage(selectedOption)} />
JSX

For guidance on when and how to use interpolation, follow this guide.