React Internationalization: Transifex Native Vs. react-i18next

Written by transifex | Published 2021/12/14
Tech Story Tags: react | i18next | i18n | localization | internationalization | javascript | good-company | programming

TLDRTransifex Native is not just easier to use for internationalization, but it's also a great solution for localizing your product without having to bother with files, thanks to its automated push & pull function! via the TL;DR App

In this document, we will showcase the capabilities of each framework and see how they map to each other and provide a foundation for how one would go about migrating from react-i18next to Transifex Native.

Initialization

Both frameworks require you to initialize a global object that will serve as a "gateway" between your application and the ability to localize it. In react-i18next, this looks like this (example taken from the official website):
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

const resources = {
  en: {
    translation: {
  	'Hello world': 'Hello world',
    },
  },
  el: {
    translation: {
  	'Hello world': 'Καλημέρα κόσμε',
    },
  },
};

i18n
  .use(initReactI18next)
  .init({
    resources,
    lng: 'en',
    interpolation: { escapeValue: false },
  });
This snippet doesn't tell the whole story. The translated content (the 'resources' field) can originate from a variety of sources and can also be added after the initialization. Also, the 'init' method accepts many more arguments and allows you to customize the experience to a great extent.
With Transifex Native, you lose some of this flexibility, given that the source of truth for your content is always the content on transifex.com, and in return, you end up with the simpler:
import { tx } from '@transifex/native';

tx.init({
  token: '...',
});
There are other initialization options in Transifex Native that allow you to:
  1. Limit the content you will receive from Transifex with the 'filterTags' field
  2. What to do when a translation is missing with the 'missingPolicy' field
  3. What to do when the translation cannot be rendered (because, for example, the translator made a formatting mistake) with the 'errorPolicy' field

Keys

react-i18next

react-i18next is key-based. This means that if your component code looks like this:
function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('a_key')}</p>;
}
Then you need to have the exact same key in your 'resources':
const resources = {
  en: {
	translation: {
  	'a_key': 'Hello world',
	},
  },
  el: {
	translation: {
  	'a_key': 'Καλημέρα κόσμε',
	},
  },
};
If the key is not found in the resources, then the key itself will be rendered in the application. For this reason, it makes sense to use the source string itself as the key:
const resources = {
  en: {
	translation: {
  	'Hello world': 'Hello world',
	},
  },
  el: {
	translation: {
  	'Hello world': 'Καλημέρα κόσμε',
	},
  },
};

// i18n.use(...).init(...)

function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('Hello world')}</p>;
}
However, this gets a little difficult to manage if you have plurals. Let's assume you have the following component:
function Paragraph() {
  const [count, setCount] = useState(2);
  const { t } = useTranslation();
  return (
    <>
  	<p>
    	  {t('You have {{count}} messages', { count })}
  	</p>
  	<p>
    	  <button onClick={() => setCount(count - 1)}>-</button>
    	  <button onClick={() => setCount(count + 1)}>+</button>
  	</p>
    </>
  );
}
And the following resources:
const resources = {
  en: {
    translation: {
  	'You have {{count}} messages_one': 'You have one message',
  	'You have {{count}} messages_other': 'You have {{count}} messages',
    },
  },
  el: {
    translation: {
  	'You have {{count}} messages_one': 'Έχετε ένα μήνυμα',
  	'You have {{count}} messages_other': 'Έχετε {{count}} μηνύματα',
    },
  },
};
Then the application will work as expected. When the 'count' becomes 1, the application will render "You have one message". If the keys are not in the resources, however, or if they are not formatted properly, you"ll get "You have 1 messages".

Transifex Native

