Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use imperative API in a non-component script #416

Closed
Danita opened this issue Apr 18, 2016 · 64 comments
Closed

How to use imperative API in a non-component script #416

Danita opened this issue Apr 18, 2016 · 64 comments

Comments

@Danita
Copy link

Danita commented Apr 18, 2016

As I understand injectIntl provides the imperative API inside a React component class, but I couldn't find a way to use that API ouside a component, for example in a helper or a utility class. Is there any way to do so?

@ericf
Copy link
Collaborator

ericf commented Apr 18, 2016

You'd have to use dependency injection to pass the methods into your utility for it to use. Or drop to a lower-level using the Intl APIs built into JS or the ones we created for message and relative-time formatting: https://github.com/yahoo/react-intl/wiki/API

It's possible that we'll create a proper FormatJS lib which contains much of what's in the src/format.js and src/components/provider.js files, but it's not something I'm working on yet.

Note though that one of the main reasons for react-intl is to bring formatting as close to the UI definition as possible. So I'm curious to know what you're looking to use in a utility that doesn't end up being rendered in a React component.

@gustavnikolaj
Copy link

I have the same issue.

I have some action definitions, where I'd want them to have a property messages, which is an object of messages to show in the notifications component, if the action succeeds, fails, or is taking a long time to complete. A piece of action middleware, will translate those message properties into actions that add / remove them from a notification store.

I understand your argument about making it a UI concern and get it as close to where it's displayed as possible, but the tight coupling to the UI makes it a bit awkward to work with for this particular case.

Move the message definition to the notifications component and using identifiers for the messages would work, but that will result in a giant defineMessages object with all the notification messages, out side of their original context.

This is about the only use case I can think of (I'm sure there are others :)), where it's not better to use the HoC and components.

@ericf
Copy link
Collaborator

ericf commented Apr 20, 2016

Why not pass a <FormattedMessage> React element in the action payload? Or even create the element in the store that's handling the action?

@gustavnikolaj
Copy link

That's what we're doing now, and that means that we have the source text for the translation string in the component and not in the action where it belongs (in this particular case - it's a generic and I don't like having to duplicate the translation string into all places where the action is called).

I have resorted to something like the below, in another case where I needed access to the context outside of a React Component:

const intlProvider = new IntlProvider({ locale: localeId, messages }, {});
const { intl } = intlProvider.getChildContext();

But it's inconvenient, as you have to pass the localeId and messages through.

I guess this is a problem that can't be solved without introducing a singleton or accepting that you have to pass a representation of the context around (at least the localeId and messages).

What is your opinion about the above snippet? Is it kosher?

@gustavnikolaj
Copy link

Another use case is in our server, where we use a react-based email templating library. It requires the subject and other headers to be passed in as arguments, and uses JSX to compose the mail body. The subject thus needs to be passed using intl.formatMessage outside of the <IntrlProvider> scope.

@ericf
Copy link
Collaborator

ericf commented Apr 20, 2016

@gustavnikolaj I'm not sure I follow. Can you provide more example code so I can better understand the action use case and how you end up with duplicated strings?

You can use defineMessages or <FormattedMessage> in any .js file that you process with Babel and use babel-plugin-react-intl to extract the messages from source. If you need a way for an action payload or store to describe a rich text message, using React Components to do that works.

I also don't understand why the email template needs to be rendered outside of an <IntlProvider>.

@gustavnikolaj
Copy link

  • We have a redux react setup.
  • We have a reducer that contains a list of active notifications
  • We have a Root component that sets up page wrap stuff - like the IntlProvider, and a notifications overlay.

A Notification can be triggered from any connected component, that dispatch an action to the store, adding a notification to the list.

If we have a single abstract action for adding a notification it might look like this:

this.props.dispatch(addNotification({
    message: this.props.intl.formatMessage(messages.notificationMessage),
});

What I would like to do is creating a notification wrapper that contains the message, so you could do this:

this.props.dispatch(addFooNotification());

Where the action addFooNotification had the message definition inside it. The problem with this is, that the translated messages are not available unless you are in the IntlProvider context, so for this to work, currently, I would have to add an IntlProvider in the action - maybe like so:

const messages = defineMessages({ /* ... */ });

function addFooNotification(locale, messagesForContext) {
    const intlProvider = new IntlProvider({ locale, messages: messagesForContext }, {});
    const { intl } = intlProvider.getChildContext();

    return {
        type: FOO,
        message: intl.formatMessage(messages.notificationMessage)
    };
}

With regards to the emails, a crude example might look something like this:

var messageBody = render(
  <EmailMessage>
    <h1><FormattedMessage defaultMessage='Foo' id='email.title' /></h1>
  </EmailMessage>
);

mailerService.send({
  headers: {
    to: recipient,
    subject: intl.formatMessage({ id: 'email.subject', defaultMessage: 'Foobar' })
  },
  body: messageBody
});

@gustavnikolaj
Copy link

I hope this makes it more concrete :-)

@smashercosmo
Copy link

Yes, we also have a lot of messages in our actions and selectors. Surely, there should a way to use format utils outside of react components.

@ericf
Copy link
Collaborator

ericf commented May 1, 2016

@smashercosmo you can use the Intl APIs built into JavaScript, and core FormatJS libs.

Why do you have lots of messages that you need to format in your actions? Can't you wait to format those messages when they end up in a React component tree? I also think using JSX to create React elements inside an action or store works well for describing rich text, localized messages; you could almost think of it as a thunk.

@smashercosmo
Copy link

@ericf Could you elaborate on how to do it as straightforward as possible? Without redoing all the work that react-intl is doing for you. Without, for example, reimplementing formatMessage function from react-intl.

@ericf
Copy link
Collaborator

ericf commented May 3, 2016

@smashercosmo You'd have to explain more about your setup, see my questions above.

@smashercosmo
Copy link

@ericf ok, here is an example: we have a notification system in our app. And these notifications are triggered in the action creators. So, for example, when user updates his profile, then USER_PROFILE_UPDATED_SUCCESS action is invoked and also NOTIFICATION_ADDED. NOTIFICATION_ADDED action has the actual message to show in its payload. And this text of course should be translated according to current locale.

@ericf
Copy link
Collaborator

ericf commented May 11, 2016

@smashercosmo so whatever is creating the NOTIFICATION_ADDED action, could put the message text on the payload as a React element created using JSX syntax, since it would represent the description (element descriptor) of the rich-text, localized message that should be displayed/rendered by some <Notification> component which is connected to the store holding the notification payload that was just added.

@smashercosmo
Copy link

@ericf well, the only problem with this approach is that third-party Notification component that we use accepts message prop only in the string form.

@ericf
Copy link
Collaborator

ericf commented May 11, 2016

@smashercosmo if it's rendering that prop as a child, then it should be switched to using PropTypes.node to be more flexible and correct 😄

@smashercosmo
Copy link

@ericf yep, surely it should) but anyway, I have irrational feeling that passing React components as arguments to actions is semantically wrong, because actions should deal with pure data, because this data often goes to app state and app state should be fully serializable.

@ericf
Copy link
Collaborator

ericf commented May 14, 2016

@smashercosmo what data model would you use to represent rich text messages passed as a payload on an action? I'd argue that React elements is a great data structure for this, more capable than a string of HTML.

@lauterry
Copy link

Hi everyone

Another use case of using imperative API is when you need to translate redux-form validation error messages.

See validate function of this redux-form

In the validate function, you return several potential error messages where you need to translate using imperative API since you're outside of a react component.

How would you handle that ?

Best regards

@smashercosmo
Copy link

@lauterry yep, we also faced that. We ended up in passing intl in validation function through own props.

@smashercosmo
Copy link

@ericf the thing is, that I don't want to pass rich text messages to actions. I want only simple strings to be be passed. And these strings could be in some cases saved to app state or sent to server for example. So that's why I don't understand, why can't we have formatMessage function, that will accept message object as a first param and locale as a second param?

@lauterry
Copy link

Hi @smashercosmo @ericf

I ended up with the following workaround :

In the validate function, instead of providing error string messages, you provide the id of the message

const validate = values => {
  const errors = {}
  if (!values.username) {
    errors.username = 'username.error.required'
  } else if (values.username.length > 15) {
    errors.username = 'username.error.length'
  }
  return errors
}

And you use injectIntl to provide intl in your component.

<input  type="text" {...username}/>
{ username.touched && username.error ? { intl.formatMessage({id: username.error}) } : false }

The problem with that solution is that babel-plugin-react-intl is not able to collect thoses messages since it is outside of a React Component.

What we need with this solution is to find a way to make babel-plugin-react-intl collect messages defined outside of a React Component

@smashercosmo
Copy link

@lauterry in that case I think, it should be something like

import usernameValidationMsg from './usernameValidationMsg';

const validate = values => {
  const errors = {}
  if (!values.username) {
    errors.username = usernameValidationMsg.required // <= we're passing message object here
  } else if (values.username.length > 15) {
    errors.username = usernameValidationMsg.length // <= we're passing message object here
  }
  return errors
}
<input  type="text" {...username}/>
{ username.touched && username.error && intl.formatMessage(username.error) }

But I'm not sure that redux-form won't fail if you set error message as an object instead of a string

@lauterry
Copy link

@smashercosmo

What does your usernameValidationMsg look like ?

@smashercosmo
Copy link

smashercosmo commented May 20, 2016

@lauterry this is the file where you messages are defined using defineMessage function. Of course you can define them right in the component file.

@lauterry
Copy link

