Description
openedon Dec 5, 2022
For some of Kibana's long term goal, we will need to support serving multiple locales on a same Kibana instance (depending on some info from the current user / context, preferred locale, space, or other...).
Why doing this now?
- We know we will need to for our long term goals
- Low hanging fruit without that much required work from the Core team
- Changing all the i18n calls in the whole codebase to use the new translation system is some long haul work, so we should provide the system, deprecates the current way and inform the teams as soon as possible so that they can prepare.
Necessary changes to Core's API
The needs are different on the browser and on the server
On the browser
This is the easy part of the feature.
On the browser, we just want the system to be able to load a different locale depending on some kind of context. We're not planning on support dynamic locale switch (without full page reload).
So the only required changes are:
- to adapt the
i18n
service from Core to load all the available locale (as opposed to only load the locale specified by the configuration file as it is done currently) - to adapt the
/translations/{locale}.json
route to accept serving any loaded locale (as opposed to returning a 404 if the requested locale is not the one specified in the config file) - to expose a 'scoped' i18n from core's i18n service (to mimic the way translation will be done on the server and to avoid calling
import i18n; i18n.translate
directly)
On the server
As you can guess, this will be the hard part.
The idea will simply be to extend Core's i18n APIs:
1. Add new APIs to the I18nService
to be able to:
- retrieve the locale bound to a request
- initially the underlying will always return the locale from the config, but we will then be able to change it whenever it will be necessary
- create a 'translator' (scoped i18n service) bound to a given locale
The contracts could look like:
type ScopedTranslateArgument = Omit<TranslateArguments, 'locale'>;
/** An instance of a translator scoped to a specific locale. */
export interface ScopedTranslator {
/** The locale the scoped translator is bound to */
readonly locale: string;
/** Translate message to the bound locale */
translate(id: string, options: ScopedTranslateArgument): string;
}
export interface I18nServiceSetup {
/** @deprecated use `getDefaultLocale` instead */
getLocale(): string;
getDefaultLocale(): string;
getTranslationFiles(): string[];
getScopedTranslator(locale: string): ScopedTranslator;
}
export interface I18nServiceStart {
getLocaleForRequest(request: KibanaRequest): string;
getScopedTranslator(locale: string): ScopedTranslator;
}
2. Introduce a i18n
request handler context
So that API consumers can easily access the scoped i18n service from the request handler, as it's done for other scoped services
e.g
handler(ctx, req, res) {
const i18n = ctx.i18n.translator;
i18n.translate('my.key', { defaultValue: '...' })
}
3. deprecate direct usages of i18n.translate
All translations should be performed using 'translators' (name open to discussion) retrieved via the new Core i18n API, as these will be the only way to know that all translations are properly scoped.
So we will deprecate i18n.translate
(the translate
function exposed from @kbn/i18n
) and inform/document of it's future removal.
4. adapt the i18n scripts
The i18n check scripts are all based on JS (and not TS) ast, and are (basically) checking for references to i18n.translate
to extract/check consistency from. We will need to adapt the parsing to take the new intermediary services into account.
For instance, we need to detect i18n translations at least for scenarios like:
handler(ctx, req, res) {
const translator = ctx.i18n.translator;
translator.translate('my.key', { defaultValue: '...' })
}
handler(ctx, req, res) {
const translator = ctx.i18n.translator;
ctx.i18n.translate('my.key', { defaultValue: '...' })
}
function doSomeTranslate(core: CoreStart, locale: string) {
const translator = core.i18n.getTranslator(locale);
return translator.translate('my.key');
}
Plus, we also need to make sure that the proxy translator (calling i18n.translate
with a variable instead of a constant string) is added to the list of exclusions.
Related PRs:
- I18n/change locale from browser #44606 (old attempt from @Bamieh to bring multi-locale support to the browser)
- [18n] expose scoped translator from route handler context #146856 (draft from @pgayvallet to expose a scope i18n service from request context holder)
- Multitenancy POC #146501 (multitenancy POC where the i18n service and route were modified to support distinct locale per browser)
Impacts on other teams
Overall that will be some massive (not hard, but long and sometimes tedious) chore work, as all calls to i18n.translate
will have to be adapted (which is why we should ready the framework as soon as possible)
i18n.translate
will be deprecated and flagged to removal. And all calls to the translation system will have to be performed using scoped services from Core instead of from the static i18n
import.
This means that statically declaring translations will no longer be possible:
import i18n from '@kbn/i18n'
const myStaticLabel = i18n.translate('my.key'); // no longer possible
const getSomeData = () => {
return {
title: i18n.translate('my.key') // no longer possible either
}
}
This will have to be replaced by something like
const myStaticLabelFn = (translator: ScopedTranslator) => translator.translate('my.key');
const getSomeData = (translator: ScopedTranslator) => {
return {
title: translator.translate('my.key') // no longer possible either
}
}
Note that, ideally, we would find a way to decouple the i18n message from the i18n call, with something like
const myMessage: I18nMessage = {
id: 'my.key',
defaultValue: 'the cat is {state}',
}
const getSomeData = (translator: ScopedTranslator) => {
return {
title: translator.translate(myMessage, { state: 'alive' })
}
}
But I guess this would significantly increase the amount of work on the i18n script / atm parsing tools, so I'm not sure on the feasibility @Bamieh?