patternfly / patternfly-react

A set of React components for the PatternFly project.
https://react-staging.patternfly.org/
MIT License
772 stars 350 forks source link

Review current react i18n libraries and recommend strategy for localization #3952

Closed dgutride closed 4 years ago

dgutride commented 4 years ago

Describe the issue. What is the expected and unexpected behavior?

Please provide the steps to reproduce. Feel free to link CodeSandbox or another tool.

Is this a bug or enhancement? If this issue is a bug, is this issue blocking you or is there a work-around?

What is your product and what release version are you targeting?

evwilkin commented 4 years ago

Goal: provide a recommendation strategy for localization that can be incorporated into OpenShift

Options:

jschuler commented 4 years ago

Internationalization (i18n) / Localization (l10n)

Internationalization (i18n)

Internationalization is the design and development of a product, application or document content that enables easy localization for target audiences that vary in culture, region, or language.[1]

It is the process of designing an application so that it can be adapted to various languages and regions without engineering changes.[2]

An internationalized program has the following characteristics [2]:

Localization (l10n)

Localization refers to the adaptation of a product, application or document content to meet the language, cultural and other requirements of a specific target market (a locale).[1]

It is the cultural and linguistic adaptation of an internationalized application to two or more culturally-distinct markets.[1] For example, Spanish is spoken around the world, but Spanish speaking users in Buenos Aires and Madrid could have different sociolinguistic expectations and thus should be addressed differently.

The primary task of localization is translating the user interface elements and documentation. Localization involves not only changing the language interaction, but also other relevant changes such as display of numbers, dates, currency, and so on. Other types of data, such as sounds and images, may require localization if they are culturally sensitive. The better internationalized an application is, the easier it is to localize it for a particular language and character encoding scheme.[2]

According to Forrester research, 64% of buyers said they valued localized content when making technology purchases.

Pros of i18n/l10n

Cons

1: https://www.javacodegeeks.com/2013/02/internationalization-vs-localization-i18n-vs-l10n.html 2: https://docs.oracle.com/javase/tutorial/i18n/intro/index.html

Dependencies used amongst products

Overall

react-intl i18next
Foreman (Console/Tasks) 3scale
Insights (Compliance/Dashboard/Vulnerability) Koku (Cost-Management)
Cockpit (incl. Composer) Fuse-Online
oVirt Insights-Curiosity
Integreatly

Comparison

Library comparisons react-i18next vs react-intl vs i18next

react-intl sees more downloads, has more forks and stars than react-i18next. However, react-i18next depends on i18next, which is the main JS library that has wrappers for various frameworks, including React. i18next gets more downloads than react-intl, but react-intl still maintains more stars and forks.

Verdict: Focusing on just the react framework, react-intl beats out react-i18next when it comes to repo stats.

react-intl react-i18next
Documentation - +
Active Maintainers - +
Built-in formatters + -
Define custom formatters - +
Load translations helpers - +
Supported frameworks - Format.js (js, react-intl, ember-intl, templates (handlebar, dust)) + i18next (js, react-i18next, ng-i18next, vue-i18next, jquery-i18next, loc-i18next, aurelia-i18next, i18next-meteor, django-18nxt, node.js, express, electron, react-native, ios native, android native,…)
Extract translations from code - +
Namespaces, split translations into files - +
Dynamic fallbacks - +

Winner: react-i18next

react-intl

Example that integrates react-intl into patternfly-react-seed

https://github.com/jschuler/patternfly-react-seed/pull/1

Viewable example: http://pf-seed-react-intl.surge.sh/support

react-i18next

Example that integrates react-i18next into patternfly-react-seed

https://github.com/jschuler/patternfly-react-seed/pull/2

Viewable example: http://pf-react-seed-i18next.surge.sh/support

OpenShift

PR

https://github.com/jschuler/console/pull/1

Using

Translation keys

We can translate text without having to worry about a key naming strategy. The text is used as the key. Example - Before:

const MyCoolComponent = () => <div>Awesome!</div>

After:

const MyCoolComponent = () => <div>{t(Awesome!)}</div>

This will generate the following json:

{
  "Awesome!": "Awesome!"
}

Namespacing

By default, translations are written to the default namespace file (public.json). But it is encouraged to namespace strings so that they end up in area relevant json files. Taking the above example, we can namespace using the namespace~ syntax:

