Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [CoreSetup](./kibana-plugin-core-server.coresetup.md) &gt; [i18n](./kibana-plugin-core-server.coresetup.i18n.md)

## CoreSetup.i18n property

[I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md)

<b>Signature:</b>

```typescript
i18n: I18nServiceSetup;
```
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
| [elasticsearch](./kibana-plugin-core-server.coresetup.elasticsearch.md) | <code>ElasticsearchServiceSetup</code> | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) |
| [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | <code>StartServicesAccessor&lt;TPluginsStart, TStart&gt;</code> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) |
| [http](./kibana-plugin-core-server.coresetup.http.md) | <code>HttpServiceSetup &amp; {</code><br/><code> resources: HttpResources;</code><br/><code> }</code> | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | <code>I18nServiceSetup</code> | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) |
| [logging](./kibana-plugin-core-server.coresetup.logging.md) | <code>LoggingServiceSetup</code> | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) |
| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | <code>MetricsServiceSetup</code> | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) |
| [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) &gt; [getLocale](./kibana-plugin-core-server.i18nservicesetup.getlocale.md)

## I18nServiceSetup.getLocale() method

Return the locale currently in use.

<b>Signature:</b>

```typescript
getLocale(): string;
```
<b>Returns:</b>

`string`

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) &gt; [getTranslationFiles](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md)

## I18nServiceSetup.getTranslationFiles() method

Return the list of translation files currently in use.

<b>Signature:</b>

```typescript
getTranslationFiles(): string[];
```
<b>Returns:</b>

`string[]`

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md)

## I18nServiceSetup interface


<b>Signature:</b>

```typescript
export interface I18nServiceSetup
```

## Methods

| Method | Description |
| --- | --- |
| [getLocale()](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) | Return the locale currently in use. |
| [getTranslationFiles()](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) | Return the list of translation files currently in use. |