@smashercosmo
I don't see how this file is created using defineMessage function ?

Can you explain me please ?

@smashercosmo
Copy link

@lauterry seems like this is not the right place for such kind of conversation) I answered you in twitter.

@ericf
Copy link
Collaborator

ericf commented May 21, 2016

I think the cleanest way might be to refactor IntlProvider to use some factory function to construct the intl object bound to an intl context (I.e. The props passed to IntlProvider), and then this function could be an export of the lib as well.

@smashercosmo @Danita do you think that could work?

@Danita
Copy link
Author

Danita commented May 21, 2016

Yes! I think that would be a good solution. I have been trying to do something like that myself but I couldn't wrap my head around the code.

@gezichenshan
Copy link

gezichenshan commented Sep 1, 2016

Hi,
when I run,

const messages = defineMessages({
    sms: {
        id: 'sign-up.sms'
    }
})
const intlProvider = new IntlProvider({locale: 'zh-CN'}, {}); 
const {intl} = intlProvider.getChildContext();
console.log(intl.formatMessage(messages.sms))

I got "[React Intl] Missing locale data for locale: "zh-CN". Using default locale: "en" as fallback.".
Anybody knows why?

@ericf
Copy link
Collaborator

ericf commented Sep 1, 2016

@gezichenshan https://github.com/yahoo/react-intl/wiki#loading-locale-data

@rszczypka
Copy link

I have done it like that using Intl: https://www.npmjs.com/package/intl

import { translateField as t } from 'Intl/IntlSetup';
const tableColumns = [
    {
        property: 'positionNumber',
        header: {
            label: t('Positions.Label.positionNumber')
        },
        cell: {
            formatters: [search.highlightCell]
        }
    },
    {
        property: 'damageDate',
        header: {
            label: t('Positions.Label.damageDate')
        },
        cell: {
            formatters: [search.highlightCell]
        }
    },
]

@tomasfrancisco
Copy link

tomasfrancisco commented Dec 30, 2016

Another use case using react-redux-toastr:

I want to be able to call a state change outside of a component, using i18n, for example, within a saga or action:

toastr.error(formatMessage(messages.userNotFoundErrorLabel));

EDITED:

I was able to call redux-toastr and intl on componentDidUpdate(), where I can make changes on redux state, but after the notification I had to clean the error details from the redux state, so the toastr never gonna be triggered again.

@cwtuan
Copy link

cwtuan commented Jun 14, 2017

@Danita
If you want to Internationalize non-compoent type functions, try alibaba/react-intl-universal. It Internationalize React apps not only for React.Component but also for Vanilla JS.

They have compatible APIs such as formatMessage and formatHTMLMessage.

@oyeanuj
Copy link

oyeanuj commented Dec 3, 2017

Hi @ericf, just looping back in here to see if this is still on the radar or the context solution is the recommended one?

@soundyogi
Copy link

+1

@SimonSomlai
Copy link

No sure if people are still looking for a solution. But, here's how we solved this problem;

1) First we create a 'IntlGlobalProvider' component that inherits the context and props from the IntlProvider in our component tree;

<ApolloProvider store={store} client={client}>
  <IntlProvider>
      <IntlGlobalProvider>
          <Router history={history} children={routes} />
      </IntlGlobalProvider>
  </IntlProvider>
</ApolloProvider>

2) (inside IntlGlobalProvider.js) Then out of the context we get the intl functionality we want and expose this by a singleton.

// NPM Modules
import { intlShape } from 'react-intl'

// ======================================================
// React intl passes the messages and format functions down the component
// tree using the 'context' scope. the injectIntl HOC basically takes these out
// of the context and injects them into the props of the component. To be able to 
// import this translation functionality as a module anywhere (and not just inside react components),
// this function inherits props & context from its parent and exports a singleton that'll 
// expose all that shizzle.
// ======================================================
var INTL
const IntlGlobalProvider = (props, context) => {
  INTL = context.intl
  return props.children
}

IntlGlobalProvider.contextTypes = {
  intl: intlShape.isRequired
}

// ======================================================
// Class that exposes translations
// ======================================================
var instance
class IntlTranslator {
  // Singleton
  constructor() {
    if (!instance) {
      instance = this;
    }
    return instance;
  }

  // ------------------------------------
  // Formatting Functions
  // ------------------------------------
  formatMessage (message, values) {
    return INTL.formatMessage(message, values)
  }
}


export const intl = new IntlTranslator()
export default IntlGlobalProvider

3) Import it anywhere as a module

import { defineMessages } from 'react-intl'
import { intl } from 'modules/core/IntlGlobalProvider'

