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

RFC: Kibana Internationalization #17201

Closed
azasypkin opened this issue Mar 16, 2018 · 30 comments
Closed

RFC: Kibana Internationalization #17201

azasypkin opened this issue Mar 16, 2018 · 30 comments
Assignees
Labels
Meta Project:i18n Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc

Comments

@azasypkin
Copy link
Member

azasypkin commented Mar 16, 2018

Summary

All user-facing text in core Kibana and plugins should be localizable. Localizable user defined text is out of scope for this RFC.

What won't be translated?

  • Error messages from Elasticsearch?
  • Terminology such as index and index-pattern? -Yes, need to be translated
  • How much do we want to customize Elasticsearch terms?
  • Should we translate aggregation types, terms like "index patterns", etc.
  • How much do we translate error message (e.g. errors the timelion server returns)?

Motivation

Kibana is used by people from all over the world that speak different languages, use different date and currency formats. Localized Kibana will reduce unnecessary friction and fit naturally into existing users' workflow.

Internationalization engine will provide a means to supply translations in a standard and future-proof format that it not dependent on a localization framework we or plugin developers may use in the future.

The feature set includes, but not limited to:

  • Easy-to-read syntax for developers and translators
  • Date, time, and number formatting
  • Plural categories
  • Proper tooling (tests for missing or orphaned translations, colliding IDs)
  • Plugins support (i18n engine should be able to consume translation files exposed by plugins)
  • BiDi support (isn't planned for the initial phase)

Guide-level explanation

Use this link to read in-depth and up-to-date guide-level explanation.

Technical-level explanation

Use this link to read in-depth and up-to-date technical-level explanation.

Experiments & proofs of concept

We decided to move forward with solution based on FormatJS core libs: it has relatively small code base, good documentation, uses well-known ICU message syntax and can be used on the server side (NodeJS), in React (react-intl) and AngularJS (custom component that we'll build).

The other front runners were Fluent and custom solution based on messageformat.js, but for the time being we decided to not base our initiative on these tools due to low adoption/immaturity (Fluent) or significantly larger amount of work needed for the bootstrap phase (messageformat.js).

Angular Translate with its very outdated messageformat.js dependency wouldn't allow us to use the same message parsing and formatting engine across the "stack" that may lead to various subtle issues and inconsistencies.

Even though i18next seems to be well supported, works in NodeJS and has components for both Angular and React, currently we don't see it as a good solution for Kibana: custom ICU-incompatible message format (at the time of writing) and hard-to-follow code base that may not give us the level of flexibility we need.

See links below for more details:

Unresolved questions

  • Is it acceptable to provide the best possible developer experience for React and good enough for Jade/Angular?

References

History

  • 2018-04-10: Added links to comments with PoC summary
  • 2018-04-23: Added summary for all PoC and rationale behind final decision (move forward with FormatJS Core libs)
  • 2018-04-25: Added References section
  • 2018-04-26: Added Technical-level explanation details
  • 2018-05-28: Corrected link to Technical-level explanation
  • 2018-08-10: Added link to Guide-level explanation
  • 2018-08-29: Added link to all ongoing i18n Kibana issues
@azasypkin azasypkin added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Meta Project:i18n WIP Work in progress labels Mar 16, 2018
@epixa
Copy link
Contributor

epixa commented Mar 16, 2018

Plugins support (may be not implemented initially)

Given that Kibana itself is powered by a bunch of core plugins, which don't do anything "special" compared to other plugins other than the fact that they can't be uninstalled, I don't think we can ignore the plugin aspect of this.

Is it acceptable to provide the best possible developer experience for React and good enough for Jade/Angular?

This seems reasonable to me, though "good enough" may be hard to identify. We'll probably just need to identify an approach for those technologies and then make a relatively subjective decision at that time.

@azasypkin
Copy link
Member Author

Given that Kibana itself is powered by a bunch of core plugins, which don't do anything "special" compared to other plugins other than the fact that they can't be uninstalled, I don't think we can ignore the plugin aspect of this.

This is fair point, thanks. Updated.

This seems reasonable to me, though "good enough" may be hard to identify. We'll probably just need to identify an approach for those technologies and then make a relatively subjective decision at that time.

Yes, it will become clearer once we have concrete proposals. By "good enough" in this context I mean "supports all the features we need, but not as expressive and easy-to-read as the best possible one", but we'll see.

@maksim-tolo
Copy link
Contributor

maksim-tolo commented Mar 27, 2018

Here you can see the examples based on PoC for angular-translate and react-intl.

Simple text

Angular

<h2
  translate="KIBANA-DISCOVER-SEARCHING"
  translate-default="Searching"
></h2>

React

<h2>
  <FormattedMessage
    id="KIBANA-DISCOVER-SEARCHING"
    defaultMessage="Searching"
  />
</h2>

Translation

{
  "KIBANA-DISCOVER-SEARCHING": "Searching"
}

Attribute

Angular

<input
  type="text"
  placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate}}"
