On September 25th, 2024, we released v2 of the Apps SDK. To learn what’s new and how to upgrade, see Migration FAQ and Migration guide.

Localization overview

Canva can translate your app into other languages.

Canva has a large global audience, with most users originating from non-English speaking countries. By localizing your app, you can potentially reach a much larger audience.

To help make your app available in more languages, Canva can perform translations for apps that have successfully completed the app review process.

Only an app’s UI strings can be localized, so strings in creative content (such as images) are not included.

Supported locales

Canva supports a broad range of locales, while the Apps SDK supports a subset of these locales. These are chosen based on their proportion of Canva’s users.

The supported locales are:

  • Turkish: tr-TR
  • Japanese: ja-JP
  • Korean: ko-KR
  • German: de-DE
  • French: fr-FR
  • Portuguese: pt-BR
  • Indonesian: id-ID
  • Spanish: es-ES and es-419

The supported locales might change over time.

How localization works

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

To internationalize your app, add the react-intl library to your project. This lets you use FormattedMessage components for the UI text, using US English.

  1. Extract the UI strings into a JSON file and upload it as part of the review process.

  2. When your app is approved, Canva identifies which languages are needed and performs the translation. This process also translates the app name, description, among others. Note that Canva translates the strings for you, and you can’t provide your own translations.

  3. Test your app with the new locales and release it when you're ready. For more information, see Testing localization.

  4. Users with a matching Canva language setting will automatically see the app in their language.

    If their language is unavailable, the app can fallback to a similar language, otherwise it will default to English.

Translated strings are only supplied to your frontend JS code, so you should avoid displaying strings directly from the backend. Instead, have the backend return a status code or other response data. The UI can then use this to display an appropriate FormattedMessage.

Our design guidelines help you create a high-quality app that easily passes app review.

MessageFormat syntax

The Canva translation process supports a subset of the International Components for Unicode (ICU) MessageFormat syntax. We recommend using ICU because it allows you to capture grammatical elements that may differ between languages, such as plurals and correct placement of placeholders within a sentence.

For more information about the supported ICU syntax with recommendations and examples, see ICU syntax.

Add notes for translators

To assist Canva’s translators, you can include notes that describe the purpose and context of each string. This information is stored in the description property for each FormattedMessage and is made available to the translators.

Preferred: UI location and intention

An effective translator note should explain where the text content is used in the UI, and what the user's intention is.

In this example, the UI element text is consistent with the user's intention, and the translator can accurately identify the meaning:

<FormattedMessage
defaultMessage="Edit"
description="A button label for the user to go back to editing the text input."
/>
TS

This example includes details about the user's intention. This prevents ambiguity and provides the translator with context, which helps them disambiguate terms:

<FormattedMessage
defaultMessage="Apply all"
description="A label for a button element that applies all of the user's selected settings to the design."
/>
TS

You can also use the translator note description to specify the intended meaning of your words and the character limit:

  • If a word can have more than one meaning, specify the intended meaning. Some words can be used as either a verb or a noun:

    <FormattedMessage
    defaultMessage="Translate"
    description="This is the name of the Canva app. It is a noun."
    />
    TS
    <FormattedMessage
    defaultMessage="Translate"
    description="This is the label on a button to translate the design content. It is a verb."
    />
    TS
  • If space is limited or characters are restricted, specify the maximum number of characters. If possible, allocate more space for the string than the English text requires, because many languages are lengthier than English and need more space than the source string:

    <FormattedMessage
    defaultMessage="Continue with work email"
    description="Option for the user to continue the login process using an email associated with their job. Max. number of characters: 32"
    />
    TS

Practices to avoid

  • Avoid differences in meaning between the source and the translator note. This is an issue when translators can't accurately identify the string's meaning. In this example, the element text in the default message doesn't match the description's meaning:

    <FormattedMessage
    defaultMessage="Unmark"
    description="A button label for the user to go back to editing the text input."
    />
    TS
  • Avoid ambiguity between the source and the translator note. Ambiguity can confuse translators when they need to identify the meaning from a range of applicable interpretations. This example is ambiguous in the description:

    <FormattedMessage
    defaultMessage="Apply all"
    description="Select and apply settings."
    />
    TS

