> ## Documentation Index > Fetch the complete documentation index at: https://www.canva.dev/docs/apps/llms.txt > Use this file to discover all available pages before exploring further. # I18n Internationalization support using react-intl with ICU syntax implementation. ## Running this example To run this example locally: 1. If you haven't already, create a new app in the [Developer Portal](https://www.canva.com/developers/apps). For more information, refer to our [Quickstart guide](https://www.canva.dev/docs/apps/quickstart/). 2. In your app's configuration on the [Developer Portal](https://www.canva.com/developers/apps), ensure the "Development URL" is set to `http://localhost:8080`. 3. Clone the starter kit: ```shell git clone https://github.com/canva-sdks/canva-apps-sdk-starter-kit.git cd canva-apps-sdk-starter-kit ``` 4. Install dependencies: ```shell npm install ``` 5. Run the example: ```shell npm run start:example i18n ``` 6. Click the **Preview URL** link shown in the terminal to open the example in the Canva editor. ## Example app source code ```typescript // For usage information, see the README.md file. 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; }) => { 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 (
{/* ==================== Basic message ==================== */} <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." /> {/* ==================== Interpolation ==================== */} {/* ==================== Numbers ==================== */} {/* ==================== Date and time formatting ==================== */} {/* ==================== Plurals ==================== */} {/* ==================== Rich Text ==================== */} ( openExternalUrl(DOCS_URL)} > {chunks} ), callToAction: (chunks) => {chunks}, }} > {(chunks) => {chunks}} {/* ==================== Message as string type ==================== */} {/* ==================== Non-visible text usage (aria-label) ==================== */}
); }; export const CreditUsage = ({ creditsCost, remainingCredits, }: { creditsCost: number; remainingCredits: number; }) => ( ); const SelectedEffects = () => { const intl = useIntl(); // In production, this list would 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 ( ); }; 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 ( ); }; ```
```typescript // For usage information, see the README.md file. 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"; import type { DesignEditorIntent } from "@canva/intents/design"; import { prepareDesignEditor } from "@canva/intents/design"; async function render() { const root = createRoot(document.getElementById("root") as Element); root.render( {/* Any Apps SDK method needs to be injected to the component, to avoid the need to mock it in tests */} , ); } const designEditor: DesignEditorIntent = { render }; prepareDesignEditor(designEditor); // Hot Module Replacement for development (automatically reloads the app when changes are made) if (module.hot) { module.hot.accept("./app", render); } ``` ```typescript // For usage information, see the README.md file. // Import Canva SDK testing utilities import * as asset from "@canva/asset/test"; import * as design from "@canva/design/test"; import * as error from "@canva/error/test"; import * as intents from "@canva/intents/test"; import * as platform from "@canva/platform/test"; import * as user from "@canva/user/test"; /* Initialize test environments for each Canva SDK package This sets up the necessary test infrastructure before mocking the actual SDK methods */ asset.initTestEnvironment(); design.initTestEnvironment(); error.initTestEnvironment(); intents.initTestEnvironment(); platform.initTestEnvironment(); user.initTestEnvironment(); /* Mock all Canva SDK packages except @canva/error This allows tests to run without making real API calls to Canva's services */ jest.mock("@canva/asset"); jest.mock("@canva/design"); jest.mock("@canva/intents"); jest.mock("@canva/platform"); jest.mock("@canva/user"); /* Important: @canva/error should not be mocked Use it to simulate API error responses from other mocks by throwing CanvaError instances This allows testing of error handling scenarios */ ``` ```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` {node} , ); } 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( , ); // get a reference to the link element const galleryExternalLink = result.getByText(/gallery/); // assert its label matches what we expect expect(galleryExternalLink.textContent).toContain("gallery"); // assert our callback has not yet been called expect(requestOpenExternalUrl).not.toHaveBeenCalled(); // programmatically simulate clicking the button fireEvent.click(galleryExternalLink); // assert our callback was called expect(requestOpenExternalUrl).toHaveBeenCalledTimes(1); }); it("Renders token counts consistently 🎉", () => { const resultToken0 = renderInTestProvider( , ); // The snapshot test can be used to detect unexpected changes in the rendered output. expect(resultToken0.container).toMatchSnapshot(); const resultToken1 = renderInTestProvider( , ); expect(resultToken1.container).toMatchSnapshot(); const resultToken10 = renderInTestProvider( , ); expect(resultToken10.container).toMatchSnapshot(); }); }); ``` ```markdown # Internationalization (i18n) Demonstrates how to implement internationalization in your Canva app. Shows message formatting, pluralization, date/time formatting, and rich text handling for multiple languages. For API reference docs and instructions on running this example, see: . ```
## API reference * [App UI Kit](https://www.canva.dev/docs/apps/app-ui-kit/) * [Unit testing](https://www.canva.dev/docs/apps/testing/) * [`prepareDesignEditor`](https://www.canva.dev/docs/apps/api/latest/intents-design-prepare-design-editor/) * [`requestOpenExternalUrl`](https://www.canva.dev/docs/apps/api/latest/platform-request-open-external-url/) ## Need help? * Join our [Community Forum](https://community.canva.dev/) * Report issues with this example on [GitHub](https://github.com/canva-sdks/canva-apps-sdk-starter-kit/issues)