How to use TAPi18n within a Meteor + React application?

I am trying to use the meteor/tap-i18n package within my application, but am usure how to use it within React components. If I take theri example on Github:

if (Meteor.isClient) {
  Meteor.startup(function () {
    Session.set("showLoadingIndicator", true);

    TAPi18n.setLanguage(getUserLanguage())
      .done(function () {
        Session.set("showLoadingIndicator", false);
      })
      .fail(function (error_message) {
        // Handle the situation
        console.log(error_message);
      });
  });
}

How do I ensure that I can access the TAPi18n object from within the React components? I am assuming that importing TAPi18n each time into a component would be a huge performance cost and that I should simply initialize the TAPi18n object once. Should I do this at the root component and pass it down as a a prop, for example?

I am calling setLanguage() once and then passing down the language through props and it’s working great.

The other option is to use React context (https://facebook.github.io/react/docs/context.html). I think it’s a pretty good example of when to use context but like that documentation says it’s easier to know what’s going on at each step if you explicitly pass it down through props.

Would you mind posting a short example of how you are passing it down to a component after the language has been set?

You might want to consider using something like universe:i18n with your React project instead. It’s very similar to TAPi18n but it doesn’t have a dependency on Blaze. The react branch of the todos app was recently updated to use it, if you want to see a working example.

1 Like

Thanks for this, I will certainly have a look. I wonder though if there is another NPM package that would suit, rather than relying on a Meteor-specific package. I am briefly looking into react-intl, but am having a little difficulty understanding where to put the messages.

So I am using react-intl for the interface, and using TAPi18n-db for content translation. I’m also using moment locales for some calendar stuff.

So I guess this is an example that might be helpful either way. Sorry if this is a lot of code but hopefully you will find something in it that’s helpful @tidee.

From the lang prop in AppContainer.jsx you can imagine just passing that down as far as you need it.

This is the code of my client/main.js file:

import { Meteor } from 'meteor/meteor';
import { render, unmountComponentAtNode } from 'react-dom';

import moment from 'moment';
import 'moment/locale/es';
import { TAPi18n } from 'meteor/tap:i18n';
import { addLocaleData } from 'react-intl'
import en from 'react-intl/locale-data/en'
import es from 'react-intl/locale-data/es'

import { renderRoutes, loadTranslation } from '../imports/startup/client/routes.jsx';

import '/imports/startup/client';

addLocaleData(en);
addLocaleData(es);

const initiateRender = () => {
  window.AppState = {
    container: document.getElementById('app'),

    getLocale: function() {
      return localStorage.getItem('locale') || 'en';
    },

    setLocale: function(lang) {
      localStorage.setItem('locale', lang);
      moment.locale(lang);
    },

    render: function() {
      var locale = this.getLocale();
      TAPi18n.setLanguage(locale);
      moment.locale(locale);

      const messages = loadTranslation( { locale });
      render(renderRoutes({ locale, messages }), this.container)
    },

    unmount: function() {
      unmountComponentAtNode(this.container);
    },

    rerender: function() {
      this.unmount();
      this.render();
    }
  }
}

Meteor.startup(() => {
  const areIntlLocalesSupported = require('intl-locales-supported');

  const localesMyAppSupports = [
      'en',
      'es'
  ];

  if (global.Intl) {
    // Determine if the built-in `Intl` has the locale data we need.
    if (!areIntlLocalesSupported(localesMyAppSupports)) {
      // `Intl` exists, but it doesn't have the data we need, so load the
      // polyfill and patch the constructors we need with the polyfill's.
      var IntlPolyfill    = require('intl');
      Intl.NumberFormat   = IntlPolyfill.NumberFormat;
      Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat;
    }
    initiateRender();
    window.AppState.render();
  } else {
    // No `Intl`, so use and load the polyfill.
    global.Intl = require('intl');

    initiateRender();
    window.AppState.render();
  }
});

Then I have an AppContainer.jsx file:

const AppContainer = createContainer(() => {
  // Language
  // You could also call TAPi18n.getLanguage() here if you don't want the extra step of all that stuff in main.js
  // const lang = TAPi18n.getLanguage();
  const lang = window.AppState.getLocale();
  const supportedLanguages = TAPi18n.getLanguages();

  return {
    user: Meteor.user(),
    connected: Meteor.status().connected,
    menuOpen: Session.get('menuOpen'),
    lang,
    supportedLanguages,
  };
}, App);

export default AppContainer;

Then I have a routes.jsx file:

export const loadTranslation = ({ locale }) => {
  let messages = {};
  if (locale === 'es') {
    messages = require("/i18n/es.json");
  }

  return messages;
};

export const renderRoutes = ({ locale, messages }) => (
  <IntlProvider locale={locale} key={locale} messages={messages}>
    <Router>
      <Route path="/" component={AppContainer}>
   </Routes>
  </IntlProvider>
);
2 Likes

PS this is the first time I’ve set this up so if anyone has suggestions on my code I would appreciate it also!

1 Like

That’s great. WOuld you mind adding a couple mroe bits of code, 1) to show the format of the es.json file, 2) to show the use of the messages prop in a child component?