With Transifex Native, the default behaviour is to not worry about keys at all. The string itself becomes the key. In case of plurals, the fact that the strings are formatted with ICU MessageFormat means that it contains all the information needed to render in both the source and target languages:
function Paragraph() {
  const [count, setCount] = useState(2);
  return (
    <>
  	<p>
    	  <T
          _str="{count, plural, one {You have one message} other {You have # messages}}"
          count={count} />
  	</p>
  	<p>
        <button onClick={() => setCount(count - 1)}>-</button>
        <button onClick={() => setCount(count + 1)}>+</button>
  	</p>
    </>
  );
}
If you want to use keys, however, probably because you are using Transifex Native with a design tool like Figma or Sketch, You can supply the '_key' prop:
function Paragraph() {
  return <T _key="a_key" _str="Hello world" />;
}
In both cases, the source language is editable by using the "edit source strings" feature of transifex.com.

t-function

react-i18next

Most of the translation methods offered by react-i18next boil down to offering you a javascript function called 't'. Using this function will render the argument in the correct language and make sure the component will re-render when the selected language changes.
'useTranslation' hook
import { useTranslation } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('Some text')}</p>;
}
'withTranslation' (Higher-Order Component)
import { withTranslation } from 'react-i18next';

function _Paragraph({ t }) {
  return <p>{t('Some text')}</p>;
}

const Paragraph = withTranslation()(_Paragraph);

function App() {
  return <Paragraph />;
}
'Translation' (Render Prop)
import { Translation } from 'react-i18next';

function Paragraph() {
  return (
    <Translation>
  	{(t) => <p>{t('Some text')}</p>}
    </Translation>
  );
}
'I18nProvider' context
import { useContext } from 'react';
import { I18nContext, I18nextProvider } from 'react-i18next';

i18n.use(...).init({...});

ReactDOM.render(
  <I18nextProvider i18n={i18n}>
    <App />
  </I18nextProvider>,
  document.getElementById('root'),
);

function App() {
  return <Paragraph />;
}

function Paragraph() {
  const { i18n: { t } } = useContext(I18nContext);
  return <p>{t('Some text')}</p>;
}
Transifex Native
With Transifex Native you can achieve the same result by using the 'useT' hook:
import { useT } from '@transifex/react';

function Paragraph() {
  const t = useT();
  return <p>{t('Some text')}</p>;
}
or by using the preferable 'T-component':
import { T } from '@transifex/react';

function Paragraph() {
  return <p><T _str="Some text" /></p>;
}

Interpolation and Plurals

react-i18next

react-i18next allows you to interpolate dynamic values into your translations by using the '{{name}}' syntax, as demonstrated here:
import { useTranslation } from 'react-i18next';
function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('Hello {{name}}', { name: 'Bob' })}</p>;
}
or
import { useTranslation, Trans } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return (
    <p>
      <Trans t={t}>
          Hello <strong>{{name}}</strong>
      </Trans>
    </p>
  );
}
In order to support plurals, you have to pay very close attention to your keys:
const resources = {
  en: { translation: { messages_one: 'You have 1 message',
                       messages_other: 'You have {{count}} messages' },
  },
  el: { translation: { messages_one: 'Έχετε 1 μήνυμα',
                       messages_other: 'Έχετε {{count}} μηνύματα' } },
};
i18n.use(...).init({ ..., resources });

function Paragraph() {
  const count = 3;
  const { t } = useTranslation();
  return <p>{t('messages', { count })}</p>;
}
Or
function Paragraph() {
  const count = 3;
  const { t } = useTranslation();
  return (
    <p>
      <Trans t={t} i18nkey="messages" count={count}>
        You have {{count}} messages
      </Trans>
    </p>
  );
}

Transifex Native

Transifex Native uses ICU MessageFormat natively (pun intended). This gives you a solid background to work with interpolation:
import { T } from '@transifex/react';

function Paragraph() {
  return <p><T _str="Hello {name}" name="Bob" /></p>;
}
ICU MessageFormat also offers you industry standard capabilities for plurals:
function Messages() {
  const count = 3;
  return (
    <p>
      <T
        _str="{cnt, plural, one {You have 1 message} other {You have # messages}}"
        cnt={count} />
    </p>
  );
}
And also for select statements and number formatting:
<T _str="It's a {gender, select, male {boy} female {girl}}" gender={gender} />
<T _str="Today is {today, date}" today={today} />

Translation with Formatted Text

