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

Passing <FormattedMessage> to a component inside a nested <IntlProvider> #419

Closed
wrwrwr opened this issue Apr 19, 2016 · 14 comments
Closed

Comments

@wrwrwr
Copy link

wrwrwr commented Apr 19, 2016

With 2-beta I used to import formatMessage() and its siblings from react-intl/lib. Obviously that was undocumented, unsupported, etc. Nevertheless, it may have been the best approach to toy with the config parameter that the format* functions get bound to during IntlProvider child context construction (I've actually tried a couple of different options with less pleasing outcomes).

Now the only way to reuse these functions seems to be to check out the sources, import from react-intl/src and make some adjustments to ones transpiling configuration. Not really an issue, except that it results in increased size of the dependent module -- as it likely means bundling a significant part of react-intl.

That's a pretty obscure case, but how about exporting the format* functions, or even better making the unbound versions available from the config object (or somewhere else)? Otherwise, just count this as an example for #416.

@ericf
Copy link
Collaborator

ericf commented Apr 19, 2016

You shouldn't be using the src/ files, they are only there for the source maps, but maybe should be removed from npm package. Can you explain why you want these functions to be exposed? What's the concrete use case?

@wrwrwr
Copy link
Author

wrwrwr commented Apr 19, 2016

Yup, I know I should not, nor the former lib/format probably :-)

The aim or use case is to introduce namespaces. In short, to allow inserting additional messages and formats scoped to a component (or some other entity), and to let translation components declare which namespace they belong to (thus which message dictionary should be used).

More specifically, the idea is to provide a custom provider that switches message and format dictionaries dynamically, depending on the namespace a given message belongs to.
The provider can be realized by wrapping the format* methods living on the intl object -- just to pass them a modified config. You can't "unbind" the config however, hence the original functions are needed.

I believe the first example at react-intl-ns may clarify the general design. The only code piece that uses those functions is the one I've linked above.

@ericf
Copy link
Collaborator

ericf commented Apr 19, 2016

Have you tried nesting <IntlProvider>s to accomplish this?

@wrwrwr
Copy link
Author

wrwrwr commented Apr 20, 2016

Nesting providers is nice to have, but that's another thing. For instance take a generic container component. Such a container may have its own strings (and own translations for them), but also display some content passed from the parent (as children, props). Some strings under the container's element subtree should use the app's message dictionary and some should use the component-provided translations; the two (or more) sets can be arbitrarily mixed.

A nested IntlProvider replaces the message dictionary in its whole subtree, while IntlNamespace stores multiple message (and format) dictionaries under intl.namespaces and lets each Format* component select which one to use (it's probably also slightly lighter than a nested provider).
[Ideally the selection would be possible by writing something like <FormattedMessage namespace=... />, but currently you have to use a shortcut construct for the scoping. 1]

Arguably, it still may be possible to get around by selectively putting some elements inside the nested providers and leaving some outside of them, but that gets convoluted too quickly. You can also use some prefixing scheme -- that's what I did before coming up with the little module -- however, I'd say that an explicit namespacing solution is less error-prone.

[1] Making this happen is another thing, it would require something like including unrecognized props on Format* components in the message descriptor

@ericf
Copy link
Collaborator

ericf commented Apr 20, 2016

@wrwrwr if this namespacing feature you described existed, then would you still want unbound format* functions?

@wrwrwr
Copy link
Author

wrwrwr commented Apr 20, 2016

Well, it kind of exists ;-) In the form of an additional module -- but that still works (currently by importing the format* functions from src and building the module against a git clone).

I think this is the only significant case I have for exposing the functions, so some other solution would be just as good (as long as it can properly handle formats). I'm not sure namespaces are as substantial as to cater to them in the base package, but maybe that's also worth considering (I guess most users should be fine with a single message dictionary).

@ericf
Copy link
Collaborator

ericf commented Apr 20, 2016

Could you open a PR against the Nested IntlProvider Example app to demonstrate the use case you have with passing elements to props from outside the nested <IntlProvider> to a component inside the nested <IntlProvider>?

wrwrwr added a commit to wrwrwr/react-intl that referenced this issue Apr 21, 2016
Nesting providers is a powerful technique that can be used to switch
to any message or format dictionary for any element subtree. It may
be a bit unwieldy at times, though.
wrwrwr added a commit to wrwrwr/react-intl that referenced this issue Apr 21, 2016
Nesting providers is a powerful technique that can be used
to switch to any message or format dictionary for any element
subtree. It may be a bit unwieldy at times, though.
@ericf
Copy link
Collaborator