>

React

import React from 'react';
import { injectIntl, intlShape } from 'react-intl';

const Component = ({ intl }) => (
  <input
    type="text"
    placeholder={intl.formatMessage({
      id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
      defaultMessage: 'Search...',
    })}
  />
);

export default injectIntl(Component);

Attribute with variables interpolation

Angular

<input
  type="text"
  placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: { TITLE: service.title }}}"
>

React

import React from 'react';
import { injectIntl, intlShape } from 'react-intl';

const Component = ({ intl, service }) => (
  <input
    type="text"
    placeholder={intl.formatMessage({
      id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
      defaultMessage: '{TITLE} search',
    }, { TITLE: service.title })}
  />
);

export default injectIntl(Component);

Text with plurals

Angular

<span
  translate="KIBANA-DISCOVER-HITS"
  translate-values="{HITS: hits}"
  translate-default="{HITS, plural, one {# hit} other {# hits}}"
></span>

React

<FormattedMessage
  id="KIBANA-DISCOVER-HITS"
  values={{ HITS: hits }}
  defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>

Translation

{
  "KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}

Text with nested formatting

Angular

<span
  translate="KIBANA-DISCOVER-REFINE_SEARCH"
  translate-values="{SIZE: '<b>{{opts.sampleSize}}</b>'}"
  translate-default="These are the first {SIZE} documents matching your search, refine your search to see others."
></span>

React

<FormattedMessage
  id="KIBANA-DISCOVER-REFINE_SEARCH"
  values={{ SIZE: <b>{opts.sampleSize}</b> }}
  defaultMessage="These are the first {SIZE} documents matching your search, refine your search to see others."
/>

Translation

{
  "KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {SIZE} documents matching your search, refine your search to see others."
}

From my point of view attribute translation in react-intl is not very convenient, so we can write our own component or helper for this purpose. Also we have to wrap each top-level react component into IntlProvider which leads to code overhead.

@maksim-tolo
Copy link
Contributor

@chiweichang I used ICU message-format (http://userguide.icu-project.org/formatparse/messages) for pluralization in this example. The numeric input is mapped to a plural category, some subset of "zero", "one", "two", "few", "many", and "other" depending on the locale and the type of plural. Also we can use "=" prefix to match for exact values (=0, =1, etc).

@maksim-tolo
Copy link
Contributor

@chiweichang np. Yes, you are right.

@maksim-tolo
Copy link
Contributor

maksim-tolo commented Mar 30, 2018

Here you can see the examples based on PoC for i18next.

Simple text

Angular

<h2 ng-i18next="[i18next]({ defaultValue: 'Searching' })KIBANA-DISCOVER-SEARCHING"></h2>

React

<h2>
  <Trans i18nKey="KIBANA-DISCOVER-SEARCHING" />
    Searching
  </Trans>
</h2>

Translation

{
  "KIBANA-DISCOVER-SEARCHING": "Searching"
}

Attribute

Angular

<input
  type="text"
  ng-i18next="[placeholder:i18next]({ defaultValue: 'Search...' })KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER"
>

React

import React from 'react';
import { I18n } from 'react-i18next';

const Component = () => (
  <I18n>
    {(t, { i18n }) => (
      <input
        type="text"
        placeholder={t(['KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER', 'Search...'])}
      />
    )}
  </I18n>
);

Attribute with variables interpolation

Angular

<input
  type="text"
  ng-i18next="[placeholder:i18next]({
    defaultValue: '\{\{TITLE\}\} search',
    TITLE: {{ service.title }}
  })KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER"
>

React

import React from 'react';
import { I18n } from 'react-i18next';

const Component = ({ service }) => (
  <I18n>
    {(t, { i18n }) => (
      <input
        type="text"
        placeholder={t(['KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER', '{{TITLE}} search'], { TITLE: service.title })}
      />
    )}
  </I18n>
);

Text with plurals

Angular

<span
  ng-i18next="[i18next]({
    count: {{ hits }},
  })KIBANA-DISCOVER-HITS"
></span>

React

<Trans
  i18nKey="KIBANA-DISCOVER-HITS"
  count={hits}
/>

Translation

{
  "KIBANA-DISCOVER-HITS": "{{count}} hit",
  "KIBANA-DISCOVER-HITS_plural": "{{count}} hits",
}

Text with nested formatting

Angular

<span
  ng-i18next="[html:i18next]({
    SIZE: '<b>{{opts.sampleSize}}</b>',
    defaultValue: 'These are the first \{\{SIZE\}\} documents matching your search, refine your search to see others.',
  })KIBANA-DISCOVER-REFINE_SEARCH"
></span>

React

