From 0a943935c270f1b0c2126e31d036fa07e29c0cfd Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Tue, 29 Oct 2024 15:46:40 +0200 Subject: [PATCH 1/6] tutorials step 1 --- website/docs/tutorials/javascript.md | 31 ++- website/docs/tutorials/react-native.md | 56 ++-- website/docs/tutorials/react-patterns.md | 2 +- website/docs/tutorials/react-rsc.md | 16 +- website/docs/tutorials/react.md | 322 ++++++++++------------- 5 files changed, 193 insertions(+), 234 deletions(-) diff --git a/website/docs/tutorials/javascript.md b/website/docs/tutorials/javascript.md index 89ce133dc..c84180d24 100644 --- a/website/docs/tutorials/javascript.md +++ b/website/docs/tutorials/javascript.md @@ -5,30 +5,36 @@ description: Learn how to use Lingui's internationalization features in your van # JavaScript Apps Internationalization -In this tutorial, we'll learn how to use Lingui's internationalization features that don't depend on React. We'll take a minimalist approach and cover the main features of the `@lingui/core` package. +This tutorial will walk you through using Lingui's internationalization (i18n) features in a vanilla JavaScript application. We'll cover the essentials of the `@lingui/core` package, which handles all translation and message catalog management. + +:::tip Example +If you're looking for a working solution, check out the [Vanilla JS example](https://github.com/lingui/js-lingui/tree/main/examples/js). This example application shows a complete setup using Lingui and vanilla JavaScript. +::: ## Installing Lingui 1. Follow the [Installation and Setup](/docs/installation.mdx) page for initial setup. -2. Install the [`@lingui/core`](/docs/ref/core.md) package, which is responsible for translation and handling of message catalogs. +2. Install the [`@lingui/core`](/docs/ref/core.md) package, which is responsible for translation and message catalog handling. ## Setting up i18n -First we need to setup the i18n singleton, which is pretty simple: +First, we need to set up the i18n singleton, which is pretty simple: ```js import { i18n } from "@lingui/core"; - -// messages.js is generated by the cli import { messages } from "path-to-locale/en/messages.js"; i18n.load("en", messages); i18n.activate("en"); ``` +The `messages.js` is generated by the Lingui CLI and contains compiled message catalogs. Alternatively, you can load catalogs dynamically using the [`@lingui/loader`](/docs/ref/loader.md) or [`@lingui/vite-plugin`](/docs/ref/vite-plugin.md). + ## Localizing Your App -Once that is done, we can go ahead and use it! Wrap your text in [`t`](/docs/ref/macro.mdx#t) macro: +To localize your application, you need to wrap your localizable texts in [Macros](/docs/ref/macro.mdx). Lingui provides a set of Core Macros that transform tagged template literals and can be used in any JavaScript context. + +Let's wrap our text in the [`t`](/docs/ref/macro.mdx#t) macro: ```js import { t } from "@lingui/core/macro"; @@ -41,7 +47,7 @@ t`My name is ${name}`; // becomes "Je m'appelle Fred" ``` -Plurals and selections are possible using plural and select methods: +Plurals and selections are possible using [`plural`](/docs/ref/macro.mdx#plural) and [`select`](/docs/ref/macro.mdx#select) macros: ```js import { plural } from "@lingui/core/macro"; @@ -55,10 +61,10 @@ plural(count, { // becomes "42 livres" ``` -It's also possible to nest message formats. Each message format method in i18n has a standalone companion, which only returns message without performing the translation: +It's also possible to nest message formats. Each message format method in `i18n` has a standalone companion, which only returns message without performing the translation: ```js -import { t, select, plural } from "@lingui/core/macro" +import { t, select, plural } from "@lingui/core/macro"; select(gender, { offset: 1, @@ -71,10 +77,11 @@ select(gender, { }), male: plural(value, {...}), other: plural(value, {...}), -}) +}); ``` -## Further Reading +## See Also -- [CLI Reference](/docs/ref/cli.md) +- [Message Extraction Guide](/docs/guides/message-extraction.md) - [Pluralization Guide](/docs/guides/plurals.md) +- [`@lingui/cli` Reference](/docs/ref/cli.md) diff --git a/website/docs/tutorials/react-native.md b/website/docs/tutorials/react-native.md index f778c9894..2644623d3 100644 --- a/website/docs/tutorials/react-native.md +++ b/website/docs/tutorials/react-native.md @@ -5,27 +5,27 @@ description: Learn how to add internationalization to a React Native application # React Native Apps Internationalization -In this tutorial, we'll learn how to add internationalization to an existing application in React Native. Before going further, please follow the [Installation and Setup](/docs/installation.mdx?transpiler=babel) (for Babel) instructions. - -:::caution Warning - -With the dependencies installed and set up, before running your app, please clear your Metro bundler cache with `npx expo start -c` or `npx react-native start --reset-cache` (if you do not use Expo). - -::: +In this tutorial, we'll learn how to add internationalization to an existing application in React Native. The React Native tutorial is similar to the one for [React](/docs/tutorials/react.md) and we highly recommend you read that one first because it goes into greater detail on many topics. Here, we will only cover parts that are relevant for React Native. -:::tip Hint -If you're looking for a working solution, check out the [sources available here](https://github.com/lingui/js-lingui/tree/main/examples/react-native). The example app showcases more functionality than this guide. +:::tip Example +If you're looking for a working solution, check out the [React Native example](https://github.com/lingui/js-lingui/tree/main/examples/react-native). This example application shows a complete setup using Lingui and React Native. ::: -:::caution Note This tutorial assumes you use Lingui >= 4.2 and React Native >=0.71 or Expo >=48, with the Hermes JavaScript Engine. `@lingui/core` depends on several APIs exposed by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). Support of the `Intl` object can vary across React Native and OS versions. -If some `Intl` feature is not supported by your runtime, you can [polyfill it](https://formatjs.io/docs/polyfills). +If some `Intl` feature is not supported by your runtime, you can [polyfill it](#polyfilling-intl-apis). -See [here](https://github.com/facebook/hermes/issues/23) for details about `Intl` support in the Hermes engine. +## Installing Lingui + +1. Follow the [Installation and Setup](/docs/installation.mdx?transpiler=babel) page for initial setup (for Babel). +2. Install the [`@lingui/core`](/docs/ref/core.md) and [`@lingui/react`](/docs/ref/react.md) packages. +3. _(optional)_ Install and configure the [`@lingui/metro-transformer`](/docs/ref/metro-transformer.mdx) package that enables Metro to compile `.po` files on the fly. + +:::caution Warning +With the dependencies installed and set up, before running your app, please clear your Metro bundler cache with `npx expo start -c` or `npx react-native start --reset-cache` (if you do not use Expo). ::: ## Polyfilling Intl APIs @@ -34,7 +34,7 @@ React Native's JS engine may not support all `Intl` features out of the box. As Follow the polyfill installation instructions before proceeding further. Import polyfills from `/polyfill-force` to avoid [slow initialization time on low-end devices](https://github.com/formatjs/formatjs/issues/4463). -## Example component +## Example Component We're going to translate the following contrived example: @@ -94,7 +94,7 @@ As you can see, it's a simple mailbox application with only one screen. ## Internationalization in React (Native) :::tip TL;DR -There are several ways to render translations: You may use the [`Trans`](/docs/ref/react.md#trans) component or the [`useLingui`](/docs/ref/react.md#uselingui) hook together with the [`t`](/docs/ref/macro.mdx#t) or [`msg`](/docs/ref/macro.mdx#definemessage) macros. When you change the active locale or load new messages, all components that consume the Lingui context provided by [`I18nProvider`](/docs/ref/react.md#i18nprovider) will re-render, making sure the UI shows the correct translations. +There are several ways to render translations: You may use the [`Trans`](/docs/ref/macro.mdx#trans) macro or the [`useLingui`](/docs/ref/macro.mdx#uselingui) hook together with the [`t`](/docs/ref/macro.mdx#t) or [`msg`](/docs/ref/macro.mdx#definemessage) macros. When you change the active locale or load new messages, all components that consume the Lingui context provided by [`I18nProvider`](/docs/ref/react.md#i18nprovider) will re-render, making sure the UI shows the correct translations. ::: Not surprisingly, this part isn't too different from the [React tutorial](/docs/tutorials/react.md). @@ -128,7 +128,7 @@ The solution is to use the `t` macro which we can obtain from the `useLingui` ho ```tsx import { useLingui } from '@lingui/react/macro'; -const { t } = useLingui() +const { t } = useLingui(); ... <Button title={t`this will be translated and rerendered with locale changes`}/> ``` @@ -161,7 +161,7 @@ const Inbox = ({ markAsRead }) => { }; ``` -## Internationalization outside of React +## Internationalization Outside of React Until now, we have covered the [`Trans`](/ref/react#trans) macro and the [`useLingui`](/ref/react#uselingui) hook. Using them will make sure our components are always in sync with the currently active locale and message catalog. @@ -184,21 +184,21 @@ const showDeleteConfirmation = () => { } ``` -## Changing the active locale +## Changing the Active Locale The last remaining piece of the puzzle is changing the active locale. The `i18n` object exposes [`i18n.loadAndActivate()`](/ref/core#i18n.loadAndActivate) for that. Call the method and the [`I18nProvider`](/docs/ref/react.md#i18nprovider) will re-render the translations. It all becomes clear when you take a look at the [final code](https://github.com/lingui/js-lingui/tree/main/examples/react-native/src/MainScreen.tsx#L29). However, we don't recommend that you change the locale like this in mobile apps, as it can cause conflicts in how your app ui is localized. This is further [explained here](https://www.youtube.com/live/uLicTDG5hSs?feature=share&t=9088). -## Choosing the default locale +## Choosing the Default Locale Lingui does not ship with functionality that would allow you to determine the best locale you should activate by default. Instead, please refer to [Expo localization](https://docs.expo.dev/versions/latest/sdk/localization/#localizationgetlocales) or [react-native-localize](https://github.com/zoontek/react-native-localize#getlocales). Both packages will provide you with information about the locales that the user prefers. Combining that information with the locales that your app supports will give you the locale you should use by default. -## Rendering and styling of translations +## Rendering and Styling of Translations -As described in the [reference](/docs/ref/react.md#rendering-translations), by default, translation components render translation as text without a wrapping tag. In React Native though, all text must be wrapped in the `Text` component. This means we would need to use the [`Trans`](/docs/ref/react.md#trans) component like this: +As described in the [reference](/docs/ref/react.md#rendering-translations), by default, translation components render translation as text without a wrapping tag. In React Native though, all text must be wrapped in the `Text` component. This means we would need to use the [`Trans`](/docs/ref/macro.mdx#trans) component like this: ```tsx <Text> @@ -214,9 +214,9 @@ You'll surely agree the `Text` component looks a little redundant. That's why th Alternatively, you may override the default locally on the i18n components, using the `render` or `component` props, as documented in the [reference](/docs/ref/react.md#rendering-translations). Use them to apply styling to the rendered string. -## Nesting components +## Nesting Components -The [`Trans`](/docs/ref/react.md#trans) macro and `Text` component may be nested, for example to achieve the effect shown in the picture. This is thanks to how React Native [handles nested text](https://facebook.github.io/react-native/docs/text#nested-text). +The [`Trans`](/docs/ref/macro.mdx#trans) macro and `Text` component may be nested, for example to achieve the effect shown in the picture. This is thanks to how React Native [handles nested text](https://facebook.github.io/react-native/docs/text#nested-text). ![image](/img/docs/rn-component-nesting.png) @@ -238,12 +238,14 @@ The extracted string for translation will look like this: The important point here is that the sentence isn't broken into pieces but remains together - that will allow the translator to deliver a quality result. -## Further reading +## See Also -- [Metro transformer for `po` files](/docs/ref/metro-transformer.mdx) -- [Common i18n patterns in React](/docs/tutorials/react-patterns.md) -- [`@lingui/react` reference documentation](/docs/ref/react.md) -- [`@lingui/cli` reference documentation](/docs/ref/cli.md) +- [Message Extraction Guide](/docs/guides/message-extraction.md) +- [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) +- [`@lingui/react` Reference](/docs/ref/react.md) +- [`@lingui/cli` Reference](/docs/ref/cli.md) - [Localizing React Native apps talk from React Native EU 2022](https://www.youtube.com/live/uLicTDG5hSs?feature=share&t=7512) +--- + This guide originally authored and contributed in full by [Vojtech Novak](https://twitter.com/vonovak). diff --git a/website/docs/tutorials/react-patterns.md b/website/docs/tutorials/react-patterns.md index fadaa12fd..57ee3cd0b 100644 --- a/website/docs/tutorials/react-patterns.md +++ b/website/docs/tutorials/react-patterns.md @@ -87,7 +87,7 @@ For better control and flexibility, it's a good idea to avoid the global `i18n` ```ts import { msg } from "@lingui/core/macro"; import { useLingui } from "@lingui/react/macro"; -import { I18n } from "@lingui/core"; +import { i18n } from "@lingui/core"; export function showAlert(i18n: I18n) { alert(i18n._(msg`...`)); diff --git a/website/docs/tutorials/react-rsc.md b/website/docs/tutorials/react-rsc.md index be9f84f49..e8ed9839c 100644 --- a/website/docs/tutorials/react-rsc.md +++ b/website/docs/tutorials/react-rsc.md @@ -5,7 +5,7 @@ description: Learn how to setup and use Lingui with RSC & Next.js Lingui provides support for React Server Components (RSC) as of v4.10.0. In this tutorial, we'll learn how to add internationalization to an application with the Next.js [App Router](https://nextjs.org/docs/app). However, the same principles are applicable to any RSC-based solution. -:::tip Hint +:::tip Example There's a working example available [here](https://github.com/lingui/js-lingui/tree/main/examples/nextjs-swc). We will make references to the important parts of it throughout the tutorial. The example is more complete than this tutorial. The example uses both Pages Router and App Router, so you can see how to use Lingui with both in [this commit](https://github.com/lingui/js-lingui/pull/1944/commits/100fc74abb49cff677f4b1cac1dfd5da60262b67). @@ -13,7 +13,7 @@ The example uses both Pages Router and App Router, so you can see how to use Lin Before going further, please follow the [Installation and Setup](/docs/installation.mdx?transpiler=swc) instructions (for SWC or Babel depending on which you use - most likely it's SWC). You may also need to configure your `tsconfig.json` according to [this visual guide](https://twitter.com/mattpocockuk/status/1724462050288587123). This is so that TypeScript understands the values exported from `@lingui/react` package. -### Adding i18n support to Next.js +### Adding i18n Support to Next.js Firstly, your Next.js app needs to be ready for routing and rendering of content in multiple languages. This is done through the middleware (see the [example app's middleware](https://github.com/lingui/js-lingui/blob/main/examples/nextjs-swc/src/middleware.ts)). Please read the [official Next.js docs](https://nextjs.org/docs/app/building-your-application/routing/internationalization) for more information. @@ -33,7 +33,7 @@ module.exports = { }; ``` -### Setup with server components +### Setup with Server Components With Lingui, the experience of localizing React is the same in client and server components: `Trans` and `useLingui` can be used identically in both worlds, even though internally there are two implementations. @@ -110,7 +110,7 @@ Why are we not passing the I18n instance directly from `RootLayout` to the clien Lastly, there's the `appRouterI18n.ts` file, which is only executed on server and holds one instance of I18n object for each locale of our application. See [here](https://github.com/lingui/js-lingui/blob/main/examples/nextjs-swc/src/appRouterI18n.ts) how it's implemented in the example app. -### Rendering translations in server and client components +### Rendering Translations in Server and Client Components Below you can see an example of a React component. This component can be rendered **both with RSC and on client**. This is great if you're migrating a Lingui-based project from pages router to App Router because you can keep the same components working in both worlds. @@ -145,7 +145,7 @@ There's one last caveat: in a real-world app, you will need to localize many pag This means you need to repeat the `setI18n` in every page and layout. Luckily, you can easily factor it out into a simple function call, or create a HOC with which you'll wrap pages and layouts [as seen here](https://github.com/lingui/js-lingui/blob/main/examples/nextjs-swc/src/withLingui.tsx). Please let us know if there's a known better way. -### Changing the active language +### Changing the Active Language Most likely, your users will not need to change the language of the application because it will render in their preferred language (obtained from the `accept-language` header in the [middleware](https://github.com/lingui/js-lingui/blob/2f1c1c3ae9e079c1c0e1a2ff617b1d0775af3170/examples/nextjs-swc/src/middleware.ts#L30)), or with a fallback. @@ -179,7 +179,7 @@ export default function SomePage() { Read more about [lazy translation](/docs/tutorials/react-patterns.md#translations-outside-react-components) to see how to handle translation defined on the module level. -## Further reading +## See Also -- [Common i18n patterns in React](/docs/tutorials/react-patterns.md) -- [`@lingui/react` reference documentation](/docs/ref/react.md) +- [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) +- [`@lingui/react` Reference](/docs/ref/react.md) diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index d59689617..2d3aa987f 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -5,12 +5,18 @@ description: Learn how to add internationalization to a React application using # React Apps Internationalization -In this tutorial, we'll learn how to add internationalization (i18n) to an existing React JS application. +In this tutorial, we'll learn how to add internationalization (i18n) to an existing React JS application. We'll focus on the most common patterns and best practices for using Lingui in React. + +:::tip Example +If you're looking for a working solution, check out the [Examples page](/docs/misc/examples.md). It contains several sample projects with the complete setup using Lingui and React. + +It includes examples for _Create React App_, _React with Vite and Babel_, _React with Vite and SWC_, and more. +::: ## Installing Lingui 1. Follow the [Installation and Setup](/docs/installation.mdx) page for initial setup. -2. Install the [`@lingui/react`](/docs/ref/react.md) package which provides React components and hooks for internationalization. +2. Install the [`@lingui/core`](/docs/ref/core.md) and [`@lingui/react`](/docs/ref/react.md) packages. ## Let's Start @@ -59,13 +65,13 @@ export default function Inbox() { } ``` -## Setup +This application is a simple mailbox with a header, a paragraph with a link and a button, another paragraph with a message count, and a footer with the last login date. We will use it as the basis for our tutorial. -We will directly start translating the `Inbox` component, but we need to complete one more step to setup our application. +## Setup -Components need to read information about current language and message catalogs from `i18n` instance. Initially, you can use the one created and exported from `@lingui/core` and later you can replace with your one if such need arises. +We will start translating the `Inbox` component right away, but we need to do one more step to set up our application. -Lingui uses the `I18nProvider` to pass the instance `i18n` to your React components. +Components need to read information about current language and message catalogs from the [`i18n`](/docs/ref/core.md#i18n) instance. Lingui uses the [`I18nProvider`](/docs/ref/react.md#i18nprovider) to pass the `i18n` instance to your React components. Let's add all required imports and wrap our app inside [`I18nProvider`](/docs/ref/react.md#i18nprovider): @@ -90,19 +96,13 @@ const App = () => ( render(<App />, document.getElementById("root")); ``` -:::tip +:::info You might be wondering: how are we going to change the active language? That's what the [`I18n.load`](/docs/ref/core.md#i18n.load) and [`i18n.activate`](/docs/ref/core.md#i18n.activate) calls are for! However, we cannot change the language unless we have the translated message catalog. And to get the catalog, we first need to extract all messages from the source code. - -Let's deal with language switching later... but if you're still curious, take a look at [example](/docs/guides/dynamic-loading-catalogs.md) with Redux and Webpack. ::: ## Introducing Internationalization -Now we're finally going to _translate_ our app. Actually, we aren't going to _translate_ from one language to another right now. Instead, we're going to _prepare_ our app for translation. This process is called _internationalization_ and you should practice saying this word aloud until you're able to say it three times very quickly. - -:::note -From now on, _internationalization_ will be shortened to a common numeronym _i18n_. -::: +Now we're finally going to _translate_ our application. Actually, we're not going to _translate_ from one language to another right now. Instead, we're going to _prepare_ our app for translation. This process is called _internationalization_. Let's start with the basics - static messages. These messages don't have any variables, HTML or components inside. Just some text: @@ -110,7 +110,7 @@ Let's start with the basics - static messages. These messages don't have any var <h1>Message Inbox</h1> ``` -All we need to make this heading translatable is wrap it in [`Trans`](/docs/ref/macro.mdx#trans) macro: +To make this heading translatable, simply wrap it in the [`Trans`](/docs/ref/macro.mdx#trans) macro: ```jsx import { Trans } from "@lingui/react/macro"; @@ -122,30 +122,28 @@ import { Trans } from "@lingui/react/macro"; ### Macros vs. Components -If you're wondering what macros are and what's the difference between macros and components, this short paragraph is for you. +If you're wondering what [Macros](/docs/ref/macro.mdx) are and the difference between macros and runtime components, here's a quick explanation. -In general, macros are executed at compile time and they transform source code in some way. We use this feature in [Lingui](https://github.com/lingui/js-lingui) to simplify writing messages. +In general, macros are executed at compile time and serve to transform the source code to make the message writing process easier. Under the hood, all JSX macros are transformed into the runtime component [`Trans`](/docs/ref/react.md#trans) (imported from `@lingui/react`). -Under the hood, all JSX macros are transformed into [`Trans`](/docs/ref/react.md#trans) component. Take a look at this short example. This is what we write: +Below is a brief example demonstrating this transformation: ```jsx import { Trans } from "@lingui/react/macro"; <Trans>Hello {name}</Trans>; -``` -And this is how the code is transformed: +// ↓ ↓ ↓ ↓ ↓ ↓ -```jsx import { Trans } from "@lingui/react"; <Trans id="OVaF9k" message="Hello {name}" values={{ name }} />; ``` -See the difference? [`Trans`](/docs/ref/react.md#trans) component receives `id` and `message` props with a message in ICU MessageFormat syntax. -We could write it manually, but it's just easier and shorter to write JSX as we're used to and let macros generate the message for us. +As you can see, the [`Trans`](/docs/ref/react.md#trans) runtime component gets `id` and `message` props with a message in [ICU MessageFormat](/docs/guides/message-format.md) syntax. We could write it manually, but it's just easier and shorter to write JSX as we're used to and let macros generate the message for us. -Another advantage of using macros is that all non-essential properties are excluded from the production build. This results in a significant reduction in the size footprint for internationalization. +:::tip Bundle Size Impact +Another advantage of using macros is that all non-essential properties are excluded from the production build. This results in a significant reduction in the size footprint for internationalization: ```jsx // NODE_ENV=production @@ -154,13 +152,19 @@ import { Trans } from "@lingui/react"; <Trans id="OVaF9k" values={{ name }} />; ``` +::: + ### Extracting Messages Back to our project. It's nice to use JSX and let macros generate messages under the hood. Let's check that it actually works correctly. -All messages from the source code must be extracted into external message catalogs. Message catalogs are interchange files between developers and translators. We're going to have one file per language. Let's enter command line for a while. +All messages from the source code must be extracted into external message catalogs. Message catalogs are interchange files between developers and translators. We're going to have one file per language. + +:::info +Refer to the [Message Extraction](/docs/guides/message-extraction.md) guide for more information about various message extraction concepts and strategies. +::: -Let's run the [`extract`](/docs/ref/cli.md#extract) CLI command: +Let's switch to the command line for a moment. Execute the [`extract`](/docs/ref/cli.md#extract) CLI command. If everything is set up correctly, you should see the extracted message statistics in the output: ```bash > lingui extract @@ -172,12 +176,11 @@ Catalog statistics: │ cs │ 1 │ 1 │ │ en │ 1 │ 1 │ └──────────┴─────────────┴─────────┘ - -(use "lingui extract" to update catalogs with new messages) -(use "lingui compile" to compile catalogs for production) ``` -Nice! It seems it worked, we have two message catalogs (one per each locale) with 1 message each. Let's take a look at file `src/locales/cs/messages.po`: +As a result, we have two new files in the `locales` directory: `en/messages.po` and `cs/messages.po`. These files contain extracted messages from the source code. + +Let's take a look at the Czech message catalog: ```gettext title="src/locales/cs/messages.po" msgid "" @@ -189,22 +192,22 @@ msgstr "" "X-Generator: @lingui/cli\n" "Language: cs\n" +// highlight-start #: src/Inbox.js:12 msgid "Message Inbox" msgstr "" +// highlight-end ``` -That's the message we've wrapped inside [`Trans`](/docs/ref/macro.mdx#trans) macro! - -Let's add the Czech translation: +It contains the message we wrapped in the [`Trans`](/docs/ref/macro.mdx#trans) macro. Let's add the Czech translation: -```po title="src/locales/cs/messages.po" +```po title="src/locales/cs/messages.po" {3} #: src/Inbox.js:12 msgid "Message Inbox" msgstr "Příchozí zprávy" ``` -If we run [`extract`](/docs/ref/cli.md#extract) command again, we'll see that all Czech messages are translated: +If we run the [`extract`](/docs/ref/cli.md#extract) command again, we'll see that all the Czech messages have been translated: ```bash > lingui extract @@ -216,12 +219,11 @@ Catalog statistics: │ cs │ 1 │ 0 │ │ en │ 1 │ 1 │ └──────────┴─────────────┴─────────┘ - -(use "lingui extract" to update catalogs with new messages) -(use "lingui compile" to compile catalogs for production) ``` -That's great! So, how we're going to load it into your app? [Lingui](https://github.com/lingui/js-lingui) introduces concept of compiled message catalogs. Before we load messages into our app, we need to compile them. As you see in the help in command output, we use [`compile`](/docs/ref/cli.md#compile) for that: +That's great! So how do we load it into your application? Lingui introduces the concept of compiled message catalogs. Before we load messages into our application, we need to compile them. + +Use the [`compile`](/docs/ref/cli.md#compile) command to do this: ```bash > lingui compile @@ -230,10 +232,10 @@ Compiling message catalogs… Done! ``` -If you look inside `locales/<locale>` directory, you'll see there's a new file for each locale: `messages.js`. This file contains compiled message catalog. +If you look inside the `locales/<locale>` directory, you'll see that there is a new file for each locale: `messages.js`. This file contains the compiled message catalog. :::tip -If you use TypeScript, you can add `--typescript` flag to `compile` script to produce compiled message catalogs with TypeScript types. +If you use TypeScript, you can add the `--typescript` flag to the `compile` command to produce compiled message catalogs with TypeScript types. ::: Let's load this file into our app and set active language to `cs`: @@ -265,25 +267,27 @@ render(<App />, document.getElementById("root")); When we run the app, we see the inbox header is translated into Czech. +:::tip +Alternatively, you can load catalogs dynamically using the [`@lingui/loader`](/docs/ref/loader.md) or [`@lingui/vite-plugin`](/docs/ref/vite-plugin.md) without the need to import compiled messages manually. +::: + ### Summary of Basic Workflow Let's go through the workflow again: -1. Add an [`I18nProvider`](/docs/ref/react.md#i18nprovider), this component provides the active language and catalog(s) to other components -2. Wrap messages in [`Trans`](/docs/ref/macro.mdx#trans) macro -3. Run [`extract`](/docs/ref/cli.md#extract) command to generate message catalogs -4. Translate message catalogs (send them to translators usually) -5. Run [`compile`](/docs/ref/cli.md#compile) to create runtime catalogs -6. Load runtime catalog -7. Profit - -Steps 1 and 7 needs to be done only once per project and locale. Steps 2 to 5 become the common workflow for internationalizing the app. +1. Add an [`I18nProvider`](/docs/ref/react.md#i18nprovider), this component provides the active language and catalog(s) to other components. +2. Wrap messages in the [`Trans`](/docs/ref/macro.mdx#trans) macro. +3. Run [`extract`](/docs/ref/cli.md#extract) command to generate message catalogs. +4. Translate message catalogs (send them to translators usually). +5. Run [`compile`](/docs/ref/cli.md#compile) to create runtime catalogs. +6. Load runtime catalog. +7. Profit! 🎉 -It isn't necessary to extract/translate messages one by one. This usually happens in batches. When you finalize your work or PR, run [`extract`](/docs/ref/cli.md#extract) to generate latest message catalogs and before building the app for production, run [`compile`](/docs/ref/cli.md#compile). +It's not necessary to extract/translate messages one by one. This is usually done in batches. When you finish your work or PR, run [`extract`](/docs/ref/cli.md#extract) to generate the latest message catalogs, and before building the application for production, run [`compile`](/docs/ref/cli.md#compile). ## Non-JSX Translation -So far we learned how to translate string inside a JSX element, but what if we want to translate something that is not inside a JSX? Or pass a translation as a prop to another component? +So far we have learned how to translate strings inside a JSX element, but what if we want to translate something that is not inside a JSX? Or pass a translation as a prop to another component? We have this piece of code in our example: @@ -293,7 +297,7 @@ const markAsRead = () => { }; ``` -To translate it, we will use the `useLingui` macro hook: +To translate it, we will use the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook: ```js import { useLingui } from "@lingui/react/macro"; @@ -305,9 +309,9 @@ const markAsRead = () => { }; ``` -Now the `Marked as read.` message would be picked up by extractor, and available for translation in the catalog. +Now the `Marked as read.` message would be picked up by the extractor, and available for translation in the catalog. -You could also pass variables and use any other macro in the message. +You could also pass variables and use any other macro in the message: ```jsx const { t } = useLingui(); @@ -342,54 +346,40 @@ Although it looks complex, there's really nothing special here. Just wrap the co </p> ``` -Spooky, right? Let's see how this message actually looks in the message catalog. Run [`extract`](/docs/ref/cli.md#extract) command and take a look at the message: +Let's see how this message actually looks in the message catalog. Run the [`extract`](/docs/ref/cli.md#extract) command and take a look at the message: -```jsx -See all <0>unread messages</0> or <1>mark them</1> as read. +```gettext +#: src/Inbox.js:20 +msgid "See all <0>unread messages</0> or <1>mark them</1> as read." +msgstr "" ``` -You may notice that components and html tags are replaced with indexed tags (`<0>`, `<1>`). This is a little extension to the ICU MessageFormat which allows rich-text formatting inside translations. Components and their props remain in the source code and don't scare our translators. The tags in the extracted message won't scare our translators either: translators are used to seeing tags and their tools support them. Also, in case we change a `className`, we don't need to update our message catalogs. How cool is that? +You may notice that components and html tags are replaced with indexed tags (`<0>`, `<1>`). This is a little extension to the ICU MessageFormat which allows rich-text formatting inside translations. Components and their props remain in the source code and don't scare our translators. Also, in case we change a `className`, we don't need to update our message catalogs. ### JSX to MessageFormat Transformations -It may look a bit _hackish_ at first sight, but these transformations are actually very easy, intuitive and feel very _Reactish_. We don't have to think about the MessageFormat, because it's created by the library. We write our components in the same way as we're used to and simply wrap text in the [`Trans`](/docs/ref/macro.mdx#trans) macro. +At first glance, these transformations might seem somewhat unconventional; however, they are straightforward, intuitive, and align well with React principles. There is no need to focus on MessageFormat, as the library handles its creation for us. We can write our components as we typically would and simply wrap the text in the [`Trans`](/docs/ref/macro.mdx#trans) macro. Let's see some examples with MessageFormat equivalents: ```jsx -// Expressions -<p> - <Trans>Hello {name}</Trans> -</p> +<Trans>Hello {name}</Trans> // Hello {name} ``` Any expressions are allowed, not just simple variables. The only difference is, only the variable name will be included in the extracted message: -- Simple variable -> named argument: - - ```jsx - <p> - <Trans>Hello {name}</Trans> - </p> - // Hello {name} - ``` - - Any expression -> positional argument: ```jsx - <p> - <Trans>Hello {user.name}</Trans> - </p> + <Trans>Hello {user.name}</Trans> // Hello {0} ``` - Object, arrays, function calls -> positional argument: ```jsx - <p> - <Trans>The random number is {Math.rand()}</Trans> - </p> + <Trans>The random number is {Math.rand()}</Trans> // The random number is {0} ``` @@ -411,17 +401,13 @@ Any expressions are allowed, not just simple variables. The only difference is, // Dear Watson,<0/>it's not exactly what I had in my mind. ``` -Some expressions are _valid_ and won't throw any error, yet it doesn't make any sense to write: - -```jsx -<Trans>{isOpen && <Modal />}</Trans> -``` +:::caution +Try to keep your messages simple and avoid complex expressions. During extraction, these expressions will be replaced by placeholders, resulting in a lack of context for translators. There is also a special rule in Lingui [ESLint Plugin](/docs/ref/eslint-plugin.md) to catch these cases: `no-expression-in-message`. +::: ### Message ID -At this point we're going to explain what message ID is and how to set it manually. - -Translators work with the _message catalogs_ we saw above. No matter what format we use (gettext, xliff, json), it's just a mapping of a message ID to the translation. +At this point, we'll explain what the message ID is and how to set it manually. Translators work with _message catalogs_. No matter what format we use, it's just a mapping of a message ID to the translation. Here's an example of a simple message catalog in **Czech** language: @@ -439,34 +425,16 @@ Here's an example of a simple message catalog in **Czech** language: | Tuesday | Mardi | | Wednesday | Mercredi | -The message ID is _what all catalogs have in common_ – Lundi and Pondělí represent the same message in different languages. It's also the same as the `id` prop in [`Trans`](/docs/ref/macro.mdx#trans) macro. - -There are two approaches to how a message ID can be created: - -1. Using the source language (e.g. `Monday` from English, as in example above) -2. Using a custom id (e.g. `weekday.monday`) - -Both approaches have their pros and cons and it's not in the scope of this tutorial to compare them. - -By default, [Lingui](https://github.com/lingui/js-lingui) generates message ID from the content of [`Trans`](/docs/ref/macro.mdx#trans) macro, which means it uses the source language. However, we can easily override it by setting the `id` prop manually: - -```jsx -<h1> - <Trans id="inbox.title">Message Inbox</Trans> -</h1> -``` +The message ID is _what all catalogs have in common_ – "Lundi" and "Pondělí" represent the same message in different languages. -This will generate: - -```jsx -<h1> - <Trans id="inbox.title" message="Message Inbox" /> -</h1> -``` +There are two approaches for creating a message ID: -In our message catalog, we'll see `inbox.title` as message ID, but we also get `Message Inbox` as default translation for English. +- Automatically generated from message (e.g. `Monday`) and context, if available. +- Explicit message ID set by the developer (e.g. `days.monday`). -For the rest of this tutorial, we'll use auto-generated message IDs to keep it simple. +:::info +Refer to the [Explicit vs Generated IDs](/docs/guides/explicit-vs-generated-ids.md) guide for more information about the pros and cons of each approach. +::: ## Plurals @@ -480,11 +448,11 @@ Let's move on and add i18n to another text in our component: </p> ``` -This message is a bit special, because it depends on the value of the `messagesCount` variable. Most languages use different forms of words when describing quantities - this is called [pluralization](https://en.wikipedia.org/wiki/Plural). +This message is a bit special, because it depends on the value of the `messagesCount` variable. Most languages use different forms of words when describing quantities - this is called [pluralization](/docs/guides/plurals.md). What's tricky is that different languages use different number of plural forms. For example, English has only two forms - singular and plural - as we can see in the example above. However, Czech language has three plural forms. Some languages have up to 6 plural forms and some don't have plurals at all! -:::tip +:::info Lingui uses `Intl.PluralRules` which is supported in [every modern browser](https://caniuse.com/intl-pluralrules) and can be polyfilled for older. So you don't need to setup anything special. ::: @@ -503,7 +471,7 @@ How do we know which plural form we should use? It's very simple: we, as develop We don't need to select these forms manually. We'll use [`Plural`](/docs/ref/macro.mdx#plural-1) component, which takes a `value` prop and based on the active language, selects the right plural form: ```jsx -import { Trans, Plural } from "@lingui/react/macro"; +import { Plural } from "@lingui/react/macro"; <p> <Plural value={messagesCount} one="There's # message in your inbox" other="There are # messages in your inbox" /> @@ -512,38 +480,33 @@ import { Trans, Plural } from "@lingui/react/macro"; This component will render `There's 1 message in your inbox` when `messageCount = 1` and `There are # messages in your inbox` for any other values of `messageCount`. `#` is a placeholder, which is replaced with `value`. -Cool! Curious how this component is transformed under the hood and how the message looks in MessageFormat syntax? Run [`extract`](/docs/ref/cli.md#extract) command and find out by yourself: +Let's run the [`extract`](/docs/ref/cli.md#extract) command to see the extracted message: ```icu-message-format {messagesCount, plural, - one {There's # message in your inbox} - other {There are # messages in your inbox}} + one {There's # message in your inbox} + other {There are # messages in your inbox} +} ``` -In the catalog, you'll see the message in one line. Here we wrapped it to make it more readable. - -The [`Plural`](/docs/ref/macro.mdx#plural-1) is gone and replaced with [`Trans`](/docs/ref/react.md#trans) again! The sole purpose of [`Plural`](/docs/ref/macro.mdx#plural-1) is to generate proper syntax in message. - -Things are getting a bit more complicated, but i18n is a complex process. At least we don't have to write this message manually! +In the catalog, you'll see the message in a single line. Here we have wrapped it to make it more readable. -### Beware of zeroes! +### Beware of Zeroes! -Just a short detour, because it's a common misunderstanding. - -You may wonder why the following code doesn't work as expected: +Just a short detour, because it's a common misunderstanding. You may wonder why the following code doesn't work as expected: ```jsx -<Plural - value={messagesCount} - zero="There are no messages" - one="There's # message in your inbox" - other="There are # messages in your inbox" -/> +<p> + <Plural + value={messagesCount} + zero="There are no messages" + one="There's # message in your inbox" + other="There are # messages in your inbox" + /> +</p> ``` -This component will render `There are 0 messages in your inbox` for `messagesCount = 0`. Why so? Because English doesn't have `zero` [plural form](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#en). - -Looking at [English plural rules](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#en), it's: +This component will render `There are 0 messages in your inbox` for `messagesCount = 0`. Why so? Because English doesn't have `zero` plural form. Looking at [English plural rules](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#en), it's: | N | Form | | --- | --------------------- | @@ -551,42 +514,46 @@ Looking at [English plural rules](http://www.unicode.org/cldr/charts/latest/supp | 1 | one | | n | other (anything else) | -However, decimal numbers (even `1.0`) use `other` form every time: +However, decimal numbers (even `1.0`) always use the `other` form: ```default There are 0.0 messages in your inbox. ``` -Aren't languages beautiful? - ### Exact Forms -Alright, back to our example. What if we really want to render `There are no messages` for `messagesCount = 0`? Exact forms to the rescue! +Going back to our example, what if we specifically want to display `There are no messages` when `messagesCount = 0`? This is where exact forms come in handy: -```jsx -<Plural - value={messagesCount} - _0="There are no messages" - one="There's # message in your inbox" - other="There are # messages in your inbox" -/> +```jsx {4} +<p> + <Plural + value={messagesCount} + _0="There are no messages" + one="There's # message in your inbox" + other="There are # messages in your inbox" + /> +</p> ``` -What's that `_0`? MessageFormat allows exact forms, like `=0`. However, React props can't start with `=` and can't be numbers either, so we need to write `_N` instead of `=0`. +:::tip +MessageFormat allows exact forms, like `=0`. However, React props can't start with `=` and can't be numbers either, so we need to write `_N` instead of `=0`. +::: -It works with any number, so we can go wild and customize it this way: +It works with any number, allowing for extensive customization as follows: -```jsx -<Plural - value={messagesCount} - _0="There are no messages" - _1="There's one message in your inbox" - _2="There are two messages in your inbox, that's not much!" - other="There are # messages in your inbox" -/> +```jsx {4-6} +<p> + <Plural + value={messagesCount} + _0="There are no messages" + _1="There's one message in your inbox" + _2="There are two messages in your inbox, that's not much!" + other="There are # messages in your inbox" + /> +</p> ``` -... and so on. Exact matches always take precedence before plural forms. +Exact matches always take precedence over plural forms. ### Variables and Components @@ -598,7 +565,7 @@ Let's go back to our original pluralized message: </p> ``` -What if we want to use variables or components inside messages? Easy! Either wrap messages in [`Trans`](/docs/ref/macro.mdx#trans) macro or use template literals (suppose we have a variable `name`): +To include variables or components within messages, simply wrap them in the [`Trans`](/docs/ref/macro.mdx#trans) macro or use template literals (for example, with a variable `name`): ```jsx <p> @@ -614,24 +581,7 @@ What if we want to use variables or components inside messages? Easy! Either wra </p> ``` -We can use nested macros, components, variables, expressions, really anything. - -This gives us enough flexibility for all usecases. - -### Custom Message ID - -Let's finish this with a short example of plurals with custom ID. We can pass an `id` prop to [`Plural`](/docs/ref/macro.mdx#plural-1) as we would to [`Trans`](/docs/ref/macro.mdx#trans): - -```jsx -<p> - <Plural - id="Inbox.messagesCount" - value={messagesCount} - one="There's # message in your inbox" - other="There are # messages in your inbox" - /> -</p> -``` +Nested macros, components, variables, and expressions are all supported, providing the flexibility needed for any use case. ## Formats @@ -641,18 +591,18 @@ The last message in our component is again a bit specific: <footer>Last login on {lastLogin.toLocaleDateString()}.</footer> ``` -`lastLogin` is a date object, and we need to format it properly. Dates are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [Intl object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use [`i18n.date()`](/docs/ref/core.md#i18n.date) function. The `i18n` object can be accessed by [`useLingui`](/docs/ref/react.md#uselingui) hook: +`lastLogin` is a date object, and we need to format it properly. Dates are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use the [`i18n.date()`](/docs/ref/core.md#i18n.date) function. -```jsx title="src/Inbox.js" +The `i18n` object can be accessed with the [`useLingui`](/docs/ref/react.md#uselingui) hook: + +```jsx title="src/Inbox.js" {4,9} import { useLingui } from "@lingui/react"; export default function Inbox() { const { i18n } = useLingui(); - // ... return ( <div> - {/* ... */} <footer> <Trans>Last login on {i18n.date(lastLogin)}.</Trans> </footer> @@ -665,7 +615,7 @@ This will format the date using the conventional format for the active language. ## Review -After all modifications, the final component with i18n looks like this: +After all modifications, the final i18n-ready component looks like this: ```jsx title="src/Inbox.js" import React from "react"; @@ -710,11 +660,11 @@ export default function Inbox() { } ``` -That's all for this tutorial! Checkout the reference documentation or various guides in the documentation for more info and happy internationalizing! +That's it for this tutorial! For more details, see the reference documentation or check out additional tutorials. Happy Internationalizing! -## Further reading +## See Also - [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) -- [`@lingui/react` Reference Documentation](/docs/ref/react.md) -- [CLI Reference](/docs/ref/cli.md) +- [`@lingui/react` Reference](/docs/ref/react.md) +- [`@lingui/cli` Reference](/docs/ref/cli.md) - [Pluralization Guide](/docs/guides/plurals.md) From 68574ad14fe9116e5e6e38f12d5a06b7be1803e5 Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Tue, 29 Oct 2024 17:53:43 +0200 Subject: [PATCH 2/6] wip [skip ci] --- website/docs/guides/lazy-translations.md | 67 ++++++++ website/docs/ref/macro.mdx | 51 +++++++ website/docs/tutorials/react-patterns.md | 185 ----------------------- website/docs/tutorials/react-rsc.md | 2 +- website/docs/tutorials/react.md | 104 +++++++------ website/sidebars.ts | 5 + 6 files changed, 181 insertions(+), 233 deletions(-) create mode 100644 website/docs/guides/lazy-translations.md diff --git a/website/docs/guides/lazy-translations.md b/website/docs/guides/lazy-translations.md new file mode 100644 index 000000000..89adab461 --- /dev/null +++ b/website/docs/guides/lazy-translations.md @@ -0,0 +1,67 @@ +--- +title: Lazy Translations +description: Lazy translations allow you to defer translation of a message until it is actually displayed. +--- + +# Lazy Translations + +You don't need to declare messages at the same code location where they are displayed. Tag a string with the [`msg`](/docs/ref/macro.mdx#definemessage) macro, and you've created a "message descriptor", which can then be passed around as a variable, and can be displayed as a translated string by passing its `id` to [`Trans`](/docs/ref/macro.mdx#trans) as its `id` prop: + +```jsx +import { msg } from "@lingui/core/macro"; +import { Trans } from "@lingui/react"; + +const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; + +export default function ColorList() { + return ( + <ul> + {favoriteColors.map((color) => ( + <li> + <Trans id={color.id} /> + </li> + ))} + </ul> + ); +} +``` + +:::note +Note that we import `<Trans>` component from `@lingui/react`, because we want to use the runtime `Trans` component here, not the (compile-time) macro. +::: + +To render the message descriptor as a string-only translation, pass it to the [`i18n._()`](/docs/ref/core.md#i18n._) method: + +```jsx +import { i18n } from "@lingui/core"; +import { msg } from "@lingui/core/macro"; + +const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; + +export function getTranslatedColorNames() { + return favoriteColors.map((color) => i18n._(color)); +} +``` + +### Picking a message based on a variable + +Sometimes you need to pick between different messages to display, depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message representing the current status. + +A simple way to do this is to create an object that maps the possible values of "status" to message descriptors (tagged with the [`msg`](/docs/ref/macro.mdx#definemessage) macro), and render them as needed with deferred translation: + +```jsx +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react"; + +const statusMessages = { + ["STATUS_OPEN"]: msg`Open`, + ["STATUS_CLOSED"]: msg`Closed`, + ["STATUS_CANCELLED"]: msg`Cancelled`, + ["STATUS_COMPLETED"]: msg`Completed`, +}; + +export default function StatusDisplay({ statusCode }) { + const { _ } = useLingui(); + return <div>{_(statusMessages[statusCode])}</div>; +} +``` diff --git a/website/docs/ref/macro.mdx b/website/docs/ref/macro.mdx index f9d7f3ffc..d320e10ba 100644 --- a/website/docs/ref/macro.mdx +++ b/website/docs/ref/macro.mdx @@ -791,6 +791,57 @@ function MyComponent() { The `useLingui` React macro is available from **Lingui v5**. ::: +## Important Notes + +### Using Macros + +All Core Macros cannot be used at the module level: + +```jsx +import { t } from "@lingui/core/macro"; + +// ❌ Bad! This won't work because the `t` macro is used at the module level. +// The `t` macro returns a string, and once this string is assigned, it won't react to locale changes. +const colors = [t`Red`, t`Orange`, t`Yellow`, t`Green`]; + +// ✅ Good! Every time the function is executed, the `t` macro will be re-executed as well, +// and the correctly translated color labels will be returned. +function getColors() { + return [t`Red`, t`Orange`, t`Yellow`, t`Green`]; +} +``` + +:::tip +There is an [ESLint Plugin](/docs/ref/eslint-plugin.md) rule designed to check for this misuse: `t-call-in-function`. +::: + +[//]: # (TODO: link to the article) + +A better option would be to use the Lazy Translations pattern. + +### Global `i18n` Instance + +When you use the [`t`](#t) macro (or [`plural`](#plural), [`select`](#select), [`selectOrdinal`](#selectordinal)), it uses a global [`i18n`](/docs/ref/core.md#i18n) instance. While this generally works, there are situations, such as server-side rendering (SSR) applications, where it may not be the best solution. + +For better control and flexibility, it's a good idea to avoid the global `i18n` instance and instead use a specific instance tailored to your needs: + +```js +import { msg } from "@lingui/core/macro"; +import { useLingui } from "@lingui/react/macro"; + +export function showAlert(i18n) { + alert(i18n._(msg`...`)); +} + +function MyComponent() { + // Get i18n instance from React Context + const { i18n } = useLingui(); + + // Pass the instance from outside + showAlert(i18n); +} +``` + ## More Examples ### Examples of JS macros diff --git a/website/docs/tutorials/react-patterns.md b/website/docs/tutorials/react-patterns.md index 57ee3cd0b..4215db592 100644 --- a/website/docs/tutorials/react-patterns.md +++ b/website/docs/tutorials/react-patterns.md @@ -5,191 +5,6 @@ description: Learn about the most common i18n patterns in React and how to use t # Common i18n Patterns in React -This page describes the most common i18n patterns in React. It's a follow-up to the [tutorial](/docs/tutorials/react.md) with practical examples. See the [API reference](/docs/ref/react.md) for detailed information about all components. - -## Macros - -Using jsx macros is the most straightforward way to translate your React components. - -[`Trans`](/docs/ref/macro.mdx#trans) handles translations of messages including variables and other React components: - -```jsx -import { Trans } from "@lingui/react/macro"; - -function render() { - return ( - <> - <h1> - <Trans>LinguiJS example</Trans> - </h1> - <p> - <Trans> - Hello <a href="/profile">{name}</a>. - </Trans> - </p> - </> - ); -} -``` - -You don't need anything special to use [`Trans`](/docs/ref/macro.mdx#trans) inside your app (except of wrapping the root component in [`I18nProvider`](/docs/ref/react.md#i18nprovider)). - -## Element attributes and string-only translations - -Sometimes you can't use [`Trans`](/docs/ref/macro.mdx#trans) component, for example when translating element attributes: - -```html -<img src="..." alt="Image caption" /> -``` - -In such case you need to use the [`useLingui()`](/docs/ref/macro.mdx#uselingui) macro: - -```jsx -import { useLingui } from "@lingui/react/macro"; - -export default function ImageWithCaption() { - const { t } = useLingui(); - - return <img src="..." alt={t`Image caption`} />; -} -``` - -If `useLingui` macro is not available in your setup you can use the [`useLingui()`](/docs/ref/react.md#uselingui) runtime hook with the [`msg`](/docs/ref/macro.mdx#definemessage) macro. - -```jsx -import { msg } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react"; - -export default function ImageWithCaption() { - const { _ } = useLingui(); - - return <img src="..." alt={_(msg`Image caption`)} />; -} -``` - -## Translations outside React components - -Sometimes, you may need to access translations outside React components, which is another common pattern. You can use [`t`](/docs/ref/macro.mdx#t) macro outside React context as usual: - -```jsx -import { t } from "@lingui/core/macro"; - -export function showAlert() { - alert(t`...`); -} -``` - -:::caution -When you use [`t`](/docs/ref/macro.mdx#t) macro (and [`plural`](/docs/ref/macro.mdx#plural), [`select`](/docs/ref/macro.mdx#select), [`selectOrdinal`](/docs/ref/macro.mdx#selectordinal)), it uses a global `i18n` instance. While this generally works, there are situations, like in server-side rendering (SSR) applications, where it may not be the best fit. - -For better control and flexibility, it's a good idea to avoid the global `i18n` instance and instead use a specific instance tailored to your needs. - -```ts -import { msg } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react/macro"; -import { i18n } from "@lingui/core"; - -export function showAlert(i18n: I18n) { - alert(i18n._(msg`...`)); -} - -function MyComponent() { - // get i18n instance from React Context - const { i18n } = useLingui(); - - // pass instance outside - showAlert(i18n); -} -``` - -Note that we import `useLingui` from `@lingui/react/macro`. There is also a runtime version of `useLingui` hook exported from `@lingui/react`. In the case above, it doesn't matter what version to choose since we use only `i18n` object which is returned by both. - -::: - -:::note -All js macros such as [`t`](/docs/ref/macro.mdx#t) [`plural`](/docs/ref/macro.mdx#plural), [`select`](/docs/ref/macro.mdx#select), [`selectOrdinal`](/docs/ref/macro.mdx#selectordinal) cannot be used on the module level. - -```jsx -import { t } from "@lingui/core/macro"; - -// ❌ Bad! This won't work because the `t` macro is used at the module level. -// The `t` macro returns a string, and once this string is assigned, it won't react to locale changes. -const colors = [t`Red`, t`Orange`, t`Yellow`, t`Green`]; - -// ✅ Good! Every time the function is executed, the `t` macro will be re-executed as well, -// and the correctly translated color labels will be returned. -function getColors() { - return [t`Red`, t`Orange`, t`Yellow`, t`Green`]; -} -``` - -There is an [ESLint Rule](https://github.com/lingui/eslint-plugin#t-call-in-function) designed to check for this misuse. - -A better option would be to use the Lazy Translations pattern described in the following paragraph. -::: - -## Lazy Translations - -You don't need to declare messages at the same code location where they are displayed. Tag a string with the [`msg`](/docs/ref/macro.mdx#definemessage) macro, and you've created a "message descriptor", which can then be passed around as a variable, and can be displayed as a translated string by passing its `id` to [`Trans`](/docs/ref/macro.mdx#trans) as its `id` prop: - -```jsx -import { msg } from "@lingui/core/macro"; -import { Trans } from "@lingui/react"; - -const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; - -export default function ColorList() { - return ( - <ul> - {favoriteColors.map((color) => ( - <li> - <Trans id={color.id} /> - </li> - ))} - </ul> - ); -} -``` - -:::note -Note that we import `<Trans>` component from `@lingui/react`, because we want to use the runtime `Trans` component here, not the (compile-time) macro. -::: - -To render the message descriptor as a string-only translation, pass it to the [`i18n._()`](/docs/ref/core.md#i18n._) method: - -```jsx -import { i18n } from "@lingui/core"; -import { msg } from "@lingui/core/macro"; - -const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; - -export function getTranslatedColorNames() { - return favoriteColors.map((color) => i18n._(color)); -} -``` - -### Picking a message based on a variable - -Sometimes you need to pick between different messages to display, depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message representing the current status. - -A simple way to do this is to create an object that maps the possible values of "status" to message descriptors (tagged with the [`msg`](/docs/ref/macro.mdx#definemessage) macro), and render them as needed with deferred translation: - -```jsx -import { msg } from "@lingui/core/macro"; -import { useLingui } from "@lingui/react"; - -const statusMessages = { - ["STATUS_OPEN"]: msg`Open`, - ["STATUS_CLOSED"]: msg`Closed`, - ["STATUS_CANCELLED"]: msg`Cancelled`, - ["STATUS_COMPLETED"]: msg`Completed`, -}; - -export default function StatusDisplay({ statusCode }) { - const { _ } = useLingui(); - return <div>{_(statusMessages[statusCode])}</div>; -} -``` ## Memoization pitfall diff --git a/website/docs/tutorials/react-rsc.md b/website/docs/tutorials/react-rsc.md index e8ed9839c..654f891a4 100644 --- a/website/docs/tutorials/react-rsc.md +++ b/website/docs/tutorials/react-rsc.md @@ -21,7 +21,7 @@ After configuring the middleware, make sure your page and route files are moved ### Next.js Config -Secondly, add the `swc-plugin` to the `next.config.js`, so that you can use [Lingui macros](https://lingui.dev/ref/macro). +Secondly, add the `swc-plugin` to the `next.config.js`, so that you can use [Lingui Macros](/docs/ref/macro.mdx). ```js title="next.config.js" /** @type {import('next').NextConfig} */ diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index 2d3aa987f..421798f03 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -120,6 +120,8 @@ import { Trans } from "@lingui/react/macro"; </h1>; ``` +Using JSX Macros is the easiest way to translate your React components. It handles translations of messages, including variables and other React components. + ### Macros vs. Components If you're wondering what [Macros](/docs/ref/macro.mdx) are and the difference between macros and runtime components, here's a quick explanation. @@ -285,46 +287,9 @@ Let's go through the workflow again: It's not necessary to extract/translate messages one by one. This is usually done in batches. When you finish your work or PR, run [`extract`](/docs/ref/cli.md#extract) to generate the latest message catalogs, and before building the application for production, run [`compile`](/docs/ref/cli.md#compile). -## Non-JSX Translation - -So far we have learned how to translate strings inside a JSX element, but what if we want to translate something that is not inside a JSX? Or pass a translation as a prop to another component? - -We have this piece of code in our example: - -```js -const markAsRead = () => { - alert("Marked as read."); -}; -``` - -To translate it, we will use the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook: - -```js -import { useLingui } from "@lingui/react/macro"; - -const { t } = useLingui(); - -const markAsRead = () => { - alert(t`Marked as read.`); -}; -``` - -Now the `Marked as read.` message would be picked up by the extractor, and available for translation in the catalog. - -You could also pass variables and use any other macro in the message: - -```jsx -const { t } = useLingui(); - -const markAsRead = () => { - const userName = "User1234"; - alert(t`Hello ${userName}, your messages marked as read!`); -}; -``` - ## Formatting -Let's move on to another paragraph in our project. This paragraph has some variables, some HTML and components inside: +Let's move on to another paragraph in our project. The following paragraph has some variables, some HTML and components inside: ```jsx <p> @@ -583,15 +548,66 @@ To include variables or components within messages, simply wrap them in the [`Tr Nested macros, components, variables, and expressions are all supported, providing the flexibility needed for any use case. -## Formats +## Translations Outside React Components + +So far, we have learned how to translate strings within a JSX element. However, what if we need to translate content that is outside JSX or pass a translation as a prop to another component? + +In our example, we have the following code: + +```js +const markAsRead = () => { + alert("Marked as read."); +}; +``` + +To translate it, we will use the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook: + +```js +import { useLingui } from "@lingui/react/macro"; + +const { t } = useLingui(); + +const markAsRead = () => { + alert(t`Marked as read.`); +}; +``` + +Now the `Marked as read.` message would be picked up by the extractor, and available for translation in the catalog. + +You could also pass variables and use any other macro in the message: + +```jsx +const { t } = useLingui(); + +const markAsRead = () => { + const userName = "User1234"; + alert(t`Hello ${userName}, your messages marked as read!`); +}; +``` + +:::tip +You can also use this approach to translate element attributes, such as `alt' in an image tag: -The last message in our component is again a bit specific: +```jsx +import { useLingui } from "@lingui/react/macro"; + +export default function ImageWithCaption() { + const { t } = useLingui(); + + return <img src="..." alt={t`Image caption`} />; +} +``` +::: + +## Dates and Numbers + +The last message in our component is again a bit specific - it contains a date: ```jsx <footer>Last login on {lastLogin.toLocaleDateString()}.</footer> ``` -`lastLogin` is a date object, and we need to format it properly. Dates are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use the [`i18n.date()`](/docs/ref/core.md#i18n.date) function. +Dates (as well as numbers) are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use the [`i18n.date()`](/docs/ref/core.md#i18n.date) function. The `i18n` object can be accessed with the [`useLingui`](/docs/ref/react.md#uselingui) hook: @@ -662,9 +678,3 @@ export default function Inbox() { That's it for this tutorial! For more details, see the reference documentation or check out additional tutorials. Happy Internationalizing! -## See Also - -- [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) -- [`@lingui/react` Reference](/docs/ref/react.md) -- [`@lingui/cli` Reference](/docs/ref/cli.md) -- [Pluralization Guide](/docs/guides/plurals.md) diff --git a/website/sidebars.ts b/website/sidebars.ts index 2c6db10f0..9c1a4ac67 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -60,6 +60,11 @@ const sidebar = [ label: "Explicit vs Generated IDs", id: "guides/explicit-vs-generated-ids", }, + { + type: "doc", + label: "Lazy Translations", + id: "guides/lazy-translations", + }, { type: "doc", label: "Pseudolocalization", From 47c3a89ff432d9b0489e479e012de0b96b9c0b78 Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Wed, 30 Oct 2024 16:40:10 +0200 Subject: [PATCH 3/6] lazy translations --- vercel.json | 3 +- website/docs/guides/lazy-translations.md | 46 +++++++------ website/docs/ref/macro.mdx | 6 +- website/docs/ref/react.md | 2 +- website/docs/releases/migration-4.md | 2 +- website/docs/tutorials/react-native.md | 1 - website/docs/tutorials/react-patterns.md | 85 ------------------------ website/docs/tutorials/react-rsc.md | 3 +- website/docs/tutorials/react.md | 64 +++++++++++++++++- website/sidebars.ts | 5 -- 10 files changed, 96 insertions(+), 121 deletions(-) delete mode 100644 website/docs/tutorials/react-patterns.md diff --git a/vercel.json b/vercel.json index bde5876d2..452617946 100644 --- a/vercel.json +++ b/vercel.json @@ -10,6 +10,7 @@ { "source": "/guides/excluding-build-files", "destination": "/ref/cli#compile", "permanent": true}, { "source": "/tutorials/explicit-vs-generated-ids", "destination": "/guides/explicit-vs-generated-ids", "permanent": true}, { "source": "/tutorials/setup-vite", "destination": "/installation#vite", "permanent": true }, - { "source": "/tutorials/setup-react", "destination": "/installation", "permanent": true } + { "source": "/tutorials/setup-react", "destination": "/installation", "permanent": true }, + { "source": "/tutorials/react-patterns", "destination": "/tutorials/react", "permanent": true } ] } diff --git a/website/docs/guides/lazy-translations.md b/website/docs/guides/lazy-translations.md index 89adab461..9543cdd6c 100644 --- a/website/docs/guides/lazy-translations.md +++ b/website/docs/guides/lazy-translations.md @@ -1,11 +1,30 @@ --- title: Lazy Translations -description: Lazy translations allow you to defer translation of a message until it is actually displayed. +description: Lazy translations allow you to defer translation of a message until it is actually displayed --- # Lazy Translations -You don't need to declare messages at the same code location where they are displayed. Tag a string with the [`msg`](/docs/ref/macro.mdx#definemessage) macro, and you've created a "message descriptor", which can then be passed around as a variable, and can be displayed as a translated string by passing its `id` to [`Trans`](/docs/ref/macro.mdx#trans) as its `id` prop: +Lazy translation allows you to defer translation of a message until it's rendered, giving you flexibility in how and where you define messages in your code. With lazy translation, you can tag a string with the [`msg`](/docs/ref/macro.mdx#definemessage) macro to create a _message descriptor_ that can be saved, passed around as a variable, and rendered later. + +## Usage Example + +To render the message descriptor as a string-only translation, pass it to the [`i18n._()`](/docs/ref/core.md#i18n._) method: + +```jsx +import { msg } from "@lingui/core/macro"; +import { i18n } from "@lingui/core"; + +const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; + +export function getTranslatedColorNames() { + return favoriteColors.map((color) => i18n._(color)); +} +``` + +## Usage in React + +To render the message descriptor in a React component, pass its `id` to the [`Trans`](/docs/ref/react.md#trans) component as a value of the `id` prop: ```jsx import { msg } from "@lingui/core/macro"; @@ -26,28 +45,15 @@ export default function ColorList() { } ``` -:::note -Note that we import `<Trans>` component from `@lingui/react`, because we want to use the runtime `Trans` component here, not the (compile-time) macro. +:::info Important +Please note that we import the `<Trans>` component from `@lingui/react` to use the runtime version, as the message is already defined and we don't need the compile-time macro here. ::: -To render the message descriptor as a string-only translation, pass it to the [`i18n._()`](/docs/ref/core.md#i18n._) method: - -```jsx -import { i18n } from "@lingui/core"; -import { msg } from "@lingui/core/macro"; - -const favoriteColors = [msg`Red`, msg`Orange`, msg`Yellow`, msg`Green`]; - -export function getTranslatedColorNames() { - return favoriteColors.map((color) => i18n._(color)); -} -``` - -### Picking a message based on a variable +### Picking a Message Based on a Variable -Sometimes you need to pick between different messages to display, depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message representing the current status. +Sometimes you need to choose between different messages to display depending on the value of a variable. For example, imagine you have a numeric "status" code that comes from an API, and you need to display a message that represents the current status. -A simple way to do this is to create an object that maps the possible values of "status" to message descriptors (tagged with the [`msg`](/docs/ref/macro.mdx#definemessage) macro), and render them as needed with deferred translation: +An easy way to do this is to create an object that maps the possible values of "status" to message descriptors (tagged with the [`msg`](/docs/ref/macro.mdx#definemessage) macro) and render them as needed with deferred translation: ```jsx import { msg } from "@lingui/core/macro"; diff --git a/website/docs/ref/macro.mdx b/website/docs/ref/macro.mdx index d320e10ba..49d2c7d06 100644 --- a/website/docs/ref/macro.mdx +++ b/website/docs/ref/macro.mdx @@ -386,7 +386,7 @@ const message = t({ ### `defineMessage` / `msg` {#definemessage} -The `defineMessage` (alias: `msg`) macro allows to define a message for later use. It has the same signature as `t` and returns a `MessageDescriptor` that you can pass to `i18n._` to get a translated string at any time later. This is useful for [lazy translations](/tutorials/react-patterns#lazy-translations). +The `defineMessage` (alias: `msg`) macro allows to define a message for later use. It has the same signature as `t` and returns a `MessageDescriptor` that you can pass to `i18n._` to get a translated string at any time later. This is useful for [Lazy Translations](/guides/lazy-translations). In other words, `t` returns a translated string at the time when it's called, while `msg` returns a `MessageDescriptor` that can produce translated strings later: @@ -815,9 +815,7 @@ function getColors() { There is an [ESLint Plugin](/docs/ref/eslint-plugin.md) rule designed to check for this misuse: `t-call-in-function`. ::: -[//]: # (TODO: link to the article) - -A better option would be to use the Lazy Translations pattern. +A better option would be to use the [Lazy Translations](/guides/lazy-translations) pattern. ### Global `i18n` Instance diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index af6d1c3db..2578a36fb 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -126,7 +126,7 @@ This hook allows access to the Lingui context. It returns an object with the fol | `_` | `I18n[_]` | reference to the [`i18n._`](/ref/core#i18n._) function, explained below | | `defaultComponent` | `React.ComponentType` | the same `defaultComponent` you passed to `I18nProvider`, if provided | -Components that use `useLingui` hook will re-render when locale and / or catalogs change. However, the reference to the `i18n` object is stable and doesn't change between re-renders. This can lead to unexpected behavior with memoization (see [memoization pitfall](/tutorials/react-patterns#memoization-pitfall)). +Components that use `useLingui` hook will re-render when locale and / or catalogs change. However, the reference to the `i18n` object is stable and doesn't change between re-renders. This can lead to unexpected behavior with memoization (see [memoization pitfall](/tutorials/react#memoization-pitfall)). To alleviate the issue, `useLingui` provides the `_` function, which is the same as [`i18n._`](/ref/core#i18n._) but _its reference changes_ with each update of the Lingui context. Thanks to that, you can safely use this `_` function as a hook dependency. diff --git a/website/docs/releases/migration-4.md b/website/docs/releases/migration-4.md index fbf5b3113..cac1b431f 100644 --- a/website/docs/releases/migration-4.md +++ b/website/docs/releases/migration-4.md @@ -117,7 +117,7 @@ You will need to make some changes as this is a misuse of the library that actua Due to the changes caused by hash-based message ID feature described earlier, this approach will no longer work. -Instead, please use [recommended](/docs/tutorials/react-patterns.md#lazy-translations) pattern for such translations: +Instead, please use [recommended](/guides/lazy-translations) pattern for such translations: ```tsx import { msg } from "@lingui/macro"; diff --git a/website/docs/tutorials/react-native.md b/website/docs/tutorials/react-native.md index 2644623d3..5c4349ff2 100644 --- a/website/docs/tutorials/react-native.md +++ b/website/docs/tutorials/react-native.md @@ -241,7 +241,6 @@ The important point here is that the sentence isn't broken into pieces but remai ## See Also - [Message Extraction Guide](/docs/guides/message-extraction.md) -- [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) - [`@lingui/react` Reference](/docs/ref/react.md) - [`@lingui/cli` Reference](/docs/ref/cli.md) - [Localizing React Native apps talk from React Native EU 2022](https://www.youtube.com/live/uLicTDG5hSs?feature=share&t=7512) diff --git a/website/docs/tutorials/react-patterns.md b/website/docs/tutorials/react-patterns.md deleted file mode 100644 index 4215db592..000000000 --- a/website/docs/tutorials/react-patterns.md +++ /dev/null @@ -1,85 +0,0 @@ ---- -title: Common i18n Patterns in React -description: Learn about the most common i18n patterns in React and how to use them with Lingui ---- - -# Common i18n Patterns in React - - -## Memoization pitfall - -In the following contrived example, we document how a welcome message will or will not be updated when locale changes. - -The documented behavior may not be intuitive at first, but it is expected, because of how `useMemo` dependencies work. - -To avoid bugs with stale translations, use the `_` function returned from [`useLingui`](/ref/react#uselingui): it is safe to use with memoization because its reference changes whenever the Lingui context updates. We are open to accepting solutions to make working with the Lingui context easier. - -Keep in mind that `useMemo` is primarily a performance optimization tool in React. Because of this, there might be no need to memoize your translations. Additionally, this issue is not present when using the `Trans` component which we recommend to use when possible. - -```jsx -import { msg } from "@lingui/core/macro"; -import { i18n } from "@lingui/core"; - -const welcomeMessage = msg`Welcome!`; - -// ❌ Bad! This code won't work -export function Welcome() { - const buggyWelcome = useMemo(() => { - return i18n._(welcomeMessage); - }, []); - - return <div>{buggyWelcome}</div>; -} - -// ❌ Bad! This code won't work either because the reference to i18n does not change -export function Welcome() { - const { i18n } = useLingui(); - - const buggyWelcome = useMemo(() => { - return i18n._(welcomeMessage); - }, [i18n]); - - return <div>{buggyWelcome}</div>; -} - -// ✅ Good! `useMemo` has i18n context in the dependency -export function Welcome() { - const linguiCtx = useLingui(); - - const welcome = useMemo(() => { - return linguiCtx.i18n._(welcomeMessage); - }, [linguiCtx]); - - return <div>{welcome}</div>; -} - -// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context -export function Welcome() { - const { _ } = useLingui(); - - const welcome = useMemo(() => { - return _(welcomeMessage); - }, [_]); - - return <div>{welcome}</div>; -} -``` - -:::note -Note on [`useLingui`](/docs/ref/macro.mdx#uselingui) macro usage. The `t` function destructured from this hook, behaves the same way as `_` from the runtime [`useLingui`](/docs/ref/react.md#uselingui) counterpart, so you can safely use it in the dependency array. - -```ts -import { useLingui } from "@lingui/react/macro"; - -export function Welcome() { - const { t } = useLingui(); - - const welcome = useMemo(() => { - return t`Welcome!`; - }, [t]); - - return <div>{welcome}</div>; -} -``` - -::: diff --git a/website/docs/tutorials/react-rsc.md b/website/docs/tutorials/react-rsc.md index 654f891a4..6b28c9c7e 100644 --- a/website/docs/tutorials/react-rsc.md +++ b/website/docs/tutorials/react-rsc.md @@ -177,9 +177,8 @@ export default function SomePage() { } ``` -Read more about [lazy translation](/docs/tutorials/react-patterns.md#translations-outside-react-components) to see how to handle translation defined on the module level. +Read more about [Lazy Translation](/docs/guides/lazy-translations.md) to see how to handle translation defined on the module level. ## See Also -- [Common i18n Patterns in React](/docs/tutorials/react-patterns.md) - [`@lingui/react` Reference](/docs/ref/react.md) diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index 421798f03..210b37f2e 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -597,6 +597,7 @@ export default function ImageWithCaption() { return <img src="..." alt={t`Image caption`} />; } ``` + ::: ## Dates and Numbers @@ -629,6 +630,68 @@ export default function Inbox() { This will format the date using the conventional format for the active language. +## Memoization Pitfall + +In the following contrived example, we document how a welcome message will or will not be updated when locale changes. The documented behavior may not be intuitive at first, but it is expected, because of the way the `useMemo` dependencies work. + +To avoid bugs with stale translations, use the `_` function returned from the [`useLingui`](/docs/ref/react.md#uselingui) hook: it is safe to use with memoization because its reference changes whenever the Lingui context updates. + +:::tip +You can also use the `t` function from the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook, which behaves the same way as `_` from the runtime [`useLingui`](/docs/ref/react.md#uselingui) counterpart. +::: + +Keep in mind that `useMemo` is primarily a performance optimization tool in React. Because of this, there might be no need to memoize your translations. Additionally, this issue is not present when using the `Trans` component, which we recommend using whenever possible. + +```jsx +import { msg } from "@lingui/core/macro"; +import { i18n } from "@lingui/core"; +import { useLingui } from "@lingui/react"; + +const welcomeMessage = msg`Welcome!`; + +// ❌ Bad! This code won't work +export function Welcome() { + const buggyWelcome = useMemo(() => { + return i18n._(welcomeMessage); + }, []); + + return <div>{buggyWelcome}</div>; +} + +// ❌ Bad! This code won't work either because the reference to i18n does not change +export function Welcome() { + const { i18n } = useLingui(); + + const buggyWelcome = useMemo(() => { + return i18n._(welcomeMessage); + }, [i18n]); + + return <div>{buggyWelcome}</div>; +} + +// ✅ Good! `useMemo` has i18n context in the dependency +export function Welcome() { + const linguiCtx = useLingui(); + + const welcome = useMemo(() => { + return linguiCtx.i18n._(welcomeMessage); + }, [linguiCtx]); + + return <div>{welcome}</div>; +} + +// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context +export function Welcome() { + const { _ } = useLingui(); + + const welcome = useMemo(() => { + return _(welcomeMessage); + }, [_]); + + return <div>{welcome}</div>; +} +``` + ## Review After all modifications, the final i18n-ready component looks like this: @@ -677,4 +740,3 @@ export default function Inbox() { ``` That's it for this tutorial! For more details, see the reference documentation or check out additional tutorials. Happy Internationalizing! - diff --git a/website/sidebars.ts b/website/sidebars.ts index 9c1a4ac67..6b38041d6 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -34,11 +34,6 @@ const sidebar = [ label: "React Native", id: "tutorials/react-native", }, - { - type: "doc", - label: "React - Common Patterns", - id: "tutorials/react-patterns", - }, { type: "doc", label: "JavaScript", From 8ca3f15aaeba9d668cb288537b01d3fbc99971de Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Wed, 30 Oct 2024 17:28:34 +0200 Subject: [PATCH 4/6] fix --- website/docs/tutorials/react-native.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/tutorials/react-native.md b/website/docs/tutorials/react-native.md index 5c4349ff2..528b35a67 100644 --- a/website/docs/tutorials/react-native.md +++ b/website/docs/tutorials/react-native.md @@ -94,7 +94,7 @@ As you can see, it's a simple mailbox application with only one screen. ## Internationalization in React (Native) :::tip TL;DR -There are several ways to render translations: You may use the [`Trans`](/docs/ref/macro.mdx#trans) macro or the [`useLingui`](/docs/ref/macro.mdx#uselingui) hook together with the [`t`](/docs/ref/macro.mdx#t) or [`msg`](/docs/ref/macro.mdx#definemessage) macros. When you change the active locale or load new messages, all components that consume the Lingui context provided by [`I18nProvider`](/docs/ref/react.md#i18nprovider) will re-render, making sure the UI shows the correct translations. +There are several ways to render translations: You may use the [`Trans`](/docs/ref/react.md#trans) component or the [`useLingui`](/docs/ref/react.md#uselingui) hook together with the [`t`](/docs/ref/macro.mdx#t) or [`msg`](/docs/ref/macro.mdx#definemessage) macros. When you change the active locale or load new messages, all components that consume the Lingui context provided by [`I18nProvider`](/docs/ref/react.md#i18nprovider) will re-render, making sure the UI shows the correct translations. ::: Not surprisingly, this part isn't too different from the [React tutorial](/docs/tutorials/react.md). From e5ce947a56021d8019ce18ca25c4352b364b77e6 Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Thu, 31 Oct 2024 11:12:01 +0200 Subject: [PATCH 5/6] rearrangement --- website/docs/guides/lazy-translations.md | 62 +++++++++++ website/docs/ref/react.md | 2 +- website/docs/tutorials/react.md | 126 ++++++----------------- 3 files changed, 95 insertions(+), 95 deletions(-) diff --git a/website/docs/guides/lazy-translations.md b/website/docs/guides/lazy-translations.md index 9543cdd6c..9c9b6e337 100644 --- a/website/docs/guides/lazy-translations.md +++ b/website/docs/guides/lazy-translations.md @@ -71,3 +71,65 @@ export default function StatusDisplay({ statusCode }) { return <div>{_(statusMessages[statusCode])}</div>; } ``` + +### Memoization Pitfall + +In the following contrived example, we document how a welcome message will or will not be updated when locale changes. The documented behavior may not be intuitive at first, but it is expected, because of the way the `useMemo` dependencies work. + +To avoid bugs with stale translations, use the `_` function returned from the [`useLingui`](/docs/ref/react.md#uselingui) hook: it is safe to use with memoization because its reference changes whenever the Lingui context updates. + +:::tip +You can also use the `t` function from the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook, which behaves the same way as `_` from the runtime [`useLingui`](/docs/ref/react.md#uselingui) counterpart. +::: + +Keep in mind that `useMemo` is primarily a performance optimization tool in React. Because of this, there might be no need to memoize your translations. Additionally, this issue is not present when using the `Trans` component, which we recommend using whenever possible. + +```jsx +import { msg } from "@lingui/core/macro"; +import { i18n } from "@lingui/core"; +import { useLingui } from "@lingui/react"; + +const welcomeMessage = msg`Welcome!`; + +// ❌ Bad! This code won't work +export function Welcome() { + const buggyWelcome = useMemo(() => { + return i18n._(welcomeMessage); + }, []); + + return <div>{buggyWelcome}</div>; +} + +// ❌ Bad! This code won't work either because the reference to i18n does not change +export function Welcome() { + const { i18n } = useLingui(); + + const buggyWelcome = useMemo(() => { + return i18n._(welcomeMessage); + }, [i18n]); + + return <div>{buggyWelcome}</div>; +} + +// ✅ Good! `useMemo` has i18n context in the dependency +export function Welcome() { + const linguiCtx = useLingui(); + + const welcome = useMemo(() => { + return linguiCtx.i18n._(welcomeMessage); + }, [linguiCtx]); + + return <div>{welcome}</div>; +} + +// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context +export function Welcome() { + const { _ } = useLingui(); + + const welcome = useMemo(() => { + return _(welcomeMessage); + }, [_]); + + return <div>{welcome}</div>; +} +``` diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 2578a36fb..d35e26194 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -126,7 +126,7 @@ This hook allows access to the Lingui context. It returns an object with the fol | `_` | `I18n[_]` | reference to the [`i18n._`](/ref/core#i18n._) function, explained below | | `defaultComponent` | `React.ComponentType` | the same `defaultComponent` you passed to `I18nProvider`, if provided | -Components that use `useLingui` hook will re-render when locale and / or catalogs change. However, the reference to the `i18n` object is stable and doesn't change between re-renders. This can lead to unexpected behavior with memoization (see [memoization pitfall](/tutorials/react#memoization-pitfall)). +Components that use `useLingui` hook will re-render when locale and / or catalogs change. However, the reference to the `i18n` object is stable and doesn't change between re-renders. This can lead to unexpected behavior with memoization (see [memoization pitfall](/guides/lazy-translations#memoization-pitfall)). To alleviate the issue, `useLingui` provides the `_` function, which is the same as [`i18n._`](/ref/core#i18n._) but _its reference changes_ with each update of the Lingui context. Thanks to that, you can safely use this `_` function as a hook dependency. diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index 210b37f2e..df887fcac 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -8,7 +8,7 @@ description: Learn how to add internationalization to a React application using In this tutorial, we'll learn how to add internationalization (i18n) to an existing React JS application. We'll focus on the most common patterns and best practices for using Lingui in React. :::tip Example -If you're looking for a working solution, check out the [Examples page](/docs/misc/examples.md). It contains several sample projects with the complete setup using Lingui and React. +If you're looking for a working solution, check out the [Examples](/docs/misc/examples.md) page. It contains several sample projects with the complete setup using Lingui and React. It includes examples for _Create React App_, _React with Vite and Babel_, _React with Vite and SWC_, and more. ::: @@ -370,6 +370,36 @@ Any expressions are allowed, not just simple variables. The only difference is, Try to keep your messages simple and avoid complex expressions. During extraction, these expressions will be replaced by placeholders, resulting in a lack of context for translators. There is also a special rule in Lingui [ESLint Plugin](/docs/ref/eslint-plugin.md) to catch these cases: `no-expression-in-message`. ::: +### Dates and Numbers + +Take a look at the message in the footer of our component. It is a bit special because it contains a date: + +```jsx +<footer>Last login on {lastLogin.toLocaleDateString()}.</footer> +``` + +Dates (as well as numbers) are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use the [`i18n.date()`](/docs/ref/core.md#i18n.date) function. + +The `i18n` object can be accessed with the [`useLingui`](/docs/ref/react.md#uselingui) hook: + +```jsx title="src/Inbox.js" {4,9} +import { useLingui } from "@lingui/react"; + +export default function Inbox() { + const { i18n } = useLingui(); + + return ( + <div> + <footer> + <Trans>Last login on {i18n.date(lastLogin)}.</Trans> + </footer> + </div> + ); +} +``` + +This will format the date using the conventional format for the active language. To format numbers, use the [`i18n.number()`](/docs/ref/core.md#i18n.number) function. + ### Message ID At this point, we'll explain what the message ID is and how to set it manually. Translators work with _message catalogs_. No matter what format we use, it's just a mapping of a message ID to the translation. @@ -403,7 +433,7 @@ Refer to the [Explicit vs Generated IDs](/docs/guides/explicit-vs-generated-ids. ## Plurals -Let's move on and add i18n to another text in our component: +Let's take a closer look at the following code in our component: ```jsx <p> @@ -600,98 +630,6 @@ export default function ImageWithCaption() { ::: -## Dates and Numbers - -The last message in our component is again a bit specific - it contains a date: - -```jsx -<footer>Last login on {lastLogin.toLocaleDateString()}.</footer> -``` - -Dates (as well as numbers) are formatted differently in different languages, but we don't have to do this manually. The heavy lifting is done by the [`Intl` object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl), we'll just use the [`i18n.date()`](/docs/ref/core.md#i18n.date) function. - -The `i18n` object can be accessed with the [`useLingui`](/docs/ref/react.md#uselingui) hook: - -```jsx title="src/Inbox.js" {4,9} -import { useLingui } from "@lingui/react"; - -export default function Inbox() { - const { i18n } = useLingui(); - - return ( - <div> - <footer> - <Trans>Last login on {i18n.date(lastLogin)}.</Trans> - </footer> - </div> - ); -} -``` - -This will format the date using the conventional format for the active language. - -## Memoization Pitfall - -In the following contrived example, we document how a welcome message will or will not be updated when locale changes. The documented behavior may not be intuitive at first, but it is expected, because of the way the `useMemo` dependencies work. - -To avoid bugs with stale translations, use the `_` function returned from the [`useLingui`](/docs/ref/react.md#uselingui) hook: it is safe to use with memoization because its reference changes whenever the Lingui context updates. - -:::tip -You can also use the `t` function from the [`useLingui`](/docs/ref/macro.mdx#uselingui) macro hook, which behaves the same way as `_` from the runtime [`useLingui`](/docs/ref/react.md#uselingui) counterpart. -::: - -Keep in mind that `useMemo` is primarily a performance optimization tool in React. Because of this, there might be no need to memoize your translations. Additionally, this issue is not present when using the `Trans` component, which we recommend using whenever possible. - -```jsx -import { msg } from "@lingui/core/macro"; -import { i18n } from "@lingui/core"; -import { useLingui } from "@lingui/react"; - -const welcomeMessage = msg`Welcome!`; - -// ❌ Bad! This code won't work -export function Welcome() { - const buggyWelcome = useMemo(() => { - return i18n._(welcomeMessage); - }, []); - - return <div>{buggyWelcome}</div>; -} - -// ❌ Bad! This code won't work either because the reference to i18n does not change -export function Welcome() { - const { i18n } = useLingui(); - - const buggyWelcome = useMemo(() => { - return i18n._(welcomeMessage); - }, [i18n]); - - return <div>{buggyWelcome}</div>; -} - -// ✅ Good! `useMemo` has i18n context in the dependency -export function Welcome() { - const linguiCtx = useLingui(); - - const welcome = useMemo(() => { - return linguiCtx.i18n._(welcomeMessage); - }, [linguiCtx]); - - return <div>{welcome}</div>; -} - -// 🤩 Better! `useMemo` consumes the `_` function from the Lingui context -export function Welcome() { - const { _ } = useLingui(); - - const welcome = useMemo(() => { - return _(welcomeMessage); - }, [_]); - - return <div>{welcome}</div>; -} -``` - ## Review After all modifications, the final i18n-ready component looks like this: From 337ec3b2f6fb8499032c7fd8e9065d00c7b61cef Mon Sep 17 00:00:00 2001 From: Andrii Bodnar <andrii.bodnar@crowdin.com> Date: Thu, 31 Oct 2024 16:09:49 +0200 Subject: [PATCH 6/6] improvements --- website/docs/installation.mdx | 2 +- website/docs/tutorials/javascript.md | 7 +++++- website/docs/tutorials/react.md | 33 ++++++++++++++++++++-------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/website/docs/installation.mdx b/website/docs/installation.mdx index 0ce2b2739..6524588f0 100644 --- a/website/docs/installation.mdx +++ b/website/docs/installation.mdx @@ -18,7 +18,7 @@ Learn how to install Lingui in your project, whether you use JavaScript, React ( - Install [Lingui CLI](/docs/ref/cli.md) to manage your translations and catalogs. :::tip -Don't miss the [Lingui ESLint Plugin](/docs/ref/eslint-plugin.md) which can help you find and prevent common l10n mistakes in your code. +Don't miss the [Lingui ESLint Plugin](/docs/ref/eslint-plugin.md) which can help you find and prevent common i18n mistakes in your code. ::: ## Choosing a Transpiler diff --git a/website/docs/tutorials/javascript.md b/website/docs/tutorials/javascript.md index c84180d24..09fcd00f9 100644 --- a/website/docs/tutorials/javascript.md +++ b/website/docs/tutorials/javascript.md @@ -28,7 +28,11 @@ i18n.load("en", messages); i18n.activate("en"); ``` -The `messages.js` is generated by the Lingui CLI and contains compiled message catalogs. Alternatively, you can load catalogs dynamically using the [`@lingui/loader`](/docs/ref/loader.md) or [`@lingui/vite-plugin`](/docs/ref/vite-plugin.md). +The `messages.js` is generated by the Lingui CLI and contains compiled message catalogs. + +:::tip +Alternatively, you can load catalogs dynamically using the [`@lingui/loader`](/docs/ref/loader.md) or [`@lingui/vite-plugin`](/docs/ref/vite-plugin.md) without the need to import compiled messages manually. +::: ## Localizing Your App @@ -84,4 +88,5 @@ select(gender, { - [Message Extraction Guide](/docs/guides/message-extraction.md) - [Pluralization Guide](/docs/guides/plurals.md) +- [Dynamic Loading of Message Catalogs](/docs/guides/dynamic-loading-catalogs.md) - [`@lingui/cli` Reference](/docs/ref/cli.md) diff --git a/website/docs/tutorials/react.md b/website/docs/tutorials/react.md index df887fcac..5839ed97f 100644 --- a/website/docs/tutorials/react.md +++ b/website/docs/tutorials/react.md @@ -18,18 +18,23 @@ It includes examples for _Create React App_, _React with Vite and Babel_, _React 1. Follow the [Installation and Setup](/docs/installation.mdx) page for initial setup. 2. Install the [`@lingui/core`](/docs/ref/core.md) and [`@lingui/react`](/docs/ref/react.md) packages. -## Let's Start +## Example Component We're going to translate the following one-page mailbox application: ```jsx title="src/index.js" import React from "react"; -import { render } from "react-dom"; +import ReactDOM from "react-dom/client"; import Inbox from "./Inbox"; const App = () => <Inbox />; -render(<App />, document.getElementById("root")); +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + <React.StrictMode> + <App /> + </React.StrictMode> +); ``` ```jsx title="src/Inbox.js" @@ -77,7 +82,7 @@ Let's add all required imports and wrap our app inside [`I18nProvider`](/docs/re ```jsx title="src/index.js" import React from "react"; -import { render } from "react-dom"; +import ReactDOM from "react-dom/client"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; @@ -93,7 +98,12 @@ const App = () => ( </I18nProvider> ); -render(<App />, document.getElementById("root")); +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + <React.StrictMode> + <App /> + </React.StrictMode> +); ``` :::info @@ -244,7 +254,7 @@ Let's load this file into our app and set active language to `cs`: ```jsx title="src/index.js" {6-7,10-14} import React from "react"; -import { render } from "react-dom"; +import ReactDOM from "react-dom/client"; import { i18n } from "@lingui/core"; import { I18nProvider } from "@lingui/react"; @@ -264,7 +274,12 @@ const App = () => ( </I18nProvider> ); -render(<App />, document.getElementById("root")); +const root = ReactDOM.createRoot(document.getElementById("root")); +root.render( + <React.StrictMode> + <App /> + </React.StrictMode> +); ``` When we run the app, we see the inbox header is translated into Czech. @@ -578,7 +593,7 @@ To include variables or components within messages, simply wrap them in the [`Tr Nested macros, components, variables, and expressions are all supported, providing the flexibility needed for any use case. -## Translations Outside React Components +## Internationalization Outside of React So far, we have learned how to translate strings within a JSX element. However, what if we need to translate content that is outside JSX or pass a translation as a prop to another component? @@ -616,7 +631,7 @@ const markAsRead = () => { ``` :::tip -You can also use this approach to translate element attributes, such as `alt' in an image tag: +You can also use this approach to translate element attributes, such as `alt` in an `img` tag: ```jsx import { useLingui } from "@lingui/react/macro";