lingui / js-lingui

🌍 📖 A readable, automated, and optimized (3 kb) internationalization for JavaScript
https://lingui.dev
MIT License
4.51k stars 378 forks source link

React Hooks support #390

Closed aralroca closed 5 years ago

aralroca commented 5 years ago

I'd like to start a discussion about react hooks in lingui-react. I know that at this moment is just in alpha, but maybe is time to start thinking about a better alternative of i18n renderprops.

IMO, I don't like this alternative:

function Example(){
  return (
    <I18n>
        {({ i18n }) => (
           <img src="..." alt={i18n._(t`Image caption`)} />
         )}
    </I18n>
  )
}

The reasons are:

A better alternative can be:

function Example(){
  const alt = useT('Image caption')

  return (
     <img src="..." alt={alt} />
  )
}

What do you think about this?

tricoder42 commented 5 years ago

Hey @aralroca, thank you for suggestion. I already mentioned hooks briefly in my talk at React Conf 2018, but let's give it a proper discussion here.

In upcoming v3 we're planning to abandon both HOC and render prop component. They both have the same purpose: consume i18n object from context and provide it to child component. Question is (and this question was asked few times, e.g: #46), why can't we access i18n object directly, like so:

function Example(){
  const alt = i18n._('Image caption')

  return (
     <img src="..." alt={alt} />
  )
}

The only reason is re-rendering - when you change the active language or re-load message catalogs, ideally you would like to re-render your translations. HOC and render prop component take care of this. However, we figure out that in practice this happens rarely and when you switch the locale, it's safe to re-render whole app.

So, instead of context we're planning to use global i18n object:

// Lingui v2.x
import { I18n } from "@lingui/react"
import { t } from "@lingui/macro"

function Example(){
  return (
    <I18n>
        {({ i18n }) => (
           <img src="..." alt={i18n._(t`Image caption`)} />
         )}
    </I18n>
  )
}

// Lingui v3.x
import { t } from "@lingui/macro"

function Example(){
  const alt = t`Image caption`

  return (
     <img src="..." alt={alt} />
  )
}

t macro is transformed into i18n._ call, where i18n is imported from @lingui/core. There's gonna be an escape hatch in macro configuration if you don't like to use global i18n object (e.g. if you prefer to use your own instance of i18n for whatever reason).

Hooks

To sum up previous section, there won't be no HOC or render props in next version.

So what about hooks? My first impression was skip them completely, but I think we could provide a single hook, which would trigger re-render. This hook would be used only once in the root of your app like this:

import { useLingui } from "@lingui/react"