<Trans i18nKey="KIBANA-DISCOVER-REFINE_SEARCH-REACT">
  These are the first <b>{{ SIZE: opts.sampleSize }}</b> documents matching your search, refine your search to see others.
</Trans>

Translation

{
  "KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {{SIZE}} documents matching your search, refine your search to see others.",
  "KIBANA-DISCOVER-REFINE_SEARCH-REACT": "These are the first <1><0>{{SIZE}}</0></1> documents matching your search, refine your search to see others."
}

It seems to me that ng-i18next is too complicated compared to angular-translate. Also i18next pluralization is not so flexible as ICU message-format.

@shikhasriva
Copy link

@azasypkin , this is great. Are we still following the design described #6515. Would love to collaborate again

@srl295
Copy link
Contributor

srl295 commented Apr 4, 2018

@azasypkin what @shikhasriva said, great to see this starting up again.

@azasypkin
Copy link
Member Author

Are we still following the design described #6515.

I believe some parts will stay the same, but some will likely change based on the current state of Kibana and how we see it in the future. We are figuring it out at the moment.

Would love to collaborate again

It's great to know! We'll be updating this RFC as we progress through our explorations, design and implementation, please stick around 🙂

@azasypkin
Copy link
Member Author

Hey @maksim-tolo, just few questions regarding your PoC's:

PoC with angular-translate/react-intl.

<input type="text" placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate}}" translate-default="Search...">

I see there is only one translate-default, but how does the framework know that default value is for placeholder attribute? E.g. we can have multiple attributes we want to localize (e.g. placeholder and title).

<input type="text" placeholder="{{'KIBANA-.....' | translate: { TITLE: service.title }}">

What is the service here and how is it "injected"/created?

From my point of view attribute translation in react-intl is not very convenient, so we can write our own component or helper for this purpose.

Could you please elaborate on this a bit more, what problems exactly do you see?

Btw what is the syntax for "Text with plurals" within attributes for Angular/React (if it's supported)?

PoC with i18next.

<input type="text" ng-i18next="[placeholder:i18next]({ defaultValue: 'Search...' })KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER">

Same question here, is it possible to localize multiple attributes?

<input type="text" ng-i18next="[placeholder:i18next]({ defaultValue: '{{TITLE}} search', TITLE: {{ ... }}})..."

\{\{TITLE\}\} - ugh, just for my understanding, why do we need to escape parameter names like this (\{\}...\}\})?

Text with plurals

I don't see any examples with default message in this section. Is there any reason why we can't define default messages when dealing with plurals?

Also i18next pluralization is not so flexible as ICU message-format.

Do you have any pluralization related use cases in mind that are supported by ICU message format, but aren't by i18next or just the syntax is cumbersome?

@maksim-tolo
Copy link
Contributor

Hi @azasypkin!

I see there is only one translate-default, but how does the framework know that default value is for placeholder attribute? E.g. we can have multiple attributes we want to localize (e.g. placeholder and title).

Oh, my fault. translate filter doesn't support default messages.

What is the service here and how is it "injected"/created?

It's just example from the app, so you can pass any value here.

Could you please elaborate on this a bit more, what problems exactly do you see?

It's just my personal opinion. We have to wrap each component into HoC using injectIntl in order to translate attributes. I prefer to use render callbacks approach.

Btw what is the syntax for "Text with plurals" within attributes for Angular/React (if it's supported)?

The syntax exactly the same as for "Attribute with variables interpolation".

Same question here, is it possible to localize multiple attributes?

Yes, it's possible. You should split attributes translation using ";" separator. The example is below:

<input type="text"
  ng-i18next="[placeholder:i18next]({ defaultValue: 'Search...' })KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER;
  [aria-label:i18next]({ defaultValue: 'Search' })KIBANA-MANAGEMENT-OBJECTS-SEARCH"
>

\{\{TITLE\}\} - ugh, just for my understanding, why do we need to escape parameter names like this (\{\}...\}\})?

If we didn't escape the value, Angular would interpolate this expression from the $scope.

I don't see any examples with default message in this section. Is there any reason why we can't define default messages when dealing with plurals?

Pluralization in i18next requires additional translation key with "_plural" postfix. I don't know how to pass 2 default messages (singular and plural) into the component.

Do you have any pluralization related use cases in mind that are supported by ICU message format, but aren't by i18next or just the syntax is cumbersome?

There are languages with multiple plural forms (Language Plural Rules). i18next pluralization format allows to declare only two forms: singular and plural.

@maksim-tolo
Copy link
Contributor

Here you can see the examples based on PoC with custom translation components using https://github.com/messageformat/messageformat.js under the hood. Also I've added addition examples for date, time, duration and number translation.

Simple text

Angular

<h2
  i18n="KIBANA-DISCOVER-SEARCHING"
  default-message="Searching"
></h2>

React

