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