From bb2d99ad95581b31e772978414e2a488bc7f2421 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sat, 11 Mar 2023 11:51:00 +0100 Subject: [PATCH 01/17] fix: do not unmount children of I18nProvider --- packages/react/src/I18nProvider.test.tsx | 145 +++++++++++++++++------ packages/react/src/I18nProvider.tsx | 42 +++---- 2 files changed, 122 insertions(+), 65 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index d1f2d9c0b..39cfa49cd 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -3,8 +3,6 @@ import { act, render } from "@testing-library/react" import { I18nProvider, useLingui } from "./I18nProvider" import { setupI18n } from "@lingui/core" -// eslint-disable-next-line import/no-extraneous-dependencies -import { mockConsole } from "@lingui/jest-mocks" describe("I18nProvider", () => { it("should pass i18n context to wrapped component", () => { @@ -78,49 +76,120 @@ describe("I18nProvider", () => { expect(unsubscribe).toBeCalled() }) - it("should re-render on locale changes", async () => { - expect.assertions(4) - - const i18n = setupI18n({ - messages: { en: {} }, - }) - - const CurrentLocale = () => { - return {i18n.locale} + test.each([ + { + forceRenderOnLocaleChange: false, + expectedTextContent: "", + expectedRenderCount: 1, + }, + { + forceRenderOnLocaleChange: true, + expectedTextContent: "cs", + expectedRenderCount: 3, // initial render, load messages, activate locale + }, + ])( + "A component that is not consuming i18n context will not re-render on locale change." + + "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", + ({ + forceRenderOnLocaleChange, + expectedTextContent, + expectedRenderCount, + }) => { + expect.assertions(6) + + const i18n = setupI18n() + let staticRenderCount = 0, + dynamicRenderCount = 0 + + const CurrentLocaleStatic = () => { + staticRenderCount++ + return {i18n.locale} + } + const CurrentLocaleContextConsumer = () => { + const { i18n } = useLingui() + dynamicRenderCount++ + return {i18n.locale} + } + + const { container, getByTestId } = render( + + <> + + + + + ) + // First render — no output, because locale isn't activated + expect(container.textContent).toEqual("") + + act(() => { + i18n.load("en", {}) + }) + // Again, no output. Catalog is loaded, but locale + // still isn't activated. + expect(container.textContent).toEqual("") + + act(() => { + i18n.load("cs", {}) + i18n.activate("cs") + }) + // After loading and activating locale, only CurrentLocaleContextConsumer re-rendered. + expect(getByTestId("static").textContent).toBe("") + expect(getByTestId("dynamic").textContent).toBe(expectedTextContent) + expect(staticRenderCount).toBe(1) + expect(dynamicRenderCount).toBe(expectedRenderCount) } - - let container: HTMLElement - - mockConsole((console) => { - const res = render( + ) + + it( + "given 'en' locale, if activate('cs') call happens before i18n.on-change subscription in useEffect(), " + + "I18nProvider detects that it's stale and re-renders with the 'cs' locale value", + () => { + const i18n = setupI18n({ + locale: "en", + messages: { en: {} }, + }) + let renderCount = 0 + + const CurrentLocaleContextConsumer = () => { + const { i18n } = useLingui() + renderCount++ + return {i18n.locale} + } + /** + * Note that we're doing exactly what the description says: + * but to simulate the equivalent situation, we pass our own mock subscriber + * to i18n.on("change", ...) and then we call i18n.activate("cs") ourselves + * so that the condition in useEffect() is met and the component re-renders + * */ + + const mockSubscriber = jest.fn(() => { + i18n.load("cs", {}) + i18n.activate("cs") + return () => { + // unsubscriber - noop to make TS happy + } + }) + jest.spyOn(i18n, "on").mockImplementation(mockSubscriber) + + const { getByTestId } = render( - + ) - container = res.container - expect(console.log.mock.calls[0][0]).toMatchInlineSnapshot( - `"I18nProvider did not render. A call to i18n.activate still needs to happen or forceRenderOnLocaleChange must be set to false."` + expect(mockSubscriber).toHaveBeenCalledWith( + "change", + expect.any(Function) ) - }) - // First render — no output, because locale isn't activated - expect(container.textContent).toEqual("") - - act(() => { - i18n.load("en", {}) - }) - // Again, no output. Catalog is loaded, but locale - // still isn't activated. - expect(container.textContent).toEqual("") - - act(() => { - i18n.load("cs", {}) - i18n.activate("cs") - }) - // After loading and activating locale, it's finally rendered. - expect(container.textContent).toEqual("cs") - }) + expect(getByTestId("child").textContent).toBe("cs") + expect(renderCount).toBe(2) + } + ) it("should render children", () => { const i18n = setupI18n({ diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx index cc5b96f22..dd1d68008 100644 --- a/packages/react/src/I18nProvider.tsx +++ b/packages/react/src/I18nProvider.tsx @@ -32,10 +32,11 @@ export const I18nProvider: FunctionComponent = ({ forceRenderOnLocaleChange = true, children, }) => { + const firstKnownLocale = React.useRef(i18n.locale) /** * We can't pass `i18n` object directly through context, because even when locale * or messages are changed, i18n object is still the same. Context provider compares - * reference identity and suggested workaround is create a wrapper object every time + * reference identity and suggested workaround is to create a wrapper object every time * we need to trigger re-render. See https://reactjs.org/docs/context.html#caveats. * * Due to this effect we also pass `defaultComponent` in the same context, instead @@ -47,14 +48,8 @@ export const I18nProvider: FunctionComponent = ({ i18n, defaultComponent, }) - const getRenderKey = () => { - return ( - forceRenderOnLocaleChange ? i18n.locale || "default" : "default" - ) as string - } - const [context, setContext] = React.useState(makeContext()), - [renderKey, setRenderKey] = React.useState(getRenderKey()) + const [context, setContext] = React.useState(makeContext()) /** * Subscribe for locale/message changes @@ -62,33 +57,26 @@ export const I18nProvider: FunctionComponent = ({ * I18n object from `@lingui/core` is the single source of truth for all i18n related * data (active locale, catalogs). When new messages are loaded or locale is changed * we need to trigger re-rendering of LinguiContext.Consumers. - * - * We call `setContext(makeContext())` after adding the observer in case the `change` - * event would already have fired between the inital renderKey calculation and the - * `useEffect` hook being called. This can happen if locales are loaded/activated - * async. */ React.useEffect(() => { + if (!forceRenderOnLocaleChange) { + return + } const unsubscribe = i18n.on("change", () => { setContext(makeContext()) - setRenderKey(getRenderKey()) }) - if (renderKey === "default") { - setRenderKey(getRenderKey()) - } - if (forceRenderOnLocaleChange && renderKey === "default") { - console.log( - "I18nProvider did not render. A call to i18n.activate still needs to happen or forceRenderOnLocaleChange must be set to false." - ) + + /** + * unlikely, but if the locale changes before the onChange listener + * was added, we need to trigger a rerender + * */ + if (firstKnownLocale.current !== i18n.locale) { + setContext(makeContext()) } - return () => unsubscribe() + return unsubscribe }, []) - if (forceRenderOnLocaleChange && renderKey === "default") return null - return ( - - {children} - + {children} ) } From b82334cf0db3e2a18c3c46fd139ee212384be457 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 12 Mar 2023 14:32:49 +0100 Subject: [PATCH 02/17] docs: document I18nProvider changes --- website/docs/releases/migration-4.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/website/docs/releases/migration-4.md b/website/docs/releases/migration-4.md index 777098150..e8247ae57 100644 --- a/website/docs/releases/migration-4.md +++ b/website/docs/releases/migration-4.md @@ -21,6 +21,16 @@ module.exports = { } ``` +### I18nProvider no longer remounts its children on locale change + +Previously, the `I18nProvider` remounted its children on locale change. This ensured that the whole app was re-rendered with the new locale and all strings were rendered correctly translated. +Apart from not being very performant, this approach had the drawback that the state of the app was lost - all components were re-mounted and their state was reset. This is not a standard behavior of React Context providers and could cause some confusion. + +In v4, the `I18nProvider` no longer remounts its children on locale change. Instead, when locale changes, the context value provided by `I18nProvider` is updated and all components that consume the provided React Context are re-rendered with the new locale. +This includes components provided by Lingui, such as `Trans` or `Plural` and also custom components that use the `useLingui` hook. + +This is the default behavior, but you can disable it by setting `forceRenderOnLocaleChange={false}`. + ### Hash-based message ID generation and Context feature The previous implementation had a flaw: there is an original message in the bundle at least 2 times + 1 translation. From c2877c3a6e91f7a9ab5d10a58cf42f4725282cdd Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 12 Mar 2023 20:06:33 +0100 Subject: [PATCH 03/17] feat: code updates --- packages/react/src/I18nProvider.test.tsx | 54 +++++++++++++++--------- packages/react/src/I18nProvider.tsx | 27 +++++++----- 2 files changed, 51 insertions(+), 30 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index 39cfa49cd..1c2369324 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -76,24 +76,27 @@ describe("I18nProvider", () => { expect(unsubscribe).toBeCalled() }) - test.each([ + it.each([ { forceRenderOnLocaleChange: false, - expectedTextContent: "", - expectedRenderCount: 1, + textContentBeforeActivate: "1_2_", + textContentStaticAfterActivate: "1_", + textContentDynamicAfterActivate: "2_", }, { forceRenderOnLocaleChange: true, - expectedTextContent: "cs", - expectedRenderCount: 3, // initial render, load messages, activate locale + textContentBeforeActivate: "", + textContentStaticAfterActivate: "1_cs", + textContentDynamicAfterActivate: "2_cs", }, ])( - "A component that is not consuming i18n context will not re-render on locale change." + - "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", + "A component that is not consuming i18n context will not re-render on locale change.", + // "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", ({ forceRenderOnLocaleChange, - expectedTextContent, - expectedRenderCount, + textContentBeforeActivate, + textContentStaticAfterActivate, + textContentDynamicAfterActivate, }) => { expect.assertions(6) @@ -103,15 +106,15 @@ describe("I18nProvider", () => { const CurrentLocaleStatic = () => { staticRenderCount++ - return {i18n.locale} + return 1_{i18n.locale} } const CurrentLocaleContextConsumer = () => { const { i18n } = useLingui() dynamicRenderCount++ - return {i18n.locale} + return 2_{i18n.locale} } - const { container, getByTestId } = render( + const { container, getByTestId, debug } = render( { ) - // First render — no output, because locale isn't activated - expect(container.textContent).toEqual("") + debug() + // First render — locale isn't activated + expect(container.textContent).toEqual(textContentBeforeActivate) act(() => { i18n.load("en", {}) }) - // Again, no output. Catalog is loaded, but locale - // still isn't activated. - expect(container.textContent).toEqual("") + // Catalog is loaded, but locale still isn't activated. + expect(container.textContent).toEqual(textContentBeforeActivate) act(() => { i18n.load("cs", {}) i18n.activate("cs") }) + // After loading and activating locale, only CurrentLocaleContextConsumer re-rendered. - expect(getByTestId("static").textContent).toBe("") - expect(getByTestId("dynamic").textContent).toBe(expectedTextContent) + expect(getByTestId("static").textContent).toBe( + textContentStaticAfterActivate + ) + expect(getByTestId("dynamic").textContent).toBe( + textContentDynamicAfterActivate + ) expect(staticRenderCount).toBe(1) - expect(dynamicRenderCount).toBe(expectedRenderCount) + + /* + * when forceRenderOnLocaleChange is false, components are rendered only once and then do not re-rendered + * when forceRenderOnLocaleChange is true, initial render skips rendering children because no locale is active. + * Later, loading messages & locale activation are batched into 1 render. + * */ + expect(dynamicRenderCount).toBe(1) } ) diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx index dd1d68008..92c17259b 100644 --- a/packages/react/src/I18nProvider.tsx +++ b/packages/react/src/I18nProvider.tsx @@ -32,7 +32,7 @@ export const I18nProvider: FunctionComponent = ({ forceRenderOnLocaleChange = true, children, }) => { - const firstKnownLocale = React.useRef(i18n.locale) + const latestKnownLocale = React.useRef(i18n.locale) /** * We can't pass `i18n` object directly through context, because even when locale * or messages are changed, i18n object is still the same. Context provider compares @@ -44,10 +44,13 @@ export const I18nProvider: FunctionComponent = ({ * * We can't use useMemo hook either, because we want to recalculate value manually. */ - const makeContext = () => ({ - i18n, - defaultComponent, - }) + const makeContext = React.useCallback( + () => ({ + i18n, + defaultComponent, + }), + [i18n, defaultComponent] + ) const [context, setContext] = React.useState(makeContext()) @@ -62,19 +65,23 @@ export const I18nProvider: FunctionComponent = ({ if (!forceRenderOnLocaleChange) { return } - const unsubscribe = i18n.on("change", () => { + const updateContext = () => { + latestKnownLocale.current = i18n.locale setContext(makeContext()) - }) + } + const unsubscribe = i18n.on("change", updateContext) /** * unlikely, but if the locale changes before the onChange listener * was added, we need to trigger a rerender * */ - if (firstKnownLocale.current !== i18n.locale) { - setContext(makeContext()) + if (latestKnownLocale.current !== i18n.locale) { + updateContext() } return unsubscribe - }, []) + }, [makeContext, forceRenderOnLocaleChange]) + + if (forceRenderOnLocaleChange && !latestKnownLocale.current) return null return ( {children} From f97e517f29c0a7b9daa1e84a26094934635e0d05 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 12 Mar 2023 20:07:56 +0100 Subject: [PATCH 04/17] docs: I18nProvider updates --- website/docs/ref/react.md | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index b923b5e6b..ec94e4d34 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -1,6 +1,6 @@ # React API Reference -Components from `@lingui/react` wrap the vanilla JS API from `lingui-i18n`. React components handle changes of active language or interpolated variables better than low-level API and also take care of re-rendering when wrapped inside pure components. +Components from `@lingui/react` wrap the vanilla JS API from `@lingui/core`. React components handle changes of active language or interpolated variables better than low-level API and also take care of re-rendering when locale or messages change. ## General Concepts @@ -15,21 +15,19 @@ All i18n components render translation as a text without a wrapping tag. This ca Default rendering component can be set using `defaultComponent` prop in [`I18nProvider`](#i18nprovider). The main use case for this is rendering translations in `` component in React Native. -~~It's possible to pass in either a string for built-in elements (`span`, `h1`)~~, React elements or React classes. This prop has the same type as `render` and `component` prop on i18n components described below. - #### Local Configuration -| Prop name | Type | Description | -|-------------| ----------------------------------------- | -------------------------------------------- | -| `className` | string | Class name to be added to `` element | -| `render` | *Function(props) -> Element \| Component* | Custom wrapper rendered as function | -| `component` | Component, `null` | Custom wrapper element to render translation | +| Prop name | Type | Description | +|-------------| ----------------------------------------- |------------------------------------------------| +| `className` | string | Class name to be added to `` element | +| `render` | *Function(props) -> Element \| Component* | Custom wrapper rendered as function | +| `component` | Component, `null` | Custom wrapper component to render translation | `className` is used only for built-in components (when *render* is string). `Function(props)` props returns the translation, an id, and a message. -When `component` is **React.Element** ~~or **string** (built-in tags)~~, it is rendered with the `translation` passed in as its child: +`component` is rendered with the `translation` passed in as its child: ``` jsx import { Text } from "react-native"; @@ -48,7 +46,7 @@ To get more control over the rendering of translation, use instead the `render` // renders as ``` -`render` or `component` also accepts `null` value to render string without wrapping component. This can be used to override custom `defaultComponent` config. +`render` or `component` also accepts `null` value to render string without a wrapping component. This can be used to override custom `defaultComponent` config. ``` jsx Heading; @@ -99,7 +97,7 @@ It's also possible to use `Trans` component directly without macros. In that cas #### Plurals -If you cannot use [@lingui/macro](/docs/ref/macro.md) for some reason(maybe you compile your code using just TS instead of babel), you can render plurals using the plain Trans component like this: +If you cannot use [@lingui/macro](/docs/ref/macro.md) for some reason, you can render plurals using the plain Trans component like this: ``` jsx import React from 'react'; @@ -154,10 +152,8 @@ const App = () => { `forceRenderOnLocaleChange` is true by default and it ensures that: -> - Children of `I18nProvider` aren't rendered before locales are -> loaded. -> - When locale changes, the whole element tree below `I18nProvider` -> is re-rendered. +> - Children of `I18nProvider` aren't rendered before locales are loaded. +> - When locale changes, the components consuming `i18n` context are re-rendered. Disable `forceRenderOnLocaleChange` when you have specific needs to handle initial state before locales are loaded and when locale changes. From 58b398a45d8257cb273ece0f0bcd1222fdb0e238 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 14:07:48 +0100 Subject: [PATCH 05/17] docs: I18nProvider updates --- packages/react/src/I18nProvider.test.tsx | 4 +- website/docs/ref/react.md | 142 ++++++++++++----------- website/sidebars.ts | 2 +- 3 files changed, 77 insertions(+), 71 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index 1c2369324..e26f8f51e 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -90,8 +90,8 @@ describe("I18nProvider", () => { textContentDynamicAfterActivate: "2_cs", }, ])( - "A component that is not consuming i18n context will not re-render on locale change.", - // "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", + "A component that is not consuming i18n context will not re-render on locale change." + + "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", ({ forceRenderOnLocaleChange, textContentBeforeActivate, diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index ec94e4d34..21842f031 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -2,13 +2,11 @@ Components from `@lingui/react` wrap the vanilla JS API from `@lingui/core`. React components handle changes of active language or interpolated variables better than low-level API and also take care of re-rendering when locale or messages change. -## General Concepts +## Rendering of Translations {#rendering-translations} -### Rendering of Translations {#rendering-translations} +All i18n components render translation as text without a wrapping tag. This can be customized in two different ways: -All i18n components render translation as a text without a wrapping tag. This can be customized in three different ways: - -- globally: using `defaultComponent` prop on [`I18nProvider`](#i18nprovider) component; +- globally: using `defaultComponent` prop on [`I18nProvider`](#i18nprovider) component - locally: using `render` prop or `component` on i18 components #### Global Configuration @@ -36,7 +34,7 @@ import { Text } from "react-native"; // renders as Link to docs ``` -To get more control over the rendering of translation, use instead the `render` method with **React.Component** (or stateless component). Component passed to `render` will receive the translation value as a `translation` prop: +To get more control over the rendering of translation, use instead the `render` prop with a render callback. Function passed to `render` will receive the translation value as a `translation` parameter: ``` jsx // custom component @@ -46,7 +44,7 @@ To get more control over the rendering of translation, use instead the `render` // renders as ``` -`render` or `component` also accepts `null` value to render string without a wrapping component. This can be used to override custom `defaultComponent` config. +`render` and `component` also accept `null` value to render string without a wrapping component. This can be used to override custom `defaultComponent` config. ``` jsx Heading; @@ -56,71 +54,18 @@ To get more control over the rendering of translation, use instead the `render` // renders as "Heading" ``` -## Components - -### Trans - -| Prop name | Type | Description | -| --------- | -------- | ------------------- | -| `id` | `string` | Key, the message ID | - -:::important - -Import [`Trans`](/docs/ref/macro.md#jsxmacro-Trans) macro instead of [`Trans`](#trans) if you use macros: - -``` jsx -import { Trans } from "@lingui/macro" - -// Trans from @lingui/react won't work in this case -// import { Trans } from "@lingui/react" - -Hello, my name is {name} -``` -::: - -It's also possible to use `Trans` component directly without macros. In that case, `id` is the message being translated. `values` and `components` are arguments and components used for formatting translation: - -``` jsx -; - -; - -// number of tag corresponds to index in `components` prop - }} -/> -``` - -#### Plurals - -If you cannot use [@lingui/macro](/docs/ref/macro.md) for some reason, you can render plurals using the plain Trans component like this: - -``` jsx -import React from 'react'; -import { Trans } from '@lingui/react'; - - -``` - -## Providers +## Lingui Context Message catalogs and the active locale are passed to the context in [`I18nProvider`](#i18nprovider). Use [`useLingui`](#uselingui) hook to access Lingui context. ### I18nProvider -| Prop name | Type | Description | -| --------------------------- | --------------------- | ----------------------------------------------------------------------------- | -| `I18n` | `i18n` | The i18n instance (usually the one imported from `@lingui/core`) | -| `children` | `React.ReactNode` | React Children node | -| `defaultComponent` | `React.ComponentType` | A React component for rendering within this component (Not required) | -| `forceRenderOnLocaleChange` | `boolean` | Force re-render when locale changes (default: true) | +| Prop name | Type | Description | +| --------------------------- | --------------------- |---------------------------------------------------------------------------| +| `I18n` | `i18n` | The i18n instance (usually the one imported from `@lingui/core`) | +| `children` | `React.ReactNode` | React Children node | +| `defaultComponent` | `React.ComponentType` | A React component for rendering within this component (optional) | +| `forceRenderOnLocaleChange` | `boolean` | Force re-render when locale changes (default: true) | `defaultComponent` has the same meaning as `component` in other i18n components. [`Rendering of translations`](#rendering-translations) is explained at the beginning of this document. @@ -157,7 +102,7 @@ const App = () => { Disable `forceRenderOnLocaleChange` when you have specific needs to handle initial state before locales are loaded and when locale changes. -This component should live above all i18n components. A good place is as a top-level application component. However, if the `locale` is stored in a `redux` store, this component should be inserted below `react-redux/Provider`: +`I18nProvider` should live above all other i18n components. A good place is as a top-level application component. However, if the `locale` is stored in a `redux` store, this component should be inserted below `react-redux/Provider`: ``` jsx import React from 'react'; @@ -181,6 +126,10 @@ const App = () => { ### useLingui +This hook allows access to the Lingui context. It returns an object with the same values that were passed to the `I18nProvider` component. + +Components that use `useLingui` hook will re-render when locale and / or catalogs change, ensuring that the translations are always up-to-date. + ``` jsx import React from "react" import { useLingui } from "@lingui/react" @@ -191,3 +140,60 @@ const CurrentLocale = () => { return Current locale: {i18n.locale} } ``` + +## Components + +The `@lingui/react` package provides `Trans` component to render translations. However, you're more likely to use [macros](/docs/ref/macro.md) instead because they are more convenient and easier to use. + +This section is intended for reference purposes. + +### Trans + +| Prop name | Type | Description | +| --------- | -------- | ------------------- | +| `id` | `string` | Key, the message ID | + +:::important + +Import [`Trans`](/docs/ref/macro.md#jsxmacro-Trans) macro instead of [`Trans`](#trans) component if you use macros: + +``` jsx +import { Trans } from "@lingui/macro" + +// Trans from @lingui/react won't work in this case +// import { Trans } from "@lingui/react" + +Hello, my name is {name} +``` +::: + +It's also possible to use `Trans` component directly without macros. In that case, `id` is the message being translated. `values` and `components` are arguments and components used for formatting translation: + +``` jsx +; + +; + +// number of tag corresponds to index in `components` prop + }} +/> +``` + +#### Plurals + +If you cannot use [@lingui/macro](/docs/ref/macro.md) for some reason, you can render plurals using the plain Trans component like this: + +``` jsx +import React from 'react'; +import { Trans } from '@lingui/react'; + + +``` diff --git a/website/sidebars.ts b/website/sidebars.ts index 64c8c4892..31db132e0 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -196,7 +196,7 @@ const sidebar = [ { type: 'category', label: 'Releases', - items: ['releases/migration-3', 'releases/migration-4'], + items: ['releases/migration-4', 'releases/migration-3'], }, ]; From a2fe263d9b3e91027c878569c9a2c029f28fbc10 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 14:09:45 +0100 Subject: [PATCH 06/17] chore: better comment --- packages/react/src/I18nProvider.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index e26f8f51e..adc62975e 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -140,20 +140,20 @@ describe("I18nProvider", () => { i18n.activate("cs") }) - // After loading and activating locale, only CurrentLocaleContextConsumer re-rendered. + // After loading and activating locale, components are re-rendered if forceRenderOnLocaleChange is true expect(getByTestId("static").textContent).toBe( textContentStaticAfterActivate ) expect(getByTestId("dynamic").textContent).toBe( textContentDynamicAfterActivate ) - expect(staticRenderCount).toBe(1) /* - * when forceRenderOnLocaleChange is false, components are rendered only once and then do not re-rendered + * when forceRenderOnLocaleChange is false, components are rendered only once and then do not re-render * when forceRenderOnLocaleChange is true, initial render skips rendering children because no locale is active. * Later, loading messages & locale activation are batched into 1 render. * */ + expect(staticRenderCount).toBe(1) expect(dynamicRenderCount).toBe(1) } ) @@ -173,13 +173,13 @@ describe("I18nProvider", () => { renderCount++ return {i18n.locale} } + /** * Note that we're doing exactly what the description says: * but to simulate the equivalent situation, we pass our own mock subscriber - * to i18n.on("change", ...) and then we call i18n.activate("cs") ourselves + * to i18n.on("change", ...) and in it we call i18n.activate("cs") ourselves * so that the condition in useEffect() is met and the component re-renders * */ - const mockSubscriber = jest.fn(() => { i18n.load("cs", {}) i18n.activate("cs") From 596b26ef643c404d8879451fc956caa8c90a6c53 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 15:14:46 +0100 Subject: [PATCH 07/17] refactor: tests --- packages/react/src/I18nProvider.test.tsx | 91 ++++++++++++------------ 1 file changed, 47 insertions(+), 44 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index adc62975e..1f06dca1e 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -5,40 +5,51 @@ import { I18nProvider, useLingui } from "./I18nProvider" import { setupI18n } from "@lingui/core" describe("I18nProvider", () => { - it("should pass i18n context to wrapped component", () => { - const i18n = setupI18n({ - locale: "cs", - messages: { - cs: {}, - }, - }) + it( + "should pass i18n context to wrapped components, " + + "and re-render components that consume the context through useLingui()", + () => { + const i18n = setupI18n({ + locale: "en", + messages: { + en: {}, + cs: {}, + }, + }) - const WithoutLingui = (props) => { - return
{props?.i18n?.locale}
- } + const WithoutLinguiHook = (props) => { + return
{props.i18n.locale}
+ } - const WithLingui = (props) => { - const { i18n } = useLingui() - return - } + const WithLinguiHook = (props) => { + const { i18n } = useLingui() + return + } - const { getByTestId } = render( - - - - - ) + const { getByTestId } = render( + + + + + ) - act(() => { - i18n.load("cs", {}) - i18n.activate("cs") - }) + act(() => { + i18n.activate("cs") + }) - expect(getByTestId("not-composed").textContent).toEqual("") - expect(getByTestId("composed").textContent).toEqual("cs") - }) + expect(getByTestId("static").textContent).toEqual("en") + expect(getByTestId("dynamic").textContent).toEqual("cs") - it("should subscribe for locale changes", () => { + act(() => { + i18n.activate("en") + }) + + expect(getByTestId("static").textContent).toEqual("en") + expect(getByTestId("dynamic").textContent).toEqual("en") + } + ) + + it("should subscribe for locale changes upon mount", () => { const i18n = setupI18n({ locale: "cs", messages: { @@ -53,7 +64,7 @@ describe("I18nProvider", () => {
) - expect(i18n.on).toBeCalledWith("change", expect.anything()) + expect(i18n.on).toBeCalledWith("change", expect.any(Function)) }) it("should unsubscribe for locale changes on unmount", () => { @@ -80,14 +91,12 @@ describe("I18nProvider", () => { { forceRenderOnLocaleChange: false, textContentBeforeActivate: "1_2_", - textContentStaticAfterActivate: "1_", - textContentDynamicAfterActivate: "2_", + textContentAfterActivate: "1_2_", }, { forceRenderOnLocaleChange: true, - textContentBeforeActivate: "", - textContentStaticAfterActivate: "1_cs", - textContentDynamicAfterActivate: "2_cs", + textContentBeforeActivate: "", // I18nProvider won't render anything until locale is activated + textContentAfterActivate: "1_cs2_cs", }, ])( "A component that is not consuming i18n context will not re-render on locale change." + @@ -95,10 +104,9 @@ describe("I18nProvider", () => { ({ forceRenderOnLocaleChange, textContentBeforeActivate, - textContentStaticAfterActivate, - textContentDynamicAfterActivate, + textContentAfterActivate, }) => { - expect.assertions(6) + expect.assertions(5) const i18n = setupI18n() let staticRenderCount = 0, @@ -114,7 +122,7 @@ describe("I18nProvider", () => { return 2_{i18n.locale} } - const { container, getByTestId, debug } = render( + const { container, debug } = render( { }) // After loading and activating locale, components are re-rendered if forceRenderOnLocaleChange is true - expect(getByTestId("static").textContent).toBe( - textContentStaticAfterActivate - ) - expect(getByTestId("dynamic").textContent).toBe( - textContentDynamicAfterActivate - ) + expect(container.textContent).toEqual(textContentAfterActivate) /* * when forceRenderOnLocaleChange is false, components are rendered only once and then do not re-render From 2b6725cec00625841c6854c5b428c4cb75172586 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 19:46:29 +0100 Subject: [PATCH 08/17] Update website/docs/ref/react.md Co-authored-by: Timofei Iatsenko --- website/docs/ref/react.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 21842f031..b148e6ce8 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -18,7 +18,7 @@ Default rendering component can be set using `defaultComponent` prop in [`I18nPr | Prop name | Type | Description | |-------------| ----------------------------------------- |------------------------------------------------| | `className` | string | Class name to be added to `` element | -| `render` | *Function(props) -> Element \| Component* | Custom wrapper rendered as function | +| `render` | *Function(props) -> Element \| Component* | Custom wrapper rendered as function | | `component` | Component, `null` | Custom wrapper component to render translation | `className` is used only for built-in components (when *render* is string). From f511fa11a4a13753a70a9a5dd35731a2ada1c101 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 19:46:36 +0100 Subject: [PATCH 09/17] Update website/docs/ref/react.md Co-authored-by: Timofei Iatsenko --- website/docs/ref/react.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index b148e6ce8..9dee0ceff 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -193,7 +193,8 @@ import React from 'react'; import { Trans } from '@lingui/react'; ``` From 4bc8c57322678b9fc55dd51ca8c7ced0206cbca0 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 20:28:08 +0100 Subject: [PATCH 10/17] refactor: address part of code review --- packages/react/src/I18nProvider.test.tsx | 4 ++-- packages/react/src/I18nProvider.tsx | 7 ++++++- website/docs/ref/react.md | 12 +++++++----- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index 1f06dca1e..5ced8e226 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -122,7 +122,7 @@ describe("I18nProvider", () => { return 2_{i18n.locale} } - const { container, debug } = render( + const { container } = render( { ) - debug() + // First render — locale isn't activated expect(container.textContent).toEqual(textContentBeforeActivate) diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx index 92c17259b..cc283de10 100644 --- a/packages/react/src/I18nProvider.tsx +++ b/packages/react/src/I18nProvider.tsx @@ -81,7 +81,12 @@ export const I18nProvider: FunctionComponent = ({ return unsubscribe }, [makeContext, forceRenderOnLocaleChange]) - if (forceRenderOnLocaleChange && !latestKnownLocale.current) return null + if (forceRenderOnLocaleChange && !latestKnownLocale.current) { + console.log( + "I18nProvider did not render. A call to i18n.activate still needs to happen or forceRenderOnLocaleChange must be set to false." + ) + return null + } return ( {children} diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 9dee0ceff..f7a0d7f12 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -167,19 +167,21 @@ import { Trans } from "@lingui/macro" ``` ::: -It's also possible to use `Trans` component directly without macros. In that case, `id` is the message being translated. `values` and `components` are arguments and components used for formatting translation: +It's also possible to use `Trans` component directly without macros. In that case, `id` identifies the message being translated. `values` and `components` are arguments and components used for formatting translation: ``` jsx -; + ; +/> // number of tag corresponds to index in `components` prop }} /> ``` From dbc3791c240bf8b7f70a42723406425e7207e209 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Mon, 13 Mar 2023 20:40:36 +0100 Subject: [PATCH 11/17] docs: single Trans tag --- website/docs/ref/react.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index f7a0d7f12..9dd62682f 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -198,5 +198,5 @@ import { Trans } from '@lingui/react'; id="my.plural.msg" message="{count, plural, =1 {car} other {cars}}" values={{ count: cars.length }} -> +/> ``` From 7f347fa4d6bf048217e1b304dee170f3122966ce Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Thu, 16 Mar 2023 09:23:19 +0100 Subject: [PATCH 12/17] refactor: remove forceRenderOnLocaleChange, export LinguiContext --- packages/react/src/I18nProvider.test.tsx | 106 ++++++++--------------- packages/react/src/I18nProvider.tsx | 19 ++-- packages/react/src/index.ts | 2 +- 3 files changed, 47 insertions(+), 80 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index 5ced8e226..2b6c9276d 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -16,14 +16,17 @@ describe("I18nProvider", () => { cs: {}, }, }) - + let staticRenderCount = 0, + dynamicRenderCount = 0 const WithoutLinguiHook = (props) => { + staticRenderCount++ return
{props.i18n.locale}
} const WithLinguiHook = (props) => { const { i18n } = useLingui() - return + dynamicRenderCount++ + return
{i18n.locale}
} const { getByTestId } = render( @@ -46,6 +49,8 @@ describe("I18nProvider", () => { expect(getByTestId("static").textContent).toEqual("en") expect(getByTestId("dynamic").textContent).toEqual("en") + expect(staticRenderCount).toEqual(1) + expect(dynamicRenderCount).toEqual(3) // initial, cs, en } ) @@ -87,79 +92,44 @@ describe("I18nProvider", () => { expect(unsubscribe).toBeCalled() }) - it.each([ - { - forceRenderOnLocaleChange: false, - textContentBeforeActivate: "1_2_", - textContentAfterActivate: "1_2_", - }, - { - forceRenderOnLocaleChange: true, - textContentBeforeActivate: "", // I18nProvider won't render anything until locale is activated - textContentAfterActivate: "1_cs2_cs", - }, - ])( - "A component that is not consuming i18n context will not re-render on locale change." + - "A component that consumes i18n context will re-render on locale change, only if forceRenderOnLocaleChange is true.", - ({ - forceRenderOnLocaleChange, - textContentBeforeActivate, - textContentAfterActivate, - }) => { - expect.assertions(5) - - const i18n = setupI18n() - let staticRenderCount = 0, - dynamicRenderCount = 0 + it("I18nProvider renders `null` until locale is activated. Children are rendered after activation.", () => { + expect.assertions(3) - const CurrentLocaleStatic = () => { - staticRenderCount++ - return 1_{i18n.locale} - } - const CurrentLocaleContextConsumer = () => { - const { i18n } = useLingui() - dynamicRenderCount++ - return 2_{i18n.locale} - } + const i18n = setupI18n() - const { container } = render( - - <> - - - - - ) + const CurrentLocaleStatic = () => { + return 1_{i18n.locale} + } + const CurrentLocaleContextConsumer = () => { + const { i18n } = useLingui() + return 2_{i18n.locale} + } - // First render — locale isn't activated - expect(container.textContent).toEqual(textContentBeforeActivate) + const { container } = render( + + <> + + + + + ) - act(() => { - i18n.load("en", {}) - }) - // Catalog is loaded, but locale still isn't activated. - expect(container.textContent).toEqual(textContentBeforeActivate) + // First render — locale isn't activated + expect(container.textContent).toEqual("") - act(() => { - i18n.load("cs", {}) - i18n.activate("cs") - }) + act(() => { + i18n.load("cs", {}) + }) + // Catalog is loaded, but locale still isn't activated. + expect(container.textContent).toEqual("") - // After loading and activating locale, components are re-rendered if forceRenderOnLocaleChange is true - expect(container.textContent).toEqual(textContentAfterActivate) + act(() => { + i18n.activate("cs") + }) - /* - * when forceRenderOnLocaleChange is false, components are rendered only once and then do not re-render - * when forceRenderOnLocaleChange is true, initial render skips rendering children because no locale is active. - * Later, loading messages & locale activation are batched into 1 render. - * */ - expect(staticRenderCount).toBe(1) - expect(dynamicRenderCount).toBe(1) - } - ) + // After loading and activating locale, components are rendered for the first time + expect(container.textContent).toEqual("1_cs2_cs") + }) it( "given 'en' locale, if activate('cs') call happens before i18n.on-change subscription in useEffect(), " + diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx index cc283de10..62dd773e9 100644 --- a/packages/react/src/I18nProvider.tsx +++ b/packages/react/src/I18nProvider.tsx @@ -8,11 +8,10 @@ export type I18nContext = { } export type I18nProviderProps = I18nContext & { - forceRenderOnLocaleChange?: boolean children?: React.ReactNode } -const LinguiContext = React.createContext(null) +export const LinguiContext = React.createContext(null) export function useLingui(): I18nContext { const context = React.useContext(LinguiContext) @@ -29,7 +28,6 @@ export function useLingui(): I18nContext { export const I18nProvider: FunctionComponent = ({ i18n, defaultComponent, - forceRenderOnLocaleChange = true, children, }) => { const latestKnownLocale = React.useRef(i18n.locale) @@ -62,9 +60,6 @@ export const I18nProvider: FunctionComponent = ({ * we need to trigger re-rendering of LinguiContext.Consumers. */ React.useEffect(() => { - if (!forceRenderOnLocaleChange) { - return - } const updateContext = () => { latestKnownLocale.current = i18n.locale setContext(makeContext()) @@ -79,12 +74,14 @@ export const I18nProvider: FunctionComponent = ({ updateContext() } return unsubscribe - }, [makeContext, forceRenderOnLocaleChange]) + }, [makeContext]) - if (forceRenderOnLocaleChange && !latestKnownLocale.current) { - console.log( - "I18nProvider did not render. A call to i18n.activate still needs to happen or forceRenderOnLocaleChange must be set to false." - ) + if (!latestKnownLocale.current) { + process.env.NODE_ENV === "development" && + console.log( + "I18nProvider rendered `null`. A call to `i18n.activate` needs to happen in order for translations to be activated and for the I18nProvider to render." + + "This is not an error but an informational message logged only in development." + ) return null } diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index aab348775..abbd7ecbc 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,4 +1,4 @@ -export { I18nProvider, useLingui } from "./I18nProvider" +export { I18nProvider, useLingui, LinguiContext } from "./I18nProvider" export type { I18nProviderProps, I18nContext } from "./I18nProvider" From d50f139a878c7696fb05d900ebdd43efa2cb286c Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 17 Mar 2023 19:47:13 +0100 Subject: [PATCH 13/17] docs: update I18nProvider docs and v4 changes --- website/docs/ref/react.md | 53 ++++++++-------------------- website/docs/releases/migration-4.md | 11 +++--- 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 9dd62682f..14a783993 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -7,7 +7,7 @@ Components from `@lingui/react` wrap the vanilla JS API from `@lingui/core`. Rea All i18n components render translation as text without a wrapping tag. This can be customized in two different ways: - globally: using `defaultComponent` prop on [`I18nProvider`](#i18nprovider) component -- locally: using `render` prop or `component` on i18 components +- locally: using `render` prop or `component` on i18n components #### Global Configuration @@ -56,16 +56,22 @@ To get more control over the rendering of translation, use instead the `render` ## Lingui Context -Message catalogs and the active locale are passed to the context in [`I18nProvider`](#i18nprovider). Use [`useLingui`](#uselingui) hook to access Lingui context. +Message catalogs and the active locale are passed via context in [`I18nProvider`](#i18nprovider). Use the [`useLingui`](#uselingui) hook to access the Lingui context. + +Lingui context object is exported from the package (`import { LinguiContext } from '@lingui/react'`). It is not expected that you would need it, but it enables advanced scenarios if the behavior of `I18nProvider` doesn't fit your needs. ### I18nProvider -| Prop name | Type | Description | -| --------------------------- | --------------------- |---------------------------------------------------------------------------| -| `I18n` | `i18n` | The i18n instance (usually the one imported from `@lingui/core`) | -| `children` | `React.ReactNode` | React Children node | -| `defaultComponent` | `React.ComponentType` | A React component for rendering within this component (optional) | -| `forceRenderOnLocaleChange` | `boolean` | Force re-render when locale changes (default: true) | +`I18nProvider` provides Lingui context to all components in the subtree. It should be rendered as top-level component of your application. + +`I18nProvider` renders its children only after a locale is activated. This ensures that the components consuming `i18n` have access to the translations. +Additionally, it subscribes to change events emitted by the `i18n` object and re-renders all components consuming the Lingui context when messages are updated or when a new locale is activated. + +| Prop name | Type | Description | +|--------------------|-----------------------|---------------------------------------------------------------------------| +| `i18n` | `I18n` | The i18n instance (usually the one imported from `@lingui/core`) | +| `children` | `React.ReactNode` | React Children node | +| `defaultComponent` | `React.ComponentType` | A React component within which translation strings will be rendered (optional) | `defaultComponent` has the same meaning as `component` in other i18n components. [`Rendering of translations`](#rendering-translations) is explained at the beginning of this document. @@ -95,35 +101,6 @@ const App = () => { } ``` -`forceRenderOnLocaleChange` is true by default and it ensures that: - -> - Children of `I18nProvider` aren't rendered before locales are loaded. -> - When locale changes, the components consuming `i18n` context are re-rendered. - -Disable `forceRenderOnLocaleChange` when you have specific needs to handle initial state before locales are loaded and when locale changes. - -`I18nProvider` should live above all other i18n components. A good place is as a top-level application component. However, if the `locale` is stored in a `redux` store, this component should be inserted below `react-redux/Provider`: - -``` jsx -import React from 'react'; -import { I18nProvider } from '@lingui/react'; -import { i18n } from '@lingui/core'; -import { messages as messagesEn } from './locales/en/messages.js'; - -i18n.load({ - en: messagesEn, -}); -i18n.activate('en'); - -const App = () => { - return ( - - // rest of the app - - ); -} -``` - ### useLingui This hook allows access to the Lingui context. It returns an object with the same values that were passed to the `I18nProvider` component. @@ -155,7 +132,7 @@ This section is intended for reference purposes. :::important -Import [`Trans`](/docs/ref/macro.md#jsxmacro-Trans) macro instead of [`Trans`](#trans) component if you use macros: +Import [`Trans`](/docs/ref/macro.md#trans) macro instead of [`Trans`](#trans) component if you use macros: ``` jsx import { Trans } from "@lingui/macro" diff --git a/website/docs/releases/migration-4.md b/website/docs/releases/migration-4.md index e8247ae57..c21f44b05 100644 --- a/website/docs/releases/migration-4.md +++ b/website/docs/releases/migration-4.md @@ -23,13 +23,16 @@ module.exports = { ### I18nProvider no longer remounts its children on locale change -Previously, the `I18nProvider` remounted its children on locale change. This ensured that the whole app was re-rendered with the new locale and all strings were rendered correctly translated. -Apart from not being very performant, this approach had the drawback that the state of the app was lost - all components were re-mounted and their state was reset. This is not a standard behavior of React Context providers and could cause some confusion. +Previously, the `I18nProvider` remounted its children on locale change. This had the effect that the whole app was re-rendered with the new locale and all strings were rendered correctly translated. +Apart from not being very performant, this approach had the drawback that the state of the app was lost - all components were re-mounted and their state was reset. This is not a standard behavior for React Context providers and could cause some confusion. In v4, the `I18nProvider` no longer remounts its children on locale change. Instead, when locale changes, the context value provided by `I18nProvider` is updated and all components that consume the provided React Context are re-rendered with the new locale. -This includes components provided by Lingui, such as `Trans` or `Plural` and also custom components that use the `useLingui` hook. +This includes components provided by Lingui, such as `Trans` or `Plural` and also custom components that use the `useLingui` hook. This should result in a more predictable behavior. + +If the changes to the `I18nProvider` pose a problem to you, please open an issue and explain what the problem is. Additionally, you can keep using the [v3 implementation](https://github.com/lingui/js-lingui/blob/31dcab5a9a8f88bfa8b3a2c7cd12aa2d908a1d80/packages/react/src/I18nProvider.tsx#L58) by copying it into your code base and using that instead. + +No migration steps are necessary for components provided by Lingui, such as `Trans` or `Plural`. However, if you rendered translations in React components using the `t` macro, you need to be sure that the `useLingui` hook is called in the component, as seen [here](/ref/react#uselingui). -This is the default behavior, but you can disable it by setting `forceRenderOnLocaleChange={false}`. ### Hash-based message ID generation and Context feature From 32ad00529676b231a19524cc60c410f88075b8cf Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 17 Mar 2023 19:54:10 +0100 Subject: [PATCH 14/17] refactor: remove react.Fragment --- packages/react/src/I18nProvider.test.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx index 2b6c9276d..4c1418b41 100644 --- a/packages/react/src/I18nProvider.test.tsx +++ b/packages/react/src/I18nProvider.test.tsx @@ -107,10 +107,8 @@ describe("I18nProvider", () => { const { container } = render( - <> - - - + + ) From b9055c1521f16e3de34e6ef7b8876e64d5180a3a Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Fri, 17 Mar 2023 19:58:08 +0100 Subject: [PATCH 15/17] docs: fix lint --- website/docs/releases/migration-4.md | 1 - 1 file changed, 1 deletion(-) diff --git a/website/docs/releases/migration-4.md b/website/docs/releases/migration-4.md index c21f44b05..a214dacef 100644 --- a/website/docs/releases/migration-4.md +++ b/website/docs/releases/migration-4.md @@ -33,7 +33,6 @@ If the changes to the `I18nProvider` pose a problem to you, please open an issue No migration steps are necessary for components provided by Lingui, such as `Trans` or `Plural`. However, if you rendered translations in React components using the `t` macro, you need to be sure that the `useLingui` hook is called in the component, as seen [here](/ref/react#uselingui). - ### Hash-based message ID generation and Context feature The previous implementation had a flaw: there is an original message in the bundle at least 2 times + 1 translation. From b1df4338574277d840bacbf437dfda8516891833 Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 19 Mar 2023 09:59:48 +0100 Subject: [PATCH 16/17] docs: fix headings --- website/docs/ref/react.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/ref/react.md b/website/docs/ref/react.md index 14a783993..96a34c783 100644 --- a/website/docs/ref/react.md +++ b/website/docs/ref/react.md @@ -9,11 +9,11 @@ All i18n components render translation as text without a wrapping tag. This can - globally: using `defaultComponent` prop on [`I18nProvider`](#i18nprovider) component - locally: using `render` prop or `component` on i18n components -#### Global Configuration +### Global Configuration Default rendering component can be set using `defaultComponent` prop in [`I18nProvider`](#i18nprovider). The main use case for this is rendering translations in `` component in React Native. -#### Local Configuration +### Local Configuration | Prop name | Type | Description | |-------------| ----------------------------------------- |------------------------------------------------| @@ -163,7 +163,7 @@ It's also possible to use `Trans` component directly without macros. In that cas /> ``` -#### Plurals +### Plurals If you cannot use [@lingui/macro](/docs/ref/macro.md) for some reason, you can render plurals using the plain Trans component like this: From 53cd2e1aa8aa873fa5e9a8c0fc1e7deb66086dfb Mon Sep 17 00:00:00 2001 From: Vojtech Novak Date: Sun, 19 Mar 2023 10:02:05 +0100 Subject: [PATCH 17/17] docs: change useLingui link --- website/docs/releases/migration-4.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/releases/migration-4.md b/website/docs/releases/migration-4.md index a214dacef..038477ce3 100644 --- a/website/docs/releases/migration-4.md +++ b/website/docs/releases/migration-4.md @@ -31,7 +31,7 @@ This includes components provided by Lingui, such as `Trans` or `Plural` and als If the changes to the `I18nProvider` pose a problem to you, please open an issue and explain what the problem is. Additionally, you can keep using the [v3 implementation](https://github.com/lingui/js-lingui/blob/31dcab5a9a8f88bfa8b3a2c7cd12aa2d908a1d80/packages/react/src/I18nProvider.tsx#L58) by copying it into your code base and using that instead. -No migration steps are necessary for components provided by Lingui, such as `Trans` or `Plural`. However, if you rendered translations in React components using the `t` macro, you need to be sure that the `useLingui` hook is called in the component, as seen [here](/ref/react#uselingui). +No migration steps are necessary for components provided by Lingui, such as `Trans` or `Plural`. However, if you rendered translations in React components using the `t` macro, you need to be sure that the `useLingui` hook is called in the component, as seen [here](/docs/ref/react.md#uselingui). ### Hash-based message ID generation and Context feature