[7.x] [i18n] .i18nrc file as the source of truth and enhance tooling (#39774) (#43284)

* merge conflicts

* add inspector path
This commit is contained in:
Ahmad Bamieh 2019-08-14 20:47:02 +03:00 committed by Stacey Gammon
parent 1326d58de5
commit 5adf0f1c97
20 changed files with 317 additions and 145 deletions

View file

@ -8,6 +8,7 @@
"core": "src/core",
"inputControl": "src/legacy/core_plugins/input_control_vis",
"inspectorViews": "src/legacy/core_plugins/inspector_views",
"inspector": "src/plugins/inspector",
"interpreter": "src/legacy/core_plugins/interpreter",
"dashboardEmbeddableContainer": "src/legacy/core_plugins/dashboard_embeddable_container",
"kbn": "src/legacy/core_plugins/kibana",
@ -24,46 +25,8 @@
"timelion": "src/legacy/core_plugins/timelion",
"visTypeTagCloud": "src/legacy/core_plugins/vis_type_tagcloud",
"tsvb": "src/legacy/core_plugins/metrics",
"kbnESQuery": "packages/kbn-es-query",
"xpack.actions": "x-pack/legacy/plugins/actions",
"xpack.alerting": "x-pack/legacy/plugins/alerting",
"xpack.apm": "x-pack/legacy/plugins/apm",
"xpack.beatsManagement": "x-pack/legacy/plugins/beats_management",
"xpack.canvas": "x-pack/legacy/plugins/canvas",
"xpack.code": "x-pack/legacy/plugins/code",
"xpack.crossClusterReplication": "x-pack/legacy/plugins/cross_cluster_replication",
"xpack.dashboardMode": "x-pack/legacy/plugins/dashboard_mode",
"xpack.fileUpload": "x-pack/legacy/plugins/file_upload",
"xpack.graph": "x-pack/legacy/plugins/graph",
"xpack.grokDebugger": "x-pack/legacy/plugins/grokdebugger",
"xpack.idxMgmt": "x-pack/legacy/plugins/index_management",
"xpack.indexLifecycleMgmt": "x-pack/legacy/plugins/index_lifecycle_management",
"xpack.infra": "x-pack/legacy/plugins/infra",
"xpack.kueryAutocomplete": "x-pack/legacy/plugins/kuery_autocomplete",
"xpack.licenseMgmt": "x-pack/legacy/plugins/license_management",
"xpack.maps": "x-pack/legacy/plugins/maps",
"xpack.ml": "x-pack/legacy/plugins/ml",
"xpack.logstash": "x-pack/legacy/plugins/logstash",
"xpack.main": "x-pack/legacy/plugins/xpack_main",
"xpack.telemetry": "x-pack/legacy/plugins/telemetry",
"xpack.monitoring": "x-pack/legacy/plugins/monitoring",
"xpack.remoteClusters": "x-pack/legacy/plugins/remote_clusters",
"xpack.reporting": "x-pack/legacy/plugins/reporting",
"xpack.rollupJobs": "x-pack/legacy/plugins/rollup",
"xpack.searchProfiler": "x-pack/legacy/plugins/searchprofiler",
"xpack.siem": "x-pack/legacy/plugins/siem",
"xpack.security": "x-pack/legacy/plugins/security",
"xpack.server": "x-pack/legacy/server",
"xpack.snapshotRestore": "x-pack/legacy/plugins/snapshot_restore",
"xpack.spaces": "x-pack/legacy/plugins/spaces",
"xpack.upgradeAssistant": "x-pack/legacy/plugins/upgrade_assistant",
"xpack.uptime": "x-pack/legacy/plugins/uptime",
"xpack.watcher": "x-pack/legacy/plugins/watcher",
"inspector": "src/plugins/inspector"
"kbnESQuery": "packages/kbn-es-query"
},
"exclude": ["src/legacy/ui/ui_render/ui_render_mixin.js"],
"translations": [
"x-pack/plugins/translations/translations/zh-CN.json",
"x-pack/plugins/translations/translations/ja-JP.json"
]
"translations": []
}

View file

@ -110,5 +110,5 @@
#ops.interval: 5000
# Specifies locale to be used for all localizable strings, dates and number formats.
# Supported languages are the following: English - en , by default , Chinese - zh-CN .
# Supported languages are the following: English - en , by default , Chinese - zh-CN .
#i18n.locale: "en"

View file

@ -47,6 +47,7 @@ export const CopySourceTask = {
'webpackShims/**',
'config/kibana.yml',
'tsconfig*.json',
'.i18nrc.json',
'kibana.d.ts'
],
});

