diff --git a/eslint.config.js b/eslint.config.js index 884c6c27b8d..6c427e66fb6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -300,9 +300,15 @@ module.exports = [ 'packages/grafana-ui/**/*.{ts,tsx,js,jsx}', ...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{ts,tsx,js,jsx}`), ], - ignores: ['**/*.story.tsx', '**/*.{test,spec}.{ts,tsx}', '**/__mocks__/', 'public/test', '**/spec/**/*.{ts,tsx}'], + ignores: [ + 'public/test/**', + '**/*.{test,spec,story}.{ts,tsx}', + '**/{tests,__mocks__,__tests__,fixtures,spec,mocks}/**', + '**/{test-utils,testHelpers,mocks}.{ts,tsx}', + '**/mock*.{ts,tsx}', + ], rules: { - '@grafana/no-untranslated-strings': 'error', + '@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }], '@grafana/no-translation-top-level': 'error', }, }, diff --git a/jest.config.js b/jest.config.js index 7e269ab19ae..34c7247ca10 100644 --- a/jest.config.js +++ b/jest.config.js @@ -36,7 +36,7 @@ module.exports = { moduleDirectories: ['public', 'node_modules'], roots: ['/public/app', '/public/test', '/packages', '/scripts/tests'], testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$', - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'cjs'], setupFiles: ['jest-canvas-mock', './public/test/jest-setup.ts'], testTimeout: 30000, resolver: `/public/test/jest-resolver.js`, diff --git a/packages/grafana-eslint-rules/README.md b/packages/grafana-eslint-rules/README.md index e57e2ec09a1..1571403244a 100644 --- a/packages/grafana-eslint-rules/README.md +++ b/packages/grafana-eslint-rules/README.md @@ -114,7 +114,47 @@ Used to find all instances of `theme` tokens being used in the codebase and emit ### `no-untranslated-strings` -Check if strings are marked for translation. +Check if strings are marked for translation inside JSX Elements, in certain JSX props, and in certain object properties. + +### Options + +#### `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. + +#### Example: + +```ts +{ + '@grafana/no-untranslated-strings': ['error', { forceFix: ['app/features/some-feature'] }], +} +``` + +#### `calleesToIgnore` + +Allows specifying regexes for methods that should be ignored when checking if object properties are untranslated. + +This is particularly useful to exclude references to properties such as `label` inside `css()` calls. + +#### Example: + +```ts +{ + '@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$'] }], +} + +// The below would not be reported as an error +const foo = css({ + label: 'test', +}); + +// The below would still be reported as an error +const bar = { + label: 'test', +}; +``` + +#### JSXText ```tsx // Bad ❌ @@ -126,7 +166,30 @@ Check if strings are marked for translation. Copied +``` + +#### JSXAttributes + +```tsx +// Bad ❌ +
+ +// Good ✅ +
+``` + +#### Object properties + +```tsx +// Bad ❌ +const someConfig = { + label: 'Some label', +}; +// Good ✅ +const getSomeConfig = () => ({ + label: t('some.key.label', 'Some label'), +}); ``` #### Passing variables to translations @@ -166,8 +229,11 @@ The below props are checked for untranslated strings: - `placeholder` - `aria-label` - `title` +- `subTitle` - `text` - `tooltip` +- `message` +- `name` ```tsx // Bad ❌ diff --git a/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs b/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs index 18d44a34daf..c6f9d0cd177 100644 --- a/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs +++ b/packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs @@ -2,7 +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', [{ forceFix: string[] }]>} RuleDefinition */ +/** @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 */ const { getNodeValue, @@ -20,13 +21,107 @@ const createRule = ESLintUtils.RuleCreator( (name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` ); -/** @type {string[]} */ +/** + * JSX props to check for untranslated strings + */ const propsToCheck = ['content', 'label', 'description', 'placeholder', 'aria-label', 'title', 'text', 'tooltip']; +/** + * Object properties to check for untranslated strings + */ +const propertiesToCheck = [ + 'label', + 'description', + 'placeholder', + 'aria-label', + 'title', + 'subTitle', + 'text', + 'tooltip', + 'message', +]; + /** @type {RuleDefinition} */ const noUntranslatedStrings = createRule({ + /** + * @param {RuleContextWithOptions} context + */ create(context) { + const calleesToIgnore = context.options[0]?.calleesToIgnore || []; + const propertiesRegexes = calleesToIgnore.map((pattern) => { + return new RegExp(pattern); + }); + return { + Property(node) { + const { key, value, parent, computed } = node; + const keyName = (() => { + if (computed) { + return null; + } + if (key.type === AST_NODE_TYPES.Identifier && typeof key.name === 'string') { + return key.name; + } + return null; + })(); + + // Catch cases of default props setting object properties, which would be at the top level + const isAssignmentPattern = parent.parent.type === AST_NODE_TYPES.AssignmentPattern; + + if ( + !keyName || + !propertiesToCheck.includes(keyName) || + parent.type === AST_NODE_TYPES.ObjectPattern || + isAssignmentPattern + ) { + return; + } + + const callExpression = parent.parent.type === AST_NODE_TYPES.CallExpression ? parent.parent.callee : null; + // Check if we're being called by something that we want to ignore + // e.g. css({ label: 'test' }) should be ignored (based on the rule configuration) + if ( + callExpression?.type === AST_NODE_TYPES.Identifier && + propertiesRegexes.some((regex) => regex.test(callExpression.name)) + ) { + return; + } + + const nodeValue = getNodeValue(node); + + const isOnlySymbols = !/[a-zA-Z0-9]/.test(nodeValue); + const isNumeric = !/[a-zA-Z]/.test(nodeValue); + + const isUntranslated = + ((value.type === AST_NODE_TYPES.Literal && nodeValue !== '') || + value.type === AST_NODE_TYPES.TemplateLiteral) && + !isOnlySymbols && + !isNumeric; + + const errorCanBeFixed = canBeFixed(node, context); + const errorShouldBeFixed = shouldBeFixed(context); + if ( + isUntranslated && + // TODO: Remove this check in the future when we've fixed all cases of untranslated properties + // For now, we're only reporting the issues that can be auto-fixed, rather than adding to betterer results + errorCanBeFixed + ) { + context.report({ + node, + messageId: 'noUntranslatedStringsProperties', + fix: errorCanBeFixed && errorShouldBeFixed ? getTFixers(node, context) : undefined, + suggest: errorCanBeFixed + ? [ + { + messageId: 'wrapWithT', + fix: getTFixers(node, context), + }, + ] + : undefined, + }); + } + }, + JSXAttribute(node) { if (!propsToCheck.includes(String(node.name.name)) || !node.value) { return; @@ -163,6 +258,7 @@ const noUntranslatedStrings = createRule({ messages: { noUntranslatedStrings: 'No untranslated strings. Wrap text with ', noUntranslatedStringsProp: `No untranslated strings in text props. Wrap text with or use t()`, + noUntranslatedStringsProperties: `No untranslated strings in object properties. Wrap text with t()`, wrapWithTrans: 'Wrap text with for manual key assignment', wrapWithT: 'Wrap text with t() for manual key assignment', }, @@ -177,12 +273,19 @@ const noUntranslatedStrings = createRule({ }, uniqueItems: true, }, + calleesToIgnore: { + type: 'array', + items: { + type: 'string', + }, + default: [], + }, }, additionalProperties: false, }, ], }, - defaultOptions: [{ forceFix: [] }], + defaultOptions: [{ forceFix: [], calleesToIgnore: [] }], }); module.exports = noUntranslatedStrings; diff --git a/packages/grafana-eslint-rules/rules/translation-utils.cjs b/packages/grafana-eslint-rules/rules/translation-utils.cjs index 3fe5540286d..77de33dbed0 100644 --- a/packages/grafana-eslint-rules/rules/translation-utils.cjs +++ b/packages/grafana-eslint-rules/rules/translation-utils.cjs @@ -5,9 +5,9 @@ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXText} JSXText */ /** @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 */ - const { AST_NODE_TYPES } = require('@typescript-eslint/utils'); /** @@ -77,7 +77,7 @@ function shouldBeFixed(context) { /** * Checks if a node can be fixed automatically - * @param {JSXAttribute|JSXElement|JSXFragment} node The node to check + * @param {JSXAttribute|JSXElement|JSXFragment|Property} node The node to check * @param {RuleContextWithOptions} context * @returns {boolean} Whether the node can be fixed */ @@ -86,16 +86,28 @@ function canBeFixed(node, context) { return false; } + const parentMethod = getParentMethod(node, context); + const isAttribute = node.type === AST_NODE_TYPES.JSXAttribute; + const isProperty = node.type === AST_NODE_TYPES.Property; + const isPropertyOrAttribute = isAttribute || isProperty; + // We can only fix JSX attribute strings that are within a function, // otherwise the `t` function call will be made too early - if (node.type === AST_NODE_TYPES.JSXAttribute) { - const parentMethod = getParentMethod(node, context); - if (!parentMethod) { + if (isPropertyOrAttribute && !parentMethod) { + return false; + } + + // If we're going to try and fix using `t`, and it already exists in the scope, + // but not from `useTranslate`, then we can't fix/provide a suggestion + if (isPropertyOrAttribute && parentMethod) { + const hasTDeclaration = getTDeclaration(parentMethod, context); + const hasUseTranslateDeclaration = methodHasUseTranslate(parentMethod, context); + if (hasTDeclaration && !hasUseTranslateDeclaration) { return false; } - if (node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) { - return isStringLiteral(node.value.expression); - } + } + if (isAttribute && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) { + return isStringLiteral(node.value.expression); } const values = @@ -130,7 +142,7 @@ function canBeFixed(node, context) { */ function getTranslationPrefix(context) { const filename = context.filename; - const match = filename.match(/public\/app\/features\/([^/]+)/); + const match = filename.match(/public\/app\/features\/(.+?)\//); if (match) { return match[1]; } @@ -139,7 +151,7 @@ function getTranslationPrefix(context) { /** * Gets the i18n key for a node - * @param {JSXAttribute|JSXText} node The node + * @param {JSXAttribute|JSXText|Property} node The node * @param {RuleContextWithOptions} context * @returns {string} The i18n key */ @@ -148,6 +160,10 @@ const getI18nKey = (node, context) => { const stringValue = getNodeValue(node); const componentNames = getComponentNames(node, context); + + const propertyName = + node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier ? String(node.key.name) : null; + const words = stringValue .trim() .replace(/[^\a-zA-Z\s]/g, '') @@ -185,14 +201,14 @@ const getI18nKey = (node, context) => { kebabString = [potentialId, propName, kebabString].filter(Boolean).join('-'); } - const fullPrefix = [prefixFromFilePath, ...componentNames, kebabString].filter(Boolean).join('.'); + const fullPrefix = [prefixFromFilePath, ...componentNames, propertyName, kebabString].filter(Boolean).join('.'); return fullPrefix; }; /** * Gets component names from ancestors - * @param {JSXAttribute|JSXText} node The node + * @param {JSXAttribute|JSXText|Property} node The node * @param {RuleContextWithOptions} context * @returns {string[]} The component names */ @@ -218,13 +234,22 @@ function getComponentNames(node, context) { } /** - * Checks if a method has a variable declaration of `t` + * For a given node, check the scope and find a variable declaration of `t` + * @param {Node} node + * @param {RuleContextWithOptions} context + */ +function getTDeclaration(node, context) { + return context.sourceCode.getScope(node).variables.find((v) => v.name === 't'); +} + +/** + * Checks if a node has a variable declaration of `t` * that came from a `useTranslate` call - * @param {Node} method The node + * @param {Node} node The node * @param {RuleContextWithOptions} context */ -function methodHasUseTranslate(method, context) { - const tDeclaration = method ? context.sourceCode.getScope(method).variables.find((v) => v.name === 't') : null; +function methodHasUseTranslate(node, context) { + const tDeclaration = getTDeclaration(node, context); return ( tDeclaration && tDeclaration.defs.find((definition) => { @@ -243,7 +268,7 @@ function methodHasUseTranslate(method, context) { /** * Gets the import fixer for a node - * @param {JSXElement|JSXFragment|JSXAttribute} node + * @param {JSXElement|JSXFragment|JSXAttribute|Property} node * @param {RuleFixer} fixer The fixer * @param {'Trans'|'t'|'useTranslate'} importName The member to import from either `@grafana/i18n` or `@grafana/i18n/internal` * @param {RuleContextWithOptions} context @@ -337,7 +362,7 @@ const firstCharIsUpper = (str) => { }; /** - * @param {JSXAttribute} node + * @param {JSXAttribute|Property} node * @param {RuleFixer} fixer * @param {RuleContextWithOptions} context * @returns {import('@typescript-eslint/utils/ts-eslint').RuleFix|undefined} The fix @@ -380,9 +405,10 @@ const getUseTranslateFixer = (node, fixer, context) => { if (!returnStatementIsJsx) { return; } + const tDeclarationExists = getTDeclaration(parentMethod, context); const useTranslateExists = methodHasUseTranslate(parentMethod, context); - if (useTranslateExists) { + if (tDeclarationExists && useTranslateExists) { return; } @@ -396,7 +422,7 @@ const getUseTranslateFixer = (node, fixer, context) => { }; /** - * @param {JSXAttribute} node + * @param {JSXAttribute|Property} node * @param {RuleContextWithOptions} context * @returns {(fixer: RuleFixer) => import('@typescript-eslint/utils/ts-eslint').RuleFix[]} */ @@ -406,9 +432,13 @@ const getTFixers = (node, context) => (fixer) => { const value = getNodeValue(node); const wrappingQuotes = value.includes('"') ? "'" : '"'; - fixes.push( - fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`) - ); + if (node.type === AST_NODE_TYPES.Property) { + fixes.push(fixer.replaceText(node.value, `t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})`)); + } else { + fixes.push( + fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`) + ); + } // Check if we need to add `useTranslate` to the node const useTranslateFixer = getUseTranslateFixer(node, fixer, context); @@ -428,11 +458,19 @@ const getTFixers = (node, context) => (fixer) => { /** * Gets the value of a node - * @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild} node The node + * @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild|Property} node The node * @returns {string} The node value */ function getNodeValue(node) { - if (node.type === AST_NODE_TYPES.JSXAttribute && node.value?.type === AST_NODE_TYPES.Literal) { + if ( + (node.type === AST_NODE_TYPES.JSXAttribute || node.type === AST_NODE_TYPES.Property) && + node.value?.type === AST_NODE_TYPES.Literal + ) { + // TODO: Update this to return bool/number values and handle the type issues elsewhere + // For now, we'll just return an empty string so we consider any numbers or booleans as not being issues + if (typeof node.value.value === 'boolean' || typeof node.value.value === 'number') { + return ''; + } return String(node.value.value) || ''; } if (node.type === AST_NODE_TYPES.JSXText) { diff --git a/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js b/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js index b57f97ddfc3..de6d3e4a608 100644 --- a/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js +++ b/packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js @@ -14,7 +14,7 @@ RuleTester.setDefaultConfig({ }, }); -const filename = 'public/app/features/some-feature/SomeFile.tsx'; +const filename = 'public/app/features/some-feature/nested/SomeFile.tsx'; const packageName = '@grafana/i18n'; @@ -31,6 +31,7 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, { { name: 'Text in Trans component', code: `const Foo = () => Translated text`, + filename, }, { name: 'Text in Trans component with whitespace/JSXText elements', @@ -38,58 +39,72 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, { Translated text `, + filename, }, { name: 'Empty component', code: `
`, + filename, }, { name: 'Text using t() function', code: `
{t('translated.key', 'Translated text')}
`, + filename, }, { name: 'Prop using t() function', code: `
`, + filename, }, { name: 'Empty string prop', code: `
`, + filename, }, { name: 'Prop using boolean', code: `
`, + filename, }, { name: 'Prop using number', code: `
`, + filename, }, { name: 'Prop using null', code: `
`, + filename, }, { name: 'Prop using undefined', code: `
`, + filename, }, { name: 'Variable interpolation', code: `
{variable}
`, + filename, }, { name: 'Entirely non-alphanumeric text (prop)', code: `
`, + filename, }, { name: 'Entirely non-alphanumeric text', code: `
-
`, + filename, }, { name: 'Non-alphanumeric siblings', code: `
({variable})
`, + filename, }, { name: "Ternary in an attribute we don't care about", code: `
`, + filename, }, { name: 'Ternary with falsy strings', @@ -98,6 +113,76 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, { { name: 'Ternary with no strings', code: `
`, + filename, + }, + { + name: 'Object property', + code: `const getThing = () => ({ + label: t('test', 'Test'), + })`, + filename, + }, + { + // Ideally we would catch this, but test case is to ensure that + // we aren't reporting an error + name: 'Object property using variable', + code: ` + const getThing = () => { + const foo = 'test'; + const thing = { + label: foo, + } + }`, + filename, + }, + { + name: 'Object property using dynamic/other keys', + code: ` + const getThing = () => { + const foo = 'label'; + const label = 'not-a-label'; + const thing = { + 1: 'a', + // We can't easily check for computed keys, so for now don't worry about this case + [foo]: 'test', + + // This is dumb, but we need to check that we don't confuse + // the name of the variable for the key name + [label]: 'test', + ['title']: 'test', + } + }`, + filename, + }, + { + name: 'Label reference inside `css` call', + code: `const getThing = () => { + const thing = css({ + label: 'red', + }); + }`, + options: [{ calleesToIgnore: ['somethingelse', '^css$'] }], + filename, + }, + { + name: 'Object property value that is a boolean or number', + code: `const getThing = () => { + const thing = { + label: true, + title: 1 + }; + }`, + filename, + }, + { + name: 'Object property at top level', + code: `const thing = { label: 'test' }`, + filename, + }, + { + name: 'Object property in default props', + code: `const Foo = ({ foobar = {label: 'test'} }) =>
{foobar.label}
`, + filename, }, ], invalid: [ @@ -667,6 +752,97 @@ const Foo = () => { ], }, + { + name: 'Untranslated object property', + code: ` +const Foo = () => { + const thing = { + label: 'test', + } + + return
{thing.label}
; +}`, + filename, + errors: [ + { + messageId: 'noUntranslatedStringsProperties', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${USE_TRANSLATE_IMPORT} +const Foo = () => { + const { t } = useTranslate(); +const thing = { + label: t(\"some-feature.foo.thing.label.test\", \"test\"), + } + + return
{thing.label}
; +}`, + }, + ], + }, + ], + }, + + { + name: 'Untranslated object property with existing import', + code: ` +${T_IMPORT} +const Foo = () => { + const thing = { + label: 'test', + } +}`, + filename, + errors: [ + { + messageId: 'noUntranslatedStringsProperties', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + const thing = { + label: t(\"some-feature.foo.thing.label.test\", \"test\"), + } +}`, + }, + ], + }, + ], + }, + + { + name: 'Untranslated object property with calleesToIgnore', + code: ` +const Foo = () => { + const thing = doAThing({ + label: 'test', + }) +}`, + options: [{ calleesToIgnore: ['doSomethingElse'] }], + filename, + errors: [ + { + messageId: 'noUntranslatedStringsProperties', + suggestions: [ + { + messageId: 'wrapWithT', + output: ` +${T_IMPORT} +const Foo = () => { + const thing = doAThing({ + label: t(\"some-feature.foo.thing.label.test\", \"test\"), + }) +}`, + }, + ], + }, + ], + }, + /** * AUTO FIXES */ @@ -723,6 +899,42 @@ return
], }, + { + name: 'Auto fixes object property', + code: ` +const Foo = () => { + return { + label: 'test', + } +}`, + filename, + options: [{ forceFix: ['public/app/features/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"), + } +}`, + }, + ], + }, + ], + }, + /** * UNFIXABLE CASES */ @@ -824,5 +1036,34 @@ const Foo = () => { filename, errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }], }, + + { + name: 'Cannot fix if `t` already exists from somewhere else', + code: ` +const Foo = () => { + const t = () => 'something else'; + return ( +
+ ) +}`, + filename, + errors: [{ messageId: 'noUntranslatedStringsProp' }], + }, + + // TODO: Enable test once all top-level issues have been fixed + // and rule is enabled again + // { + // name: 'Object property at top level scope', + // code: ` + // const thing = { + // label: 'test', + // }`, + // filename, + // errors: [ + // { + // messageId: 'noUntranslatedStringsProperties', + // }, + // ], + // }, ], }); diff --git a/public/app/features/actions/ActionVariablesEditor.tsx b/public/app/features/actions/ActionVariablesEditor.tsx index 7c264b53a6c..9a5bf423f5a 100644 --- a/public/app/features/actions/ActionVariablesEditor.tsx +++ b/public/app/features/actions/ActionVariablesEditor.tsx @@ -54,7 +54,13 @@ export const ActionVariablesEditor = ({ value, onChange }: Props) => { const isAddButtonDisabled = name === '' || key === ''; - const variableTypeOptions: ComboboxOption[] = [{ label: 'string', value: ActionVariableType.String }]; + const variableTypeOptions: ComboboxOption[] = [ + { + // eslint-disable-next-line @grafana/no-untranslated-strings + label: 'string', + value: ActionVariableType.String, + }, + ]; return (
diff --git a/public/app/features/admin/AdminEditOrgPage.tsx b/public/app/features/admin/AdminEditOrgPage.tsx index 7bcc7f26fe5..b00faf82e0c 100644 --- a/public/app/features/admin/AdminEditOrgPage.tsx +++ b/public/app/features/admin/AdminEditOrgPage.tsx @@ -86,7 +86,10 @@ const AdminEditOrgPage = () => { const pageNav: NavModelItem = { text: orgState?.value?.name ?? '', icon: 'shield', - subTitle: 'Manage settings and user roles for an organization.', + subTitle: t( + 'admin.admin-edit-org-page.page-nav.subTitle.manage-settings-roles-organization', + 'Manage settings and user roles for an organization.' + ), }; return ( diff --git a/public/app/features/admin/UserAdminPage.tsx b/public/app/features/admin/UserAdminPage.tsx index 9af0af250b3..b0befdb403e 100644 --- a/public/app/features/admin/UserAdminPage.tsx +++ b/public/app/features/admin/UserAdminPage.tsx @@ -3,6 +3,7 @@ import { connect, ConnectedProps } from 'react-redux'; import { useParams } from 'react-router-dom-v5-compat'; import { NavModelItem } from '@grafana/data'; +import { useTranslate } from '@grafana/i18n'; import { featureEnabled } from '@grafana/runtime'; import { Stack } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; @@ -59,6 +60,7 @@ export const UserAdminPage = ({ revokeAllSessions, syncLdapUser, }: Props) => { + const { t } = useTranslate(); const { id = '' } = useParams(); useEffect(() => { loadAdminUserPage(id); @@ -123,7 +125,10 @@ export const UserAdminPage = ({ const pageNav: NavModelItem = { text: user?.login ?? '', icon: 'shield', - subTitle: 'Manage settings for an individual user.', + subTitle: t( + 'admin.user-admin-page.page-nav.subTitle.manage-settings-for-an-individual-user', + 'Manage settings for an individual user.' + ), }; return ( diff --git a/public/app/features/admin/UserListAdminPage.tsx b/public/app/features/admin/UserListAdminPage.tsx index d29a0f2b1a3..3737700b255 100644 --- a/public/app/features/admin/UserListAdminPage.tsx +++ b/public/app/features/admin/UserListAdminPage.tsx @@ -88,8 +88,11 @@ const UserListAdminPageUnConnected = ({ /> changeFilter({ name: 'activeLast30Days', value })} value={filters.find((f) => f.name === 'activeLast30Days')?.value} diff --git a/public/app/features/admin/UserListAnonymousPage.tsx b/public/app/features/admin/UserListAnonymousPage.tsx index cdcfe7d4d7c..04f5557634b 100644 --- a/public/app/features/admin/UserListAnonymousPage.tsx +++ b/public/app/features/admin/UserListAnonymousPage.tsx @@ -70,7 +70,15 @@ const UserListAnonymousDevicesPageUnConnected = ({ onChange={changeAnonQuery} /> changeFilter({ name: 'activeLast30Days', value })} value={filters.find((f) => f.name === 'activeLast30Days')?.value} className={styles.filter} diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index efa0180968e..3eaee4e8e50 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -1,5 +1,6 @@ import { isArray, reduce } from 'lodash'; +import { t } from '@grafana/i18n/internal'; import { IconName } from '@grafana/ui'; import { QueryPart, QueryPartDef } from 'app/features/alerting/state/query_part'; @@ -114,42 +115,42 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel { case 'normal': case 'ok': { return { - text: 'OK', + text: t('alerting.get-state-display-model.text.ok', 'OK'), iconClass: 'heart', stateClass: 'alert-state-ok', }; } case 'alerting': { return { - text: 'ALERTING', + text: t('alerting.get-state-display-model.text.alerting', 'ALERTING'), iconClass: 'heart-break', stateClass: 'alert-state-critical', }; } case 'nodata': { return { - text: 'NO DATA', + text: t('alerting.get-state-display-model.text.no-data', 'NO DATA'), iconClass: 'question-circle', stateClass: 'alert-state-warning', }; } case 'paused': { return { - text: 'PAUSED', + text: t('alerting.get-state-display-model.text.paused', 'PAUSED'), iconClass: 'pause', stateClass: 'alert-state-paused', }; } case 'pending': { return { - text: 'PENDING', + text: t('alerting.get-state-display-model.text.pending', 'PENDING'), iconClass: 'hourglass', stateClass: 'alert-state-warning', }; } case 'recovering': { return { - text: 'RECOVERING', + text: t('alerting.get-state-display-model.text.recovering', 'RECOVERING'), iconClass: 'hourglass', stateClass: 'alert-state-warning', }; @@ -157,7 +158,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel { case 'firing': { return { - text: 'FIRING', + text: t('alerting.get-state-display-model.text.firing', 'FIRING'), iconClass: 'fire', stateClass: '', }; @@ -165,7 +166,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel { case 'inactive': { return { - text: 'INACTIVE', + text: t('alerting.get-state-display-model.text.inactive', 'INACTIVE'), iconClass: 'check', stateClass: '', }; @@ -173,7 +174,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel { case 'error': { return { - text: 'ERROR', + text: t('alerting.get-state-display-model.text.error', 'ERROR'), iconClass: 'heart-break', stateClass: 'alert-state-critical', }; @@ -182,7 +183,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel { case 'unknown': default: { return { - text: 'UNKNOWN', + text: t('alerting.get-state-display-model.text.unknown', 'UNKNOWN'), iconClass: 'question-circle', stateClass: '.alert-state-paused', }; diff --git a/public/app/features/alerting/unified/AlertingNotEnabled.tsx b/public/app/features/alerting/unified/AlertingNotEnabled.tsx index 9250fafed2c..d9e202e20c0 100644 --- a/public/app/features/alerting/unified/AlertingNotEnabled.tsx +++ b/public/app/features/alerting/unified/AlertingNotEnabled.tsx @@ -1,17 +1,22 @@ import { NavModel } from '@grafana/data'; +import { useTranslate } from '@grafana/i18n'; import { Page } from 'app/core/components/Page/Page'; import { withPageErrorBoundary } from './withPageErrorBoundary'; function FeatureTogglePage() { + const { t } = useTranslate(); const navModel: NavModel = { node: { - text: 'Alerting is not enabled', + text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'), hideFromBreadcrumbs: true, - subTitle: 'To enable alerting, enable it in the Grafana config', + subTitle: t( + 'alerting.feature-toggle-page.nav-model.subTitle.enable-alerting-grafana-config', + 'To enable alerting, enable it in the Grafana config' + ), }, main: { - text: 'Alerting is not enabled', + text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'), }, }; diff --git a/public/app/features/alerting/unified/NewSilencePage.tsx b/public/app/features/alerting/unified/NewSilencePage.tsx index 2f2dc4e79cd..9cabbcdc4b2 100644 --- a/public/app/features/alerting/unified/NewSilencePage.tsx +++ b/public/app/features/alerting/unified/NewSilencePage.tsx @@ -1,5 +1,6 @@ import { useLocation } from 'react-router-dom-v5-compat'; +import { useTranslate } from '@grafana/i18n'; import { defaultsFromQuery, getDefaultSilenceFormValues, @@ -37,10 +38,14 @@ const SilencesEditorComponent = () => { }; function NewSilencePage() { + const { t } = useTranslate(); const pageNav = { id: 'silence-new', - text: 'Silence alert rule', - subTitle: 'Configure silences to stop notifications from a particular alert rule', + text: t('alerting.new-silence-page.page-nav.text.silence-alert-rule', 'Silence alert rule'), + subTitle: t( + 'alerting.new-silence-page.page-nav.subTitle.configure-silences-notifications-particular-alert', + 'Configure silences to stop notifications from a particular alert rule' + ), }; return ( diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx index 18409638f32..c5cf0c4c238 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { useMemo } from 'react'; import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { Trans, useTranslate } from '@grafana/i18n'; import { useStyles2 } from '@grafana/ui'; import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; @@ -22,13 +22,14 @@ type AlertGroupAlertsTableColumnProps = DynamicTableColumnProps; export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => { + const { t } = useTranslate(); const styles = useStyles2(getStyles); const columns = useMemo( (): AlertGroupAlertsTableColumnProps[] => [ { id: 'state', - label: 'Notification state', + label: t('alerting.alert-group-alerts-table.columns.label.notification-state', 'Notification state'), // eslint-disable-next-line react/display-name renderCell: ({ data: alert }) => ( <> @@ -52,13 +53,13 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) }, { id: 'labels', - label: 'Instance labels', + label: t('alerting.alert-group-alerts-table.columns.label.instance-labels', 'Instance labels'), // eslint-disable-next-line react/display-name renderCell: ({ data: { labels } }) => , size: 1, }, ], - [styles] + [styles, t] ); const items = useMemo( diff --git a/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx b/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx index 614f74eaa2a..8f98ca9beb1 100644 --- a/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx +++ b/public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx @@ -1,13 +1,16 @@ +import { useTranslate } from '@grafana/i18n'; + import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; function ExportNewGrafanaRulePage() { + const { t } = useTranslate(); return ( diff --git a/public/app/features/alerting/unified/components/export/FileExportPreview.tsx b/public/app/features/alerting/unified/components/export/FileExportPreview.tsx index 1b8b00b63b2..009cee2c529 100644 --- a/public/app/features/alerting/unified/components/export/FileExportPreview.tsx +++ b/public/app/features/alerting/unified/components/export/FileExportPreview.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { Trans } from '@grafana/i18n'; +import { Trans, useTranslate } from '@grafana/i18n'; import { Alert, Button, ClipboardButton, CodeEditor, TextLink, useStyles2 } from '@grafana/ui'; import { ExportFormats, ExportProvider, ProvisioningType, allGrafanaExportProviders } from './providers'; @@ -92,11 +92,15 @@ const fileExportPreviewStyles = (theme: GrafanaTheme2) => ({ }); function FileExportInlineDocumentation({ exportProvider }: { exportProvider: ExportProvider }) { + const { t } = useTranslate(); const { name, type } = exportProvider; const exportInlineDoc: Record = { file: { - title: 'File-provisioning format', + title: t( + 'alerting.file-export-inline-documentation.export-inline-doc.title.fileprovisioning-format', + 'File-provisioning format' + ), component: ( {{ name }} format is only valid for File Provisioning.{' '} @@ -110,7 +114,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp ), }, api: { - title: 'API-provisioning format', + title: t( + 'alerting.file-export-inline-documentation.export-inline-doc.title.apiprovisioning-format', + 'API-provisioning format' + ), component: ( {{ name }} format is only valid for API Provisioning.{' '} @@ -124,7 +131,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp ), }, terraform: { - title: 'Terraform-provisioning format', + title: t( + 'alerting.file-export-inline-documentation.export-inline-doc.title.terraformprovisioning-format', + 'Terraform-provisioning format' + ), component: ( {{ name }} format is only valid for Terraform Provisioning.{' '} diff --git a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx index cef9bf81713..5d7bf9d51b8 100644 --- a/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx +++ b/public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx @@ -93,11 +93,12 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier } } function GrafanaModifyExportPage() { + const { t } = useTranslate(); return ( { name="targetDatasourceUID" control={control} rules={{ - required: { value: true, message: 'Please select a target data source' }, + required: { + value: true, + message: t( + 'alerting.import-from-dsrules.message.please-select-a-target-data-source', + 'Please select a target data source' + ), + }, }} /> diff --git a/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx b/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx index b87bb96c9cf..6de37a665fe 100644 --- a/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx @@ -1,5 +1,6 @@ import { Navigate } from 'react-router-dom-v5-compat'; +import { useTranslate } from '@grafana/i18n'; import { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings'; import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams'; @@ -39,10 +40,14 @@ const EditTimingRoute = () => { }; function EditMuteTimingPage() { + const { t } = useTranslate(); return ( diff --git a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx index 302f47dd73c..c93113bcbf6 100644 --- a/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { Trans, useTranslate } from '@grafana/i18n'; +import { t } from '@grafana/i18n/internal'; import { Alert, Button, LinkButton, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui'; import { MuteTimingActionsButtons } from 'app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons'; import { @@ -149,7 +150,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) { const columns: Array> = [ { id: 'name', - label: 'Name', + label: t('alerting.use-columns.columns.label.name', 'Name'), renderCell: function renderName({ data }) { return (
@@ -164,7 +165,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) { }, { id: 'timeRange', - label: 'Time range', + label: t('alerting.use-columns.columns.label.time-range', 'Time range'), renderCell: ({ data }) => { return renderTimeIntervals(data); }, @@ -174,7 +175,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) { if (showActions) { columns.push({ id: 'actions', - label: 'Actions', + label: t('alerting.use-columns.label.actions', 'Actions'), alignColumn: 'end', renderCell: ({ data }) => ( diff --git a/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx b/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx index 14959eaa4cc..ee2ded9c0db 100644 --- a/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx +++ b/public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx @@ -1,13 +1,19 @@ +import { useTranslate } from '@grafana/i18n'; + import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import MuteTimingForm from './MuteTimingForm'; function NewMuteTimingPage() { + const { t } = useTranslate(); return ( diff --git a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx index aabaf0e06ef..5242e197578 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx @@ -70,7 +70,9 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi )} control={control} name="receiver" - rules={{ required: { value: true, message: 'Required.' } }} + rules={{ + required: { value: true, message: t('alerting.am-root-route-form.message.required', 'Required.') }, + }} /> or diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index 6ed5b594596..e577ed8187a 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -133,7 +133,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults defaultValue={field.operator} control={control} name={`object_matchers.${index}.operator`} - rules={{ required: { value: true, message: 'Required.' } }} + rules={{ + required: { + value: true, + message: t('alerting.am-routes-expanded-form.message.required', 'Required.'), + }, + }} /> | undefined>(undefined); @@ -138,7 +139,10 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe const templateOptions: Array> = [ { - label: 'Select notification template', + label: t( + 'alerting.template-selector.template-options.label.select-notification-template', + 'Select notification template' + ), ariaLabel: 'Select notification template', value: 'Existing', description: `Select an existing notification template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save saves your changes to the selected template.' : ''}`, @@ -156,7 +160,6 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe setCustomTemplateValue(getUseTemplateText(template.value.name)); } }, [template]); - const { t } = useTranslate(); function onCustomTemplateChange(customInput: string) { setCustomTemplateValue(customInput); diff --git a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx index 0f634d90df8..59aa5d59f11 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx @@ -66,7 +66,10 @@ export const AlertRuleNameAndMetric = () => { id="name" width={38} {...register('name', { - required: { value: true, message: 'Must enter a name' }, + required: { + value: true, + message: t('alerting.alert-rule-name-and-metric.message.must-enter-a-name', 'Must enter a name'), + }, pattern: isCloudRecordingRule ? recordingRuleNameValidationPattern(RuleFormType.cloudRecording) : undefined, @@ -89,7 +92,13 @@ export const AlertRuleNameAndMetric = () => { id="metric" width={38} {...register('metric', { - required: { value: true, message: 'Must enter a metric name' }, + required: { + value: true, + message: t( + 'alerting.alert-rule-name-and-metric.message.must-enter-a-metric-name', + 'Must enter a metric name' + ), + }, pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording), })} aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')} @@ -130,7 +139,13 @@ export const AlertRuleNameAndMetric = () => { name="targetDatasourceUid" control={control} rules={{ - required: { value: true, message: 'Please select a data source' }, + required: { + value: true, + message: t( + 'alerting.alert-rule-name-and-metric.message.please-select-a-data-source', + 'Please select a data source' + ), + }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/AnnotationHeaderField.tsx b/public/app/features/alerting/unified/components/rule-editor/AnnotationHeaderField.tsx index 7f6ed397714..da5089d27f3 100644 --- a/public/app/features/alerting/unified/components/rule-editor/AnnotationHeaderField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/AnnotationHeaderField.tsx @@ -1,5 +1,6 @@ import { Controller, FieldArrayWithId, useFormContext } from 'react-hook-form'; +import { useTranslate } from '@grafana/i18n'; import { Stack, Text } from '@grafana/ui'; import { RuleFormValues } from '../../types/rule-form'; @@ -20,6 +21,7 @@ const AnnotationHeaderField = ({ index: number; labelId: string; }) => { + const { t } = useTranslate(); const { control } = useFormContext(); return ( @@ -56,7 +58,12 @@ const AnnotationHeaderField = ({ ); }} control={control} - rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }} + rules={{ + required: { + value: !!annotations[index]?.value, + message: t('alerting.annotation-header-field.message.required', 'Required.'), + }, + }} /> } diff --git a/public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx index c7b2cae97db..ed256a109d7 100644 --- a/public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx @@ -40,7 +40,15 @@ export const CloudEvaluationBehavior = () => {
diff --git a/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx index 01d408d9073..8a2ca875bca 100644 --- a/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx @@ -69,7 +69,10 @@ export function FolderSelector() { )} name="folder" rules={{ - required: { value: true, message: 'Select a folder' }, + required: { + value: true, + message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'), + }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx index 99eb44dc4f7..0ced69efa48 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx @@ -5,7 +5,7 @@ import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } fr import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; -import { Trans } from '@grafana/i18n'; +import { Trans, useTranslate } from '@grafana/i18n'; import { t } from '@grafana/i18n/internal'; import { Box, @@ -74,7 +74,7 @@ const sortByLabel = (a: SelectableValue, b: SelectableValue) => const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({ required: { value: true, - message: 'Required.', + message: t('alerting.for-validation-options.message.required', 'Required.'), }, validate: (value) => { // parsePrometheusDuration does not allow 0 but does allow 0s @@ -118,6 +118,7 @@ export function GrafanaEvaluationBehaviorStep({ existing: boolean; enableProvisionedGroups: boolean; }) { + const { t } = useTranslate(); const styles = useStyles2(getStyles); const [showErrorHandling, setShowErrorHandling] = useState(false); @@ -237,7 +238,13 @@ export function GrafanaEvaluationBehaviorStep({ name="group" control={control} rules={{ - required: { value: true, message: 'Must enter a group name' }, + required: { + value: true, + message: t( + 'alerting.grafana-evaluation-behavior-step.message.must-enter-a-group-name', + 'Must enter a group name' + ), + }, }} /> @@ -380,7 +387,13 @@ export function GrafanaEvaluationBehaviorStep({ )} id="missing-series-resolve" {...register('missingSeriesEvalsToResolve', { - pattern: { value: /^\d+$/, message: 'Must be a positive integer.' }, + pattern: { + value: /^\d+$/, + message: t( + 'alerting.grafana-evaluation-behavior-step.message.must-be-a-positive-integer', + 'Must be a positive integer.' + ), + }, })} width={21} /> @@ -402,6 +415,7 @@ function EvaluationGroupCreationModal({ onCreate: (group: string, evaluationInterval: string) => void; groupfoldersForGrafana?: RulerRulesConfigDTO | null; }): React.ReactElement { + const { t } = useTranslate(); const styles = useStyles2(getStyles); const { watch } = useFormContext(); @@ -477,7 +491,12 @@ function EvaluationGroupCreationModal({ autoFocus={true} id={evaluationGroupNameId} placeholder={t('alerting.evaluation-group-creation-modal.placeholder-enter-a-name', 'Enter a name')} - {...register('group', { required: { value: true, message: 'Required.' } })} + {...register('group', { + required: { + value: true, + message: t('alerting.evaluation-group-creation-modal.message.required', 'Required.'), + }, + })} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx index 9b6779b0d67..db8fee5792e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx @@ -73,7 +73,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => { name="namespace" control={control} rules={{ - required: { value: true, message: 'Required.' }, + required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') }, }} /> @@ -105,7 +105,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => { name="group" control={control} rules={{ - required: { value: true, message: 'Required.' }, + required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx index c7b0411b0d4..409a2facf57 100644 --- a/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx @@ -153,14 +153,27 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => { * */ function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) { + const { t } = useTranslate(); const { watch, setValue } = useFormContext(); const styles = useStyles2(getStyles); const [manualRouting] = watch(['manualRouting']); const routingOptions = [ - { label: 'Select contact point', value: RoutingOptions.ContactPoint }, - { label: 'Use notification policy', value: RoutingOptions.NotificationPolicy }, + { + label: t( + 'alerting.manual-and-automatic-routing.routing-options.label.select-contact-point', + 'Select contact point' + ), + value: RoutingOptions.ContactPoint, + }, + { + label: t( + 'alerting.manual-and-automatic-routing.routing-options.label.use-notification-policy', + 'Use notification policy' + ), + value: RoutingOptions.NotificationPolicy, + }, ]; const onRoutingOptionChange = (option: RoutingOptions) => { diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx index e112fd3f2f2..53bc7df4493 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx @@ -78,7 +78,10 @@ export function ContactPointSelector({ alertManager, onSelectContactPoint }: Con rules={{ required: { value: true, - message: 'Contact point is required.', + message: t( + 'alerting.contact-point-selector.message.contact-point-is-required', + 'Contact point is required.' + ), }, }} control={control} diff --git a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx index a690fd34ea3..a05765cff55 100644 --- a/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx @@ -116,6 +116,7 @@ export function useCombinedLabels( labelsInSubform: Array<{ key: string; value: string }>, selectedKey: string ) { + const { t } = useTranslate(); // ------- Get labels keys and their values from existing alerts const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName); // ------- Get only the keys from the ops labels, as we will fetch the values for the keys once the key is selected. @@ -143,12 +144,12 @@ export function useCombinedLabels( // create two groups of labels, one for ops and one for custom const groupedOptions = [ { - label: 'From alerts', + label: t('alerting.use-combined-labels.grouped-options.label.from-alerts', 'From alerts'), options: keysFromExistingAlerts, expanded: true, }, { - label: 'From system', + label: t('alerting.use-combined-labels.grouped-options.label.from-system', 'From system'), options: keysFromGopsLabels, expanded: true, }, @@ -372,7 +373,10 @@ export const LabelsWithoutSuggestions: FC = () => { > { > diff --git a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx index 31c9ede6b6b..76cee819cb0 100644 --- a/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx @@ -544,7 +544,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod }} control={control} rules={{ - required: { value: true, message: 'A valid expression is required' }, + required: { + value: true, + message: t( + 'alerting.query-and-expressions-step.message.a-valid-expression-is-required', + 'A valid expression is required' + ), + }, }} /> diff --git a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx index 0d5e4094f48..41b5ca49e5f 100644 --- a/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx +++ b/public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx @@ -5,6 +5,7 @@ import { useMeasure } from 'react-use'; import { NavModelItem, UrlQueryValue } from '@grafana/data'; import { Trans, useTranslate } from '@grafana/i18n'; +import { t } from '@grafana/i18n/internal'; import { Alert, LinkButton, @@ -205,14 +206,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => { {truncatedUrl} ); metadata.push({ - label: 'Runbook URL', + label: t('alerting.create-metadata.label.runbook-url', 'Runbook URL'), value: valueToAdd, }); } if (hasDashboardAndPanel) { metadata.push({ - label: 'Dashboard and panel', + label: t('alerting.create-metadata.label.dashboard-and-panel', 'Dashboard and panel'), value: ( { }); } else if (hasDashboard) { metadata.push({ - label: 'Dashboard', + label: t('alerting.create-metadata.label.dashboard', 'Dashboard'), value: ( { if (rulerRuleType.grafana.recordingRule(rule.rulerRule)) { const metric = rule.rulerRule?.grafana_alert.record?.metric ?? ''; metadata.push({ - label: 'Metric name', + label: t('alerting.create-metadata.label.metric-name', 'Metric name'), value: {metric}, }); } if (interval) { metadata.push({ - label: 'Evaluation interval', + label: t('alerting.create-metadata.label.evaluation-interval', 'Evaluation interval'), value: ( Every {{ interval }} @@ -260,7 +261,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => { if (hasLabels) { metadata.push({ - label: 'Labels', + label: t('alerting.create-metadata.label.labels', 'Labels'), /* TODO truncate number of labels, maybe build in to component? */ value: , }); @@ -415,14 +416,14 @@ function usePageNav(rule: CombinedRule) { subTitle: summary, children: [ { - text: 'Query and conditions', + text: t('alerting.use-page-nav.page-nav.text.query-and-conditions', 'Query and conditions'), active: activeTab === ActiveTab.Query, onClick: () => { setActiveTab(ActiveTab.Query); }, }, { - text: 'Instances', + text: t('alerting.use-page-nav.page-nav.text.instances', 'Instances'), active: activeTab === ActiveTab.Instances, onClick: () => { setActiveTab(ActiveTab.Instances); @@ -431,7 +432,7 @@ function usePageNav(rule: CombinedRule) { hideFromTabs: isRecordingRuleType, }, { - text: 'History', + text: t('alerting.use-page-nav.page-nav.text.history', 'History'), active: activeTab === ActiveTab.History, onClick: () => { setActiveTab(ActiveTab.History); @@ -440,14 +441,14 @@ function usePageNav(rule: CombinedRule) { hideFromTabs: !isGrafanaAlertRule, }, { - text: 'Details', + text: t('alerting.use-page-nav.page-nav.text.details', 'Details'), active: activeTab === ActiveTab.Details, onClick: () => { setActiveTab(ActiveTab.Details); }, }, { - text: 'Versions', + text: t('alerting.use-page-nav.page-nav.text.versions', 'Versions'), active: activeTab === ActiveTab.VersionHistory, onClick: () => { setActiveTab(ActiveTab.VersionHistory); diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx index d6b7497dd19..4ff5da3f8e1 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx @@ -259,8 +259,8 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => { options={[ - { label: 'Show', value: undefined }, - { label: 'Hide', value: 'hide' }, + { label: t('alerting.rules-filter.label.show', 'Show'), value: undefined }, + { label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' }, ]} value={filterState.plugins} onChange={(value) => updateFilters({ ...filterState, plugins: value })} diff --git a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx index fe4f736ea88..73570bf18b5 100644 --- a/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx +++ b/public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx @@ -81,6 +81,7 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) { } const FilterOptions = () => { + const { t } = useTranslate(); return ( @@ -110,11 +111,11 @@ const FilterOptions = () => { diff --git a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx index 996bea7e4a5..5c7c68365f4 100644 --- a/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx +++ b/public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx @@ -8,6 +8,7 @@ import { isUnsignedPluginSignature, } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { useTranslate } from '@grafana/i18n'; import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/internal'; import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui'; @@ -20,6 +21,7 @@ export interface MultipleDataSourcePickerProps extends Omit { + const { t } = useTranslate(); const dataSourceSrv = getDataSourceSrv(); const [state, setState] = useState(); @@ -121,8 +123,22 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) = })); const groupedOptions = [ - { label: 'Data sources with configured alert rules', options: alertManagingDs, expanded: true }, - { label: 'Other data sources', options: nonAlertManagingDs, expanded: true }, + { + label: t( + 'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.data-sources-with-configured-alert-rules', + 'Data sources with configured alert rules' + ), + options: alertManagingDs, + expanded: true, + }, + { + label: t( + 'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.other-data-sources', + 'Other data sources' + ), + options: nonAlertManagingDs, + expanded: true, + }, ]; return groupedOptions; diff --git a/public/app/features/alerting/unified/components/rules/RulesTable.tsx b/public/app/features/alerting/unified/components/rules/RulesTable.tsx index 8e44d0953cf..46406324d8b 100644 --- a/public/app/features/alerting/unified/components/rules/RulesTable.tsx +++ b/public/app/features/alerting/unified/components/rules/RulesTable.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'react'; import Skeleton from 'react-loading-skeleton'; import { GrafanaTheme2 } from '@grafana/data'; +import { useTranslate } from '@grafana/i18n'; import { Pagination, Tooltip, useStyles2 } from '@grafana/ui'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; @@ -192,17 +193,18 @@ function useColumns( showNextEvaluationColumn: boolean, isRulerLoading: boolean ) { + const { t } = useTranslate(); return useMemo((): RuleTableColumnProps[] => { const columns: RuleTableColumnProps[] = [ { id: 'state', - label: 'State', + label: t('alerting.use-columns.columns.label.state', 'State'), renderCell: ({ data: rule }) => , size: '165px', }, { id: 'name', - label: 'Name', + label: t('alerting.use-columns.columns.label.name', 'Name'), // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => rule.name, size: showNextEvaluationColumn ? 4 : 5, @@ -237,7 +239,7 @@ function useColumns( }, { id: 'health', - label: 'Health', + label: t('alerting.use-columns.columns.label.health', 'Health'), // eslint-disable-next-line react/display-name renderCell: ({ data: { promRule, group } }) => (promRule ? : null), size: '75px', @@ -246,7 +248,7 @@ function useColumns( if (showSummaryColumn) { columns.push({ id: 'summary', - label: 'Summary', + label: t('alerting.use-columns.label.summary', 'Summary'), // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => { return ; @@ -258,7 +260,7 @@ function useColumns( if (showNextEvaluationColumn) { columns.push({ id: 'nextEvaluation', - label: 'Next evaluation', + label: t('alerting.use-columns.label.next-evaluation', 'Next evaluation'), renderCell: ({ data: rule }) => { const nextEvalInfo = calculateNextEvaluationEstimate(rule.promRule?.lastEvaluation, rule.group.interval); @@ -282,7 +284,7 @@ function useColumns( if (showGroupColumn) { columns.push({ id: 'group', - label: 'Group', + label: t('alerting.use-columns.label.group', 'Group'), // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => { const { namespace, group } = rule; @@ -301,14 +303,14 @@ function useColumns( } columns.push({ id: 'actions', - label: 'Actions', + label: t('alerting.use-columns.label.actions', 'Actions'), // eslint-disable-next-line react/display-name renderCell: ({ data: rule }) => , size: '215px', }); return columns; - }, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isRulerLoading]); + }, [t, showNextEvaluationColumn, showSummaryColumn, showGroupColumn, isRulerLoading]); } function RuleStateCell({ rule }: { rule: CombinedRule }) { diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx index c9b4d405afa..ae3e052e79d 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx @@ -67,6 +67,7 @@ export const StateFilterValues = { export const CentralAlertHistoryScene = () => { //track the loading of the central alert state history + const { t } = useTranslate(); useEffect(() => { logInfo(LogMessages.loadedCentralAlertStateHistory); }, []); @@ -78,14 +79,17 @@ export const CentralAlertHistoryScene = () => { // textbox variable for filtering by labels const labelsFilterVariable = new TextBoxVariable({ name: LABELS_FILTER, - label: 'Labels: ', + label: t('alerting.central-alert-history-scene.scene.labels-filter-variable.label.labels', 'Labels: '), }); //custom variable for filtering by the current state const transitionsToFilterVariable = new CustomVariable({ name: STATE_FILTER_TO, value: StateFilterValues.all, - label: 'End state:', + label: t( + 'alerting.central-alert-history-scene.scene.transitions-to-filter-variable.label.end-state', + 'End state:' + ), hide: VariableHide.dontHide, query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending},To Recovering : ${StateFilterValues.recovering}`, }); @@ -94,7 +98,10 @@ export const CentralAlertHistoryScene = () => { const transitionsFromFilterVariable = new CustomVariable({ name: STATE_FILTER_FROM, value: StateFilterValues.all, - label: 'Start state:', + label: t( + 'alerting.central-alert-history-scene.scene.transitions-from-filter-variable.label.start-state', + 'Start state:' + ), hide: VariableHide.dontHide, query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending},From Recovering : ${StateFilterValues.recovering}`, }); @@ -132,7 +139,7 @@ export const CentralAlertHistoryScene = () => { ], }), }); - }, []); + }, [t]); // we need to call this to sync the url with the scene state const isUrlSyncInitialized = useUrlSync(scene); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts b/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts index 3013b097e16..76e55ef4f8f 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts @@ -1,6 +1,7 @@ import { useEffect, useMemo } from 'react'; import { DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data'; +import { t } from '@grafana/i18n/internal'; import { getTemplateSrv } from '@grafana/runtime'; import { RuntimeDataSource, sceneUtils } from '@grafana/scenes'; import { DataQuery } from '@grafana/schema'; @@ -72,7 +73,11 @@ class HistoryAPIDatasource extends RuntimeDataSource { } testDatasource(): Promise { - return Promise.resolve({ status: 'success', message: 'Data source is working', title: 'Success' }); + return Promise.resolve({ + status: 'success', + message: t('alerting.history-apidatasource.message.data-source-is-working', 'Data source is working'), + title: t('alerting.history-apidatasource.title.success', 'Success'), + }); } } diff --git a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx index 64455d039ff..913d484802d 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx @@ -2,6 +2,7 @@ import { memo } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data'; +import { useTranslate } from '@grafana/i18n'; import { VisibilityMode } from '@grafana/schema'; import { LegendDisplayMode, useTheme2 } from '@grafana/ui'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; @@ -16,6 +17,7 @@ interface LogTimelineViewerProps { const replaceVariables: InterpolateFunction = (v) => v; export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerProps) => { + const { t } = useTranslate(); const theme = useTheme2(); return ( @@ -38,12 +40,36 @@ export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerP showLegend: true, }} legendItems={[ - { label: 'Normal', color: theme.colors.success.main, yAxis: 1 }, - { label: 'Pending', color: theme.colors.warning.main, yAxis: 1 }, - { label: 'Recovering', color: theme.colors.warning.main, yAxis: 1 }, - { label: 'Firing', color: theme.colors.error.main, yAxis: 1 }, - { label: 'No Data', color: theme.colors.info.main, yAxis: 1 }, - { label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 }, + { + label: t('alerting.log-timeline-viewer.label.normal', 'Normal'), + color: theme.colors.success.main, + yAxis: 1, + }, + { + label: t('alerting.log-timeline-viewer.label.pending', 'Pending'), + color: theme.colors.warning.main, + yAxis: 1, + }, + { + label: t('alerting.log-timeline-viewer.label.recovering', 'Recovering'), + color: theme.colors.warning.main, + yAxis: 1, + }, + { + label: t('alerting.log-timeline-viewer.label.firing', 'Firing'), + color: theme.colors.error.main, + yAxis: 1, + }, + { + label: t('alerting.log-timeline-viewer.label.no-data', 'No Data'), + color: theme.colors.info.main, + yAxis: 1, + }, + { + label: t('alerting.log-timeline-viewer.label.mixed', 'Mixed'), + color: theme.colors.text.secondary, + yAxis: 1, + }, ]} replaceVariables={replaceVariables} /> diff --git a/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx b/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx index 170a674931a..e9d5ee8e158 100644 --- a/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx +++ b/public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx @@ -59,9 +59,19 @@ const StateHistory = ({ ruleUID }: Props) => { } const columns: Array> = [ - { id: 'state', label: 'State', size: 'max-content', renderCell: renderStateCell }, + { + id: 'state', + label: t('alerting.state-history.columns.label.state', 'State'), + size: 'max-content', + renderCell: renderStateCell, + }, { id: 'value', label: '', size: 'auto', renderCell: renderValueCell }, - { id: 'timestamp', label: 'Time', size: 'max-content', renderCell: renderTimestampCell }, + { + id: 'timestamp', + label: t('alerting.state-history.columns.label.time', 'Time'), + size: 'max-content', + renderCell: renderTimestampCell, + }, ]; // group the state history list by unique set of labels diff --git a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx index f67922b42f1..5524e12fd2b 100644 --- a/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx +++ b/public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx @@ -80,7 +80,10 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave // manually register the config field with validation // @TODO sometimes the value doesn't get registered – find out why register('configJSON', { - required: { value: true, message: 'Configuration cannot be empty' }, + required: { + value: true, + message: t('alerting.alertmanager-config.message.configuration-cannot-be-empty', 'Configuration cannot be empty'), + }, validate: (value: string) => { try { JSON.parse(value); diff --git a/public/app/features/alerting/unified/components/silences/MatchersField.tsx b/public/app/features/alerting/unified/components/silences/MatchersField.tsx index 29626247248..6e7755624d2 100644 --- a/public/app/features/alerting/unified/components/silences/MatchersField.tsx +++ b/public/app/features/alerting/unified/components/silences/MatchersField.tsx @@ -63,7 +63,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => { > { )} defaultValue={matcher.operator || matcherFieldOptions[0].value} name={`matchers.${index}.operator`} - rules={{ required: { value: required, message: 'Required.' } }} + rules={{ + required: { + value: required, + message: t('alerting.matchers-field.message.required', 'Required.'), + }, + }} /> { > > { + const { t } = useTranslate(); const styles = useStyles2(getStyles); return [ { id: 'state', - label: 'State', + label: t('alerting.use-columns.label.state', 'State'), renderCell: function renderStateTag({ data }) { return ; }, @@ -162,7 +163,7 @@ function useColumns(): Array> { }, { id: 'labels', - label: 'Labels', + label: t('alerting.use-columns.label.labels', 'Labels'), renderCell: function renderName({ data }) { return ; }, @@ -170,7 +171,7 @@ function useColumns(): Array> { }, { id: 'created', - label: 'Created', + label: t('alerting.use-columns.label.created', 'Created'), renderCell: function renderSummary({ data }) { return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}; }, diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index 0868ab5bd0d..b5e2187218a 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -254,7 +254,9 @@ export const SilencesEditor = ({ invalid={!!formState.errors.comment} >