Closed dgutride closed 4 years ago
Goal: provide a recommendation strategy for localization that can be incorporated into OpenShift
Options:
react-intl
(Insights uses this lightly)react-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 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.
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
react-intl 0: Foreman-Console 1: Foreman-Tasks 2: Insights-Compliance 3: Insights-Dashboard 4: Insights-Vulnerability 5: Product-Shared-Library 6: Product-Shared-Library 7: RHEL-Composer 8: Settings-Catalog 9: Settings-RBAC 10: Settings-Sources 11: Virtualization-Frontend
react-intl-po 0: RHEL-Composer 1: Virtualization-Frontend
babel-plugin-react-intl 0: Insights-Dashboard 1: Product-Shared-Library 2: Product-Shared-Library 3: RHEL-Composer
babel-plugin-react-intl-auto 0: RHEL-Composer
i18next 0: 3Scale 1: Cost-Management 2: Fuse-Online 3: Insights-Curiosity 4: Integreatly
i18next-browser-languagedetector 0: 3Scale 1: Fuse-Online
i18next-express-middleware 0:Cost-Management
i18next-json-sync 0: Cost-Management 1: Integreatly
i18next-xhr-backend 0: Cost-Management 1: Insights-Curiosity 2: Integreatly
react-i18next 0: 3Scale 1: Cost-Management 2: Fuse-Online 3: Insights-Curiosity 4: Integreatly
react-intl | i18next |
---|---|
Foreman (Console/Tasks) | 3scale |
Insights (Compliance/Dashboard/Vulnerability) | Koku (Cost-Management) |
Cockpit (incl. Composer) | Fuse-Online |
oVirt | Insights-Curiosity |
Integreatly |
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 | - | + |
https://github.com/jschuler/patternfly-react-seed/pull/1
https://github.com/jschuler/patternfly-react-seed/pull/2
https://github.com/jschuler/console/pull/1
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!"
}
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'],
});
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
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
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.
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': 'عربي' };
i18n.js
yarn i18n
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
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}}"
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 })
t
function, otherwise the i18n parser will not automatically pick up the key
t(`nav~hello, it is now {{date}}`, { date: new Date() })
t('nav~hello, it is now {{date}}', { date: new Date() })
t
function, you can add this shortcut to VSCode by navigating to Code > Preferences > Keyboard Shortcuts
and then clicking on the upper-right icon to open up the keyboard bindings json file
{
"key": "cmd+k",
"command": "editor.action.insertSnippet",
"args": { "snippet": "{t(${TM_SELECTED_TEXT/['\"']/'/gi})}" }
}
Use this shortcut by highlighting a string, e.g. "Monitor"
and using the shortcut key CMD+K
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 |
i18n libraries have been reviewed, and a PR was created for OpenShift following the findings.
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:
Prefers flat json data, i.e
// works with this
{
"hello": "hello",
"world": "world"
}
// but not with nested data
{
"welcome": {
"hello": "hello",
"world": "world"
}
}
Koku makes extensive use of nested data, i.e. by calling t('welcome.hello')
To workaround this, additional logic has to be added to flatten the json like it was done here https://github.com/jschuler/koku-ui/pull/1/files#diff-0417c9525c1207ae1b79720843d58550R8
Little more work to use outside of components (e.g. in utility functions) To use it outside of components, need to create an intl instance and then use that, example: https://formatjs.io/docs/react-intl/api#createintl
simple function calls to translate messages require an id to be passed, e.g.
// does not work with
t('hello.world')
// wants
t({ id: 'hello.world' })
No built-in mechanism to load messages from another web-server
Does not support referencing keys in values in message json files. Koku uses this feature quite a bit in the json file: https://www.i18next.com/translation-function/nesting#basic For example:
"widget_subtitle": "{{startDate}} $t(months.{{month}})",
Using $t
in a value lets you reference other keys. This cannot be done in react-intl.
Splitting translations into multiple files. Only load translations needed.
There are plugins to detect languages for most environments (browser, native, server). This enables to set priority of where to detect and even enables to cache set languages over requests / visits.
There are endless plugins to load translation from server, filesystem, ... these backends also assert that loading gets retried on failure, or that a file does not get loaded twice and callbacks of success are only called once. Those backends can even provide an additional layer for local caching eg. in localStorage.
Options what to load and how to fallback depending on language.
Support for objects and arrays
Full control over management of the translations stored.
Rich system of events to react on changes important to your application.
Freedom of i18n formats - prefer ICU? Just use i18next-icu plugin.
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?