Improvements for eslint-i18n-package (#171588)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Coen Warmer 2023-11-23 13:02:36 +01:00 committed by GitHub
parent aae3e5d087
commit 0bf4998514
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 468 additions and 100 deletions

View file

@ -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',
},
},
{

2
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -6,22 +6,63 @@ 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.
## `@kbn/i18n/strings_should_be_translated_with_formatted_message`
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 warns engineers to translate their strings by using `<FormattedMessage>` 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 kicks in on:
## Exemptions and exceptions
- 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 (
<EuiText>You know, for search</EuiText>
)
}
```
will be autofixed with:
```
import React from 'react';
import { i18n } from '@kbn/i18n';
import { EuiText } from '@elastic/eui';
function MyComponent() {
return (
<EuiText>
{i18n.translate('xpack.observability.myComponent.textLabel', { defaultMessage: 'You know, for search' } )}
</EuiText>
)
}
```
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:
@ -32,3 +73,77 @@ 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/strings_should_be_translated_with_formatted_message`
This rule warns engineers to translate their strings by using `<FormattedMessage>` 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:
- 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:
- 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/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.

View file

@ -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', () => {

View file

@ -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 i18nrcFile = fs.readFileSync(xpackRC, 'utf8');
const i18nrc = JSON.parse(i18nrcFile);
const xpackI18nrcFile = fs.readFileSync(xpackRC, 'utf8');
const xpackI18nrc = JSON.parse(xpackI18nrcFile);
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';
const rootI18nrcFile = fs.readFileSync(rootRC, 'utf8');
const rootI18nrc = JSON.parse(rootI18nrcFile);
const allPaths = { ...xpackI18nrc.paths, ...rootI18nrc.paths };
if (Object.keys(allPaths).length === 0) return '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'
);
}

View file

@ -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',
};
}

View file

@ -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,
};

View file

@ -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
}

View file

@ -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,
}
);
});
}

View file

@ -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;
},
};

View file

@ -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 <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -64,7 +67,7 @@ function TestComponent() {
return (
<div>
<FormattedMessage
id="app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel"
id="xpack.observability.testComponent.div.thisIsATestLabel"
defaultMessage="This is a test"
/></div>
)
@ -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 <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -104,7 +107,7 @@ function AnotherComponent() {
<EuiFlexItem>
<EuiButton>
<FormattedMessage
id="app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel"
id="xpack.observability.anotherComponent.thisIsATestButtonLabel"
defaultMessage="This is a test"
/></EuiButton>
</EuiFlexItem>
@ -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 <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -141,7 +144,7 @@ function YetAnotherComponent() {
<div>
<EuiSelect>
<FormattedMessage
id="app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel"
id="xpack.observability.yetAnotherComponent.selectMeSelectLabel"
defaultMessage="Select me"
/></EuiSelect>
</div>
@ -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 <FormattedMessage />. 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 (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
{
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 <FormattedMessage />. 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 (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
{
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 <FormattedMessage />. Use the autofix suggestion or add your own.`,
message: RULE_WARNING_MESSAGE,
},
],
output: `
@ -228,7 +231,7 @@ function TestComponent() {
function TestComponent() {
return (
<SomeChildComponent label={<FormattedMessage id="app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
<SomeChildComponent label={<FormattedMessage id="xpack.observability.testComponent.someChildComponent.thisIsATestLabel" defaultMessage="This is a test" />} />
)
}`,
},
@ -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';

View file

@ -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 <FormattedMessage />. 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 <FormattedMessage />. 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 <FormattedMessage />. 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 = {
`{<FormattedMessage id="${translationIdSuggestion}" defaultMessage="${val}" />}`
),
!hasI18nImportLine && rangeToAddI18nImportLine
? mode === 'replace'
? replaceMode === 'replace'
? fixer.replaceTextRange(rangeToAddI18nImportLine, i18nImportLine)
: fixer.insertTextAfterRange(rangeToAddI18nImportLine, `\n${i18nImportLine}`)
: null,

View file

@ -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 (
<div>{i18n.translate('app_not_found_in_i18nrc.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}</div>
<div>{i18n.translate('xpack.observability.testComponent.div.thisIsATestLabel', { defaultMessage: 'This is a test' })}</div>
)
}`,
},
{
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() {
<EuiPanel>
<EuiFlexGroup>
<EuiFlexItem>
<EuiButton>{i18n.translate('app_not_found_in_i18nrc.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })}</EuiButton>
<EuiButton>{i18n.translate('xpack.observability.anotherComponent.thisIsATestButtonLabel', { defaultMessage: 'This is a test' })}</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
@ -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 (
<div>
<EuiSelect>{i18n.translate('app_not_found_in_i18nrc.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })}</EuiSelect>
<EuiSelect>{i18n.translate('xpack.observability.yetAnotherComponent.selectMeSelectLabel', { defaultMessage: 'Select me' })}</EuiSelect>
</div>
)
}`,
},
{
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 (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
{
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 (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
{
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 (
<SomeChildComponent label={i18n.translate('app_not_found_in_i18nrc.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
<SomeChildComponent label={i18n.translate('xpack.observability.testComponent.someChildComponent.thisIsATestLabel', { defaultMessage: 'This is a test' })} />
)
}`,
},
@ -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';

View file

@ -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,

View file

@ -50,7 +50,10 @@ export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ a
</EuiFlexItem>
<EuiFlexGroup direction="row" justifyContent="flexStart" alignItems="center">
<EuiFlexItem grow={false}>
<EuiLink onClick={() => createConnector()}>
<EuiLink
data-test-subj="serverlessSearchConnectorIngestionPanelSetUpAConnectorLink"
onClick={() => createConnector()}
>
{i18n.translate(
'xpack.serverlessSearch.ingestData.alternativeOptions.setupConnectorLabel',
{
@ -74,6 +77,7 @@ export const ConnectorIngestionPanel: React.FC<{ assetBasePath: string }> = ({ a
<EuiFlexItem grow={false}>
<EuiText size="s">
<EuiLink
data-test-subj="serverlessSearchConnectorIngestionPanelDockerLink"
target="_blank"
href="https://github.com/elastic/connectors-python/blob/main/docs/DOCKER.md"
>