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 does React-Intl recommend allowing users to select a different language? #39

Closed
alanhogan opened this issue Dec 4, 2014 · 29 comments

Comments

@alanhogan
Copy link

No description provided.

@ericf
Copy link
Collaborator

ericf commented Dec 4, 2014

We haven't written an a specific guide for how we recommend implementing a user-selectable language feature — as that's more application specific. We do provide details on automatically determining the locale.

But once we have the locale it can simply be passed as a prop to a React component that uses the ReactIntlMixin mixin. We recommend that you add this mixin to your top-level app component as it will allow any other component anywhere in your React component hierarchy which also uses the mixin to be able to access the locale. i.e. you don't have to thread through the locale through every component.

I'm not sure if this answers your question… if not, could you provide more details in the issue description?

@alanhogan
Copy link
Author

Thanks, that’s helpful. I guess what’s odd to me about the ReactIntlMixin “API” is that the top-level usage (which I am following) accepts a "locales" array, as well as a "messages" object. (which seems to work without the top level of the object being a locale, e.g., top level can be "buttons" and "errors", not "en" and "fr")… so… I’m confused by the semantics of passing multiple "locales" but one locale's messages. Maybe I should be adding a top-level locale key?

I see from the guide you linked to that I am supposed to chose a locale automatically for the user. But then if I have passed multiple locales to the React-Intl mixin, how does it know which one locale it should be using?

@alanhogan
Copy link
Author

I see that the React-Intl mixin makes the following available to children thu context:

  • formats
  • locales
  • messages

But what about the 1 locale that you can actually use at a time?

@caridy
Copy link
Collaborator

caridy commented Dec 5, 2014

@alanhogan all the low level APIs (e.g. Intl.numberFormat()) expect a list of locales to match that with the available locales in the runtime (or the polyfill CLDR data) to compute the most common denominator, more details here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat

@alanhogan
Copy link
Author

@caridy I think I see. So, correct me if I am wrong, but an optimal implementation might look like:

  1. Guess from the browser (or remember from preferences) the user's locale
  2. Load that locale’s messages into the browser, and set only one locale as the locales prop, or that one locale (first) and others as fallbacks. So, ['fr-CA','fr-US','en-US'] ?
  3. If the user picks another locale, initiate load of the required messages
  4. On load, replace the locales prop with the new locale-with-fallbacks (maybe just ['en-US'] in this example) and the messages prop with the new messages

Yeah?

@caridy
Copy link
Collaborator

caridy commented Dec 5, 2014

@alanhogan

  1. guess from request, many app frameworks will do that for you, e.g.: expressjs uses this: https://github.com/jshttp/negotiator/#accept-language-negotiation).
  2. messages has nothing to do with the locales since you only load the messages in a particular locale (the one resolved by the server). it is true that inside a message you might format numbers and other stuff, in which case it will have to use the locales computed when producing the page.
  3. switching locales on the fly is complicated, I haven't see a real use-case for this in years, you switch language, the app is reloaded :)
  4. keep in mind that you have to load the locale-data for the polyfill when needed, plus the messages, plus the locale-data for react-intl.

@alanhogan
Copy link
Author

Right… true. Thanks very much.

@caridy caridy closed this as completed Dec 5, 2014
@ericf
Copy link
Collaborator

ericf commented Dec 5, 2014

@alanhogan the one vs many confusion is something we can work to fix — I agree that it makes it hard to understand that when the prop's name is locales (plural). Maybe we could do something like:

Have two props locale and fallbackLocales, where fallbackLocales is more of an advanced feature and not introduced in the intro docs. Under the the mixin could do this:

this.props.locales = [this.props.locale].concat(this.props.fallbackLocales);

Essentially the idea would to make locales the singular locale since that's the main use case as @caridy is explaining above.

@caridy
Copy link
Collaborator

caridy commented Dec 5, 2014

I think we should stick to locales, and be more explicit on how this piece works, essentially the browser (and the polyfill) covers a subset of the locales that you can use, and that should be understood for anyone doing i18n.

@gpbl
Copy link

gpbl commented Dec 20, 2014

@caridy

switching locales on the fly is complicated, I haven't see a real use-case for this in years, you switch language, the app is reloaded :)

This should be trivial, since props can be reset with setProps() to render again the components tree. There's also a working solution when using react-router.

@ericf
Copy link
Collaborator