<h2>
  <I18n
    path="KIBANA-DISCOVER-SEARCHING"
    defaultMessage="Searching"
  />
</h2>

Translation

{
  "KIBANA-DISCOVER-SEARCHING": "Searching"
}

Attribute

Angular

<input
  type="text"
  placeholder="{{ 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: { defaultMessage: 'Search...' } }}"
>

React

<I18n>
  {translate => (
    <input
      type="text"
      placeholder={translate({
        path: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
        defaultMessage: 'Search...',
      })}
    />
  )}
</I18n>

Attribute with variables interpolation

Angular

<input
  type="text"
  placeholder="{{ 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
    vars: { TITLE: service.title },
    defaultMessage: '{TITLE} search',
  } }}"
>

React

<I18n>
  {translate => (
    <input
      type="text"
      placeholder={translate({
        path: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
        vars: { TITLE: service.title },
        defaultMessage: '{TITLE} search',
      })}
    />
  )}
</I18n>

Text with plurals

Angular

<span
  i18n="KIBANA-DISCOVER-HITS"
  vars="{HITS: hits}"
  default-message="{HITS, plural, one {# hit} other {# hits}}"
></span>

React

<I18n
  path="KIBANA-DISCOVER-HITS"
  vars={{ HITS: hits }}
  defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>

Translation

