Closed azasypkin closed 5 years ago
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.
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.
Here you can see the examples based on PoC for angular-translate and react-intl.
<h2
translate="KIBANA-DISCOVER-SEARCHING"
translate-default="Searching"
></h2>
<h2>
<FormattedMessage
id="KIBANA-DISCOVER-SEARCHING"
defaultMessage="Searching"
/>
</h2>
{
"KIBANA-DISCOVER-SEARCHING": "Searching"
}
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate}}"
>
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);
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: { TITLE: service.title }}}"
>
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);
<span
translate="KIBANA-DISCOVER-HITS"
translate-values="{HITS: hits}"
translate-default="{HITS, plural, one {# hit} other {# hits}}"
></span>
<FormattedMessage
id="KIBANA-DISCOVER-HITS"
values={{ HITS: hits }}
defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>
{
"KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}
<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>
<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."
/>
{
"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.
@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).
@chiweichang np. Yes, you are right.
Here you can see the examples based on PoC for i18next.
<h2 ng-i18next="[i18next]({ defaultValue: 'Searching' })KIBANA-DISCOVER-SEARCHING"></h2>
<h2>
<Trans i18nKey="KIBANA-DISCOVER-SEARCHING" />
Searching
</Trans>
</h2>
{
"KIBANA-DISCOVER-SEARCHING": "Searching"
}
<input
type="text"
ng-i18next="[placeholder:i18next]({ defaultValue: 'Search...' })KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER"
>
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>
);
<input
type="text"
ng-i18next="[placeholder:i18next]({
defaultValue: '\{\{TITLE\}\} search',
TITLE: {{ service.title }}
})KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER"
>
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>
);
<span
ng-i18next="[i18next]({
count: {{ hits }},
})KIBANA-DISCOVER-HITS"
></span>
<Trans
i18nKey="KIBANA-DISCOVER-HITS"
count={hits}
/>
{
"KIBANA-DISCOVER-HITS": "{{count}} hit",
"KIBANA-DISCOVER-HITS_plural": "{{count}} hits",
}
<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>
<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>
{
"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.
@azasypkin , this is great. Are we still following the design described #6515. Would love to collaborate again
@azasypkin what @shikhasriva said, great to see this starting up again.
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 :slightly_smiling_face:
Hey @maksim-tolo, just few questions regarding your PoC's:
<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)?
<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?
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.
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.
<h2
i18n="KIBANA-DISCOVER-SEARCHING"
default-message="Searching"
></h2>
<h2>
<I18n
path="KIBANA-DISCOVER-SEARCHING"
defaultMessage="Searching"
/>
</h2>
{
"KIBANA-DISCOVER-SEARCHING": "Searching"
}
<input
type="text"
placeholder="{{ 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: { defaultMessage: 'Search...' } }}"
>
<I18n>
{translate => (
<input
type="text"
placeholder={translate({
path: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
defaultMessage: 'Search...',
})}
/>
)}
</I18n>
<input
type="text"
placeholder="{{ 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
vars: { TITLE: service.title },
defaultMessage: '{TITLE} search',
} }}"
>
<I18n>
{translate => (
<input
type="text"
placeholder={translate({
path: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
vars: { TITLE: service.title },
defaultMessage: '{TITLE} search',
})}
/>
)}
</I18n>
<span
i18n="KIBANA-DISCOVER-HITS"
vars="{HITS: hits}"
default-message="{HITS, plural, one {# hit} other {# hits}}"
></span>
<I18n
path="KIBANA-DISCOVER-HITS"
vars={{ HITS: hits }}
defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>
{
"KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}
<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>
<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."
/>
{
"KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {SIZE} documents matching your search, refine your search to see others."
}
Supported parameters are short
, default
, long
, or full
.
<span
i18n="KIBANA-TODAY"
vars="{ DATE: Date.now() }"
default-message="Today is {DATE, date}"
></span>
<I18n
path="KIBANA-TODAY"
vars={{ DATE: Date.now() }}
defaultMessage="Today is {DATE, date}"
/>
{
"KIBANA-TODAY": "Today is {DATE, date}"
}
// 'Today is Apr 10, 2018'
Represent a duration in seconds as a string.
<span
i18n="KIBANA-SINCE"
vars="{ D: 123 }"
default-message="It has been {D, duration}"
></span>
<I18n
path="KIBANA-SINCE"
vars={{ D: 123 }}
defaultMessage="It has been {D, duration}"
/>
{
"KIBANA-SINCE": "It has been {D, duration}"
}
// 'It has been 2:03'
Supported parameters are integer
, percent
, or currency
.
<span
i18n="KIBANA-ALMOST"
vars="{ N: 3.14 }"
default-message="{N} is almost {N, number, integer}"
></span>
<I18n
path="KIBANA-ALMOST"
vars={{ N: 3.14 }}
defaultMessage="{N} is almost {N, number, integer}"
/>
{
"KIBANA-ALMOST": "{N} is almost {N, number, integer}"
}
// '3.14 is almost 3'
Supported parameters are short
, default
, long
, or full
.
<span
i18n="KIBANA-NOW"
vars="{ T: Date.now() }"
default-message="The time is now {T, time}"
></span>
<I18n
path="KIBANA-NOW"
vars={{ T: Date.now() }}
defaultMessage="The time is now {T, time}"
/>
{
"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:
Cons:
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>
);
}
<h2 data-l10n-id="L10N.SEARCH">Search...</h2>
<Localized id="L10N.SEARCH">
<h2>Search...</h2>
</Localized>
{
'en-US': `
L10N.SEARCH = Search...
`
}
<input data-l10n-id="L10N.SEARCH" type="text" placeholder="Search..." title="Title..." />
<span data-l10n-id="L10N.SOMETHING" title="Title...">Something...</span>
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>
{
'en-US': `
L10N.SEARCH =
.placeholder = Search...
.title = Title...
L10N.SOMETHING = Something...
.title = Title...
`
}
<div data-l10n-id="L10N.TEXT_WITH_PARAM" data-l10n-args='{"param": "some param"}'
title="Title with $param">
Text with $param
</div>
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>
{
'en-US': `
L10N.TEXT_WITH_PARAM = Text with {$param}
.title = Title with {$param}
`
}
<div data-l10n-id="L10N.TEXT_WITH_PLURALS" data-l10n-args='{"numberOfParams": "5"}'
title="Title with $numberOfParams">
Text with $numberOfParams
</div>
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>
{
'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 }.
}
`
}
<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>.
`
}
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:
Cons:
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?
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.
Another examples based on PoC for react-intl and custom AngularJS wrapper for format.js library are below.
<h2
translate="KIBANA-DISCOVER-SEARCHING"
default-message="Searching"
></h2>
<h2>
<FormattedMessage
id="KIBANA-DISCOVER-SEARCHING"
defaultMessage="Searching"
/>
</h2>
{
"KIBANA-DISCOVER-SEARCHING": "Searching"
}
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: { defaultMessage: 'Search...' }}}"
>
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);
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | translate: {
values: { TITLE: service.title },
defaultMessage: '{TITLE} search'
} }}"
>
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);
<span
translate="KIBANA-DISCOVER-HITS"
translate-values="{HITS: hits}"
default-message="{HITS, plural, one {# hit} other {# hits}}"
></span>
<FormattedMessage
id="KIBANA-DISCOVER-HITS"
values={{ HITS: hits }}
defaultMessage="{HITS, plural, one {# hit} other {# hits}}"
/>
{
"KIBANA-DISCOVER-HITS": "{HITS, plural, one {# hit} other {# hits}}"
}
<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>
<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."
/>
{
"KIBANA-DISCOVER-REFINE_SEARCH": "These are the first {SIZE} documents matching your search, refine your search to see others."
}
Supported parameters are short
, medium
, long
, or full
.
<span
translate="KIBANA-TODAY"
translate-values="{ DATE: Date.now() }"
default-message="Today is {DATE, date, medium}"
></span>
<FormattedMessage
id="KIBANA-TODAY"
values={{ DATE: Date.now() }}
defaultMessage="Today is {DATE, date, medium}"
/>
{
"KIBANA-TODAY": "Today is {DATE, date, medium}"
}
// 'Today is Apr 20, 2018'
Supported parameters are percent
, or currency
.
<span
translate="KIBANA-PERCENT"
translate-values="{ P: 0.15 }"
default-message="{P, number, percent}"
></span>
<I18n
id="KIBANA-PERCENT"
values={{ P: 0.15 }}
defaultMessage="{P, number, percent}"
/>
{
"KIBANA-PERCENT": "{P, number, percent}"
}
// '15%'
Supported parameters are short
, medium
, long
, or full
.
<span
translate="KIBANA-NOW"
translate-values="{ T: Date.now() }"
default-message="The time is now {T, time, medium}"
></span>
<I18n
id="KIBANA-NOW"
values={{ T: Date.now() }}
defaultMessage="The time is now {T, time, medium}"
/>
{
"KIBANA-NOW": "The time is now {T, time, medium}"
}
// 'The time is now 4:00:00 PM'
Pros:
Cons:
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). |
Wrote a blog about the POC. https://www.elastic.co/blog/keeping-up-with-kibana-2018-04-16
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?
@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.
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)
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.
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
.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.
I hope spanish translation will be included.
I hope spanish translation will be included.
Not at the initial release, but at some point it will definitely be included.
@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
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.
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?
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:
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
References
History
References
sectionTechnical-level explanation
detailsTechnical-level explanation
Guide-level explanation