ericf commented Dec 21, 2014

@gpbl switching the locales prop will be trivial, but what @caridy is talking about is loading the locale data on demand.

When you combine the locale data for the Intl.js polyfill, Format.js, and your app's strings it will be large enough that you'll only want to load this data for the user's current locale. Switching the locale on the fly means that you'll need to implement a script loader that makes sure the locale data for the new locale is loaded before re-rendering the page in React.

To complicate things more, the Intl.js polyfill should ideally only be loaded in the browsers that actually need it. We're working this make Intl.js available through Polyfill.io.

@gpbl
Copy link

gpbl commented Dec 21, 2014

@ericf I see what you mean. However it's not an uncommon task to require modules asynchronously, for example with webpack's require.ensure().

As for the missing Intl, in my own implementation I thought to create, for each language, two different webpack chunks: one with just the strings, and the other with the app strings and the Intl's locale-data. According to the browser's support, I'd load async one or the other:

function switchLocale(i18n) {
  // Render the component
  this.setProps({messages: i18n.messages, locales: i18n.locales});
);

function switchToJapanese() {
  if (has('Intl'))
    require.ensure(['../i18n/jp'], function(require){
       // Now we have loaded japanese data
       var jp = require('../i18n/jp');
       switchLocale(jp);
    });
  else
    require.ensure(['../i18n/jp-with-intl'], function(require){
       var jp = require('../i18n/jp-with-intl'); // Load Intl locale-data as well
       switchLocale(jp);
    });
}

Not sure if there are better ways for doing this, anyway :-) Since I don't believe dynamic requires would work, I need to explicitly specify the requires for each language.

I still have to check if polyfills.io may help, but I believe if using web pack it wouldn't.

@ericf
Copy link
Collaborator

ericf commented Dec 21, 2014

I still have to check if polyfills.io may help, but I believe if using web pack it wouldn't.

Modern browsers (with the exception of Safari) don't need the Intl.js polyfill, and it's pretty large (14kb for the polyfill + data for a locale), so this is why we want to get this on polyfill.io. We feel strongly that you should not require() polyfills as they're not the same kind of dependencies as libs or app code, and we think of them as our JS runtime requirements.

@gpbl
Copy link

gpbl commented Dec 21, 2014

@ericf I don't see why it shouldn't work – just make sure you "require" the polyfills and the localized strings before mounting the app, or before setting the new props.

Here is a working example of what I mean: https://github.com/gpbl/react-locale-hot-switch
This react app loads the i18n data only as the user switches the language, without reloading the page. On Safari it loads also the Intl polyfills.

What could go wrong? Maybe I'm missing an important part of the whole concept :-)

@alanhogan is this something can help you too?

@alanhogan
Copy link
Author

Yes, this is relevant to my interests, as they say on the Internet.

Thanks. I’ll be taking a look before long.

Alan

Le 21 déc. 2014 à 15:44, Giampaolo Bellavite notifications@github.com a écrit :

@ericf I don't see why it shouldn't work – just make sure you "require" the polyfills and the localized strings before mounting the app, or before setting the new props.

Here is a working example of what I mean: https://github.com/gpbl/react-locale-hot-switch
This react app loads the i18n data only as the user switches the language, without reloading the page. On Safari it loads also the Intl polyfills.

What could go wrong? Maybe I'm missing an important part of the whole concept :-)

@alanhogan is this something can help you too?


Reply to this email directly or view it on GitHub.

@ericf
Copy link
Collaborator

ericf commented Dec 22, 2014

@gpbl I wasn't saying it's not possible, just qualifying hot-swap locale switching as being complex rather than trivial :)

It also never felt like a strong requirement to support hot-swapping locales without reloading the page. But that may be because our apps also support server-side rendering.

@gpbl
Copy link

gpbl commented Dec 22, 2014

@ericf Actually the reason I landed to hot switching the locale was because my projects also start from a server rendered page.

This also comes easy with webpack. When naming the i18n chunks during the build process, we can <script>-load the localised data without requiring it before the mount.

This way we can have the same URL for different languages. Crawlers will still see the alternate localised domain (e.g. fr.example.com), but we can now avoid showing the user a foreign language on a localised url (common case when it has been shared on a social network). In fact with different urls for different languages, english users may reach the french domain seeing it in english (= wrong and confusing) or in french (= lost visitor).