1 change: 1 addition & 0 deletions docs/development/core/server/kibana-plugin-core-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ The plugin integrates with the core system via lifecycle events: `setup`<!-- -->
| [HttpServerInfo](./kibana-plugin-core-server.httpserverinfo.md) | |
| [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | Kibana HTTP Service provides own abstraction for work with HTTP stack. Plugins don't have direct access to <code>hapi</code> server and its primitives anymore. Moreover, plugins shouldn't rely on the fact that HTTP Service uses one or another library under the hood. This gives the platform flexibility to upgrade or changing our internal HTTP stack without breaking plugins. If the HTTP Service lacks functionality you need, we are happy to discuss and support your needs. |
| [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | |
| [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | |
| [IClusterClient](./kibana-plugin-core-server.iclusterclient.md) | Represents an Elasticsearch cluster API client created by the platform. It allows to call API on behalf of the internal Kibana user and the actual user that is derived from the request headers (via <code>asScoped(...)</code>). |
| [IContextContainer](./kibana-plugin-core-server.icontextcontainer.md) | An object that handles registration of context providers and configuring handlers with context. |
| [ICspConfig](./kibana-plugin-core-server.icspconfig.md) | CSP configuration for use in Kibana. |
Expand Down
3 changes: 1 addition & 2 deletions src/core/server/environment/environment_service.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@
* under the License.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';

import { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service';
import type { EnvironmentService, InternalEnvironmentServiceSetup } from './environment_service';

const createSetupContractMock = () => {
const setupContract: jest.Mocked<InternalEnvironmentServiceSetup> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@
* under the License.
*/

export const I18N_RC = '.i18nrc.json';
import Fs from 'fs';
import { promisify } from 'util';

export const readFile = promisify(Fs.readFile);
81 changes: 81 additions & 0 deletions src/core/server/i18n/get_kibana_translation_files.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { getKibanaTranslationFiles } from './get_kibana_translation_files';
import { getTranslationPaths } from './get_translation_paths';

const mockGetTranslationPaths = getTranslationPaths as jest.Mock;

jest.mock('./get_translation_paths', () => ({
getTranslationPaths: jest.fn().mockResolvedValue([]),
}));
jest.mock('../utils', () => ({
fromRoot: jest.fn().mockImplementation((path: string) => path),
}));

const locale = 'en';

describe('getKibanaTranslationPaths', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('calls getTranslationPaths against kibana root and kibana-extra', async () => {
await getKibanaTranslationFiles(locale, []);

expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2);

expect(mockGetTranslationPaths).toHaveBeenCalledWith({
cwd: '.',
nested: true,
});

expect(mockGetTranslationPaths).toHaveBeenCalledWith({
cwd: '../kibana-extra',
nested: true,
});
});

it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => {
const pluginPaths = ['/path/to/pluginA', '/path/to/pluginB'];

await getKibanaTranslationFiles(locale, pluginPaths);

expect(mockGetTranslationPaths).toHaveBeenCalledTimes(2 + pluginPaths.length);

pluginPaths.forEach((pluginPath) => {
expect(mockGetTranslationPaths).toHaveBeenCalledWith({
cwd: pluginPath,
nested: false,
});
});
});

it('only return files for specified locale', async () => {
mockGetTranslationPaths.mockResolvedValueOnce(['/root/en.json', '/root/fr.json']);
mockGetTranslationPaths.mockResolvedValueOnce([
'/kibana-extra/en.json',
'/kibana-extra/fr.json',
]);

const translationFiles = await getKibanaTranslationFiles('en', []);

expect(translationFiles).toEqual(['/root/en.json', '/kibana-extra/en.json']);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,27 @@
* under the License.
*/

import { KibanaConfig } from '../kbn_server';
import { fromRoot } from '../../../core/server/utils';
import { I18N_RC } from './constants';
import { basename } from 'path';
import { fromRoot } from '../utils';
import { getTranslationPaths } from './get_translation_paths';

export async function getKibanaTranslationPaths(config: Pick<KibanaConfig, 'get'>) {
return await Promise.all([
export const getKibanaTranslationFiles = async (
locale: string,
pluginPaths: string[]
): Promise<string[]> => {
const translationPaths = await Promise.all([
getTranslationPaths({
cwd: fromRoot('.'),
glob: `*/${I18N_RC}`,
nested: true,
}),
Comment on lines 29 to 32
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK, this is only used to get the x-pack/.i18nrc.json file. However to avoid any risk / breaking changes (User could have custom i18nrc files in another non-plugin folder eventually), I kept it as it was done in legacy.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgayvallet only in x-pack. I believe it is safe to change it as long as x-pack and kibana's root dir are discoverable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it mean that it traverses all the node_modules and other unnecessary folders?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. */something globbing is only scanning a single level (as opposed to **/something)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I didn't check implementation. but nested: true traversing on a single level in-depth is confusing 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, you're right. I kinda wanted to rename it to includeSubFolders, but is was way longer, and not really more explicit that we are only scanning one level down. No issue renaming if you have a better idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need something similar to maxScanDepth in discovery to support migration to Domain-based folder structure. But I'm okay to keep it as is for now.

...(config.get('plugins.paths') as string[]).map((cwd) =>
getTranslationPaths({ cwd, glob: I18N_RC })
),
...(config.get('plugins.scanDirs') as string[]).map((cwd) =>
getTranslationPaths({ cwd, glob: `*/${I18N_RC}` })
),
...pluginPaths.map((pluginPath) => getTranslationPaths({ cwd: pluginPath, nested: false })),
Comment on lines -31 to +33
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a slight change of behavior here:

Legacy was just reading the plugin config and scanning the scanDirs (1 lvl deep) and paths (root lvl) for i18n rc files.

I chose to change that by retrieving the list of enabled plugins from the plugin service, and check if there is a i18n file at the root level of each plugin instead. This is basically the same, except that we are not loading translation files for disabled plugins (note that this only concerns 3rd parties plugin, as kibana translations are all defined from a single x-pack/.i18nrc.json file anyway). Overall, I think it should not really impact 3rd parties, and not loading translations for disabled plugins does make sense imho. (This is also way better at the code level in term of responsibilities per service. I was really awkward to load the plugins config from the i18n service)

getTranslationPaths({
cwd: fromRoot('../kibana-extra'),
glob: `*/${I18N_RC}`,
nested: true,
}),
Comment on lines 34 to 37
Copy link
Contributor Author

@pgayvallet pgayvallet Nov 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we really still using kibana-extra? Kept that, but not sure if it is really necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as we still support running plugins from that path then we should keep it. If not there is no need to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still search for a plugin in kibana-extra

resolve(rootDir, '..', 'kibana-extra'),
and kbn-pm supports it as well https://github.com/elastic/kibana/blob/0068c87d6fe7713351acbf42080faa09ff73ef15/packages/kbn-pm/README.md
But I haven't checked whether it works

]);
}

return ([] as string[])
.concat(...translationPaths)
.filter((translationPath) => basename(translationPath, '.json') === locale);
};
26 changes: 26 additions & 0 deletions src/core/server/i18n/get_translation_paths.test.mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export const globbyMock = jest.fn();
jest.doMock('globby', () => globbyMock);

export const readFileMock = jest.fn();
jest.doMock('./fs', () => ({
readFile: readFileMock,
}));
90 changes: 90 additions & 0 deletions src/core/server/i18n/get_translation_paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { resolve, join } from 'path';
import { globbyMock, readFileMock } from './get_translation_paths.test.mocks';
import { getTranslationPaths } from './get_translation_paths';

describe('getTranslationPaths', () => {
beforeEach(() => {
globbyMock.mockReset();
readFileMock.mockReset();

globbyMock.mockResolvedValue([]);
readFileMock.mockResolvedValue('{}');
});

it('calls `globby` with the correct parameters', async () => {
getTranslationPaths({ cwd: '/some/cwd', nested: false });

expect(globbyMock).toHaveBeenCalledTimes(1);
expect(globbyMock).toHaveBeenCalledWith('.i18nrc.json', { cwd: '/some/cwd' });

globbyMock.mockClear();

await getTranslationPaths({ cwd: '/other/cwd', nested: true });

expect(globbyMock).toHaveBeenCalledTimes(1);
expect(globbyMock).toHaveBeenCalledWith('*/.i18nrc.json', { cwd: '/other/cwd' });
});

it('calls `readFile` for each entry returned by `globby`', async () => {
const entries = [join('pathA', '.i18nrc.json'), join('pathB', '.i18nrc.json')];
globbyMock.mockResolvedValue(entries);

const cwd = '/kibana-extra';

await getTranslationPaths({ cwd, nested: true });

expect(readFileMock).toHaveBeenCalledTimes(2);

expect(readFileMock).toHaveBeenNthCalledWith(1, resolve(cwd, entries[0]), 'utf8');
expect(readFileMock).toHaveBeenNthCalledWith(2, resolve(cwd, entries[1]), 'utf8');
});

it('returns the absolute path to the translation files', async () => {
const entries = ['.i18nrc.json'];
globbyMock.mockResolvedValue(entries);

const i18nFileContent = {
translations: ['translations/en.json', 'translations/fr.json'],
};
readFileMock.mockResolvedValue(JSON.stringify(i18nFileContent));

const cwd = '/cwd';

const translationPaths = await getTranslationPaths({ cwd, nested: true });

expect(translationPaths).toEqual([
resolve(cwd, 'translations/en.json'),
resolve(cwd, 'translations/fr.json'),
]);
});

it('throws if i18nrc parsing fails', async () => {
globbyMock.mockResolvedValue(['.i18nrc.json']);
readFileMock.mockRejectedValue(new Error('error parsing file'));

await expect(
getTranslationPaths({ cwd: '/cwd', nested: true })
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Failed to parse .i18nrc.json file at /cwd/.i18nrc.json"`
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +17,26 @@
* under the License.
*/

import { promisify } from 'util';
import { readFile } from 'fs';
import { resolve, dirname } from 'path';
import globby from 'globby';

const readFileAsync = promisify(readFile);
import { readFile } from './fs';

interface I18NRCFileStructure {
translations?: string[];
}

export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: string }) {
const I18N_RC = '.i18nrc.json';

export async function getTranslationPaths({ cwd, nested }: { cwd: string; nested: boolean }) {
const glob = nested ? `*/${I18N_RC}` : I18N_RC;
const entries = await globby(glob, { cwd });
const translationPaths: string[] = [];

for (const entry of entries) {
const entryFullPath = resolve(cwd, entry);
const pluginBasePath = dirname(entryFullPath);
try {
const content = await readFileAsync(entryFullPath, 'utf8');
const content = await readFile(entryFullPath, 'utf8');
const { translations } = JSON.parse(content) as I18NRCFileStructure;
if (translations && translations.length) {
translations.forEach((translation) => {
Expand Down
Loading