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.
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
andes-419
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.
-
Extract the UI strings into a JSON file and upload it as part of the review process.
-
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.
-
Test your app with the new locales and release it when you're ready. For more information, see Testing localization.
-
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
.
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:
<FormattedMessagedefaultMessage="Edit"description="A button label for the user to go back to editing the text input."/>
This example includes details about the user's intention. This prevents ambiguity and provides the translator with context, which helps them disambiguate terms:
<FormattedMessagedefaultMessage="Apply all"description="A label for a button element that applies all of the user's selected settings to the design."/>
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:
<FormattedMessagedefaultMessage="Translate"description="This is the name of the Canva app. It is a noun."/>ts<FormattedMessagedefaultMessage="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:
<FormattedMessagedefaultMessage="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:
<FormattedMessagedefaultMessage="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:
<FormattedMessagedefaultMessage="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>
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.tsximport { 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 neededasync 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;}};
// component.messages.tsximport { 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.",},});
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 */}<FormattedMessageid={errorCode}defaultMessage="An error occurred"/></Text>);};typescript -
Don't use a dynamic
defaultMessage
. This is problematic becauseformatjs/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 predefineddefaultMessage
values.export const Component = ({ errorMessage }) => {return (<Text>{/* Bad practice: using dynamic defaultMessage */}<FormattedMessagedefaultMessage={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.tsximport type { Video } from "./api";import { findVideos } from "./api";export const App = () => {// ... stateconst 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.tsxexport 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:
// backend/routers/videos.tsconst 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 localeres.send({/* localized video response */});});
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:
-
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 -
Remove npm libraries for localization:
npm uninstall react-intl @formatjs/cli @formatjs/ts-transformer @canva/app-i18n-kitshell -
Remove all code related to the localization libraries. For example, remove any usage of
AppI18nProvider
,FormattedMessage
, and others.
More information
- 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 the supported syntax and exceptions: ICU syntax