const MyCoolComponent = () => <div>{t(cool~Awesome!)}</div>

Which will write the key into cool.json.

Namespaces have to be registered in i18n.js, example:

i18n.init({
  ns: ['nav', 'cool'],
});

useTranslation hook

For functional components, the useTranslation hook can be used to access the t function

import { useTranslation } from  'react-i18next';

export const MyCoolComponent = () => {
  const { t, i18n } = useTranslation();
  return (
    <>
      <div>{t(Awesome!)}</div>
      <div>Current locale: {i18n.language}</div>
      <button onClick={() => i18n.changeLanguage('de')}>Change language to German</button>
    </>
  )
}

More information: https://react.i18next.com/latest/usetranslation-hook

withTranslation HOC

For class based components, the withTranslation higher-order-component can be used to inject the t prop.

import { withTranslation } from  'react-i18next';

class MyCoolComponent extends React.Component {
  render() {
    const { t, i18n } = this.props;
    return (
      <>
        <div>{t(Awesome!)}</div>
        <div>Current locale: {i18n.language}</div>
        <button onClick={() => i18n.changeLanguage('de')}>Change language to German</button>
      </>
    )
  }
}

const MyCoolComponentWithTranslation = withTranslation()(MyCoolComponent);

export default MyCoolComponentWithTranslation;
// or export { MyCoolComponentWithTranslation as MyCoolComponent }

More information: https://react.i18next.com/latest/withtranslation-hoc

Generate language strings

Run yarn i18n to parse the code for usages of the t function, and write out the key/value pairs to their relevant json files.

Adding languages

  1. First update in i18next-parser.config.js the SUPPORTED_LOCALES object with the new locale code, e.g. if we're adding Arabic const SUPPORTED_LOCALES = { 'en': 'English', 'ar': 'عربي' };
  2. Import the corresponding moment.js locale at the top of i18n.js
  3. Run the parser to generate the new language json files yarn i18n

Pluralization

t('{{count}} item', {count:  2})

Running yarn i18n generates (depending on language and pluralization rules for numbers)

// English has simple singular and plural rules
{
  "{{count}} item": "{{count}} item", // singular
  "{{count}} item_plural": "{{count}} item" // plural
}

or

// Arabic has more pluralization rules
{
  "{{count}} item_0": "{{count}} item", // zero
  "{{count}} item_1": "{{count}} item", // singular
  "{{count}} item_2": "{{count}} item", // two
  "{{count}} item_3": "{{count}} item", // few
  "{{count}} item_4": "{{count}} item", // many
  "{{count}} item_5": "{{count}} item" // other
}

More info: https://www.i18next.com/translation-function/plurals

Dates

moment.js is used to format locale-aware dates, formatting of the date is specified in the json file. For example, the following: t('Date: It is now {{date, MM/DD/YYYY}}', { date: new Date() }) would create in locales/en/public.json "Date: It is now {{date, MM/DD/YYYY}}": "Date: It is now {{date, MM/DD/YYYY}}" In locales/de/public.json the value would initially be the same, but we can easily change the value to start with the day instead of the month as we did for the en locale: "Date: It is now {{date, MM/DD/YYYY}}": "Datum: Heute ist es {{date, DD/MM/YYYY}}"

Numbers

The browser supported Intl.NumberFormat api is used to format numbers by locale. When calling the t function, pass number as the second argument after value. Example usage: t('Number: ${{value, number}}', { value: 1550.95 })

Gotchas and misc

OpenShift bundle size

The following numbers included the addition of these dependencies:

Note: moment.js was already a project dev-dependency, now it is also used as a dependency

Before adding i18n After adding i18n Difference
Parsed 13.36 mb 13.45 mb 90 kb
Gzipped 3.57mb 3.6 mb 30 kb
jschuler commented 4 years ago

i18n libraries have been reviewed, and a PR was created for OpenShift following the findings.

jschuler commented 4 years ago

(WIP) Koku (Cost Management)

Koku uses i18next. An investigation was done to see what it would take to convert it over to react-intl, and to explore further what the shortcomings of react-intl are.

In summary, I would recommend sticking with i18next.

The PR (WIP) is here https://github.com/jschuler/koku-ui/pull/1

Shortcomings of react-intl:

Reasons for i18next