diff --git a/.eslintrc.js b/.eslintrc.js index 82fd75db13c9a..23706483a4426 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -941,7 +941,7 @@ module.exports = { ], rules: { '@kbn/i18n/strings_should_be_translated_with_i18n': 'warn', - '@kbn/i18n/strings_should_be_translated_with_formatted_message': 'warn', + '@kbn/i18n/i18n_translate_should_start_with_the_right_id': 'warn', }, }, { diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 790c6dbedd4e5..65a6381cb622c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -368,7 +368,7 @@ src/plugins/es_ui_shared @elastic/platform-deployment-management packages/kbn-eslint-config @elastic/kibana-operations packages/kbn-eslint-plugin-disable @elastic/kibana-operations packages/kbn-eslint-plugin-eslint @elastic/kibana-operations -packages/kbn-eslint-plugin-i18n @elastic/obs-knowledge-team +packages/kbn-eslint-plugin-i18n @elastic/obs-knowledge-team @elastic/kibana-operations packages/kbn-eslint-plugin-imports @elastic/kibana-operations packages/kbn-eslint-plugin-telemetry @elastic/obs-knowledge-team x-pack/test/encrypted_saved_objects_api_integration/plugins/api_consumer_plugin @elastic/kibana-security diff --git a/packages/kbn-eslint-plugin-i18n/README.mdx b/packages/kbn-eslint-plugin-i18n/README.mdx index 174457477e81a..6b836eb8bb528 100644 --- a/packages/kbn-eslint-plugin-i18n/README.mdx +++ b/packages/kbn-eslint-plugin-i18n/README.mdx @@ -6,22 +6,88 @@ description: Custom ESLint rules to support translations in the Kibana repositor tags: ['kibana', 'dev', 'contributor', 'operations', 'eslint', 'i18n'] --- -`@kbn/eslint-plugin-i18n` is an ESLint plugin providing custom rules for validating JSXCode in the Kibana repo to make sure they are translated. +# Summary -Note: At the moment these rules only work for apps that are inside `/x-pack/plugins`. -If you want to enable this rule on code that is outside of this path, adjust `/helpers/get_i18n_identifier_from_file_path.ts`. +`@kbn/eslint-plugin-i18n` is an ESLint plugin providing custom ESLint rules to help validating code in the Kibana repo in the area of translations. + +The aim of this package is to help engineers type less and have a nicer experience. + +If a rule does not behave as you expect or you have an idea of how these rules can be improved, please reach out to the Observability Knowledge Team or the Kibana Operations team. + +# Rules ## `@kbn/i18n/strings_should_be_translated_with_i18n` -This rule warns engineers to translate their strings by using i18n.translate from the '@kbn/i18n' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree to generate a translation ID. -It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value. +This rule warns engineers to translate their strings by using `i18n.translate` from the `@kbn/i18n` package. + +It provides an autofix that takes into account the context of the translatable string in the JSX tree to generate a translation ID. + +This rule kicks in on: + +- JSXText elements; +- specific JSXAttributes (`label` and `aria-label`) which expect a translated value. + +### Example + +This code: + +``` +// Filename: /x-pack/plugins/observability/public/my_component.tsx + +import React from 'react'; +import { EuiText } from '@elastic/eui'; + +function MyComponent() { + return ( + You know, for search + ) +} +``` + +will be autofixed with: + +``` +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; + +function MyComponent() { + return ( + + {i18n.translate('xpack.observability.myComponent.textLabel', { defaultMessage: 'You know, for search' } )} + + ) +} +``` + +If `i18n` has not been imported yet, the autofix will automatically add the import statement as well. + +### Exemptions and exceptions + +A JSXText element or JSXAttribute `label` or `aria-label` of which the value is: + +- wrapped in a `EuiCode` or `EuiBetaBadge` component, +- made up of non alpha characters such as `!@#$%^&*(){}` or numbers, +- wrapped in three backticks, + +are exempt from this rule. + +If this rule kicks in on a string value that you don't like, you can escape it by wrapping the string inside a JSXExpression: `{'my escaped value'}`. + +--- ## `@kbn/i18n/strings_should_be_translated_with_formatted_message` -This rule warns engineers to translate their strings by using `` from the '@kbn/i18n-react' package. It provides an autofix that takes into account the context of the translatable string in the JSX tree and to generate a translation ID. -It kicks in on JSXText elements and specific JSXAttributes (`label` and `aria-label`) which expect a translated value. +This rule warns engineers to translate their strings by using `` from the `@kbn/i18n-react` package. + +It provides an autofix that takes into account the context of the translatable string in the JSX tree and to generate a translation ID. + +This rule kicks in on: -## Exemptions and exceptions +- JSXText elements; +- specific JSXAttributes (`label` and `aria-label`) which expect a translated value. + +### Exemptions and exceptions A JSXText element or JSXAttribute `label` or `aria-label` of which the value is: @@ -32,3 +98,52 @@ A JSXText element or JSXAttribute `label` or `aria-label` of which the value is: are exempt from this rule. If this rule kicks in on a string value that you don't like, you can escape it by wrapping the string inside a JSXExpression: `{'my escaped value'}`. + +--- + +## `@kbn/i18n/i18n_translate_should_start_with_the_right_id` + +This rule checks every instance of `i18n.translate()` if the first parameter passed: + +1. has a string value, +2. if the parameter starts with the correct i18n app identifier for the file. + +It checks the repo for the `i18nrc.json` and `/x-pack/i18nrc.json` files and determines what the right i18n identifier should be. + +If the parameter is missing or does not start with the right i18n identifier, it can autofix the parameter. + +This rule is useful when defining translated values in plain functions (non-JSX), but it works in JSX as well. + +### Example + +This code: + +``` +// Filename: /x-pack/plugins/observability/public/my_function.ts + +function myFunction() { + const translations = [ + { + id: 'copy'; + label: i18n.translate() + } + ] +} +``` + +will be autofixed with: + +``` +import { i18n } from '@kbn/i18n'; + +function myFunction() { + const translations = [ + { + id: 'copy'; + label: i18n.translate('xpack.observability.myFunction.', { defaultMessage: '' }) + } + ] +} +``` + +If `i18n` has not been imported yet, the autofix will automatically add the import statement as well. diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts index 6e01b89b23565..cea9fa1c333d9 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.test.ts @@ -11,17 +11,15 @@ import { getI18nIdentifierFromFilePath } from './get_i18n_identifier_from_file_p const SYSTEMPATH = 'systemPath'; const testMap = [ - ['x-pack/plugins/observability/foo/bar/baz/header_actions.tsx', 'xpack.observability'], - ['x-pack/plugins/apm/public/components/app/correlations/correlations_table.tsx', 'xpack.apm'], - ['x-pack/plugins/cases/public/components/foo.tsx', 'xpack.cases'], + ['x-pack/plugins/observability/public/header_actions.tsx', 'xpack.observability'], + ['x-pack/plugins/apm/common/components/app/correlations/correlations_table.tsx', 'xpack.apm'], + ['x-pack/plugins/cases/server/components/foo.tsx', 'xpack.cases'], [ 'x-pack/plugins/synthetics/public/apps/synthetics/components/alerts/toggle_alert_flyout_button.tsx', 'xpack.synthetics', ], - [ - 'packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', - 'app_not_found_in_i18nrc', - ], + ['src/plugins/vis_types/gauge/public/editor/collections.ts', 'visTypeGauge'], + ['packages/kbn-alerts-ui-shared/src/alert_lifecycle_status_badge/index.tsx', 'alertsUIShared'], ]; describe('Get i18n Identifier for file', () => { diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts index d23a42f4ebcfb..7b39d119ee845 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_identifier_from_file_path.ts @@ -14,18 +14,38 @@ export function getI18nIdentifierFromFilePath(fileName: string, cwd: string) { const { dir } = parse(fileName); const relativePathToFile = dir.replace(cwd, ''); - const relativePathArray = relativePathToFile.split('/'); + // We need to match the path of the file that is being worked in with the path + // that is noted in the values inside the i18nrc.json object. + // These values differ depending on which i18nrc.json object you look at (there are multiple) + // so we need to account for both notations. + const relativePathArray = relativePathToFile.includes('src') + ? relativePathToFile.split('/').slice(1) + : relativePathToFile.split('/').slice(2); - const path = `${relativePathArray[2]}/${relativePathArray[3]}`; + const pluginNameIndex = relativePathArray.findIndex( + (el) => el === 'public' || el === 'server' || el === 'common' + ); + + const path = relativePathArray.slice(0, pluginNameIndex).join('/'); const xpackRC = resolve(join(__dirname, '../../../'), 'x-pack/.i18nrc.json'); + const rootRC = resolve(join(__dirname, '../../../'), '.i18nrc.json'); + + const xpackI18nrcFile = fs.readFileSync(xpackRC, 'utf8'); + const xpackI18nrc = JSON.parse(xpackI18nrcFile); + + const rootI18nrcFile = fs.readFileSync(rootRC, 'utf8'); + const rootI18nrc = JSON.parse(rootI18nrcFile); + + const allPaths = { ...xpackI18nrc.paths, ...rootI18nrc.paths }; - const i18nrcFile = fs.readFileSync(xpackRC, 'utf8'); - const i18nrc = JSON.parse(i18nrcFile); + if (Object.keys(allPaths).length === 0) return 'could_not_find_i18nrc'; - return i18nrc && i18nrc.paths - ? findKey(i18nrc.paths, (v) => - Array.isArray(v) ? v.find((e) => e === path) : typeof v === 'string' && v === path - ) ?? 'app_not_found_in_i18nrc' - : 'could_not_find_i18nrc'; + return ( + findKey(allPaths, (value) => + Array.isArray(value) + ? value.find((el) => el === path) + : typeof value === 'string' && value === path + ) ?? 'app_not_found_in_i18nrc' + ); } diff --git a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_import_fixer.ts b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_import_fixer.ts index cf3a7330f7584..7b81c0f7a6b8c 100644 --- a/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_import_fixer.ts +++ b/packages/kbn-eslint-plugin-i18n/helpers/get_i18n_import_fixer.ts @@ -10,10 +10,10 @@ import { SourceCode } from 'eslint'; export function getI18nImportFixer({ sourceCode, - mode, + translationFunction, }: { sourceCode: SourceCode; - mode: 'i18n.translate' | 'FormattedMessage'; + translationFunction: 'i18n.translate' | 'FormattedMessage'; }) { let existingI18nImportLineIndex = -1; let i18nImportLineToBeAdded = ''; @@ -27,7 +27,7 @@ export function getI18nImportFixer({ * * */ - if (mode === 'i18n.translate') { + if (translationFunction === 'i18n.translate') { existingI18nImportLineIndex = sourceCode.lines.findIndex((l) => l.includes("from '@kbn/i18n'")); const i18nImportLineInSource = sourceCode.lines[existingI18nImportLineIndex]; @@ -46,7 +46,7 @@ export function getI18nImportFixer({ } } - if (mode === 'FormattedMessage') { + if (translationFunction === 'FormattedMessage') { existingI18nImportLineIndex = sourceCode.lines.findIndex((l) => l.includes("from '@kbn/i18n-react'") ); @@ -83,21 +83,27 @@ export function getI18nImportFixer({ return { i18nImportLine: i18nImportLineToBeAdded, rangeToAddI18nImportLine: [start, end] as [number, number], - mode: 'replace', + replaceMode: 'replace', }; } // If the file doesn't have an import line for the translation package yet, we need to add it. // Pretty safe bet to add it underneath the import line for React. - const lineIndex = sourceCode.lines.findIndex((l) => l.includes("from 'react'")); + let lineIndex = sourceCode.lines.findIndex((l) => l.includes("from 'react'") || l.includes('*/')); + + if (lineIndex === -1) { + lineIndex = 0; + } + const targetLine = sourceCode.lines[lineIndex]; + // `getIndexFromLoc` is 0-based, so we need to add 1 to the line index. const start = sourceCode.getIndexFromLoc({ line: lineIndex + 1, column: 0 }); const end = start + targetLine.length; return { i18nImportLine: i18nImportLineToBeAdded, rangeToAddI18nImportLine: [start, end] as [number, number], - mode: 'insert', + replaceMode: 'insert', }; } diff --git a/packages/kbn-eslint-plugin-i18n/index.ts b/packages/kbn-eslint-plugin-i18n/index.ts index be5661cf46dec..dd99785204c33 100644 --- a/packages/kbn-eslint-plugin-i18n/index.ts +++ b/packages/kbn-eslint-plugin-i18n/index.ts @@ -8,6 +8,7 @@ import { StringsShouldBeTranslatedWithI18n } from './rules/strings_should_be_translated_with_i18n'; import { StringsShouldBeTranslatedWithFormattedMessage } from './rules/strings_should_be_translated_with_formatted_message'; +import { I18nTranslateShouldStartWithTheRightId } from './rules/i18n_translate_should_start_with_the_right_id'; /** * Custom ESLint rules, add `'@kbn/eslint-plugin-i18n'` to your eslint config to use them @@ -17,4 +18,5 @@ export const rules = { strings_should_be_translated_with_i18n: StringsShouldBeTranslatedWithI18n, strings_should_be_translated_with_formatted_message: StringsShouldBeTranslatedWithFormattedMessage, + i18n_translate_should_start_with_the_right_id: I18nTranslateShouldStartWithTheRightId, }; diff --git a/packages/kbn-eslint-plugin-i18n/kibana.jsonc b/packages/kbn-eslint-plugin-i18n/kibana.jsonc index 72e051941db68..b234cc835ed3a 100644 --- a/packages/kbn-eslint-plugin-i18n/kibana.jsonc +++ b/packages/kbn-eslint-plugin-i18n/kibana.jsonc @@ -1,6 +1,6 @@ { "type": "shared-common", "id": "@kbn/eslint-plugin-i18n", - "owner": "@elastic/obs-knowledge-team", + "owner": ["@elastic/obs-knowledge-team", "@elastic/kibana-operations"], "devOnly": true } diff --git a/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.test.ts b/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.test.ts new file mode 100644 index 0000000000000..49bdb03a4f476 --- /dev/null +++ b/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.test.ts @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { RuleTester } from 'eslint'; +import { + I18nTranslateShouldStartWithTheRightId, + RULE_WARNING_MESSAGE, +} from './i18n_translate_should_start_with_the_right_id'; + +const tsTester = [ + '@typescript-eslint/parser', + new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, + }), +] as const; + +const babelTester = [ + '@babel/eslint-parser', + new RuleTester({ + parser: require.resolve('@babel/eslint-parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + requireConfigFile: false, + babelOptions: { + presets: ['@kbn/babel-preset/node_preset'], + }, + }, + }), +] as const; + +const invalid: RuleTester.InvalidTestCase[] = [ + { + name: 'When a string literal is passed to i18n.translate, it should start with the correct i18n identifier.', + filename: '/x-pack/plugins/observability/public/test_component.ts', + code: ` +import { i18n } from '@kbn/i18n'; + +function TestComponent() { + const foo = i18n.translate('foo'); +}`, + errors: [ + { + line: 5, + message: RULE_WARNING_MESSAGE, + }, + ], + output: ` +import { i18n } from '@kbn/i18n'; + +function TestComponent() { + const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' }); +}`, + }, + { + name: 'When no string literal is passed to i18n.translate, it should start with the correct i18n identifier.', + filename: '/x-pack/plugins/observability/public/test_component.ts', + code: ` +import { i18n } from '@kbn/i18n'; + +function TestComponent() { + const foo = i18n.translate(); +}`, + errors: [ + { + line: 5, + message: RULE_WARNING_MESSAGE, + }, + ], + output: ` +import { i18n } from '@kbn/i18n'; + +function TestComponent() { + const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' }); +}`, + }, + { + name: 'When i18n is not imported yet, the rule should add it.', + filename: '/x-pack/plugins/observability/public/test_component.ts', + code: ` +function TestComponent() { + const foo = i18n.translate(); +}`, + errors: [ + { + line: 3, + message: RULE_WARNING_MESSAGE, + }, + ], + output: ` +import { i18n } from '@kbn/i18n'; +function TestComponent() { + const foo = i18n.translate('xpack.observability.testComponent.', { defaultMessage: '' }); +}`, + }, +]; + +const valid: RuleTester.ValidTestCase[] = [ + { + name: invalid[0].name, + filename: invalid[0].filename, + code: invalid[0].output as string, + }, + { + name: invalid[1].name, + filename: invalid[1].filename, + code: invalid[1].output as string, + }, +]; + +for (const [name, tester] of [tsTester, babelTester]) { + describe(name, () => { + tester.run( + '@kbn/i18n_translate_should_start_with_the_right_id', + I18nTranslateShouldStartWithTheRightId, + { + valid, + invalid, + } + ); + }); +} diff --git a/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.ts b/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.ts new file mode 100644 index 0000000000000..d6510ba588a4e --- /dev/null +++ b/packages/kbn-eslint-plugin-i18n/rules/i18n_translate_should_start_with_the_right_id.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { TSESTree } from '@typescript-eslint/typescript-estree'; +import type { Rule } from 'eslint'; +import { getI18nIdentifierFromFilePath } from '../helpers/get_i18n_identifier_from_file_path'; +import { getFunctionName } from '../helpers/get_function_name'; +import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer'; +import { isTruthy } from '../helpers/utils'; + +export const RULE_WARNING_MESSAGE = + 'First parameter passed to i18n.translate should start with the correct i18n identifier for this file. Correct it or use the autofix suggestion.'; + +export const I18nTranslateShouldStartWithTheRightId: Rule.RuleModule = { + meta: { + type: 'suggestion', + fixable: 'code', + }, + create(context) { + const { cwd, filename, getScope, sourceCode, report } = context; + + return { + CallExpression: (node: TSESTree.CallExpression) => { + const { callee } = node; + + if ( + !callee || + !('object' in callee) || + !('property' in callee) || + !('name' in callee.object) || + !('name' in callee.property) || + callee.object.name !== 'i18n' || + callee.property.name !== 'translate' + ) + return; + + const identifier = + Array.isArray(node.arguments) && + node.arguments.length && + 'value' in node.arguments[0] && + typeof node.arguments[0].value === 'string' && + node.arguments[0].value; + + const i18nAppId = getI18nIdentifierFromFilePath(filename, cwd); + const functionDeclaration = getScope().block as TSESTree.FunctionDeclaration; + const functionName = getFunctionName(functionDeclaration); + + // Check if i18n has already been imported into the file + const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } = + getI18nImportFixer({ + sourceCode, + translationFunction: 'i18n.translate', + }); + + if (!identifier || (identifier && !identifier.startsWith(`${i18nAppId}.`))) { + report({ + node: node as any, + message: RULE_WARNING_MESSAGE, + fix(fixer) { + return [ + fixer.replaceTextRange( + node.range, + `i18n.translate('${i18nAppId}.${functionName}.', { defaultMessage: '' })` + ), + !hasI18nImportLine && rangeToAddI18nImportLine + ? replaceMode === 'replace' + ? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine) + : fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) + : null, + ].filter(isTruthy); + }, + }); + } + }, + } as Rule.RuleListener; + }, +}; diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts index 009fac255fc63..6faf6732f9015 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.test.ts @@ -7,7 +7,10 @@ */ import { RuleTester } from 'eslint'; -import { StringsShouldBeTranslatedWithFormattedMessage } from './strings_should_be_translated_with_formatted_message'; +import { + StringsShouldBeTranslatedWithFormattedMessage, + RULE_WARNING_MESSAGE, +} from './strings_should_be_translated_with_formatted_message'; const tsTester = [ '@typescript-eslint/parser', @@ -41,7 +44,7 @@ const babelTester = [ const invalid: RuleTester.InvalidTestCase[] = [ { name: 'A JSX element with a string literal should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -53,7 +56,7 @@ function TestComponent() { errors: [ { line: 6, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -64,7 +67,7 @@ function TestComponent() { return (
) @@ -72,7 +75,7 @@ function TestComponent() { }, { name: 'A JSX element with a string literal that are inside an Eui component should take the component name of the parent into account', - filename: 'x-pack/plugins/observability/public/another_component.tsx', + filename: '/x-pack/plugins/observability/public/another_component.tsx', code: ` import React from 'react'; @@ -90,7 +93,7 @@ function AnotherComponent() { errors: [ { line: 9, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -104,7 +107,7 @@ function AnotherComponent() { @@ -115,7 +118,7 @@ function AnotherComponent() { }, { name: 'When no import of the translation module is present, the import line should be added', - filename: 'x-pack/plugins/observability/public/yet_another_component.tsx', + filename: '/x-pack/plugins/observability/public/yet_another_component.tsx', code: ` import React from 'react'; @@ -129,7 +132,7 @@ function YetAnotherComponent() { errors: [ { line: 7, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -141,7 +144,7 @@ function YetAnotherComponent() {
@@ -150,7 +153,7 @@ function YetAnotherComponent() { }, { name: 'Import lines without the necessary translation module should be updated to include i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { SomeOtherModule } from '@kbn/i18n-react'; @@ -163,7 +166,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -172,13 +175,13 @@ import { SomeOtherModule, FormattedMessage } from '@kbn/i18n-react'; function TestComponent() { return ( - } /> + } /> ) }`, }, { name: 'JSX elements that have a label or aria-label prop with a string value should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -191,7 +194,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -200,13 +203,13 @@ import { FormattedMessage } from '@kbn/i18n-react'; function TestComponent() { return ( - } /> + } /> ) }`, }, { name: 'JSX elements that have a label or aria-label prop with a JSXExpression value that is a string should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -219,7 +222,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with . Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -228,7 +231,7 @@ function TestComponent() { function TestComponent() { return ( - } /> + } /> ) }`, }, @@ -237,7 +240,7 @@ function TestComponent() { const valid: RuleTester.ValidTestCase[] = [ { name: 'A JSXText element inside a EuiCode component should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -249,7 +252,7 @@ function TestComponent() { }, { name: 'A JSXText element that contains anything other than alpha characters should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -261,7 +264,7 @@ function TestComponent() { }, { name: 'A JSXText element that is wrapped in three backticks (markdown) should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts index 77b5918951036..ea96cf313d1b4 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_formatted_message.ts @@ -14,6 +14,8 @@ import { getFunctionName } from '../helpers/get_function_name'; import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer'; import { cleanString, isTruthy } from '../helpers/utils'; +export const RULE_WARNING_MESSAGE = + 'Strings should be translated with . Use the autofix suggestion or add your own.'; export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { meta: { type: 'suggestion', @@ -44,17 +46,16 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' // Check if i18n has already been imported into the file - const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } = + const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } = getI18nImportFixer({ sourceCode, - mode: 'FormattedMessage', + translationFunction: 'FormattedMessage', }); // Show warning to developer and offer autofix suggestion report({ node: node as any, - message: - 'Strings should be translated with . Use the autofix suggestion or add your own.', + message: RULE_WARNING_MESSAGE, fix(fixer) { return [ fixer.replaceText( @@ -65,7 +66,7 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { />` ), !hasI18nImportLine && rangeToAddI18nImportLine - ? mode === 'replace' + ? replaceMode === 'replace' ? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine) : fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) : null, @@ -106,17 +107,16 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' // Check if i18n has already been imported into the file. - const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } = + const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } = getI18nImportFixer({ sourceCode, - mode: 'FormattedMessage', + translationFunction: 'FormattedMessage', }); // Show warning to developer and offer autofix suggestion report({ node: node as any, - message: - 'Strings should be translated with . Use the autofix suggestion or add your own.', + message: RULE_WARNING_MESSAGE, fix(fixer) { return [ fixer.replaceTextRange( @@ -124,7 +124,7 @@ export const StringsShouldBeTranslatedWithFormattedMessage: Rule.RuleModule = { `{}` ), !hasI18nImportLine && rangeToAddI18nImportLine - ? mode === 'replace' + ? replaceMode === 'replace' ? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine) : fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) : null, diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts index f470ed885682f..3142d368b0764 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.test.ts @@ -7,7 +7,10 @@ */ import { RuleTester } from 'eslint'; -import { StringsShouldBeTranslatedWithI18n } from './strings_should_be_translated_with_i18n'; +import { + StringsShouldBeTranslatedWithI18n, + RULE_WARNING_MESSAGE, +} from './strings_should_be_translated_with_i18n'; const tsTester = [ '@typescript-eslint/parser', @@ -41,7 +44,7 @@ const babelTester = [ const invalid: RuleTester.InvalidTestCase[] = [ { name: 'A JSX element with a string literal should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -53,7 +56,7 @@ function TestComponent() { errors: [ { line: 6, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -62,13 +65,13 @@ import { i18n } from '@kbn/i18n'; function TestComponent() { return ( -
{i18n.translate('app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}
+
{i18n.translate('xpack.observability.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}
) }`, }, { name: 'A JSX element with a string literal that are inside an Eui component should take the component name of the parent into account', - filename: 'x-pack/plugins/observability/public/another_component.tsx', + filename: '/x-pack/plugins/observability/public/another_component.tsx', code: ` import React from 'react'; @@ -86,7 +89,7 @@ function AnotherComponent() { errors: [ { line: 9, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -98,7 +101,7 @@ function AnotherComponent() { - {i18n.translate('app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })} + {i18n.translate('xpack.observability.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })} @@ -107,7 +110,7 @@ function AnotherComponent() { }, { name: 'When no import of the translation module is present, the import line should be added', - filename: 'x-pack/plugins/observability/public/yet_another_component.tsx', + filename: '/x-pack/plugins/observability/public/yet_another_component.tsx', code: ` import React from 'react'; @@ -121,7 +124,7 @@ function YetAnotherComponent() { errors: [ { line: 7, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -131,14 +134,14 @@ import { i18n } from '@kbn/i18n'; function YetAnotherComponent() { return (
- {i18n.translate('app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })} + {i18n.translate('xpack.observability.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })}
) }`, }, { name: 'Import lines without the necessary translation module should be updated to include i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { SomeOtherModule } from '@kbn/i18n'; @@ -151,7 +154,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -160,13 +163,13 @@ import { SomeOtherModule, i18n } from '@kbn/i18n'; function TestComponent() { return ( - + ) }`, }, { name: 'JSX elements that have a label or aria-label prop with a string value should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { i18n } from '@kbn/i18n'; @@ -179,7 +182,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -188,13 +191,13 @@ import { i18n } from '@kbn/i18n'; function TestComponent() { return ( - + ) }`, }, { name: 'JSX elements that have a label or aria-label prop with a JSXExpression value that is a string should be translated with i18n', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; import { i18n } from '@kbn/i18n'; @@ -207,7 +210,7 @@ function TestComponent() { errors: [ { line: 7, - message: `Strings should be translated with i18n. Use the autofix suggestion or add your own.`, + message: RULE_WARNING_MESSAGE, }, ], output: ` @@ -216,7 +219,7 @@ import { i18n } from '@kbn/i18n'; function TestComponent() { return ( - + ) }`, }, @@ -225,7 +228,7 @@ function TestComponent() { const valid: RuleTester.ValidTestCase[] = [ { name: 'A JSXText element inside a EuiCode component should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -237,7 +240,7 @@ function TestComponent() { }, { name: 'A JSXText element that contains anything other than alpha characters should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; @@ -249,7 +252,7 @@ function TestComponent() { }, { name: 'A JSXText element that is wrapped in three backticks (markdown) should not be translated', - filename: 'x-pack/plugins/observability/public/test_component.tsx', + filename: '/x-pack/plugins/observability/public/test_component.tsx', code: ` import React from 'react'; diff --git a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts index fea04d33d555f..ec1630de115e6 100644 --- a/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts +++ b/packages/kbn-eslint-plugin-i18n/rules/strings_should_be_translated_with_i18n.ts @@ -14,6 +14,9 @@ import { getFunctionName } from '../helpers/get_function_name'; import { getI18nImportFixer } from '../helpers/get_i18n_import_fixer'; import { cleanString, isTruthy } from '../helpers/utils'; +export const RULE_WARNING_MESSAGE = + 'Strings should be translated with i18n. Use the autofix suggestion or add your own.'; + export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { meta: { type: 'suggestion', @@ -44,17 +47,16 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' // Check if i18n has already been imported into the file - const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } = + const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } = getI18nImportFixer({ sourceCode, - mode: 'i18n.translate', + translationFunction: 'i18n.translate', }); // Show warning to developer and offer autofix suggestion report({ node: node as any, - message: - 'Strings should be translated with i18n. Use the autofix suggestion or add your own.', + message: RULE_WARNING_MESSAGE, fix(fixer) { return [ fixer.replaceText( @@ -62,7 +64,7 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { `${whiteSpaces}{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${value}' })}` ), !hasI18nImportLine && rangeToAddI18nImportLine - ? mode === 'replace' + ? replaceMode === 'replace' ? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine) : fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) : null, @@ -103,17 +105,16 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { const translationIdSuggestion = `${i18nAppId}.${functionName}.${intent}`; // 'xpack.observability.overview.logs.loadMoreLabel' // Check if i18n has already been imported into the file. - const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, mode } = + const { hasI18nImportLine, i18nImportLine, rangeToAddI18nImportLine, replaceMode } = getI18nImportFixer({ sourceCode, - mode: 'i18n.translate', + translationFunction: 'i18n.translate', }); // Show warning to developer and offer autofix suggestion report({ node: node as any, - message: - 'Strings should be translated with i18n. Use the autofix suggestion or add your own.', + message: RULE_WARNING_MESSAGE, fix(fixer) { return [ fixer.replaceTextRange( @@ -121,7 +122,7 @@ export const StringsShouldBeTranslatedWithI18n: Rule.RuleModule = { `{i18n.translate('${translationIdSuggestion}', { defaultMessage: '${val}' })}` ), !hasI18nImportLine && rangeToAddI18nImportLine - ? mode === 'replace' + ? replaceMode === 'replace' ? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine) : fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`) : null, diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx index 6be9b28707dff..ede574abba12b 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors_ingestion.tsx @@ -50,7 +50,10 @@ export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ a - createConnector()}> + createConnector()} + > {i18n.translate( 'xpack.serverlessSearch.ingestData.alternativeOptions.setupConnectorLabel', { @@ -74,6 +77,7 @@ export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ a