Repository: lingui/js-lingui
Pull request #1242 (2022-05-13 14:34:45)

feat: Pass props to I18nProvider.defaultComponent in Trans.render style
The reason for that PR is to have more control inside defaultComponent.

I'm using your awesome library with nextjs and loading translations asyncronically on a client side, but for the first render I'd like to be able to display some kind of skeleton.

The possibilities with this change are huge: we can even make some kind of i18n-WYSIWYG.



ilyadoroshin: @andrii-bodnar could you, please, check? and merge if it's ok?)
andrii-bodnar: Hi @ilyadoroshin!

Unfortunately, I have no permission to review and merge PRs. Probably, someone else should look into this
ilyadoroshin: @semoal hi, perhaps you could check'n'merge? 
thanks!

Patch:
diff --git a/docs/misc/showroom.rst b/docs/misc/showroom.rst
index 2386d23a..61af941b 100644
--- a/docs/misc/showroom.rst
+++ b/docs/misc/showroom.rst
@@ -20,3 +20,4 @@ Feel free to `send a PR <https://github.com/lingui/js-lingui/issues/new>`__ to l
 - `mapflow.ai <https://mapflow.ai>`__
 - `Ascendero.com <https://ascendero.com/>`__
 - `InterMenu.com <https://intermenu.com>`__
+- `Fider <https://fider.io/>`__ (`source <https://github.com/getfider/fider>`__)
diff --git a/docs/ref/react.rst b/docs/ref/react.rst
index 01170fe4..3d4978c5 100644
--- a/docs/ref/react.rst
+++ b/docs/ref/react.rst
@@ -171,6 +171,32 @@ I18nProvider
 components. :ref:`Rendering of translations <rendering-translations>` is explained
 at the beginning of this document.
 
+.. code-block:: 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 DefaultI18n = ({ isTranslated, children }) => (
+      <span style={{ color: isTranslated ? undefined : 'red' }}>
+         {children}
+      </span>
+   )
+
+   const App = () => {
+      return (
+         <I18nProvider i18n={i18n} defaultComponent={DefaultI18n}>
+            // rest of the app
+         </I18nProvider>
+      );
+   }
+
 ``forceRenderOnLocaleChange`` is true by default and it ensures that:
 
   - Children of ``I18nProvider`` aren't rendered before locales are loaded.
diff --git a/docs/tutorials/react-native.rst b/docs/tutorials/react-native.rst
index d24310a7..598d5843 100644
--- a/docs/tutorials/react-native.rst
+++ b/docs/tutorials/react-native.rst
@@ -70,6 +70,17 @@ Introducing internationalization
 
 Not surprisingly, this part isn't too different from the :ref:`React tutorial <react-tutorial-label>`.
 
+⚠️ make sure to update ``metro.config.js`` resolvers with ``mjs`` extension to avoid `this problem <https://github.com/eemeli/make-plural/issues/15>`_:
+
+.. code-block:: jsx
+
+  resolver: {
+   sourceExts: ['js', 'ts', 'tsx', 'mjs'],
+  },
+
+
+
+
 Let's use the :jsxmacro:`Trans` macro first. Don't forget that we need to wrap our root
 component with the :component:`I18nProvider` so we can set the active language
 and load catalogs:
diff --git a/packages/react/src/I18nProvider.tsx b/packages/react/src/I18nProvider.tsx
index 17a42b2c..be2b4ee9 100644
--- a/packages/react/src/I18nProvider.tsx
+++ b/packages/react/src/I18nProvider.tsx
@@ -1,9 +1,10 @@
-import React, { FunctionComponent, ComponentType, ReactNode } from "react"
+import React, { ComponentType, FunctionComponent } from "react"
 import { I18n } from "@lingui/core"