Excluding text

If you have strings that shouldn’t be localized, then you can exclude them from the translation process by not putting them within the FormattedMessage component. This means that the string won’t be included in the extracted JSON file. In addition, you must use an ESLint directive to ignore the rule which would usually prevent you from hard-coding English text. For example:

// eslint-disable-next-line no-literal-string-in-jsx
<Text>Text that must not be translated</Text>
TYPESCRIPT

Locale fallback

When your app has been translated and released, Canva will identify the locale for the current user and supply the best supported translations it has for your app.

For example, if your app has "Standard" language translation and the user's language is Canadian French (fr-CA), Canva will fallback to the best supported translations, which will be French (fr-FR).

If Canva can't find translations for a user's preferred language and there are no fallbacks available, then the app always falls back to English en.

Localize backend responses

This section 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:

// 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.",
},
});
TYPESCRIPT

Practices to avoid

  • Don't use dynamic id values. This is problematic because @formatjs/cli will not extract these messages.

    export const Component = ({ errorCode }) => {
    return (
    <Text>
    {/* Bad practice: using dynamic id */}
    <FormattedMessage
    id={errorCode}
    defaultMessage="An error occurred"
    />
    </Text>
    );
    };
    TYPESCRIPT
  • Don't use a dynamic defaultMessage. This is problematic because formatjs/cli can't reliably extract these message strings for localization, since they're determined at runtime rather than at compile time. As a result, it's better to use predefined defaultMessage values.

    export const Component = ({ errorMessage }) => {
    return (
    <Text>
    {/* Bad practice: using dynamic defaultMessage */}
    <FormattedMessage
    defaultMessage={errorMessage}
    />
    </Text>
    );
    };
    TYPESCRIPT
  • Avoid rendering strings returned from the backend, because you'd need to localize them yourself. Instead, you can map to predefined messages on your frontend, so that you can use translations provided by Canva.

    type GenerateImageResponse =
    | { status: "SUCCESS"; data: Image }
    | { status: "ERROR"; errorMessage: string };
    // ...
    export const Component = () => {
    // ... call generateImageResponse, data fetching logic, state, etc.
    if (response.status === "ERROR") {
    return (
    <Text>
    {/*
    Bad practice: displaying messages directly from the backend.
    NOTE: This is only okay if you are also localizing your responses on the BE yourself.
    */}
    {response.errorMessage}
    </Text>
    );
    }
    // ... handle other cases
    };
    TYPESCRIPT

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:

// 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
};
JSX

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

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

// backend/routers/videos.ts
const router = express.Router();
router.post("/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 */
});
});
TS

When you don't need to localize

Localization isn't available for team apps, and there might be other situations where you might not want to localize your app.

In these situations, we recommend you do the following to develop your app without localization:

  1. Disable ESLint rules related to localization. For example:

    {
    "rules": {
    "formatjs/no-invalid-icu": "off",
    "formatjs/no-literal-string-in-jsx": "off",
    "formatjs/enforce-description": "off",
    "formatjs/enforce-default-message": "off",
    "formatjs/enforce-placeholders": "off",
    "formatjs/no-id": "off",
    "formatjs/no-emoji": "off",
    "formatjs/no-useless-message": "off",
    "formatjs/no-multiple-plurals": "off",
    "formatjs/no-offset": "off",
    "formatjs/blocklist-elements": "off",
    "formatjs/no-complex-selectors": "off"
    }
    }
    JSON
  2. Remove npm libraries for localization:

    npm uninstall react-intl @formatjs/cli @formatjs/ts-transformer @canva/app-i18n-kit
    SHELL
  3. Remove all code related to the localization libraries. For example, remove any usage of AppI18nProvider, FormattedMessage, and others.

More information