Skip to content

Localization

ICU Syntax

Canva supports a subset of International Components for Unicode (ICU).

The Canva translation process supports a subset of ICU MessageFormat syntax. For more information about ICU, see the official FormatJS documentation.

NOTE: Canva's translation process doesn't support the entire ICU syntax, and uploaded files will be rejected if they contain any unsupported syntax. For more information, see Unsupported syntax.

ICU basics

This section describes the supported ICU syntax, with recommendations and examples.

Follow these examples and guidelines so that your app can be translated more accurately and efficiently.

Basic messages

How to do basic message localization with React, using the FormattedMessage component from react-intl.

  • The defaultMessage string is the source for translation text. It's displayed to users that either have an English locale, or a locale with no translations available.
  • Use the description string to give as much context as possible to a human translator. For guidance on writing translator notes, see Add notes for translators.

For example:

tsx
<FormattedMessage
  defaultMessage="My internationalized app"
  description="The title the user sees when opening the app. Appears at top of page."
/>

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Ensure that defaultMessage is clear and written in US English.</DoText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DoText&gt;Include a detailed `description` that gives context to translators.&lt;/DoText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

Interpolation

You can use dynamic values (like firstName) in the defaultMessage prop of FormattedMessage. This process is known as interpolation.

This section explains how to use interpolation for localized messages in React, using the FormattedMessage component from react-intl.

For example:

tsx
const name = "John"
// ...
<FormattedMessage
  defaultMessage="Welcome to the world of AI creativity, {firstName}!"
  description="A greeting that welcomes the user to the AI image generation app"
  values={{
    firstName: name,
  }}
/>

This renders as: "Welcome to the world of AI creativity, John!"

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>When inserting dynamic content (such as user-specific data), use placeholders like {firstName}.</DoText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DoText&gt;`"Hello {name}"`&lt;/DoText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Don't manually combine strings and variables, using techniques like the + operator. This can lead to incorrect and confusing sentences when translations are combined. This is because languages will reorder words in various ways. For more information, see this blog post.</DontText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DontText&gt;`"Hello " + name`&lt;/DontText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

Tip: There are some cases where select is preferable to interpolation. For more information, see Using select or interpolation.

Plurals

How to handle plurals in localized messages in React.

For example:

tsx
export const CreditUsage = ({
  creditsCost,
  remainingCredits,
}: {
  creditsCost: number;
  remainingCredits: number;
}) => (
  <Text>
    <FormattedMessage
      defaultMessage={`Use {creditsCost, number} of {remainingCredits, plural,
        one {# credit}
        other {# credits}
      }`}
      description="Informs the user about the number of credits they will use for the image generation task. Appears below the image generation button."
      values={{
        creditsCost,
        remainingCredits,
      }}
    />
  </Text>
);
tsx
<CreditUsage creditsCost={5} remainingCredits={50} />
<CreditUsage creditsCost={1} remainingCredits={1} />

This would render as:

  • "Use 5 of 50 credits"
  • "Use 1 of 1 credit"

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Consider edge cases: Ensure that pluralization handles various scenarios, such as zero, one, and other amounts.</DoText> </td> </tr> </tbody> </table>

Numbers

To localize numbers for the user's locale, ICU needs them to be formatted with a specific argument.

NOTE: If you are only presenting a number by itself, you can use FormattedNumber from the react-intl library to localize the number. If you are presenting a number in a sentence with other words, you should use the ICU syntax, as shown below.

This example shows how to format numbers in localized messages in React, using the FormattedMessage component from react-intl.

tsx
<FormattedMessage
  defaultMessage="Image generation is {progress, number, ::percent} complete."
  description="Displays the progress of the current image generation task that the user requested"
  values={{
    progress: 0.75,
  }}
/>

Assuming the locale uses this percentage format, this can render as: "Image generation is 75% complete."

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Use ICU number formatting for numbers, such as percentages.</DoText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DoText&gt;
      `Members ({(members, number, integer)})` - `123456 members` displays
      as `Members (123,456)` in English, `Membres (123 456)` in French, and
      `Thành viên (123.456)` in Vietnamese.
    &lt;/DoText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Avoid formatting numbers manually. Instead, you can use an ICU formatting library like react-intl.</DontText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DontText&gt;
      `Members ({members})` - The number doesn't respect the user's locale
      and always displays 123456 as the number, without separators.
    &lt;/DontText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

Currency

Automatically adjusts currency symbols and formats based on the user's locale. This function does not convert currency into other denominations, and does not consider exchange rates at any point.

  • ::currency/CODE: Specifies the currency code for number formatting (such as USD, EUR).

In this example, the number 1234.56 is formatted as currency in USD, displaying "$1,234.56" for locales using the dollar sign format. The ::currency/USD syntax configures the number formatting for the specified currency.