Hot switching the locale is not mandatory but it's nice to have and I still believe requires little effort. Plus it's a benefit for our translators :-)

@caridy
Copy link
Collaborator

caridy commented Jan 13, 2015

ok ok, there is nothing (as far as I can tell) that will prevent you for doing all those things (even though we think those are crazy things jejejeje). The internal cache system used by react-intl will take in consideration the locales, and therefore if you change locales, formats and messages values, it will automatically create a new internal instance in cache when calling render().

As for the use of context under the hood to resolve locales, you can always avoid that by passing those props into every component in your application, that should do the trick since it will automatically resolve those values without trying to do so thru the context.

@sebastienbarre
Copy link

Thanks for all the pointers, guys. I have to admit I have a tough time making this work with Safari & webpack. If I use require.ensure to load intl from a separate chunk, it seems that it "finishes" loading it after the react-intl's mixins is evaluated from its own chunk, which results in an error because it is referencing intl already. I've described the problem in gpbl/react-locale-hot-switch#2.

@ericf
Copy link
Collaborator

ericf commented Mar 20, 2015

@sebastienbarre not being able to use dependencies when a module is being executed seems like a problem with Webpack or how it's being configured. How are you conditionally loading other polyfills your app uses?

@sebastienbarre
Copy link

@ericf so far I had bit the bullet and included my polyfills in my vendor chunk, because they were pretty small. intl is... rather large. Using require('intl') brings a whopping 850+KB to the plate for some reasons (before minification), using require('intl/Intl') and just the en-US locale is about 150+KB, still pretty big, hence me trying to get by with require.ensure on that one...

@ericf
Copy link
Collaborator

ericf commented Mar 20, 2015

The Intl polyfill + en-US is ~17kb gz IIRC. Hopefully we can land it in Polyfill.io which will make loading it and your other polyfills much simpler: polyfillpolyfill/polyfill-service#108

@sebastienbarre
Copy link

Yes, ultimately when UglifyJS has done its job and the server is gzipping, we are down to much smaller numbers. But I've to admit my surprise when I first tried require('intl') and saw my vendor chunk inflate so much. Looking forward what will happen with Polyfill.io. Thanks.

@gpbl
Copy link

gpbl commented Mar 20, 2015

@sebastienbarre my bad: the react-locale-hot-switch app wasn't using the react-intl@latest, which indeed requires Intl in the global scope. So you need to require react-intl in the webpack's ensure callback, hence after the polyfill has been loaded (see updated project).

@sebastienbarre
Copy link

@gpbl I see. Thanks a lot for the quick reply. It seems the change was made by @ericf in e188cc1.