{
  "KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}

Text with nested formatting

Angular

<span
  i18n="KIBANA-DISCOVER-REFINE_SEARCH"
  vars="{SIZE: '<b>{{opts.sampleSize}}</b>'}"
  default-message="These are the first {SIZE} documents matching your search, refine your search to see others."
></span>

React

<I18n
  path="KIBANA-DISCOVER-REFINE_SEARCH"
  vars={{ SIZE: <b>{opts.sampleSize}</b> }}
  defaultMessage="These are the first {SIZE} documents matching your search, refine your search to see others."
/>

Translation

{
  "KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {SIZE} documents matching your search, refine your search to see others."
}

Date

Supported parameters are short, default, long, or full.

Angular

<span
  i18n="KIBANA-TODAY"
  vars="{ DATE: Date.now() }"
  default-message="Today is {DATE, date}"
></span>

React

<I18n
  path="KIBANA-TODAY"
  vars={{ DATE: Date.now() }}
  defaultMessage="Today is {DATE, date}"
/>

Translation

{
  "KIBANA-TODAY": "Today is {DATE, date}"
}
// 'Today is Apr 10, 2018'

Duration

Represent a duration in seconds as a string.

Angular

<span
  i18n="KIBANA-SINCE"
  vars="{ D: 123 }"
  default-message="It has been {D, duration}"
></span>

React

<I18n
  path="KIBANA-SINCE"
  vars={{ D: 123 }}
  defaultMessage="It has been {D, duration}"
/>

Translation

{
  "KIBANA-SINCE": "It has been {D, duration}"
}
// 'It has been 2:03'

Number

Supported parameters are integer, percent, or currency.

Angular

<span
  i18n="KIBANA-ALMOST"
  vars="{ N: 3.14 }"
  default-message="{N} is almost {N, number, integer}"
></span>

React

<I18n
  path="KIBANA-ALMOST"
  vars={{ N: 3.14 }}
  defaultMessage="{N} is almost {N, number, integer}"
/>

Translation

{
  "KIBANA-ALMOST": "{N} is almost {N, number, integer}"
}
// '3.14 is almost 3'

Time

Supported parameters are short, default, long, or full.

Angular

<span
  i18n="KIBANA-NOW"
  vars="{ T: Date.now() }"
  default-message="The time is now {T, time}"
></span>

React

<I18n
  path="KIBANA-NOW"
  vars={{ T: Date.now() }}
  defaultMessage="The time is now {T, time}"
/>

Translation

{
  "KIBANA-NOW": "The time is now {T, time}"
}
// 'The time is now 10:00:00 PM'

As any custom solution, this approach has own pros and cons.

Pros:

  • we are able to modify components API, it gives us more flexibility
  • we can make translation syntax for both frameworks really similar
  • we can replace pluralization engine at any time

Cons:

  • have to maintain more code
  • potentially can leads to more bugs
  • not standard syntax

@azasypkin
Copy link
Member Author

I've spent some time plyaing with Fluent and here is PoC based on it. Unfortunately there is no "native" Angular wrapper for it, so I used framework-independent JS version instead.

Common bootstrapping code:

import { MessageContext } from 'fluent/compat';
import { negotiateLanguages } from 'fluent-langneg/compat';

// Asynchronous generator/iterator (semi-pseudo code).
function* generateMessages(resourceIds) {
  const resources = await Promise.all(
    resourceIds.map((resourcePath) => fetch(resourcePath))
  );

  const requestedLocales = navigator.languages // ['en-US', 'ru', ...];
  const availableLocales = // e.g. extract BCP47 locale IDs from loaded resources.
  
  // Choose locales that are best for the user.
  const currentLocales = negotiateLanguages(
    requestedLocales,
    availableLocales,
    { defaultLocale: 'en-US' }
  );

  for (const locale of currentLocales) {
    const cx = new MessageContext(locale);
    for (const resource of resources) {
      cx.addMessages(resources[locale]);
    }
    yield cx;
  }
}

Angular bootstrapping code:

import { DOMLocalization } from 'fluent-dom/compat';

// Called once, then observes root for mutations and (re-)translate when needed.
const l10n = new DOMLocalization(window, ['/plugins/security.ftl'], generateMessages);
l10n.connectRoot(document.documentElement);
l10n.translateRoots();

React bootstrapping code:

import { LocalizationProvider } from 'fluent-react/compat';

export function Root() {
  return (
    <LocalizationProvider messages={generateMessages(['/plugins/security.ftl'])}>
      <App />
    </LocalizationProvider>
  );
}

Simple text

Angular

<h2 data-l10n-id="L10N.SEARCH">Search...</h2>

React

<Localized id="L10N.SEARCH">
   <h2>Search...</h2>
</Localized>

Message format (*.ftl)

{
  'en-US': `
L10N.SEARCH = Search...
` 
}

Attribute (multiple attributes, attribute + main content)

Angular

<input data-l10n-id="L10N.SEARCH" type="text" placeholder="Search..." title="Title..." />

<span data-l10n-id="L10N.SOMETHING" title="Title...">Something...</span>

React

import { Localized } from 'fluent-react/compat';

<Localized id="L10N.SEARCH" attrs={{ placeholder: true, title: true }}>
  <input type="text" placeholder="Search..." title="Title..." />
</Localized>

<Localized id="L10N.SOMETHING" attrs={{ title: true }}>
  <span title="Title...">Something...</span>
</Localized>

Message format (*.ftl)

{
  'en-US': `
L10N.SEARCH =
  .placeholder = Search...
  .title = Title...
L10N.SOMETHING = Something...
  .title = Title...
`
}

Text with parameters

Angular

<div data-l10n-id="L10N.TEXT_WITH_PARAM" data-l10n-args='{"param": "some param"}' 
     title="Title with $param">
  Text with $param
</div>

React

import { Localized } from 'fluent-react/compat';

<Localized id="L10N.TEXT_WITH_PARAM" $param="some param" attrs={{ title: true }}>
  <div title="Title with $param">
    Text with $param
  </div>
</Localized>

Message format (*.ftl)

{
  'en-US': `
L10N.TEXT_WITH_PARAM = Text with {$param}
  .title = Title with {$param}
`
}

Text with plurals

Angular

<div data-l10n-id="L10N.TEXT_WITH_PLURALS" data-l10n-args='{"numberOfParams": "5"}' 
     title="Title with $numberOfParams">
  Text with $numberOfParams
</div>

React

import { Localized } from 'fluent-react/compat';

<Localized id="L10N.TEXT_WITH_PLURALS" $numberOfParams={5} attrs={{ title: true }}>
  <div title="Title with $numberOfParams">
    Text with $numberOfParams
  </div>
</Localized>

Message format (*.ftl)

{
  'en-US': `
L10N.TEXT_WITH_PLURALS =
  { $numberOfParams ->
      [one] One param.
     *[other] Params number: { $numberOfParams }.
  }
  .title =
    { $numberOfParams ->
        [one] One param in title.
       *[other] Params number in title: { $numberOfParams }.
    }
`
}

Text with nested formatting

Angular (based on DOM Overlays)

<div data-l10n-id="L10N.TEXT_WITH_STYLE">
  Text with <span class="some-style">styled arg</span>.
</div>
{
  'en-US': `
L10N.TEXT_WITH_STYLE = Text with <span>styled arg</span>.
`
}

React

import { Localized } from 'fluent-react/compat';

<Localized id="L10N.TEXT_WITH_STYLE" 
    arg={<span className="some-style">styled arg</span>}>
  <div>Text with <arg />.</div>
</Localized>
{
  'en-US': `
L10N.TEXT_WITH_STYLE = Text with <arg>styled arg</arg>.
`
}

Pros:

  • Subjectively FTL syntax looks more readable than ICU message format (e.g. support for multiline text, grouping of attributes)
  • Seems to support everything we may need (plurals, genders, conjugations, date and number formatting, default messages, BiDi)
  • Designed for web from the beginning, alligned with standards (Unicode, CLDR, ECMA402, ICU and W3C LTLI) and codebase is relatively small in case we decide to take it over and extend

Cons:

  • There is no large comunity behind Fluent even though it's used by Firefox
  • There is no Angular "native" bindings provided out of the box
  • Built around its own localization file format (FTL) that isn't used by anyone else
  • It's still relatively young

@timroes
Copy link
Contributor

timroes commented Apr 20, 2018

Out of a forum post I am wondering, what is the current plan for server side internationalization? We're having some strings that are generated server side, like app names and descriptions. Will that work with the above approaches, will we introduce a new header to signal the server which locale the current user is using per request, will we only have one language per Kibana instance available and thus can read it out of the config?

I think for some of the static code like app names and descriptions, that's still rather easy to transfer the translation key and translate it in the frontend, but what's about more dynamic content like server side generated error messages, etc?

@azasypkin
Copy link
Member Author

Out of a forum post I am wondering, what is the current plan for server side internationalization?

Ability to localize text/numbers/dates on the server side (NodeJS) is something we plan to support from the day one.

Will that work with the above approaches, will we introduce a new header to signal the server which locale the current user is using per request, will we only have one language per Kibana instance available and thus can read it out of the config?

Everything is still in flux, but we can rely on Accept-Language HTTP header or config value in kibana.yml. Current plan is to have only one language per instance + fallback (English). We may reconsider this along the way though.

I think for some of the static code like app names and descriptions, that's still rather easy to transfer the translation key and translate it in the frontend, but what's about more dynamic content like server side generated error messages, etc?

Yeah, agree, user-facing errors should be localized too.

@maksim-tolo
Copy link
Contributor

Another examples based on PoC for react-intl and custom AngularJS wrapper for format.js library are below.

Simple text

Angular

<h2
  translate="KIBANA-DISCOVER-SEARCHING"
  default-message="Searching"
></h2>

React

<h2>
  <FormattedMessage
    id="KIBANA-DISCOVER-SEARCHING"
    defaultMessage="Searching"
  />
</h2>

Translation

{
  "KIBANA-DISCOVER-SEARCHING": "Searching"
}

Attribute

Angular

<input
  type="text"
  placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: { defaultMessage: 'Search...' }}}"