tsx
<FormattedMessage
  defaultMessage="The total amount is {amount, number, ::currency/USD}"
  description="Displays the total amount in USD currency"
  values={{
    amount: 1234.56,
  }}

Rendered result: The total amount is $1,234.56

Decimal

Automatically adjusts decimal number formats based on the user's locale.

  • {temperature, number, .0}: Formats the number as a decimal with one fractional digit.

This example shows how to display a temperature reading. The {temperature, number, .0} syntax formats the number 23.456 as "23.5", representing the temperature with one decimal place.

tsx
<FormattedMessage
  defaultMessage="The current temperature is {temperature, number, .0} degrees."
  description="Displays the current temperature with one decimal place"
  values={{
    temperature: 23.456,
  }}
/>

Rendered result: The current temperature is 23.5 degrees.

Percentage

Automatically adjusts percentage formats based on the user's locale.

  • {progress, number, ::percent}: Formats a number as a percentage.

This example shows how to display task completion progress. The {progress, number, ::percent} syntax formats the number 0.71 as "71%", representing the progress in whole percentage terms.

tsx
<FormattedMessage
  defaultMessage="Task completion is at {progress, number, ::percent}."
  description="Displays the task completion progress as a full percentage"
  values={{
    progress: 0.71,
  }}
/>

Rendered result: Task completion is at 71%.

Dates

To localize date values for the user's locale, ICU needs them to be formatted with a specific argument.

NOTE: If you are only presenting a date by itself, you can use FormattedDate from the react-intl library to localize the date. If you are presenting a date in a sentence with other words, you should use the ICU syntax, as shown below.

This example demonstrates how to format dates and times in localized messages in React, using the FormattedMessage component from react-intl.

tsx
<FormattedMessage
  defaultMessage="Credits refresh on: {refreshDate, date, short} at {refreshTime, time, short}"
  description="Informs users when their credits for image generation will refresh, including the time"
  values={{
    refreshDate: new Date("2023-11-24T15:00:00"),
    refreshTime: new Date("2023-11-24T15:00:00"),
  }}
/>

Depending on the user's locale, this can render as: Credits refresh on: 11/24/23 at 3:00 PM".

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText> Expires {(expiryDate, date, short)} - When formatted with a date value (such as new Date('2024-09-25') in JavaScript), this will correctly display as:

      * `Expires 9/25/2024` for US English (en-US).
      * `Verfällt am 25.09.2024` for German (de-DE).
    &lt;/DoText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> Expires {expiryDate} - The number doesn't respect the user’s locale and always displays the date as 2024-09-25. For example: (Verfällt am 2024-09-25) </DontText> </td> </tr> </tbody> </table>

Time

To localize time values for the user's locale, ICU needs them to be formatted with a specific argument.

NOTE: If you are only presenting a time by itself, you can use FormattedTime from the react-intl library to localize the time. If you are presenting a time in a sentence with other words, you should use the ICU syntax, as shown below.

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText> Current time: {(currentTime, time, short)} - This displays as Current time: 3:45 PM in English. </DoText> </td> </tr> </tbody> </table>

Relative time

This section provides guidelines for displaying relative time.

tsx
const LastGeneratedMessage = ({
  lastGeneratedTime,
}: {
  lastGeneratedTime: Date;
}) => {
  const intl = useIntl();

  const [generatedTimeAgoInSeconds, setGeneratedTimeAgoInSeconds] = React.useState(0);

  // ...

  return (
    <Text>
      <FormattedMessage
        defaultMessage="Last image generated {timeAgo}"
        description="Tells the user how long ago they generated their last image. timeAgo is a relative time string. e.g. '5 seconds ago'"
        values={{
          timeAgo: intl.formatRelativeTime(
            -generatedTimeAgoInSeconds,
            "seconds",
          ),
        }}
      />
    </Text>
  );
};

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Use functions or libraries that handle relative time calculations and formatting, such as intl.formatRelativeTime of react-intl, or Intl.RelativeTimeFormat</DoText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> Avoid hardcoding custom logic for relative time calculations, as this might not adapt well across different locales.</DontText> </td> </tr> </tbody> </table>