react-i18next

When you want to translate HTML content, react-i18next offers the '<Trans>' component:
import { useTranslation, Trans } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return (
    <Trans t={t}>
  	<p>Some <strong>bold</strong> text</p>
    </Trans>
  );
}
In order for this to work, the source text in i18n's resources must have the form
<1>Some <2>bold</2> text</1>
Which you have to generate by hand.

Transifex Native

With Transifex Native you have the option to use the 'UT'-component:
import { UT } from '@transifex/react';

function Paragraph() {
  return <UT _str="<p>Some <strong>bold</strong> text</p>" />;
}
Or to interpolate the outer template with react elements (that can be also translated):
import { T } from '@transifex/react';
function Paragraph() {
  return (
    <T
  	_str="<p>Some {strong} text</p>"
  	strong={<strong><T _str="bold" /></strong>} />
  );
}
With both ways, what your translators will be asked to work on will have the exact same form as the argument you used for '_str'.

Language Selection

Both frameworks allow you to dynamically set the currently active language. With react-i18next you can do:
function Languages() {
  return (
    <>
  	<button onClick={() => i18n.changeLanguage('en')}>English</button>
  	<button onClick={() => i18n.changeLanguage('el')}>Greek</button>
    </>
  );
}
And with Transifex Native you can do the similar:
function Languages() {
  return (
    <>
  	<button onClick={() => tx.setCurrentLocale('en')}>English</button>
  	<button onClick={() => tx.setCurrentLocale('el')}>Greek</button>
    </>
  );
}
What Transifex Native can offer you above this is due to the fact that transifex.com is your source-of-truth not only for your translation content but also for your available languages. Taking this into account, you can do:
import { useLanguages } from '@transifex/react';

function Languages() {
  const languages = useLanguages();
  return (
    <>
  	{languages.map(({ code, name }) => (
    	  <button
                key={code}
                onClick={() => tx.setCurrentLocale(code)}>
              {name}
            </button>
      ))}
    </>
  );
}
Or the more direct:
import { LanguagePicker } from '@transifex/react';

function Languages() {
  return <LanguagePicker className="pretty" />;
}
Which will render a language selector dropdown.
After this, languages added on transifex.com will be reflected in your application without requiring you to publish a new release.

String Extraction

react-i18next

There are some third-party tools to help with extracting strings from your code in the i18next ecosystem. One that can work with react applications is i18next-parser. Assuming you have the following application:
export default function App() {
  const { t } = useTranslation();
  return (
    <>
  	<p>
    	  {t('Hello world 1')}
  	</p>
  	<p>
    	  <Trans t={t}>
      	Hello <strong>world</strong> 2
    	  </Trans>
  	</p>
  	<p>
    	  {t('Hello {{count}} worlds', { count: 3 })}
  	</p>
    </>
  );
}
And use this configuration:
module.exports = {
  useKeysAsDefaultValue: true,
  lexers: {
	js: ['JsxLexer'],
  },
};
Then, if you run i18next-parser, you will end up with the following resource file:
{
  "Hello world 1": "Hello world 1",
  "Hello <1>world</1> 2": "Hello <1>world</1> 2",
  "Hello {{count}} worlds_one": "Hello {{count}} worlds",
  "Hello {{count}} worlds_other": "Hello {{count}} worlds"
}
΅΅΅Which is a good starting point and even takes care of the unintuitive key generation for the '<Trans>' component and of plurals.
After that, you will of course have to worry about how to generate translated versions of these files (hint: you should upload them to Transifex) and how to import these files into the application when it's running.

Transifex Native

With Transifex Native, you don't have to worry about files at all. You simply have to run:
export TRANSIFEX_TOKEN=...
export TRANSIFEX_SECRET=...
npx txjs-cli push src
After that:
  1. Your content will be available on transifex.com for your translators to work on
  2. Any ready translations will be available to your application during runtime

Namespaces / Lazy loading