ericf commented Apr 23, 2016

I thought about this some more. One thing that stands out to me is when you said generic container. I don't think components that are considered generic should have their own strings because there isn't enough context to provide a translator to get the correct phasing since the component is generic. Instead, I recommend using slots that map to props which accept PropTypes.node.

Using the props-for-slots should address this problem for generic components. But there are still other cases where a component is specialized, but still receives other elements to render. One example is React Native's <ListView> component which has a renderRow prop which is a function. Passing functions as props also solves this issue because it delegates rendering to another context, and in your case a context where the ancestor <IntlProvider> contains the correct translated messages.

I think these two patterns: props-for-slots and function-props will cover many cases. In fact, using a function as a prop will always work, but starts making less sense when the component isn't something like a <ListView> which needs to render a collection. What I think we need at the core of this issue is a way to model React < 0.14's context semantics which were owner-based instead of parent-based.

I could create something like bindIntl(element: ReactElement, intl: Object), which would wrap the element similar to a function-prop. But it would require that injectIntl was used on the parent component so that the intl object to bind to could be passed to bindIntl.

Another approach you can take is to force the i18n formatting to occur before you pass things into the other i18n context. To do this, you'd need to use the imperative API; this will work because renders everything to a string, but therefore you'd loose support for rich-text message formatting.

@ericf
Copy link
Collaborator

ericf commented Apr 23, 2016

The bindIntl() idea, in the end, might not be too different from using nested <IntlProvider>s, the one possibility is that it could be made more performant since it doesn't need to support mutation. What works today is this:

const App = injectIntl(({intl}) => (
    <IntlProvider messages={fooMessages}>
        <Foo
            title={
                <IntlProvider {...intl}>
                    <FormattedMessage id='some_message'/>
                </IntlProvider>
            }
        />
    </IntlProvider>
));

React.render(
    <IntlProvider
        locale='en'
        messages={messages}
    >
        <App/>
    </IntlProvider>
);

@wrwrwr
Copy link
Author

wrwrwr commented Apr 24, 2016

Thanks a lot for the thorough look. This seems to have turned into a discussion on using multiple message spaces -- it was definitely not my intent to press on changes related to this most-likely off-stream approach. The original idea of noting some possible direction of extending the module seemed fairly innocuous :-)

It is certainly possible to wrap the passed down messages in providers and to eliminate strings defined in containers completely (one more trick is to pass down "string promises" that hold the proper context, but are evaluated only when used as strings). Here are some (completely subjective) circumstances in which you could still want something more than nested providers:

  • I often develop translations as part of coding the component. 1 Having too much at the app scope ends in a large collection of strings that needs some tooling to keep it consistent with components.
  • Making strings in generic components configurable (props-for-slots) is nice and sometimes (though not always) necessary, but I may still want to provide some pre-translated defaults.
  • Suppose I would like to publish a single component that mixes user strings with its own strings as a package. It is possible to have both: -- keep the component's code clean (a single message namespace), -- neither ask your users to wrap their strings nor provide any component strings.

[1] That's a different perspective from having a hoard of translators at your whim -- which is probably the typical situation ;-)

@ericf
Copy link
Collaborator

ericf commented Apr 25, 2016

@wrwrwr are you using babel-plugin-react-intl?

If you're authoring a component or package, you can always namespace your strings ids and house those string definitions inside the component.

@wrwrwr
Copy link
Author

wrwrwr commented Apr 26, 2016

Yes, I used to do that, but I don't think I want to any longer :-) Currently, I keep all of the message definitions, their translations, and component-specific formats in a file side-by-side with the code of each component. Messages and formats of components live in separate namespaces by default (with 3 lines of boilerplate per component).

You could view my whole namespacing solution as an automated ids prefixer. There are really just some small bits of convenience to it: how easy it is to change the namespace, the ability to use unprefixed names of namespaced formats in translations of messages from the same namespace, and hardly any way to run into a name clash while using single-word ids.

Babel extraction -- no, not at the moment, although I have a todo entry about customizing it for the specific setup.

@ericf ericf changed the title [wish] Expose format* functions Passing <FormattedMessage> to a component inside a nested <IntlProvider> May 2, 2016
@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
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

2 participants