>

React

import React from 'react';
import { injectIntl, intlShape } from 'react-intl';

const Component = ({ intl }) => (
  <input
    type="text"
    placeholder={intl.formatMessage({
      id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
      defaultMessage: 'Search...',
    })}
  />
);

export default injectIntl(Component);

Attribute with variables interpolation

Angular

<input
  type="text"
  placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: {
    values: { TITLE: service.title },
    defaultMessage: '{TITLE} search'
  } }}"
>

React

import React from 'react';
import { injectIntl, intlShape } from 'react-intl';

const Component = ({ intl, service }) => (
  <input
    type="text"
    placeholder={intl.formatMessage({
      id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
      defaultMessage: '{TITLE} search',
    }, { TITLE: service.title })}
  />
);

export default injectIntl(Component);

Text with plurals

Angular

<span
  translate="KIBANA-DISCOVER-HITS"
  translate-values="{HITS: hits}"
  default-message="{HITS, plural, one {# hit} other {# hits}}"
></span>

React

<FormattedMessage
  id="KIBANA-DISCOVER-HITS"
  values={{ HITS: hits }}
  defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>

Translation

{
  "KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}

Text with nested formatting

Angular

<span
  translate="KIBANA-DISCOVER-REFINE_SEARCH"
  translate-values="{SIZE: '<b>{{opts.sampleSize}}</b>'}"
  default-message="These are the first {SIZE} documents matching your search, refine your search to see others."
></span>

React

<FormattedMessage
  id="KIBANA-DISCOVER-REFINE_SEARCH"
  values={{ SIZE: <b>{opts.sampleSize}</b> }}
  defaultMessage="These are the first {SIZE} documents matching your search, refine your search to see others."
/>

Translation

{
  "KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {SIZE} documents matching your search, refine your search to see others."
}

Date

Supported parameters are short, medium, long, or full.

Angular

<span
  translate="KIBANA-TODAY"
  translate-values="{ DATE: Date.now() }"
  default-message="Today is {DATE, date, medium}"
></span>

React

<FormattedMessage
  id="KIBANA-TODAY"
  values={{ DATE: Date.now() }}
  defaultMessage="Today is {DATE, date, medium}"
/>

Translation

{
  "KIBANA-TODAY": "Today is {DATE, date, medium}"
}
// 'Today is Apr 20, 2018'

Number

Supported parameters are percent, or currency.

Angular

<span
  translate="KIBANA-PERCENT"
  translate-values="{ P: 0.15 }"
  default-message="{P, number, percent}"
></span>

React

<I18n
  id="KIBANA-PERCENT"
  values={{ P: 0.15 }}
  defaultMessage="{P, number, percent}"
/>

Translation

{
  "KIBANA-PERCENT": "{P, number, percent}"
}
// '15%'

Time

Supported parameters are short, medium, long, or full.

Angular

<span
  translate="KIBANA-NOW"
  translate-values="{ T: Date.now() }"
  default-message="The time is now {T, time, medium}"
></span>

React

<I18n
  id="KIBANA-NOW"
  values={{ T: Date.now() }}
  defaultMessage="The time is now {T, time, medium}"
/>

Translation

{
  "KIBANA-NOW": "The time is now {T, time, medium}"
}
// 'The time is now 4:00:00 PM'

Pros:

  • single format.js engine (intl-messageformat) for React, AngularJS and NodeJS
  • uses the ICU Message syntax and works for all CLDR languages
  • we are able to modify angular wrapper API, it gives us more flexibility

Cons:

  • have to write some additional tools for messages extraction from angular translation wrapper
  • have to maintain more code

@azasypkin
Copy link
Member Author

Here is the summary for all the frameworks and approaches we tried.

TL;DR: we decided to move forward with solution based on FormatJS core libs: it has relatively small code base, good documentation, uses well-known ICU message syntax and can be used on the server side (NodeJS), in React (react-intl) and AngularJS (custom component that we'll build).

The other front runners were Fluent and custom solution based on messageformat.js, but for the time being we decided to not base our initiative on these tools due to low adoption/immaturity (Fluent) or significantly larger amount of work needed for the bootstrap phase (messageformat.js).

Angular Translate with its very outdated messageformat.js dependency wouldn't allow us to use the same message parsing and formatting engine across the "stack" that may lead to various subtle issues and inconsistencies.

Even though i18next seems to be well supported, works in NodeJS and has components for both Angular and React, currently we don't see it as a good solution for Kibana: custom ICU-incompatible message format (at the time of writing) and hard-to-follow code base that may not give us the level of flexibility we need.

See table below for more details.

Metrics Angular-translate react-intl i18next Custom component based on messageformat.js Format.js for angular Fluent
ICU Message format support yes yes no yes yes no (custom FTL syntax)
Easy-to-read/good syntax ergonomics yes (default ICU message-format syntax) yes (default ICU message-format syntax) no (angular wrapper requires passing all parameters as a string to the single attribute, react Trans component uses not obvious placeholders for messages) yes (default ICU message-format syntax, flexible components API) yes (default ICU message-format syntax, flexible components API) yes (default messages are just normal text you'd write in text HTML elements and attributes, selectors and pluralization syntax may be cumbersome, FTL allows to add comments and reference one message within another)
Pluralization yes yes yes, but not so flexible as ICU message-format (there are languages with multiple plural forms, but i18next pluralization format allows to declare only two forms: singular and plural) yes yes yes (incl. CLDR pl. cat., follows Intl.PluralRules)
Date and Time yes yes no yes yes yes (follows Intl.DateTimeFormat)
Number (incl. percent, currency) yes yes no yes yes yes (follows Intl.NumberFormat)
Duration yes yes no yes yes yes (via custom formatter)
BiDi/RTL support no no no no no yes
Text with nested formatting yes yes yes, but translattion messages for react requres additional placeholders yes, but React integration is complicated yes yes
Attributes (placeholder, title etc.) yes, but without default messages yes, but not very convient (doesn't use render callback approach) yes yes yes yes
Attribute with variables interpolation yes yes yes yes yes yes
Default message yes, but filter doesn't support default messages yes yes, but pluralization doesn't support default messages yes yes yes
Exisitng tools for ID/message extraction yes (angular-translate-extract, gulp-angular-translate-extract) yes (babel-plugin-react-intl) yes (i18next-scanner) no, have to create own tools no no (requires our own tooling similar to babel-plugin-react-intl and i18next-scanner built on top of fluent-syntax FTL parser)
Community 4k+ stars, ~300k downloads per month, last published 4 months ago, 126 contributors 7k+ stars, ~500k downloads per month, last published 7 months ago, 39 contributors 3k+ stars, ~400k downloads per month, last published 1 day ago, 97 contributors messageformat.js itself has 1.2k+ stars, ~400k downloads per month, last published 9 hours ago, 34 contributors Intl MessageFormat itself has 400+ stars, ~600k downloads per month, last published 6 months ago, 19 contributors ~1 year old, 59 stars, last publish 6 days ago, 3 active full-time maintainers, used mainly by Firefox and other Mozilla projects
Plugin system support yes (custom interpolators) yes (custom formatters) yes yes (custom formatters) yes (custom formatters) yes (custom formatters/"functions")
Angular/React support out of the box yes, but only Angular yes, but only React yes, but Angular wrapper is too complicated compared to angular-translate. no, requires custom wrappers Custom wrapper yes, but Angular may require some additional work
Can be used on the server side (NodeJS)? no core library (Intl MessageFormat) can be used on the server side yes yes yes (single engine for React, Angular and NodeJS) yes (single engine for React, Angular and NodeJS)
Overall Summary Uses old messageformat.js dependency under the hood (v1.0.2) which leads to poor conformance with the ICU MessageFormat spec with respect to quoting and escaping. Has some syntax limitations, for example filter doesn't support default messages. Large community, build on the JavaScript Intl built-ins and industry-wide i18n standards, well-documented API Pluralization is limited, angular wrapper is ugly. Namespaces, graceful fallbacks, a lot of modules built for and around i18next, well-documented API. we can make translation syntax for both frameworks really similar. We are able to modify components API, it gives us more flexibility. We can replace pluralization engine at any time Uses Intl MessageFormat under the hood (single engine for React, Angular and NodeJS). We are able to modify angular wrapper API, it gives us more flexibility. Have to write some additional tools for messages extraction. Young, but capable framework/toolset that can be used in React, Angular and NodeJS with relatively small and well documented codebase. Main risks are low adoption (used and governed mainly by a single entity) and non-universal language file format (FTL, hard to say where its limits are). Will require custom tooling, nothing extraordinary though (very similar to tools for other better known frameworks).

@jinmu03
Copy link
Contributor

jinmu03 commented Apr 24, 2018

Wrote a blog about the POC. https://www.elastic.co/blog/keeping-up-with-kibana-2018-04-16

@RobertPaulson90
Copy link

Aren't you guys way overthinking this? There's so little text on the UI as is, in English, I find it hard to see the big problem? It was asked for 5-years ago. Can you please just do it, instead of worrying about the perfect implementation which seems to never happen?

@timroes
Copy link
Contributor

timroes commented May 17, 2018

@amivit Internationalizing a large UI like Kibana, isn't as easy as it sounds. Also internationalizing is way more then "replacing text strings". When talking about internationalization you'll always also talk about different number or date formats, pluralization of strings (since every language has a flexible amount of plural forms, and different forms for ordinal, cardinal and nominal - see e.g. Language Plural Rules) and way more (which we might not even touch, like different cultural understanding of colors).

Also since we are using several different web frameworks we need a solution, that works at least in Angular, React and Vanilla JS. Since internationalization hasn't been build in from the beginning it means, we currently need to replace around 8500 unique strings in a couple of thousand files.. and all that while ideally not a whole team of developers can continue working at the code base in the meanwhile.

The team will happily evaluate any constructive suggestions on how to improve the technical implementation in a clean and future-proof manner to solve these common i18n issues.

@azasypkin
Copy link
Member Author

azasypkin commented May 28, 2018

UPDATE: Initial version of technical-level explanation has just been merged: https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md (main RFC comment has been updated too)

@azasypkin
Copy link
Member Author

UPDATE: Initial version of guide-level explanation has just been merged: https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/GUIDELINE.md (main RFC comment has been updated too).

We also migrated Index Patterns tab to use i18n aware components instead of hard coded strings, follow this link if you're curious how that looks like.

@maryia-lapata
Copy link
Contributor

UPDATE: There is a good progress on usage of i18n components in Kibana code. Here is the list of plugins that are completely migrated to use localizable strings/primitives that will automatically pick up translations once they are ready:

  • Status page
  • Nav bar
  • Tutorial
  • Home page
  • Management: Index management, Advanced settings, Saved objects
  • Visualization: Input controls, Tag cloud, Markdown, Metric, Bar/Area/Line chart, Gauge/Goal chart, Pie chart, Data table, Coordinate map, Region map, Heat map.

@azasypkin azasypkin removed the WIP Work in progress label Feb 5, 2019
@azasypkin
Copy link
Member Author

The initial support for Kibana i18n has been just merged into 6.7 branch. The remaining work will be done in a separate more focused issues.

@hepam4
Copy link

hepam4 commented Feb 18, 2019

I hope spanish translation will be included.

@azasypkin
Copy link
Member Author

I hope spanish translation will be included.

Not at the initial release, but at some point it will definitely be included.

@pbassut
Copy link

pbassut commented Apr 3, 2019

@azasypkin I'm sorry I didn't follow along the entire thread or the internals of the feature. But will translating to a given language be as simple as touching a es_ES.txt, pt_BR.txt file? If so, I volunteer to work on the pt_BR one

@azasypkin
Copy link
Member Author

azasypkin commented Apr 3, 2019

Hey @pbassut,

But will translating to a given language be as simple as touching a es_ES.txt, pt_BR.txt file?

Yeah, almost. You'll need to generate en.json from required branch using node scripts/i18n_extract --output-dir ./. Then rename en.json to es.json, translate the content, put it for example into your-custom-plugin/translations/es.json and make sure Kibana is aware of this plugin path, that's it.

You can take a look at zh-CN.json to see how file with translations looks like. Current 6.7 branch includes ~9200 localized labels (master branch has ~9500 and it's constantly growing), so it will take some time to translate everything.

For pt-BR you may need to tweak locales.js, see details here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Meta Project:i18n Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc
Projects
None yet
Development

No branches or pull requests

14 participants