react-i18next and i18next in general offers extensive support for compartmentalizing your translated content and, via plugins, loading these compartmentalized namespaces into your application via HTTP after the initial booting of the application.
Transifex Native has limited support for namespacing via tags. You can add a tag to a translatable string with the '_tags' property:
<T _str="Hello world" _tags="main" />
or by specifying tags during the extraction cli execution:
npx txjs-cli push --append-tags=helpdesk src/helpdesk
npx txjs-cli push --append-tags=dashboard src/dashboard
Then, when initializing the 'tx' object, you can specify which tags you want to filter against:
tx.init({ token: ..., filterTags: 'android' });
This is useful in case you are using the same transifex.com project for different platforms and you want each platform to only pull translations that are relevant.
As of now, we don't support lazy loading of translations but we have plans to implement this in the near future.

Migration Guide

Preparing the code

For minimal inconvenience, you should replace the invocation of react-i18next's 't' function with Transifex Native's 't' function.
From:
import { useTranslation } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('Some text')}</p>;
}
to
import { useT } from '@transifex/react';

function Paragraph() {
  const t = useT();
  return <p>{t('Some text')}</p>;
}
However, it may be preferable to use the T-component:
import { T} from '@transifex/react';

function Paragraph() {
  return <p><T _str="Some text" /></p>;
}
Simple variable interpolation is done with single curly brackets instead of double.
From:
import { useTranslation } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return <p>{t('Hello {{username}}', { username: 'Bob' })}</p>;
}
to:
import { T } from '@transifex/react';

function Paragraph() {
  return <p><T _str="Hello {username}" username="Bob" /></p>;
}
For formatted content, your best bet is to replace '<Trans>' with <UT />'.
From:
import { useTranslation, Trans } from 'react-i18next';

function Paragraph() {
  const { t } = useTranslation();
  return (
    <Trans t={t}>
  	<p>Some <strong>bold</strong> text</p>
    </Trans>
  );
}
To:
import { UT } from '@transifex/react';

function Paragraph() {
  return <UT _str="<p>Some <strong>bold</strong> text</p>" />;
}

Migrating the translated content

First, you'll have to upload your current translations to transifex.com on a file-based project. After creating the project, you can use the transifex-client to help with uploading the resources (one namespace per resource):
# Install the client
wget https://github.com/transifex/cli/releases/download/v0.3.0/tx-linux-amd64.tar.gz
tar xf tx-linux-amd64.tar.gz tx
rm tx-linux-amd64.tar.gz
mv tx ~/bin

# Set up the mapping to transifex.com
tx init
tx add \
  --organization=... \
  --project=... \
  --resource=translations \
  --file-filter=locales/translations/<lang>.json \
  --type=KEYVALUEJSON \
  locales/translations/en.json
tx add \
  --organization=... \
  --project=... \
  --resource=helpdesk \
  --file-filter=locales/helpdesk/<lang>.json \
  --type=KEYVALUEJSON \
  locales/helpdesk/en.json

# Push the content to transifex
tx push --source --translation --all
Next, you need to create a Native project.
Make sure you keep the public and secret tokens for later.
Before pushing the content to the new project, you need to:
  1. Add the target languages to the new project
  2. Add both projects to the same TM group
  3. Now in your local project, use the extraction cli to push content to the native project in transifex.com (use tags for namespacing as discussed above:
npm install --save-dev @transifex/cli
npx txjs-cli push --token=... --secret=... src
Because we put both projects in the same TM group, existing translations will be used to fill up the new ones. In case you had to make minor changes to some of the strings while migrating the code, take a look in the editor to fill these in. Even if they were not filled up automatically, partial TM matches should make this job easy.
Finally, make sure you initialize the 'tx' object when your application boots up and you should be good to go!
Once everything is up and running, you can delete the old file-based project.

Wrapping Up

Wanna share a quick React localization guide with a fellow developer? Find it on this page!
This post was co-authored by Konstantinos Bairaktaris and Mike Giannakopoulos
Find Transifex on our: Website, Facebook, Twitter, and LinkedIn.

Written by transifex | The #1 localization platform for developers.
Published by HackerNoon on 2021/12/14