const intlStrings = defineMessages({
  translation: {
    id: 'myid',
    defaultMessage: 'Hey there',
    description: 'someStuff'
  },

intl.formatMessage(intlStrings.translation)

@sergioviniciuss
Copy link

sergioviniciuss commented May 3, 2018

In my case, I had some labels set in a separated js file outside react components, so I followed the same approach @smashercosmo mentioned before..

import { IntlProvider, addLocaleData } from 'react-intl';
import localeDataDE from 'react-intl/locale-data/de';
import localeDataEN from 'react-intl/locale-data/en';
import formMessages from '../../../store/i18n/formMessages';
import Locale from '../../../../utils/locale';

addLocaleData([...localeDataEN, ...localeDataDE]);
const locale = Locale.setLocale();
const messages = Locale.setMessages();
const intlProvider = new IntlProvider({ locale, messages });
const { intl } = intlProvider.getChildContext();

export const BLABLA = {
  salutation: {
    label: intl.formatMessage(formMessages.retailSalutationLabel),
    errormessages: {
      required: intl.formatMessage(formMessages.retailSalutationError),
    },
  },
//...

@RahulChundeth
Copy link

IntlGlobalProvider.contextTypes = { intl: intlShape.isRequired }

Im getting this error
Uncaught TypeError: Cannot read property 'formatMessage' of undefined
at IntlTranslator.formatMessage (IntlGlobalProvider.js:41)
at Object../src/constants/messageConstants.js (messageConstants.js:13)
at webpack_require (bootstrap 9b71e3856537abd92b89:678)
at fn (bootstrap 9b71e3856537abd92b89:88)
at Object../src/helpers/apiHelper.js (apiHelper.js:7)
at webpack_require (bootstrap 9b71e3856537abd92b89:678)
at fn (bootstrap 9b71e3856537abd92b89:88)
at Object../src/helpers/index.js (index.js:2)
at webpack_require (bootstrap 9b71e3856537abd92b89:678)
at fn (bootstrap 9b71e3856537abd92b89:88)

@andreialecu
Copy link

andreialecu commented Oct 9, 2018

Here's a similar approach to the one posted by @SimonSomlai (note, uses typescript):

import { Component } from 'react';
import { InjectedIntl, InjectedIntlProps, injectIntl } from 'react-intl';

/**
 * Should only use this when not inside a React component (such as redux actions), see:
 * https://github.com/yahoo/react-intl/issues/416
 */
export let intl: InjectedIntl = null;

class IntlGlobalProvider extends Component<InjectedIntlProps> {
  constructor(props: any) {
    super(props);
    intl = this.props.intl;
  }

  public render() {
    return this.props.children;
  }
}

export default injectIntl(IntlGlobalProvider);

Usage:

 import IntlGlobalProvider from '../core/globalIntl';

 <IntlProvider>
      <IntlGlobalProvider>
          <YourAppRoot />
      </IntlGlobalProvider>
  </IntlProvider>

Then anywhere you may need, such as in a redux action:

import { intl } from '../core/globalIntl';
...
const messages = defineMessages({
  errorTitle: {
    id: 'errors.title',
    defaultMessage: 'Error',
  },
});
...
intl.formatMessage(messages.errorTitle)

@phifa
Copy link

phifa commented Nov 8, 2018

@SimonSomlai INTL is undefined when using your solution. Any idea why? Could this be related to the new context API?

@kuby
Copy link

kuby commented Nov 26, 2018

@phifa I had the same problem and the reason it is happening is that you are probably trying to execute the formatMessage from the intl instance in some util/handler function which is being called immediately when you run the application, but at this point the intl is undefined, because the IntlGlobalProvider haven't been rendered. Try to wrap your formatMessage call in a timer, and it should work.

Example:

import { intl } from '../core/globalIntl'

const myUtilMethod = () => {
   setTimeout(() => {
      const { formatMessage } = intl
      formatMessage(messages.errorTitle)
   }, 0)
}

For me this solved the issue.

@stale
Copy link

stale bot commented May 30, 2019

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label May 30, 2019
@stale
Copy link

stale bot commented Jun 6, 2019

Issue was closed because of inactivity. If you think this is still a valid issue, please file a new issue with additional information.

@stale stale bot closed this as completed Jun 6, 2019
@stale
Copy link

stale bot commented Jun 13, 2019

Issue was closed because of inactivity. If you think this is still a valid issue, please file a new issue with additional information.

@rubayethossain
Copy link

Just want to keep this link here if anyone needs. react-intl provides an api called createIntl which can help you to use the library in non-compoent type functions (e.g. Redux actions )

https://github.com/formatjs/react-intl/blob/master/docs/API.md#createintl

@lauterry
Copy link

@rubayethossain Thx for the link. How would you use it ? Do you have an example ?

@baleato
Copy link

baleato commented Apr 2, 2023

  const intl = createIntl({ locale: 'es', messages: messagesInSpanish })
  // ...
  intl.formatMessage({ id: 'seo.title' })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests