// @ts-check /** @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', [{ forceFix: string[] }]>} RuleDefinition */ const { getNodeValue, getTFixers, getTransFixers, canBeFixed, elementIsTrans, shouldBeFixed, isStringNonAlphanumeric, } = require('./translation-utils.cjs'); 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}` ); /** @type {string[]} */ const propsToCheck = ['label', 'description', 'placeholder', 'aria-label', 'title', 'text', 'tooltip']; /** @type {RuleDefinition} */ const noUntranslatedStrings = createRule({ create(context) { return { JSXAttribute(node) { if (!propsToCheck.includes(String(node.name.name)) || !node.value) { return; } const nodeValue = getNodeValue(node); const isAlphaNumeric = !isStringNonAlphanumeric(nodeValue); const isTemplateLiteral = node.value.type === AST_NODE_TYPES.JSXExpressionContainer && node.value.expression.type === 'TemplateLiteral'; const isUntranslatedProp = (nodeValue.trim() && isAlphaNumeric) || isTemplateLiteral; if (isUntranslatedProp) { const errorShouldBeFixed = shouldBeFixed(context); const errorCanBeFixed = canBeFixed(node, context); return context.report({ node, messageId: 'noUntranslatedStringsProp', fix: errorShouldBeFixed && errorCanBeFixed ? getTFixers(node, context) : undefined, suggest: errorCanBeFixed ? [ { messageId: 'wrapWithT', fix: getTFixers(node, context), }, ] : undefined, }); } }, /** * @param {JSXElement|JSXFragment} node */ 'JSXElement, JSXFragment'(node) { const parent = node.parent; const children = node.children; const untranslatedTextNodes = children.filter((child) => { if (child.type === AST_NODE_TYPES.JSXText) { const nodeValue = child.value.trim(); if (!nodeValue || isStringNonAlphanumeric(nodeValue)) { return false; } const ancestors = context.sourceCode.getAncestors(node); const hasTransAncestor = elementIsTrans(node) || ancestors.some((ancestor) => { return elementIsTrans(ancestor); }); return !hasTransAncestor; } return false; }); const parentHasChildren = parent.type === AST_NODE_TYPES.JSXElement || parent.type === AST_NODE_TYPES.JSXFragment; // We don't want to report if the parent has a text node, // as we'd end up doing it twice. This makes it awkward for us to auto fix const parentHasText = parentHasChildren ? parent.children.some((child) => child.type === AST_NODE_TYPES.JSXText && getNodeValue(child).trim()) : false; if (untranslatedTextNodes.length && !parentHasText) { const errorShouldBeFixed = shouldBeFixed(context); const errorCanBeFixed = canBeFixed(node, context); context.report({ node, messageId: 'noUntranslatedStrings', fix: errorShouldBeFixed && errorCanBeFixed ? getTransFixers(node, context) : undefined, suggest: errorCanBeFixed ? [ { messageId: 'wrapWithTrans', fix: getTransFixers(node, context), }, ] : undefined, }); } }, }; }, name: 'no-untranslated-strings', meta: { type: 'suggestion', hasSuggestions: true, fixable: 'code', docs: { description: 'Check untranslated strings', }, messages: { noUntranslatedStrings: 'No untranslated strings. Wrap text with ', noUntranslatedStringsProp: `No untranslated strings in text props. Wrap text with or use t()`, wrapWithTrans: 'Wrap text with for manual key assignment', wrapWithT: 'Wrap text with t() for manual key assignment', }, schema: [ { type: 'object', properties: { forceFix: { type: 'array', items: { type: 'string', }, uniqueItems: true, }, }, additionalProperties: false, }, ], }, defaultOptions: [{ forceFix: [] }], }); module.exports = noUntranslatedStrings;