I18n: Update eslint rule to catch some untranslated object properties (#105072)

This commit is contained in:
Tom Ratcliffe
2025-05-28 21:14:43 +01:00
committed by GitHub
parent 57ec71a7a0
commit 7bfa78c6e6
189 changed files with 3633 additions and 648 deletions
+8 -2
View File
@@ -300,9 +300,15 @@ module.exports = [
'packages/grafana-ui/**/*.{ts,tsx,js,jsx}', 'packages/grafana-ui/**/*.{ts,tsx,js,jsx}',
...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{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: { rules: {
'@grafana/no-untranslated-strings': 'error', '@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
'@grafana/no-translation-top-level': 'error', '@grafana/no-translation-top-level': 'error',
}, },
}, },
+1 -1
View File
@@ -36,7 +36,7 @@ module.exports = {
moduleDirectories: ['public', 'node_modules'], moduleDirectories: ['public', 'node_modules'],
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts/tests'], roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts/tests'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$', 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'], setupFiles: ['jest-canvas-mock', './public/test/jest-setup.ts'],
testTimeout: 30000, testTimeout: 30000,
resolver: `<rootDir>/public/test/jest-resolver.js`, resolver: `<rootDir>/public/test/jest-resolver.js`,
+67 -1
View File
@@ -114,7 +114,47 @@ Used to find all instances of `theme` tokens being used in the codebase and emit
### `no-untranslated-strings` ### `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 ```tsx
// Bad ❌ // Bad ❌
@@ -126,7 +166,30 @@ Check if strings are marked for translation.
<InlineToast placement="top" referenceElement={buttonRef.current}> <InlineToast placement="top" referenceElement={buttonRef.current}>
<Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans> <Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
</InlineToast> </InlineToast>
```
#### JSXAttributes
```tsx
// Bad ❌
<div title="foo bar" />
// Good ✅
<div title={t('some.key.foo-bar', 'foo bar')} />
```
#### Object properties
```tsx
// Bad ❌
const someConfig = {
label: 'Some label',
};
// Good ✅
const getSomeConfig = () => ({
label: t('some.key.label', 'Some label'),
});
``` ```
#### Passing variables to translations #### Passing variables to translations
@@ -166,8 +229,11 @@ The below props are checked for untranslated strings:
- `placeholder` - `placeholder`
- `aria-label` - `aria-label`
- `title` - `title`
- `subTitle`
- `text` - `text`
- `tooltip` - `tooltip`
- `message`
- `name`
```tsx ```tsx
// Bad ❌ // Bad ❌
@@ -2,7 +2,8 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */ /** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */ /** @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 { const {
getNodeValue, getNodeValue,
@@ -20,13 +21,107 @@ const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}` (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']; 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} */ /** @type {RuleDefinition} */
const noUntranslatedStrings = createRule({ const noUntranslatedStrings = createRule({
/**
* @param {RuleContextWithOptions} context
*/
create(context) { create(context) {
const calleesToIgnore = context.options[0]?.calleesToIgnore || [];
const propertiesRegexes = calleesToIgnore.map((pattern) => {
return new RegExp(pattern);
});
return { 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) { JSXAttribute(node) {
if (!propsToCheck.includes(String(node.name.name)) || !node.value) { if (!propsToCheck.includes(String(node.name.name)) || !node.value) {
return; return;
@@ -163,6 +258,7 @@ const noUntranslatedStrings = createRule({
messages: { messages: {
noUntranslatedStrings: 'No untranslated strings. Wrap text with <Trans />', noUntranslatedStrings: 'No untranslated strings. Wrap text with <Trans />',
noUntranslatedStringsProp: `No untranslated strings in text props. Wrap text with <Trans /> or use t()`, noUntranslatedStringsProp: `No untranslated strings in text props. Wrap text with <Trans /> or use t()`,
noUntranslatedStringsProperties: `No untranslated strings in object properties. Wrap text with t()`,
wrapWithTrans: 'Wrap text with <Trans /> for manual key assignment', wrapWithTrans: 'Wrap text with <Trans /> for manual key assignment',
wrapWithT: 'Wrap text with t() for manual key assignment', wrapWithT: 'Wrap text with t() for manual key assignment',
}, },
@@ -177,12 +273,19 @@ const noUntranslatedStrings = createRule({
}, },
uniqueItems: true, uniqueItems: true,
}, },
calleesToIgnore: {
type: 'array',
items: {
type: 'string',
},
default: [],
},
}, },
additionalProperties: false, additionalProperties: false,
}, },
], ],
}, },
defaultOptions: [{ forceFix: [] }], defaultOptions: [{ forceFix: [], calleesToIgnore: [] }],
}); });
module.exports = noUntranslatedStrings; module.exports = noUntranslatedStrings;
@@ -5,9 +5,9 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXText} JSXText */ /** @typedef {import('@typescript-eslint/utils').TSESTree.JSXText} JSXText */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */ /** @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').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[]}]>} RuleContextWithOptions */
const { AST_NODE_TYPES } = require('@typescript-eslint/utils'); const { AST_NODE_TYPES } = require('@typescript-eslint/utils');
/** /**
@@ -77,7 +77,7 @@ function shouldBeFixed(context) {
/** /**
* Checks if a node can be fixed automatically * 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 * @param {RuleContextWithOptions} context
* @returns {boolean} Whether the node can be fixed * @returns {boolean} Whether the node can be fixed
*/ */
@@ -86,16 +86,28 @@ function canBeFixed(node, context) {
return false; 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, // We can only fix JSX attribute strings that are within a function,
// otherwise the `t` function call will be made too early // otherwise the `t` function call will be made too early
if (node.type === AST_NODE_TYPES.JSXAttribute) { if (isPropertyOrAttribute && !parentMethod) {
const parentMethod = getParentMethod(node, context); return false;
if (!parentMethod) { }
// 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; 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 = const values =
@@ -130,7 +142,7 @@ function canBeFixed(node, context) {
*/ */
function getTranslationPrefix(context) { function getTranslationPrefix(context) {
const filename = context.filename; const filename = context.filename;
const match = filename.match(/public\/app\/features\/([^/]+)/); const match = filename.match(/public\/app\/features\/(.+?)\//);
if (match) { if (match) {
return match[1]; return match[1];
} }
@@ -139,7 +151,7 @@ function getTranslationPrefix(context) {
/** /**
* Gets the i18n key for a node * Gets the i18n key for a node
* @param {JSXAttribute|JSXText} node The node * @param {JSXAttribute|JSXText|Property} node The node
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
* @returns {string} The i18n key * @returns {string} The i18n key
*/ */
@@ -148,6 +160,10 @@ const getI18nKey = (node, context) => {
const stringValue = getNodeValue(node); const stringValue = getNodeValue(node);
const componentNames = getComponentNames(node, context); 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 const words = stringValue
.trim() .trim()
.replace(/[^\a-zA-Z\s]/g, '') .replace(/[^\a-zA-Z\s]/g, '')
@@ -185,14 +201,14 @@ const getI18nKey = (node, context) => {
kebabString = [potentialId, propName, kebabString].filter(Boolean).join('-'); 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; return fullPrefix;
}; };
/** /**
* Gets component names from ancestors * Gets component names from ancestors
* @param {JSXAttribute|JSXText} node The node * @param {JSXAttribute|JSXText|Property} node The node
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
* @returns {string[]} The component names * @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`
* that came from a `useTranslate` call * @param {Node} node
* @param {Node} method The node
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
*/ */
function methodHasUseTranslate(method, context) { function getTDeclaration(node, context) {
const tDeclaration = method ? context.sourceCode.getScope(method).variables.find((v) => v.name === 't') : null; 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} node The node
* @param {RuleContextWithOptions} context
*/
function methodHasUseTranslate(node, context) {
const tDeclaration = getTDeclaration(node, context);
return ( return (
tDeclaration && tDeclaration &&
tDeclaration.defs.find((definition) => { tDeclaration.defs.find((definition) => {
@@ -243,7 +268,7 @@ function methodHasUseTranslate(method, context) {
/** /**
* Gets the import fixer for a node * Gets the import fixer for a node
* @param {JSXElement|JSXFragment|JSXAttribute} node * @param {JSXElement|JSXFragment|JSXAttribute|Property} node
* @param {RuleFixer} fixer The fixer * @param {RuleFixer} fixer The fixer
* @param {'Trans'|'t'|'useTranslate'} importName The member to import from either `@grafana/i18n` or `@grafana/i18n/internal` * @param {'Trans'|'t'|'useTranslate'} importName The member to import from either `@grafana/i18n` or `@grafana/i18n/internal`
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
@@ -337,7 +362,7 @@ const firstCharIsUpper = (str) => {
}; };
/** /**
* @param {JSXAttribute} node * @param {JSXAttribute|Property} node
* @param {RuleFixer} fixer * @param {RuleFixer} fixer
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
* @returns {import('@typescript-eslint/utils/ts-eslint').RuleFix|undefined} The fix * @returns {import('@typescript-eslint/utils/ts-eslint').RuleFix|undefined} The fix
@@ -380,9 +405,10 @@ const getUseTranslateFixer = (node, fixer, context) => {
if (!returnStatementIsJsx) { if (!returnStatementIsJsx) {
return; return;
} }
const tDeclarationExists = getTDeclaration(parentMethod, context);
const useTranslateExists = methodHasUseTranslate(parentMethod, context); const useTranslateExists = methodHasUseTranslate(parentMethod, context);
if (useTranslateExists) { if (tDeclarationExists && useTranslateExists) {
return; return;
} }
@@ -396,7 +422,7 @@ const getUseTranslateFixer = (node, fixer, context) => {
}; };
/** /**
* @param {JSXAttribute} node * @param {JSXAttribute|Property} node
* @param {RuleContextWithOptions} context * @param {RuleContextWithOptions} context
* @returns {(fixer: RuleFixer) => import('@typescript-eslint/utils/ts-eslint').RuleFix[]} * @returns {(fixer: RuleFixer) => import('@typescript-eslint/utils/ts-eslint').RuleFix[]}
*/ */
@@ -406,9 +432,13 @@ const getTFixers = (node, context) => (fixer) => {
const value = getNodeValue(node); const value = getNodeValue(node);
const wrappingQuotes = value.includes('"') ? "'" : '"'; const wrappingQuotes = value.includes('"') ? "'" : '"';
fixes.push( if (node.type === AST_NODE_TYPES.Property) {
fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`) 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 // Check if we need to add `useTranslate` to the node
const useTranslateFixer = getUseTranslateFixer(node, fixer, context); const useTranslateFixer = getUseTranslateFixer(node, fixer, context);
@@ -428,11 +458,19 @@ const getTFixers = (node, context) => (fixer) => {
/** /**
* Gets the value of a node * 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 * @returns {string} The node value
*/ */
function getNodeValue(node) { 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) || ''; return String(node.value.value) || '';
} }
if (node.type === AST_NODE_TYPES.JSXText) { if (node.type === AST_NODE_TYPES.JSXText) {
@@ -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'; const packageName = '@grafana/i18n';
@@ -31,6 +31,7 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
{ {
name: 'Text in Trans component', name: 'Text in Trans component',
code: `const Foo = () => <Bar><Trans>Translated text</Trans></Bar>`, code: `const Foo = () => <Bar><Trans>Translated text</Trans></Bar>`,
filename,
}, },
{ {
name: 'Text in Trans component with whitespace/JSXText elements', name: 'Text in Trans component with whitespace/JSXText elements',
@@ -38,58 +39,72 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
<Trans> <Trans>
Translated text Translated text
</Trans> </Bar>`, </Trans> </Bar>`,
filename,
}, },
{ {
name: 'Empty component', name: 'Empty component',
code: `<div> </div>`, code: `<div> </div>`,
filename,
}, },
{ {
name: 'Text using t() function', name: 'Text using t() function',
code: `<div>{t('translated.key', 'Translated text')}</div>`, code: `<div>{t('translated.key', 'Translated text')}</div>`,
filename,
}, },
{ {
name: 'Prop using t() function', name: 'Prop using t() function',
code: `<div aria-label={t('aria.label', 'Accessible label')} />`, code: `<div aria-label={t('aria.label', 'Accessible label')} />`,
filename,
}, },
{ {
name: 'Empty string prop', name: 'Empty string prop',
code: `<div title="" />`, code: `<div title="" />`,
filename,
}, },
{ {
name: 'Prop using boolean', name: 'Prop using boolean',
code: `<div title={false} />`, code: `<div title={false} />`,
filename,
}, },
{ {
name: 'Prop using number', name: 'Prop using number',
code: `<div title={0} />`, code: `<div title={0} />`,
filename,
}, },
{ {
name: 'Prop using null', name: 'Prop using null',
code: `<div title={null} />`, code: `<div title={null} />`,
filename,
}, },
{ {
name: 'Prop using undefined', name: 'Prop using undefined',
code: `<div title={undefined} />`, code: `<div title={undefined} />`,
filename,
}, },
{ {
name: 'Variable interpolation', name: 'Variable interpolation',
code: `<div>{variable}</div>`, code: `<div>{variable}</div>`,
filename,
}, },
{ {
name: 'Entirely non-alphanumeric text (prop)', name: 'Entirely non-alphanumeric text (prop)',
code: `<div title="-" />`, code: `<div title="-" />`,
filename,
}, },
{ {
name: 'Entirely non-alphanumeric text', name: 'Entirely non-alphanumeric text',
code: `<div>-</div>`, code: `<div>-</div>`,
filename,
}, },
{ {
name: 'Non-alphanumeric siblings', name: 'Non-alphanumeric siblings',
code: `<div>({variable})</div>`, code: `<div>({variable})</div>`,
filename,
}, },
{ {
name: "Ternary in an attribute we don't care about", name: "Ternary in an attribute we don't care about",
code: `<div icon={isAThing ? 'Foo' : 'Bar'} />`, code: `<div icon={isAThing ? 'Foo' : 'Bar'} />`,
filename,
}, },
{ {
name: 'Ternary with falsy strings', name: 'Ternary with falsy strings',
@@ -98,6 +113,76 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
{ {
name: 'Ternary with no strings', name: 'Ternary with no strings',
code: `<div title={isAThing ? 1 : 2} />`, code: `<div title={isAThing ? 1 : 2} />`,
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'} }) => <div>{foobar.label}</div>`,
filename,
}, },
], ],
invalid: [ invalid: [
@@ -667,6 +752,97 @@ const Foo = () => {
], ],
}, },
{
name: 'Untranslated object property',
code: `
const Foo = () => {
const thing = {
label: 'test',
}
return <div>{thing.label}</div>;
}`,
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 <div>{thing.label}</div>;
}`,
},
],
},
],
},
{
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 * AUTO FIXES
*/ */
@@ -723,6 +899,42 @@ return <div title={t("some-feature.foo.title-foo", "foo")} />
], ],
}, },
{
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 * UNFIXABLE CASES
*/ */
@@ -824,5 +1036,34 @@ const Foo = () => {
filename, filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }], errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }],
}, },
{
name: 'Cannot fix if `t` already exists from somewhere else',
code: `
const Foo = () => {
const t = () => 'something else';
return (
<div title="foo" />
)
}`,
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',
// },
// ],
// },
], ],
}); });
@@ -54,7 +54,13 @@ export const ActionVariablesEditor = ({ value, onChange }: Props) => {
const isAddButtonDisabled = name === '' || key === ''; 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 ( return (
<div> <div>
@@ -86,7 +86,10 @@ const AdminEditOrgPage = () => {
const pageNav: NavModelItem = { const pageNav: NavModelItem = {
text: orgState?.value?.name ?? '', text: orgState?.value?.name ?? '',
icon: 'shield', 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 ( return (
+6 -1
View File
@@ -3,6 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { useParams } from 'react-router-dom-v5-compat'; import { useParams } from 'react-router-dom-v5-compat';
import { NavModelItem } from '@grafana/data'; import { NavModelItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { featureEnabled } from '@grafana/runtime'; import { featureEnabled } from '@grafana/runtime';
import { Stack } from '@grafana/ui'; import { Stack } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
@@ -59,6 +60,7 @@ export const UserAdminPage = ({
revokeAllSessions, revokeAllSessions,
syncLdapUser, syncLdapUser,
}: Props) => { }: Props) => {
const { t } = useTranslate();
const { id = '' } = useParams(); const { id = '' } = useParams();
useEffect(() => { useEffect(() => {
loadAdminUserPage(id); loadAdminUserPage(id);
@@ -123,7 +125,10 @@ export const UserAdminPage = ({
const pageNav: NavModelItem = { const pageNav: NavModelItem = {
text: user?.login ?? '', text: user?.login ?? '',
icon: 'shield', 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 ( return (
@@ -88,8 +88,11 @@ const UserListAdminPageUnConnected = ({
/> />
<RadioButtonGroup <RadioButtonGroup
options={[ options={[
{ label: 'All users', value: false }, { label: t('admin.user-list-admin-page-un-connected.label.all-users', 'All users'), value: false },
{ label: 'Active last 30 days', value: true }, {
label: t('admin.user-list-admin-page-un-connected.label.active-last-days', 'Active last 30 days'),
value: true,
},
]} ]}
onChange={(value) => changeFilter({ name: 'activeLast30Days', value })} onChange={(value) => changeFilter({ name: 'activeLast30Days', value })}
value={filters.find((f) => f.name === 'activeLast30Days')?.value} value={filters.find((f) => f.name === 'activeLast30Days')?.value}
@@ -70,7 +70,15 @@ const UserListAnonymousDevicesPageUnConnected = ({
onChange={changeAnonQuery} onChange={changeAnonQuery}
/> />
<RadioButtonGroup <RadioButtonGroup
options={[{ label: 'Active last 30 days', value: true }]} options={[
{
label: t(
'admin.user-list-anonymous-devices-page-un-connected.label.active-last-days',
'Active last 30 days'
),
value: true,
},
]}
// onChange={(value) => changeFilter({ name: 'activeLast30Days', value })} // onChange={(value) => changeFilter({ name: 'activeLast30Days', value })}
value={filters.find((f) => f.name === 'activeLast30Days')?.value} value={filters.find((f) => f.name === 'activeLast30Days')?.value}
className={styles.filter} className={styles.filter}
+11 -10
View File
@@ -1,5 +1,6 @@
import { isArray, reduce } from 'lodash'; import { isArray, reduce } from 'lodash';
import { t } from '@grafana/i18n/internal';
import { IconName } from '@grafana/ui'; import { IconName } from '@grafana/ui';
import { QueryPart, QueryPartDef } from 'app/features/alerting/state/query_part'; import { QueryPart, QueryPartDef } from 'app/features/alerting/state/query_part';
@@ -114,42 +115,42 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'normal': case 'normal':
case 'ok': { case 'ok': {
return { return {
text: 'OK', text: t('alerting.get-state-display-model.text.ok', 'OK'),
iconClass: 'heart', iconClass: 'heart',
stateClass: 'alert-state-ok', stateClass: 'alert-state-ok',
}; };
} }
case 'alerting': { case 'alerting': {
return { return {
text: 'ALERTING', text: t('alerting.get-state-display-model.text.alerting', 'ALERTING'),
iconClass: 'heart-break', iconClass: 'heart-break',
stateClass: 'alert-state-critical', stateClass: 'alert-state-critical',
}; };
} }
case 'nodata': { case 'nodata': {
return { return {
text: 'NO DATA', text: t('alerting.get-state-display-model.text.no-data', 'NO DATA'),
iconClass: 'question-circle', iconClass: 'question-circle',
stateClass: 'alert-state-warning', stateClass: 'alert-state-warning',
}; };
} }
case 'paused': { case 'paused': {
return { return {
text: 'PAUSED', text: t('alerting.get-state-display-model.text.paused', 'PAUSED'),
iconClass: 'pause', iconClass: 'pause',
stateClass: 'alert-state-paused', stateClass: 'alert-state-paused',
}; };
} }
case 'pending': { case 'pending': {
return { return {
text: 'PENDING', text: t('alerting.get-state-display-model.text.pending', 'PENDING'),
iconClass: 'hourglass', iconClass: 'hourglass',
stateClass: 'alert-state-warning', stateClass: 'alert-state-warning',
}; };
} }
case 'recovering': { case 'recovering': {
return { return {
text: 'RECOVERING', text: t('alerting.get-state-display-model.text.recovering', 'RECOVERING'),
iconClass: 'hourglass', iconClass: 'hourglass',
stateClass: 'alert-state-warning', stateClass: 'alert-state-warning',
}; };
@@ -157,7 +158,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'firing': { case 'firing': {
return { return {
text: 'FIRING', text: t('alerting.get-state-display-model.text.firing', 'FIRING'),
iconClass: 'fire', iconClass: 'fire',
stateClass: '', stateClass: '',
}; };
@@ -165,7 +166,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'inactive': { case 'inactive': {
return { return {
text: 'INACTIVE', text: t('alerting.get-state-display-model.text.inactive', 'INACTIVE'),
iconClass: 'check', iconClass: 'check',
stateClass: '', stateClass: '',
}; };
@@ -173,7 +174,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'error': { case 'error': {
return { return {
text: 'ERROR', text: t('alerting.get-state-display-model.text.error', 'ERROR'),
iconClass: 'heart-break', iconClass: 'heart-break',
stateClass: 'alert-state-critical', stateClass: 'alert-state-critical',
}; };
@@ -182,7 +183,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'unknown': case 'unknown':
default: { default: {
return { return {
text: 'UNKNOWN', text: t('alerting.get-state-display-model.text.unknown', 'UNKNOWN'),
iconClass: 'question-circle', iconClass: 'question-circle',
stateClass: '.alert-state-paused', stateClass: '.alert-state-paused',
}; };
@@ -1,17 +1,22 @@
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { withPageErrorBoundary } from './withPageErrorBoundary'; import { withPageErrorBoundary } from './withPageErrorBoundary';
function FeatureTogglePage() { function FeatureTogglePage() {
const { t } = useTranslate();
const navModel: NavModel = { const navModel: NavModel = {
node: { 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, 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: { main: {
text: 'Alerting is not enabled', text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'),
}, },
}; };
@@ -1,5 +1,6 @@
import { useLocation } from 'react-router-dom-v5-compat'; import { useLocation } from 'react-router-dom-v5-compat';
import { useTranslate } from '@grafana/i18n';
import { import {
defaultsFromQuery, defaultsFromQuery,
getDefaultSilenceFormValues, getDefaultSilenceFormValues,
@@ -37,10 +38,14 @@ const SilencesEditorComponent = () => {
}; };
function NewSilencePage() { function NewSilencePage() {
const { t } = useTranslate();
const pageNav = { const pageNav = {
id: 'silence-new', id: 'silence-new',
text: 'Silence alert rule', text: t('alerting.new-silence-page.page-nav.text.silence-alert-rule', 'Silence alert rule'),
subTitle: 'Configure silences to stop notifications from a particular 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 ( return (
<AlertmanagerPageWrapper navId="silences" pageNav={pageNav} accessType="instance"> <AlertmanagerPageWrapper navId="silences" pageNav={pageNav} accessType="instance">
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data'; import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
@@ -22,13 +22,14 @@ type AlertGroupAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAler
type AlertGroupAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>; type AlertGroupAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>;
export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => { export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const columns = useMemo( const columns = useMemo(
(): AlertGroupAlertsTableColumnProps[] => [ (): AlertGroupAlertsTableColumnProps[] => [
{ {
id: 'state', 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 // eslint-disable-next-line react/display-name
renderCell: ({ data: alert }) => ( renderCell: ({ data: alert }) => (
<> <>
@@ -52,13 +53,13 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props)
}, },
{ {
id: 'labels', 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 // eslint-disable-next-line react/display-name
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />, renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />,
size: 1, size: 1,
}, },
], ],
[styles] [styles, t]
); );
const items = useMemo( const items = useMemo(
@@ -1,13 +1,16 @@
import { useTranslate } from '@grafana/i18n';
import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertingPageWrapper } from '../AlertingPageWrapper'; import { AlertingPageWrapper } from '../AlertingPageWrapper';
import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm'; import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
function ExportNewGrafanaRulePage() { function ExportNewGrafanaRulePage() {
const { t } = useTranslate();
return ( return (
<AlertingPageWrapper <AlertingPageWrapper
navId="alert-list" navId="alert-list"
pageNav={{ pageNav={{
text: 'Export new Grafana rule', text: t('alerting.export-new-grafana-rule-page.text.export-new-grafana-rule', 'Export new Grafana rule'),
subTitle: 'Export a new rule definition in Terraform(HCL) format. Any changes you make will not be saved.', subTitle: 'Export a new rule definition in Terraform(HCL) format. Any changes you make will not be saved.',
}} }}
> >
@@ -5,7 +5,7 @@ import * as React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; 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 { Alert, Button, ClipboardButton, CodeEditor, TextLink, useStyles2 } from '@grafana/ui';
import { ExportFormats, ExportProvider, ProvisioningType, allGrafanaExportProviders } from './providers'; import { ExportFormats, ExportProvider, ProvisioningType, allGrafanaExportProviders } from './providers';
@@ -92,11 +92,15 @@ const fileExportPreviewStyles = (theme: GrafanaTheme2) => ({
}); });
function FileExportInlineDocumentation({ exportProvider }: { exportProvider: ExportProvider<unknown> }) { function FileExportInlineDocumentation({ exportProvider }: { exportProvider: ExportProvider<unknown> }) {
const { t } = useTranslate();
const { name, type } = exportProvider; const { name, type } = exportProvider;
const exportInlineDoc: Record<ProvisioningType, { title: string; component: React.ReactNode }> = { const exportInlineDoc: Record<ProvisioningType, { title: string; component: React.ReactNode }> = {
file: { file: {
title: 'File-provisioning format', title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.fileprovisioning-format',
'File-provisioning format'
),
component: ( component: (
<Trans i18nKey="alerting.file-export-inline-documentation.file-provisioning"> <Trans i18nKey="alerting.file-export-inline-documentation.file-provisioning">
{{ name }} format is only valid for File Provisioning.{' '} {{ name }} format is only valid for File Provisioning.{' '}
@@ -110,7 +114,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
), ),
}, },
api: { api: {
title: 'API-provisioning format', title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.apiprovisioning-format',
'API-provisioning format'
),
component: ( component: (
<Trans i18nKey="alerting.file-export-inline-documentation.api-provisioning"> <Trans i18nKey="alerting.file-export-inline-documentation.api-provisioning">
{{ name }} format is only valid for API Provisioning.{' '} {{ name }} format is only valid for API Provisioning.{' '}
@@ -124,7 +131,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
), ),
}, },
terraform: { terraform: {
title: 'Terraform-provisioning format', title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.terraformprovisioning-format',
'Terraform-provisioning format'
),
component: ( component: (
<Trans i18nKey="alerting.file-export-inline-documentation.terraform-provisioning"> <Trans i18nKey="alerting.file-export-inline-documentation.terraform-provisioning">
{{ name }} format is only valid for Terraform Provisioning.{' '} {{ name }} format is only valid for Terraform Provisioning.{' '}
@@ -93,11 +93,12 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
} }
function GrafanaModifyExportPage() { function GrafanaModifyExportPage() {
const { t } = useTranslate();
return ( return (
<AlertingPageWrapper <AlertingPageWrapper
navId="alert-list" navId="alert-list"
pageNav={{ pageNav={{
text: 'Modify export', text: t('alerting.grafana-modify-export-page.text.modify-export', 'Modify export'),
subTitle: subTitle:
'Modify the current alert rule and export the rule definition in the format of your choice. Any changes you make will not be saved.', 'Modify the current alert rule and export the rule definition in the format of your choice. Any changes you make will not be saved.',
}} }}
@@ -240,7 +240,13 @@ const ImportFromDSRules = () => {
name="targetDatasourceUID" name="targetDatasourceUID"
control={control} control={control}
rules={{ 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'
),
},
}} }}
/> />
</Field> </Field>
@@ -1,5 +1,6 @@
import { Navigate } from 'react-router-dom-v5-compat'; 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 { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams'; import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams';
@@ -39,10 +40,14 @@ const EditTimingRoute = () => {
}; };
function EditMuteTimingPage() { function EditMuteTimingPage() {
const { t } = useTranslate();
return ( return (
<AlertmanagerPageWrapper <AlertmanagerPageWrapper
navId="am-routes" navId="am-routes"
pageNav={{ id: 'alert-policy-edit', text: 'Edit time interval' }} pageNav={{
id: 'alert-policy-edit',
text: t('alerting.edit-mute-timing-page.text.edit-time-interval', 'Edit time interval'),
}}
accessType="notification" accessType="notification"
> >
<EditTimingRoute /> <EditTimingRoute />
@@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Alert, Button, LinkButton, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui'; import { Alert, Button, LinkButton, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui';
import { MuteTimingActionsButtons } from 'app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons'; import { MuteTimingActionsButtons } from 'app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons';
import { import {
@@ -149,7 +150,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
const columns: Array<DynamicTableColumnProps<MuteTiming>> = [ const columns: Array<DynamicTableColumnProps<MuteTiming>> = [
{ {
id: 'name', id: 'name',
label: 'Name', label: t('alerting.use-columns.columns.label.name', 'Name'),
renderCell: function renderName({ data }) { renderCell: function renderName({ data }) {
return ( return (
<div> <div>
@@ -164,7 +165,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
}, },
{ {
id: 'timeRange', id: 'timeRange',
label: 'Time range', label: t('alerting.use-columns.columns.label.time-range', 'Time range'),
renderCell: ({ data }) => { renderCell: ({ data }) => {
return renderTimeIntervals(data); return renderTimeIntervals(data);
}, },
@@ -174,7 +175,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
if (showActions) { if (showActions) {
columns.push({ columns.push({
id: 'actions', id: 'actions',
label: 'Actions', label: t('alerting.use-columns.label.actions', 'Actions'),
alignColumn: 'end', alignColumn: 'end',
renderCell: ({ data }) => ( renderCell: ({ data }) => (
<MuteTimingActionsButtons muteTiming={data} alertManagerSourceName={alertManagerSourceName} /> <MuteTimingActionsButtons muteTiming={data} alertManagerSourceName={alertManagerSourceName} />
@@ -1,13 +1,19 @@
import { useTranslate } from '@grafana/i18n';
import { withPageErrorBoundary } from '../../withPageErrorBoundary'; import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper'; import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import MuteTimingForm from './MuteTimingForm'; import MuteTimingForm from './MuteTimingForm';
function NewMuteTimingPage() { function NewMuteTimingPage() {
const { t } = useTranslate();
return ( return (
<AlertmanagerPageWrapper <AlertmanagerPageWrapper
navId="am-routes" navId="am-routes"
pageNav={{ id: 'alert-policy-new', text: 'Add time interval' }} pageNav={{
id: 'alert-policy-new',
text: t('alerting.new-mute-timing-page.text.add-time-interval', 'Add time interval'),
}}
accessType="notification" accessType="notification"
> >
<MuteTimingForm /> <MuteTimingForm />
@@ -70,7 +70,9 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
)} )}
control={control} control={control}
name="receiver" name="receiver"
rules={{ required: { value: true, message: 'Required.' } }} rules={{
required: { value: true, message: t('alerting.am-root-route-form.message.required', 'Required.') },
}}
/> />
<span> <span>
<Trans i18nKey="alerting.am-root-route-form.or">or</Trans> <Trans i18nKey="alerting.am-root-route-form.or">or</Trans>
@@ -133,7 +133,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
defaultValue={field.operator} defaultValue={field.operator}
control={control} control={control}
name={`object_matchers.${index}.operator`} 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.'),
},
}}
/> />
</Field> </Field>
<Field <Field
@@ -202,7 +202,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
> >
<Input <Input
{...register('title', { {...register('title', {
required: { value: true, message: 'Required.' }, required: { value: true, message: t('alerting.template-form.message.required', 'Required.') },
validate: { titleIsUnique }, validate: { titleIsUnique },
})} })}
placeholder={t( placeholder={t(
@@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import type { Monaco } from '@grafana/ui'; import type { Monaco } from '@grafana/ui';
import { import {
@@ -16,12 +17,18 @@ import { SuggestionDefinition } from './suggestionDefinition';
export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] { export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field; const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [ return [
{ {
label: 'Alerts', label: 'Alerts',
kind, kind,
detail: 'Alert[]', detail: 'Alert[]',
documentation: { value: 'An Array containing all alerts' }, documentation: {
value: t(
'alerting.get-global-suggestions.value.an-array-containing-all-alerts',
'An Array containing all alerts'
),
},
}, },
{ label: 'Receiver', kind, detail: 'string' }, { label: 'Receiver', kind, detail: 'string' },
{ label: 'Status', kind, detail: 'string' }, { label: 'Status', kind, detail: 'string' },
@@ -32,36 +39,54 @@ export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
{ label: 'GroupKey', kind, detail: 'string' }, { label: 'GroupKey', kind, detail: 'string' },
{ label: 'TruncatedAlerts', kind, detail: 'integer' }, { label: 'TruncatedAlerts', kind, detail: 'integer' },
]; ];
/* eslint-enable @grafana/no-untranslated-strings */
} }
// Suggestions that are valid only in the scope of an alert (e.g. in the .Alerts loop) // Suggestions that are valid only in the scope of an alert (e.g. in the .Alerts loop)
export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] { export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field; const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [ return [
{ {
label: { label: 'Status', detail: '(Alert)', description: 'string' }, label: { label: 'Status', detail: '(Alert)', description: 'string' },
kind, kind,
detail: 'string', detail: 'string',
documentation: { value: 'Status of the alert. It can be `firing` or `resolved`' }, documentation: {
value: t(
'alerting.get-alert-suggestions.value.status-alert-firing-resolved',
'Status of the alert. It can be `firing` or `resolved`'
),
},
}, },
{ {
label: { label: 'Labels', detail: '(Alert)' }, label: { label: 'Labels', detail: '(Alert)' },
kind, kind,
detail: '[]KeyValue', detail: '[]KeyValue',
documentation: { value: 'A set of labels attached to the alert.' }, documentation: {
value: t(
'alerting.get-alert-suggestions.value.labels-attached-alert',
'A set of labels attached to the alert.'
),
},
}, },
{ {
label: { label: 'Annotations', detail: '(Alert)' }, label: { label: 'Annotations', detail: '(Alert)' },
kind, kind,
detail: '[]KeyValue', detail: '[]KeyValue',
documentation: 'A set of annotations attached to the alert.', documentation: t(
'alerting.get-alert-suggestions.documentation.annotations-attached-alert',
'A set of annotations attached to the alert.'
),
}, },
{ {
label: { label: 'StartsAt', detail: '(Alert)' }, label: { label: 'StartsAt', detail: '(Alert)' },
kind, kind,
detail: 'time.Time', detail: 'time.Time',
documentation: 'Time the alert started firing.', documentation: t(
'alerting.get-alert-suggestions.documentation.time-the-alert-started-firing',
'Time the alert started firing.'
),
}, },
{ {
label: { label: 'EndsAt', detail: '(Alert)' }, label: { label: 'EndsAt', detail: '(Alert)' },
@@ -74,7 +99,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'GeneratorURL', detail: '(Alert)' }, label: { label: 'GeneratorURL', detail: '(Alert)' },
kind, kind,
detail: 'string', detail: 'string',
documentation: 'Back link to Grafana or external Alertmanager.', documentation: t(
'alerting.get-alert-suggestions.documentation.grafana-external-alertmanager',
'Back link to Grafana or external Alertmanager.'
),
}, },
{ {
label: { label: 'SilenceURL', detail: '(Alert)' }, label: { label: 'SilenceURL', detail: '(Alert)' },
@@ -99,7 +127,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'Fingerprint', detail: '(Alert)' }, label: { label: 'Fingerprint', detail: '(Alert)' },
kind, kind,
detail: 'string', detail: 'string',
documentation: 'Fingerprint that can be used to identify the alert.', documentation: t(
'alerting.get-alert-suggestions.documentation.fingerprint-identify-alert',
'Fingerprint that can be used to identify the alert.'
),
}, },
{ {
label: { label: 'ValueString', detail: '(Alert)' }, label: { label: 'ValueString', detail: '(Alert)' },
@@ -111,25 +142,32 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'OrgID', detail: '(Alert)' }, label: { label: 'OrgID', detail: '(Alert)' },
kind, kind,
detail: 'integer', detail: 'integer',
documentation: 'The ID of the organization that owns the alert.', documentation: t(
'alerting.get-alert-suggestions.documentation.organization-alert',
'The ID of the organization that owns the alert.'
),
}, },
]; ];
/* eslint-enable @grafana/no-untranslated-strings */
} }
// Suggestions for .Alerts // Suggestions for .Alerts
export function getAlertsSuggestions(monaco: Monaco): SuggestionDefinition[] { export function getAlertsSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field; const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [ return [
{ label: 'Firing', kind, detail: 'Alert[]' }, { label: 'Firing', kind, detail: 'Alert[]' },
{ label: 'Resolved', kind, detail: 'Alert[]' }, { label: 'Resolved', kind, detail: 'Alert[]' },
]; ];
/* eslint-disable @grafana/no-untranslated-strings */
} }
// Suggestions for the KeyValue types // Suggestions for the KeyValue types
export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] { export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field; const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [ return [
{ label: 'SortedPairs', kind, detail: '[]KeyValue' }, { label: 'SortedPairs', kind, detail: '[]KeyValue' },
{ label: 'Names', kind, detail: '[]string' }, { label: 'Names', kind, detail: '[]string' },
@@ -140,6 +178,7 @@ export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
kind: monaco.languages.CompletionItemKind.Method, kind: monaco.languages.CompletionItemKind.Method,
}, },
]; ];
/* eslint-enable @grafana/no-untranslated-strings */
} }
export const snippets = { export const snippets = {
@@ -82,10 +82,10 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
}; };
const alertOptions: AlertOption[] = [ const alertOptions: AlertOption[] = [
{ {
label: 'Firing', label: t('alerting.generate-alert-data-modal.alert-options.label.firing', 'Firing'),
value: 'firing', value: 'firing',
}, },
{ label: 'Resolved', value: 'resolved' }, { label: t('alerting.generate-alert-data-modal.alert-options.label.resolved', 'Resolved'), value: 'resolved' },
]; ];
return ( return (
@@ -123,6 +123,7 @@ interface TemplateSelectorProps {
} }
function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) { function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const valueInFormIsCustom = Boolean(valueInForm) && !matchesOnlyOneTemplate(valueInForm); const valueInFormIsCustom = Boolean(valueInForm) && !matchesOnlyOneTemplate(valueInForm);
const [template, setTemplate] = useState<SelectableValue<Template> | undefined>(undefined); const [template, setTemplate] = useState<SelectableValue<Template> | undefined>(undefined);
@@ -138,7 +139,10 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
const templateOptions: Array<SelectableValue<TemplateFieldOption>> = [ const templateOptions: Array<SelectableValue<TemplateFieldOption>> = [
{ {
label: 'Select notification template', label: t(
'alerting.template-selector.template-options.label.select-notification-template',
'Select notification template'
),
ariaLabel: 'Select notification template', ariaLabel: 'Select notification template',
value: 'Existing', 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.' : ''}`, 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)); setCustomTemplateValue(getUseTemplateText(template.value.name));
} }
}, [template]); }, [template]);
const { t } = useTranslate();
function onCustomTemplateChange(customInput: string) { function onCustomTemplateChange(customInput: string) {
setCustomTemplateValue(customInput); setCustomTemplateValue(customInput);
@@ -66,7 +66,10 @@ export const AlertRuleNameAndMetric = () => {
id="name" id="name"
width={38} width={38}
{...register('name', { {...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 pattern: isCloudRecordingRule
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording) ? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
: undefined, : undefined,
@@ -89,7 +92,13 @@ export const AlertRuleNameAndMetric = () => {
id="metric" id="metric"
width={38} width={38}
{...register('metric', { {...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), pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording),
})} })}
aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')} aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')}
@@ -130,7 +139,13 @@ export const AlertRuleNameAndMetric = () => {
name="targetDatasourceUid" name="targetDatasourceUid"
control={control} control={control}
rules={{ 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'
),
},
}} }}
/> />
</Field> </Field>
@@ -1,5 +1,6 @@
import { Controller, FieldArrayWithId, useFormContext } from 'react-hook-form'; import { Controller, FieldArrayWithId, useFormContext } from 'react-hook-form';
import { useTranslate } from '@grafana/i18n';
import { Stack, Text } from '@grafana/ui'; import { Stack, Text } from '@grafana/ui';
import { RuleFormValues } from '../../types/rule-form'; import { RuleFormValues } from '../../types/rule-form';
@@ -20,6 +21,7 @@ const AnnotationHeaderField = ({
index: number; index: number;
labelId: string; labelId: string;
}) => { }) => {
const { t } = useTranslate();
const { control } = useFormContext<RuleFormValues>(); const { control } = useFormContext<RuleFormValues>();
return ( return (
@@ -56,7 +58,12 @@ const AnnotationHeaderField = ({
); );
}} }}
control={control} 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.'),
},
}}
/> />
} }
</label> </label>
@@ -40,7 +40,15 @@ export const CloudEvaluationBehavior = () => {
<div className={styles.flexRow}> <div className={styles.flexRow}>
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}> <Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
<Input <Input
{...register('forTime', { pattern: { value: /^\d+$/, message: 'Must be a positive integer.' } })} {...register('forTime', {
pattern: {
value: /^\d+$/,
message: t(
'alerting.cloud-evaluation-behavior.message.must-be-a-positive-integer',
'Must be a positive integer.'
),
},
})}
width={8} width={8}
/> />
</Field> </Field>
@@ -69,7 +69,10 @@ export function FolderSelector() {
)} )}
name="folder" name="folder"
rules={{ rules={{
required: { value: true, message: 'Select a folder' }, required: {
value: true,
message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'),
},
}} }}
/> />
<CreateNewFolder onCreate={handleFolderCreation} /> <CreateNewFolder onCreate={handleFolderCreation} />
@@ -5,7 +5,7 @@ import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } fr
import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal'; import { t } from '@grafana/i18n/internal';
import { import {
Box, Box,
@@ -74,7 +74,7 @@ const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) =>
const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({ const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({
required: { required: {
value: true, value: true,
message: 'Required.', message: t('alerting.for-validation-options.message.required', 'Required.'),
}, },
validate: (value) => { validate: (value) => {
// parsePrometheusDuration does not allow 0 but does allow 0s // parsePrometheusDuration does not allow 0 but does allow 0s
@@ -118,6 +118,7 @@ export function GrafanaEvaluationBehaviorStep({
existing: boolean; existing: boolean;
enableProvisionedGroups: boolean; enableProvisionedGroups: boolean;
}) { }) {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false); const [showErrorHandling, setShowErrorHandling] = useState(false);
@@ -237,7 +238,13 @@ export function GrafanaEvaluationBehaviorStep({
name="group" name="group"
control={control} control={control}
rules={{ 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'
),
},
}} }}
/> />
</Field> </Field>
@@ -380,7 +387,13 @@ export function GrafanaEvaluationBehaviorStep({
)} )}
id="missing-series-resolve" id="missing-series-resolve"
{...register('missingSeriesEvalsToResolve', { {...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} width={21}
/> />
@@ -402,6 +415,7 @@ function EvaluationGroupCreationModal({
onCreate: (group: string, evaluationInterval: string) => void; onCreate: (group: string, evaluationInterval: string) => void;
groupfoldersForGrafana?: RulerRulesConfigDTO | null; groupfoldersForGrafana?: RulerRulesConfigDTO | null;
}): React.ReactElement { }): React.ReactElement {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { watch } = useFormContext<RuleFormValues>(); const { watch } = useFormContext<RuleFormValues>();
@@ -477,7 +491,12 @@ function EvaluationGroupCreationModal({
autoFocus={true} autoFocus={true}
id={evaluationGroupNameId} id={evaluationGroupNameId}
placeholder={t('alerting.evaluation-group-creation-modal.placeholder-enter-a-name', 'Enter a name')} 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.'),
},
})}
/> />
</Field> </Field>
@@ -73,7 +73,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
name="namespace" name="namespace"
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'Required.' }, required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
}} }}
/> />
</Field> </Field>
@@ -105,7 +105,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
name="group" name="group"
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'Required.' }, required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
}} }}
/> />
</Field> </Field>
@@ -153,14 +153,27 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
* *
*/ */
function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) { function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) {
const { t } = useTranslate();
const { watch, setValue } = useFormContext<RuleFormValues>(); const { watch, setValue } = useFormContext<RuleFormValues>();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [manualRouting] = watch(['manualRouting']); const [manualRouting] = watch(['manualRouting']);
const routingOptions = [ 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) => { const onRoutingOptionChange = (option: RoutingOptions) => {
@@ -78,7 +78,10 @@ export function ContactPointSelector({ alertManager, onSelectContactPoint }: Con
rules={{ rules={{
required: { required: {
value: true, value: true,
message: 'Contact point is required.', message: t(
'alerting.contact-point-selector.message.contact-point-is-required',
'Contact point is required.'
),
}, },
}} }}
control={control} control={control}
@@ -116,6 +116,7 @@ export function useCombinedLabels(
labelsInSubform: Array<{ key: string; value: string }>, labelsInSubform: Array<{ key: string; value: string }>,
selectedKey: string selectedKey: string
) { ) {
const { t } = useTranslate();
// ------- Get labels keys and their values from existing alerts // ------- Get labels keys and their values from existing alerts
const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName); 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. // ------- 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 // create two groups of labels, one for ops and one for custom
const groupedOptions = [ const groupedOptions = [
{ {
label: 'From alerts', label: t('alerting.use-combined-labels.grouped-options.label.from-alerts', 'From alerts'),
options: keysFromExistingAlerts, options: keysFromExistingAlerts,
expanded: true, expanded: true,
}, },
{ {
label: 'From system', label: t('alerting.use-combined-labels.grouped-options.label.from-system', 'From system'),
options: keysFromGopsLabels, options: keysFromGopsLabels,
expanded: true, expanded: true,
}, },
@@ -372,7 +373,10 @@ export const LabelsWithoutSuggestions: FC = () => {
> >
<Input <Input
{...register(`labels.${index}.key`, { {...register(`labels.${index}.key`, {
required: { value: !!labels[index]?.value, message: 'Required.' }, required: {
value: !!labels[index]?.value,
message: t('alerting.labels-without-suggestions.message.required', 'Required.'),
},
})} })}
placeholder={t('alerting.labels-without-suggestions.placeholder-key', 'key')} placeholder={t('alerting.labels-without-suggestions.placeholder-key', 'key')}
data-testid={`label-key-${index}`} data-testid={`label-key-${index}`}
@@ -387,7 +391,10 @@ export const LabelsWithoutSuggestions: FC = () => {
> >
<Input <Input
{...register(`labels.${index}.value`, { {...register(`labels.${index}.value`, {
required: { value: !!labels[index]?.key, message: 'Required.' }, required: {
value: !!labels[index]?.key,
message: t('alerting.labels-without-suggestions.message.required', 'Required.'),
},
})} })}
placeholder={t('alerting.labels-without-suggestions.placeholder-value', 'value')} placeholder={t('alerting.labels-without-suggestions.placeholder-value', 'value')}
data-testid={`label-value-${index}`} data-testid={`label-value-${index}`}
@@ -53,7 +53,13 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
name="dataSourceName" name="dataSourceName"
control={control} control={control}
rules={{ rules={{
required: { value: true, message: 'Please select a data source' }, required: {
value: true,
message: t(
'alerting.cloud-data-source-selector.message.please-select-a-data-source',
'Please select a data source'
),
},
}} }}
/> />
</Field> </Field>
@@ -544,7 +544,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod
}} }}
control={control} control={control}
rules={{ 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'
),
},
}} }}
/> />
</Field> </Field>
@@ -5,6 +5,7 @@ import { useMeasure } from 'react-use';
import { NavModelItem, UrlQueryValue } from '@grafana/data'; import { NavModelItem, UrlQueryValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { import {
Alert, Alert,
LinkButton, LinkButton,
@@ -205,14 +206,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
<Text variant="bodySmall">{truncatedUrl}</Text> <Text variant="bodySmall">{truncatedUrl}</Text>
); );
metadata.push({ metadata.push({
label: 'Runbook URL', label: t('alerting.create-metadata.label.runbook-url', 'Runbook URL'),
value: valueToAdd, value: valueToAdd,
}); });
} }
if (hasDashboardAndPanel) { if (hasDashboardAndPanel) {
metadata.push({ metadata.push({
label: 'Dashboard and panel', label: t('alerting.create-metadata.label.dashboard-and-panel', 'Dashboard and panel'),
value: ( value: (
<WithReturnButton <WithReturnButton
title={rule.name} title={rule.name}
@@ -226,7 +227,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
}); });
} else if (hasDashboard) { } else if (hasDashboard) {
metadata.push({ metadata.push({
label: 'Dashboard', label: t('alerting.create-metadata.label.dashboard', 'Dashboard'),
value: ( value: (
<WithReturnButton <WithReturnButton
title={rule.name} title={rule.name}
@@ -242,14 +243,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
if (rulerRuleType.grafana.recordingRule(rule.rulerRule)) { if (rulerRuleType.grafana.recordingRule(rule.rulerRule)) {
const metric = rule.rulerRule?.grafana_alert.record?.metric ?? ''; const metric = rule.rulerRule?.grafana_alert.record?.metric ?? '';
metadata.push({ metadata.push({
label: 'Metric name', label: t('alerting.create-metadata.label.metric-name', 'Metric name'),
value: <Text color="primary">{metric}</Text>, value: <Text color="primary">{metric}</Text>,
}); });
} }
if (interval) { if (interval) {
metadata.push({ metadata.push({
label: 'Evaluation interval', label: t('alerting.create-metadata.label.evaluation-interval', 'Evaluation interval'),
value: ( value: (
<Text color="primary"> <Text color="primary">
<Trans i18nKey="alerting.rule-viewer.evaluation-interval">Every {{ interval }}</Trans> <Trans i18nKey="alerting.rule-viewer.evaluation-interval">Every {{ interval }}</Trans>
@@ -260,7 +261,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
if (hasLabels) { if (hasLabels) {
metadata.push({ metadata.push({
label: 'Labels', label: t('alerting.create-metadata.label.labels', 'Labels'),
/* TODO truncate number of labels, maybe build in to component? */ /* TODO truncate number of labels, maybe build in to component? */
value: <AlertLabels labels={labels} size="sm" />, value: <AlertLabels labels={labels} size="sm" />,
}); });
@@ -415,14 +416,14 @@ function usePageNav(rule: CombinedRule) {
subTitle: summary, subTitle: summary,
children: [ children: [
{ {
text: 'Query and conditions', text: t('alerting.use-page-nav.page-nav.text.query-and-conditions', 'Query and conditions'),
active: activeTab === ActiveTab.Query, active: activeTab === ActiveTab.Query,
onClick: () => { onClick: () => {
setActiveTab(ActiveTab.Query); setActiveTab(ActiveTab.Query);
}, },
}, },
{ {
text: 'Instances', text: t('alerting.use-page-nav.page-nav.text.instances', 'Instances'),
active: activeTab === ActiveTab.Instances, active: activeTab === ActiveTab.Instances,
onClick: () => { onClick: () => {
setActiveTab(ActiveTab.Instances); setActiveTab(ActiveTab.Instances);
@@ -431,7 +432,7 @@ function usePageNav(rule: CombinedRule) {
hideFromTabs: isRecordingRuleType, hideFromTabs: isRecordingRuleType,
}, },
{ {
text: 'History', text: t('alerting.use-page-nav.page-nav.text.history', 'History'),
active: activeTab === ActiveTab.History, active: activeTab === ActiveTab.History,
onClick: () => { onClick: () => {
setActiveTab(ActiveTab.History); setActiveTab(ActiveTab.History);
@@ -440,14 +441,14 @@ function usePageNav(rule: CombinedRule) {
hideFromTabs: !isGrafanaAlertRule, hideFromTabs: !isGrafanaAlertRule,
}, },
{ {
text: 'Details', text: t('alerting.use-page-nav.page-nav.text.details', 'Details'),
active: activeTab === ActiveTab.Details, active: activeTab === ActiveTab.Details,
onClick: () => { onClick: () => {
setActiveTab(ActiveTab.Details); setActiveTab(ActiveTab.Details);
}, },
}, },
{ {
text: 'Versions', text: t('alerting.use-page-nav.page-nav.text.versions', 'Versions'),
active: activeTab === ActiveTab.VersionHistory, active: activeTab === ActiveTab.VersionHistory,
onClick: () => { onClick: () => {
setActiveTab(ActiveTab.VersionHistory); setActiveTab(ActiveTab.VersionHistory);
@@ -259,8 +259,8 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
</Label> </Label>
<RadioButtonGroup<'hide'> <RadioButtonGroup<'hide'>
options={[ options={[
{ label: 'Show', value: undefined }, { label: t('alerting.rules-filter.label.show', 'Show'), value: undefined },
{ label: 'Hide', value: 'hide' }, { label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' },
]} ]}
value={filterState.plugins} value={filterState.plugins}
onChange={(value) => updateFilters({ ...filterState, plugins: value })} onChange={(value) => updateFilters({ ...filterState, plugins: value })}
@@ -81,6 +81,7 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
} }
const FilterOptions = () => { const FilterOptions = () => {
const { t } = useTranslate();
return ( return (
<Stack direction="column" alignItems="end" gap={2}> <Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center"> <Grid columns={2} gap={2} alignItems="center">
@@ -110,11 +111,11 @@ const FilterOptions = () => {
<RadioButtonGroup <RadioButtonGroup
value={'*'} value={'*'}
options={[ options={[
{ label: 'All', value: '*' }, { label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: 'Normal', value: 'normal' }, { label: t('alerting.filter-options.label.normal', 'Normal'), value: 'normal' },
{ label: 'Pending', value: 'pending' }, { label: t('alerting.filter-options.label.pending', 'Pending'), value: 'pending' },
{ label: 'Recovering', value: 'recovering' }, { label: t('alerting.filter-options.label.recovering', 'Recovering'), value: 'recovering' },
{ label: 'Firing', value: 'firing' }, { label: t('alerting.filter-options.label.firing', 'Firing'), value: 'firing' },
]} ]}
/> />
<Label> <Label>
@@ -123,9 +124,9 @@ const FilterOptions = () => {
<RadioButtonGroup <RadioButtonGroup
value={'*'} value={'*'}
options={[ options={[
{ label: 'All', value: '*' }, { label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: 'Alert rule', value: 'alerting' }, { label: t('alerting.filter-options.label.alert-rule', 'Alert rule'), value: 'alerting' },
{ label: 'Recording rule', value: 'recording' }, { label: t('alerting.filter-options.label.recording-rule', 'Recording rule'), value: 'recording' },
]} ]}
/> />
<Label> <Label>
@@ -134,10 +135,10 @@ const FilterOptions = () => {
<RadioButtonGroup <RadioButtonGroup
value={'*'} value={'*'}
options={[ options={[
{ label: 'All', value: '*' }, { label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: 'OK', value: 'ok' }, { label: t('alerting.filter-options.label.ok', 'OK'), value: 'ok' },
{ label: 'No data', value: 'no_data' }, { label: t('alerting.filter-options.label.no-data', 'No data'), value: 'no_data' },
{ label: 'Error', value: 'error' }, { label: t('alerting.filter-options.label.error', 'Error'), value: 'error' },
]} ]}
/> />
</Grid> </Grid>
@@ -8,6 +8,7 @@ import {
isUnsignedPluginSignature, isUnsignedPluginSignature,
} from '@grafana/data'; } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useTranslate } from '@grafana/i18n';
import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime'; import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/internal'; import { ExpressionDatasourceRef } from '@grafana/runtime/internal';
import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui'; import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui';
@@ -20,6 +21,7 @@ export interface MultipleDataSourcePickerProps extends Omit<DataSourcePickerProp
} }
export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) => { export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) => {
const { t } = useTranslate();
const dataSourceSrv = getDataSourceSrv(); const dataSourceSrv = getDataSourceSrv();
const [state, setState] = useState<DataSourcePickerState>(); const [state, setState] = useState<DataSourcePickerState>();
@@ -121,8 +123,22 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) =
})); }));
const groupedOptions = [ 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; return groupedOptions;
@@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'react';
import Skeleton from 'react-loading-skeleton'; import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Pagination, Tooltip, useStyles2 } from '@grafana/ui'; import { Pagination, Tooltip, useStyles2 } from '@grafana/ui';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting'; import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
@@ -192,17 +193,18 @@ function useColumns(
showNextEvaluationColumn: boolean, showNextEvaluationColumn: boolean,
isRulerLoading: boolean isRulerLoading: boolean
) { ) {
const { t } = useTranslate();
return useMemo((): RuleTableColumnProps[] => { return useMemo((): RuleTableColumnProps[] => {
const columns: RuleTableColumnProps[] = [ const columns: RuleTableColumnProps[] = [
{ {
id: 'state', id: 'state',
label: 'State', label: t('alerting.use-columns.columns.label.state', 'State'),
renderCell: ({ data: rule }) => <RuleStateCell rule={rule} />, renderCell: ({ data: rule }) => <RuleStateCell rule={rule} />,
size: '165px', size: '165px',
}, },
{ {
id: 'name', id: 'name',
label: 'Name', label: t('alerting.use-columns.columns.label.name', 'Name'),
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => rule.name, renderCell: ({ data: rule }) => rule.name,
size: showNextEvaluationColumn ? 4 : 5, size: showNextEvaluationColumn ? 4 : 5,
@@ -237,7 +239,7 @@ function useColumns(
}, },
{ {
id: 'health', id: 'health',
label: 'Health', label: t('alerting.use-columns.columns.label.health', 'Health'),
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: { promRule, group } }) => (promRule ? <RuleHealth rule={promRule} /> : null), renderCell: ({ data: { promRule, group } }) => (promRule ? <RuleHealth rule={promRule} /> : null),
size: '75px', size: '75px',
@@ -246,7 +248,7 @@ function useColumns(
if (showSummaryColumn) { if (showSummaryColumn) {
columns.push({ columns.push({
id: 'summary', id: 'summary',
label: 'Summary', label: t('alerting.use-columns.label.summary', 'Summary'),
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />; return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />;
@@ -258,7 +260,7 @@ function useColumns(
if (showNextEvaluationColumn) { if (showNextEvaluationColumn) {
columns.push({ columns.push({
id: 'nextEvaluation', id: 'nextEvaluation',
label: 'Next evaluation', label: t('alerting.use-columns.label.next-evaluation', 'Next evaluation'),
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
const nextEvalInfo = calculateNextEvaluationEstimate(rule.promRule?.lastEvaluation, rule.group.interval); const nextEvalInfo = calculateNextEvaluationEstimate(rule.promRule?.lastEvaluation, rule.group.interval);
@@ -282,7 +284,7 @@ function useColumns(
if (showGroupColumn) { if (showGroupColumn) {
columns.push({ columns.push({
id: 'group', id: 'group',
label: 'Group', label: t('alerting.use-columns.label.group', 'Group'),
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => { renderCell: ({ data: rule }) => {
const { namespace, group } = rule; const { namespace, group } = rule;
@@ -301,14 +303,14 @@ function useColumns(
} }
columns.push({ columns.push({
id: 'actions', id: 'actions',
label: 'Actions', label: t('alerting.use-columns.label.actions', 'Actions'),
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => <RuleActionsCell rule={rule} isLoadingRuler={isRulerLoading} />, renderCell: ({ data: rule }) => <RuleActionsCell rule={rule} isLoadingRuler={isRulerLoading} />,
size: '215px', size: '215px',
}); });
return columns; return columns;
}, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isRulerLoading]); }, [t, showNextEvaluationColumn, showSummaryColumn, showGroupColumn, isRulerLoading]);
} }
function RuleStateCell({ rule }: { rule: CombinedRule }) { function RuleStateCell({ rule }: { rule: CombinedRule }) {
@@ -67,6 +67,7 @@ export const StateFilterValues = {
export const CentralAlertHistoryScene = () => { export const CentralAlertHistoryScene = () => {
//track the loading of the central alert state history //track the loading of the central alert state history
const { t } = useTranslate();
useEffect(() => { useEffect(() => {
logInfo(LogMessages.loadedCentralAlertStateHistory); logInfo(LogMessages.loadedCentralAlertStateHistory);
}, []); }, []);
@@ -78,14 +79,17 @@ export const CentralAlertHistoryScene = () => {
// textbox variable for filtering by labels // textbox variable for filtering by labels
const labelsFilterVariable = new TextBoxVariable({ const labelsFilterVariable = new TextBoxVariable({
name: LABELS_FILTER, 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 //custom variable for filtering by the current state
const transitionsToFilterVariable = new CustomVariable({ const transitionsToFilterVariable = new CustomVariable({
name: STATE_FILTER_TO, name: STATE_FILTER_TO,
value: StateFilterValues.all, 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, hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending},To Recovering : ${StateFilterValues.recovering}`, 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({ const transitionsFromFilterVariable = new CustomVariable({
name: STATE_FILTER_FROM, name: STATE_FILTER_FROM,
value: StateFilterValues.all, 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, hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending},From Recovering : ${StateFilterValues.recovering}`, 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 // we need to call this to sync the url with the scene state
const isUrlSyncInitialized = useUrlSync(scene); const isUrlSyncInitialized = useUrlSync(scene);
@@ -1,6 +1,7 @@
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data'; import { DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { RuntimeDataSource, sceneUtils } from '@grafana/scenes'; import { RuntimeDataSource, sceneUtils } from '@grafana/scenes';
import { DataQuery } from '@grafana/schema'; import { DataQuery } from '@grafana/schema';
@@ -72,7 +73,11 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
} }
testDatasource(): Promise<TestDataSourceResponse> { testDatasource(): Promise<TestDataSourceResponse> {
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'),
});
} }
} }
@@ -2,6 +2,7 @@ import { memo } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data'; import { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { VisibilityMode } from '@grafana/schema'; import { VisibilityMode } from '@grafana/schema';
import { LegendDisplayMode, useTheme2 } from '@grafana/ui'; import { LegendDisplayMode, useTheme2 } from '@grafana/ui';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
@@ -16,6 +17,7 @@ interface LogTimelineViewerProps {
const replaceVariables: InterpolateFunction = (v) => v; const replaceVariables: InterpolateFunction = (v) => v;
export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerProps) => { export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerProps) => {
const { t } = useTranslate();
const theme = useTheme2(); const theme = useTheme2();
return ( return (
@@ -38,12 +40,36 @@ export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerP
showLegend: true, showLegend: true,
}} }}
legendItems={[ legendItems={[
{ label: 'Normal', color: theme.colors.success.main, yAxis: 1 }, {
{ label: 'Pending', color: theme.colors.warning.main, yAxis: 1 }, label: t('alerting.log-timeline-viewer.label.normal', 'Normal'),
{ label: 'Recovering', color: theme.colors.warning.main, yAxis: 1 }, color: theme.colors.success.main,
{ label: 'Firing', color: theme.colors.error.main, yAxis: 1 }, 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.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} replaceVariables={replaceVariables}
/> />
@@ -59,9 +59,19 @@ const StateHistory = ({ ruleUID }: Props) => {
} }
const columns: Array<DynamicTableColumnProps<StateHistoryRowItem>> = [ const columns: Array<DynamicTableColumnProps<StateHistoryRowItem>> = [
{ 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: '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 // group the state history list by unique set of labels
@@ -80,7 +80,10 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave
// manually register the config field with validation // manually register the config field with validation
// @TODO sometimes the value doesn't get registered – find out why // @TODO sometimes the value doesn't get registered – find out why
register('configJSON', { 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) => { validate: (value: string) => {
try { try {
JSON.parse(value); JSON.parse(value);
@@ -63,7 +63,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
> >
<Input <Input
{...register(`matchers.${index}.name` as const, { {...register(`matchers.${index}.name` as const, {
required: { value: required, message: 'Required.' }, required: {
value: required,
message: t('alerting.matchers-field.message.required', 'Required.'),
},
})} })}
defaultValue={matcher.name} defaultValue={matcher.name}
placeholder={t('alerting.matchers-field.placeholder-label', 'label')} placeholder={t('alerting.matchers-field.placeholder-label', 'label')}
@@ -85,7 +88,12 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
)} )}
defaultValue={matcher.operator || matcherFieldOptions[0].value} defaultValue={matcher.operator || matcherFieldOptions[0].value}
name={`matchers.${index}.operator`} name={`matchers.${index}.operator`}
rules={{ required: { value: required, message: 'Required.' } }} rules={{
required: {
value: required,
message: t('alerting.matchers-field.message.required', 'Required.'),
},
}}
/> />
</Field> </Field>
<Field <Field
@@ -95,7 +103,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
> >
<Input <Input
{...register(`matchers.${index}.value` as const, { {...register(`matchers.${index}.value` as const, {
required: { value: required, message: 'Required.' }, required: {
value: required,
message: t('alerting.matchers-field.message.required', 'Required.'),
},
})} })}
defaultValue={matcher.value} defaultValue={matcher.value}
placeholder={t('alerting.matchers-field.placeholder-value', 'value')} placeholder={t('alerting.matchers-field.placeholder-value', 'value')}
@@ -148,12 +148,13 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
}; };
function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> { function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
return [ return [
{ {
id: 'state', id: 'state',
label: 'State', label: t('alerting.use-columns.label.state', 'State'),
renderCell: function renderStateTag({ data }) { renderCell: function renderStateTag({ data }) {
return <AmAlertStateTag state={data.status.state} />; return <AmAlertStateTag state={data.status.state} />;
}, },
@@ -162,7 +163,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
}, },
{ {
id: 'labels', id: 'labels',
label: 'Labels', label: t('alerting.use-columns.label.labels', 'Labels'),
renderCell: function renderName({ data }) { renderCell: function renderName({ data }) {
return <AlertLabels labels={data.labels} size="sm" />; return <AlertLabels labels={data.labels} size="sm" />;
}, },
@@ -170,7 +171,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
}, },
{ {
id: 'created', id: 'created',
label: 'Created', label: t('alerting.use-columns.label.created', 'Created'),
renderCell: function renderSummary({ data }) { renderCell: function renderSummary({ data }) {
return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}</>; return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}</>;
}, },
@@ -254,7 +254,9 @@ export const SilencesEditor = ({
invalid={!!formState.errors.comment} invalid={!!formState.errors.comment}
> >
<TextArea <TextArea
{...register('comment', { required: { value: true, message: 'Required.' } })} {...register('comment', {
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
})}
rows={5} rows={5}
placeholder={t( placeholder={t(
'alerting.silences-editor.comment-placeholder-details-about-the-silence', 'alerting.silences-editor.comment-placeholder-details-about-the-silence',
@@ -271,7 +273,9 @@ export const SilencesEditor = ({
invalid={!!formState.errors.createdBy} invalid={!!formState.errors.createdBy}
> >
<Input <Input
{...register('createdBy', { required: { value: true, message: 'Required.' } })} {...register('createdBy', {
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
})}
placeholder={t( placeholder={t(
'alerting.silences-editor.placeholder-whos-creating-the-silence', 'alerting.silences-editor.placeholder-whos-creating-the-silence',
"Who's creating the silence" "Who's creating the silence"
@@ -321,9 +325,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
}); });
function ExistingSilenceEditorPage() { function ExistingSilenceEditorPage() {
const { t } = useTranslate();
const pageNav = { const pageNav = {
id: 'silence-edit', id: 'silence-edit',
text: 'Edit silence', text: t('alerting.existing-silence-editor-page.page-nav.text.edit-silence', 'Edit silence'),
subTitle: 'Recreate existing silence to stop notifications from a particular alert rule', subTitle: 'Recreate existing silence to stop notifications from a particular alert rule',
}; };
return ( return (
@@ -293,7 +293,7 @@ function useColumns(alertManagerSourceName: string) {
const columns: SilenceTableColumnProps[] = [ const columns: SilenceTableColumnProps[] = [
{ {
id: 'state', id: 'state',
label: 'State', label: t('alerting.use-columns.columns.label.state', 'State'),
renderCell: function renderStateTag({ data: { status } }) { renderCell: function renderStateTag({ data: { status } }) {
return <SilenceStateTag state={status.state} />; return <SilenceStateTag state={status.state} />;
}, },
@@ -301,7 +301,7 @@ function useColumns(alertManagerSourceName: string) {
}, },
{ {
id: 'alert-rule', id: 'alert-rule',
label: 'Alert rule targeted', label: t('alerting.use-columns.columns.label.alert-rule-targeted', 'Alert rule targeted'),
renderCell: function renderAlertRuleLink({ data: { metadata } }) { renderCell: function renderAlertRuleLink({ data: { metadata } }) {
return metadata?.rule_title ? ( return metadata?.rule_title ? (
<Link <Link
@@ -317,7 +317,7 @@ function useColumns(alertManagerSourceName: string) {
}, },
{ {
id: 'matchers', id: 'matchers',
label: 'Matching labels', label: t('alerting.use-columns.columns.label.matching-labels', 'Matching labels'),
renderCell: function renderMatchers({ data: { matchers } }) { renderCell: function renderMatchers({ data: { matchers } }) {
const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || []; const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || [];
return <Matchers matchers={filteredMatchers} />; return <Matchers matchers={filteredMatchers} />;
@@ -326,7 +326,7 @@ function useColumns(alertManagerSourceName: string) {
}, },
{ {
id: 'alerts', id: 'alerts',
label: 'Alerts silenced', label: t('alerting.use-columns.columns.label.alerts-silenced', 'Alerts silenced'),
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) { renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return ( return (
<span data-testid="alerts"> <span data-testid="alerts">
@@ -341,7 +341,7 @@ function useColumns(alertManagerSourceName: string) {
}, },
{ {
id: 'schedule', id: 'schedule',
label: 'Schedule', label: t('alerting.use-columns.columns.label.schedule', 'Schedule'),
renderCell: function renderSchedule({ data: { startsAt, endsAt } }) { renderCell: function renderSchedule({ data: { startsAt, endsAt } }) {
const startsAtDate = dateMath.parse(startsAt); const startsAtDate = dateMath.parse(startsAt);
const endsAtDate = dateMath.parse(endsAt); const endsAtDate = dateMath.parse(endsAt);
@@ -354,7 +354,7 @@ function useColumns(alertManagerSourceName: string) {
if (updateSupported) { if (updateSupported) {
columns.push({ columns.push({
id: 'actions', id: 'actions',
label: 'Actions', label: t('alerting.use-columns.label.actions', 'Actions'),
renderCell: function renderActions({ data: silence }) { renderCell: function renderActions({ data: silence }) {
const isExpired = silence.status.state === SilenceState.Expired; const isExpired = silence.status.state === SilenceState.Expired;
@@ -1,5 +1,6 @@
import { FieldValues, RegisterOptions } from 'react-hook-form'; import { FieldValues, RegisterOptions } from 'react-hook-form';
import { t } from '@grafana/i18n/internal';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
import { MIN_TIME_RANGE_STEP_S } from '../components/rule-editor/GrafanaEvaluationBehavior'; import { MIN_TIME_RANGE_STEP_S } from '../components/rule-editor/GrafanaEvaluationBehavior';
@@ -10,7 +11,7 @@ import { formatPrometheusDuration, parsePrometheusDuration, safeParsePrometheusD
export const evaluateEveryValidationOptions = <T extends FieldValues>(rules: RulerRuleDTO[]): RegisterOptions<T> => ({ export const evaluateEveryValidationOptions = <T extends FieldValues>(rules: RulerRuleDTO[]): RegisterOptions<T> => ({
required: { required: {
value: true, value: true,
message: 'Required.', message: t('alerting.evaluate-every-validation-options.message.required', 'Required.'),
}, },
validate: (evaluateEvery: string) => { validate: (evaluateEvery: string) => {
try { try {
@@ -1,5 +1,6 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data'; import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { import {
EmbeddedScene, EmbeddedScene,
@@ -214,7 +215,7 @@ export function getInsightsScenes() {
function getGrafanaManagedScenes() { function getGrafanaManagedScenes() {
return new NestedScene({ return new NestedScene({
title: 'Grafana-managed alert rules', title: t('alerting.get-grafana-managed-scenes.title.grafanamanaged-alert-rules', 'Grafana-managed alert rules'),
canCollapse: true, canCollapse: true,
isCollapsed: false, isCollapsed: false,
body: new SceneFlexLayout({ body: new SceneFlexLayout({
@@ -304,7 +305,7 @@ function getGrafanaManagedScenes() {
function getGrafanaAlertmanagerScenes() { function getGrafanaAlertmanagerScenes() {
return new NestedScene({ return new NestedScene({
title: 'Grafana Alertmanager', title: t('alerting.get-grafana-alertmanager-scenes.title.grafana-alertmanager', 'Grafana Alertmanager'),
canCollapse: true, canCollapse: true,
isCollapsed: false, isCollapsed: false,
body: new SceneFlexLayout({ body: new SceneFlexLayout({
@@ -327,7 +328,7 @@ function getGrafanaAlertmanagerScenes() {
function getCloudScenes() { function getCloudScenes() {
return new NestedScene({ return new NestedScene({
title: 'Mimir Alertmanager', title: t('alerting.get-cloud-scenes.title.mimir-alertmanager', 'Mimir Alertmanager'),
canCollapse: true, canCollapse: true,
isCollapsed: false, isCollapsed: false,
body: new SceneFlexLayout({ body: new SceneFlexLayout({
@@ -361,7 +362,7 @@ function getCloudScenes() {
function getMimirManagedRulesScenes() { function getMimirManagedRulesScenes() {
return new NestedScene({ return new NestedScene({
title: 'Mimir-managed alert rules', title: t('alerting.get-mimir-managed-rules-scenes.title.mimirmanaged-alert-rules', 'Mimir-managed alert rules'),
canCollapse: true, canCollapse: true,
isCollapsed: false, isCollapsed: false,
body: new SceneFlexLayout({ body: new SceneFlexLayout({
@@ -402,14 +403,17 @@ function getMimirManagedRulesScenes() {
function getMimirManagedRulesPerGroupScenes() { function getMimirManagedRulesPerGroupScenes() {
const ruleGroupHandler = new QueryVariable({ const ruleGroupHandler = new QueryVariable({
label: 'Rule Group', label: t('alerting.get-mimir-managed-rules-per-group-scenes.rule-group-handler.label.rule-group', 'Rule Group'),
name: 'rule_group', name: 'rule_group',
datasource: cloudUsageDs, datasource: cloudUsageDs,
query: 'label_values(grafanacloud_instance_rule_group_rules,rule_group)', query: 'label_values(grafanacloud_instance_rule_group_rules,rule_group)',
}); });
return new NestedScene({ return new NestedScene({
title: 'Mimir-managed alert rules - per rule group', title: t(
'alerting.get-mimir-managed-rules-per-group-scenes.title.mimirmanaged-alert-rules-per-rule-group',
'Mimir-managed alert rules - per rule group'
),
canCollapse: true, canCollapse: true,
isCollapsed: false, isCollapsed: false,
body: new SceneFlexLayout({ body: new SceneFlexLayout({
@@ -1,6 +1,7 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DataQueryRequest, DataQueryResponse, DataQueryResponseData, TestDataSourceResponse } from '@grafana/data'; import { DataQueryRequest, DataQueryResponse, DataQueryResponseData, TestDataSourceResponse } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv } from '@grafana/runtime';
import { import {
PanelBuilders, PanelBuilders,
@@ -37,7 +38,11 @@ class LokiAPIDatasource extends RuntimeDataSource {
} }
testDatasource(): Promise<TestDataSourceResponse> { testDatasource(): Promise<TestDataSourceResponse> {
return Promise.resolve({ status: 'success', message: 'Data source is working', title: 'Success' }); return Promise.resolve({
status: 'success',
message: t('alerting.loki-apidatasource.message.data-source-is-working', 'Data source is working'),
title: t('alerting.loki-apidatasource.title.success', 'Success'),
});
} }
} }
@@ -1,3 +1,4 @@
/* eslint-disable @grafana/no-untranslated-strings */
import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data'; import { PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
import { AppPluginConfig, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime'; import { AppPluginConfig, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges'; import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
@@ -3,6 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
import { NavModelItem } from '@grafana/data'; import { NavModelItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n'; import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Badge, Stack, Text } from '@grafana/ui'; import { Badge, Stack, Text } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound'; import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound';
@@ -16,8 +17,11 @@ import { SSOProvider } from './types';
const getPageNav = (config?: SSOProvider): NavModelItem => { const getPageNav = (config?: SSOProvider): NavModelItem => {
if (!config) { if (!config) {
return { return {
text: 'Authentication', text: t('auth-config.get-page-nav.text.authentication', 'Authentication'),
subTitle: 'Configure authentication providers', subTitle: t(
'auth-config.get-page-nav.subTitle.configure-authentication-providers',
'Configure authentication providers'
),
icon: 'shield', icon: 'shield',
id: 'authentication', id: 'authentication',
}; };
+5 -1
View File
@@ -313,7 +313,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
), ),
multi: false, multi: false,
options: clientAuthenticationOptions(provider), options: clientAuthenticationOptions(provider),
defaultValue: { value: 'none', label: 'None' }, defaultValue: { value: 'none', label: t('auth-config.field-map.label.none', 'None') },
validation: { validation: {
required: true, required: true,
message: t('auth-config.fields.required', 'This field is required'), message: t('auth-config.fields.required', 'This field is required'),
@@ -405,11 +405,13 @@ export function fieldMap(provider: string): Record<string, FieldData> {
), ),
multi: false, multi: false,
options: [ options: [
/* eslint-disable @grafana/no-untranslated-strings */
{ value: 'AutoDetect', label: 'AutoDetect' }, { value: 'AutoDetect', label: 'AutoDetect' },
{ value: 'InParams', label: 'InParams' }, { value: 'InParams', label: 'InParams' },
{ value: 'InHeader', label: 'InHeader' }, { value: 'InHeader', label: 'InHeader' },
], ],
defaultValue: { value: 'AutoDetect', label: 'AutoDetect' }, defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },
/* eslint-enable @grafana/no-untranslated-strings */
}, },
tokenUrl: { tokenUrl: {
label: tokenURLLabel, label: tokenURLLabel,
@@ -884,6 +886,7 @@ function orgMappingDescription(provider: string): string {
function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> { function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> {
// Other options are purposefully not translated // Other options are purposefully not translated
/* eslint-disable @grafana/no-untranslated-strings */
switch (provider) { switch (provider) {
case 'azuread': case 'azuread':
return [ return [
@@ -898,4 +901,5 @@ function clientAuthenticationOptions(provider: string): Array<SelectableValue<st
{ value: 'client_secret_post', label: 'Client secret' }, { value: 'client_secret_post', label: 'Client secret' },
]; ];
} }
/* eslint-enable @grafana/no-untranslated-strings */
} }
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useState } from 'react'; import { useState } from 'react';
import { GrafanaTheme2, PluginState } from '@grafana/data'; import { GrafanaTheme2, PluginState } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { TextDimensionMode } from '@grafana/schema'; import { TextDimensionMode } from '@grafana/schema';
import { Button, Spinner, useStyles2 } from '@grafana/ui'; import { Button, Spinner, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
@@ -194,9 +195,9 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.button-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.button-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.button-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -206,7 +207,7 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.button-item.placeholder.auto', 'Auto'),
}, },
}) })
.addCustomEditor({ .addCustomEditor({
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -153,9 +154,9 @@ export const cloudItem: CanvasElementItem = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.cloud-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.cloud-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.cloud-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -166,9 +167,9 @@ export const cloudItem: CanvasElementItem = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.cloud-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.cloud-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.cloud-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -178,7 +179,7 @@ export const cloudItem: CanvasElementItem = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.cloud-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -159,9 +160,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.ellipse-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.ellipse-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.ellipse-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -172,9 +173,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.ellipse-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.ellipse-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.ellipse-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -184,7 +185,7 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.ellipse-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -4,6 +4,7 @@ import { useObservable } from 'react-use';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data'; import { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { TextDimensionMode } from '@grafana/schema'; import { TextDimensionMode } from '@grafana/schema';
import { usePanelContext, useStyles2 } from '@grafana/ui'; import { usePanelContext, useStyles2 } from '@grafana/ui';
import { FieldNamePicker, frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/internal'; import { FieldNamePicker, frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/internal';
@@ -220,9 +221,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.metric-value-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.metric-value-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.metric-value-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -233,9 +234,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.metric-value-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.metric-value-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.metric-value-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -245,7 +246,7 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.metric-value-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -153,9 +154,9 @@ export const parallelogramItem: CanvasElementItem = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.parallelogram-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.parallelogram-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.parallelogram-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -166,9 +167,9 @@ export const parallelogramItem: CanvasElementItem = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.parallelogram-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.parallelogram-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.parallelogram-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -178,7 +179,7 @@ export const parallelogramItem: CanvasElementItem = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.parallelogram-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { PureComponent } from 'react'; import { PureComponent } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { stylesFactory } from '@grafana/ui'; import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
@@ -120,9 +121,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.rectangle-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.rectangle-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.rectangle-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -133,9 +134,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.rectangle-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.rectangle-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.rectangle-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -145,7 +146,7 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.rectangle-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
+8 -7
View File
@@ -6,6 +6,7 @@ import { of } from 'rxjs';
import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n'; import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Input, usePanelContext, useStyles2 } from '@grafana/ui'; import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context'; import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -197,9 +198,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.text-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.text-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.text-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -210,9 +211,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.text-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.text-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.text-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -222,7 +223,7 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.text-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions'; import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor'; import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@@ -154,9 +155,9 @@ export const triangleItem: CanvasElementItem = {
name: 'Align text', name: 'Align text',
settings: { settings: {
options: [ options: [
{ value: Align.Left, label: 'Left' }, { value: Align.Left, label: t('canvas.triangle-item.label.left', 'Left') },
{ value: Align.Center, label: 'Center' }, { value: Align.Center, label: t('canvas.triangle-item.label.center', 'Center') },
{ value: Align.Right, label: 'Right' }, { value: Align.Right, label: t('canvas.triangle-item.label.right', 'Right') },
], ],
}, },
defaultValue: Align.Left, defaultValue: Align.Left,
@@ -167,9 +168,9 @@ export const triangleItem: CanvasElementItem = {
name: 'Vertical align', name: 'Vertical align',
settings: { settings: {
options: [ options: [
{ value: VAlign.Top, label: 'Top' }, { value: VAlign.Top, label: t('canvas.triangle-item.label.top', 'Top') },
{ value: VAlign.Middle, label: 'Middle' }, { value: VAlign.Middle, label: t('canvas.triangle-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: 'Bottom' }, { value: VAlign.Bottom, label: t('canvas.triangle-item.label.bottom', 'Bottom') },
], ],
}, },
defaultValue: VAlign.Middle, defaultValue: VAlign.Middle,
@@ -179,7 +180,7 @@ export const triangleItem: CanvasElementItem = {
path: 'config.size', path: 'config.size',
name: 'Text size', name: 'Text size',
settings: { settings: {
placeholder: 'Auto', placeholder: t('canvas.triangle-item.placeholder.auto', 'Auto'),
}, },
}); });
}, },
@@ -1,6 +1,7 @@
import { useLocation, useParams } from 'react-router-dom-v5-compat'; import { useLocation, useParams } from 'react-router-dom-v5-compat';
import { NavModel, NavModelItem } from '@grafana/data'; import { NavModel, NavModelItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { getNavModel } from 'app/core/selectors/navModel'; import { getNavModel } from 'app/core/selectors/navModel';
import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks'; import { useDataSource, useDataSourceMeta, useDataSourceSettings } from 'app/features/datasources/state/hooks';
@@ -9,6 +10,7 @@ import { useGetSingle } from 'app/features/plugins/admin/state/hooks';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
export function useDataSourceSettingsNav(pageIdParam?: string) { export function useDataSourceSettingsNav(pageIdParam?: string) {
const { t } = useTranslate();
const { uid = '' } = useParams<{ uid: string }>(); const { uid = '' } = useParams<{ uid: string }>();
const location = useLocation(); const location = useLocation();
const datasource = useDataSource(uid); const datasource = useDataSource(uid);
@@ -27,17 +29,17 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-settings-${uid}`; const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-settings-${uid}`;
let pageNav: NavModel = { let pageNav: NavModel = {
node: { node: {
text: 'Data Source Nav Node', text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
}, },
main: { main: {
text: 'Data Source Nav Node', text: t('connections.use-data-source-settings-nav.page-nav.text.data-source-nav-node', 'Data Source Nav Node'),
}, },
}; };
if (loadError) { if (loadError) {
const node: NavModelItem = { const node: NavModelItem = {
text: loadError, text: loadError,
subTitle: 'Data Source Error', subTitle: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'),
icon: 'exclamation-triangle', icon: 'exclamation-triangle',
}; };
@@ -1,6 +1,6 @@
import { useParams } from 'react-router-dom-v5-compat'; import { useParams } from 'react-router-dom-v5-compat';
import { Trans } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { Alert, Badge, TextLink } from '@grafana/ui'; import { Alert, Badge, TextLink } from '@grafana/ui';
import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage'; import { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage';
import { StoreState, useSelector, AppNotificationSeverity } from 'app/types'; import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
@@ -8,6 +8,7 @@ import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
import { ROUTES } from '../constants'; import { ROUTES } from '../constants';
export function DataSourceDetailsPage() { export function DataSourceDetailsPage() {
const { t } = useTranslate();
const overrideNavId = 'standalone-plugin-page-/connections/add-new-connection'; const overrideNavId = 'standalone-plugin-page-/connections/add-new-connection';
const { id = '' } = useParams<{ id: string }>(); const { id = '' } = useParams<{ id: string }>();
const navIndex = useSelector((state: StoreState) => state.navIndex); const navIndex = useSelector((state: StoreState) => state.navIndex);
@@ -20,8 +21,11 @@ export function DataSourceDetailsPage() {
navId={navId} navId={navId}
notFoundComponent={<NotFoundDatasource />} notFoundComponent={<NotFoundDatasource />}
notFoundNavModel={{ notFoundNavModel={{
text: 'Unknown datasource', text: t('connections.data-source-details-page.text.unknown-datasource', 'Unknown datasource'),
subTitle: 'No datasource with this ID could be found.', subTitle: t(
'connections.data-source-details-page.subTitle.datasource-could-found',
'No datasource with this ID could be found.'
),
active: true, active: true,
}} }}
/> />
@@ -1,11 +1,17 @@
import { useTranslate } from '@grafana/i18n';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { NewDataSource } from 'app/features/datasources/components/NewDataSource'; import { NewDataSource } from 'app/features/datasources/components/NewDataSource';
export function NewDataSourcePage() { export function NewDataSourcePage() {
const { t } = useTranslate();
return ( return (
<Page <Page
navId={'connections-datasources'} navId={'connections-datasources'}
pageNav={{ text: 'Add data source', subTitle: 'Choose a data source type', active: true }} pageNav={{
text: t('connections.new-data-source-page.text.add-data-source', 'Add data source'),
subTitle: t('connections.new-data-source-page.subTitle.choose-a-data-source-type', 'Choose a data source type'),
active: true,
}}
> >
<Page.Contents> <Page.Contents>
<NewDataSource /> <NewDataSource />
@@ -69,9 +69,12 @@ export function AddNewConnection() {
); );
const { t } = useTranslate(); const { t } = useTranslate();
const filterByOptions = [ const filterByOptions = [
{ value: 'all', label: 'All' }, { value: 'all', label: t('connections.add-new-connection.filter-by-options.label.all', 'All') },
{ value: 'installed', label: 'Installed' }, { value: 'installed', label: t('connections.add-new-connection.filter-by-options.label.installed', 'Installed') },
{ value: 'has-update', label: 'New Updates' }, {
value: 'has-update',
label: t('connections.add-new-connection.filter-by-options.label.new-updates', 'New Updates'),
},
]; ];
const onClickCardGridItem = (e: MouseEvent<HTMLElement>, item: CardGridItem) => { const onClickCardGridItem = (e: MouseEvent<HTMLElement>, item: CardGridItem) => {
@@ -160,11 +163,17 @@ export function AddNewConnection() {
value={sortBy?.toString()} value={sortBy?.toString()}
onChange={onSortByChange} onChange={onSortByChange}
options={[ options={[
{ value: 'nameAsc', label: 'By name (A-Z)' }, { value: 'nameAsc', label: t('connections.add-new-connection.label.by-name-az', 'By name (A-Z)') },
{ value: 'nameDesc', label: 'By name (Z-A)' }, { value: 'nameDesc', label: t('connections.add-new-connection.label.by-name-za', 'By name (Z-A)') },
{ value: 'updated', label: 'By updated date' }, {
{ value: 'published', label: 'By published date' }, value: 'updated',
{ value: 'downloads', label: 'By downloads' }, label: t('connections.add-new-connection.label.by-updated-date', 'By updated date'),
},
{
value: 'published',
label: t('connections.add-new-connection.label.by-published-date', 'By published date'),
},
{ value: 'downloads', label: t('connections.add-new-connection.label.by-downloads', 'By downloads') },
]} ]}
/> />
</Field> </Field>
@@ -66,14 +66,14 @@ export function AddToDashboardForm<TOptions = undefined>({
if (canCreateDashboard) { if (canCreateDashboard) {
saveTargets.push({ saveTargets.push({
label: 'New dashboard', label: t('dashboard-scene.add-to-dashboard-form.label.new-dashboard', 'New dashboard'),
value: SaveTarget.NewDashboard, value: SaveTarget.NewDashboard,
}); });
} }
if (canWriteDashboard) { if (canWriteDashboard) {
saveTargets.push({ saveTargets.push({
label: 'Existing dashboard', label: t('dashboard-scene.add-to-dashboard-form.label.existing-dashboard', 'Existing dashboard'),
value: SaveTarget.ExistingDashboard, value: SaveTarget.ExistingDashboard,
}); });
} }
@@ -154,7 +154,15 @@ export function AddToDashboardForm<TOptions = undefined>({
control={control} control={control}
name="dashboardUid" name="dashboardUid"
shouldUnregister shouldUnregister
rules={{ required: { value: true, message: 'This field is required.' } }} rules={{
required: {
value: true,
message: t(
'dashboard-scene.add-to-dashboard-form.message.this-field-is-required',
'This field is required.'
),
},
}}
/> />
); );
})()} })()}
@@ -1,4 +1,5 @@
import { locationUtil, TimeRange } from '@grafana/data'; import { locationUtil, TimeRange } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { Panel } from '@grafana/schema'; import { Panel } from '@grafana/schema';
import store from 'app/core/store'; import store from 'app/core/store';
@@ -56,7 +57,10 @@ export function addToDashboard({
} catch { } catch {
return { return {
error: AddToDashboardError.SET_DASHBOARD_LS, error: AddToDashboardError.SET_DASHBOARD_LS,
message: 'Could not add panel to dashboard. Please try again.', message: t(
'dashboard-scene.add-to-dashboard.message.could-panel-dashboard-please-again',
'Could not add panel to dashboard. Please try again.'
),
}; };
} }
@@ -69,7 +73,10 @@ export function addToDashboard({
store.delete(DASHBOARD_FROM_LS_KEY); store.delete(DASHBOARD_FROM_LS_KEY);
return { return {
error: GenericError.NAVIGATION, error: GenericError.NAVIGATION,
message: 'Could not navigate to the selected dashboard. Please try again.', message: t(
'dashboard-scene.add-to-dashboard.message.could-navigate-selected-dashboard-please-again',
'Could not navigate to the selected dashboard. Please try again.'
),
}; };
} }
@@ -1,18 +1,26 @@
import { useState } from 'react'; import { useState } from 'react';
import { PageLayoutType } from '@grafana/data'; import { PageLayoutType } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Box } from '@grafana/ui'; import { Box } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { EmbeddedDashboard } from './EmbeddedDashboard'; import { EmbeddedDashboard } from './EmbeddedDashboard';
export function EmbeddedDashboardTestPage() { export function EmbeddedDashboardTestPage() {
const { t } = useTranslate();
const [state, setState] = useState('?from=now-5m&to=now'); const [state, setState] = useState('?from=now-5m&to=now');
return ( return (
<Page <Page
navId="dashboards/browse" navId="dashboards/browse"
pageNav={{ text: 'Embedding dashboard', subTitle: 'Showing dashboard: Panel Tests - Pie chart' }} pageNav={{
text: t('dashboard-scene.embedded-dashboard-test-page.text.embedding-dashboard', 'Embedding dashboard'),
subTitle: t(
'dashboard-scene.embedded-dashboard-test-page.subTitle.showing-dashboard-panel-tests-pie-chart',
'Showing dashboard: Panel Tests - Pie chart'
),
}}
layout={PageLayoutType.Canvas} layout={PageLayoutType.Canvas}
> >
{/* this is a test page, no need to translate */} {/* this is a test page, no need to translate */}
@@ -63,8 +63,8 @@ export function HelpWizard({ panel, onClose }: Props) {
} }
const tabs = [ const tabs = [
{ label: 'Snapshot', value: SnapshotTab.Support }, { label: t('dashboard-scene.help-wizard.tabs.label.snapshot', 'Snapshot'), value: SnapshotTab.Support },
{ label: 'Data', value: SnapshotTab.Data }, { label: t('dashboard-scene.help-wizard.tabs.label.data', 'Data'), value: SnapshotTab.Data },
]; ];
const hasSupportBundleAccess = const hasSupportBundleAccess =
@@ -1,6 +1,7 @@
import saveAs from 'file-saver'; import saveAs from 'file-saver';
import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data'; import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes'; import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
import { StateManagerBase } from 'app/core/services/StateManagerBase'; import { StateManagerBase } from 'app/core/services/StateManagerBase';
@@ -54,13 +55,16 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
snapshotUpdate: 0, snapshotUpdate: 0,
options: [ options: [
{ {
label: 'GitHub comment', label: t('dashboard-scene.support-snapshot-service.label.git-hub-comment', 'GitHub comment'),
description: 'Copy and paste this message into a GitHub issue or comment', description: 'Copy and paste this message into a GitHub issue or comment',
value: ShowMessage.GithubComment, value: ShowMessage.GithubComment,
}, },
{ {
label: 'Panel support snapshot', label: t('dashboard-scene.support-snapshot-service.label.panel-support-snapshot', 'Panel support snapshot'),
description: 'Dashboard JSON used to help troubleshoot visualization issues', description: t(
'dashboard-scene.support-snapshot-service.description.dashboard-troubleshoot-visualization-issues',
'Dashboard JSON used to help troubleshoot visualization issues'
),
value: ShowMessage.PanelSnapshot, value: ShowMessage.PanelSnapshot,
}, },
], ],
@@ -102,7 +106,10 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
if (markdownText.length > maxLen) { if (markdownText.length > maxLen) {
this.setState({ this.setState({
error: { error: {
title: 'Copy to clipboard failed', title: t(
'dashboard-scene.support-snapshot-service.title.copy-to-clipboard-failed',
'Copy to clipboard failed'
),
message: 'Snapshot is too large, consider download and attaching a file instead', message: 'Snapshot is too large, consider download and attaching a file instead',
}, },
}); });
@@ -149,6 +149,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
} }
if (annotationsCount > 0) { if (annotationsCount > 0) {
const DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE = 'Annotations';
dashboard.panels.push({ dashboard.panels.push({
id: 7, id: 7,
gridPos: { gridPos: {
@@ -158,7 +159,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
y: 20, y: 20,
}, },
type: 'table', type: 'table',
title: 'Annotations', title: DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE,
datasource: { datasource: {
type: 'datasource', type: 'datasource',
uid: '-- Dashboard --', uid: '-- Dashboard --',
@@ -1,6 +1,7 @@
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { locationUtil, UrlQueryMap } from '@grafana/data'; import { locationUtil, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes'; import { sceneGraph } from '@grafana/scenes';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
@@ -545,7 +546,10 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
isLoading: false, isLoading: false,
loadError: { loadError: {
status: 404, status: 404,
message: 'Dashboard not found', message: t(
'dashboard-scene.dashboard-scene-page-state-manager.message.dashboard-not-found',
'Dashboard not found'
),
}, },
}); });
return; return;
@@ -728,7 +732,10 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
isLoading: false, isLoading: false,
loadError: { loadError: {
status: 404, status: 404,
message: 'Dashboard not found', message: t(
'dashboard-scene.dashboard-scene-page-state-manager-v2.message.dashboard-not-found',
'Dashboard not found'
),
}, },
}); });
return; return;
@@ -2,6 +2,7 @@ import * as H from 'history';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { NavIndex, PanelPlugin } from '@grafana/data'; import { NavIndex, PanelPlugin } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { import {
PanelBuilders, PanelBuilders,
@@ -245,7 +246,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
const dashboard = getDashboardSceneFor(this); const dashboard = getDashboardSceneFor(this);
return { return {
text: 'Edit panel', text: t('dashboard-scene.panel-editor.text.edit-panel', 'Edit panel'),
parentItem: dashboard.getPageNav(location, navIndex), parentItem: dashboard.getPageNav(location, navIndex),
}; };
} }
@@ -76,8 +76,14 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
const { t } = useTranslate(); const { t } = useTranslate();
const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [ const radioOptions: Array<SelectableValue<VisualizationSelectPaneTab>> = [
{ label: 'Visualizations', value: VisualizationSelectPaneTab.Visualizations }, {
{ label: 'Suggestions', value: VisualizationSelectPaneTab.Suggestions }, label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.visualizations', 'Visualizations'),
value: VisualizationSelectPaneTab.Visualizations,
},
{
label: t('dashboard-scene.panel-viz-type-picker.radio-options.label.suggestions', 'Suggestions'),
value: VisualizationSelectPaneTab.Suggestions,
},
]; ];
return ( return (
@@ -1,5 +1,6 @@
import { CoreApp } from '@grafana/data'; import { CoreApp } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes'; import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui'; import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
@@ -19,7 +20,7 @@ import { getDashboardSceneFor } from '../utils/utils';
export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescriptor { export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescriptor {
const descriptor = new OptionsPaneCategoryDescriptor({ const descriptor = new OptionsPaneCategoryDescriptor({
title: 'Panel options', title: t('dashboard-scene.get-panel-frame-options.descriptor.title.panel-options', 'Panel options'),
id: 'Panel options', id: 'Panel options',
isOpenDefault: true, isOpenDefault: true,
}); });
@@ -32,7 +33,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
descriptor descriptor
.addItem( .addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Title', title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'),
value: panel.state.title, value: panel.state.title,
popularRank: 1, popularRank: 1,
render: function renderTitle() { render: function renderTitle() {
@@ -49,7 +50,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
) )
.addItem( .addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Description', title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'),
value: panel.state.description, value: panel.state.description,
render: function renderDescription() { render: function renderDescription() {
return <PanelDescriptionTextArea panel={panel} />; return <PanelDescriptionTextArea panel={panel} />;
@@ -64,7 +65,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
) )
.addItem( .addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Transparent background', title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'),
render: function renderTransparent() { render: function renderTransparent() {
return <PanelBackgroundSwitch panel={panel} />; return <PanelBackgroundSwitch panel={panel} />;
}, },
@@ -72,13 +73,13 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
) )
.addCategory( .addCategory(
new OptionsPaneCategoryDescriptor({ new OptionsPaneCategoryDescriptor({
title: 'Panel links', title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
id: 'Panel links', id: 'Panel links',
isOpenDefault: false, isOpenDefault: false,
itemsCount: links?.length, itemsCount: links?.length,
}).addItem( }).addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Panel links', title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />, render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
}) })
) )
@@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { RepositoryView } from 'app/api/clients/provisioning'; import { RepositoryView } from 'app/api/clients/provisioning';
export function getDefaultWorkflow(config?: RepositoryView, loadedFromRef?: string) { export function getDefaultWorkflow(config?: RepositoryView, loadedFromRef?: string) {
@@ -27,7 +28,10 @@ export function getWorkflowOptions(config?: RepositoryView, ref?: string) {
case 'write': case 'write':
return { label: ref ? `Push to ${ref}` : 'Save', value }; return { label: ref ? `Push to ${ref}` : 'Save', value };
case 'branch': case 'branch':
return { label: 'Push to a new branch', value }; return {
label: t('dashboard-scene.get-workflow-options.label.push-to-a-new-branch', 'Push to a new branch'),
value,
};
} }
return { label: value, value }; return { label: value, value };
}); });
@@ -1,6 +1,7 @@
import * as H from 'history'; import * as H from 'history';
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data'; import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, locationService, RefreshEvent } from '@grafana/runtime'; import { config, locationService, RefreshEvent } from '@grafana/runtime';
import { import {
sceneGraph, sceneGraph,
@@ -192,7 +193,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
public constructor(state: Partial<DashboardSceneState>, serializerVersion: 'v1' | 'v2' = 'v1') { public constructor(state: Partial<DashboardSceneState>, serializerVersion: 'v1' | 'v2' = 'v1') {
super({ super({
title: 'Dashboard', title: t('dashboard-scene.dashboard-scene.title.dashboard', 'Dashboard'),
meta: {}, meta: {},
editable: true, editable: true,
$timeRange: state.$timeRange ?? new SceneTimeRange({}), $timeRange: state.$timeRange ?? new SceneTimeRange({}),
@@ -321,7 +322,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
appEvents.publish( appEvents.publish(
new ShowConfirmModalEvent({ new ShowConfirmModalEvent({
title: 'Discard changes to dashboard?', title: t('dashboard-scene.dashboard-scene.title.discard-changes-to-dashboard', 'Discard changes to dashboard?'),
text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`, text: `You have unsaved changes to this dashboard. Are you sure you want to discard them?`,
icon: 'trash-alt', icon: 'trash-alt',
yesText: 'Discard', yesText: 'Discard',
@@ -462,7 +463,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
if (viewPanelScene) { if (viewPanelScene) {
pageNav = { pageNav = {
text: 'View panel', text: t('dashboard-scene.dashboard-scene.text.view-panel', 'View panel'),
parentItem: pageNav, parentItem: pageNav,
url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()), url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()),
}; };
@@ -470,7 +471,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
if (editPanel) { if (editPanel) {
pageNav = { pageNav = {
text: 'Edit panel', text: t('dashboard-scene.dashboard-scene.text.edit-panel', 'Edit panel'),
parentItem: pageNav, parentItem: pageNav,
}; };
} }
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { textUtil } from '@grafana/data'; import { textUtil } from '@grafana/data';
import { useTranslate } from '@grafana/i18n'; import { useTranslate } from '@grafana/i18n';
import { t as internalT } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { ConfirmModal, ToolbarButton } from '@grafana/ui'; import { ConfirmModal, ToolbarButton } from '@grafana/ui';
@@ -33,7 +34,10 @@ const onOpenSnapshotOriginalDashboard = (originalUrl: string) => {
new ShowModalReactEvent({ new ShowModalReactEvent({
component: ConfirmModal, component: ConfirmModal,
props: { props: {
title: 'Proceed to external site?', title: internalT(
'dashboard-scene.on-open-snapshot-original-dashboard.title.proceed-to-external-site',
'Proceed to external site?'
),
modalClass: css({ modalClass: css({
width: 'max-content', width: 'max-content',
maxWidth: '80vw', maxWidth: '80vw',
@@ -328,7 +328,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
// Add specific "Metrics drilldown" menu // Add specific "Metrics drilldown" menu
if (metricsDrilldownLinks.length > 0) { if (metricsDrilldownLinks.length > 0) {
items.push({ items.push({
text: 'Metrics drilldown', text: t('dashboard-scene.panel-menu-behavior.async-func.text.metrics-drilldown', 'Metrics drilldown'),
iconClassName: 'code-branch', iconClassName: 'code-branch',
type: 'submenu', type: 'submenu',
subMenu: createExtensionSubMenu(metricsDrilldownLinks), subMenu: createExtensionSubMenu(metricsDrilldownLinks),
@@ -338,7 +338,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
// Add generic "Extensions" menu for other links // Add generic "Extensions" menu for other links
if (otherLinks.length > 0) { if (otherLinks.length > 0) {
items.push({ items.push({
text: 'Extensions', text: t('dashboard-scene.panel-menu-behavior.async-func.text.extensions', 'Extensions'),
iconClassName: 'plug', iconClassName: 'plug',
type: 'submenu', type: 'submenu',
subMenu: createExtensionSubMenu(otherLinks), subMenu: createExtensionSubMenu(otherLinks),
@@ -544,8 +544,8 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu
export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) { export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
appEvents.publish( appEvents.publish(
new ShowConfirmModalEvent({ new ShowConfirmModalEvent({
title: 'Remove panel', title: t('dashboard-scene.on-remove-panel.title.remove-panel', 'Remove panel'),
text: 'Are you sure you want to remove this panel?', text: t('dashboard-scene.on-remove-panel.text.remove-panel', 'Are you sure you want to remove this panel?'),
icon: 'trash-alt', icon: 'trash-alt',
yesText: 'Remove', yesText: 'Remove',
onConfirm: () => dashboard.removePanel(panel), onConfirm: () => dashboard.removePanel(panel),
@@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n'; import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui'; import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@@ -14,7 +15,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
options.push( options.push(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Column options', title: t('dashboard-scene.get-edit-options.title.column-options', 'Column options'),
skipField: true, skipField: true,
render: () => <GridLayoutColumns layoutManager={layoutManager} />, render: () => <GridLayoutColumns layoutManager={layoutManager} />,
}) })
@@ -22,7 +23,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
options.push( options.push(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: 'Row height options', title: t('dashboard-scene.get-edit-options.title.row-height-options', 'Row height options'),
skipField: true, skipField: true,
render: () => <GridLayoutRows layoutManager={layoutManager} />, render: () => <GridLayoutRows layoutManager={layoutManager} />,
}) })
@@ -292,7 +292,7 @@ export class DefaultGridLayoutManager
const row = new SceneGridRow({ const row = new SceneGridRow({
key: getVizPanelKeyForPanelId(id), key: getVizPanelKeyForPanelId(id),
title: 'Row title', title: t('dashboard-scene.default-grid-layout-manager.row.title.row-title', 'Row title'),
actions: new RowActions({}), actions: new RowActions({}),
y: 0, y: 0,
}); });
@@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { VariableModel, defaultDashboard } from '@grafana/schema'; import { VariableModel, defaultDashboard } from '@grafana/schema';
import { import {
@@ -55,7 +56,7 @@ export async function buildNewDashboardSaveModel(urlFolderUid?: string): Promise
dashboard: { dashboard: {
...defaultDashboard, ...defaultDashboard,
uid: '', uid: '',
title: 'New dashboard', title: t('dashboard-scene.build-new-dashboard-save-model.data.title.new-dashboard', 'New dashboard'),
panels: [], panels: [],
timezone: config.bootData.user?.timezone || defaultDashboard.timezone, timezone: config.bootData.user?.timezone || defaultDashboard.timezone,
}, },
@@ -112,7 +113,7 @@ export async function buildNewDashboardSaveModelV2(
kind: 'DashboardWithAccessInfo', kind: 'DashboardWithAccessInfo',
spec: { spec: {
...defaultDashboardV2Spec(), ...defaultDashboardV2Spec(),
title: 'New dashboard', title: t('dashboard-scene.build-new-dashboard-save-model-v2.data.title.new-dashboard', 'New dashboard'),
timeSettings: { timeSettings: {
...defaultTimeSettingsSpec(), ...defaultTimeSettingsSpec(),
timezone: config.bootData.user?.timezone || defaultTimeSettingsSpec().timezone, timezone: config.bootData.user?.timezone || defaultTimeSettingsSpec().timezone,
@@ -1,4 +1,5 @@
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data'; import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema'; import { DashboardLink } from '@grafana/schema';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
@@ -119,8 +120,9 @@ interface EditLinkViewProps {
} }
function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }: EditLinkViewProps) { function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }: EditLinkViewProps) {
const { t } = useTranslate();
const editLinkPageNav = { const editLinkPageNav = {
text: 'Edit link', text: t('dashboard-scene.edit-link-view.edit-link-page-nav.text.edit-link', 'Edit link'),
parentItem: pageNav, parentItem: pageNav,
}; };
@@ -36,7 +36,7 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi
const dashboardPageNav = dashboard.getPageNav(location, navIndex); const dashboardPageNav = dashboard.getPageNav(location, navIndex);
const pageNav: NavModelItem = { const pageNav: NavModelItem = {
text: 'Settings', text: t('dashboard-scene.use-dashboard-edit-page-nav.page-nav.text.settings', 'Settings'),
url: locationUtil.getUrlForPartial(location, { editview: 'settings', editIndex: null }), url: locationUtil.getUrlForPartial(location, { editview: 'settings', editIndex: null }),
children: [], children: [],
parentItem: dashboardPageNav, parentItem: dashboardPageNav,
@@ -136,7 +136,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
fill="outline" fill="outline"
onClick={() => { onClick={() => {
showModal(ConfirmModal, { showModal(ConfirmModal, {
title: 'Delete variable', title: t('dashboard-scene.variable-editor-form.title.delete-variable', 'Delete variable'),
body: `Are you sure you want to delete: ${name}?`, body: `Are you sure you want to delete: ${name}?`,
confirmText: 'Delete variable', confirmText: 'Delete variable',
onConfirm: onDeleteVariable(hideModal), onConfirm: onDeleteVariable(hideModal),
@@ -59,9 +59,15 @@ export function ResourceExport({
<Label>{switchExportModeLabel}</Label> <Label>{switchExportModeLabel}</Label>
<RadioButtonGroup <RadioButtonGroup
options={[ options={[
{ label: 'Classic', value: ExportMode.Classic }, { label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
{ label: 'V1 Resource', value: ExportMode.V1Resource }, {
{ label: 'V2 Resource', value: ExportMode.V2Resource }, label: t('dashboard-scene.resource-export.label.v1-resource', 'V1 Resource'),
value: ExportMode.V1Resource,
},
{
label: t('dashboard-scene.resource-export.label.v2-resource', 'V2 Resource'),
value: ExportMode.V2Resource,
},
]} ]}
value={exportMode} value={exportMode}
onChange={(value) => onExportModeChange(value)} onChange={(value) => onExportModeChange(value)}
@@ -73,8 +79,8 @@ export function ResourceExport({
<Label>{switchExportFormatLabel}</Label> <Label>{switchExportFormatLabel}</Label>
<RadioButtonGroup <RadioButtonGroup
options={[ options={[
{ label: 'JSON', value: 'json' }, { label: t('dashboard-scene.resource-export.label.json', 'JSON'), value: 'json' },
{ label: 'YAML', value: 'yaml' }, { label: t('dashboard-scene.resource-export.label.yaml', 'YAML'), value: 'yaml' },
]} ]}
value={isViewingYAML ? 'yaml' : 'json'} value={isViewingYAML ? 'yaml' : 'json'}
onChange={onViewYAML} onChange={onViewYAML}
@@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n'; import { Trans, useTranslate } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes'; import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
@@ -24,6 +24,7 @@ interface Props extends SceneComponentProps<SharePublicDashboardTab> {
} }
export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: Props) { export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: Props) {
const { t } = useTranslate();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite); const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
@@ -45,7 +46,7 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }:
dashboard.showModal( dashboard.showModal(
new ConfirmModal({ new ConfirmModal({
isOpen: true, isOpen: true,
title: 'Revoke public URL', title: t('dashboard-scene.config-public-dashboard.title.revoke-public-url', 'Revoke public URL'),
icon: 'trash-alt', icon: 'trash-alt',
confirmText: 'Revoke public URL', confirmText: 'Revoke public URL',
body: ( body: (
@@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { SceneVariable, SceneVariableState } from '@grafana/scenes'; import { SceneVariable, SceneVariableState } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen'; import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { safeStringifyValue } from 'app/core/utils/explore'; import { safeStringifyValue } from 'app/core/utils/explore';
@@ -72,7 +73,12 @@ export function transformUsagesToNetwork(
const { variable, tree } = usage; const { variable, tree } = usage;
const result: UsagesToNetwork = { const result: UsagesToNetwork = {
variable, variable,
nodes: [{ id: 'dashboard', label: 'dashboard' }], nodes: [
{
id: 'dashboard',
label: t('dashboard-scene.transform-usages-to-network.result.label.dashboard', 'dashboard'),
},
],
edges: [], edges: [],
showGraph: false, showGraph: false,
}; };
+4 -1
View File
@@ -1,4 +1,5 @@
import { AppEvents, UrlQueryMap } from '@grafana/data'; import { AppEvents, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { FetchError, getBackendSrv } from '@grafana/runtime'; import { FetchError, getBackendSrv } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema'; import { Dashboard } from '@grafana/schema';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
@@ -38,7 +39,9 @@ export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard>
const fetchError: FetchError = { const fetchError: FetchError = {
status: 404, status: 404,
config: { url: `/api/dashboards/uid/${uid}` }, config: { url: `/api/dashboards/uid/${uid}` },
data: { message: 'Dashboard not found' }, data: {
message: t('dashboard.legacy-dashboard-api.fetch-error.message.dashboard-not-found', 'Dashboard not found'),
},
}; };
throw fetchError; throw fetchError;
} }
+2 -1
View File
@@ -1,4 +1,5 @@
import { locationUtil } from '@grafana/data'; import { locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { Dashboard } from '@grafana/schema'; import { Dashboard } from '@grafana/schema';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
@@ -103,7 +104,7 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
return this.client.delete(uid, showSuccessAlert).then((v) => ({ return this.client.delete(uid, showSuccessAlert).then((v) => ({
id: 0, id: 0,
message: v.message, message: v.message,
title: 'deleted', title: t('dashboard.k8s-dashboard-api.title.deleted', 'deleted'),
})); }));
} }
+2 -1
View File
@@ -1,4 +1,5 @@
import { locationUtil } from '@grafana/data'; import { locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen'; import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors'; import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
@@ -91,7 +92,7 @@ export class K8sDashboardV2API
return this.client.delete(uid, showSuccessAlert).then((v) => ({ return this.client.delete(uid, showSuccessAlert).then((v) => ({
id: 0, id: 0,
message: v.message, message: v.message,
title: 'deleted', title: t('dashboard.k8s-dashboard-v2api.title.deleted', 'deleted'),
})); }));
} }
@@ -103,7 +103,10 @@ export const DashNav = memo<Props>((props) => {
new ShowModalReactEvent({ new ShowModalReactEvent({
component: ConfirmModal, component: ConfirmModal,
props: { props: {
title: 'Proceed to external site?', title: t(
'dashboard.dash-nav.on-open-snapshot-original.title.proceed-to-external-site',
'Proceed to external site?'
),
modalClass: modalStyles, modalClass: modalStyles,
body: ( body: (
<> <>
@@ -85,7 +85,7 @@ export class UnthemedDashboardRow extends Component<DashboardRowProps> {
onDelete = () => { onDelete = () => {
appEvents.publish( appEvents.publish(
new ShowConfirmModalEvent({ new ShowConfirmModalEvent({
title: 'Delete row', title: t('dashboard.unthemed-dashboard-row.title.delete-row', 'Delete row'),
text: 'Are you sure you want to remove this row and all its panels?', text: 'Are you sure you want to remove this row and all its panels?',
altActionText: 'Delete row only', altActionText: 'Delete row only',
icon: 'trash-alt', icon: 'trash-alt',
@@ -116,8 +116,8 @@ export function GeneralSettingsUnconnected({
}; };
const editableOptions = [ const editableOptions = [
{ label: 'Editable', value: true }, { label: t('dashboard.general-settings-unconnected.editable-options.label.editable', 'Editable'), value: true },
{ label: 'Read-only', value: false }, { label: t('dashboard.general-settings-unconnected.editable-options.label.readonly', 'Read-only'), value: false },
]; ];
return ( return (

Some files were not shown because too many files have changed in this diff Show More