Repository: lingui/js-lingui
Pull request #756 (2020-10-11 13:49:11)

feat: introduced withI18n
For an easier migration to v3.0.0, i've introduced a little hoc for an easy migration to big codebases where withI18n it's used a lot!

On master branch, this hoc accepts some configuration, should we accept that configuration?

Feel free to request changes 

vercel[bot]: This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).  
To see the status of your deployment, click below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/lingui-js/js-lingui/155xykwzx](https://vercel.com/lingui-js/js-lingui/155xykwzx)  
✅ Preview: *In Progress*


codecov[bot]: # [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=h1) Report
> Merging [#756](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=desc) into [next](https://codecov.io/gh/lingui/js-lingui/commit/f44ecdd7b6f47f1230262c73a3020e128820b162?el=desc) will **decrease** coverage by `0.15%`.
> The diff coverage is `62.50%`.

[![Impacted file tree graph](https://codecov.io/gh/lingui/js-lingui/pull/756/graphs/tree.svg?width=650&height=150&src=pr&token=CuJ1VD5BY5)](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##             next     #756      +/-   ##
==========================================
- Coverage   84.40%   84.25%   -0.16%     
==========================================
  Files          38       38              
  Lines        1225     1232       +7     
  Branches      325      327       +2     
==========================================
+ Hits         1034     1038       +4     
- Misses        114      115       +1     
- Partials       77       79       +2     
```


| [Impacted Files](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/react/src/I18nProvider.tsx](https://codecov.io/gh/lingui/js-lingui/pull/756/diff?src=pr&el=tree#diff-cGFja2FnZXMvcmVhY3Qvc3JjL0kxOG5Qcm92aWRlci50c3g=) | `82.60% <62.50%> (-11.15%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=footer). Last update [f44ecdd...988ffb3](https://codecov.io/gh/lingui/js-lingui/pull/756?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

tricoder42: Looks good, thank you @semoal! 👍 

Patch:
diff --git a/docs/releases/migration-3.rst b/docs/releases/migration-3.rst
index 0a53645a..b0087bc8 100644
--- a/docs/releases/migration-3.rst
+++ b/docs/releases/migration-3.rst
@@ -14,8 +14,7 @@ Minimal required versions are:
 @lingui/react
 -------------
 
-- ``<I18n>`` render-prop component and ``withI18n`` high-order component were
-  removed in favor of :js:func:`useLingui` hook.
+- ``<I18n>`` render-prop component was removed in favor of :js:func:`useLingui` hook.
 
 - In :component:`Trans`, ``defaults`` prop was renamed to ``message`` and
   ``description`` to ``comment``.
diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx
index e3a554c3..cc4fdb88 100644
--- a/packages/react/src/I18nProvider.tsx
+++ b/packages/react/src/I18nProvider.tsx
@@ -1,4 +1,4 @@
-import React, { FunctionComponent } from "react"
+import React, { FunctionComponent, ComponentType } from "react"
 import { I18n } from "@lingui/core"
 import { TransRenderType } from "./Trans"
 
@@ -14,7 +14,7 @@ export type I18nProviderProps = I18nContext & {
 const LinguiContext = React.createContext<I18nContext>(null)
 
 export function useLingui(): I18nContext {
-  const context = React.useContext(LinguiContext)
+  const context = React.useContext<I18nContext>(LinguiContext)
 
   if (process.env.NODE_ENV !== "production") {
     if (context == null) {
@@ -25,6 +25,25 @@ export function useLingui(): I18nContext {
   return context
 }
 
+export function withI18n(o?: object): <P extends { i18n: I18n }>(Component: ComponentType<P>) => React.ComponentType<P> {
+  return <P extends { i18n: I18n }>(WrappedComponent: ComponentType<P>) : ComponentType<P> => {
+    return (props) => {
+      if (process.env.NODE_ENV !== "production") {
+        if (typeof o === "function" || React.isValidElement(o)) {
+          throw new Error(
+            "withI18n([options]) takes options as a first argument, " +
+              "but received React component itself. Without options, the Component " +
+              "should be wrapped as withI18n()(Component), not withI18n(Component)."
+          )
+        }
+      }
+
+      const { i18n } = useLingui();
+      return <WrappedComponent {...props} i18n={i18n} />;
+    }
+  }
+}
+
 export const I18nProvider: FunctionComponent<I18nProviderProps> = ({
   i18n,
   defaultRender,


Test patch:
diff --git a/packages/react/src/I18nProvider.test.tsx b/packages/react/src/I18nProvider.test.tsx
index 07eded6a..18258a9e 100644
--- a/packages/react/src/I18nProvider.test.tsx
+++ b/packages/react/src/I18nProvider.test.tsx
@@ -1,11 +1,39 @@
 import * as React from "react"
 import { act, render } from "@testing-library/react"
 
-import { I18nProvider, useLingui } from "@lingui/react"
+import { withI18n, I18nProvider } from "./I18nProvider"
 import { setupI18n } from "@lingui/core"
 
-describe("I18nProvider", function () {
-  it("should subscribe for locale changes", function () {
+describe("I18nProvider", () => {
+
+  it("should pass i18n context to wrapped component", () => {
+    const i18n = setupI18n({
+      locale: "cs",
+    })
+
+    const WithoutHoc = (props) => {
+      return <div {...props}>{props?.i18n?.locale}</div>
+    }
+
+    const WithHoc = withI18n()(WithoutHoc)
+
+    const { getByTestId } = render(
+      <I18nProvider i18n={i18n}>
+        <WithoutHoc data-testid="not-composed" />
+        <WithHoc data-testid="composed" />
+      </I18nProvider>
+    )
+
+    act(() => {
+      i18n.load("cs", {})
+      i18n.activate("cs")
+    })
+
+    expect(getByTestId("not-composed").textContent).toEqual("")
+    expect(getByTestId("composed").textContent).toEqual("cs")
+  })
+
+  it("should subscribe for locale changes", () => {
     const i18n = setupI18n()
     i18n.on = jest.fn(() => jest.fn())
 
@@ -18,7 +46,7 @@ describe("I18nProvider", function () {
     expect(i18n.on).toBeCalledWith("change", expect.anything())
   })
 
-  it("should unsubscribe for locale changes on unmount", function () {
+  it("should unsubscribe for locale changes on unmount", () => {
     const unsubscribe = jest.fn()
     const i18n = setupI18n()
     i18n.on = jest.fn(() => unsubscribe)
@@ -38,7 +66,7 @@ describe("I18nProvider", function () {
 
     const i18n = setupI18n()
 
-    function CurrentLocale() {
+    const CurrentLocale = () => {
       return <span>{i18n.locale}</span>
     }
 
@@ -65,7 +93,7 @@ describe("I18nProvider", function () {
     expect(container.textContent).toEqual("cs")
   })
 
-  it("should render children", function () {
+  it("should render children", () => {
     const i18n = setupI18n({
       locale: "en",
     })