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

pull/106186/head
Tom Ratcliffe 4 weeks ago committed by GitHub
parent 57ec71a7a0
commit 7bfa78c6e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 10
      eslint.config.js
  2. 2
      jest.config.js
  3. 68
      packages/grafana-eslint-rules/README.md
  4. 109
      packages/grafana-eslint-rules/rules/no-untranslated-strings.cjs
  5. 80
      packages/grafana-eslint-rules/rules/translation-utils.cjs
  6. 243
      packages/grafana-eslint-rules/tests/no-untranslated-strings.test.js
  7. 8
      public/app/features/actions/ActionVariablesEditor.tsx
  8. 5
      public/app/features/admin/AdminEditOrgPage.tsx
  9. 7
      public/app/features/admin/UserAdminPage.tsx
  10. 7
      public/app/features/admin/UserListAdminPage.tsx
  11. 10
      public/app/features/admin/UserListAnonymousPage.tsx
  12. 21
      public/app/features/alerting/state/alertDef.ts
  13. 11
      public/app/features/alerting/unified/AlertingNotEnabled.tsx
  14. 9
      public/app/features/alerting/unified/NewSilencePage.tsx
  15. 9
      public/app/features/alerting/unified/components/alert-groups/AlertGroupAlertsTable.tsx
  16. 5
      public/app/features/alerting/unified/components/export/ExportNewGrafanaRule.tsx
  17. 18
      public/app/features/alerting/unified/components/export/FileExportPreview.tsx
  18. 3
      public/app/features/alerting/unified/components/export/GrafanaModifyExport.tsx
  19. 8
      public/app/features/alerting/unified/components/import-to-gma/ImportFromDSRules.tsx
  20. 7
      public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx
  21. 7
      public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx
  22. 8
      public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx
  23. 4
      public/app/features/alerting/unified/components/notification-policies/EditDefaultPolicyForm.tsx
  24. 7
      public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx
  25. 2
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  26. 55
      public/app/features/alerting/unified/components/receivers/editor/templateDataSuggestions.ts
  27. 4
      public/app/features/alerting/unified/components/receivers/form/GenerateAlertDataModal.tsx
  28. 7
      public/app/features/alerting/unified/components/receivers/form/fields/TemplateSelector.tsx
  29. 21
      public/app/features/alerting/unified/components/rule-editor/AlertRuleNameInput.tsx
  30. 9
      public/app/features/alerting/unified/components/rule-editor/AnnotationHeaderField.tsx
  31. 10
      public/app/features/alerting/unified/components/rule-editor/CloudEvaluationBehavior.tsx
  32. 5
      public/app/features/alerting/unified/components/rule-editor/FolderSelector.tsx
  33. 29
      public/app/features/alerting/unified/components/rule-editor/GrafanaEvaluationBehavior.tsx
  34. 4
      public/app/features/alerting/unified/components/rule-editor/GroupAndNamespaceFields.tsx
  35. 17
      public/app/features/alerting/unified/components/rule-editor/NotificationsStep.tsx
  36. 5
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/contactPoint/ContactPointSelector.tsx
  37. 15
      public/app/features/alerting/unified/components/rule-editor/labels/LabelsField.tsx
  38. 8
      public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/CloudDataSourceSelector.tsx
  39. 8
      public/app/features/alerting/unified/components/rule-editor/query-and-alert-condition/QueryAndExpressionsStep.tsx
  40. 23
      public/app/features/alerting/unified/components/rule-viewer/RuleViewer.tsx
  41. 4
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v1.tsx
  42. 25
      public/app/features/alerting/unified/components/rules/Filter/RulesFilter.v2.tsx
  43. 20
      public/app/features/alerting/unified/components/rules/MultipleDataSourcePicker.tsx
  44. 18
      public/app/features/alerting/unified/components/rules/RulesTable.tsx
  45. 15
      public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx
  46. 7
      public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts
  47. 38
      public/app/features/alerting/unified/components/rules/state-history/LogTimelineViewer.tsx
  48. 14
      public/app/features/alerting/unified/components/rules/state-history/StateHistory.tsx
  49. 5
      public/app/features/alerting/unified/components/settings/AlertmanagerConfig.tsx
  50. 17
      public/app/features/alerting/unified/components/silences/MatchersField.tsx
  51. 7
      public/app/features/alerting/unified/components/silences/SilencedInstancesPreview.tsx
  52. 11
      public/app/features/alerting/unified/components/silences/SilencesEditor.tsx
  53. 12
      public/app/features/alerting/unified/components/silences/SilencesTable.tsx
  54. 3
      public/app/features/alerting/unified/group-details/validation.ts
  55. 16
      public/app/features/alerting/unified/home/Insights.tsx
  56. 7
      public/app/features/alerting/unified/insights/grafana/MostFiringLabels.tsx
  57. 1
      public/app/features/alerting/unified/testSetup/plugins.ts
  58. 8
      public/app/features/auth-config/ProviderConfigPage.tsx
  59. 6
      public/app/features/auth-config/fields.tsx
  60. 9
      public/app/features/canvas/elements/button.tsx
  61. 15
      public/app/features/canvas/elements/cloud.tsx
  62. 15
      public/app/features/canvas/elements/ellipse.tsx
  63. 15
      public/app/features/canvas/elements/metricValue.tsx
  64. 15
      public/app/features/canvas/elements/parallelogram.tsx
  65. 15
      public/app/features/canvas/elements/rectangle.tsx
  66. 15
      public/app/features/canvas/elements/text.tsx
  67. 15
      public/app/features/canvas/elements/triangle.tsx
  68. 8
      public/app/features/connections/hooks/useDataSourceSettingsNav.ts
  69. 10
      public/app/features/connections/pages/DataSourceDetailsPage.tsx
  70. 8
      public/app/features/connections/pages/NewDataSourcePage.tsx
  71. 25
      public/app/features/connections/tabs/ConnectData/ConnectData.tsx
  72. 14
      public/app/features/dashboard-scene/addToDashboard/AddToDashboardForm.tsx
  73. 11
      public/app/features/dashboard-scene/addToDashboard/addToDashboard.ts
  74. 10
      public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx
  75. 4
      public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx
  76. 15
      public/app/features/dashboard-scene/inspect/HelpWizard/SupportSnapshotService.ts
  77. 3
      public/app/features/dashboard-scene/inspect/HelpWizard/utils.ts
  78. 11
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
  79. 3
      public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
  80. 10
      public/app/features/dashboard-scene/panel-edit/PanelVizTypePicker.tsx
  81. 13
      public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx
  82. 6
      public/app/features/dashboard-scene/saving/provisioned/defaults.ts
  83. 9
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  84. 6
      public/app/features/dashboard-scene/scene/GoToSnapshotOriginButton.tsx
  85. 8
      public/app/features/dashboard-scene/scene/PanelMenuBehavior.tsx
  86. 5
      public/app/features/dashboard-scene/scene/layout-auto-grid/AutoGridLayoutManagerEditor.tsx
  87. 2
      public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
  88. 5
      public/app/features/dashboard-scene/serialization/buildNewDashboardSaveModel.ts
  89. 4
      public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx
  90. 2
      public/app/features/dashboard-scene/settings/utils.ts
  91. 2
      public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx
  92. 16
      public/app/features/dashboard-scene/sharing/ExportButton/ResourceExport.tsx
  93. 5
      public/app/features/dashboard-scene/sharing/public-dashboards/ConfigPublicDashboard.tsx
  94. 8
      public/app/features/dashboard-scene/variables/utils.ts
  95. 5
      public/app/features/dashboard/api/legacy.ts
  96. 3
      public/app/features/dashboard/api/v1.ts
  97. 3
      public/app/features/dashboard/api/v2.ts
  98. 5
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  99. 2
      public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx
  100. 4
      public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

@ -300,9 +300,15 @@ module.exports = [
'packages/grafana-ui/**/*.{ts,tsx,js,jsx}',
...pluginsToTranslate.map((plugin) => `${plugin}/**/*.{ts,tsx,js,jsx}`),
],
ignores: ['**/*.story.tsx', '**/*.{test,spec}.{ts,tsx}', '**/__mocks__/', 'public/test', '**/spec/**/*.{ts,tsx}'],
ignores: [
'public/test/**',
'**/*.{test,spec,story}.{ts,tsx}',
'**/{tests,__mocks__,__tests__,fixtures,spec,mocks}/**',
'**/{test-utils,testHelpers,mocks}.{ts,tsx}',
'**/mock*.{ts,tsx}',
],
rules: {
'@grafana/no-untranslated-strings': 'error',
'@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
'@grafana/no-translation-top-level': 'error',
},
},

