Examples
App elements
Assets and media
Fundamentals
Intents
Design interaction
Drag and drop
Design elements
Localization
Content replacement
I18n
Internationalization support using react-intl with ICU syntax implementation.
Running this example
To run this example locally:
-
If you haven't already, create a new app in the Developer Portal(opens in a new tab or window). For more information, refer to our Quickstart guide.
-
In your app's configuration on the Developer Portal(opens in a new tab or window), ensure the "Development URL" is set to
http://localhost:8080
. -
Clone the starter kit:
git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.gitcd canva-apps-sdk-starter-kitSHELL -
Install dependencies:
npm installSHELL -
Run the example:
npm run start i18nSHELL -
Click the Preview URL link shown in the terminal to open the example in the Canva editor.
Example app source code
import {Box,Button,Link,MultilineInput,Rows,Slider,SortIcon,Text,Title,} from "@canva/app-ui-kit";import * as React from "react";import * as styles from "styles/components.css";import { FormattedMessage, useIntl } from "react-intl";import type {OpenExternalUrlRequest,OpenExternalUrlResponse,} from "@canva/platform";const DOCS_URL = "https://canva.dev/docs/apps/localization";const NAME = "Anto";export const App = ({requestOpenExternalUrl,}: {requestOpenExternalUrl: (request: OpenExternalUrlRequest,) => Promise<OpenExternalUrlResponse>;}) => {const openExternalUrl = async (url: string) => {const response = await requestOpenExternalUrl({url,});if (response.status === "aborted") {// user decided not to navigate to the link}};const intl = useIntl();const now = new Date();const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);return (<div className={styles.scrollContainer}><Rows spacing="2u">{/* ==================== Basic message ==================== */}<Title size="small"><FormattedMessage/*** The defaultMessage string is used as the source of translations. Should be written in English (US).* This is also displayed to users with an English locale, or a locale for which no translations* can be found.*/defaultMessage="My internationalized app"/*** Use the description string to convey as much context as possible to a human translator.* For guidance on writing translator notes, see here: https://canva.dev/docs/apps/localization/#add-notes-for-translators*/description="This is the title of the app that the user sees when they open it. Appears at the top of the page."/></Title>{/* ==================== Interpolation ==================== */}<Text><FormattedMessagedefaultMessage="Welcome to the world of AI creativity, {firstName}!"description="Greeting to welcome the user to the AI image generation app"values={{firstName: NAME,}}/></Text>{/* ==================== Numbers ==================== */}<Text><FormattedMessagedefaultMessage="Image generation is {progress, number, ::percent} complete."description="Displays the progress of the current image generation task that the user has requested"values={{progress: 0.75,}}/></Text>{/* ==================== Date and time formatting ==================== */}<Text><FormattedMessagedefaultMessage="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: nextWeek,refreshTime: nextWeek,}}/></Text>{/* ==================== Plurals ==================== */}<CreditUsage creditsCost={5} remainingCredits={50} /><CreditUsage creditsCost={1} remainingCredits={1} /><CreditUsage creditsCost={1} remainingCredits={0} />{/* ==================== Rich Text ==================== */}<FormattedMessagedefaultMessage="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) => (<Linkhref={DOCS_URL}requestOpenExternalUrl={() => openExternalUrl(DOCS_URL)}>{chunks}</Link>),callToAction: (chunks) => <strong>{chunks}</strong>,}}>{(chunks) => <Text>{chunks}</Text>}</FormattedMessage>{/* ==================== Message as string type ==================== */}<Button variant="primary">{intl.formatMessage({defaultMessage: "Generate image",description: "A button label to generate an image from a prompt",})}</Button>{/* ==================== Non-visible text usage (aria-label) ==================== */}<Buttonvariant="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.",})}/>{/* ==================== Component that changes between LTR and RTL languages ==================== */}<Box paddingStart="2u"><Slider min={0} max={100} /></Box>{/* ==================== List formatting ==================== */}<SelectedEffects />{/* ==================== Relative time formatting ==================== */}<LastGeneratedMessage lastGeneratedTime={now} />{/* ==================== Display name formatting ==================== */}<Text><FormattedMessagedefaultMessage="You are currently viewing this app in {language}"description="Informs the user about the language in which they are viewing the app"values={{language: intl.formatDisplayName(intl.locale, {type: "language",}),}}/></Text>{/* ==================== Multiline Example ==================== */}<Text><FormattedMessagedefaultMessage="This is a multi-line {breakingLine}text example!"description="An example text block that carries over multiple lines."values={{breakingLine: <br />,}}/></Text><MultilineInputminRows={2}maxRows={2}autoGrow={false}placeholder={intl.formatMessage({description:"Placeholder text to a MultilineInput that carries over multiple lines.",defaultMessage: `This is a multi {breakingLine}line input example!`,},{breakingLine: "\n",},)}/></Rows></div>);};export const CreditUsage = ({creditsCost,remainingCredits,}: {creditsCost: number;remainingCredits: number;}) => (<Text><FormattedMessagedefaultMessage={`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>);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><FormattedMessagedefaultMessage="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>);};const LastGeneratedMessage = ({lastGeneratedTime,}: {lastGeneratedTime: Date;}) => {const intl = useIntl();const [generatedTimeAgoInSeconds, setGeneratedTimeAgoInSeconds] =React.useState(Math.floor((new Date().getTime() - lastGeneratedTime.getTime()) / 1000),);React.useEffect(() => {const interval = setInterval(() => {setGeneratedTimeAgoInSeconds(Math.floor((new Date().getTime() - lastGeneratedTime.getTime()) / 1000),);}, 1000);return () => clearInterval(interval);}, [lastGeneratedTime]);return (<Text><FormattedMessagedefaultMessage="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>);};
TYPESCRIPT
import { AppUiProvider } from "@canva/app-ui-kit";import { createRoot } from "react-dom/client";import { App } from "./app";import "@canva/app-ui-kit/styles.css";import { AppI18nProvider } from "@canva/app-i18n-kit";import { requestOpenExternalUrl } from "@canva/platform";const root = createRoot(document.getElementById("root") as Element);function render() {root.render(<AppI18nProvider><AppUiProvider>{/* Any Apps SDK method needs to be injected to the component, to avoid the need to mock it in tests */}<App requestOpenExternalUrl={requestOpenExternalUrl} /></AppUiProvider></AppI18nProvider>,);}render();if (module.hot) {module.hot.accept("./app", render);}
TYPESCRIPT
// Import testing sub-packagesimport * as asset from "@canva/asset/test";import * as design from "@canva/design/test";import * as error from "@canva/error/test";import * as platform from "@canva/platform/test";import * as user from "@canva/user/test";// Initialize the test environmentsasset.initTestEnvironment();design.initTestEnvironment();error.initTestEnvironment();platform.initTestEnvironment();user.initTestEnvironment();// Once they're initialized, mock the SDKsjest.mock("@canva/asset");jest.mock("@canva/design");jest.mock("@canva/platform");jest.mock("@canva/user");// n.b. @canva/error should not be mocked - use it to simulate API error responses from other mocks by throwing CanvaError
TYPESCRIPT
import type { RenderResult } from "@testing-library/react";import { fireEvent, render } from "@testing-library/react";import { TestAppUiProvider } from "@canva/app-ui-kit";import { TestAppI18nProvider } from "@canva/app-i18n-kit";import { App, CreditUsage } from "../app";function renderInTestProvider(node: React.ReactNode): RenderResult {return render(// In a test environment, you should wrap your apps in `TestAppI18nProvider` and `TestAppUiProvider`, rather than `AppI18nProvider` and `AppUiProvider`<TestAppI18nProvider><TestAppUiProvider>{node}</TestAppUiProvider></TestAppI18nProvider>,);}describe("app", () => {let requestOpenExternalUrl: jest.Mock;beforeEach(() => {requestOpenExternalUrl = jest.fn().mockResolvedValue({});jest.useFakeTimers({now: new Date("2024-09-25"), // For consistent snapshots, pretend today is always Canva Extend 2024});});afterEach(() => {jest.useRealTimers();});it("calls openExternalUrl onClick", async () => {const result = renderInTestProvider(<App requestOpenExternalUrl={requestOpenExternalUrl} />,);// get a reference to the link elementconst galleryExternalLink = result.getByText(/gallery/);// assert its label matches what we expectexpect(galleryExternalLink.textContent).toContain("gallery");// assert our callback has not yet been calledexpect(requestOpenExternalUrl).not.toHaveBeenCalled();// programmatically simulate clicking the buttonfireEvent.click(galleryExternalLink);// assert our callback was calledexpect(requestOpenExternalUrl).toHaveBeenCalledTimes(1);});it("Renders token counts consistently 🎉", () => {const resultToken0 = renderInTestProvider(<CreditUsage creditsCost={5} remainingCredits={50} />,);// The snapshot test can be used to detect unexpected changes in the rendered output.expect(resultToken0.container).toMatchSnapshot();const resultToken1 = renderInTestProvider(<CreditUsage creditsCost={1} remainingCredits={1} />,);expect(resultToken1.container).toMatchSnapshot();const resultToken10 = renderInTestProvider(<CreditUsage creditsCost={1} remainingCredits={0} />,);expect(resultToken10.container).toMatchSnapshot();});});
TYPESCRIPT
API Reference
Need Help?
- Join our Community Forum(opens in a new tab or window)
- Report issues with this example on GitHub(opens in a new tab or window)