diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index 1fb043a4ec0..060db7211d9 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -111,3 +111,53 @@ const getStyles = (theme: GrafanaTheme2) => ({ ### `theme-token-usage` Used to find all instances of `theme` tokens being used in the codebase and emit the counts as metrics. Should **not** be used as an actual lint rule! + +### `no-untranslated-strings` + +Check if strings are marked for translation. + +```tsx +// Bad ❌ + + Copied + + +// Good ✅ + + Copied + + +``` + +#### Passing variables to translations + +```tsx +// Bad ❌ +const SearchTitle = ({ term }) => ( +
+ Results for {term} +
+); + +//Good ✅ +const SearchTitle = ({ term }) => ( + + Results for {{ term }} + +); +``` + +#### How to translate props or attributes + +Right now, we only check if a string is wrapped up by the `Trans` tag. We currently do not apply this rule to props, attributes or similar, but we also ask for them to be translated with the `t()` function. + +```tsx +// Bad ❌ +; + +// Good ✅ +const placeholder = t('form.username-placeholder', 'Username'); +return ; +``` + +Check more info about how translations work in Grafana in [Internationalization.md](https://github.com/grafana/grafana/blob/main/contribute/internationalization.md) diff --git a/packages/grafana-eslint-rules/index.cjs b/packages/grafana-eslint-rules/index.cjs index d7df5009b06..3ebefd0cae5 100644 --- a/packages/grafana-eslint-rules/index.cjs +++ b/packages/grafana-eslint-rules/index.cjs @@ -1,6 +1,7 @@ const noAriaLabelSelectors = require('./rules/no-aria-label-e2e-selectors.cjs'); const noBorderRadiusLiteral = require('./rules/no-border-radius-literal.cjs'); const noUnreducedMotion = require('./rules/no-unreduced-motion.cjs'); +const noUntranslatedStrings = require('./rules/no-utranslated-strings.cjs'); const themeTokenUsage = require('./rules/theme-token-usage.cjs'); module.exports = { @@ -9,5 +10,6 @@ module.exports = { 'no-aria-label-selectors': noAriaLabelSelectors, 'no-border-radius-literal': noBorderRadiusLiteral, 'theme-token-usage': themeTokenUsage, + 'no-untranslated-strings': noUntranslatedStrings, }, }; diff --git a/packages/grafana-eslint-rules/rules/no-utranslated-strings.cjs b/packages/grafana-eslint-rules/rules/no-utranslated-strings.cjs new file mode 100644 index 00000000000..b1e90de21f4 --- /dev/null +++ b/packages/grafana-eslint-rules/rules/no-utranslated-strings.cjs @@ -0,0 +1,46 @@ +// @ts-check +const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils'); + +const createRule = ESLintUtils.RuleCreator( + (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` +); + +const noUntranslatedStrings = createRule({ + create(context) { + return { + JSXText(node) { + const ancestors = context.getAncestors(); + const isEmpty = !node.value.trim(); + const hasTransAncestor = ancestors.some((ancestor) => { + return ( + ancestor.type === AST_NODE_TYPES.JSXElement && + ancestor.openingElement.type === AST_NODE_TYPES.JSXOpeningElement && + ancestor.openingElement.name.type === AST_NODE_TYPES.JSXIdentifier && + ancestor.openingElement.name.name === 'Trans' + ); + }); + if (!isEmpty && !hasTransAncestor) { + context.report({ + node, + messageId: 'noUntranslatedStrings', + }); + } + }, + }; + }, + name: 'no-untranslated-strings', + meta: { + type: 'suggestion', + docs: { + description: 'Check untranslated strings', + }, + messages: { + noUntranslatedStrings: 'No untranslated strings. Wrap text with ', + }, + schema: [], + }, + defaultOptions: [], +}); + + +module.exports = noUntranslatedStrings;