@ -36,7 +36,7 @@ module.exports = {
moduleDirectories: ['public', 'node_modules'],
roots: ['<rootDir>/public/app', '<rootDir>/public/test', '<rootDir>/packages', '<rootDir>/scripts/tests'],
testRegex: '(\\.|/)(test)\\.(jsx?|tsx?)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'cjs'],
setupFiles: ['jest-canvas-mock', './public/test/jest-setup.ts'],
testTimeout: 30000,
resolver: `<rootDir>/public/test/jest-resolver.js`,

@ -114,7 +114,47 @@ Used to find all instances of `theme` tokens being used in the codebase and emit
### `no-untranslated-strings`
Check if strings are marked for translation.
Check if strings are marked for translation inside JSX Elements, in certain JSX props, and in certain object properties.
### Options
#### `forceFix`
Allows specifying directories that, if the file is present within, then the rule will automatically fix the errors. This is primarily a workaround to allow for automatic mark up of new violations as the rule evolves.
#### Example:
```ts
{
'@grafana/no-untranslated-strings': ['error', { forceFix: ['app/features/some-feature'] }],
}
```
#### `calleesToIgnore`
Allows specifying regexes for methods that should be ignored when checking if object properties are untranslated.
This is particularly useful to exclude references to properties such as `label` inside `css()` calls.
#### Example:
```ts
{
'@grafana/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$'] }],
}
// The below would not be reported as an error
const foo = css({
label: 'test',
});
// The below would still be reported as an error
const bar = {
label: 'test',
};
```
#### JSXText
```tsx
// Bad ❌
@ -126,7 +166,30 @@ Check if strings are marked for translation.
<InlineToast placement="top" referenceElement={buttonRef.current}>
<Trans i18nKey="clipboard-button.inline-toast.success">Copied</Trans>
</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
@ -166,8 +229,11 @@ The below props are checked for untranslated strings:
- `placeholder`
- `aria-label`
- `title`
- `subTitle`
- `text`
- `tooltip`
- `message`
- `name`
```tsx
// Bad ❌

@ -2,7 +2,8 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.Node} Node */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXElement} JSXElement */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{ forceFix: string[] }]>} RuleDefinition */
/** @typedef {import('@typescript-eslint/utils').TSESLint.RuleModule<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{ forceFix: string[] , calleesToIgnore: string[] }]>} RuleDefinition */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT' | 'noUntranslatedStringsProperties', [{forceFix: string[], calleesToIgnore: string[]}]>} RuleContextWithOptions */
const {
getNodeValue,
@ -20,13 +21,107 @@ const createRule = ESLintUtils.RuleCreator(
(name) => `https://github.com/grafana/grafana/blob/main/packages/grafana-eslint-rules/README.md#${name}`
);
/** @type {string[]} */
/**
* JSX props to check for untranslated strings
*/
const propsToCheck = ['content', 'label', 'description', 'placeholder', 'aria-label', 'title', 'text', 'tooltip'];
/**
* Object properties to check for untranslated strings
*/
const propertiesToCheck = [
'label',
'description',
'placeholder',
'aria-label',
'title',
'subTitle',
'text',
'tooltip',
'message',
];
/** @type {RuleDefinition} */
const noUntranslatedStrings = createRule({
/**
* @param {RuleContextWithOptions} context
*/
create(context) {
const calleesToIgnore = context.options[0]?.calleesToIgnore || [];
const propertiesRegexes = calleesToIgnore.map((pattern) => {
return new RegExp(pattern);
});
return {
Property(node) {
const { key, value, parent, computed } = node;
const keyName = (() => {
if (computed) {
return null;
}
if (key.type === AST_NODE_TYPES.Identifier && typeof key.name === 'string') {
return key.name;
}
return null;
})();
// Catch cases of default props setting object properties, which would be at the top level
const isAssignmentPattern = parent.parent.type === AST_NODE_TYPES.AssignmentPattern;
if (
!keyName ||
!propertiesToCheck.includes(keyName) ||
parent.type === AST_NODE_TYPES.ObjectPattern ||
isAssignmentPattern
) {
return;
}
const callExpression = parent.parent.type === AST_NODE_TYPES.CallExpression ? parent.parent.callee : null;
// Check if we're being called by something that we want to ignore
// e.g. css({ label: 'test' }) should be ignored (based on the rule configuration)
if (
callExpression?.type === AST_NODE_TYPES.Identifier &&
propertiesRegexes.some((regex) => regex.test(callExpression.name))
) {
return;
}
const nodeValue = getNodeValue(node);
const isOnlySymbols = !/[a-zA-Z0-9]/.test(nodeValue);
const isNumeric = !/[a-zA-Z]/.test(nodeValue);
const isUntranslated =
((value.type === AST_NODE_TYPES.Literal && nodeValue !== '') ||
value.type === AST_NODE_TYPES.TemplateLiteral) &&
!isOnlySymbols &&
!isNumeric;
const errorCanBeFixed = canBeFixed(node, context);
const errorShouldBeFixed = shouldBeFixed(context);
if (
isUntranslated &&
// TODO: Remove this check in the future when we've fixed all cases of untranslated properties
// For now, we're only reporting the issues that can be auto-fixed, rather than adding to betterer results
errorCanBeFixed
) {
context.report({
node,
messageId: 'noUntranslatedStringsProperties',
fix: errorCanBeFixed && errorShouldBeFixed ? getTFixers(node, context) : undefined,
suggest: errorCanBeFixed
? [
{
messageId: 'wrapWithT',
fix: getTFixers(node, context),
},
]
: undefined,
});
}
},
JSXAttribute(node) {
if (!propsToCheck.includes(String(node.name.name)) || !node.value) {
return;
@ -163,6 +258,7 @@ const noUntranslatedStrings = createRule({
messages: {
noUntranslatedStrings: 'No untranslated strings. Wrap text with <Trans />',
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',
wrapWithT: 'Wrap text with t() for manual key assignment',
},
@ -177,12 +273,19 @@ const noUntranslatedStrings = createRule({
},
uniqueItems: true,
},
calleesToIgnore: {
type: 'array',
items: {
type: 'string',
},
default: [],
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ forceFix: [] }],
defaultOptions: [{ forceFix: [], calleesToIgnore: [] }],
});
module.exports = noUntranslatedStrings;

@ -5,9 +5,9 @@
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXFragment} JSXFragment */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXText} JSXText */
/** @typedef {import('@typescript-eslint/utils').TSESTree.JSXChild} JSXChild */
/** @typedef {import('@typescript-eslint/utils').TSESTree.Property} Property */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleFixer} RuleFixer */
/** @typedef {import('@typescript-eslint/utils/ts-eslint').RuleContext<'noUntranslatedStrings' | 'noUntranslatedStringsProp' | 'wrapWithTrans' | 'wrapWithT', [{forceFix: string[]}]>} RuleContextWithOptions */
const { AST_NODE_TYPES } = require('@typescript-eslint/utils');
/**
@ -77,7 +77,7 @@ function shouldBeFixed(context) {
/**
* Checks if a node can be fixed automatically
* @param {JSXAttribute|JSXElement|JSXFragment} node The node to check
* @param {JSXAttribute|JSXElement|JSXFragment|Property} node The node to check
* @param {RuleContextWithOptions} context
* @returns {boolean} Whether the node can be fixed
*/
@ -86,16 +86,28 @@ function canBeFixed(node, context) {
return false;
}
const parentMethod = getParentMethod(node, context);
const isAttribute = node.type === AST_NODE_TYPES.JSXAttribute;
const isProperty = node.type === AST_NODE_TYPES.Property;
const isPropertyOrAttribute = isAttribute || isProperty;
// We can only fix JSX attribute strings that are within a function,
// otherwise the `t` function call will be made too early
if (node.type === AST_NODE_TYPES.JSXAttribute) {
const parentMethod = getParentMethod(node, context);
if (!parentMethod) {
if (isPropertyOrAttribute && !parentMethod) {
return false;
}
// If we're going to try and fix using `t`, and it already exists in the scope,
// but not from `useTranslate`, then we can't fix/provide a suggestion
if (isPropertyOrAttribute && parentMethod) {
const hasTDeclaration = getTDeclaration(parentMethod, context);
const hasUseTranslateDeclaration = methodHasUseTranslate(parentMethod, context);
if (hasTDeclaration && !hasUseTranslateDeclaration) {
return false;
}
if (node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) {
return isStringLiteral(node.value.expression);
}
if (isAttribute && node.value?.type === AST_NODE_TYPES.JSXExpressionContainer) {
return isStringLiteral(node.value.expression);
}
const values =
@ -130,7 +142,7 @@ function canBeFixed(node, context) {
*/
function getTranslationPrefix(context) {
const filename = context.filename;
const match = filename.match(/public\/app\/features\/([^/]+)/);
const match = filename.match(/public\/app\/features\/(.+?)\//);
if (match) {
return match[1];
}
@ -139,7 +151,7 @@ function getTranslationPrefix(context) {
/**
* Gets the i18n key for a node
* @param {JSXAttribute|JSXText} node The node
* @param {JSXAttribute|JSXText|Property} node The node
* @param {RuleContextWithOptions} context
* @returns {string} The i18n key
*/
@ -148,6 +160,10 @@ const getI18nKey = (node, context) => {
const stringValue = getNodeValue(node);
const componentNames = getComponentNames(node, context);
const propertyName =
node.type === AST_NODE_TYPES.Property && node.key.type === AST_NODE_TYPES.Identifier ? String(node.key.name) : null;
const words = stringValue
.trim()
.replace(/[^\a-zA-Z\s]/g, '')
@ -185,14 +201,14 @@ const getI18nKey = (node, context) => {
kebabString = [potentialId, propName, kebabString].filter(Boolean).join('-');
}
const fullPrefix = [prefixFromFilePath, ...componentNames, kebabString].filter(Boolean).join('.');
const fullPrefix = [prefixFromFilePath, ...componentNames, propertyName, kebabString].filter(Boolean).join('.');
return fullPrefix;
};
/**
* Gets component names from ancestors
* @param {JSXAttribute|JSXText} node The node
* @param {JSXAttribute|JSXText|Property} node The node
* @param {RuleContextWithOptions} context
* @returns {string[]} The component names
*/
@ -218,13 +234,22 @@ function getComponentNames(node, context) {
}
/**
* Checks if a method has a variable declaration of `t`
* For a given node, check the scope and find a variable declaration of `t`
* @param {Node} node
* @param {RuleContextWithOptions} context
*/
function getTDeclaration(node, context) {
return context.sourceCode.getScope(node).variables.find((v) => v.name === 't');
}
/**
* Checks if a node has a variable declaration of `t`
* that came from a `useTranslate` call
* @param {Node} method The node
* @param {Node} node The node
* @param {RuleContextWithOptions} context
*/
function methodHasUseTranslate(method, context) {
const tDeclaration = method ? context.sourceCode.getScope(method).variables.find((v) => v.name === 't') : null;
function methodHasUseTranslate(node, context) {
const tDeclaration = getTDeclaration(node, context);
return (
tDeclaration &&
tDeclaration.defs.find((definition) => {
@ -243,7 +268,7 @@ function methodHasUseTranslate(method, context) {
/**
* Gets the import fixer for a node
* @param {JSXElement|JSXFragment|JSXAttribute} node
* @param {JSXElement|JSXFragment|JSXAttribute|Property} node
* @param {RuleFixer} fixer The fixer
* @param {'Trans'|'t'|'useTranslate'} importName The member to import from either `@grafana/i18n` or `@grafana/i18n/internal`
* @param {RuleContextWithOptions} context
@ -337,7 +362,7 @@ const firstCharIsUpper = (str) => {
};
/**
* @param {JSXAttribute} node
* @param {JSXAttribute|Property} node
* @param {RuleFixer} fixer
* @param {RuleContextWithOptions} context
* @returns {import('@typescript-eslint/utils/ts-eslint').RuleFix|undefined} The fix
@ -380,9 +405,10 @@ const getUseTranslateFixer = (node, fixer, context) => {
if (!returnStatementIsJsx) {
return;
}
const tDeclarationExists = getTDeclaration(parentMethod, context);
const useTranslateExists = methodHasUseTranslate(parentMethod, context);
if (useTranslateExists) {
if (tDeclarationExists && useTranslateExists) {
return;
}
@ -396,7 +422,7 @@ const getUseTranslateFixer = (node, fixer, context) => {
};
/**
* @param {JSXAttribute} node
* @param {JSXAttribute|Property} node
* @param {RuleContextWithOptions} context
* @returns {(fixer: RuleFixer) => import('@typescript-eslint/utils/ts-eslint').RuleFix[]}
*/
@ -406,9 +432,13 @@ const getTFixers = (node, context) => (fixer) => {
const value = getNodeValue(node);
const wrappingQuotes = value.includes('"') ? "'" : '"';
if (node.type === AST_NODE_TYPES.Property) {
fixes.push(fixer.replaceText(node.value, `t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})`));
} else {
fixes.push(
fixer.replaceText(node, `${node.name.name}={t("${i18nKey}", ${wrappingQuotes}${value}${wrappingQuotes})}`)
);
}
// Check if we need to add `useTranslate` to the node
const useTranslateFixer = getUseTranslateFixer(node, fixer, context);
@ -428,11 +458,19 @@ const getTFixers = (node, context) => (fixer) => {
/**
* Gets the value of a node
* @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild} node The node
* @param {JSXAttribute|JSXText|JSXElement|JSXFragment|JSXChild|Property} node The node
* @returns {string} The node value
*/
function getNodeValue(node) {
if (node.type === AST_NODE_TYPES.JSXAttribute && node.value?.type === AST_NODE_TYPES.Literal) {
if (
(node.type === AST_NODE_TYPES.JSXAttribute || node.type === AST_NODE_TYPES.Property) &&
node.value?.type === AST_NODE_TYPES.Literal
) {
// TODO: Update this to return bool/number values and handle the type issues elsewhere
// For now, we'll just return an empty string so we consider any numbers or booleans as not being issues
if (typeof node.value.value === 'boolean' || typeof node.value.value === 'number') {
return '';
}
return String(node.value.value) || '';
}
if (node.type === AST_NODE_TYPES.JSXText) {

@ -14,7 +14,7 @@ RuleTester.setDefaultConfig({
},
});
const filename = 'public/app/features/some-feature/SomeFile.tsx';
const filename = 'public/app/features/some-feature/nested/SomeFile.tsx';
const packageName = '@grafana/i18n';
@ -31,6 +31,7 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
{
name: 'Text in Trans component',
code: `const Foo = () => <Bar><Trans>Translated text</Trans></Bar>`,
filename,
},
{
name: 'Text in Trans component with whitespace/JSXText elements',
@ -38,58 +39,72 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
<Trans>
Translated text
</Trans> </Bar>`,
filename,
},
{
name: 'Empty component',
code: `<div> </div>`,
filename,
},
{
name: 'Text using t() function',
code: `<div>{t('translated.key', 'Translated text')}</div>`,
filename,
},
{
name: 'Prop using t() function',
code: `<div aria-label={t('aria.label', 'Accessible label')} />`,
filename,
},
{
name: 'Empty string prop',
code: `<div title="" />`,
filename,
},
{
name: 'Prop using boolean',
code: `<div title={false} />`,
filename,
},
{
name: 'Prop using number',
code: `<div title={0} />`,
filename,
},
{
name: 'Prop using null',
code: `<div title={null} />`,
filename,
},
{
name: 'Prop using undefined',
code: `<div title={undefined} />`,
filename,
},
{
name: 'Variable interpolation',
code: `<div>{variable}</div>`,
filename,
},
{
name: 'Entirely non-alphanumeric text (prop)',
code: `<div title="-" />`,
filename,
},
{
name: 'Entirely non-alphanumeric text',
code: `<div>-</div>`,
filename,
},
{
name: 'Non-alphanumeric siblings',
code: `<div>({variable})</div>`,
filename,
},
{
name: "Ternary in an attribute we don't care about",
code: `<div icon={isAThing ? 'Foo' : 'Bar'} />`,
filename,
},
{
name: 'Ternary with falsy strings',
@ -98,6 +113,76 @@ ruleTester.run('eslint no-untranslated-strings', noUntranslatedStrings, {
{
name: 'Ternary with no strings',
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: [
@ -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
*/
@ -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
*/
@ -824,5 +1036,34 @@ const Foo = () => {
filename,
errors: [{ messageId: 'noUntranslatedStringsProp' }, { messageId: 'noUntranslatedStringsProp' }],
},
{
name: 'Cannot fix if `t` already exists from somewhere else',
code: `
const Foo = () => {
const t = () => 'something else';
return (
<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 variableTypeOptions: ComboboxOption[] = [{ label: 'string', value: ActionVariableType.String }];
const variableTypeOptions: ComboboxOption[] = [
{
// eslint-disable-next-line @grafana/no-untranslated-strings
label: 'string',
value: ActionVariableType.String,
},
];
return (
<div>

@ -86,7 +86,10 @@ const AdminEditOrgPage = () => {
const pageNav: NavModelItem = {
text: orgState?.value?.name ?? '',
icon: 'shield',
subTitle: 'Manage settings and user roles for an organization.',
subTitle: t(
'admin.admin-edit-org-page.page-nav.subTitle.manage-settings-roles-organization',
'Manage settings and user roles for an organization.'
),
};
return (

@ -3,6 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { useParams } from 'react-router-dom-v5-compat';
import { NavModelItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { featureEnabled } from '@grafana/runtime';
import { Stack } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
@ -59,6 +60,7 @@ export const UserAdminPage = ({
revokeAllSessions,
syncLdapUser,
}: Props) => {
const { t } = useTranslate();
const { id = '' } = useParams();
useEffect(() => {
loadAdminUserPage(id);
@ -123,7 +125,10 @@ export const UserAdminPage = ({
const pageNav: NavModelItem = {
text: user?.login ?? '',
icon: 'shield',
subTitle: 'Manage settings for an individual user.',
subTitle: t(
'admin.user-admin-page.page-nav.subTitle.manage-settings-for-an-individual-user',
'Manage settings for an individual user.'
),
};
return (

@ -88,8 +88,11 @@ const UserListAdminPageUnConnected = ({
/>
<RadioButtonGroup
options={[
{ label: 'All users', value: false },
{ label: 'Active last 30 days', value: true },
{ label: t('admin.user-list-admin-page-un-connected.label.all-users', 'All users'), value: false },
{
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 })}
value={filters.find((f) => f.name === 'activeLast30Days')?.value}

@ -70,7 +70,15 @@ const UserListAnonymousDevicesPageUnConnected = ({
onChange={changeAnonQuery}
/>
<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 })}
value={filters.find((f) => f.name === 'activeLast30Days')?.value}
className={styles.filter}

@ -1,5 +1,6 @@
import { isArray, reduce } from 'lodash';
import { t } from '@grafana/i18n/internal';
import { IconName } from '@grafana/ui';
import { QueryPart, QueryPartDef } from 'app/features/alerting/state/query_part';
@ -114,42 +115,42 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'normal':
case 'ok': {
return {
text: 'OK',
text: t('alerting.get-state-display-model.text.ok', 'OK'),
iconClass: 'heart',
stateClass: 'alert-state-ok',
};
}
case 'alerting': {
return {
text: 'ALERTING',
text: t('alerting.get-state-display-model.text.alerting', 'ALERTING'),
iconClass: 'heart-break',
stateClass: 'alert-state-critical',
};
}
case 'nodata': {
return {
text: 'NO DATA',
text: t('alerting.get-state-display-model.text.no-data', 'NO DATA'),
iconClass: 'question-circle',
stateClass: 'alert-state-warning',
};
}
case 'paused': {
return {
text: 'PAUSED',
text: t('alerting.get-state-display-model.text.paused', 'PAUSED'),
iconClass: 'pause',
stateClass: 'alert-state-paused',
};
}
case 'pending': {
return {
text: 'PENDING',
text: t('alerting.get-state-display-model.text.pending', 'PENDING'),
iconClass: 'hourglass',
stateClass: 'alert-state-warning',
};
}
case 'recovering': {
return {
text: 'RECOVERING',
text: t('alerting.get-state-display-model.text.recovering', 'RECOVERING'),
iconClass: 'hourglass',
stateClass: 'alert-state-warning',
};
@ -157,7 +158,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'firing': {
return {
text: 'FIRING',
text: t('alerting.get-state-display-model.text.firing', 'FIRING'),
iconClass: 'fire',
stateClass: '',
};
@ -165,7 +166,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'inactive': {
return {
text: 'INACTIVE',
text: t('alerting.get-state-display-model.text.inactive', 'INACTIVE'),
iconClass: 'check',
stateClass: '',
};
@ -173,7 +174,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'error': {
return {
text: 'ERROR',
text: t('alerting.get-state-display-model.text.error', 'ERROR'),
iconClass: 'heart-break',
stateClass: 'alert-state-critical',
};
@ -182,7 +183,7 @@ function getStateDisplayModel(state: string): AlertStateDisplayModel {
case 'unknown':
default: {
return {
text: 'UNKNOWN',
text: t('alerting.get-state-display-model.text.unknown', 'UNKNOWN'),
iconClass: 'question-circle',
stateClass: '.alert-state-paused',
};

@ -1,17 +1,22 @@
import { NavModel } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Page } from 'app/core/components/Page/Page';
import { withPageErrorBoundary } from './withPageErrorBoundary';
function FeatureTogglePage() {
const { t } = useTranslate();
const navModel: NavModel = {
node: {
text: 'Alerting is not enabled',
text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'),
hideFromBreadcrumbs: true,
subTitle: 'To enable alerting, enable it in the Grafana config',
subTitle: t(
'alerting.feature-toggle-page.nav-model.subTitle.enable-alerting-grafana-config',
'To enable alerting, enable it in the Grafana config'
),
},
main: {
text: 'Alerting is not enabled',
text: t('alerting.feature-toggle-page.nav-model.text.alerting-is-not-enabled', 'Alerting is not enabled'),
},
};

@ -1,5 +1,6 @@
import { useLocation } from 'react-router-dom-v5-compat';
import { useTranslate } from '@grafana/i18n';
import {
defaultsFromQuery,
getDefaultSilenceFormValues,
@ -37,10 +38,14 @@ const SilencesEditorComponent = () => {
};
function NewSilencePage() {
const { t } = useTranslate();
const pageNav = {
id: 'silence-new',
text: 'Silence alert rule',
subTitle: 'Configure silences to stop notifications from a particular alert rule',
text: t('alerting.new-silence-page.page-nav.text.silence-alert-rule', 'Silence alert rule'),
subTitle: t(
'alerting.new-silence-page.page-nav.subTitle.configure-silences-notifications-particular-alert',
'Configure silences to stop notifications from a particular alert rule'
),
};
return (
<AlertmanagerPageWrapper navId="silences" pageNav={pageNav} accessType="instance">

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { useMemo } from 'react';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Trans, useTranslate } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { AlertmanagerAlert } from 'app/plugins/datasource/alertmanager/types';
@ -22,13 +22,14 @@ type AlertGroupAlertsTableColumnProps = DynamicTableColumnProps<AlertmanagerAler
type AlertGroupAlertsTableItemProps = DynamicTableItemProps<AlertmanagerAlert>;
export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const columns = useMemo(
(): AlertGroupAlertsTableColumnProps[] => [
{
id: 'state',
label: 'Notification state',
label: t('alerting.alert-group-alerts-table.columns.label.notification-state', 'Notification state'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: alert }) => (
<>
@ -52,13 +53,13 @@ export const AlertGroupAlertsTable = ({ alerts, alertManagerSourceName }: Props)
},
{
id: 'labels',
label: 'Instance labels',
label: t('alerting.alert-group-alerts-table.columns.label.instance-labels', 'Instance labels'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: { labels } }) => <AlertLabels labels={labels} size="sm" />,
size: 1,
},
],
[styles]
[styles, t]
);
const items = useMemo(

@ -1,13 +1,16 @@
import { useTranslate } from '@grafana/i18n';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertingPageWrapper } from '../AlertingPageWrapper';
import { ModifyExportRuleForm } from '../rule-editor/alert-rule-form/ModifyExportRuleForm';
function ExportNewGrafanaRulePage() {
const { t } = useTranslate();
return (
<AlertingPageWrapper
navId="alert-list"
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.',
}}
>

@ -5,7 +5,7 @@ import * as React from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Trans, useTranslate } from '@grafana/i18n';
import { Alert, Button, ClipboardButton, CodeEditor, TextLink, useStyles2 } from '@grafana/ui';
import { ExportFormats, ExportProvider, ProvisioningType, allGrafanaExportProviders } from './providers';
@ -92,11 +92,15 @@ const fileExportPreviewStyles = (theme: GrafanaTheme2) => ({
});
function FileExportInlineDocumentation({ exportProvider }: { exportProvider: ExportProvider<unknown> }) {
const { t } = useTranslate();
const { name, type } = exportProvider;
const exportInlineDoc: Record<ProvisioningType, { title: string; component: React.ReactNode }> = {
file: {
title: 'File-provisioning format',
title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.fileprovisioning-format',
'File-provisioning format'
),
component: (
<Trans i18nKey="alerting.file-export-inline-documentation.file-provisioning">
{{ name }} format is only valid for File Provisioning.{' '}
@ -110,7 +114,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
),
},
api: {
title: 'API-provisioning format',
title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.apiprovisioning-format',
'API-provisioning format'
),
component: (
<Trans i18nKey="alerting.file-export-inline-documentation.api-provisioning">
{{ name }} format is only valid for API Provisioning.{' '}
@ -124,7 +131,10 @@ function FileExportInlineDocumentation({ exportProvider }: { exportProvider: Exp
),
},
terraform: {
title: 'Terraform-provisioning format',
title: t(
'alerting.file-export-inline-documentation.export-inline-doc.title.terraformprovisioning-format',
'Terraform-provisioning format'
),
component: (
<Trans i18nKey="alerting.file-export-inline-documentation.terraform-provisioning">
{{ name }} format is only valid for Terraform Provisioning.{' '}

@ -93,11 +93,12 @@ function RuleModifyExport({ ruleIdentifier }: { ruleIdentifier: RuleIdentifier }
}
function GrafanaModifyExportPage() {
const { t } = useTranslate();
return (
<AlertingPageWrapper
navId="alert-list"
pageNav={{
text: 'Modify export',
text: t('alerting.grafana-modify-export-page.text.modify-export', 'Modify export'),
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.',
}}

@ -240,7 +240,13 @@ const ImportFromDSRules = () => {
name="targetDatasourceUID"
control={control}
rules={{
required: { value: true, message: 'Please select a target data source' },
required: {
value: true,
message: t(
'alerting.import-from-dsrules.message.please-select-a-target-data-source',
'Please select a target data source'
),
},
}}
/>
</Field>

@ -1,5 +1,6 @@
import { Navigate } from 'react-router-dom-v5-compat';
import { useTranslate } from '@grafana/i18n';
import { useGetMuteTiming } from 'app/features/alerting/unified/components/mute-timings/useMuteTimings';
import { useURLSearchParams } from 'app/features/alerting/unified/hooks/useURLSearchParams';
@ -39,10 +40,14 @@ const EditTimingRoute = () => {
};
function EditMuteTimingPage() {
const { t } = useTranslate();
return (
<AlertmanagerPageWrapper
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"
>
<EditTimingRoute />

@ -3,6 +3,7 @@ import { useMemo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Alert, Button, LinkButton, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui';
import { MuteTimingActionsButtons } from 'app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons';
import {
@ -149,7 +150,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
const columns: Array<DynamicTableColumnProps<MuteTiming>> = [
{
id: 'name',
label: 'Name',
label: t('alerting.use-columns.columns.label.name', 'Name'),
renderCell: function renderName({ data }) {
return (
<div>
@ -164,7 +165,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
},
{
id: 'timeRange',
label: 'Time range',
label: t('alerting.use-columns.columns.label.time-range', 'Time range'),
renderCell: ({ data }) => {
return renderTimeIntervals(data);
},
@ -174,7 +175,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false) {
if (showActions) {
columns.push({
id: 'actions',
label: 'Actions',
label: t('alerting.use-columns.label.actions', 'Actions'),
alignColumn: 'end',
renderCell: ({ data }) => (
<MuteTimingActionsButtons muteTiming={data} alertManagerSourceName={alertManagerSourceName} />

@ -1,13 +1,19 @@
import { useTranslate } from '@grafana/i18n';
import { withPageErrorBoundary } from '../../withPageErrorBoundary';
import { AlertmanagerPageWrapper } from '../AlertingPageWrapper';
import MuteTimingForm from './MuteTimingForm';
function NewMuteTimingPage() {
const { t } = useTranslate();
return (
<AlertmanagerPageWrapper
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"
>
<MuteTimingForm />

@ -70,7 +70,9 @@ export const AmRootRouteForm = ({ actionButtons, alertManagerSourceName, onSubmi
)}
control={control}
name="receiver"
rules={{ required: { value: true, message: 'Required.' } }}
rules={{
required: { value: true, message: t('alerting.am-root-route-form.message.required', 'Required.') },
}}
/>
<span>
<Trans i18nKey="alerting.am-root-route-form.or">or</Trans>

@ -133,7 +133,12 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
defaultValue={field.operator}
control={control}
name={`object_matchers.${index}.operator`}
rules={{ required: { value: true, message: 'Required.' } }}
rules={{
required: {
value: true,
message: t('alerting.am-routes-expanded-form.message.required', 'Required.'),
},
}}
/>
</Field>
<Field

@ -202,7 +202,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
>
<Input
{...register('title', {
required: { value: true, message: 'Required.' },
required: { value: true, message: t('alerting.template-form.message.required', 'Required.') },
validate: { titleIsUnique },
})}
placeholder={t(

@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import type { Monaco } from '@grafana/ui';
import {
@ -16,12 +17,18 @@ import { SuggestionDefinition } from './suggestionDefinition';
export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [
{
label: 'Alerts',
kind,
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: 'Status', kind, detail: 'string' },
@ -32,36 +39,54 @@ export function getGlobalSuggestions(monaco: Monaco): SuggestionDefinition[] {
{ label: 'GroupKey', kind, detail: 'string' },
{ 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)
export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [
{
label: { label: 'Status', detail: '(Alert)', description: 'string' },
kind,
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)' },
kind,
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)' },
kind,
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)' },
kind,
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)' },
@ -74,7 +99,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'GeneratorURL', detail: '(Alert)' },
kind,
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)' },
@ -99,7 +127,10 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'Fingerprint', detail: '(Alert)' },
kind,
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)' },
@ -111,25 +142,32 @@ export function getAlertSuggestions(monaco: Monaco): SuggestionDefinition[] {
label: { label: 'OrgID', detail: '(Alert)' },
kind,
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
export function getAlertsSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [
{ label: 'Firing', kind, detail: 'Alert[]' },
{ label: 'Resolved', kind, detail: 'Alert[]' },
];
/* eslint-disable @grafana/no-untranslated-strings */
}
// Suggestions for the KeyValue types
export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
const kind = monaco.languages.CompletionItemKind.Field;
/* eslint-disable @grafana/no-untranslated-strings */
return [
{ label: 'SortedPairs', kind, detail: '[]KeyValue' },
{ label: 'Names', kind, detail: '[]string' },
@ -140,6 +178,7 @@ export function getKeyValueSuggestions(monaco: Monaco): SuggestionDefinition[] {
kind: monaco.languages.CompletionItemKind.Method,
},
];
/* eslint-enable @grafana/no-untranslated-strings */
}
export const snippets = {

@ -82,10 +82,10 @@ export const GenerateAlertDataModal = ({ isOpen, onDismiss, onAccept }: Props) =
};
const alertOptions: AlertOption[] = [
{
label: 'Firing',
label: t('alerting.generate-alert-data-modal.alert-options.label.firing', 'Firing'),
value: 'firing',
},
{ label: 'Resolved', value: 'resolved' },
{ label: t('alerting.generate-alert-data-modal.alert-options.label.resolved', 'Resolved'), value: 'resolved' },
];
return (

@ -123,6 +123,7 @@ interface TemplateSelectorProps {
}
function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSelectorProps) {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const valueInFormIsCustom = Boolean(valueInForm) && !matchesOnlyOneTemplate(valueInForm);
const [template, setTemplate] = useState<SelectableValue<Template> | undefined>(undefined);
@ -138,7 +139,10 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
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',
value: 'Existing',
description: `Select an existing notification template and preview it, or copy it to paste it in the custom tab. ${templateOption === 'Existing' ? 'Clicking Save saves your changes to the selected template.' : ''}`,
@ -156,7 +160,6 @@ function TemplateSelector({ onSelect, onClose, option, valueInForm }: TemplateSe
setCustomTemplateValue(getUseTemplateText(template.value.name));
}
}, [template]);
const { t } = useTranslate();
function onCustomTemplateChange(customInput: string) {
setCustomTemplateValue(customInput);

@ -66,7 +66,10 @@ export const AlertRuleNameAndMetric = () => {
id="name"
width={38}
{...register('name', {
required: { value: true, message: 'Must enter a name' },
required: {
value: true,
message: t('alerting.alert-rule-name-and-metric.message.must-enter-a-name', 'Must enter a name'),
},
pattern: isCloudRecordingRule
? recordingRuleNameValidationPattern(RuleFormType.cloudRecording)
: undefined,
@ -89,7 +92,13 @@ export const AlertRuleNameAndMetric = () => {
id="metric"
width={38}
{...register('metric', {
required: { value: true, message: 'Must enter a metric name' },
required: {
value: true,
message: t(
'alerting.alert-rule-name-and-metric.message.must-enter-a-metric-name',
'Must enter a metric name'
),
},
pattern: recordingRuleNameValidationPattern(RuleFormType.grafanaRecording),
})}
aria-label={t('alerting.alert-rule-name-and-metric.metric-aria-label-metric', 'metric')}
@ -130,7 +139,13 @@ export const AlertRuleNameAndMetric = () => {
name="targetDatasourceUid"
control={control}
rules={{
required: { value: true, message: 'Please select a data source' },
required: {
value: true,
message: t(
'alerting.alert-rule-name-and-metric.message.please-select-a-data-source',
'Please select a data source'
),
},
}}
/>
</Field>

@ -1,5 +1,6 @@
import { Controller, FieldArrayWithId, useFormContext } from 'react-hook-form';
import { useTranslate } from '@grafana/i18n';
import { Stack, Text } from '@grafana/ui';
import { RuleFormValues } from '../../types/rule-form';
@ -20,6 +21,7 @@ const AnnotationHeaderField = ({
index: number;
labelId: string;
}) => {
const { t } = useTranslate();
const { control } = useFormContext<RuleFormValues>();
return (
@ -56,7 +58,12 @@ const AnnotationHeaderField = ({
);
}}
control={control}
rules={{ required: { value: !!annotations[index]?.value, message: 'Required.' } }}
rules={{
required: {
value: !!annotations[index]?.value,
message: t('alerting.annotation-header-field.message.required', 'Required.'),
},
}}
/>
}
</label>

@ -40,7 +40,15 @@ export const CloudEvaluationBehavior = () => {
<div className={styles.flexRow}>
<Field invalid={!!errors.forTime?.message} error={errors.forTime?.message} className={styles.inlineField}>
<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}
/>
</Field>

@ -69,7 +69,10 @@ export function FolderSelector() {
)}
name="folder"
rules={{
required: { value: true, message: 'Select a folder' },
required: {
value: true,
message: t('alerting.folder-selector.message.select-a-folder', 'Select a folder'),
},
}}
/>
<CreateNewFolder onCreate={handleFolderCreation} />

@ -5,7 +5,7 @@ import { Controller, FormProvider, RegisterOptions, useForm, useFormContext } fr
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import {
Box,
@ -74,7 +74,7 @@ const sortByLabel = (a: SelectableValue<string>, b: SelectableValue<string>) =>
const forValidationOptions = (evaluateEvery: string): RegisterOptions<{ evaluateFor: string }> => ({
required: {
value: true,
message: 'Required.',
message: t('alerting.for-validation-options.message.required', 'Required.'),
},
validate: (value) => {
// parsePrometheusDuration does not allow 0 but does allow 0s
@ -118,6 +118,7 @@ export function GrafanaEvaluationBehaviorStep({
existing: boolean;
enableProvisionedGroups: boolean;
}) {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const [showErrorHandling, setShowErrorHandling] = useState(false);
@ -237,7 +238,13 @@ export function GrafanaEvaluationBehaviorStep({
name="group"
control={control}
rules={{
required: { value: true, message: 'Must enter a group name' },
required: {
value: true,
message: t(
'alerting.grafana-evaluation-behavior-step.message.must-enter-a-group-name',
'Must enter a group name'
),
},
}}
/>
</Field>
@ -380,7 +387,13 @@ export function GrafanaEvaluationBehaviorStep({
)}
id="missing-series-resolve"
{...register('missingSeriesEvalsToResolve', {
pattern: { value: /^\d+$/, message: 'Must be a positive integer.' },
pattern: {
value: /^\d+$/,
message: t(
'alerting.grafana-evaluation-behavior-step.message.must-be-a-positive-integer',
'Must be a positive integer.'
),
},
})}
width={21}
/>
@ -402,6 +415,7 @@ function EvaluationGroupCreationModal({
onCreate: (group: string, evaluationInterval: string) => void;
groupfoldersForGrafana?: RulerRulesConfigDTO | null;
}): React.ReactElement {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const { watch } = useFormContext<RuleFormValues>();
@ -477,7 +491,12 @@ function EvaluationGroupCreationModal({
autoFocus={true}
id={evaluationGroupNameId}
placeholder={t('alerting.evaluation-group-creation-modal.placeholder-enter-a-name', 'Enter a name')}
{...register('group', { required: { value: true, message: 'Required.' } })}
{...register('group', {
required: {
value: true,
message: t('alerting.evaluation-group-creation-modal.message.required', 'Required.'),
},
})}
/>
</Field>

@ -73,7 +73,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
name="namespace"
control={control}
rules={{
required: { value: true, message: 'Required.' },
required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
}}
/>
</Field>
@ -105,7 +105,7 @@ export const GroupAndNamespaceFields = ({ rulesSourceName }: Props) => {
name="group"
control={control}
rules={{
required: { value: true, message: 'Required.' },
required: { value: true, message: t('alerting.group-and-namespace-fields.message.required', 'Required.') },
}}
/>
</Field>

@ -153,14 +153,27 @@ export const NotificationsStep = ({ alertUid }: NotificationsStepProps) => {
*
*/
function ManualAndAutomaticRouting({ alertUid }: { alertUid?: string }) {
const { t } = useTranslate();
const { watch, setValue } = useFormContext<RuleFormValues>();
const styles = useStyles2(getStyles);
const [manualRouting] = watch(['manualRouting']);
const routingOptions = [
{ label: 'Select contact point', value: RoutingOptions.ContactPoint },
{ label: 'Use notification policy', value: RoutingOptions.NotificationPolicy },
{
label: t(
'alerting.manual-and-automatic-routing.routing-options.label.select-contact-point',
'Select contact point'
),
value: RoutingOptions.ContactPoint,
},
{
label: t(
'alerting.manual-and-automatic-routing.routing-options.label.use-notification-policy',
'Use notification policy'
),
value: RoutingOptions.NotificationPolicy,
},
];
const onRoutingOptionChange = (option: RoutingOptions) => {

@ -78,7 +78,10 @@ export function ContactPointSelector({ alertManager, onSelectContactPoint }: Con
rules={{
required: {
value: true,
message: 'Contact point is required.',
message: t(
'alerting.contact-point-selector.message.contact-point-is-required',
'Contact point is required.'
),
},
}}
control={control}

@ -116,6 +116,7 @@ export function useCombinedLabels(
labelsInSubform: Array<{ key: string; value: string }>,
selectedKey: string
) {
const { t } = useTranslate();
// ------- Get labels keys and their values from existing alerts
const { labels: labelsByKeyFromExisingAlerts, isLoading } = useGetLabelsFromDataSourceName(dataSourceName);
// ------- Get only the keys from the ops labels, as we will fetch the values for the keys once the key is selected.
@ -143,12 +144,12 @@ export function useCombinedLabels(
// create two groups of labels, one for ops and one for custom
const groupedOptions = [
{
label: 'From alerts',
label: t('alerting.use-combined-labels.grouped-options.label.from-alerts', 'From alerts'),
options: keysFromExistingAlerts,
expanded: true,
},
{
label: 'From system',
label: t('alerting.use-combined-labels.grouped-options.label.from-system', 'From system'),
options: keysFromGopsLabels,
expanded: true,
},
@ -372,7 +373,10 @@ export const LabelsWithoutSuggestions: FC = () => {
>
<Input
{...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')}
data-testid={`label-key-${index}`}
@ -387,7 +391,10 @@ export const LabelsWithoutSuggestions: FC = () => {
>
<Input
{...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')}
data-testid={`label-value-${index}`}

@ -53,7 +53,13 @@ export const CloudDataSourceSelector = ({ disabled, onChangeCloudDatasource }: C
name="dataSourceName"
control={control}
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>

@ -544,7 +544,13 @@ export const QueryAndExpressionsStep = ({ editingExistingRule, onDataChange, mod
}}
control={control}
rules={{
required: { value: true, message: 'A valid expression is required' },
required: {
value: true,
message: t(
'alerting.query-and-expressions-step.message.a-valid-expression-is-required',
'A valid expression is required'
),
},
}}
/>
</Field>

@ -5,6 +5,7 @@ import { useMeasure } from 'react-use';
import { NavModelItem, UrlQueryValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import {
Alert,
LinkButton,
@ -205,14 +206,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
<Text variant="bodySmall">{truncatedUrl}</Text>
);
metadata.push({
label: 'Runbook URL',
label: t('alerting.create-metadata.label.runbook-url', 'Runbook URL'),
value: valueToAdd,
});
}
if (hasDashboardAndPanel) {
metadata.push({
label: 'Dashboard and panel',
label: t('alerting.create-metadata.label.dashboard-and-panel', 'Dashboard and panel'),
value: (
<WithReturnButton
title={rule.name}
@ -226,7 +227,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
});
} else if (hasDashboard) {
metadata.push({
label: 'Dashboard',
label: t('alerting.create-metadata.label.dashboard', 'Dashboard'),
value: (
<WithReturnButton
title={rule.name}
@ -242,14 +243,14 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
if (rulerRuleType.grafana.recordingRule(rule.rulerRule)) {
const metric = rule.rulerRule?.grafana_alert.record?.metric ?? '';
metadata.push({
label: 'Metric name',
label: t('alerting.create-metadata.label.metric-name', 'Metric name'),
value: <Text color="primary">{metric}</Text>,
});
}
if (interval) {
metadata.push({
label: 'Evaluation interval',
label: t('alerting.create-metadata.label.evaluation-interval', 'Evaluation interval'),
value: (
<Text color="primary">
<Trans i18nKey="alerting.rule-viewer.evaluation-interval">Every {{ interval }}</Trans>
@ -260,7 +261,7 @@ const createMetadata = (rule: CombinedRule): PageInfoItem[] => {
if (hasLabels) {
metadata.push({
label: 'Labels',
label: t('alerting.create-metadata.label.labels', 'Labels'),
/* TODO truncate number of labels, maybe build in to component? */
value: <AlertLabels labels={labels} size="sm" />,
});
@ -415,14 +416,14 @@ function usePageNav(rule: CombinedRule) {
subTitle: summary,
children: [
{
text: 'Query and conditions',
text: t('alerting.use-page-nav.page-nav.text.query-and-conditions', 'Query and conditions'),
active: activeTab === ActiveTab.Query,
onClick: () => {
setActiveTab(ActiveTab.Query);
},
},
{
text: 'Instances',
text: t('alerting.use-page-nav.page-nav.text.instances', 'Instances'),
active: activeTab === ActiveTab.Instances,
onClick: () => {
setActiveTab(ActiveTab.Instances);
@ -431,7 +432,7 @@ function usePageNav(rule: CombinedRule) {
hideFromTabs: isRecordingRuleType,
},
{
text: 'History',
text: t('alerting.use-page-nav.page-nav.text.history', 'History'),
active: activeTab === ActiveTab.History,
onClick: () => {
setActiveTab(ActiveTab.History);
@ -440,14 +441,14 @@ function usePageNav(rule: CombinedRule) {
hideFromTabs: !isGrafanaAlertRule,
},
{
text: 'Details',
text: t('alerting.use-page-nav.page-nav.text.details', 'Details'),
active: activeTab === ActiveTab.Details,
onClick: () => {
setActiveTab(ActiveTab.Details);
},
},
{
text: 'Versions',
text: t('alerting.use-page-nav.page-nav.text.versions', 'Versions'),
active: activeTab === ActiveTab.VersionHistory,
onClick: () => {
setActiveTab(ActiveTab.VersionHistory);

@ -259,8 +259,8 @@ const RulesFilter = ({ onClear = () => undefined }: RulesFilerProps) => {
</Label>
<RadioButtonGroup<'hide'>
options={[
{ label: 'Show', value: undefined },
{ label: 'Hide', value: 'hide' },
{ label: t('alerting.rules-filter.label.show', 'Show'), value: undefined },
{ label: t('alerting.rules-filter.label.hide', 'Hide'), value: 'hide' },
]}
value={filterState.plugins}
onChange={(value) => updateFilters({ ...filterState, plugins: value })}

@ -81,6 +81,7 @@ export default function RulesFilter({ onClear = () => {} }: RulesFilterProps) {
}
const FilterOptions = () => {
const { t } = useTranslate();
return (
<Stack direction="column" alignItems="end" gap={2}>
<Grid columns={2} gap={2} alignItems="center">
@ -110,11 +111,11 @@ const FilterOptions = () => {
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Normal', value: 'normal' },
{ label: 'Pending', value: 'pending' },
{ label: 'Recovering', value: 'recovering' },
{ label: 'Firing', value: 'firing' },
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: t('alerting.filter-options.label.normal', 'Normal'), value: 'normal' },
{ label: t('alerting.filter-options.label.pending', 'Pending'), value: 'pending' },
{ label: t('alerting.filter-options.label.recovering', 'Recovering'), value: 'recovering' },
{ label: t('alerting.filter-options.label.firing', 'Firing'), value: 'firing' },
]}
/>
<Label>
@ -123,9 +124,9 @@ const FilterOptions = () => {
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'Alert rule', value: 'alerting' },
{ label: 'Recording rule', value: 'recording' },
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: t('alerting.filter-options.label.alert-rule', 'Alert rule'), value: 'alerting' },
{ label: t('alerting.filter-options.label.recording-rule', 'Recording rule'), value: 'recording' },
]}
/>
<Label>
@ -134,10 +135,10 @@ const FilterOptions = () => {
<RadioButtonGroup
value={'*'}
options={[
{ label: 'All', value: '*' },
{ label: 'OK', value: 'ok' },
{ label: 'No data', value: 'no_data' },
{ label: 'Error', value: 'error' },
{ label: t('alerting.filter-options.label.all', 'All'), value: '*' },
{ label: t('alerting.filter-options.label.ok', 'OK'), value: 'ok' },
{ label: t('alerting.filter-options.label.no-data', 'No data'), value: 'no_data' },
{ label: t('alerting.filter-options.label.error', 'Error'), value: 'error' },
]}
/>
</Grid>

@ -8,6 +8,7 @@ import {
isUnsignedPluginSignature,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useTranslate } from '@grafana/i18n';
import { DataSourcePickerProps, DataSourcePickerState, getDataSourceSrv } from '@grafana/runtime';
import { ExpressionDatasourceRef } from '@grafana/runtime/internal';
import { ActionMeta, MultiSelect, PluginSignatureBadge, Stack } from '@grafana/ui';
@ -20,6 +21,7 @@ export interface MultipleDataSourcePickerProps extends Omit<DataSourcePickerProp
}
export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) => {
const { t } = useTranslate();
const dataSourceSrv = getDataSourceSrv();
const [state, setState] = useState<DataSourcePickerState>();
@ -121,8 +123,22 @@ export const MultipleDataSourcePicker = (props: MultipleDataSourcePickerProps) =
}));
const groupedOptions = [
{ label: 'Data sources with configured alert rules', options: alertManagingDs, expanded: true },
{ label: 'Other data sources', options: nonAlertManagingDs, expanded: true },
{
label: t(
'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.data-sources-with-configured-alert-rules',
'Data sources with configured alert rules'
),
options: alertManagingDs,
expanded: true,
},
{
label: t(
'alerting.multiple-data-source-picker.get-data-source-options.grouped-options.label.other-data-sources',
'Other data sources'
),
options: nonAlertManagingDs,
expanded: true,
},
];
return groupedOptions;

@ -3,6 +3,7 @@ import { useEffect, useMemo } from 'react';
import Skeleton from 'react-loading-skeleton';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Pagination, Tooltip, useStyles2 } from '@grafana/ui';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
@ -192,17 +193,18 @@ function useColumns(
showNextEvaluationColumn: boolean,
isRulerLoading: boolean
) {
const { t } = useTranslate();
return useMemo((): RuleTableColumnProps[] => {
const columns: RuleTableColumnProps[] = [
{
id: 'state',
label: 'State',
label: t('alerting.use-columns.columns.label.state', 'State'),
renderCell: ({ data: rule }) => <RuleStateCell rule={rule} />,
size: '165px',
},
{
id: 'name',
label: 'Name',
label: t('alerting.use-columns.columns.label.name', 'Name'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => rule.name,
size: showNextEvaluationColumn ? 4 : 5,
@ -237,7 +239,7 @@ function useColumns(
},
{
id: 'health',
label: 'Health',
label: t('alerting.use-columns.columns.label.health', 'Health'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: { promRule, group } }) => (promRule ? <RuleHealth rule={promRule} /> : null),
size: '75px',
@ -246,7 +248,7 @@ function useColumns(
if (showSummaryColumn) {
columns.push({
id: 'summary',
label: 'Summary',
label: t('alerting.use-columns.label.summary', 'Summary'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => {
return <Tokenize input={rule.annotations[Annotation.summary] ?? ''} />;
@ -258,7 +260,7 @@ function useColumns(
if (showNextEvaluationColumn) {
columns.push({
id: 'nextEvaluation',
label: 'Next evaluation',
label: t('alerting.use-columns.label.next-evaluation', 'Next evaluation'),
renderCell: ({ data: rule }) => {
const nextEvalInfo = calculateNextEvaluationEstimate(rule.promRule?.lastEvaluation, rule.group.interval);
@ -282,7 +284,7 @@ function useColumns(
if (showGroupColumn) {
columns.push({
id: 'group',
label: 'Group',
label: t('alerting.use-columns.label.group', 'Group'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => {
const { namespace, group } = rule;
@ -301,14 +303,14 @@ function useColumns(
}
columns.push({
id: 'actions',
label: 'Actions',
label: t('alerting.use-columns.label.actions', 'Actions'),
// eslint-disable-next-line react/display-name
renderCell: ({ data: rule }) => <RuleActionsCell rule={rule} isLoadingRuler={isRulerLoading} />,
size: '215px',
});
return columns;
}, [showSummaryColumn, showGroupColumn, showNextEvaluationColumn, isRulerLoading]);
}, [t, showNextEvaluationColumn, showSummaryColumn, showGroupColumn, isRulerLoading]);
}
function RuleStateCell({ rule }: { rule: CombinedRule }) {

@ -67,6 +67,7 @@ export const StateFilterValues = {
export const CentralAlertHistoryScene = () => {
//track the loading of the central alert state history
const { t } = useTranslate();
useEffect(() => {
logInfo(LogMessages.loadedCentralAlertStateHistory);
}, []);
@ -78,14 +79,17 @@ export const CentralAlertHistoryScene = () => {
// textbox variable for filtering by labels
const labelsFilterVariable = new TextBoxVariable({
name: LABELS_FILTER,
label: 'Labels: ',
label: t('alerting.central-alert-history-scene.scene.labels-filter-variable.label.labels', 'Labels: '),
});
//custom variable for filtering by the current state
const transitionsToFilterVariable = new CustomVariable({
name: STATE_FILTER_TO,
value: StateFilterValues.all,
label: 'End state:',
label: t(
'alerting.central-alert-history-scene.scene.transitions-to-filter-variable.label.end-state',
'End state:'
),
hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending},To Recovering : ${StateFilterValues.recovering}`,
});
@ -94,7 +98,10 @@ export const CentralAlertHistoryScene = () => {
const transitionsFromFilterVariable = new CustomVariable({
name: STATE_FILTER_FROM,
value: StateFilterValues.all,
label: 'Start state:',
label: t(
'alerting.central-alert-history-scene.scene.transitions-from-filter-variable.label.start-state',
'Start state:'
),
hide: VariableHide.dontHide,
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending},From Recovering : ${StateFilterValues.recovering}`,
});
@ -132,7 +139,7 @@ export const CentralAlertHistoryScene = () => {
],
}),
});
}, []);
}, [t]);
// we need to call this to sync the url with the scene state
const isUrlSyncInitialized = useUrlSync(scene);

@ -1,6 +1,7 @@
import { useEffect, useMemo } from 'react';
import { DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { getTemplateSrv } from '@grafana/runtime';
import { RuntimeDataSource, sceneUtils } from '@grafana/scenes';
import { DataQuery } from '@grafana/schema';
@ -72,7 +73,11 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
}
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 { DataFrame, InterpolateFunction, TimeRange } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { VisibilityMode } from '@grafana/schema';
import { LegendDisplayMode, useTheme2 } from '@grafana/ui';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
@ -16,6 +17,7 @@ interface LogTimelineViewerProps {
const replaceVariables: InterpolateFunction = (v) => v;
export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerProps) => {
const { t } = useTranslate();
const theme = useTheme2();
return (
@ -38,12 +40,36 @@ export const LogTimelineViewer = memo(({ frames, timeRange }: LogTimelineViewerP
showLegend: true,
}}
legendItems={[
{ label: 'Normal', color: theme.colors.success.main, yAxis: 1 },
{ label: 'Pending', color: theme.colors.warning.main, yAxis: 1 },
{ label: 'Recovering', color: theme.colors.warning.main, yAxis: 1 },
{ label: 'Firing', color: theme.colors.error.main, yAxis: 1 },
{ label: 'No Data', color: theme.colors.info.main, yAxis: 1 },
{ label: 'Mixed', color: theme.colors.text.secondary, yAxis: 1 },
{
label: t('alerting.log-timeline-viewer.label.normal', 'Normal'),
color: theme.colors.success.main,
yAxis: 1,
},
{
label: t('alerting.log-timeline-viewer.label.pending', 'Pending'),
color: theme.colors.warning.main,
yAxis: 1,
},
{
label: t('alerting.log-timeline-viewer.label.recovering', 'Recovering'),
color: theme.colors.warning.main,
yAxis: 1,
},
{
label: t('alerting.log-timeline-viewer.label.firing', 'Firing'),
color: theme.colors.error.main,
yAxis: 1,
},
{
label: t('alerting.log-timeline-viewer.label.no-data', 'No Data'),
color: theme.colors.info.main,
yAxis: 1,
},
{
label: t('alerting.log-timeline-viewer.label.mixed', 'Mixed'),
color: theme.colors.text.secondary,
yAxis: 1,
},
]}
replaceVariables={replaceVariables}
/>

@ -59,9 +59,19 @@ const StateHistory = ({ ruleUID }: Props) => {
}
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: '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

@ -80,7 +80,10 @@ export default function AlertmanagerConfig({ alertmanagerName, onDismiss, onSave
// manually register the config field with validation
// @TODO sometimes the value doesn't get registered – find out why
register('configJSON', {
required: { value: true, message: 'Configuration cannot be empty' },
required: {
value: true,
message: t('alerting.alertmanager-config.message.configuration-cannot-be-empty', 'Configuration cannot be empty'),
},
validate: (value: string) => {
try {
JSON.parse(value);

@ -63,7 +63,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
>
<Input
{...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}
placeholder={t('alerting.matchers-field.placeholder-label', 'label')}
@ -85,7 +88,12 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
)}
defaultValue={matcher.operator || matcherFieldOptions[0].value}
name={`matchers.${index}.operator`}
rules={{ required: { value: required, message: 'Required.' } }}
rules={{
required: {
value: required,
message: t('alerting.matchers-field.message.required', 'Required.'),
},
}}
/>
</Field>
<Field
@ -95,7 +103,10 @@ const MatchersField = ({ className, required, ruleUid }: Props) => {
>
<Input
{...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}
placeholder={t('alerting.matchers-field.placeholder-value', 'value')}

@ -148,12 +148,13 @@ export const SilencedInstancesPreview = ({ amSourceName, matchers: inputMatchers
};
function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
return [
{
id: 'state',
label: 'State',
label: t('alerting.use-columns.label.state', 'State'),
renderCell: function renderStateTag({ data }) {
return <AmAlertStateTag state={data.status.state} />;
},
@ -162,7 +163,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
},
{
id: 'labels',
label: 'Labels',
label: t('alerting.use-columns.label.labels', 'Labels'),
renderCell: function renderName({ data }) {
return <AlertLabels labels={data.labels} size="sm" />;
},
@ -170,7 +171,7 @@ function useColumns(): Array<DynamicTableColumnProps<AlertmanagerAlert>> {
},
{
id: 'created',
label: 'Created',
label: t('alerting.use-columns.label.created', 'Created'),
renderCell: function renderSummary({ data }) {
return <>{isNullDate(data.startsAt) ? '-' : dateTime(data.startsAt).format('YYYY-MM-DD HH:mm:ss')}</>;
},

@ -254,7 +254,9 @@ export const SilencesEditor = ({
invalid={!!formState.errors.comment}
>
<TextArea
{...register('comment', { required: { value: true, message: 'Required.' } })}
{...register('comment', {
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
})}
rows={5}
placeholder={t(
'alerting.silences-editor.comment-placeholder-details-about-the-silence',
@ -271,7 +273,9 @@ export const SilencesEditor = ({
invalid={!!formState.errors.createdBy}
>
<Input
{...register('createdBy', { required: { value: true, message: 'Required.' } })}
{...register('createdBy', {
required: { value: true, message: t('alerting.silences-editor.message.required', 'Required.') },
})}
placeholder={t(
'alerting.silences-editor.placeholder-whos-creating-the-silence',
"Who's creating the silence"
@ -321,9 +325,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
});
function ExistingSilenceEditorPage() {
const { t } = useTranslate();
const pageNav = {
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',
};
return (

@ -293,7 +293,7 @@ function useColumns(alertManagerSourceName: string) {
const columns: SilenceTableColumnProps[] = [
{
id: 'state',
label: 'State',
label: t('alerting.use-columns.columns.label.state', 'State'),
renderCell: function renderStateTag({ data: { status } }) {
return <SilenceStateTag state={status.state} />;
},
@ -301,7 +301,7 @@ function useColumns(alertManagerSourceName: string) {
},
{
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 } }) {
return metadata?.rule_title ? (
<Link
@ -317,7 +317,7 @@ function useColumns(alertManagerSourceName: string) {
},
{
id: 'matchers',
label: 'Matching labels',
label: t('alerting.use-columns.columns.label.matching-labels', 'Matching labels'),
renderCell: function renderMatchers({ data: { matchers } }) {
const filteredMatchers = matchers?.filter((matcher) => matcher.name !== MATCHER_ALERT_RULE_UID) || [];
return <Matchers matchers={filteredMatchers} />;
@ -326,7 +326,7 @@ function useColumns(alertManagerSourceName: string) {
},
{
id: 'alerts',
label: 'Alerts silenced',
label: t('alerting.use-columns.columns.label.alerts-silenced', 'Alerts silenced'),
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return (
<span data-testid="alerts">
@ -341,7 +341,7 @@ function useColumns(alertManagerSourceName: string) {
},
{
id: 'schedule',
label: 'Schedule',
label: t('alerting.use-columns.columns.label.schedule', 'Schedule'),
renderCell: function renderSchedule({ data: { startsAt, endsAt } }) {
const startsAtDate = dateMath.parse(startsAt);
const endsAtDate = dateMath.parse(endsAt);
@ -354,7 +354,7 @@ function useColumns(alertManagerSourceName: string) {
if (updateSupported) {
columns.push({
id: 'actions',
label: 'Actions',
label: t('alerting.use-columns.label.actions', 'Actions'),
renderCell: function renderActions({ data: silence }) {
const isExpired = silence.status.state === SilenceState.Expired;

@ -1,5 +1,6 @@
import { FieldValues, RegisterOptions } from 'react-hook-form';
import { t } from '@grafana/i18n/internal';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';
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> => ({
required: {
value: true,
message: 'Required.',
message: t('alerting.evaluate-every-validation-options.message.required', 'Required.'),
},
validate: (evaluateEvery: string) => {
try {

@ -1,5 +1,6 @@
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { getDataSourceSrv } from '@grafana/runtime';
import {
EmbeddedScene,
@ -214,7 +215,7 @@ export function getInsightsScenes() {
function getGrafanaManagedScenes() {
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,
isCollapsed: false,
body: new SceneFlexLayout({
@ -304,7 +305,7 @@ function getGrafanaManagedScenes() {
function getGrafanaAlertmanagerScenes() {
return new NestedScene({
title: 'Grafana Alertmanager',
title: t('alerting.get-grafana-alertmanager-scenes.title.grafana-alertmanager', 'Grafana Alertmanager'),
canCollapse: true,
isCollapsed: false,
body: new SceneFlexLayout({
@ -327,7 +328,7 @@ function getGrafanaAlertmanagerScenes() {
function getCloudScenes() {
return new NestedScene({
title: 'Mimir Alertmanager',
title: t('alerting.get-cloud-scenes.title.mimir-alertmanager', 'Mimir Alertmanager'),
canCollapse: true,
isCollapsed: false,
body: new SceneFlexLayout({
@ -361,7 +362,7 @@ function getCloudScenes() {
function getMimirManagedRulesScenes() {
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,
isCollapsed: false,
body: new SceneFlexLayout({
@ -402,14 +403,17 @@ function getMimirManagedRulesScenes() {
function getMimirManagedRulesPerGroupScenes() {
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',
datasource: cloudUsageDs,
query: 'label_values(grafanacloud_instance_rule_group_rules,rule_group)',
});
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,
isCollapsed: false,
body: new SceneFlexLayout({

@ -1,6 +1,7 @@
import { Observable } from 'rxjs';
import { DataQueryRequest, DataQueryResponse, DataQueryResponseData, TestDataSourceResponse } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { getBackendSrv } from '@grafana/runtime';
import {
PanelBuilders,
@ -37,7 +38,11 @@ class LokiAPIDatasource extends RuntimeDataSource {
}
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 { AppPluginConfig, setPluginComponentsHook, setPluginLinksHook } from '@grafana/runtime';
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 { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Badge, Stack, Text } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { PageNotFound } from 'app/core/components/PageNotFound/PageNotFound';
@ -16,8 +17,11 @@ import { SSOProvider } from './types';
const getPageNav = (config?: SSOProvider): NavModelItem => {
if (!config) {
return {
text: 'Authentication',
subTitle: 'Configure authentication providers',
text: t('auth-config.get-page-nav.text.authentication', 'Authentication'),
subTitle: t(
'auth-config.get-page-nav.subTitle.configure-authentication-providers',
'Configure authentication providers'
),
icon: 'shield',
id: 'authentication',
};

@ -313,7 +313,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
),
multi: false,
options: clientAuthenticationOptions(provider),
defaultValue: { value: 'none', label: 'None' },
defaultValue: { value: 'none', label: t('auth-config.field-map.label.none', 'None') },
validation: {
required: true,
message: t('auth-config.fields.required', 'This field is required'),
@ -405,11 +405,13 @@ export function fieldMap(provider: string): Record<string, FieldData> {
),
multi: false,
options: [
/* eslint-disable @grafana/no-untranslated-strings */
{ value: 'AutoDetect', label: 'AutoDetect' },
{ value: 'InParams', label: 'InParams' },
{ value: 'InHeader', label: 'InHeader' },
],
defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },
/* eslint-enable @grafana/no-untranslated-strings */
},
tokenUrl: {
label: tokenURLLabel,
@ -884,6 +886,7 @@ function orgMappingDescription(provider: string): string {
function clientAuthenticationOptions(provider: string): Array<SelectableValue<string>> {
// Other options are purposefully not translated
/* eslint-disable @grafana/no-untranslated-strings */
switch (provider) {
case 'azuread':
return [
@ -898,4 +901,5 @@ function clientAuthenticationOptions(provider: string): Array<SelectableValue<st
{ 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 { GrafanaTheme2, PluginState } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { TextDimensionMode } from '@grafana/schema';
import { Button, Spinner, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context';
@ -194,9 +195,9 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.button-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.button-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.button-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -206,7 +207,7 @@ export const buttonItem: CanvasElementItem<ButtonConfig, ButtonData> = {
path: 'config.size',
name: 'Text size',
settings: {
placeholder: 'Auto',
placeholder: t('canvas.button-item.placeholder.auto', 'Auto'),
},
})
.addCustomEditor({

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -153,9 +154,9 @@ export const cloudItem: CanvasElementItem = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.cloud-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.cloud-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.cloud-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -166,9 +167,9 @@ export const cloudItem: CanvasElementItem = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.cloud-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.cloud-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.cloud-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -178,7 +179,7 @@ export const cloudItem: CanvasElementItem = {
path: 'config.size',
name: 'Text size',
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 { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -159,9 +160,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.ellipse-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.ellipse-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.ellipse-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -172,9 +173,9 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.ellipse-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.ellipse-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.ellipse-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -184,7 +185,7 @@ export const ellipseItem: CanvasElementItem<CanvasElementConfig, CanvasElementDa
path: 'config.size',
name: 'Text size',
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 { DataFrame, FieldNamePickerConfigSettings, GrafanaTheme2, StandardEditorsRegistryItem } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { TextDimensionMode } from '@grafana/schema';
import { usePanelContext, useStyles2 } from '@grafana/ui';
import { FieldNamePicker, frameHasName, getFrameFieldsDisplayNames } from '@grafana/ui/internal';
@ -220,9 +221,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.metric-value-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.metric-value-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.metric-value-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -233,9 +234,9 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.metric-value-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.metric-value-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.metric-value-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -245,7 +246,7 @@ export const metricValueItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size',
name: 'Text size',
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 { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -153,9 +154,9 @@ export const parallelogramItem: CanvasElementItem = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.parallelogram-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.parallelogram-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.parallelogram-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -166,9 +167,9 @@ export const parallelogramItem: CanvasElementItem = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.parallelogram-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.parallelogram-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.parallelogram-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -178,7 +179,7 @@ export const parallelogramItem: CanvasElementItem = {
path: 'config.size',
name: 'Text size',
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 { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions/context';
@ -120,9 +121,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.rectangle-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.rectangle-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.rectangle-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -133,9 +134,9 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.rectangle-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.rectangle-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.rectangle-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -145,7 +146,7 @@ export const rectangleItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size',
name: 'Text size',
settings: {
placeholder: 'Auto',
placeholder: t('canvas.rectangle-item.placeholder.auto', 'Auto'),
},
});
},

@ -6,6 +6,7 @@ import { of } from 'rxjs';
import { DataFrame, GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Input, usePanelContext, useStyles2 } from '@grafana/ui';
import { DimensionContext } from 'app/features/dimensions/context';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -197,9 +198,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.text-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.text-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.text-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -210,9 +211,9 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.text-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.text-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.text-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -222,7 +223,7 @@ export const textItem: CanvasElementItem<TextConfig, TextData> = {
path: 'config.size',
name: 'Text size',
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 { GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from 'app/core/config';
import { DimensionContext } from 'app/features/dimensions';
import { ColorDimensionEditor } from 'app/features/dimensions/editors/ColorDimensionEditor';
@ -154,9 +155,9 @@ export const triangleItem: CanvasElementItem = {
name: 'Align text',
settings: {
options: [
{ value: Align.Left, label: 'Left' },
{ value: Align.Center, label: 'Center' },
{ value: Align.Right, label: 'Right' },
{ value: Align.Left, label: t('canvas.triangle-item.label.left', 'Left') },
{ value: Align.Center, label: t('canvas.triangle-item.label.center', 'Center') },
{ value: Align.Right, label: t('canvas.triangle-item.label.right', 'Right') },
],
},
defaultValue: Align.Left,
@ -167,9 +168,9 @@ export const triangleItem: CanvasElementItem = {
name: 'Vertical align',
settings: {
options: [
{ value: VAlign.Top, label: 'Top' },
{ value: VAlign.Middle, label: 'Middle' },
{ value: VAlign.Bottom, label: 'Bottom' },
{ value: VAlign.Top, label: t('canvas.triangle-item.label.top', 'Top') },
{ value: VAlign.Middle, label: t('canvas.triangle-item.label.middle', 'Middle') },
{ value: VAlign.Bottom, label: t('canvas.triangle-item.label.bottom', 'Bottom') },
],
},
defaultValue: VAlign.Middle,
@ -179,7 +180,7 @@ export const triangleItem: CanvasElementItem = {
path: 'config.size',
name: 'Text size',
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 { NavModel, NavModelItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { getNavModel } from 'app/core/selectors/navModel';
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';
export function useDataSourceSettingsNav(pageIdParam?: string) {
const { t } = useTranslate();
const { uid = '' } = useParams<{ uid: string }>();
const location = useLocation();
const datasource = useDataSource(uid);
@ -27,17 +29,17 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
const navIndexId = pageId ? `datasource-${pageId}-${uid}` : `datasource-settings-${uid}`;
let pageNav: NavModel = {
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: {
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) {
const node: NavModelItem = {
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',
};

@ -1,6 +1,6 @@
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 { PluginDetailsPage } from 'app/features/plugins/admin/components/PluginDetailsPage';
import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
@ -8,6 +8,7 @@ import { StoreState, useSelector, AppNotificationSeverity } from 'app/types';
import { ROUTES } from '../constants';
export function DataSourceDetailsPage() {
const { t } = useTranslate();
const overrideNavId = 'standalone-plugin-page-/connections/add-new-connection';
const { id = '' } = useParams<{ id: string }>();
const navIndex = useSelector((state: StoreState) => state.navIndex);
@ -20,8 +21,11 @@ export function DataSourceDetailsPage() {
navId={navId}
notFoundComponent={<NotFoundDatasource />}
notFoundNavModel={{
text: 'Unknown datasource',
subTitle: 'No datasource with this ID could be found.',
text: t('connections.data-source-details-page.text.unknown-datasource', 'Unknown datasource'),
subTitle: t(
'connections.data-source-details-page.subTitle.datasource-could-found',
'No datasource with this ID could be found.'
),
active: true,
}}
/>

@ -1,11 +1,17 @@
import { useTranslate } from '@grafana/i18n';
import { Page } from 'app/core/components/Page/Page';
import { NewDataSource } from 'app/features/datasources/components/NewDataSource';
export function NewDataSourcePage() {
const { t } = useTranslate();
return (
<Page
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>
<NewDataSource />

@ -69,9 +69,12 @@ export function AddNewConnection() {
);
const { t } = useTranslate();
const filterByOptions = [
{ value: 'all', label: 'All' },
{ value: 'installed', label: 'Installed' },
{ value: 'has-update', label: 'New Updates' },
{ value: 'all', label: t('connections.add-new-connection.filter-by-options.label.all', 'All') },
{ value: 'installed', label: t('connections.add-new-connection.filter-by-options.label.installed', 'Installed') },
{
value: 'has-update',
label: t('connections.add-new-connection.filter-by-options.label.new-updates', 'New Updates'),
},
];
const onClickCardGridItem = (e: MouseEvent<HTMLElement>, item: CardGridItem) => {
@ -160,11 +163,17 @@ export function AddNewConnection() {
value={sortBy?.toString()}
onChange={onSortByChange}
options={[
{ value: 'nameAsc', label: 'By name (A-Z)' },
{ value: 'nameDesc', label: 'By name (Z-A)' },
{ value: 'updated', label: 'By updated date' },
{ value: 'published', label: 'By published date' },
{ value: 'downloads', label: 'By downloads' },
{ value: 'nameAsc', label: t('connections.add-new-connection.label.by-name-az', 'By name (A-Z)') },
{ value: 'nameDesc', label: t('connections.add-new-connection.label.by-name-za', 'By name (Z-A)') },
{
value: 'updated',
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>

@ -66,14 +66,14 @@ export function AddToDashboardForm<TOptions = undefined>({
if (canCreateDashboard) {
saveTargets.push({
label: 'New dashboard',
label: t('dashboard-scene.add-to-dashboard-form.label.new-dashboard', 'New dashboard'),
value: SaveTarget.NewDashboard,
});
}
if (canWriteDashboard) {
saveTargets.push({
label: 'Existing dashboard',
label: t('dashboard-scene.add-to-dashboard-form.label.existing-dashboard', 'Existing dashboard'),
value: SaveTarget.ExistingDashboard,
});
}
@ -154,7 +154,15 @@ export function AddToDashboardForm<TOptions = undefined>({
control={control}
name="dashboardUid"
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 { t } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime';
import { Panel } from '@grafana/schema';
import store from 'app/core/store';
@ -56,7 +57,10 @@ export function addToDashboard({
} catch {
return {
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);
return {
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 { PageLayoutType } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Box } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { EmbeddedDashboard } from './EmbeddedDashboard';
export function EmbeddedDashboardTestPage() {
const { t } = useTranslate();
const [state, setState] = useState('?from=now-5m&to=now');
return (
<Page
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}
>
{/* this is a test page, no need to translate */}

@ -63,8 +63,8 @@ export function HelpWizard({ panel, onClose }: Props) {
}
const tabs = [
{ label: 'Snapshot', value: SnapshotTab.Support },
{ label: 'Data', value: SnapshotTab.Data },
{ label: t('dashboard-scene.help-wizard.tabs.label.snapshot', 'Snapshot'), value: SnapshotTab.Support },
{ label: t('dashboard-scene.help-wizard.tabs.label.data', 'Data'), value: SnapshotTab.Data },
];
const hasSupportBundleAccess =

@ -1,6 +1,7 @@
import saveAs from 'file-saver';
import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
@ -54,13 +55,16 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
snapshotUpdate: 0,
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',
value: ShowMessage.GithubComment,
},
{
label: 'Panel support snapshot',
description: 'Dashboard JSON used to help troubleshoot visualization issues',
label: t('dashboard-scene.support-snapshot-service.label.panel-support-snapshot', 'Panel support snapshot'),
description: t(
'dashboard-scene.support-snapshot-service.description.dashboard-troubleshoot-visualization-issues',
'Dashboard JSON used to help troubleshoot visualization issues'
),
value: ShowMessage.PanelSnapshot,
},
],
@ -102,7 +106,10 @@ export class SupportSnapshotService extends StateManagerBase<SupportSnapshotStat
if (markdownText.length > maxLen) {
this.setState({
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',
},
});

@ -149,6 +149,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
}
if (annotationsCount > 0) {
const DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE = 'Annotations';
dashboard.panels.push({
id: 7,
gridPos: {
@ -158,7 +159,7 @@ export async function getDebugDashboard(panel: VizPanel, rand: Randomize, timeRa
y: 20,
},
type: 'table',
title: 'Annotations',
title: DEBUG_DASHBOARD_TITLE_DO_NOT_TRANSLATE,
datasource: {
type: 'datasource',
uid: '-- Dashboard --',

@ -1,6 +1,7 @@
import { isEqual } from 'lodash';
import { locationUtil, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes';
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,
loadError: {
status: 404,
message: 'Dashboard not found',
message: t(
'dashboard-scene.dashboard-scene-page-state-manager.message.dashboard-not-found',
'Dashboard not found'
),
},
});
return;
@ -728,7 +732,10 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
isLoading: false,
loadError: {
status: 404,
message: 'Dashboard not found',
message: t(
'dashboard-scene.dashboard-scene-page-state-manager-v2.message.dashboard-not-found',
'Dashboard not found'
),
},
});
return;

@ -2,6 +2,7 @@ import * as H from 'history';
import { debounce } from 'lodash';
import { NavIndex, PanelPlugin } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime';
import {
PanelBuilders,
@ -245,7 +246,7 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
const dashboard = getDashboardSceneFor(this);
return {
text: 'Edit panel',
text: t('dashboard-scene.panel-editor.text.edit-panel', 'Edit panel'),
parentItem: dashboard.getPageNav(location, navIndex),
};
}

@ -76,8 +76,14 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
const { t } = useTranslate();
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 (

@ -1,5 +1,6 @@
import { CoreApp } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime';
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
import { DataLinksInlineEditor, Input, TextArea, Switch } from '@grafana/ui';
@ -19,7 +20,7 @@ import { getDashboardSceneFor } from '../utils/utils';
export function getPanelFrameOptions(panel: VizPanel): 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',
isOpenDefault: true,
});
@ -32,7 +33,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
descriptor
.addItem(
new OptionsPaneItemDescriptor({
title: 'Title',
title: t('dashboard-scene.get-panel-frame-options.title.title', 'Title'),
value: panel.state.title,
popularRank: 1,
render: function renderTitle() {
@ -49,7 +50,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Description',
title: t('dashboard-scene.get-panel-frame-options.title.description', 'Description'),
value: panel.state.description,
render: function renderDescription() {
return <PanelDescriptionTextArea panel={panel} />;
@ -64,7 +65,7 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
)
.addItem(
new OptionsPaneItemDescriptor({
title: 'Transparent background',
title: t('dashboard-scene.get-panel-frame-options.title.transparent-background', 'Transparent background'),
render: function renderTransparent() {
return <PanelBackgroundSwitch panel={panel} />;
},
@ -72,13 +73,13 @@ export function getPanelFrameOptions(panel: VizPanel): OptionsPaneCategoryDescri
)
.addCategory(
new OptionsPaneCategoryDescriptor({
title: 'Panel links',
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
id: 'Panel links',
isOpenDefault: false,
itemsCount: links?.length,
}).addItem(
new OptionsPaneItemDescriptor({
title: 'Panel links',
title: t('dashboard-scene.get-panel-frame-options.title.panel-links', 'Panel links'),
render: () => <ScenePanelLinksEditor panelLinks={panelLinksObject ?? undefined} />,
})
)

@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { RepositoryView } from 'app/api/clients/provisioning';
export function getDefaultWorkflow(config?: RepositoryView, loadedFromRef?: string) {
@ -27,7 +28,10 @@ export function getWorkflowOptions(config?: RepositoryView, ref?: string) {
case 'write':
return { label: ref ? `Push to ${ref}` : 'Save', value };
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 };
});

@ -1,6 +1,7 @@
import * as H from 'history';
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config, locationService, RefreshEvent } from '@grafana/runtime';
import {
sceneGraph,
@ -192,7 +193,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
public constructor(state: Partial<DashboardSceneState>, serializerVersion: 'v1' | 'v2' = 'v1') {
super({
title: 'Dashboard',
title: t('dashboard-scene.dashboard-scene.title.dashboard', 'Dashboard'),
meta: {},
editable: true,
$timeRange: state.$timeRange ?? new SceneTimeRange({}),
@ -321,7 +322,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
appEvents.publish(
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?`,
icon: 'trash-alt',
yesText: 'Discard',
@ -462,7 +463,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
if (viewPanelScene) {
pageNav = {
text: 'View panel',
text: t('dashboard-scene.dashboard-scene.text.view-panel', 'View panel'),
parentItem: pageNav,
url: getViewPanelUrl(viewPanelScene.state.panelRef.resolve()),
};
@ -470,7 +471,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
if (editPanel) {
pageNav = {
text: 'Edit panel',
text: t('dashboard-scene.dashboard-scene.text.edit-panel', 'Edit panel'),
parentItem: pageNav,
};
}

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { textUtil } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { t as internalT } from '@grafana/i18n/internal';
import { config, locationService } from '@grafana/runtime';
import { ConfirmModal, ToolbarButton } from '@grafana/ui';
@ -33,7 +34,10 @@ const onOpenSnapshotOriginalDashboard = (originalUrl: string) => {
new ShowModalReactEvent({
component: ConfirmModal,
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({
width: 'max-content',
maxWidth: '80vw',

@ -328,7 +328,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
// Add specific "Metrics drilldown" menu
if (metricsDrilldownLinks.length > 0) {
items.push({
text: 'Metrics drilldown',
text: t('dashboard-scene.panel-menu-behavior.async-func.text.metrics-drilldown', 'Metrics drilldown'),
iconClassName: 'code-branch',
type: 'submenu',
subMenu: createExtensionSubMenu(metricsDrilldownLinks),
@ -338,7 +338,7 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
// Add generic "Extensions" menu for other links
if (otherLinks.length > 0) {
items.push({
text: 'Extensions',
text: t('dashboard-scene.panel-menu-behavior.async-func.text.extensions', 'Extensions'),
iconClassName: 'plug',
type: 'submenu',
subMenu: createExtensionSubMenu(otherLinks),
@ -544,8 +544,8 @@ function createExtensionContext(panel: VizPanel, dashboard: DashboardScene): Plu
export function onRemovePanel(dashboard: DashboardScene, panel: VizPanel) {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Remove panel',
text: 'Are you sure you want to remove this panel?',
title: t('dashboard-scene.on-remove-panel.title.remove-panel', 'Remove panel'),
text: t('dashboard-scene.on-remove-panel.text.remove-panel', 'Are you sure you want to remove this panel?'),
icon: 'trash-alt',
yesText: 'Remove',
onConfirm: () => dashboard.removePanel(panel),

@ -4,6 +4,7 @@ import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { Button, Combobox, ComboboxOption, Field, InlineSwitch, Input, Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@ -14,7 +15,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
options.push(
new OptionsPaneItemDescriptor({
title: 'Column options',
title: t('dashboard-scene.get-edit-options.title.column-options', 'Column options'),
skipField: true,
render: () => <GridLayoutColumns layoutManager={layoutManager} />,
})
@ -22,7 +23,7 @@ export function getEditOptions(layoutManager: AutoGridLayoutManager): OptionsPan
options.push(
new OptionsPaneItemDescriptor({
title: 'Row height options',
title: t('dashboard-scene.get-edit-options.title.row-height-options', 'Row height options'),
skipField: true,
render: () => <GridLayoutRows layoutManager={layoutManager} />,
})

@ -292,7 +292,7 @@ export class DefaultGridLayoutManager
const row = new SceneGridRow({
key: getVizPanelKeyForPanelId(id),
title: 'Row title',
title: t('dashboard-scene.default-grid-layout-manager.row.title.row-title', 'Row title'),
actions: new RowActions({}),
y: 0,
});

@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime';
import { VariableModel, defaultDashboard } from '@grafana/schema';
import {
@ -55,7 +56,7 @@ export async function buildNewDashboardSaveModel(urlFolderUid?: string): Promise
dashboard: {
...defaultDashboard,
uid: '',
title: 'New dashboard',
title: t('dashboard-scene.build-new-dashboard-save-model.data.title.new-dashboard', 'New dashboard'),
panels: [],
timezone: config.bootData.user?.timezone || defaultDashboard.timezone,
},
@ -112,7 +113,7 @@ export async function buildNewDashboardSaveModelV2(
kind: 'DashboardWithAccessInfo',
spec: {
...defaultDashboardV2Spec(),
title: 'New dashboard',
title: t('dashboard-scene.build-new-dashboard-save-model-v2.data.title.new-dashboard', 'New dashboard'),
timeSettings: {
...defaultTimeSettingsSpec(),
timezone: config.bootData.user?.timezone || defaultTimeSettingsSpec().timezone,

@ -1,4 +1,5 @@
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema';
import { Page } from 'app/core/components/Page/Page';
@ -119,8 +120,9 @@ interface EditLinkViewProps {
}
function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }: EditLinkViewProps) {
const { t } = useTranslate();
const editLinkPageNav = {
text: 'Edit link',
text: t('dashboard-scene.edit-link-view.edit-link-page-nav.text.edit-link', 'Edit link'),
parentItem: pageNav,
};

@ -36,7 +36,7 @@ export function useDashboardEditPageNav(dashboard: DashboardScene, currentEditVi
const dashboardPageNav = dashboard.getPageNav(location, navIndex);
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 }),
children: [],
parentItem: dashboardPageNav,

@ -136,7 +136,7 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete
fill="outline"
onClick={() => {
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}?`,
confirmText: 'Delete variable',
onConfirm: onDeleteVariable(hideModal),

@ -59,9 +59,15 @@ export function ResourceExport({
<Label>{switchExportModeLabel}</Label>
<RadioButtonGroup
options={[
{ label: 'Classic', value: ExportMode.Classic },
{ label: 'V1 Resource', value: ExportMode.V1Resource },
{ label: 'V2 Resource', value: ExportMode.V2Resource },
{ label: t('dashboard-scene.resource-export.label.classic', 'Classic'), value: ExportMode.Classic },
{
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}
onChange={(value) => onExportModeChange(value)}
@ -73,8 +79,8 @@ export function ResourceExport({
<Label>{switchExportFormatLabel}</Label>
<RadioButtonGroup
options={[
{ label: 'JSON', value: 'json' },
{ label: 'YAML', value: 'yaml' },
{ label: t('dashboard-scene.resource-export.label.json', 'JSON'), value: 'json' },
{ label: t('dashboard-scene.resource-export.label.yaml', 'YAML'), value: 'yaml' },
]}
value={isViewingYAML ? 'yaml' : 'json'}
onChange={onViewYAML}

@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Trans, useTranslate } from '@grafana/i18n';
import { SceneComponentProps, sceneGraph } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
@ -24,6 +24,7 @@ interface Props extends SceneComponentProps<SharePublicDashboardTab> {
}
export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }: Props) {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const hasWritePermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPublicWrite);
@ -45,7 +46,7 @@ export function ConfigPublicDashboard({ model, publicDashboard, isGetLoading }:
dashboard.showModal(
new ConfirmModal({
isOpen: true,
title: 'Revoke public URL',
title: t('dashboard-scene.config-public-dashboard.title.revoke-public-url', 'Revoke public URL'),
icon: 'trash-alt',
confirmText: 'Revoke public URL',
body: (

@ -1,3 +1,4 @@
import { t } from '@grafana/i18n/internal';
import { SceneVariable, SceneVariableState } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema/dist/esm/index.gen';
import { safeStringifyValue } from 'app/core/utils/explore';
@ -72,7 +73,12 @@ export function transformUsagesToNetwork(
const { variable, tree } = usage;
const result: UsagesToNetwork = {
variable,
nodes: [{ id: 'dashboard', label: 'dashboard' }],
nodes: [
{
id: 'dashboard',
label: t('dashboard-scene.transform-usages-to-network.result.label.dashboard', 'dashboard'),
},
],
edges: [],
showGraph: false,
};

@ -1,4 +1,5 @@
import { AppEvents, UrlQueryMap } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { FetchError, getBackendSrv } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import appEvents from 'app/core/app_events';
@ -38,7 +39,9 @@ export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard>
const fetchError: FetchError = {
status: 404,
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;
}

@ -1,4 +1,5 @@
import { locationUtil } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { Dashboard } from '@grafana/schema';
import { backendSrv } from 'app/core/services/backend_srv';
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) => ({
id: 0,
message: v.message,
title: 'deleted',
title: t('dashboard.k8s-dashboard-api.title.deleted', 'deleted'),
}));
}

@ -1,4 +1,5 @@
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 { backendSrv } from 'app/core/services/backend_srv';
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
@ -91,7 +92,7 @@ export class K8sDashboardV2API
return this.client.delete(uid, showSuccessAlert).then((v) => ({
id: 0,
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({
component: ConfirmModal,
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,
body: (
<>

@ -85,7 +85,7 @@ export class UnthemedDashboardRow extends Component<DashboardRowProps> {
onDelete = () => {
appEvents.publish(
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?',
altActionText: 'Delete row only',
icon: 'trash-alt',

@ -116,8 +116,8 @@ export function GeneralSettingsUnconnected({
};
const editableOptions = [
{ label: 'Editable', value: true },
{ label: 'Read-only', value: false },
{ label: t('dashboard.general-settings-unconnected.editable-options.label.editable', 'Editable'), value: true },
{ label: t('dashboard.general-settings-unconnected.editable-options.label.readonly', 'Read-only'), value: false },
];
return (

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

Loading…
Cancel
Save