exports["default"] = {
[...]
    getNumberFormat  : intl$format$cache$$["default"](Intl.NumberFormat),
    getDateTimeFormat: intl$format$cache$$["default"](Intl.DateTimeFormat),
    getMessageFormat : intl$format$cache$$["default"](intl$messageformat$$["default"]),
[...]
    _format: function (type, value, options, formatOptions) {
[...]
        switch(type) {
            case 'date':
            case 'time':
                return this.getDateTimeFormat(locales, options).format(value);
            case 'number':
                return this.getNumberFormat(locales, options).format(value);
            case 'relative':
                return this.getRelativeFormat(locales, options).format(value, formatOptions);
            default:

My gut feeling is that people will use the formatDate, formatNumber, etc. functions, not getNumberFormat, etc. directly. Could they be initialized later? in _format maybe? Or simply turned into functions and a call to apply?

It's not a huge issue, but the problem here is that it involves loading yet one more chunk, thus requiring yet another round-trip to the server. Performance-wise, you would want to avoid that.
For example:
index.html: load bootstrap.js using a <script> tag.
bootstrap.js: load Intl conditionally, using require.ensure, which puts intl in its own chunk,
bootstrap.js: now that we have Intl (whether we had it already or not), load app.js using require, which will require('react-intl'), etc.
The consequence of this, is that webpack is going to create a separate chunk for bootstrap.js, intl, and app.js (since app.js is called in two different code paths). That's 3 round-trips. Previously we could get by with 2 round-trips. @ericf ?

Again, thanks for the all the hard work.

@ericf
Copy link
Collaborator

ericf commented Mar 20, 2015

My gut feeling is that people will use the formatDate, formatNumber, etc. functions, not getNumberFormat, etc. directly. Could they be initialized later? in _format maybe? Or simply turned into functions and a call to apply?

This is like saying I can't access Math eagerly in my module and have to wait until a function is invoked before I can access it. The Intl APIs must be available before the code for React Intl is executed. This sounds like it will be a problem for Webpack for any polyfill you conditionally load in your app. This is why the Polyfill.io solution is so attractive — you simpye include a script to polyfill the runtime, then load your app code.

The consequence of this, is that webpack is going to create a separate chunk for bootstrap.js, intl, and app.js (since app.js is called in two different code paths). That's 3 round-trips. Previously we could get by with 2 round-trips. @ericf ?

I would assume Webpack loads the intl.js and app.js chunks in parallel. This extra request for intl.js is only for browsers which don't have the Intl APIs built in. If your app only has three JavaScript HTTP requests you're doing really well!

@sebastienbarre
Copy link

Agreed, 3 separate requests wouldn't be too bad, I guess I was curious as to why I suddenly needed an extra one. I'm wholeheartedly with you on this one:

The Intl APIs must be available before the code for React Intl is executed.

but I think that might be where my confusion is here, I'm not executing any react-intl code here, at all. I'm only loading my vendor bundle/chunk, where all the third-party exports are. I noticed that issue in an app where only one page uses i18n. The other pages do not. What I'm hinting at is that my user might never route to that i18n page, but the whole app fails to load now because my vendor chunk bundles 'react-intl', which makes this explicit reference to Intl. The functionality exposed by react-intl is not executed/called at that point and might never be.

I've to admit I've not have had that issue with other polyfills before, that's why I would put them in my vendor chunk (small so far). No worries, but I figured I might not be the only one bumping into that, might as well document it.

@henrylingumi
Copy link

Hi all,

I came across this issue, and while it is old/closed I felt I might see if there's any new perspective from the React Intl team on how to allow users to select another language?

@Tomekmularczyk
Copy link

Tomekmularczyk commented Jul 27, 2018

Hi guys, this is what I came up with with a new React Context API. Pure React solution, no redux required. Tell me what you think:

IntlContext.jsx

import React from "react";
import Types from "prop-types";
import { IntlProvider, addLocaleData } from "react-intl";
import en from "react-intl/locale-data/en";
import de from "react-intl/locale-data/de";
import deTranslation from "../../lang/de";
import enTranslation from "../../lang/en";

addLocaleData([...en, ...de]);

const { Provider, Consumer } = React.createContext();

class IntlProviderWrapper extends React.Component {
  constructor(...args) {
    super(...args);

    this.switchToEnglish = () =>
      this.setState({ locale: "en", messages: enTranslation });

    this.switchToDeutsch = () =>
      this.setState({ locale: "de", messages: deTranslation });

    // pass everything in state to avoid creating object inside render method (like explained in the documentation)
    this.state = {
      locale: "en",
      messages: enTranslation,
      switchToEnglish: this.switchToEnglish, 
      switchToDeutsch: this.switchToDeutsch 
    };
  }

  render() {
    const { children } = this.props;
    const { locale, messages } = this.state;
    return (
      <Provider value={this.state}>
        <IntlProvider
          key={locale}
          locale={locale}
          messages={messages}
          defaultLocale="en"
        >
          {children}
        </IntlProvider>
      </Provider>
    );
  }
}

export { IntlProviderWrapper as IntlProvider, Consumer as IntlConsumer };

Main App.jsx component:

import { Provider } from "react-redux";
import {  IntlProvider } from "./IntlContext";

class App extends Component {
  render() {
    return (
      <Provider store={store}>
        <IntlProvider>
          ...
        </IntlProvider>
      </Provider>
    );
  }
}

LanguageSwitch.jsx

import React from "react";
import { Text, Button } from "native-base";
import { IntlConsumer } from "../IntlContext";

const LanguageSwitch = () => (
  <IntlConsumer>
    {({ switchToEnglish, switchToDeutsch }) => (
      <React.Fragment>
        <Button onPress={switchToEnglish}>
          <Text>English</Text>
        </Button>
        <Button onPress={switchToDeutsch}>
          <Text>Deutsch</Text>
        </Button>
      </React.Fragment>
    )}
  </IntlConsumer>
);

export default LanguageSwitch;

@ericf I would like to hear your thoughts.

I've put the idea into the repository

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

No branches or pull requests

7 participants