View file

@ -21,45 +21,47 @@ import { resolve } from 'path';
// @ts-ignore
import { normalizePath, readFileAsync } from '.';
// @ts-ignore
import rootConfig from '../../../.i18nrc.json';
export interface I18nConfig {
paths: Record<string, string>;
exclude: string[];
translations: string[];
prefix?: string;
}
/**
* Merges root .i18nrc.json config with any other additional configs (e.g. from
* third-party plugins).
* @param configPaths List of config paths.
*/
export async function mergeConfigs(configPaths: string | string[] = []) {
const mergedConfig: I18nConfig = { exclude: [], translations: [], ...rootConfig };
for (const configPath of Array.isArray(configPaths) ? configPaths : [configPaths]) {
const additionalConfig: I18nConfig = {
paths: {},
exclude: [],
translations: [],
...JSON.parse(await readFileAsync(resolve(configPath))),
};
for (const [namespace, path] of Object.entries(additionalConfig.paths)) {
mergedConfig.paths[namespace] = normalizePath(resolve(configPath, '..', path));
}
for (const exclude of additionalConfig.exclude) {
mergedConfig.exclude.push(normalizePath(resolve(configPath, '..', exclude)));
}
for (const translations of additionalConfig.translations) {
mergedConfig.translations.push(normalizePath(resolve(configPath, '..', translations)));
export async function checkConfigNamespacePrefix(configPath: string) {
const { prefix, paths } = JSON.parse(await readFileAsync(resolve(configPath)));
for (const [namespace] of Object.entries(paths)) {
if (prefix && prefix !== namespace.split('.')[0]) {
throw new Error(`namespace ${namespace} must be prefixed with ${prefix} in ${configPath}`);
}
}
}
return mergedConfig;
export async function assignConfigFromPath(
config: I18nConfig = { exclude: [], translations: [], paths: {} },
configPath: string
) {
const additionalConfig: I18nConfig = {
paths: {},
exclude: [],
translations: [],
...JSON.parse(await readFileAsync(resolve(configPath))),
};
for (const [namespace, path] of Object.entries(additionalConfig.paths)) {
config.paths[namespace] = normalizePath(resolve(configPath, '..', path));
}
for (const exclude of additionalConfig.exclude) {
config.exclude.push(normalizePath(resolve(configPath, '..', exclude)));
}
for (const translations of additionalConfig.translations) {
config.translations.push(normalizePath(resolve(configPath, '..', translations)));
}
return config;
}
/**

View file

@ -20,3 +20,4 @@
export const DEFAULT_MESSAGE_KEY = 'defaultMessage';
export const DESCRIPTION_KEY = 'description';
export const VALUES_KEY = 'values';
export const I18N_RC = '.i18nrc.json';

View file

@ -22,7 +22,12 @@ export { extractMessagesFromPathToMap } from './extract_default_translations';
// @ts-ignore
export { matchEntriesWithExctractors } from './extract_default_translations';
// @ts-ignore
export { writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils';
export { arrayify, writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils';
export { serializeToJson, serializeToJson5 } from './serializers';
export { I18nConfig, filterConfigPaths, mergeConfigs } from './config';
export {
I18nConfig,
filterConfigPaths,
assignConfigFromPath,
checkConfigNamespacePrefix,
} from './config';
export { integrateLocaleFiles } from './integrate_locale_files';

View file

@ -0,0 +1,44 @@
/*
* 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 { I18N_RC } from '../constants';
import { ErrorReporter, checkConfigNamespacePrefix, arrayify } from '..';
export function checkConfigs(additionalConfigPaths: string | string[] = []) {
const root = join(__dirname, '../../../../');
const kibanaRC = resolve(root, I18N_RC);
const xpackRC = resolve(root, 'x-pack', I18N_RC);
const configPaths = [kibanaRC, xpackRC, ...arrayify(additionalConfigPaths)];
return configPaths.map(configPath => ({
task: async (context: { reporter: ErrorReporter }) => {
try {
await checkConfigNamespacePrefix(configPath);
} catch (err) {
const { reporter } = context;
const reporterWithContext = reporter.withContext({ name: configPath });
reporterWithContext.report(err);
throw reporter;
}
},
title: `Checking configs in ${configPath}`,
}));
}

View file

@ -21,14 +21,7 @@ import chalk from 'chalk';
import { createFailError } from '../../run';
import { ErrorReporter, extractMessagesFromPathToMap, filterConfigPaths, I18nConfig } from '..';
export function extractDefaultMessages({
path,
config,
}: {
path?: string | string[];
config: I18nConfig;
}) {
const inputPaths = Array.isArray(path) ? path : [path || './'];
export function extractDefaultMessages(config: I18nConfig, inputPaths: string[]) {
const filteredPaths = filterConfigPaths(inputPaths, config) as string[];
if (filteredPaths.length === 0) {
throw createFailError(
@ -37,7 +30,6 @@ export function extractDefaultMessages({
)} None of input paths is covered by the mappings in .i18nrc.json.`
);
}
return filteredPaths.map(filteredPath => ({
task: async (context: {
messages: Map<string, { message: string }>;

View file

@ -92,13 +92,13 @@ export async function extractUntrackedMessagesTask({
}
}
export function extractUntrackedMessages(srcPaths: string[], config: I18nConfig) {
return srcPaths.map(srcPath => ({
title: `Checking untracked messages in ${srcPath}`,
task: async (context: { reporter: ErrorReporter }) => {
const { reporter } = context;
export function extractUntrackedMessages(inputPaths: string[]) {
return inputPaths.map(inputPath => ({
title: `Checking untracked messages in ${inputPath}`,
task: async (context: { reporter: ErrorReporter; config: I18nConfig }) => {
const { reporter, config } = context;
const initialErrorsNumber = reporter.errors.length;
const result = await extractUntrackedMessagesTask({ path: srcPath, config, reporter });
const result = await extractUntrackedMessagesTask({ path: inputPath, config, reporter });
if (reporter.errors.length === initialErrorsNumber) {
return result;
}

View file

@ -20,3 +20,5 @@
export { extractDefaultMessages } from './extract_default_translations';
export { extractUntrackedMessages } from './extract_untracked_translations';
export { checkCompatibility } from './check_compatibility';
export { mergeConfigs } from './merge_configs';
export { checkConfigs } from './check_configs';

View file

@ -0,0 +1,43 @@
/*
* 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 { ErrorReporter, I18nConfig, assignConfigFromPath, arrayify } from '..';
export function mergeConfigs(additionalConfigPaths: string | string[] = []) {
const root = join(__dirname, '../../../../');
const kibanaRC = resolve(root, '.i18nrc.json');
const xpackRC = resolve(root, 'x-pack/.i18nrc.json');
const configPaths = [kibanaRC, xpackRC, ...arrayify(additionalConfigPaths)];
return configPaths.map(configPath => ({
task: async (context: { reporter: ErrorReporter; config?: I18nConfig }) => {
try {
context.config = await assignConfigFromPath(context.config, configPath);
} catch (err) {
const { reporter } = context;
const reporterWithContext = reporter.withContext({ name: configPath });
reporterWithContext.report(err);
throw reporter;
}
},
title: `Merging configs in ${configPath}`,
}));
}

View file

@ -324,3 +324,8 @@ export class ErrorReporter {
);
}
}
// export function arrayify<Subj = any>(subj: Subj | Subj[]): Subj[] {
export function arrayify(subj) {
return Array.isArray(subj) ? subj : [subj];
}

View file

@ -20,10 +20,18 @@
import chalk from 'chalk';
import Listr from 'listr';
import { ErrorReporter, mergeConfigs } from './i18n';
import { extractDefaultMessages, extractUntrackedMessages, checkCompatibility } from './i18n/tasks';
import { ErrorReporter, I18nConfig } from './i18n';
import {
extractDefaultMessages,
extractUntrackedMessages,
checkCompatibility,
checkConfigs,
mergeConfigs,
} from './i18n/tasks';
import { createFailError, run } from './run';
const skipNoTranslations = ({ config }: { config: I18nConfig }) => !config.translations.length;
run(
async ({
flags: {
@ -59,27 +67,34 @@ run(
throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} --fix can't have a value`);
}
const config = await mergeConfigs(includeConfig);
const srcPaths = Array().concat(path || ['./src', './packages', './x-pack']);
if (config.translations.length === 0) {
return;
}
const list = new Listr(
[
{
title: 'Checking .i18nrc.json files',
task: () => new Listr(checkConfigs(includeConfig), { exitOnError: true }),
},
{
title: 'Merging .i18nrc.json files',
task: () => new Listr(mergeConfigs(includeConfig), { exitOnError: true }),
},
{
title: 'Checking For Untracked Messages based on .i18nrc.json',
task: () => new Listr(extractUntrackedMessages(srcPaths, config), { exitOnError: true }),
skip: skipNoTranslations,
task: ({ config }) =>
new Listr(extractUntrackedMessages(srcPaths), { exitOnError: true }),
},
{
title: 'Validating Default Messages',
task: () =>
new Listr(extractDefaultMessages({ path: srcPaths, config }), { exitOnError: true }),
skip: skipNoTranslations,
task: ({ config }) =>
new Listr(extractDefaultMessages(config, srcPaths), { exitOnError: true }),
},
{
title: 'Compatibility Checks',
task: () =>
skip: skipNoTranslations,
task: ({ config }) =>
new Listr(
checkCompatibility(
config,

View file

@ -21,14 +21,8 @@ import chalk from 'chalk';
import Listr from 'listr';
import { resolve } from 'path';
import {
ErrorReporter,
mergeConfigs,
serializeToJson,
serializeToJson5,
writeFileAsync,
} from './i18n';
import { extractDefaultMessages } from './i18n/tasks';
import { ErrorReporter, serializeToJson, serializeToJson5, writeFileAsync } from './i18n';
import { extractDefaultMessages, mergeConfigs } from './i18n/tasks';
import { createFailError, run } from './run';
run(
@ -52,13 +46,17 @@ run(
`${chalk.white.bgRed(' I18N ERROR ')} --path and --include-config require a value`
);
}
const config = await mergeConfigs(includeConfig);
const srcPaths = Array().concat(path || ['./src', './packages', './x-pack']);
const list = new Listr([
{
title: 'Merging .i18nrc.json files',
task: () => new Listr(mergeConfigs(includeConfig), { exitOnError: true }),
},
{
title: 'Extracting Default Messages',
task: () => new Listr(extractDefaultMessages({ path, config }), { exitOnError: true }),
task: ({ config }) =>
new Listr(extractDefaultMessages(config, srcPaths), { exitOnError: true }),
},
{
title: 'Writing to file',

View file

@ -20,8 +20,8 @@
import chalk from 'chalk';
import Listr from 'listr';
import { ErrorReporter, integrateLocaleFiles, mergeConfigs } from './i18n';
import { extractDefaultMessages } from './i18n/tasks';
import { ErrorReporter, integrateLocaleFiles } from './i18n';
import { extractDefaultMessages, mergeConfigs } from './i18n/tasks';
import { createFailError, run } from './run';
run(
@ -75,15 +75,21 @@ run(
);
}
const config = await mergeConfigs(includeConfig);
const srcPaths = Array().concat(path || ['./src', './packages', './x-pack']);
const list = new Listr([
{
title: 'Merging .i18nrc.json files',
task: () => new Listr(mergeConfigs(includeConfig), { exitOnError: true }),
},
{
title: 'Extracting Default Messages',
task: () => new Listr(extractDefaultMessages({ path, config }), { exitOnError: true }),
task: ({ config }) =>
new Listr(extractDefaultMessages(config, srcPaths), { exitOnError: true }),
},
{
title: 'Intregrating Locale File',
task: async ({ messages }) => {
task: async ({ messages, config }) => {
await integrateLocaleFiles(messages, {
sourceFileName: source,
targetFileName: target,

View file

@ -0,0 +1,20 @@
/*
* 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 I18N_RC = '.i18nrc.json';

View file

@ -0,0 +1,47 @@
/*
* 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 { promisify } from 'util';
import { readFile } from 'fs';
import { resolve, dirname } from 'path';
import globby from 'globby';
const readFileAsync = promisify(readFile);
export async function getTranslationPaths({ cwd, glob }) {
const entries = await globby(glob, { cwd });
const translationPaths = [];
for (const entry of entries) {
const entryFullPath = resolve(cwd, entry);
const pluginBasePath = dirname(entryFullPath);
try {
const content = await readFileAsync(entryFullPath, 'utf8');
const { translations } = JSON.parse(content);
translations.forEach(translation => {
const translationFullPath = resolve(pluginBasePath, translation);
translationPaths.push(translationFullPath);
});
} catch (err) {
throw new Error(`Failed to parse .i18nrc.json file at ${entryFullPath}`);
}
}
return translationPaths;
}

View file

@ -17,46 +17,30 @@
* under the License.
*/
import { resolve } from 'path';
import globby from 'globby';
import { i18n, i18nLoader } from '@kbn/i18n';
import { basename } from 'path';
import { fromRoot } from '../../utils';
import { getTranslationPaths } from './get_translations_path';
import { I18N_RC } from './constants';
export async function i18nMixin(kbnServer, server, config) {
const locale = config.get('i18n.locale');
// eslint-disable-next-line max-len
const translationsDirs = [fromRoot('src/legacy/ui/translations'), fromRoot('src/legacy/server/translations'), fromRoot('src/core/translations')];
const groupedEntries = await Promise.all([
...config.get('plugins.scanDirs').map(async path => {
const entries = await globby(`*/translations/${locale}.json`, {
cwd: path,
});
return entries.map(entry => resolve(path, entry));
const translationPaths = await Promise.all([
getTranslationPaths({
cwd: fromRoot('.'),
glob: I18N_RC,
}),
...config.get('plugins.paths').map(async path => {
const entries = await globby(
[`translations/${locale}.json`, `plugins/*/translations/${locale}.json`],
{
cwd: path,
}
);
return entries.map(entry => resolve(path, entry));
}),
...translationsDirs.map(async path => {
const entries = await globby(`${locale}.json`, {
cwd: path,
});
return entries.map(entry => resolve(path, entry));
...config.get('plugins.paths').map(cwd => getTranslationPaths({ cwd, glob: I18N_RC })),
...config.get('plugins.scanDirs').map(cwd => getTranslationPaths({ cwd, glob: `*/${I18N_RC}` })),
getTranslationPaths({
cwd: fromRoot('../kibana-extra'),
glob: `*/${I18N_RC}`,
}),
]);
const translationPaths = [].concat(...groupedEntries);
i18nLoader.registerTranslationFiles(translationPaths);
const currentTranslationPaths = [].concat(...translationPaths).filter(translationPath => basename(translationPath, '.json') === locale);
i18nLoader.registerTranslationFiles(currentTranslationPaths);
const translations = await i18nLoader.getTranslationsByLocale(locale);
i18n.init(Object.freeze({
@ -64,5 +48,5 @@ export async function i18nMixin(kbnServer, server, config) {
...translations,
}));
server.decorate('server', 'getTranslationsFilePaths', () => translationPaths);
server.decorate('server', 'getTranslationsFilePaths', () => currentTranslationPaths);
}

43
x-pack/.i18nrc.json Normal file
View file

@ -0,0 +1,43 @@
{
"prefix": "xpack",
"paths": {
"xpack.actions": "legacy/plugins/actions",
"xpack.alerting": "legacy/plugins/alerting",
"xpack.apm": "legacy/plugins/apm",
"xpack.beatsManagement": "legacy/plugins/beats_management",
"xpack.canvas": "legacy/plugins/canvas",
"xpack.code": "legacy/plugins/code",
"xpack.crossClusterReplication": "legacy/plugins/cross_cluster_replication",
"xpack.dashboardMode": "legacy/plugins/dashboard_mode",
"xpack.fileUpload": "legacy/plugins/file_upload",
"xpack.graph": "legacy/plugins/graph",
"xpack.grokDebugger": "legacy/plugins/grokdebugger",
"xpack.idxMgmt": "legacy/plugins/index_management",
"xpack.indexLifecycleMgmt": "legacy/plugins/index_lifecycle_management",
"xpack.infra": "legacy/plugins/infra",
"xpack.kueryAutocomplete": "legacy/plugins/kuery_autocomplete",
"xpack.licenseMgmt": "legacy/plugins/license_management",
"xpack.maps": "legacy/plugins/maps",
"xpack.ml": "legacy/plugins/ml",
"xpack.logstash": "legacy/plugins/logstash",
"xpack.main": "legacy/plugins/xpack_main",
"xpack.telemetry": "legacy/plugins/telemetry",
"xpack.monitoring": "legacy/plugins/monitoring",
"xpack.remoteClusters": "legacy/plugins/remote_clusters",
"xpack.reporting": "legacy/plugins/reporting",
"xpack.rollupJobs": "legacy/plugins/rollup",
"xpack.searchProfiler": "legacy/plugins/searchprofiler",
"xpack.siem": "legacy/plugins/siem",
"xpack.security": "legacy/plugins/security",
"xpack.server": "legacy/server",
"xpack.snapshotRestore": "legacy/plugins/snapshot_restore",
"xpack.spaces": "legacy/plugins/spaces",
"xpack.upgradeAssistant": "legacy/plugins/upgrade_assistant",
"xpack.uptime": "legacy/plugins/uptime",
"xpack.watcher": "legacy/plugins/watcher"
},
"translations": [
"plugins/translations/translations/zh-CN.json",
"plugins/translations/translations/ja-JP.json"
]
}

View file

@ -11,6 +11,7 @@
"yarn.lock",
"tsconfig.json",
"index.js",
".i18nrc.json",
"plugins/**/*",
"legacy/plugins/reporting/.phantom/*",
"legacy/plugins/reporting/.chromium/*",