diff --git a/eslint.config.js b/eslint.config.js index 4aa4e15a0f9..a0245a55b48 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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', }, }, diff --git a/packages/grafana-i18n/src/eslint/README.md b/packages/grafana-i18n/src/eslint/README.md index 4fb1abdf8f6..12b3a559f9b 100644 --- a/packages/grafana-i18n/src/eslint/README.md +++ b/packages/grafana-i18n/src/eslint/README.md @@ -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'] }], +// } + +No results found + +// Without basePaths: +// { +// '@grafana/i18n/no-untranslated-strings': ['error'], +// } + +No results found +``` + #### `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. diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs index 7d741720bc2..2b058426d53 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.cjs @@ -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; diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js index 9913ab10f73..684823ae152 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/no-untranslated-strings.test.js @@ -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 = () =>
test
`, filename, - options: [{ forceFix: ['public/app/features/some-feature'] }], + options: [{ forceFix: ['src/some-feature'] }], output: `${TRANS_IMPORT} const Foo = () =>
test
`, errors: [ @@ -821,7 +821,7 @@ const Foo = () => { return
}`, 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 = () =>
test
`, + 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 = () =>
test
`, + errors: [ + { + messageId: 'noUntranslatedStrings', + suggestions: [ + { + messageId: 'wrapWithTrans', + output: `${TRANS_IMPORT} +const Foo = () =>
test
`, + }, + ], + }, + ], + }, + + { + name: 'Auto fixes when options are configured for a different basePath - prop', + code: ` +const Foo = () => { + return
+}`, + 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
+}`, + errors: [ + { + messageId: 'noUntranslatedStringsProp', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + return
+}`, + }, + ], + }, + ], + }, + + { + 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 = () => { diff --git a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs index 070e0c5ee53..8d7d4456ace 100644 --- a/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs +++ b/packages/grafana-i18n/src/eslint/no-untranslated-strings/translation-utils.cjs @@ -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; }