Select

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText> Hello, {role, select, admin {Administrator} user {User} guest {Guest} other {Visitor&#125;&#125;!

      * If `role` is `'admin'`, it renders: `Hello, Administrator!`
      * If `role` is `'user'`, it renders: `Hello, User!`
      * If `role` is `'guest'`, it renders: `Hello, Guest!`
      * If `role` is anything else or missing, it renders: `Hello, Visitor!`
    &lt;/DoText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

Using select or interpolation

If your message needs to adapt to a specific, categorical value (such as status or day) and an obvious fallback value, then select might be easier to implement than interpolation. Select lets you define the various message strings directly within the message format, making your code more concise and easier to maintain. While select might appear simpler and preferable to implement at first, it can become more complex once you start nesting ICU elements.

Interpolation is more familiar to JavaScript and TypeScript developers and is a flexible and powerful option, especially when you need to handle multiple dynamic values.

The following table summarizes the use cases for select and interpolation.

FeatureSelectInterpolation
PurposeConditional selection based on predefined categories.Dynamic value insertion.
Example Input"male", "female", "other"."Jane", "123", "New York".
Use caseGender, user roles, statuses.Names, numbers, locations, dynamic content.
Fallback required?Yes (other case).No, just inserts the value.

These examples compare how select and interpolation would handle a dynamic greeting:

Select:

When using select, you can create a message that changes based the document type:

json
Here are your stats for the year. {doctype, select, presentation{Your slides are the talk of the town!} social {Your followers love your work!} other {Way to go!}}

Interpolation:

When using string interpolation instead of select, you need to define logic outside the message to choose the appropriate string for the doctype. Here's and example of how the interpolation code might look:

javascript
import { defineMessages, useIntl } from 'react-intl';

const messages = defineMessages({
  presentation: { 
    defaultMessage: "Your slides are the talk of the town!", 
    description: "Message for presentation documents" 
  },
  social: { 
    defaultMessage: "Your followers love your work!", 
    description: "Message for social media posts" 
  },
  other: { 
    defaultMessage: "Way to go!", 
    description: "Fallback message" 
  },
});

const interpolateSelect = (key, intl) => {
  return intl.formatMessage(messages[key] || messages.other);
};

const DocumentMessage = ({ doctype }) => {
  const intl = useIntl();
  const message = interpolateSelect(doctype, intl);

  return <p>{message}</p>;
};

// Usage example
<DocumentMessage doctype="presentation" />

Rich text

How to format rich text in localized messages in React, using the FormattedMessage component from react-intl.

For example:

tsx
<FormattedMessage
  defaultMessage="Discover stunning AI-generated example images in our <link>gallery</link> and <callToAction>start exploring now!</callToAction>"
  description="A call to action directing the user to explore the AI image gallery"
  values={{
    link: (chunks) => (
      <Link
        href={DOCS_URL}
        requestOpenExternalUrl={() => openExternalUrl(DOCS_URL)}
      >
        {chunks}
      </Link>
    ),
    callToAction: (chunks) => <strong>{chunks}</strong>,
  }}
>
  {(chunks) => <Text>{chunks}</Text>}
</FormattedMessage>

This can render as: "Discover stunning AI-generated example images in our gallery and start exploring now!"

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Use functions for rich text: To ensure dynamic content is properly rendered, you can wrap parts of your message in React components by passing functions in the values prop.</DoText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Hardcode HTML Elements: Avoid embedding raw HTML within your defaultMessage.</DontText> </td> </tr> </tbody> </table>

Message as string type

How to use localized strings as button labels in React, using formatMessage from react-intl.

For example:

tsx
<Button variant="primary">
  {intl.formatMessage({
    defaultMessage: "Generate image",
    description: "A button label to generate an image from a prompt",
  })}
</Button>

This renders a button with the label "Generate image", and the text is localized based on the user's locale.

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>For consistency with the rest of the app's localization, use formatMessage to localize user-facing text that is required to be a string type (like button labels).</DoText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Avoid hardcoding button text, since it will be left untranslated.</DontText> </td> </tr> </tbody> </table>

Non-visible text for accessibility

How to use non-visible text for accessibility purposes (such as screen reader descriptions) in React, using ariaLabel and formatMessage from react-intl.

For example:

tsx
<Button
  variant="primary"
  icon={SortIcon}
  ariaLabel={intl.formatMessage({
    defaultMessage: "Sort images by creation date (Newest to Oldest)",
    description:
      "Screenreader text for a button. When pressed, the button will sort the generated images by creation date from newest to oldest.",
  })}
/>

The screen reader will read the aria label as: "Sort images by creation date (Newest to Oldest)".

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Use formatMessage for accessibility by providing localized ariaLabel text.</DoText> </td> </tr> </tbody> </table>

LTR and RTL languages

To support left-to-right (LTR) and right-to-left (RTL) languages, you can use CSS properties in UI components.

This example uses paddingStart instead of paddingLeft or paddingRight, which lets your UI automatically adapt to the language's text direction.

tsx
<Box paddingStart="2u">
  <Slider min={0} max={100} />
</Box>

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>To help components adapt to LTR and RTL languages, use logical properties like paddingStart instead of directional properties like paddingLeft.</DoText> </td> </tr> </tbody> </table>

Lists

How to format lists in a way that is compatible with localization.

In this example, the SelectedEffects component defines a list of image effects, each formatted for localization.

tsx
const SelectedEffects = () => {
  const intl = useIntl();

  // TODO: Make this list change based on user selection!
  const selectedEffects = [
    intl.formatMessage({
      defaultMessage: "black and white",
      description:
        "An option that when selected, will apply a black and white effect to the generated image",
    }),
    intl.formatMessage({
      defaultMessage: "high contrast",
      description:
        "An option that when selected, will apply a high contrast effect to the generated image",
    }),
    intl.formatMessage({
      defaultMessage: "cartoon",
      description:
        "An option that when selected, will apply a cartoon effect to the generated image",
    }),
  ];

  return (
    <Text>
      <FormattedMessage
        defaultMessage="You have selected the following image effects: {effects}"
        description="Informs the user about the image effects they have selected. effects is a list of effects that will be applied to the generated image."
        values={{
          effects: intl.formatList(selectedEffects, {
            type: "conjunction",
          }),
        }}
      />
    </Text>
  );
};

This component renders based on the user's locale. For example, an English list uses an Oxford comma, but German does not:

  • English (en-US): "You have selected the following image effects: black and white, high contrast, and cartoon."
  • German (de-DE): "Sie haben die folgenden Bildeffekte ausgewählt: Schwarzweiß, hoher Kontrast und Cartoon."

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Ensure that lists are formatted using methods that adapt to locale-specific conventions, such as comma placement and conjunction usage. Use library functions like intl.formatList of react-intl to automatically handle these variations, or use built-in JS APIs like Intl.ListFormat</DoText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Avoid concatenating list items by using separators like commas or "and," as this might not be appropriate for all languages and locales.</DontText> </td> </tr>

&lt;tr valign="top"&gt;
  &lt;td&gt;
    &lt;DontText&gt;Don't overlook variations in list formatting conventions between different languages.&lt;/DontText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

Display name formatting

How to use display name formatting to inform users about the app's active language, using the FormattedMessage component with React.

For example:

tsx
<FormattedMessage
  defaultMessage="You are currently viewing this app in {language}"
  description="Shows the user which language the app is currently using"
  values={{
    language: intl.formatDisplayName(intl.locale, {
      type: "language",
    }),
  }}
/>

<table> <thead> <tr> <th>Do</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText>Use intl.formatDisplayName or Intl.DisplayNames to retrieve and display the name of the active language.</DoText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText>Avoid hardcoding language names directly in the UI, as this limits flexibility and might not adapt to all locale-specific language names.</DontText> </td> </tr> </tbody> </table>

Unsupported syntax

<table> <thead> <tr> <th>Don't use strings with multiple plural arguments. For example:</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> {like_count, plural, one {1 like} other &#123;&#123;like_count} likes&#125;&#125; and {subscriber_count, plural, one {1 sub} other &#123;&#123;subscriber_count} subs&#125;&#125; </DontText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't use strings with a plural argument and offset property. For example:</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> {like_count, plural, offset:1 =0 {no likes} other &#123;&#123;like_count} likes&#125;&#125; </DontText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't use strings with a selectordinal argument. For example:</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> {n, selectordinal, one {This is the #st item} two {This is the #nd item} few {This is the #rd item} other {This is the #th item&#125;&#125; </DontText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't use strings containing the choice argument. For example:</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> {user_category, choice, ... </DontText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th>Don't use strings that result in over 20 combinations. For example:</th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> ts {firstChoice, select, optionA {First A} optionB {First B} other {First C} } {secondChoice, select, optionX {Second X} optionY {Second Y} other {Second Z} } {thirdChoice, select, option1 {Third 1} option2 {Third 2} other {Third 3} }

      * This example contains a select with 3 choices, followed by another select with 3 choices, followed by another select with 3 choices. This would result in 3 x 3 x 3 = 27 possible string combinations.
    &lt;/DontText&gt;
  &lt;/td&gt;
&lt;/tr&gt;

</tbody> </table>

<table> <thead> <tr> <th> Don't use nested selects/plurals/select-plural combinations, except when the nested argument is a simple placeholder. For example, this nesting is not supported: </th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DontText> ts {gender, select, male { {count, plural, one {He has one item} other {He has # items} } } other { {count, plural, one {They have one item} other {They have # items} } } } </DontText> </td> </tr> </tbody> </table>

<table> <thead> <tr> <th> This nesting is supported: </th> </tr> </thead>

<tbody> <tr valign="top"> <td> <DoText> ts {task_count, plural, one { You have {# task_count} task: {task_name}. } other { You have {# task_count} tasks. Next task: {task_name}. } } </DoText> </td> </tr> </tbody> </table>

More information

  • Overview of the localization process: Localization overview
  • How to use the recommended workflow: Recommended workflow
  • How to localize an existing app: Migrate an existing app
  • Review the localization design guidelines: Localization
  • See bad practices: Bad practices

Canva can translate your app into other languages.

For app internationalization, Canva has defined a supported workflow that uses React and FormatJS. To help you get started, we’ve created a starter kit example that demonstrates the recommended tooling. For more information, see the Canva Apps SDK starter kit repository.

Prerequisites

This workflow assumes your app already has react-intl, @canva/app-i18n-kit, and @formatjs/cli installed and configured. If these dependencies aren't present in your package.json, see Migrate an existing app.

Step 1: Using FormattedMessage

Canva’s recommended i18n workflow uses the FormattedMessage component from react-intl. Ensure that your app's relevant text strings are in FormattedMessage or useIntl().formatMessage(), which allows @formatjs/cli to identify the text that needs to be extracted and translated.

  1. Import the required components from the react-intl library. For example:

    ts
    import { FormattedMessage, useIntl } from "react-intl";
  2. Use the FormattedMessage component for any text that is displayed to users. Include the following properties:

    • defaultMessage: The message to be used as the source of translations. Should be written in English (US). This is displayed to users with an English locale, or a locale for which no translations can be found.
    • description: Add a description to convey as much context as possible to a human translator. For more information, see Add notes for translators.
    • values: (Optional) Any dynamic values that need to be included in the string.

    For example:

    ts
    import { FormattedMessage, useIntl } from "react-intl";
    // ...
            <Text>
              <FormattedMessage
                description="Message that welcomes the user to the app"
                defaultMessage="Welcome to {appName}."
                values={{ appName: 'My Cool App' }}
              />
            </Text>

    Avoid hardcoding English language for aria-labels and other assistive text. Instead, you can useintl.formatMessage. For example:

    ts
    const intl = useIntl();
    // ...
    <Button
      variant="primary"
      icon={RotateIcon}
      ariaLabel={intl.formatMessage({
        defaultMessage: "Rotate the image",
        description:
          "Label text for a button. Explains that the image will rotate when pressed",
      })}
    />

Step 2: Testing localization

The @canva/app-i18n-kit package lets you test your app using pseudolocalization, and lets you change the locale using the Dev Toolkit. This gives you a preview of what your app could look like in a different locale.

The text lets you read your original English text, but inserts non-ASCII characters and adds width to each string to simulate lengthier languages.

  1. Enable pseudolocalization in the Dev Toolkit. This shows you a preview of how different characters and text lengths affect the UI.

    • Check that any custom components respect the left-to-right and right-to-left setting.
    • Make sure there are no rendering issues or truncated text.

NOTE: You can’t preview or test actual translations, since these are only available after your app has been approved. However, you can then preview the translations before releasing your app.

Step 3: Generate the JSON file

This process extracts all the FormattedMessage and useIntl().formatMessage() strings in your code and saves them to a JSON file. You can then upload this file to Canva for translation.

The JSON file is subject to these limitations:

  • The description and defaultMessage keys must be present.
  • Don't add additional keys.
  • The description and defaultMessage values must not be empty or missing.
  • The description and defaultMessage values must be text strings and must not exceed 500 characters.
  • The number of messages must not exceed 500.
  • Messages must not use unsupported ICU syntax. For more information, see ICU syntax.

The recommended tooling automatically generates files in the required format. To generate the messages_en.json file, run the following command:

shell
npm run build

This example shows what a typical message can look like:

json
{
  "1eigR4": {
    "defaultMessage": "Rotate the image",
    "description": "Label text for a button. Explains that the image will rotate when pressed."
  }
}

Step 4: Upload the JSON file

As part of the app submission process, you can upload the JSON file for translation.

  1. Locate your app in the Developer Portal.
  2. Upload the messages_en.json file, using the Translations file input.

Next steps

Canva reviews your app and identifies which locales should be supported. Canva then performs the translation of the supplied strings. You’ll be automatically notified once Canva has finished the translation process. You can then preview your app in the supported locales.

More information

  • Overview of the localization process: Localization overview
  • How to localize an existing app: Migrate an existing app
  • Review the localization design guidelines: Localization
  • See the supported syntax and exceptions: ICU syntax
  • See bad practices: Bad practices

Backend Responses

Handle translations when your app's content depends on API responses.

This page explains how to use translations if your app's content depends on responses from an API.

Preferred: Frontend localization

To keep your app compatible with various languages, the backend should return status codes or identifiers instead of translated strings. The frontend can then use these to display the correct translated messages, using the FormattedMessage component.

In this example, the backend response contains an error code which the frontend maps to a FormattedMessage to render a localized message:

typescript
// component.tsx

import { ComponentMessages as Messages } from "./component.messages";

type GenerateImageResponse =
  | { status: "SUCCESS"; data: Image }
  | { status: "ERROR"; errorCode: ErrorCode };

type ErrorCode = "INAPPROPRIATE_CONTENT" | "RATE_LIMIT_EXCEEDED"; // Add more error codes as needed

async function generateImageResponse(): Promise<GenerateImageResponse> {
  // ... call to your backend
}

export const Component = () => {
  // ... call generateImageResponse, data fetching logic, state, etc.

  if (response.status === "ERROR") {
    return (
      <Text>
        <FormattedMessage {...getErrorMessage(response.errorCode)} />
      </Text>
    );
  }
  // ... handle other cases
};

const getErrorMessage = (errorCode: ErrorCode) => {
  switch (errorCode) {
    case "INAPPROPRIATE_CONTENT":
      return Messages.inappropriateContent;
    case "RATE_LIMIT_EXCEEDED":
      return Messages.rateLimitExceeded;
    default:
      return Messages.unknownError;
  }
};
typescript
// component.messages.tsx

import { defineMessages } from "react-intl";

export const ComponentMessages = defineMessages({
  inappropriateContent: {
    defaultMessage:
      "The content you submitted has been flagged as inappropriate. Please review and modify your request.",
    description:
      "Error message shown in red text below the input field when the user inputs inappropriate content that we don't want to generate images for.",
  },
  rateLimitExceeded: {
    defaultMessage:
      "You've made too many requests in the last hour. Please try again later.",
    description:
      "Error message shown in red text below the input field when the user has made too many image generation requests.",
  },
  unknownError: {
    defaultMessage: "An unknown error occurred. Please try again later.",
    description:
      "Error message shown in red text below the input field when the request fails for an unknown reason.",
  },
});

Backend response depends on user locale

This section explains what to do when the backend uses dynamic content that can't be mapped to static frontend messages. For example, when a backend returns locale-specific data.

  • The backend should deliver content tailored to the user's locale, along with any extra context or identifiers to guide the frontend on how to present it.
  • To help ensure compatibility with localization tools, keep strings and static content on the frontend, where possible.

To inform the backend of the user's locale, we send a query parameter containing intl.locale:

jsx
// frontend/app.tsx
import type { Video } from "./api";
import { findVideos } from "./api";
export const App = () => {
  // ... state
  const intl = useIntl();
  const onSearch = useCallback(
    async (query: string) => {
      const result = await findVideos(query, intl.locale);
      if (result) {
        setVideos(result);
      }
      // ...
    },
    [intl.locale],
  );
  return <div>{/* ... components, use onSearch */}</div>;
};

// frontend/api.tsx
export const findVideos = async (
  query: string,
  locale: string,
): Promise<Video[]> => {
  // Best practice: Send locale information to your server as a query param.
  // Your server can then be modified to return localized content.
  const params = new URLSearchParams({ query, locale });
  const url = `${BACKEND_HOST}/videos/find?${params.toString()}`;
  const res = await fetch(url, { /* ... headers, e.g. Authorization */ });
  if (res.ok) {
    return res.json();
  }
  // ... error handling
};

In the backend, we use the following query parameter to return localized videos:

Note: Your backend must handle all locale values listed in Supported locales.

ts
// backend/routers/videos.ts
const router = express.Router();

router.get("/videos/find", async (req, res) => {
  const { locale } = req.query; // Extract locale value, e.g. "en", "ja-JP", "es-ES" etc.

  // ... fetch videos for locale

  res.send({
    /* localized video response */
  });
});

More information

  • Overview of the localization process: Localization overview
  • Localization outside React components: Localize outside React components
  • How to use the recommended workflow: Recommended workflow

Outside React Components

Use Canva-provided translations outside React components with the initIntl function.

For strings used outside React components, use the initIntl function from @canva/app-i18n-kit. This is useful for scenarios like localizing intent configurations or other non-React code.

Using initIntl

typescript
import { initIntl } from "@canva/app-i18n-kit";

const intl = initIntl();

// Example: Localizing Content Publisher intent configuration
async function getPublishConfiguration() {
  return {
    status: "completed",
    outputTypes: [
      {
        id: "post",
        displayName: intl.formatMessage({
          defaultMessage: "Feed Post",
          description:
            "Label shown in the output type dropdown for publishing to social media feeds",
        }),
        mediaSlots: [
          {
            id: "media",
            displayName: intl.formatMessage({
              defaultMessage: "Media",
              description:
                "Label for the media slot where users upload images or videos",
            }),
            // ... other configuration
          },
        ],
      },
    ],
  };
}

NOTE: Call initIntl() once at module level. The returned intl object can then be used throughout your non-React code.

More information

  • Overview of the localization process: Localization overview
  • Localize backend responses: Localize backend responses
  • How to use the recommended workflow: Recommended workflow

Migrate an Existing App

How to add localization support to an existing app.

This procedure describes how to update an existing app to support Canva’s translation process.

The Canva translation process currently requires use of the react-intl library.

Phase 1: Configure your workspace

This phase explains how to configure your environment to use the recommended i18n tools.

Step 1: Install prerequisites

  1. Install the react-intl, @formatjs/cli, and @canva/app-i18n-kit packages, as well as the latest version of @canva/app-ui-kit:

    shell
    npm install @canva/app-i18n-kit react-intl @canva/app-ui-kit@latest && npm install --save-dev @formatjs/cli

    NOTE: To be able to localize components, your app must use version 4 or later of the App UI Kit. Older versions of the App UI Kit don't support localization.

Step 2: Configure ESLint

This step explains how to configure ESLint for Canva app development. For more information on the rules and configuration options, see the @canva/app-eslint-plugin documentation.

  1. Install the @canva/app-eslint-plugin package, if not already installed.

    shell
    npm install --save-dev eslint @canva/app-eslint-plugin
  2. Add the recommended shared config to your eslint.config.mjs:

js
import canvaPlugin from "@canva/app-eslint-plugin";

export default [
  {
    ignores: [
      "**/node_modules/",
      "**/dist",
      "**/*.d.ts",
      "**/*.d.tsx",
      "**/*.config.*",
    ],
  },
  ...canvaPlugin.configs.apps,
];

Step 3: Configure webpack for FormatJS

  1. Configure the FormatJS TS transformer to automatically generate IDs. To do this, add the @formatjs/ts-transformer to your webpack configuration:

    shell
    npm install --save-dev @formatjs/ts-transformer
  2. To generate unique message IDs, add a transformer to your webpack.config.js. This example adds a function called getCustomTransformers:

    json
    const { transform } = require("@formatjs/ts-transformer");
    // ...
        module: {
          rules: [
            {
              test: /\.tsx?$/,
              exclude: /node_modules/,
              use: [
                {
                  loader: "ts-loader",
                  options: {
                    transpileOnly: true,
                     getCustomTransformers() {
                       return {
                         before: [
                           transform({
                             overrideIdFn: "[sha512:contenthash:base64:6]",
                           }),
                         ],
                       };
                     },
                  },
                },
              ],
  3. In package.json, add an extract script with the following contents:

    json
    formatjs extract \"src/**/*{ts,tsx}\" --out-file dist/messages_en.json

    Append the extract script to the build script. For example:

    diff
    "scripts": {
      "start": "ts-node «/scripts/start/start.ts",
    +  "extract": "formatjs extract \"src/**/*{ts,tsx}\" --out-file dist/messages_en.json",
    +  "build": "webpack --config webpack.config.js --mode production && npm run extract",
    -  "build": "webpack --config webpack.config.js --mode production",
      "lint: types": "tsc",
      "lint": "eslint .",
      "lint:fix": "eslint . --fix",

Step 4: Configure webpack chunk limit

Canva's app submission process requires a single app bundle. Implementing localization might increase the size of the app and cause webpack to split the bundle into multiple chunks.

To make sure webpack produces a single bundle, use the following steps:

  1. Open webpack.config.js, and add the optimize module to the webpack import:

    diff
    - const { DefinePlugin } = require("webpack");
    + const { DefinePlugin, optimize } = require("webpack");
  2. In webpack.config.js, add the chunk limit before buildDevConfig is called. Modify this plugins definition:

    diff
    plugins: [
      new DefinePlugin({
        BACKEND_HOST: JSON.stringify(backendHost),
      }),
    +  new optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
    ],

Phase 2: Updating your app

This phase explains how to update your app to support localization.

Step 1: Add the dependency

Add @canva/app-i18n-kit to your app. You don't need to pass messages or locale into AppI18nProvider, because it internally detects the user's locale and loads the appropriate translated messages. In addition, it also lets you test localization in a later step.

ts
// index.tsx
import { AppUiProvider } from "@canva/app-ui-kit";
import type { DesignEditorIntent } from "@canva/intents/design";
import { prepareDesignEditor } from "@canva/intents/design";
import { createRoot } from "react-dom/client";
import { App } from "./app";
import "@canva/app-ui-kit/styles.css";
import { AppI18nProvider } from "@canva/app-i18n-kit";

async function render() {
  const root = createRoot(document.getElementById("root") as Element);

  root.render(
    <AppI18nProvider>
      <AppUiProvider>
        <App />
      </AppUiProvider>
    </AppI18nProvider>,
  );
}

const designEditor: DesignEditorIntent = { render };
prepareDesignEditor(designEditor);

NOTE: Don't use IntlProvider from react-intl, since AppI18nProvider is a Canva-specific replacement for IntlProvider.

Step 2: Update UI strings

The react-intl package lets you use FormattedMessage or the useIntl() hook. We recommend using FormattedMessage wherever possible.

In your app’s code, update the UI strings to use FormattedMessage. For example:

ts
<Text>
<FormattedMessage
  description="Label text for a button. Explains that the image will rotate when pressed"
  defaultMessage="Rotate the image"
/>
</Text>

If using the useIntl() hook:

ts
const intl = useIntl();
// ...
<Button
  variant="primary"
  icon={RotateIcon}
  ariaLabel={intl.formatMessage({
    defaultMessage: "Rotate the image",
    description:
      "Label text for a button. Explains that the image will rotate when pressed",
  })}
/>

Phase 3: Testing and release

This phase explains how to test localization in your app and prepare it for release.

Step 1: Testing localization

The @canva/app-i18n-kit package lets you test your app using pseudolocalization and lets you change the locale using the Dev Toolkit. This gives you a preview of what your app could look like in a different locale.

The text lets you read your original English text, but inserts non-ASCII characters and adds width to each string to simulate lengthier languages.

NOTE: This approach is only compatible with our recommended tooling.

  1. Enable pseudolocalization in the Dev Toolkit. This shows you a preview of how different characters and text lengths affect the UI:

    • Check that any custom components respect the left-to-right and right-to-left setting.
    • Make sure there are no rendering issues or truncated text.

NOTE: You can’t preview or test actual translations, since these are only available after your app has been approved. However, you can then preview the translations before releasing your app.

Step 2: Generate the JSON file

This process extracts all the FormattedMessage strings in your code and saves them to a JSON file. You can then upload this file to Canva for translation.

The recommended tooling automatically generates files in the required format. To generate the messages_en.json file, run the following command:

shell
npm run build

Once this processed has finished, you should see a messages_en.json file appear in the dist directory, alongside your app.js. If this doesn't appear, re-run this step.

Step 3: Upload the JSON file

As part of the app submission process, you can upload the JSON file for translation:

  1. Locate your app in the Developer Portal.
  2. Upload the messages_en.json file, using the Translations file input.

Next steps

Canva reviews your app and identifies which locales should be supported. Canva then performs the translation of the supplied strings. You’ll be automatically notified once Canva has finished the translation process. You can then preview your app in the supported locales.

More information

  • Overview of the localization process: Localization overview
  • How to use the recommended workflow: Recommended workflow
  • Review the localization design guidelines: Localization
  • See the supported syntax and exceptions: ICU syntax
  • See bad practices: Bad practices

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.

Not setting up i18n linting

<DontText>Don't skip setting up the recommended i18n linting configuration or ignore linting errors.</DontText>

<DoText>Do set up the recommended i18n linting and fix all linting errors. See Step 2: Configure ESLint.</DoText>

Unformatted strings

<DontText>Don't render strings without localizing them first.</DontText>

jsx
<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" />

<DoText>Do use intl.formatMessage or &lt;FormattedMessage&gt; for any user-facing text.</DoText>

jsx
<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" })} />

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

Use dynamic id or defaultMessage values

<DontText>Don't use dynamic <code>id</code> values in your messages. @formatjs/cli won't extract these messages for translation.</DontText>

jsx
<FormattedMessage id={menuItemKey} />
<FormattedMessage id={`menu.${menuItemKey}`} />
// OR
intl.formatMessage({ id: menuItemKey })
intl.formatMessage({ id: `menu.${menuItemKey}` })

<DontText>Don't use dynamic <code>defaultMessage</code> values in your messages. @formatjs/cli won't extract these messages for translation.</DontText>

jsx
<FormattedMessage defaultMessage={menuItemLabel} />
<FormattedMessage defaultMessage={`Menu: ${menuItemLabel}`} />
// OR
intl.formatMessage({ defaultMessage: menuItemLabel })
intl.formatMessage({ defaultMessage: `Menu: ${menuItemLabel}` })

<DoText>Do use predefined messages and select the message based on a dynamic value. @formatjs/cli will correctly extract these messages for translation.</DoText>

jsx
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)} />

Render strings returned from the backend

<DontText>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.</DontText>

jsx
<Text>{response.errorMessage}</Text>

<DoText>Do map backend responses to predefined messages on the frontend.</DoText>

jsx
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)} />

See Preferred: Frontend localization for a more complete example.

Define placeholder-only messages

<DontText>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.</DontText>

jsx
<FormattedMessage
  defaultMessage="{selectedOption}"
  values={{ selectedOption }}
/>

<DoText>Do define a static message for each possible value, so that each value is translated.</DoText>

jsx
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)} />

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

Canva Developer Documentation SOP Site