diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md new file mode 100644 index 0000000000000..cac878c1e4449 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.i18n.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [i18n](./kibana-plugin-core-server.coresetup.i18n.md) + +## CoreSetup.i18n property + +[I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +Signature: + +```typescript +i18n: I18nServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index 7a733cc34dace..1171dbad570ce 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -21,6 +21,7 @@ export interface CoreSetupElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-core-server.elasticsearchservicesetup.md) | | [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | StartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | +| [i18n](./kibana-plugin-core-server.coresetup.i18n.md) | I18nServiceSetup | [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | | [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md new file mode 100644 index 0000000000000..2fe8e564e7ce5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.getlocale.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getLocale](./kibana-plugin-core-server.i18nservicesetup.getlocale.md) + +## I18nServiceSetup.getLocale() method + +Return the locale currently in use. + +Signature: + +```typescript +getLocale(): string; +``` +Returns: + +`string` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md new file mode 100644 index 0000000000000..81caed287454e --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) > [getTranslationFiles](./kibana-plugin-core-server.i18nservicesetup.gettranslationfiles.md) + +## I18nServiceSetup.getTranslationFiles() method + +Return the absolute paths to translation files currently in use. + +Signature: + +```typescript +getTranslationFiles(): string[]; +``` +Returns: + +`string[]` + diff --git a/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md new file mode 100644 index 0000000000000..f68b7877953e7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.i18nservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [I18nServiceSetup](./kibana-plugin-core-server.i18nservicesetup.md) + +## I18nServiceSetup interface + + +Signature: + +```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 absolute paths to translation files currently in use. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 68f5e72915556..adbb2460dc80a 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -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 hapi 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 asScoped(...)). | | [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. | diff --git a/src/core/server/environment/environment_service.mock.ts b/src/core/server/environment/environment_service.mock.ts index a956e369ba4a7..3c579b0f68b00 100644 --- a/src/core/server/environment/environment_service.mock.ts +++ b/src/core/server/environment/environment_service.mock.ts @@ -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 = { diff --git a/src/legacy/server/i18n/constants.ts b/src/core/server/i18n/fs.ts similarity index 88% rename from src/legacy/server/i18n/constants.ts rename to src/core/server/i18n/fs.ts index a7a410dbcb5b3..23d729504f81c 100644 --- a/src/legacy/server/i18n/constants.ts +++ b/src/core/server/i18n/fs.ts @@ -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); diff --git a/src/core/server/i18n/get_kibana_translation_files.test.ts b/src/core/server/i18n/get_kibana_translation_files.test.ts new file mode 100644 index 0000000000000..737d8ed8bc4e2 --- /dev/null +++ b/src/core/server/i18n/get_kibana_translation_files.test.ts @@ -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']); + }); +}); diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.ts b/src/core/server/i18n/get_kibana_translation_files.ts similarity index 64% rename from src/legacy/server/i18n/get_kibana_translation_paths.ts rename to src/core/server/i18n/get_kibana_translation_files.ts index d7f77d3185ba4..dacb6a1e16a5c 100644 --- a/src/legacy/server/i18n/get_kibana_translation_paths.ts +++ b/src/core/server/i18n/get_kibana_translation_files.ts @@ -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) { - return await Promise.all([ +export const getKibanaTranslationFiles = async ( + locale: string, + pluginPaths: string[] +): Promise => { + const translationPaths = await Promise.all([ getTranslationPaths({ cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, + nested: true, }), - ...(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 })), getTranslationPaths({ cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, + nested: true, }), ]); -} + + return ([] as string[]) + .concat(...translationPaths) + .filter((translationPath) => basename(translationPath, '.json') === locale); +}; diff --git a/src/core/server/i18n/get_translation_paths.test.mocks.ts b/src/core/server/i18n/get_translation_paths.test.mocks.ts new file mode 100644 index 0000000000000..f3b688062523c --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.mocks.ts @@ -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, +})); diff --git a/src/core/server/i18n/get_translation_paths.test.ts b/src/core/server/i18n/get_translation_paths.test.ts new file mode 100644 index 0000000000000..c6af59da07fb5 --- /dev/null +++ b/src/core/server/i18n/get_translation_paths.test.ts @@ -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"` + ); + }); +}); diff --git a/src/legacy/server/i18n/get_translation_paths.ts b/src/core/server/i18n/get_translation_paths.ts similarity index 85% rename from src/legacy/server/i18n/get_translation_paths.ts rename to src/core/server/i18n/get_translation_paths.ts index a2a292e2278be..41d9dc4722e37 100644 --- a/src/legacy/server/i18n/get_translation_paths.ts +++ b/src/core/server/i18n/get_translation_paths.ts @@ -17,18 +17,18 @@ * 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[] = []; @@ -36,7 +36,7 @@ export async function getTranslationPaths({ cwd, glob }: { cwd: string; glob: st 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) => { diff --git a/src/core/server/i18n/i18n_config.ts b/src/core/server/i18n/i18n_config.ts new file mode 100644 index 0000000000000..f181c52dc4b06 --- /dev/null +++ b/src/core/server/i18n/i18n_config.ts @@ -0,0 +1,29 @@ +/* + * 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 { schema, TypeOf } from '@kbn/config-schema'; + +export const config = { + path: 'i18n', + schema: schema.object({ + locale: schema.string({ defaultValue: 'en' }), + }), +}; + +export type I18nConfigType = TypeOf; diff --git a/src/core/server/i18n/i18n_service.mock.ts b/src/core/server/i18n/i18n_service.mock.ts new file mode 100644 index 0000000000000..679751aefbf27 --- /dev/null +++ b/src/core/server/i18n/i18n_service.mock.ts @@ -0,0 +1,50 @@ +/* + * 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 { PublicMethodsOf } from '@kbn/utility-types'; +import type { I18nServiceSetup, I18nService } from './i18n_service'; + +const createSetupContractMock = () => { + const mock: jest.Mocked = { + getLocale: jest.fn(), + getTranslationFiles: jest.fn(), + }; + + mock.getLocale.mockReturnValue('en'); + mock.getTranslationFiles.mockReturnValue([]); + + return mock; +}; + +type I18nServiceContract = PublicMethodsOf; + +const createMock = () => { + const mock: jest.Mocked = { + setup: jest.fn(), + }; + + mock.setup.mockResolvedValue(createSetupContractMock()); + + return mock; +}; + +export const i18nServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, +}; diff --git a/src/core/server/i18n/i18n_service.test.mocks.ts b/src/core/server/i18n/i18n_service.test.mocks.ts new file mode 100644 index 0000000000000..23f97a1404fff --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.mocks.ts @@ -0,0 +1,28 @@ +/* + * 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 getKibanaTranslationFilesMock = jest.fn(); +jest.doMock('./get_kibana_translation_files', () => ({ + getKibanaTranslationFiles: getKibanaTranslationFilesMock, +})); + +export const initTranslationsMock = jest.fn(); +jest.doMock('./init_translations', () => ({ + initTranslations: initTranslationsMock, +})); diff --git a/src/core/server/i18n/i18n_service.test.ts b/src/core/server/i18n/i18n_service.test.ts new file mode 100644 index 0000000000000..87de39a92ab26 --- /dev/null +++ b/src/core/server/i18n/i18n_service.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { getKibanaTranslationFilesMock, initTranslationsMock } from './i18n_service.test.mocks'; + +import { BehaviorSubject } from 'rxjs'; +import { I18nService } from './i18n_service'; + +import { configServiceMock } from '../config/mocks'; +import { mockCoreContext } from '../core_context.mock'; + +const getConfigService = (locale = 'en') => { + const configService = configServiceMock.create(); + configService.atPath.mockImplementation((path) => { + if (path === 'i18n') { + return new BehaviorSubject({ + locale, + }); + } + return new BehaviorSubject({}); + }); + return configService; +}; + +describe('I18nService', () => { + let service: I18nService; + let configService: ReturnType; + + beforeEach(() => { + jest.clearAllMocks(); + configService = getConfigService(); + + const coreContext = mockCoreContext.create({ configService }); + service = new I18nService(coreContext); + }); + + describe('#setup', () => { + it('calls `getKibanaTranslationFiles` with the correct parameters', async () => { + getKibanaTranslationFilesMock.mockResolvedValue([]); + + const pluginPaths = ['/pathA', '/pathB']; + await service.setup({ pluginPaths }); + + expect(getKibanaTranslationFilesMock).toHaveBeenCalledTimes(1); + expect(getKibanaTranslationFilesMock).toHaveBeenCalledWith('en', pluginPaths); + }); + + it('calls `initTranslations` with the correct parameters', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + await service.setup({ pluginPaths: [] }); + + expect(initTranslationsMock).toHaveBeenCalledTimes(1); + expect(initTranslationsMock).toHaveBeenCalledWith('en', translationFiles); + }); + + it('returns accessors for locale and translation files', async () => { + const translationFiles = ['/path/to/file', 'path/to/another/file']; + getKibanaTranslationFilesMock.mockResolvedValue(translationFiles); + + const { getLocale, getTranslationFiles } = await service.setup({ pluginPaths: [] }); + + expect(getLocale()).toEqual('en'); + expect(getTranslationFiles()).toEqual(translationFiles); + }); + }); +}); diff --git a/src/core/server/i18n/i18n_service.ts b/src/core/server/i18n/i18n_service.ts new file mode 100644 index 0000000000000..fd32dd7fdd6ef --- /dev/null +++ b/src/core/server/i18n/i18n_service.ts @@ -0,0 +1,75 @@ +/* + * 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 { take } from 'rxjs/operators'; +import { Logger } from '../logging'; +import { IConfigService } from '../config'; +import { CoreContext } from '../core_context'; +import { config as i18nConfigDef, I18nConfigType } from './i18n_config'; +import { getKibanaTranslationFiles } from './get_kibana_translation_files'; +import { initTranslations } from './init_translations'; + +interface SetupDeps { + pluginPaths: string[]; +} + +/** + * @public + */ +export interface I18nServiceSetup { + /** + * Return the locale currently in use. + */ + getLocale(): string; + + /** + * Return the absolute paths to translation files currently in use. + */ + getTranslationFiles(): string[]; +} + +export class I18nService { + private readonly log: Logger; + private readonly configService: IConfigService; + + constructor(coreContext: CoreContext) { + this.log = coreContext.logger.get('i18n'); + this.configService = coreContext.configService; + } + + public async setup({ pluginPaths }: SetupDeps): Promise { + const i18nConfig = await this.configService + .atPath(i18nConfigDef.path) + .pipe(take(1)) + .toPromise(); + + const locale = i18nConfig.locale; + this.log.debug(`Using locale: ${locale}`); + + const translationFiles = await getKibanaTranslationFiles(locale, pluginPaths); + + this.log.debug(`Using translation files: [${translationFiles.join(', ')}]`); + await initTranslations(locale, translationFiles); + + return { + getLocale: () => locale, + getTranslationFiles: () => translationFiles, + }; + } +} diff --git a/src/legacy/server/i18n/index.ts b/src/core/server/i18n/index.ts similarity index 86% rename from src/legacy/server/i18n/index.ts rename to src/core/server/i18n/index.ts index a7ef49f44532c..2db3fdeb405d9 100644 --- a/src/legacy/server/i18n/index.ts +++ b/src/core/server/i18n/index.ts @@ -17,4 +17,5 @@ * under the License. */ -export { i18nMixin } from './i18n_mixin'; +export { config, I18nConfigType } from './i18n_config'; +export { I18nService, I18nServiceSetup } from './i18n_service'; diff --git a/src/core/server/i18n/init_translations.ts b/src/core/server/i18n/init_translations.ts new file mode 100644 index 0000000000000..94e6d41019ad2 --- /dev/null +++ b/src/core/server/i18n/init_translations.ts @@ -0,0 +1,31 @@ +/* + * 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 { i18n, i18nLoader } from '@kbn/i18n'; + +export const initTranslations = async (locale: string, translationFiles: string[]) => { + i18nLoader.registerTranslationFiles(translationFiles); + const translations = await i18nLoader.getTranslationsByLocale(locale); + i18n.init( + Object.freeze({ + locale, + ...translations, + }) + ); +}; diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 0adda4770639d..7b19c3a686757 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -64,6 +64,7 @@ import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; import { CoreUsageDataStart } from './core_usage_data'; +import { I18nServiceSetup } from './i18n'; // Because of #79265 we need to explicity import, then export these types for // scripts/telemetry_check.js to work as expected @@ -336,6 +337,8 @@ export { MetricsServiceStart, } from './metrics'; +export { I18nServiceSetup } from './i18n'; + export { AppCategory } from '../types'; export { DEFAULT_APP_CATEGORIES } from '../utils'; @@ -421,6 +424,8 @@ export interface CoreSetup = KbnServer as any; @@ -75,6 +76,7 @@ beforeEach(() => { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: { legacy: {} } as any, + i18n: i18nServiceMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), http: { ...httpServiceMock.createInternalSetupContract(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index c42771179aba2..3111c8daf7981 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -251,6 +251,7 @@ export class LegacyService implements CoreService { csp: setupDeps.core.http.csp, getServerInfo: setupDeps.core.http.getServerInfo, }, + i18n: setupDeps.core.i18n, logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 0d9e9af39317c..6faccc9c9da56 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -19,7 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { MetricsService } from './metrics_service'; +import type { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 8ca0c82219ed4..03a0ae2d6443a 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -38,6 +38,7 @@ import { metricsServiceMock } from './metrics/metrics_service.mock'; import { environmentServiceMock } from './environment/environment_service.mock'; import { statusServiceMock } from './status/status_service.mock'; import { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +import { i18nServiceMock } from './i18n/i18n_service.mock'; export { configServiceMock } from './config/mocks'; export { httpServerMock } from './http/http_server.mocks'; @@ -57,6 +58,7 @@ export { statusServiceMock } from './status/status_service.mock'; export { contextServiceMock } from './context/context_service.mock'; export { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; export { coreUsageDataServiceMock } from './core_usage_data/core_usage_data_service.mock'; +export { i18nServiceMock } from './i18n/i18n_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -136,6 +138,7 @@ function createCoreSetupMock({ context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), http: httpMock, + i18n: i18nServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createSetupContract(), uiSettings: uiSettingsMock, @@ -172,6 +175,7 @@ function createInternalCoreSetupMock() { savedObjects: savedObjectsServiceMock.createInternalSetupContract(), status: statusServiceMock.createInternalSetupContract(), environment: environmentServiceMock.createSetupContract(), + i18n: i18nServiceMock.createSetupContract(), httpResources: httpResourcesMock.createSetupContract(), rendering: renderingMock.createSetupContract(), uiSettings: uiSettingsServiceMock.createSetupContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 22e79741e854c..3b2634ddbe315 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,7 @@ export function createPluginSetupContext( csp: deps.http.csp, getServerInfo: deps.http.getServerInfo, }, + i18n: deps.i18n, logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, diff --git a/src/core/server/plugins/plugins_service.mock.ts b/src/core/server/plugins/plugins_service.mock.ts index 14d6de889dd42..3ea8badb0f450 100644 --- a/src/core/server/plugins/plugins_service.mock.ts +++ b/src/core/server/plugins/plugins_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { PluginsService, PluginsServiceSetup } from './plugins_service'; +import type { PluginsService, PluginsServiceSetup } from './plugins_service'; type PluginsServiceMock = jest.Mocked>; diff --git a/src/core/server/plugins/plugins_service.test.ts b/src/core/server/plugins/plugins_service.test.ts index 64a382e539fb0..02b82c17ed4fc 100644 --- a/src/core/server/plugins/plugins_service.test.ts +++ b/src/core/server/plugins/plugins_service.test.ts @@ -127,6 +127,7 @@ async function testSetup(options: { isDevClusterMaster?: boolean } = {}) { [mockPluginSystem] = MockPluginsSystem.mock.instances as any; mockPluginSystem.uiPlugins.mockReturnValue(new Map()); + mockPluginSystem.getPlugins.mockReturnValue([]); environmentSetup = environmentServiceMock.createSetupContract(); } @@ -469,6 +470,22 @@ describe('PluginsService', () => { deprecationProvider ); }); + + it('returns the paths of the plugins', async () => { + const pluginA = createPlugin('A', { path: '/plugin-A-path', configPath: 'pathA' }); + const pluginB = createPlugin('B', { path: '/plugin-B-path', configPath: 'pathB' }); + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([]), + }); + + mockPluginSystem.getPlugins.mockReturnValue([pluginA, pluginB]); + + const { pluginPaths } = await pluginsService.discover({ environment: environmentSetup }); + + expect(pluginPaths).toEqual(['/plugin-A-path', '/plugin-B-path']); + }); }); describe('#generateUiPluginsConfigs()', () => { @@ -633,6 +650,7 @@ describe('PluginService when isDevClusterMaster is true', () => { await expect(pluginsService.discover({ environment: environmentSetup })).resolves .toMatchInlineSnapshot(` Object { + "pluginPaths": Array [], "pluginTree": undefined, "uiPlugins": Object { "browserConfigs": Map {}, diff --git a/src/core/server/plugins/plugins_service.ts b/src/core/server/plugins/plugins_service.ts index a1062bde7765f..5967e6d5358de 100644 --- a/src/core/server/plugins/plugins_service.ts +++ b/src/core/server/plugins/plugins_service.ts @@ -118,6 +118,7 @@ export class PluginsService implements CoreService plugin.path), uiPlugins: { internal: this.uiPluginInternalInfo, public: uiPlugins, diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index ae9267ca5cf60..89a3697ebe9cd 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -92,6 +92,15 @@ test('can be setup even without plugins', async () => { expect(pluginsSetup.size).toBe(0); }); +test('getPlugins returns the list of plugins', () => { + const pluginA = createPlugin('plugin-a'); + const pluginB = createPlugin('plugin-b'); + pluginsSystem.addPlugin(pluginA); + pluginsSystem.addPlugin(pluginB); + + expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]); +}); + test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] })); pluginsSystem.addPlugin( diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index 72d2cfe158b37..23dc77b7bf673 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -42,6 +42,10 @@ export class PluginsSystem { this.plugins.set(plugin.name, plugin); } + public getPlugins() { + return [...this.plugins.values()]; + } + /** * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal diff --git a/src/core/server/rendering/__mocks__/rendering_service.ts b/src/core/server/rendering/__mocks__/rendering_service.ts index 01d084f9ae53c..00e617de82542 100644 --- a/src/core/server/rendering/__mocks__/rendering_service.ts +++ b/src/core/server/rendering/__mocks__/rendering_service.ts @@ -17,8 +17,8 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { RenderingService as Service } from '../rendering_service'; -import { InternalRenderingServiceSetup } from '../types'; +import type { RenderingService as Service } from '../rendering_service'; +import type { InternalRenderingServiceSetup } from '../types'; import { mockRenderingServiceParams } from './params'; type IRenderingService = PublicMethodsOf; diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index c56cdabf6e4cd..85dbf4b5e8c6a 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -19,8 +19,7 @@ import { BehaviorSubject } from 'rxjs'; import type { PublicMethodsOf } from '@kbn/utility-types'; - -import { +import type { SavedObjectsService, InternalSavedObjectsServiceSetup, InternalSavedObjectsServiceStart, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 52500673f7f31..88d7fecbcf502 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -480,6 +480,8 @@ export interface CoreSetup HttpServerInfo; } +// @public (undocumented) +export interface I18nServiceSetup { + getLocale(): string; + getTranslationFiles(): string[]; +} + // @public export type IBasePath = Pick; diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index fe299c6d11675..d2d6990dc5451 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -100,3 +100,9 @@ export const mockLoggingService = loggingServiceMock.create(); jest.doMock('./logging/logging_service', () => ({ LoggingService: jest.fn(() => mockLoggingService), })); + +import { i18nServiceMock } from './i18n/i18n_service.mock'; +export const mockI18nService = i18nServiceMock.create(); +jest.doMock('./i18n/i18n_service', () => ({ + I18nService: jest.fn(() => mockI18nService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 78703ceeec7ae..0c7ebbcb527ec 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -31,6 +31,7 @@ import { mockMetricsService, mockStatusService, mockLoggingService, + mockI18nService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -49,6 +50,7 @@ beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -70,6 +72,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -83,6 +86,7 @@ test('sets up services on "setup"', async () => { expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); expect(mockStatusService.setup).toHaveBeenCalledTimes(1); expect(mockLoggingService.setup).toHaveBeenCalledTimes(1); + expect(mockI18nService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -96,6 +100,7 @@ test('injects legacy dependency to context#setup()', async () => { ]); mockPluginsService.discover.mockResolvedValue({ pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, + pluginPaths: [], uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); @@ -185,6 +190,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -207,6 +213,7 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockMetricsService.setup).not.toHaveBeenCalled(); expect(mockStatusService.setup).not.toHaveBeenCalled(); expect(mockLoggingService.setup).not.toHaveBeenCalled(); + expect(mockI18nService.setup).not.toHaveBeenCalled(); }); test(`doesn't validate config if env.isDevClusterMaster is true`, async () => { diff --git a/src/core/server/server.ts b/src/core/server/server.ts index eaa03d11cab98..55ed88e55a9f5 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -16,11 +16,13 @@ * specific language governing permissions and limitations * under the License. */ + import apm from 'elastic-apm-node'; import { config as pathConfig } from '@kbn/utils'; import { mapToObject } from '@kbn/std'; import { ConfigService, Env, RawConfigurationProvider, coreDeprecationProvider } from './config'; import { CoreApp } from './core_app'; +import { I18nService } from './i18n'; import { ElasticsearchService } from './elasticsearch'; import { HttpService } from './http'; import { HttpResourcesService } from './http_resources'; @@ -29,10 +31,11 @@ import { LegacyService, ensureValidConfiguration } from './legacy'; import { Logger, LoggerFactory, LoggingService, ILoggingSystem } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; -import { SavedObjectsService } from '../server/saved_objects'; +import { SavedObjectsService } from './saved_objects'; import { MetricsService, opsConfig } from './metrics'; import { CapabilitiesService } from './capabilities'; import { EnvironmentService, config as pidConfig } from './environment'; +// do not try to shorten the import to `./status`, it will break server test mocking import { StatusService } from './status/status_service'; import { config as cspConfig } from './csp'; @@ -44,6 +47,7 @@ import { config as kibanaConfig } from './kibana_config'; import { savedObjectsConfig, savedObjectsMigrationConfig } from './saved_objects'; import { config as uiSettingsConfig } from './ui_settings'; import { config as statusConfig } from './status'; +import { config as i18nConfig } from './i18n'; import { ContextService } from './context'; import { RequestHandlerContext } from '.'; import { InternalCoreSetup, InternalCoreStart, ServiceConfigDescriptor } from './internal_types'; @@ -72,6 +76,7 @@ export class Server { private readonly logging: LoggingService; private readonly coreApp: CoreApp; private readonly coreUsageData: CoreUsageDataService; + private readonly i18n: I18nService; #pluginsInitialized?: boolean; private coreStart?: InternalCoreStart; @@ -103,6 +108,7 @@ export class Server { this.httpResources = new HttpResourcesService(core); this.logging = new LoggingService(core); this.coreUsageData = new CoreUsageDataService(core); + this.i18n = new I18nService(core); } public async setup() { @@ -112,7 +118,7 @@ export class Server { const environmentSetup = await this.environment.setup(); // Discover any plugins before continuing. This allows other systems to utilize the plugin dependency graph. - const { pluginTree, uiPlugins } = await this.plugins.discover({ + const { pluginTree, pluginPaths, uiPlugins } = await this.plugins.discover({ environment: environmentSetup, }); const legacyConfigSetup = await this.legacy.setupLegacyConfig(); @@ -125,6 +131,9 @@ export class Server { await ensureValidConfiguration(this.configService, legacyConfigSetup); } + // setup i18n prior to any other service, to have translations ready + const i18nServiceSetup = await this.i18n.setup({ pluginPaths }); + const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: // 1) Can access context from any KP plugin @@ -190,6 +199,7 @@ export class Server { elasticsearch: elasticsearchServiceSetup, environment: environmentSetup, http: httpSetup, + i18n: i18nServiceSetup, savedObjects: savedObjectsSetup, status: statusSetup, uiSettings: uiSettingsSetup, @@ -302,6 +312,7 @@ export class Server { opsConfig, statusConfig, pidConfig, + i18nConfig, ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 0ee2d03229a78..42892ddbb490c 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -17,7 +17,7 @@ * under the License. */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { StatusService } from './status_service'; +import type { StatusService } from './status_service'; import { InternalStatusServiceSetup, StatusServiceSetup, diff --git a/src/core/server/ui_settings/ui_settings_service.mock.ts b/src/core/server/ui_settings/ui_settings_service.mock.ts index b1ed0dd188cde..818e9b43889e3 100644 --- a/src/core/server/ui_settings/ui_settings_service.mock.ts +++ b/src/core/server/ui_settings/ui_settings_service.mock.ts @@ -22,7 +22,7 @@ import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart, } from './types'; -import { UiSettingsService } from './ui_settings_service'; +import type { UiSettingsService } from './ui_settings_service'; const createClientMock = () => { const mocked: jest.Mocked = { diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index e1f03b8a08847..39df3990ff2ff 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -128,21 +128,12 @@ export default () => cGroupOverrides: HANDLED_IN_NEW_PLATFORM, }).default(), - // still used by the legacy i18n mixin - plugins: Joi.object({ - paths: Joi.array().items(Joi.string()).default([]), - scanDirs: Joi.array().items(Joi.string()).default([]), - initialize: Joi.boolean().default(true), - }).default(), - + plugins: HANDLED_IN_NEW_PLATFORM, path: HANDLED_IN_NEW_PLATFORM, stats: HANDLED_IN_NEW_PLATFORM, status: HANDLED_IN_NEW_PLATFORM, map: HANDLED_IN_NEW_PLATFORM, - - i18n: Joi.object({ - locale: Joi.string().default('en'), - }).default(), + i18n: HANDLED_IN_NEW_PLATFORM, // temporarily moved here from the (now deleted) kibana legacy plugin kibana: Joi.object({ diff --git a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts b/src/legacy/server/i18n/get_kibana_translation_paths.test.ts deleted file mode 100644 index 0f202c4d433c0..0000000000000 --- a/src/legacy/server/i18n/get_kibana_translation_paths.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 { I18N_RC } from './constants'; -import { fromRoot } from '../../../core/server/utils'; - -jest.mock('./get_translation_paths', () => ({ getTranslationPaths: jest.fn() })); -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import { getTranslationPaths as mockGetTranslationPaths } from './get_translation_paths'; - -describe('getKibanaTranslationPaths', () => { - const mockConfig = { get: jest.fn() }; - - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('calls getTranslationPaths against kibana root and kibana-extra', async () => { - mockConfig.get.mockReturnValue([]); - await getKibanaTranslationPaths(mockConfig); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(1, { - cwd: fromRoot('.'), - glob: `*/${I18N_RC}`, - }); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { - cwd: fromRoot('../kibana-extra'), - glob: `*/${I18N_RC}`, - }); - }); - - it('calls getTranslationPaths for each config returned in plugin.paths and plugins.scanDirs', async () => { - mockConfig.get.mockReturnValueOnce(['a', 'b']).mockReturnValueOnce(['c']); - await getKibanaTranslationPaths(mockConfig); - expect(mockConfig.get).toHaveBeenNthCalledWith(1, 'plugins.paths'); - expect(mockConfig.get).toHaveBeenNthCalledWith(2, 'plugins.scanDirs'); - - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(2, { cwd: 'a', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(3, { cwd: 'b', glob: I18N_RC }); - expect(mockGetTranslationPaths).toHaveBeenNthCalledWith(4, { cwd: 'c', glob: `*/${I18N_RC}` }); - }); -}); diff --git a/src/legacy/server/i18n/i18n_mixin.ts b/src/legacy/server/i18n/i18n_mixin.ts deleted file mode 100644 index 0b3879073c164..0000000000000 --- a/src/legacy/server/i18n/i18n_mixin.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 { i18n, i18nLoader } from '@kbn/i18n'; -import { basename } from 'path'; -import { Server } from '@hapi/hapi'; -import type { UsageCollectionSetup } from '../../../plugins/usage_collection/server'; -import { getKibanaTranslationPaths } from './get_kibana_translation_paths'; -import KbnServer, { KibanaConfig } from '../kbn_server'; -import { registerLocalizationUsageCollector } from './localization'; - -export async function i18nMixin( - kbnServer: KbnServer, - server: Server, - config: Pick -) { - const locale = config.get('i18n.locale') as string; - - const translationPaths = await getKibanaTranslationPaths(config); - - const currentTranslationPaths = ([] as string[]) - .concat(...translationPaths) - .filter((translationPath) => basename(translationPath, '.json') === locale); - i18nLoader.registerTranslationFiles(currentTranslationPaths); - - const translations = await i18nLoader.getTranslationsByLocale(locale); - i18n.init( - Object.freeze({ - locale, - ...translations, - }) - ); - - const getTranslationsFilePaths = () => currentTranslationPaths; - - server.decorate('server', 'getTranslationsFilePaths', getTranslationsFilePaths); - - if (kbnServer.newPlatform.setup.plugins.usageCollection) { - const { usageCollection } = kbnServer.newPlatform.setup.plugins as { - usageCollection: UsageCollectionSetup; - }; - registerLocalizationUsageCollector(usageCollection, { - getLocale: () => config.get('i18n.locale') as string, - getTranslationsFilePaths, - }); - } -} diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index e29563a7c6266..013da35d2acb7 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -31,7 +31,6 @@ import warningsMixin from './warnings'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import { uiMixin } from '../ui'; -import { i18nMixin } from './i18n'; /** * @typedef {import('./kbn_server').KibanaConfig} KibanaConfig @@ -82,9 +81,6 @@ export default class KbnServer { loggingMixin, warningsMixin, - // scan translations dirs, register locale files and initialize i18n engine. - i18nMixin, - // tell the config we are done loading plugins configCompleteMixin, diff --git a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap index c479562795512..c782ce9c8cc84 100644 --- a/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap +++ b/src/plugins/kibana_usage_collection/server/__snapshots__/index.test.ts.snap @@ -13,3 +13,5 @@ exports[`kibana_usage_collection Runs the setup method without issues 5`] = `fal exports[`kibana_usage_collection Runs the setup method without issues 6`] = `true`; exports[`kibana_usage_collection Runs the setup method without issues 7`] = `false`; + +exports[`kibana_usage_collection Runs the setup method without issues 8`] = `true`; diff --git a/src/plugins/kibana_usage_collection/server/collectors/index.ts b/src/plugins/kibana_usage_collection/server/collectors/index.ts index 2408dc84c2e56..f3b7d8ca5eea0 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/index.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/index.ts @@ -24,3 +24,4 @@ export { registerKibanaUsageCollector } from './kibana'; export { registerOpsStatsCollector } from './ops_stats'; export { registerCspCollector } from './csp'; export { registerCoreUsageCollector } from './core'; +export { registerLocalizationUsageCollector } from './localization'; diff --git a/src/legacy/server/i18n/localization/file_integrity.test.mocks.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.mocks.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.mocks.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.test.ts diff --git a/src/legacy/server/i18n/localization/file_integrity.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts similarity index 100% rename from src/legacy/server/i18n/localization/file_integrity.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/file_integrity.ts diff --git a/src/legacy/server/i18n/localization/index.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/index.ts similarity index 100% rename from src/legacy/server/i18n/localization/index.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/index.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts similarity index 100% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.test.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.test.ts diff --git a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts similarity index 82% rename from src/legacy/server/i18n/localization/telemetry_localization_collector.ts rename to src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts index fb837f5ae28df..f2b00bd629a07 100644 --- a/src/legacy/server/i18n/localization/telemetry_localization_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/localization/telemetry_localization_collector.ts @@ -20,6 +20,7 @@ import { i18nLoader } from '@kbn/i18n'; import { size } from 'lodash'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { I18nServiceSetup } from '../../../../../core/server'; import { getIntegrityHashes, Integrities } from './file_integrity'; export interface UsageStats { @@ -28,11 +29,6 @@ export interface UsageStats { labelsCount?: number; } -export interface LocalizationUsageCollectorHelpers { - getLocale: () => string; - getTranslationsFilePaths: () => string[]; -} - export async function getTranslationCount( loader: typeof i18nLoader, locale: string @@ -41,13 +37,10 @@ export async function getTranslationCount( return size(translations.messages); } -export function createCollectorFetch({ - getLocale, - getTranslationsFilePaths, -}: LocalizationUsageCollectorHelpers) { +export function createCollectorFetch({ getLocale, getTranslationFiles }: I18nServiceSetup) { return async function fetchUsageStats(): Promise { const locale = getLocale(); - const translationFilePaths: string[] = getTranslationsFilePaths(); + const translationFilePaths: string[] = getTranslationFiles(); const [labelsCount, integrities] = await Promise.all([ getTranslationCount(i18nLoader, locale), @@ -62,15 +55,14 @@ export function createCollectorFetch({ }; } -// TODO: Migrate out of the Legacy dir export function registerLocalizationUsageCollector( usageCollection: UsageCollectionSetup, - helpers: LocalizationUsageCollectorHelpers + i18n: I18nServiceSetup ) { const collector = usageCollection.makeUsageCollector({ type: 'localization', isReady: () => true, - fetch: createCollectorFetch(helpers), + fetch: createCollectorFetch(i18n), schema: { locale: { type: 'keyword' }, integrities: { DYNAMIC_KEY: { type: 'text' } }, diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 198fdbb7a8703..16cb620351aaa 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -41,6 +41,7 @@ import { registerUiMetricUsageCollector, registerCspCollector, registerCoreUsageCollector, + registerLocalizationUsageCollector, } from './collectors'; interface KibanaUsageCollectionPluginsDepsSetup { @@ -104,5 +105,6 @@ export class KibanaUsageCollectionPlugin implements Plugin { ); registerCspCollector(usageCollection, coreSetup.http); registerCoreUsageCollector(usageCollection, getCoreUsageDataService); + registerLocalizationUsageCollector(usageCollection, coreSetup.i18n); } } diff --git a/src/plugins/telemetry/schema/legacy_plugins.json b/src/plugins/telemetry/schema/legacy_plugins.json index 1a7c0ccb15082..d5b0514b64918 100644 --- a/src/plugins/telemetry/schema/legacy_plugins.json +++ b/src/plugins/telemetry/schema/legacy_plugins.json @@ -1,21 +1,3 @@ { - "properties": { - "localization": { - "properties": { - "locale": { - "type": "keyword" - }, - "integrities": { - "properties": { - "DYNAMIC_KEY": { - "type": "text" - } - } - }, - "labelsCount": { - "type": "long" - } - } - } - } + "properties": {} } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index c840cbe8fc94d..a1eae69ffaed0 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -1581,6 +1581,23 @@ } } }, + "localization": { + "properties": { + "locale": { + "type": "keyword" + }, + "integrities": { + "properties": { + "DYNAMIC_KEY": { + "type": "text" + } + } + }, + "labelsCount": { + "type": "long" + } + } + }, "stack_management": { "properties": { "visualize:enableLabs": {