function App() {
  const { locale } = useLingui()
  // When locale changes, the whole App re-renders
  return <App key={locale}>...</App>

Everywhere else you would be fine with macros:

import { t, Trans } from "@lingui/macro" 

function Component() {
  const title = t`Header title`
  return <h1 title={title}><Trans>Hello World</Trans></h1>
}

Macros

Transforming macros into i18n._ calls is just one thing. The main purpose is to transform JSX or tagged template literals into ICU Message Format. That includes plurals and date/number formatting:

import { plural } fro "@lingui/macro"

const bottles = plural({
  value: numBottles,
  one: `# bottle is hanging on the wall`,
  other: `# bottles are hanging on the wall`
})

// Under the hood it's the same as
import i18n from "@lingui/core"

const bottles = i18n._(
  "{numBottles, plural, one {# bottle is hanging on the wall} other {# bottles are hanging on the wall}}",
  { numBottles }
)

The same API can be used also in projects without React.

However in JSX it's more convenient to use components instead, which allows us to use inline components without any limitations:

import { Trans } from "@lingui/macro"

const Link = () => <Trans>Read the <a href="/docs">full story</a>.</Trans>

// Under the hood it's transformed into
import { Trans } from "@lingui/react"

const Link = () => (
  <Trans 
    id="Read the <0>full story</0>." 
    components={[<a href="/docs" />]} 
  />
)

Conclusion

That's why I don't want to use hooks for translation:

On the other hand, I'm open to useLingui hook which returns active locale that could trigger re-render on locale change.

What's your opinion about all of this?

aralroca commented 5 years ago

@tricoder42 I agree with all you said. 😊

My proposal was in order to keep the same functionality that the current HOC and RenderProps.

However, after have knowledge about v3 I think different. As you said, the case of re-render the translations is very rare. So I agree to use macros directly instead.

About useLingui on top of the app I also agree. I guess that for now is the only possible hook here.


BTW, I have a question about your elegant example:

import { t } from "@lingui/macro"

function Example(){
  const alt = t`Image caption`

  return (
     <img src="..." alt={alt} />
  )
}

Is there some inconvenient to use it outside the component?

import { t } from "@lingui/macro"

const alt = t`Image caption`

function Example(){
  return (
     <img src="..." alt={alt} />
  )
}

In this case, I understand that now there is no need to re-render.

aralroca commented 5 years ago

Sorry, I closed the issue unintentionally. I re-opened again.

About the issue, maybe we should go in the useLingui direction

tricoder42 commented 5 years ago

Is there some inconvenient to use it outside the component?

import { t } from "@lingui/macro"

const alt = t`Image caption`

function Example(){
  return (
     <img src="..." alt={alt} />
  )
}

It is, actually! If you use it outside component, then the translation is evaluated only once when the module is loaded. Most probably the translations won't be loaded at the time.

This is a problem and we still need to figure out how to handle it. More common usecase are translations in default props:

import { t } from "@lingui/macro"

function Example({ alt }){
  return (
     <img src="..." alt={alt} />
  )
}

Example.defaultProps {
  alt: t`Image caption`
}

It's the same problem. When module is loaded, Image caption is translated, but at that point we might not know the active locale or message catalogs aren't loaded. Usually this is solved using lazy translations, i.e.: you define that this string should be translated, but it isn't translated immediately:

import { t } from "@lingui/macro"

function Example({ alt }){
  return (
     <img src="..." alt={alt} />
  )
}

Example.defaultProps {
  // example, the API isn't final yet
  alt: t.lazy`Image caption`
}

// under the hood it's translated in something like this:
// alt: () => i18n._('Image caption')
stereobooster commented 5 years ago

Hooks will be trivial if we will migrate to new context, then we can do

export const useI18n = () => useContext(Context)
sourcecaster commented 5 years ago

Since linguijs v.2 doesn't operate with React context but with Legacy Context which is obsolete it seems you can't get I18nProvider context via hooks. But still there are 2 ways of getting i18n object:

I've got used to CoffeScript, so...

import {withI18n} from "@lingui/react"

MyComponent = withI18n() (props) =>
    {i18n} = props
    <span>{i18n._ 'Whatever you want'}</span>

There is another way though. But I would not recommend it for the obvious reason:

import PropTypes from 'prop-types'

MyComponent = (props, context) =>
    i18n = context.linguiPublisher.i18n
    <span>{i18n._ 'Whatever you want'}</span>

MyComponent.contextTypes =
    linguiPublisher: PropTypes.object

It will work... but don't do that :)

revskill10 commented 5 years ago

I prefer this API:

const i18n = useI18n();
return <span>{i18n._(t`Hello world`)}</span>

It's just the same as <I18n> render props api.

tricoder42 commented 5 years ago

useLingui hook was already added to next version. See #334 if you want to test it.

ignatevdev commented 5 years ago

In the migration guide from v2 to v3 there is a line stating that withI18n HOC will be removed in favor of useLingui hook.

Does that mean that we would not be able to update our project using class components to the new version of lingui in the future?

tricoder42 commented 5 years ago

@NSLS It just means that you have to implement your own withI18n using useLingui hook if you want to use class components:

const withI18n = WrappedComponent => props => {
  const { i18n } = useLingui()
  return <WrappedComponent i18n={i18n} {...props} />
}
N1kto commented 4 years ago

Hello guys. Just started using this amazing package and have a question concerning react hooks.

Though it's passed almost a year now since the last message in this thread, the current stable version is still 2.x (2.9.1), meaning there are still now hooks. However, it's fairy simple to create one (hook) yourself. As mentioned, I've just started using the package and might not know all the details, could you please advise if the following hooks implementation is safe to use in production.

  1. Init i18n in a separate module to be used across the whole app (not only within react components)
    
    // i18n.js
    import { setupI18n } from '@lingui/core';
    import deTranslation from './locales/de/messages';

export default setupI18n({ language: 'de', catalogs: { de: deTranslation }, });


2. Pass initiated `i18n` instance to `I18nProvider`:

// index.jsx ReactDOM.render(

, document.getElementById('root') ); ``` 3. Define a `useI81n` hook: ``` // useI18n.js import i18n from '../i18n'; export function useI18n() { return i18n; } ``` At this point, should I use `useI18n` hook or the `Trans` component from `@lingui/macro`, they both will utilize the same `i18n` instance, won't they? The thing which concerns me is this `language` option/prop I pass to `setupI18n` and to `I18nProvider`, they should be in sync, right? And what will happen if I have a multi-locale app, and switch the language to say `en`? For `I18nProvider` it's clear that all the components beneath it using `Trans` will get messages updated to corresponding language (at least this is my expectation), but what about the ones using the hook? In other words, does the change of the `language` prop on `I18nProvider` set it on the `i18n` instance powering it under the hood? I'd really want to use a separately initiated instance of `i18n` both for `I18nProvider` and for other non-react parts of the application.
oceantume commented 4 years ago

