Internationalisation: Allow parameterising basePaths for key generation in lint rule (#111999)

* allow parameterising the basePaths for autofixes

* add documentation
This commit is contained in:
Ashley Harrison
2025-10-03 16:38:31 +01:00
committed by GitHub
parent 99784bea14
commit 15ec2f0c17
5 changed files with 136 additions and 12 deletions
+4 -1
View File
@@ -361,7 +361,10 @@ module.exports = [
'**/mock*.{ts,tsx}',
],
rules: {
'@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
'@grafana/i18n/no-untranslated-strings': [
'error',
{ calleesToIgnore: ['^css$', 'use[A-Z].*'], basePaths: ['public/app/features'] },
],
'@grafana/i18n/no-translation-top-level': 'error',
},
},
@@ -10,6 +10,30 @@ Check if strings are marked for translation inside JSX Elements, in certain JSX
### Options
#### `basePaths`
Allows specifying base paths that should be stripped when generating i18n keys. Defaults to `['src']`.
#### Example
```tsx
// For a file located at public/app/features/search/EmptyState.tsx
// Specifying basePaths:
// {
// '@grafana/i18n/no-untranslated-strings': ['error', { basePaths: ['public/app/features'] }],
// }
<Trans i18nKey="search.empty-state.no-results-found">No results found</Trans>
// Without basePaths:
// {
// '@grafana/i18n/no-untranslated-strings': ['error'],
// }
<Trans i18nKey="public.app.features.search.empty-state.no-results-found">No results found</Trans>
```
#### `forceFix`
Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves.
@@ -2,8 +2,8 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[] }]>} RuleDefinition */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[]}]>} RuleContextWithOptions */
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[], basePaths: string[] }]>} RuleDefinition */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */
const {
getNodeValue,
@@ -301,12 +301,19 @@ const noUntranslatedStrings = createRule({
},
default: [],
},
basePaths: {
type: 'array',
items: {
type: 'string',
},
default: ['src'],
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ forceFix: [], calleesToIgnore: [] }],
defaultOptions: [{ forceFix: [], calleesToIgnore: [], basePaths: ['src'] }],
});
module.exports = noUntranslatedStrings;
@@ -2,7 +2,7 @@ import { RuleTester } from 'eslint';
import noUntranslatedStrings from './no-untranslated-strings.cjs';
const filename = 'public/app/features/some-feature/nested/SomeFile.tsx';
const filename = 'src/some-feature/nested/SomeFile.tsx';
const packageName = '@grafana/i18n';
@@ -797,7 +797,7 @@ const Foo = () => {
name: 'Auto fixes when options are configured',
code: `const Foo = () => <div>test</div>`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
options: [{ forceFix: ['src/some-feature'] }],
output: `${TRANS_IMPORT}
const Foo = () => <div><Trans i18nKey="some-feature.foo.test">test</Trans></div>`,
errors: [
@@ -821,7 +821,7 @@ const Foo = () => {
return <div title="foo" />
}`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
options: [{ forceFix: ['src/some-feature'] }],
output: `
${T_IMPORT}
const Foo = () => {
@@ -853,7 +853,94 @@ const Foo = () => {
}
}`,
filename,
options: [{ forceFix: ['public/app/features/some-feature'] }],
options: [{ forceFix: ['src/some-feature'] }],
output: `
${T_IMPORT}
const Foo = () => {
return {
label: t("some-feature.foo.label.test", "test"),
}
}`,
errors: [
{
messageId: 'noUntranslatedStringsProperties',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
return {
label: t("some-feature.foo.label.test", "test"),
}
}`,
},
],
},
],
},
{
name: 'Auto fixes when options are configured for a different basePath',
code: `const Foo = () => <div>test</div>`,
filename: 'public/app/features/some-feature/nested/SomeFile.tsx',
options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }],
output: `${TRANS_IMPORT}
const Foo = () => <div><Trans i18nKey="some-feature.foo.test">test</Trans></div>`,
errors: [
{
messageId: 'noUntranslatedStrings',
suggestions: [
{
messageId: 'wrapWithTrans',
output: `${TRANS_IMPORT}
const Foo = () => <div><Trans i18nKey="some-feature.foo.test">test</Trans></div>`,
},
],
},
],
},
{
name: 'Auto fixes when options are configured for a different basePath - prop',
code: `
const Foo = () => {
return <div title="foo" />
}`,
filename: 'public/app/features/some-feature/nested/SomeFile.tsx',
options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }],
output: `
${T_IMPORT}
const Foo = () => {
return <div title={t("some-feature.foo.title-foo", "foo")} />
}`,
errors: [
{
messageId: 'noUntranslatedStringsProp',
suggestions: [
{
messageId: 'wrapWithT',
output: `
${T_IMPORT}
const Foo = () => {
return <div title={t("some-feature.foo.title-foo", "foo")} />
}`,
},
],
},
],
},
{
name: 'Auto fixes object property for a different basePath',
code: `
const Foo = () => {
return {
label: 'test',
}
}`,
filename: 'public/app/features/some-feature/nested/SomeFile.tsx',
options: [{ forceFix: ['public/app/features/some-feature'], basePaths: ['public/app/features'] }],
output: `
${T_IMPORT}
const Foo = () => {
@@ -7,7 +7,7 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */
/** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[]}]>} RuleContextWithOptions */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[], calleesToIgnore: string[], basePaths: string[]}]>} RuleContextWithOptions */
const { AST_NODE_TYPES } = require('@typescript-eslint/utils');
/**
@@ -150,9 +150,12 @@ function getTDeclaration(node, context) {
*/
function getTranslationPrefix(context) {
const filename = context.filename;
const match = filename.match(/public\/app\/features\/(.+?)\//);
if (match) {
return match[1];
const basePaths = context.options[0]?.basePaths ?? ['src'];
for (const path of basePaths) {
const match = filename.match(new RegExp(`${path}/(.+?)/`));
if (match) {
return match[1];
}
}
return null;
}