+import { TransRenderProps } from "./Trans"
 
 export type I18nContext = {
   i18n: I18n
-  defaultComponent?: ComponentType<{ children?: ReactNode }>
+  defaultComponent?: ComponentType<TransRenderProps>
 }
 
 export type withI18nProps = {
diff --git a/packages/react/src/Trans.tsx b/packages/react/src/Trans.tsx
index af379d15..339d0950 100644
--- a/packages/react/src/Trans.tsx
+++ b/packages/react/src/Trans.tsx
@@ -8,6 +8,7 @@ export type TransRenderProps = {
   translation?: React.ReactNode
   children?: React.ReactNode
   message?: string | null
+  isTranslated?: boolean
 }
 
 export type TransProps = {
@@ -72,6 +73,13 @@ export function Trans(props: TransProps): React.ReactElement<any, any> | null {
   const FallbackComponent = (defaultComponent ||
     React.Fragment) as React.ComponentType<any>
 
+  const i18nProps = {
+    id,
+    message,
+    translation,
+    isTranslated: id !== translation && message !== translation,
+  }
+
   // Validation of `render` and `component` props
   if (render && component) {
     console.error(
@@ -87,22 +95,24 @@ export function Trans(props: TransProps): React.ReactElement<any, any> | null {
     console.error(
       `Invalid value supplied to prop \`component\`. It must be a React component, provided ${component}`
     )
-    return <FallbackComponent>{translation}</FallbackComponent>
+    return <FallbackComponent {...i18nProps}>{translation}</FallbackComponent>
   }
 
   // Rendering using a render prop
   if (typeof render === "function") {
     // Component: render={(props) => <a title={props.translation}>x</a>}
-    return render({
-      id,
-      translation,
-      message,
-    })
+    return render(i18nProps)
   }
 
   // `component` prop has a higher precedence over `defaultComponent`
   const Component = (component || FallbackComponent) as React.ComponentType<any>
-  return <Component>{translation}</Component>
+  const DefaultComponent = defaultComponent
+
+  return DefaultComponent && !component ? (
+    <DefaultComponent {...i18nProps}>{translation}</DefaultComponent>
+  ) : (
+    <Component>{translation}</Component>
+  )
 }
 
 Trans.defaultProps = {


Test patch:
diff --git a/packages/react/src/Trans.test.tsx b/packages/react/src/Trans.test.tsx
index e0d47212..0b89b813 100644
--- a/packages/react/src/Trans.test.tsx
+++ b/packages/react/src/Trans.test.tsx
@@ -172,6 +172,7 @@ describe("Trans component", function () {
         id: "ID",
         message: "Default",
         translation: "Translation",
+        isTranslated: true,
       })
     })
 
@@ -241,4 +242,54 @@ describe("Trans component", function () {
       expect(element).toEqual(`<div>value</div>`)
     })
   })
+
+  describe("I18nProvider defaulComponent accepts render-like props", function () {
+    const DefaultComponent: React.FunctionComponent = (props: {
+      children?: React.ReactNode
+      id: string
+      translation: string
+      message: string
+      isTranslated: boolean
+    }) => (
+      <>
+        <div data-testid="children">{props.children}</div>
+        {props.id && <div data-testid="id">{props.id}</div>}
+        {props.message && <div data-testid="message">{props.message}</div>}
+        {props.translation && (
+          <div data-testid="translation">{props.translation}</div>
+        )}
+
+        <div data-testid="is-translated">{String(props.isTranslated)}</div>
+      </>
+    )
+
+    it("should render defaultComponent with Trans props", function () {
+      const markup = render(
+        <I18nProvider i18n={i18n} defaultComponent={DefaultComponent}>
+          <Trans id="ID" message="Some message" />
+        </I18nProvider>
+      )
+
+      expect(markup.queryByTestId("id").innerHTML).toEqual("ID")
+      expect(markup.queryByTestId("message").innerHTML).toEqual("Some message")
+      expect(markup.queryByTestId("translation").innerHTML).toEqual(
+        "Translation"
+      )
+      expect(markup.queryByTestId("is-translated").innerHTML).toEqual("true")
+    })
+
+    it("should pass isTranslated: false if no translation", function () {
+      const markup = render(
+        <I18nProvider i18n={i18n} defaultComponent={DefaultComponent}>
+          <Trans id="NO_ID" message="Some message" />
+        </I18nProvider>
+      )
+
+      expect(markup.queryByTestId("id").innerHTML).toEqual("NO_ID")
+      expect(markup.queryByTestId("translation").innerHTML).toEqual(
+        "Some message"
+      )
+      expect(markup.queryByTestId("is-translated").innerHTML).toEqual("false")
+    })
+  })
 })