I found a pretty simple solution to replace the withI18n hoc with a useI18n hook.

I'm using this amazing library in a new React project, but I absolutely did not want to rely on Higher Order Components because they create messy components files that are hard to reason about.

Here's the heavily commented solution in TypeScript that I came up with. It basically leverages React context to propagate the i18n object value and any forced update that you would get when using withI18n (for example when changing language or dynamically loading a new catalog).

import React from 'react';
import { withI18n, withI18nProps } from "@lingui/react"
import { I18n } from "@lingui/core"

/**
 * This is a hack that exposes LinguiJS's i18n object as a context value so
 * it can be consumed by a hook instead of relying on the withI18n HOC.
 *
 * Make sure this is lower in the tree than your I18nProvider but higher than
 * any component that calls `useI18n`.
 * 
 * This component works by intercepting the value provided by withI18n and
 * passing it to its consumers in a unique object every time the component
 * renders. Because we create a unique object every time, the consumers are
 * properly updated when language is changed.
 */
export const I18nHookHackProvider = withI18n()<withI18nProps>(({ i18n, children }) => {
  return (
    <i18nHookContext.Provider value={{ i18n }}>
      {children}
    </i18nHookContext.Provider>
  )
})

/**
 * React hook that gets the current i18n value of LinguiJS and properly
 * updates the component when required, like withI18n would.
 *
 * This offer a syntax that is similar to what we will get in Lingui v3
 * except it will most likely look like `const { i18n } = useLingui()`.
 */
export const useI18n = () => React.useContext(i18nHookContext).i18n

/**
 * The context that is used to propagate i18n object and updates.
 */
const i18nHookContext = React.createContext<{ i18n: I18n }>({} as any)

Here's a basic example of how you can use it:

// index.ts
const App = () => (
  <I18nProvider language="en" catalogs={catalogs}>
    <I18nHookHackProvider>
      <AppContent />
    </I18nHookHackProvider>
  </I18nProvider>
)

render(<App />, document.getElementById('app'))

// my-component.ts
const MyComponent = () => {
  const i18n = useI18n();

  return (
    <input placeholder={i18n._(t`Enter your username`)} />
  );
}