1 Like

Sure thing.

  1. es.json
{
  "navigation.siteName": "Mapa Mundial de Teatro",
  ...
}
  1. which is used in a react component like this:
import { FormattedMessage } from 'react-intl';
[...]
<FormattedMessage
  id='navigation.siteName'
  description="Site Name"
  defaultMessage="World Theatre Map"
/>

You don’t use the messages prop directly, it gets handled by react-intl when you use FormattedMessage.

Does that answer your question?

1 Like

Thanks for posting your methodology. I have a few questions:

  1. Where you have const lang = window.AppState.getLocale(); in the container - if the value of the locale is changed in a child component (using window.AppState.setLocale()), does this update the value of lang here (which then subsequently propogates it down the component tree)?
  2. I couldn’t see any specific reason for using this line TAPi18n.setLanguage(locale); - if the react-intl library is being used, do we even need TAPi18n?
  3. I currrently use the moment package, but I set it each time at the component level - I noticed that you have the line moment.locale(locale); - does this allow moment to be accessible globally across the application?
  1. window.AppState.setLocale() actually re-renders everything with the new language. I forget where I got that code from, but it was some example of react-intl language switcher that suggested that rerendering was the only way. For me it’s fast enough that it doesn’t bother me.

  2. You are correct that you probably don’t need any TAPi18n code. I am using TAPi18n-db for content translation (documents in the database) so that’s why I have both. https://github.com/TAPevents/tap-i18n-db

  3. Good question. Honestly I’m only using moment locals for a calendar widget and it just worked out of the box once I set this so it appears the answer to your question is yes, but I honestly haven’t investigated it much.

1 Like

You could always try Universe:i18n with React, which I found easier to use.
I just wrote a short tutorial on it here -> http://www.sonicviz.com/wp/2016/10/23/internationalizing-meteor/

We considered using Tap for our app with React, React Router and GraphQL. Instead looking at the source code of Tap, we found that it uses i18next under the hood that instead if we used directly we could get all the features we wanted including dynamically translating in an server side render and all other types of features you need. Tap does a lot for you but it I do prefer using something that is way more used around the node community than tap is in the meteor community. Sorry but it’s time to break up from Tap. :frowning:

I can also recommend the higher order component for i18next, that makes your translate function available as a prop in your component. Optimally having it wait to render until the language has loaded.

1 Like

There is great package i18n designed to working with react in meteor environment.
Check it out the Universe i18n

The advantage of this package is that this package use pure events reactivity instead of using new instance of Tracker.Dependency on every translation token that is currently on page.

i18next doesn’t need any Tracker at all, so I recommend using it together with https://github.com/i18next/react-i18next