Internationalisation: Mark up panel plugins for translations (#106068)

* config for panel plugin translations

* markup

* tweaks

* prettier

* use data-testid

* rename disable rule now it's been moved

* final markup
pull/106216/head
Ashley Harrison 4 weeks ago committed by GitHub
parent e1400e67d3
commit 4ac248a512
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      apps/alerting/notifications/pkg/apis/alerting_manifest.go
  2. 2
      apps/dashboard/pkg/apis/dashboard_manifest.go
  3. 2
      apps/folder/pkg/apis/folder_manifest.go
  4. 2
      apps/iam/pkg/apis/iam_manifest.go
  5. 2
      apps/playlist/pkg/apis/playlist_manifest.go
  6. 6
      eslint.config.js
  7. 1
      packages/grafana-e2e-selectors/src/selectors/components.ts
  8. 4
      pkg/services/folder/folderimpl/sqlstore_test.go
  9. 8
      pkg/services/org/orgimpl/org_delete_svc_test.go
  10. 6
      public/app/plugins/panel/alertlist/GroupByWithLoading.tsx
  11. 11
      public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
  12. 3
      public/app/plugins/panel/alertlist/module.tsx
  13. 25
      public/app/plugins/panel/alertlist/unified-alerting/UngroupedView.tsx
  14. 24
      public/app/plugins/panel/annolist/AnnoListPanel.tsx
  15. 7
      public/app/plugins/panel/annolist/AnnotationListItem.tsx
  16. 12
      public/app/plugins/panel/barchart/TickSpacingEditor.tsx
  17. 29
      public/app/plugins/panel/canvas/components/CanvasContextMenu.tsx
  18. 4
      public/app/plugins/panel/canvas/editor/LineStyleEditor.tsx
  19. 26
      public/app/plugins/panel/canvas/editor/element/APIEditor.tsx
  20. 4
      public/app/plugins/panel/canvas/editor/element/ButtonStyleEditor.tsx
  21. 27
      public/app/plugins/panel/canvas/editor/element/ParamsEditor.tsx
  22. 12
      public/app/plugins/panel/canvas/editor/element/PlacementEditor.tsx
  23. 14
      public/app/plugins/panel/canvas/editor/element/QuickPositioning.tsx
  24. 8
      public/app/plugins/panel/canvas/editor/inline/InlineEdit.tsx
  25. 14
      public/app/plugins/panel/canvas/editor/inline/InlineEditBody.tsx
  26. 26
      public/app/plugins/panel/canvas/editor/layer/TreeNavigationEditor.tsx
  27. 10
      public/app/plugins/panel/canvas/editor/layer/TreeNodeTitle.tsx
  28. 22
      public/app/plugins/panel/canvas/editor/panZoomHelp.tsx
  29. 5
      public/app/plugins/panel/datagrid/components/AddColumn.tsx
  30. 23
      public/app/plugins/panel/datagrid/components/DatagridContextMenu.tsx
  31. 13
      public/app/plugins/panel/debug/CursorView.tsx
  32. 1
      public/app/plugins/panel/debug/EventBusLogger.tsx
  33. 55
      public/app/plugins/panel/debug/RenderInfoViewer.tsx
  34. 12
      public/app/plugins/panel/debug/StateView.tsx
  35. 3
      public/app/plugins/panel/geomap/GeomapPanel.tsx
  36. 9
      public/app/plugins/panel/geomap/components/DebugOverlay.tsx
  37. 4
      public/app/plugins/panel/geomap/components/MarkersLegend.tsx
  38. 6
      public/app/plugins/panel/geomap/components/MeasureOverlay.tsx
  39. 14
      public/app/plugins/panel/geomap/editor/CoordinatesMapViewEditor.tsx
  40. 17
      public/app/plugins/panel/geomap/editor/FitMapViewEditor.tsx
  41. 12
      public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx
  42. 10
      public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx
  43. 10
      public/app/plugins/panel/geomap/editor/LayersEditor.tsx
  44. 18
      public/app/plugins/panel/geomap/editor/MapViewEditor.tsx
  45. 36
      public/app/plugins/panel/geomap/editor/StyleEditor.tsx
  46. 18
      public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx
  47. 9
      public/app/plugins/panel/geomap/module.tsx
  48. 17
      public/app/plugins/panel/gettingstarted/GettingStarted.tsx
  49. 9
      public/app/plugins/panel/gettingstarted/components/DocsCard.tsx
  50. 6
      public/app/plugins/panel/gettingstarted/components/TutorialCard.tsx
  51. 7
      public/app/plugins/panel/histogram/HistogramPanel.tsx
  52. 27
      public/app/plugins/panel/live/LiveChannelEditor.tsx
  53. 21
      public/app/plugins/panel/live/LivePanel.tsx
  54. 5
      public/app/plugins/panel/live/LivePublish.tsx
  55. 5
      public/app/plugins/panel/logs/LogsPanel.tsx
  56. 25
      public/app/plugins/panel/news/NewsPanel.tsx
  57. 8
      public/app/plugins/panel/nodeGraph/Edge.tsx
  58. 12
      public/app/plugins/panel/nodeGraph/Marker.tsx
  59. 8
      public/app/plugins/panel/nodeGraph/Node.tsx
  60. 24
      public/app/plugins/panel/nodeGraph/NodeGraph.tsx
  61. 5
      public/app/plugins/panel/nodeGraph/NodeGraphPanel.tsx
  62. 10
      public/app/plugins/panel/nodeGraph/ViewControls.tsx
  63. 12
      public/app/plugins/panel/nodeGraph/editor/ArcOptionsEditor.tsx
  64. 17
      public/app/plugins/panel/nodeGraph/useContextMenu.tsx
  65. 7
      public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
  66. 9
      public/app/plugins/panel/table/cells/AutoCellOptionsEditor.tsx
  67. 6
      public/app/plugins/panel/table/cells/BarGaugeCellOptionsEditor.tsx
  68. 28
      public/app/plugins/panel/table/cells/ColorBackgroundCellOptionsEditor.tsx
  69. 17
      public/app/plugins/panel/table/cells/ImageCellOptionsEditor.tsx
  70. 4
      public/app/plugins/panel/table/table-new/cells/AutoCellOptionsEditor.tsx
  71. 6
      public/app/plugins/panel/table/table-new/cells/BarGaugeCellOptionsEditor.tsx
  72. 15
      public/app/plugins/panel/table/table-new/cells/ColorBackgroundCellOptionsEditor.tsx
  73. 17
      public/app/plugins/panel/table/table-new/cells/ImageCellOptionsEditor.tsx
  74. 9
      public/app/plugins/panel/timeseries/LineStyleEditor.tsx
  75. 4
      public/app/plugins/panel/timeseries/NullsThresholdInput.tsx
  76. 14
      public/app/plugins/panel/timeseries/TimezonesEditor.tsx
  77. 7
      public/app/plugins/panel/timeseries/plugins/OutsideRangePlugin.tsx
  78. 24
      public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationEditor2.tsx
  79. 13
      public/app/plugins/panel/timeseries/plugins/annotations2/AnnotationTooltip2.tsx
  80. 5
      public/app/plugins/panel/traces/TracesPanel.tsx
  81. 9
      public/app/plugins/panel/welcome/Welcome.tsx
  82. 27
      public/app/plugins/panel/xychart/SeriesEditor.tsx
  83. 418
      public/locales/en-US/grafana.json
  84. 2
      public/locales/i18next-parser.config.cjs

@ -14,8 +14,6 @@ import (
v0alpha1 "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/alerting/v0alpha1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "alerting",
Group: "notifications.alerting.grafana.app",

@ -14,8 +14,6 @@ import (
v2alpha1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v2alpha1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "dashboard",
Group: "dashboard.grafana.app",

@ -14,8 +14,6 @@ import (
v1beta1 "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "folder",
Group: "folder.grafana.app",

@ -14,8 +14,6 @@ import (
v0alpha1 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "iam",
Group: "iam.grafana.app",

@ -14,8 +14,6 @@ import (
v0alpha1 "github.com/grafana/grafana/apps/playlist/pkg/apis/playlist/v0alpha1"
)
var ()
var appManifestData = app.ManifestData{
AppName: "playlist",
Group: "playlist.grafana.app",

@ -20,7 +20,11 @@ const getEnvConfig = require('./scripts/webpack/env-util');
const envConfig = getEnvConfig();
const enableBettererRules = envConfig.frontend_dev_betterer_eslint_rules;
const pluginsToTranslate = ['public/app/plugins/datasource/azuremonitor', 'public/app/plugins/datasource/mssql'];
const pluginsToTranslate = [
'public/app/plugins/panel',
'public/app/plugins/datasource/azuremonitor',
'public/app/plugins/datasource/mssql',
];
/**
* @type {Array<import('eslint').Linter.Config>}

@ -578,6 +578,7 @@ export const versionedComponents = {
},
measureButton: {
'12.1.0': 'data-testid panel-editor-measure-button',
'9.2.0': 'show measure tools',
},

@ -1002,10 +1002,10 @@ func CreateOrg(t *testing.T, db db.DB, cfg *setting.Cfg) int64 {
requester := &identity.StaticRequester{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: map[string][]string{
1: {
accesscontrol.ActionOrgsDelete: {"*"},
},
2: map[string][]string{
2: {
accesscontrol.ActionOrgsDelete: {"*"},
},
},

@ -31,10 +31,10 @@ func TestDeletionService_Delete(t *testing.T) {
requester := &identity.StaticRequester{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: map[string][]string{
1: {
accesscontrol.ActionOrgsDelete: {"*"},
},
2: map[string][]string{
2: {
accesscontrol.ActionOrgsDelete: {"*"},
},
},
@ -52,10 +52,10 @@ func TestDeletionService_Delete(t *testing.T) {
requester = &identity.StaticRequester{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: map[string][]string{
1: {
accesscontrol.ActionOrgsRead: {"*"},
},
2: map[string][]string{
2: {
accesscontrol.ActionOrgsRead: {"*"},
},
},

@ -2,6 +2,7 @@ import { isEmpty, uniq } from 'lodash';
import { useEffect, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Icon, MultiSelect } from '@grafana/ui';
import { useUnifiedAlertingSelector } from 'app/features/alerting/unified/hooks/useUnifiedAlertingSelector';
import { fetchAllPromRulesAction } from 'app/features/alerting/unified/state/actions';
@ -24,6 +25,7 @@ interface Props {
}
export const GroupBy = (props: Props) => {
const { t } = useTranslate();
const { onChange, id, defaultValue, dataSource } = props;
const dispatch = useDispatch();
@ -65,8 +67,8 @@ export const GroupBy = (props: Props) => {
id={id}
isLoading={loading}
defaultValue={defaultValue}
aria-label={'group by label keys'}
placeholder="Group by"
aria-label={t('alertlist.group-by.aria-label-group-by-label-keys', 'group by label keys')}
placeholder={t('alertlist.group-by.placeholder-group-by', 'Group by')}
prefix={<Icon name={'tag-alt'} />}
onChange={(items) => {
onChange(items.map((item) => item.value ?? ''));

@ -4,6 +4,7 @@ import { useEffect, useMemo } from 'react';
import { useEffectOnce, useToggle } from 'react-use';
import { GrafanaTheme2, PanelProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { TimeRangeUpdatedEvent } from '@grafana/runtime';
import {
Alert,
@ -94,6 +95,7 @@ const fetchPromAndRuler = ({
};
function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const { t } = useTranslate();
const dispatch = useDispatch();
const [limitInstances, toggleLimit] = useToggle(true);
const [, gmaViewAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
@ -249,7 +251,7 @@ function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
</section>
)}
{/* loading moved here to avoid twitching */}
{renderLoading && <LoadingPlaceholder text="Loading..." />}
{renderLoading && <LoadingPlaceholder text={t('alertlist.unified-alert-list.text-loading', 'Loading...')} />}
</ScrollContainer>
);
}
@ -450,12 +452,17 @@ export const getStyles = (theme: GrafanaTheme2) => ({
});
export function UnifiedAlertListPanel(props: PanelProps<UnifiedAlertListOptions>) {
const { t } = useTranslate();
const [, gmaReadAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
const [, externalReadAllowed] = useAlertingAbility(AlertingAction.ViewExternalAlertRule);
if (!gmaReadAllowed && !externalReadAllowed) {
return (
<Alert title="Permission required">Sorry, you do not have the required permissions to read alert rules</Alert>
<Alert title={t('alertlist.unified-alert-list-panel.title-permission-required', 'Permission required')}>
<Trans i18nKey="alertlist.unified-alert-list-panel.body-permission-required">
Sorry, you do not have the required permissions to read alert rules.
</Trans>
</Alert>
);
}

@ -1,4 +1,5 @@
import { DataSourceInstanceSettings, PanelPlugin } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Button, Stack } from '@grafana/ui';
import { NestedFolderPicker } from 'app/core/components/NestedFolderPicker/NestedFolderPicker';
import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker';
@ -127,7 +128,7 @@ const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertLi
}}
/>
<Button variant="secondary" onClick={() => props.onChange(null)}>
Clear
<Trans i18nKey="alertlist.unified-alert-list.clear">Clear</Trans>
</Button>
</Stack>
);

@ -2,6 +2,7 @@ import { css, cx } from '@emotion/css';
import { useLocation } from 'react-use';
import { GrafanaTheme2, intervalToAbbreviatedDurationString } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Icon, Stack, useStyles2 } from '@grafana/ui';
import alertDef from 'app/features/alerting/state/alertDef';
import { Spacer } from 'app/features/alerting/unified/components/Spacer';
@ -36,6 +37,7 @@ function getGrafanaInstancesTotal(totals: Partial<Record<AlertInstanceTotalState
}
const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstances, hideViewRuleLinkText }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const stateStyle = useStyles2(getStateTagStyles);
const { href: returnTo } = useLocation();
@ -93,9 +95,11 @@ const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstance
target="__blank"
className={styles.link}
rel="noopener"
aria-label="View alert rule"
aria-label={t('alertlist.ungrouped-mode-view.aria-label-view-alert-rule', 'View alert rule')}
>
<span className={cx({ [styles.hidden]: hideViewRuleLinkText })}>View alert rule</span>
<span className={cx({ [styles.hidden]: hideViewRuleLinkText })}>
<Trans i18nKey="alertlist.ungrouped-mode-view.view-alert-rule">View alert rule</Trans>
</span>
<Icon name={'external-link-alt'} size="sm" />
</a>
)}
@ -105,15 +109,14 @@ const UngroupedModeView = ({ rules, options, handleInstancesLimit, limitInstance
{alertStateToReadable(alertingRule.state)}
</span>{' '}
{firstActiveAt && alertingRule.state !== PromAlertingRuleState.Inactive && (
<>
for{' '}
<span>
{intervalToAbbreviatedDurationString({
start: firstActiveAt,
end: Date.now(),
})}
</span>
</>
<Trans
i18nKey="alertlist.ungrouped-mode-view.active-for"
values={{
duration: intervalToAbbreviatedDurationString({ start: firstActiveAt, end: Date.now() }),
}}
>
for <span>{'{{duration}}'}</span>
</Trans>
)}
</div>
</div>

@ -12,6 +12,8 @@ import {
locationUtil,
PanelProps,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { config, getBackendSrv, locationService } from '@grafana/runtime';
import { Button, ScrollContainer, stylesFactory, TagList } from '@grafana/ui';
import { AbstractList } from '@grafana/ui/internal';
@ -247,7 +249,11 @@ export class AnnoListPanel extends PureComponent<Props, State> {
render() {
const { loaded, annotations, queryUser, queryTags } = this.state;
if (!loaded) {
return <div>loading...</div>;
return (
<div>
<Trans i18nKey="annolist.anno-list-panel.loading">Loading...</Trans>
</div>
);
}
// Previously we showed inidication that it covered all time
@ -262,14 +268,20 @@ export class AnnoListPanel extends PureComponent<Props, State> {
<ScrollContainer minHeight="100%">
{hasFilter && (
<div className={this.style.filter}>
<b>Filter:</b>
<b>
<Trans i18nKey="annolist.anno-list-panel.filter">Filter:</Trans>
</b>
{queryUser && (
<Button
size="sm"
variant="secondary"
fill="text"
onClick={this.onClearUser}
aria-label={`Remove filter: ${queryUser.email}`}
aria-label={t(
'annolist.anno-list-panel.aria-label-remove-filter',
'Remove filter: {{filterToRemove}}',
{ filterToRemove: queryUser.email }
)}
>
{queryUser.email}
</Button>
@ -287,7 +299,11 @@ export class AnnoListPanel extends PureComponent<Props, State> {
</div>
)}
{annotations.length < 1 && <div className={this.style.noneFound}>No Annotations Found</div>}
{annotations.length < 1 && (
<div className={this.style.noneFound}>
<Trans i18nKey="annolist.anno-list-panel.no-annotations-found">No annotations found</Trans>
</div>
)}
<AbstractList items={annotations} renderItem={this.renderItem} getItemKey={(item) => `${item.id}`} />
</ScrollContainer>

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { MouseEvent } from 'react';
import { AnnotationEvent, DateTimeInput, GrafanaTheme2, PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { Card, TagList, Tooltip, RenderUserContentAsHTML, useStyles2 } from '@grafana/ui';
import { Options } from './panelcfg.gen';
@ -79,8 +80,10 @@ const Avatar = ({ onClick, avatarUrl, login, email }: AvatarProps) => {
};
const tooltipContent = (
<span>
Created by:
<br /> {email}
<Trans i18nKey="annolist.annotation-list-item.tooltip-created-by">
Created by:
<br /> {{ email }}
</Trans>
</span>
);

@ -1,4 +1,5 @@
import { SelectableValue, StandardEditorProps } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Checkbox, HorizontalGroup, RadioButtonGroup, Tooltip } from '@grafana/ui';
const GAPS_OPTIONS: Array<SelectableValue<number>> = [
@ -25,6 +26,7 @@ const GAPS_OPTIONS: Array<SelectableValue<number>> = [
];
export const TickSpacingEditor = (props: StandardEditorProps<number>) => {
const { t } = useTranslate();
let value = props.value ?? 0;
const isRTL = value < 0;
if (isRTL) {
@ -50,9 +52,15 @@ export const TickSpacingEditor = (props: StandardEditorProps<number>) => {
<HorizontalGroup>
<RadioButtonGroup value={gap.value} options={GAPS_OPTIONS} onChange={onSpacingChange} />
{value !== 0 && (
<Tooltip content="Require space from the right side" placement="top">
<Tooltip
content={t(
'barchart.tick-spacing-editor.content-require-space-from-the-right-side',
'Require space from the right side'
)}
placement="top"
>
<div>
<Checkbox value={isRTL} onChange={onRTLChange} label="RTL" />
<Checkbox value={isRTL} onChange={onRTLChange} label={t('barchart.tick-spacing-editor.label-rtl', 'RTL')} />
</div>
</Tooltip>
)}

@ -4,6 +4,7 @@ import * as React from 'react';
import { first } from 'rxjs/operators';
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ContextMenu, MenuItem, MenuItemProps } from '@grafana/ui';
import { ElementState } from 'app/features/canvas/runtime/element';
import { FrameState } from 'app/features/canvas/runtime/frame';
@ -21,6 +22,7 @@ type Props = {
};
export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) => {
const { t } = useTranslate();
const inlineEditorOpen = panel.state.openInlineEdit;
const [isMenuVisible, setIsMenuVisible] = useState<boolean>(false);
const [anchorPoint, setAnchorPoint] = useState<AnchorPoint>({ x: 0, y: 0 });
@ -76,7 +78,11 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
// This is disabled when panel is in edit mode because opening inline editor over panel editor is not ideal UX
const openCloseEditorMenuItem = !scene.isPanelEditing && (
<MenuItem
label={inlineEditorOpen ? 'Close Editor' : 'Open Editor'}
label={
inlineEditorOpen
? t('canvas.canvas-context-menu.close-editor', 'Close Editor')
: t('canvas.canvas-context-menu.open-editor', 'Open Editor')
}
onClick={() => {
if (scene.inlineEditingCallback) {
if (inlineEditorOpen) {
@ -102,7 +108,11 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
return (
element &&
element.item.hasEditMode && (
<MenuItem label="Edit" onClick={onClickEditElementMenuItem} className={styles.menuItem} />
<MenuItem
label={t('canvas.canvas-context-menu.render-menu-items.edit-element-menu-item.label-edit', 'Edit')}
onClick={onClickEditElementMenuItem}
className={styles.menuItem}
/>
)
);
}
@ -144,7 +154,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
const addItemMenuItem = (
<MenuItem
label="Add item"
label={t('canvas.canvas-context-menu.render-menu-items.add-item-menu-item.label-add-item', 'Add item')}
className={styles.menuItem}
childItems={getTypeOptionsSubmenu()}
customSubMenuContainerStyles={{ maxHeight: '150px', overflowY: 'auto' }}
@ -153,7 +163,10 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
const setBackgroundMenuItem = (
<MenuItem
label={'Set background'}
label={t(
'canvas.canvas-context-menu.render-menu-items.set-background-menu-item.label-set-background',
'Set background'
)}
onClick={() => {
if (scene.setBackgroundCallback) {
scene.setBackgroundCallback(anchorPoint);
@ -169,7 +182,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
<>
{editElementMenuItem()}
<MenuItem
label="Delete"
label={t('canvas.canvas-context-menu.render-menu-items.label-delete', 'Delete')}
onClick={() => {
contextMenuAction(LayerActionID.Delete);
closeContextMenu();
@ -177,7 +190,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
className={styles.menuItem}
/>
<MenuItem
label="Duplicate"
label={t('canvas.canvas-context-menu.render-menu-items.label-duplicate', 'Duplicate')}
onClick={() => {
contextMenuAction(LayerActionID.Duplicate);
closeContextMenu();
@ -185,7 +198,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
className={styles.menuItem}
/>
<MenuItem
label="Bring to front"
label={t('canvas.canvas-context-menu.render-menu-items.label-bring-to-front', 'Bring to front')}
onClick={() => {
contextMenuAction(LayerActionID.MoveTop);
closeContextMenu();
@ -193,7 +206,7 @@ export const CanvasContextMenu = ({ scene, panel, onVisibilityChange }: Props) =
className={styles.menuItem}
/>
<MenuItem
label="Send to back"
label={t('canvas.canvas-context-menu.render-menu-items.label-send-to-back', 'Send to back')}
onClick={() => {
contextMenuAction(LayerActionID.MoveBottom);
closeContextMenu();

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue, StandardEditorProps } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Field, RadioButtonGroup, Switch } from '@grafana/ui';
import { LineStyle } from '../types';
@ -24,6 +25,7 @@ export const defaultLineStyleConfig: LineStyleConfig = {
};
export const LineStyleEditor = ({ value, onChange }: Props) => {
const { t } = useTranslate();
if (!value) {
value = defaultLineStyleConfig;
} else if (typeof value !== 'object') {
@ -53,7 +55,7 @@ export const LineStyleEditor = ({ value, onChange }: Props) => {
{value.style !== LineStyle.Solid && (
<>
<br />
<Field label="Animate">
<Field label={t('canvas.line-style-editor.label-animate', 'Animate')}>
<Switch value={value.animate} onChange={(e) => onAnimateChange(e.currentTarget.checked)} />
</Field>
</>

@ -6,6 +6,7 @@ import {
StringFieldConfigSettings,
SelectableValue,
} from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Button, Field, InlineField, InlineFieldRow, JSONFormatter, RadioButtonGroup, Select } from '@grafana/ui';
import { StringValueEditor } from 'app/core/components/OptionsUI/string';
import { defaultApiConfig } from 'app/features/canvas/elements/button';
@ -50,6 +51,7 @@ const contentTypeOptions: SelectableValue[] = [
];
export function APIEditor({ value, context, onChange }: Props) {
const { t } = useTranslate();
const LABEL_WIDTH = 13;
if (!value) {
@ -136,8 +138,11 @@ export function APIEditor({ value, context, onChange }: Props) {
const renderTestAPIButton = (api: APIEditorConfig) => {
if (api && api.endpoint) {
return (
<Button onClick={() => callApi(api)} title="Test API">
Test API
<Button
onClick={() => callApi(api)}
title={t('canvas.apieditor.render-test-apibutton.title-test-api', 'Test API')}
>
<Trans i18nKey="canvas.apieditor.render-test-apibutton.test-api">Test API</Trans>
</Button>
);
}
@ -148,7 +153,7 @@ export function APIEditor({ value, context, onChange }: Props) {
return (
<>
<InlineFieldRow>
<InlineField label="Endpoint" labelWidth={LABEL_WIDTH} grow={true}>
<InlineField label={t('canvas.apieditor.label-endpoint', 'Endpoint')} labelWidth={LABEL_WIDTH} grow={true}>
<StringValueEditor
context={context}
value={value?.endpoint}
@ -158,13 +163,18 @@ export function APIEditor({ value, context, onChange }: Props) {
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Method" labelWidth={LABEL_WIDTH} grow={true}>
<InlineField label={t('canvas.apieditor.label-method', 'Method')} labelWidth={LABEL_WIDTH} grow={true}>
<RadioButtonGroup value={value?.method} options={httpMethodOptions} onChange={onMethodChange} fullWidth />
</InlineField>
</InlineFieldRow>
{value?.method !== HttpRequestMethod.GET && (
<InlineFieldRow>
<InlineField label="Content-Type" labelWidth={LABEL_WIDTH} grow={true}>
<InlineField
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
label="Content-Type"
labelWidth={LABEL_WIDTH}
grow={true}
>
<Select
options={contentTypeOptions}
allowCustomValue={true}
@ -177,14 +187,14 @@ export function APIEditor({ value, context, onChange }: Props) {
)}
<br />
<Field label="Query parameters">
<Field label={t('canvas.apieditor.label-query-parameters', 'Query parameters')}>
<ParamsEditor value={value?.queryParams ?? []} onChange={onQueryParamsChange} />
</Field>
<Field label="Header parameters">
<Field label={t('canvas.apieditor.label-header-parameters', 'Header parameters')}>
<ParamsEditor value={value?.headerParams ?? []} onChange={onHeaderParamsChange} />
</Field>
{value?.method !== HttpRequestMethod.GET && value?.contentType && (
<Field label="Payload">
<Field label={t('canvas.apieditor.label-payload', 'Payload')}>
<StringValueEditor
context={context}
value={value?.data ?? '{}'}

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { SelectableValue, StandardEditorProps } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ButtonVariant, InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { defaultStyleConfig } from 'app/features/canvas/elements/button';
@ -18,6 +19,7 @@ const variantOptions: SelectableValue[] = [
];
export const ButtonStyleEditor = ({ value, onChange }: Props) => {
const { t } = useTranslate();
if (!value) {
value = defaultStyleConfig;
}
@ -35,7 +37,7 @@ export const ButtonStyleEditor = ({ value, onChange }: Props) => {
return (
<>
<InlineFieldRow>
<InlineField label="Variant" grow={true}>
<InlineField label={t('canvas.button-style-editor.label-variant', 'Variant')} grow={true}>
<Select options={variantOptions} value={value?.variant} onChange={onVariantChange} />
</InlineField>
</InlineFieldRow>

@ -1,6 +1,7 @@
import { useState } from 'react';
import * as React from 'react';
import { useTranslate } from '@grafana/i18n';
import { IconButton, Input, Stack } from '@grafana/ui';
interface Props {
@ -9,6 +10,7 @@ interface Props {
}
export const ParamsEditor = ({ value, onChange }: Props) => {
const { t } = useTranslate();
const [paramName, setParamName] = useState('');
const [paramValue, setParamValue] = useState('');
@ -46,16 +48,33 @@ export const ParamsEditor = ({ value, onChange }: Props) => {
return (
<div>
<Stack direction="row">
<Input placeholder="Key" value={paramName} onChange={changeParamName} />
<Input placeholder="Value" value={paramValue} onChange={changeParamValue} />
<IconButton aria-label="add" name="plus-circle" onClick={addParam} disabled={isAddParamsDisabled} />
<Input
placeholder={t('canvas.params-editor.placeholder-key', 'Key')}
value={paramName}
onChange={changeParamName}
/>
<Input
placeholder={t('canvas.params-editor.placeholder-value', 'Value')}
value={paramValue}
onChange={changeParamValue}
/>
<IconButton
aria-label={t('canvas.params-editor.aria-label-add', 'Add')}
name="plus-circle"
onClick={addParam}
disabled={isAddParamsDisabled}
/>
</Stack>
<Stack direction="column">
{Array.from(value || []).map((entry) => (
<Stack key={entry[0]} direction="row">
<Input disabled value={entry[0]} />
<Input disabled value={entry[1]} />
<IconButton aria-label="delete" onClick={removeParam(entry[0])} name="trash-alt" />
<IconButton
aria-label={t('canvas.params-editor.aria-label-delete', 'Delete')}
onClick={removeParam(entry[0])}
name="trash-alt"
/>
</Stack>
))}
</Stack>

@ -2,6 +2,7 @@ import { useObservable } from 'react-use';
import { Subject } from 'rxjs';
import { SelectableValue, StandardEditorProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Field, Icon, InlineField, InlineFieldRow, Select, Stack } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
@ -32,13 +33,18 @@ const verticalOptions: Array<SelectableValue<VerticalConstraint>> = [
type Props = StandardEditorProps<unknown, CanvasEditorOptions, Options>;
export function PlacementEditor({ item }: Props) {
const { t } = useTranslate();
const settings = item.settings;
// Will force a rerender whenever the subject changes
useObservable(settings?.scene ? settings.scene.moved : new Subject());
if (!settings) {
return <div>Loading...</div>;
return (
<div>
<Trans i18nKey="canvas.placement-editor.loading">Loading...</Trans>
</div>
);
}
const element = settings.element;
@ -95,7 +101,7 @@ export function PlacementEditor({ item }: Props) {
<div>
<QuickPositioning onPositionChange={onPositionChange} settings={settings} element={element} />
<br />
<Field label="Constraints">
<Field label={t('canvas.placement-editor.label-constraints', 'Constraints')}>
<Stack direction="row">
<ConstraintSelectionBox
onVerticalConstraintChange={onVerticalConstraintChange}
@ -121,7 +127,7 @@ export function PlacementEditor({ item }: Props) {
<br />
<Field label="Position">
<Field label={t('canvas.placement-editor.label-position', 'Position')}>
<>
{places.map((p) => {
const v = placement![p];

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { IconButton, useStyles2 } from '@grafana/ui';
import { ElementState } from 'app/features/canvas/runtime/element';
import { QuickPlacement } from 'app/features/canvas/types';
@ -16,6 +17,7 @@ type Props = {
};
export const QuickPositioning = ({ onPositionChange, element, settings }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const onQuickPositioningChange = (position: QuickPlacement) => {
@ -70,41 +72,41 @@ export const QuickPositioning = ({ onPositionChange, element, settings }: Props)
onClick={() => onQuickPositioningChange(QuickPlacement.Left)}
className={styles.button}
size="lg"
tooltip="Align left"
tooltip={t('canvas.quick-positioning.tooltip-align-left', 'Align left')}
/>
<IconButton
name="horizontal-align-center"
onClick={() => onQuickPositioningChange(QuickPlacement.HorizontalCenter)}
className={styles.button}
size="lg"
tooltip="Align horizontal centers"
tooltip={t('canvas.quick-positioning.tooltip-align-horizontal-centers', 'Align horizontal centers')}
/>
<IconButton
name="horizontal-align-right"
onClick={() => onQuickPositioningChange(QuickPlacement.Right)}
className={styles.button}
size="lg"
tooltip="Align right"
tooltip={t('canvas.quick-positioning.tooltip-align-right', 'Align right')}
/>
<IconButton
name="vertical-align-top"
onClick={() => onQuickPositioningChange(QuickPlacement.Top)}
size="lg"
tooltip="Align top"
tooltip={t('canvas.quick-positioning.tooltip-align-top', 'Align top')}
/>
<IconButton
name="vertical-align-center"
onClick={() => onQuickPositioningChange(QuickPlacement.VerticalCenter)}
className={styles.button}
size="lg"
tooltip="Align vertical centers"
tooltip={t('canvas.quick-positioning.tooltip-align-vertical-centers', 'Align vertical centers')}
/>
<IconButton
name="vertical-align-bottom"
onClick={() => onQuickPositioningChange(QuickPlacement.Bottom)}
className={styles.button}
size="lg"
tooltip="Align bottom"
tooltip={t('canvas.quick-positioning.tooltip-align-bottom', 'Align bottom')}
/>
</div>
);

@ -4,6 +4,7 @@ import Draggable, { DraggableEventHandler } from 'react-draggable';
import { Resizable, ResizeCallbackData } from 'react-resizable';
import { Dimensions2D, GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { IconButton, Portal, useStyles2 } from '@grafana/ui';
import store from 'app/core/store';
import { Scene } from 'app/features/canvas/runtime/scene';
@ -20,6 +21,7 @@ const OFFSET_X = 10;
const OFFSET_Y = 32;
export function InlineEdit({ onClose, id, scene }: Props) {
const { t } = useTranslate();
const root = scene.root.div?.getBoundingClientRect();
const windowHeight = window.innerHeight;
const windowWidth = window.innerWidth;
@ -83,13 +85,15 @@ export function InlineEdit({ onClose, id, scene }: Props) {
>
<strong className={styles.inlineEditorHeader}>
<div className={styles.placeholder} />
<div>Canvas Inline Editor</div>
<div>
<Trans i18nKey="canvas.inline-edit.canvas-inline-editor">Canvas Inline Editor</Trans>
</div>
<IconButton
name="times"
size="xl"
className={styles.inlineEditorClose}
onClick={onClose}
tooltip="Close inline editor"
tooltip={t('canvas.inline-edit.tooltip-close-inline-editor', 'Close inline editor')}
/>
</strong>
<div className={styles.inlineEditorContentWrapper}>

@ -5,6 +5,7 @@ import { useObservable } from 'react-use';
import { DataFrame, GrafanaTheme2, PanelOptionsEditorBuilder, StandardEditorContext } from '@grafana/data';
import { NestedValueAccess, PanelOptionsSupplier } from '@grafana/data/internal';
import { useTranslate, Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
import { FrameState } from 'app/features/canvas/runtime/frame';
@ -25,6 +26,7 @@ import { getLayerEditor } from '../layer/layerEditor';
import { TabsEditor } from './TabsEditor';
export function InlineEditBody() {
const { t } = useTranslate();
const activePanel = useObservable(activePanelSubject);
const instanceState = activePanel?.panel.context?.instanceState;
const styles = useStyles2(getStyles);
@ -103,12 +105,20 @@ export function InlineEditBody() {
<>
<div style={topLevelItemsContainerStyle}>{pane.items.map((item) => item.render())}</div>
<div style={topLevelItemsContainerStyle}>
<AddLayerButton onChange={(sel) => onAddItem(sel, rootLayer)} options={typeOptions} label={'Add item'} />
<AddLayerButton
onChange={(sel) => onAddItem(sel, rootLayer)}
options={typeOptions}
label={t('canvas.inline-edit-body.label-add-item', 'Add item')}
/>
</div>
<div style={topLevelItemsContainerStyle}>
<TabsEditor onTabChange={onTabChange} />
{pane.categories.map((p) => renderOptionsPaneCategoryDescriptor(p))}
{noElementSelected && <div className={styles.selectElement}>Please select an element</div>}
{noElementSelected && (
<div className={styles.selectElement}>
<Trans i18nKey="canvas.inline-edit-body.please-select-an-element">Please select an element</Trans>
</div>
)}
</div>
</>
);

@ -4,6 +4,7 @@ import Tree, { TreeNodeProps } from 'rc-tree';
import { Key, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { Button, Icon, Stack, useStyles2, useTheme2 } from '@grafana/ui';
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
@ -22,6 +23,7 @@ import { getTreeData, onNodeDrop, TreeElement } from './tree';
let allowSelection = true;
export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, TreeViewEditorProps, Options>) => {
const { t } = useTranslate();
const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
@ -50,12 +52,20 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
}, [item?.settings?.scene.root, selectedBgColor, selection, selectionByUID]);
if (!settings) {
return <div>No settings</div>;
return (
<div>
<Trans i18nKey="canvas.tree-navigation-editor.no-settings">No settings</Trans>
</div>
);
}
const layer = settings.layer;
if (!layer) {
return <div>Missing layer?</div>;
return (
<div>
<Trans i18nKey="canvas.tree-navigation-editor.missing-layer">Missing layer?</Trans>
</div>
);
}
const onSelect = (selectedKeys: Key[], info: { node: { dataRef: ElementState } }) => {
@ -95,7 +105,7 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
return (
<Icon
name="angle-right"
title={'Node Icon'}
title={t('canvas.tree-navigation-editor.switcher-icon.title-node-icon', 'Node Icon')}
style={{
transform: `rotate(${obj.expanded ? 90 : 0}deg)`,
fill: theme.colors.text.primary,
@ -150,16 +160,20 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
<Stack justifyContent="space-between" direction="row">
<div className={styles.addLayerButton}>
<AddLayerButton onChange={(sel) => onAddItem(sel, layer)} options={typeOptions} label={'Add item'} />
<AddLayerButton
onChange={(sel) => onAddItem(sel, layer)}
options={typeOptions}
label={t('canvas.tree-navigation-editor.label-add-item', 'Add item')}
/>
</div>
{selection.length > 0 && (
<Button size="sm" variant="secondary" onClick={onClearSelection}>
Clear selection
<Trans i18nKey="canvas.tree-navigation-editor.clear-selection">Clear selection</Trans>
</Button>
)}
{selection.length > 1 && config.featureToggles.canvasPanelNesting && (
<Button size="sm" variant="secondary" onClick={onFrameSelection}>
Frame selection
<Trans i18nKey="canvas.tree-navigation-editor.frame-selection">Frame selection</Trans>
</Button>
)}
</Stack>

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { IconButton, useStyles2 } from '@grafana/ui';
import { LayerName } from 'app/core/components/Layers/LayerName';
import { ElementState } from 'app/features/canvas/runtime/element';
@ -17,6 +18,7 @@ interface Props {
}
export const TreeNodeTitle = ({ settings, nodeData, setAllowSelection }: Props) => {
const { t } = useTranslate();
const element = nodeData.dataRef;
const name = nodeData.dataRef.getName();
@ -72,17 +74,17 @@ export const TreeNodeTitle = ({ settings, nodeData, setAllowSelection }: Props)
<div className={styles.actionButtonsWrapper}>
<IconButton
name="copy"
title="Duplicate"
title={t('canvas.tree-node-title.title-duplicate', 'Duplicate')}
className={styles.actionIcon}
onClick={() => onDuplicate(element)}
tooltip="Duplicate"
tooltip={t('canvas.tree-node-title.tooltip-duplicate', 'Duplicate')}
/>
<IconButton
name="trash-alt"
title="remove"
title={t('canvas.tree-node-title.title-remove', 'Remove')}
className={styles.actionIcon}
onClick={() => onDelete(element)}
tooltip="Remove"
tooltip={t('canvas.tree-node-title.tooltip-remove', 'Remove')}
/>
</div>
)}

@ -1,18 +1,20 @@
import { css } from '@emotion/css';
import { StandardEditorProps, GrafanaTheme2 } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Alert, Icon, Stack, useStyles2 } from '@grafana/ui';
const helpUrl = 'https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/canvas/';
export const PanZoomHelp = ({}: StandardEditorProps<string, unknown, unknown, unknown>) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
return (
<>
<Stack direction="row">
<Alert
title="Pan and zoom controls"
title={t('canvas.pan-zoom-help.title-pan-and-zoom-controls', 'Pan and zoom controls')}
severity="info"
buttonContent={<Icon name="question-circle" size="xl" />}
className={styles.alert}
@ -26,14 +28,22 @@ export const PanZoomHelp = ({}: StandardEditorProps<string, unknown, unknown, un
<Stack direction="column">
<ul>
<li>
Pan:
<Trans i18nKey="canvas.pan-zoom-help.pan-title">Pan:</Trans>
<ul>
<li>Middle mouse</li>
<li>CTRL + right mouse</li>
<li>
<Trans i18nKey="canvas.pan-zoom-help.middle-mouse">Middle mouse</Trans>
</li>
<li>
<Trans i18nKey="canvas.pan-zoom-help.ctrl-right-mouse">CTRL + right mouse</Trans>
</li>
</ul>
</li>
<li>Zoom: Scroll wheel</li>
<li>Reset: Double click</li>
<li>
<Trans i18nKey="canvas.pan-zoom-help.zoom-scroll-wheel">Zoom: Scroll wheel</Trans>
</li>
<li>
<Trans i18nKey="canvas.pan-zoom-help.reset-double-click">Reset: Double click</Trans>
</li>
</ul>
</Stack>
</Alert>

@ -1,6 +1,8 @@
import { useState } from 'react';
import * as React from 'react';
import { useTranslate } from '@grafana/i18n';
import { SimpleInput } from './SimpleInput';
interface AddColumnProps {
@ -9,6 +11,7 @@ interface AddColumnProps {
}
export const AddColumn = ({ divStyle, onColumnInputBlur }: AddColumnProps) => {
const { t } = useTranslate();
const [showInput, setShowInput] = useState<boolean>(false);
const setupColumnInput = () => {
@ -27,7 +30,7 @@ export const AddColumn = ({ divStyle, onColumnInputBlur }: AddColumnProps) => {
return (
<div className={divStyle}>
{showInput ? (
<SimpleInput placeholder="Column Name" onBlur={onBlur} />
<SimpleInput placeholder={t('datagrid.add-column.placeholder-column-name', 'Column name')} onBlur={onBlur} />
) : (
<button onClick={setupColumnInput}>+</button>
)}

@ -4,6 +4,7 @@ import * as React from 'react';
import { DataFrame, FieldType } from '@grafana/data';
import { convertFieldType } from '@grafana/data/internal';
import { useTranslate } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { ContextMenu, MenuGroup, MenuItem } from '@grafana/ui';
import { MenuDivider } from '@grafana/ui/internal';
@ -39,6 +40,7 @@ export const DatagridContextMenu = ({
columnFreezeIndex,
renameColumnClicked,
}: ContextMenuProps) => {
const { t } = useTranslate();
let selectedRows: number[] = [];
let selectedColumns: number[] = [];
const { row, column, x, y, isHeaderMenu } = menuData;
@ -118,7 +120,7 @@ export const DatagridContextMenu = ({
{showDeleteColumn || showDeleteRow ? <MenuDivider /> : null}
{showClearRow ? (
<MenuItem
label="Clear row"
label={t('datagrid.datagrid-context-menu.render-context-menu-items.label-clear-row', 'Clear row')}
onClick={() => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONTEXT_MENU_ACTION,
@ -130,7 +132,7 @@ export const DatagridContextMenu = ({
) : null}
{showClearColumn ? (
<MenuItem
label="Clear column"
label={t('datagrid.datagrid-context-menu.render-context-menu-items.label-clear-column', 'Clear column')}
onClick={() => {
const field = data.fields[column];
field.values = field.values.map(() => null);
@ -146,7 +148,7 @@ export const DatagridContextMenu = ({
) : null}
{showClearRow || showClearColumn ? <MenuDivider /> : null}
<MenuItem
label="Remove all data"
label={t('datagrid.datagrid-context-menu.render-context-menu-items.label-remove-all-data', 'Remove all data')}
onClick={() => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONTEXT_MENU_ACTION,
@ -156,7 +158,7 @@ export const DatagridContextMenu = ({
}}
/>
<MenuItem
label="Search..."
label={t('datagrid.datagrid-context-menu.render-context-menu-items.label-search', 'Search...')}
onClick={() => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.CONTEXT_MENU_ACTION,
@ -216,7 +218,9 @@ export const DatagridContextMenu = ({
return (
<>
{fieldTypeConversionData.length ? (
<MenuGroup label="Set field type">
<MenuGroup
label={t('datagrid.datagrid-context-menu.render-header-menu-items.label-set-field-type', 'Set field type')}
>
{fieldTypeConversionData.map((conversionData, index) => (
<MenuItem
key={index}
@ -259,10 +263,13 @@ export const DatagridContextMenu = ({
}
}}
/>
<MenuItem label="Rename column" onClick={renameColumnClicked} />
<MenuItem
label={t('datagrid.datagrid-context-menu.render-header-menu-items.label-rename-column', 'Rename column')}
onClick={renameColumnClicked}
/>
<MenuDivider />
<MenuItem
label="Delete column"
label={t('datagrid.datagrid-context-menu.render-header-menu-items.label-delete-column', 'Delete column')}
onClick={() => {
reportInteraction(INTERACTION_EVENT_NAME, {
item: INTERACTION_ITEM.HEADER_MENU_ACTION,
@ -278,7 +285,7 @@ export const DatagridContextMenu = ({
}}
/>
<MenuItem
label="Clear column"
label={t('datagrid.datagrid-context-menu.render-header-menu-items.label-clear-column', 'Clear column')}
onClick={() => {
const field = data.fields[column];
field.values = field.values.map(() => null);

@ -9,6 +9,7 @@ import {
DataHoverClearEvent,
BusEventBase,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { CustomScrollbar } from '@grafana/ui';
import { DataHoverView } from 'app/features/visualization/data-hover/DataHoverView';
@ -58,13 +59,19 @@ export class CursorView extends Component<Props, State> {
render() {
const { event } = this.state;
if (!event) {
return <div>no events yet</div>;
return (
<div>
<Trans i18nKey="debug.cursor-view.no-events-yet">No events yet</Trans>
</div>
);
}
const { type, payload, origin } = event;
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<h3>Origin: {(origin as any)?.path}</h3>
<span>Type: {type}</span>
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
<h3>event.origin: {(origin as any)?.path}</h3>
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
<span>event.type: {type}</span>
{Boolean(payload) && (
<>
<pre>{JSON.stringify(payload.point, null, ' ')}</pre>

@ -67,6 +67,7 @@ export class EventBusLoggerPanel extends PureComponent<Props, State> {
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
{this.history.map((v, idx) => (
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
<div key={v.key}>
{JSON.stringify(v.path)} {v.type} / X:{JSON.stringify(v.payload.x)} / Y:{JSON.stringify(v.payload.y)}
</div>

@ -9,6 +9,8 @@ import {
PanelProps,
ReducerID,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { IconButton } from '@grafana/ui';
import { Options, UpdateConfig } from './panelcfg.gen';
@ -74,12 +76,43 @@ export class RenderInfoViewer extends Component<Props> {
return (
<div>
<div>
<IconButton name="step-backward" title="reset counters" onClick={this.resetCounters} tooltip="Step back" />
<IconButton
name="step-backward"
title={t('debug.render-info-viewer.title-reset-counters', 'Reset counters')}
onClick={this.resetCounters}
tooltip={t('debug.render-info-viewer.tooltip-step-back', 'Step back')}
/>
<span>
{showCounters.render && <span>Render: {this.counters.render}&nbsp;</span>}
{showCounters.dataChanged && <span>Data: {this.counters.dataChanged}&nbsp;</span>}
{showCounters.schemaChanged && <span>Schema: {this.counters.schemaChanged}&nbsp;</span>}
<span>TIME: {elapsed}ms</span>
{showCounters.render && (
<span>
<Trans i18nKey="debug.render-info-viewer.render-counter" values={{ numRenders: this.counters.render }}>
Render: {'{{numRenders}}'}&nbsp;
</Trans>
</span>
)}
{showCounters.dataChanged && (
<span>
<Trans
i18nKey="debug.render-info-viewer.data-counter"
values={{ numDataChanges: this.counters.dataChanged }}
>
Data: {'{{numDataChanges}}'}&nbsp;
</Trans>
</span>
)}
{showCounters.schemaChanged && (
<span>
<Trans
i18nKey="debug.render-info-viewer.schema-counter"
values={{ numSchemaChanges: this.counters.schemaChanged }}
>
Schema: {'{{numSchemaChanges}}'}&nbsp;
</Trans>
</span>
)}
<span>
<Trans i18nKey="debug.render-info-viewer.elapsed-time">Time: {{ elapsed }}ms</Trans>
</span>
</span>
</div>
@ -92,9 +125,15 @@ export class RenderInfoViewer extends Component<Props> {
<table className="filter-table">
<thead>
<tr>
<td>Field</td>
<td>Type</td>
<td>Last</td>
<td>
<Trans i18nKey="debug.render-info-viewer.field">Field</Trans>
</td>
<td>
<Trans i18nKey="debug.render-info-viewer.type">Type</Trans>
</td>
<td>
<Trans i18nKey="debug.render-info-viewer.last">Last</Trans>
</td>
</tr>
</thead>
<tbody>

@ -1,11 +1,13 @@
import { FormEvent } from 'react';
import { PanelOptionsEditorProps, PanelProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Field, Input, usePanelContext } from '@grafana/ui';
import { Options } from './panelcfg.gen';
export function StateView(props: PanelProps<Options>) {
const { t } = useTranslate();
const context = usePanelContext();
const onChangeName = (e: FormEvent<HTMLInputElement>) => {
@ -16,7 +18,7 @@ export function StateView(props: PanelProps<Options>) {
return (
<>
<Field label="State name">
<Field label={t('debug.state-view.label-state-name', 'State name')}>
<Input value={context.instanceState?.name ?? ''} onChange={onChangeName} />
</Field>
</>
@ -24,5 +26,11 @@ export function StateView(props: PanelProps<Options>) {
}
export function StateViewEditor({ value, context, onChange, item }: PanelOptionsEditorProps<string>) {
return <div>Current value: {context.instanceState?.name} </div>;
return (
<div>
<Trans i18nKey="debug.state-view.current-value" values={{ currentValue: context.instanceState?.name }}>
Current value: {'{{currentValue}}'}{' '}
</Trans>
</div>
);
}

@ -13,6 +13,7 @@ import * as React from 'react';
import { Subscription } from 'rxjs';
import { DataHoverEvent, PanelData, PanelProps } from '@grafana/data';
import { t } from '@grafana/i18n/internal';
import { config } from '@grafana/runtime';
import { PanelContext, PanelContextRoot } from '@grafana/ui';
import { appEvents } from 'app/core/app_events';
@ -423,7 +424,7 @@ export class GeomapPanel extends Component<Props, State> {
className={styles.map}
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0} // Interactivity is added through the ref
aria-label={`Navigable map`}
aria-label={t('geomap.geomap-panel.aria-label-map', 'Navigable map')}
ref={this.initMapRef}
></div>
<GeomapOverlay

@ -7,6 +7,7 @@ import tinycolor from 'tinycolor2';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { Trans } from '@grafana/i18n';
import { config } from 'app/core/config';
interface Props {
@ -47,11 +48,15 @@ export class DebugOverlay extends PureComponent<Props, State> {
<table>
<tbody>
<tr>
<th>Zoom:</th>
<th>
<Trans i18nKey="geomap.debug-overlay.zoom">Zoom:</Trans>
</th>
<td>{zoom?.toFixed(1)}</td>
</tr>
<tr>
<th>Center:&nbsp;</th>
<th>
<Trans i18nKey="geomap.debug-overlay.center">Center:</Trans>&nbsp;
</th>
<td>
{center[0].toFixed(5)}, {center[1].toFixed(5)}
</td>

@ -11,6 +11,7 @@ import {
getFieldColorModeForField,
GrafanaTheme2,
} from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { useStyles2, VizLegendItem } from '@grafana/ui';
import { ColorScale } from 'app/core/components/ColorScale/ColorScale';
import { SanitizedSVG } from 'app/core/components/SVG/SanitizedSVG';
@ -29,6 +30,7 @@ export interface MarkersLegendProps {
}
export function MarkersLegend(props: MarkersLegendProps) {
const { t } = useTranslate();
const { layerName, styleConfig, layer } = props;
const style = useStyles2(getStyles);
@ -66,7 +68,7 @@ export function MarkersLegend(props: MarkersLegendProps) {
<SanitizedSVG
src={`public/${symbol}`}
className={style.legendSymbol}
title={'Symbol'}
title={t('geomap.markers-legend.title-symbol', 'Symbol')}
style={{ fill: color, opacity: opacity }}
/>
</div>

@ -3,6 +3,8 @@ import Map from 'ol/Map';
import { useMemo, useRef, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useTranslate } from '@grafana/i18n';
import { Button, IconButton, RadioButtonGroup, Select } from '@grafana/ui';
import { config } from 'app/core/config';
@ -16,6 +18,7 @@ type Props = {
};
export const MeasureOverlay = ({ map, menuActiveState }: Props) => {
const { t } = useTranslate();
const vector = useRef(new MeasureVectorLayer());
const measureStyle = getStyles(config.theme2);
@ -99,8 +102,9 @@ export const MeasureOverlay = ({ map, menuActiveState }: Props) => {
) : (
<IconButton
className={measureStyle.icon}
data-testid={selectors.components.PanelEditor.measureButton}
name="ruler-combined"
tooltip="show measure tools"
tooltip={t('geomap.measure-overlay.tooltip-show-measure-tools', 'Show measure tools')}
tooltipPlacement="left"
onClick={toggleMenu}
/>

@ -1,3 +1,4 @@
import { useTranslate } from '@grafana/i18n';
import { InlineFieldRow, InlineField } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
@ -10,6 +11,7 @@ type Props = {
};
export const CoordinatesMapViewEditor = ({ labelWidth, value, onChange }: Props) => {
const { t } = useTranslate();
const onLatitudeChange = (latitude: number | undefined) => {
onChange({ ...value, lat: latitude });
};
@ -21,12 +23,20 @@ export const CoordinatesMapViewEditor = ({ labelWidth, value, onChange }: Props)
return (
<>
<InlineFieldRow>
<InlineField label="Latitude" labelWidth={labelWidth} grow={true}>
<InlineField
label={t('geomap.coordinates-map-view-editor.label-latitude', 'Latitude')}
labelWidth={labelWidth}
grow={true}
>
<NumberInput value={value.lat} min={-90} max={90} step={0.001} onChange={onLatitudeChange} />
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Longitude" labelWidth={labelWidth} grow={true}>
<InlineField
label={t('geomap.coordinates-map-view-editor.label-longitude', 'Longitude')}
labelWidth={labelWidth}
grow={true}
>
<NumberInput value={value.lon} min={-180} max={180} step={0.001} onChange={onLongitudeChange} />
</InlineField>
</InlineFieldRow>

@ -1,6 +1,7 @@
import { useCallback, useMemo } from 'react';
import { SelectableValue, StandardEditorContext } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { InlineFieldRow, InlineField, RadioButtonGroup, Select } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
@ -33,6 +34,7 @@ const DataScopeOptions: Array<SelectableValue<DataScopeValues>> = ScopeOptions.m
}));
export const FitMapViewEditor = ({ labelWidth, value, onChange, context }: Props) => {
const { t } = useTranslate();
const layers = useMemo(() => {
if (context.options?.layers) {
return context.options.layers.map((layer) => ({
@ -53,7 +55,11 @@ export const FitMapViewEditor = ({ labelWidth, value, onChange, context }: Props
const allLayersEditorFragment = (
<InlineFieldRow>
<InlineField label="Layer" labelWidth={labelWidth} grow={true}>
<InlineField
label={t('geomap.fit-map-view-editor.all-layers-editor-fragment.label-layer', 'Layer')}
labelWidth={labelWidth}
grow={true}
>
<Select options={layers} onChange={onSelectLayer} placeholder={layers[0]?.label} value={value.layer} />
</InlineField>
</InlineFieldRow>
@ -66,10 +72,13 @@ export const FitMapViewEditor = ({ labelWidth, value, onChange, context }: Props
const lastOnlyEditorFragment = (
<InlineFieldRow>
<InlineField
label="Padding"
label={t('geomap.fit-map-view-editor.last-only-editor-fragment.label-padding', 'Padding')}
labelWidth={labelWidth}
grow={true}
tooltip="sets padding in relative percent beyond data extent"
tooltip={t(
'geomap.fit-map-view-editor.last-only-editor-fragment.tooltip-padding-relative-percent-beyond-extent',
'Sets padding in relative percent beyond data extent'
)}
>
<NumberInput value={value?.padding ?? 5} min={0} step={1} onChange={onChangePadding} />
</InlineField>
@ -102,7 +111,7 @@ export const FitMapViewEditor = ({ labelWidth, value, onChange, context }: Props
return (
<>
<InlineFieldRow>
<InlineField label="Data" labelWidth={labelWidth} grow={true}>
<InlineField label={t('geomap.fit-map-view-editor.label-data', 'Data')} labelWidth={labelWidth} grow={true}>
<RadioButtonGroup
value={currentDataScope}
options={DataScopeOptions}

@ -1,11 +1,13 @@
import { useCallback } from 'react';
import { FrameMatcherID, MatcherConfig, StandardEditorProps } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { RefIDMultiPicker, RefIDPicker, stringsToRegexp } from '@grafana/ui/internal';
type Props = StandardEditorProps<MatcherConfig>;
export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
const { t } = useTranslate();
const onFilterChange = useCallback(
(v: string) => {
onChange(
@ -21,13 +23,19 @@ export const FrameSelectionEditor = ({ value, context, onChange }: Props) => {
);
return (
<RefIDPicker value={value?.options} onChange={onFilterChange} data={context.data} placeholder="Change filter" />
<RefIDPicker
value={value?.options}
onChange={onFilterChange}
data={context.data}
placeholder={t('geomap.frame-selection-editor.placeholder-change-filter', 'Change filter')}
/>
);
};
type FrameMultiSelectionEditorProps = Omit<StandardEditorProps<MatcherConfig>, 'item'>;
export const FrameMultiSelectionEditor = ({ value, context, onChange }: FrameMultiSelectionEditorProps) => {
const { t } = useTranslate();
const onFilterChange = useCallback(
(v: string[]) => {
onChange(
@ -47,7 +55,7 @@ export const FrameMultiSelectionEditor = ({ value, context, onChange }: FrameMul
value={value?.options}
onChange={onFilterChange}
data={context.data}
placeholder="Change filter"
placeholder={t('geomap.frame-multi-selection-editor.placeholder-change-filter', 'Change filter')}
/>
);
};

@ -1,6 +1,7 @@
import { useCallback } from 'react';
import { StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Button, useTheme2 } from '@grafana/ui';
import { DEFAULT_STYLE_RULE } from '../layers/data/geojsonLayer';
@ -12,6 +13,7 @@ import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
type Props = StandardEditorProps<FeatureStyleConfig[], StyleRuleEditorSettings, unknown>;
export const GeomapStyleRulesEditor = ({ value, onChange, context, item }: Props) => {
const { t } = useTranslate();
const theme = useTheme2();
const settings = item.settings;
@ -61,7 +63,13 @@ export const GeomapStyleRulesEditor = ({ value, onChange, context, item }: Props
return (
<>
{styleOptions}
<Button size="sm" icon="plus" onClick={onAddRule} variant="secondary" aria-label={'Add geomap style rule'}>
<Button
size="sm"
icon="plus"
onClick={onAddRule}
variant="secondary"
aria-label={t('geomap.geomap-style-rules-editor.aria-label-add-geomap-style-rule', 'Add geomap style rule')}
>
{'Add style rule'}
</Button>
</>

@ -1,6 +1,7 @@
import { DropResult } from '@hello-pangea/dnd';
import { StandardEditorProps } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Container } from '@grafana/ui';
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
@ -11,9 +12,14 @@ import { Options, MapLayerState, GeomapInstanceState } from '../types';
type LayersEditorProps = StandardEditorProps<unknown, unknown, Options, GeomapInstanceState>;
export const LayersEditor = (props: LayersEditorProps) => {
const { t } = useTranslate();
const { layers, selected, actions } = props.context.instanceState ?? {};
if (!layers || !actions) {
return <div>No layers?</div>;
return (
<div>
<Trans i18nKey="geomap.layers-editor.no-layers">No layers?</Trans>
</div>
);
}
const onDragEnd = (result: DropResult) => {
@ -58,7 +64,7 @@ export const LayersEditor = (props: LayersEditorProps) => {
<AddLayerButton
onChange={(v) => actions.addlayer(v.value!)}
options={getLayersOptions(false).options}
label={'Add layer'}
label={t('geomap.layers-editor.label-add-layer', 'Add layer')}
/>
</Container>
<br />

@ -2,6 +2,7 @@ import { toLonLat } from 'ol/proj';
import { useMemo, useCallback } from 'react';
import { StandardEditorProps, SelectableValue } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
@ -16,6 +17,7 @@ export const MapViewEditor = ({
onChange,
context,
}: StandardEditorProps<MapViewConfig, unknown, Options, GeomapInstanceState>) => {
const { t } = useTranslate();
const labelWidth = 10;
const views = useMemo(() => {
@ -65,7 +67,7 @@ export const MapViewEditor = ({
return (
<>
<InlineFieldRow>
<InlineField label="View" labelWidth={labelWidth} grow={true}>
<InlineField label={t('geomap.map-view-editor.label-view', 'View')} labelWidth={labelWidth} grow={true}>
<Select options={views.options} value={views.current} onChange={onSelectView} />
</InlineField>
</InlineFieldRow>
@ -77,7 +79,15 @@ export const MapViewEditor = ({
)}
<InlineFieldRow>
<InlineField label={value?.id === MapCenterID.Fit ? 'Max Zoom' : 'Zoom'} labelWidth={labelWidth} grow={true}>
<InlineField
label={
value?.id === MapCenterID.Fit
? t('geomap.map-view-editor.label-max-zoom', 'Max Zoom')
: t('geomap.map-view-editor.label-zoom', 'Zoom')
}
labelWidth={labelWidth}
grow={true}
>
<NumberInput
value={value?.zoom ?? 1}
min={1}
@ -92,7 +102,9 @@ export const MapViewEditor = ({
<VerticalGroup>
<Button variant="secondary" size="sm" fullWidth onClick={onSetCurrentView}>
<span>Use current map settings</span>
<span>
<Trans i18nKey="geomap.map-view-editor.use-current-map-settings">Use current map settings</Trans>
</span>
</Button>
</VerticalGroup>
</>

@ -4,6 +4,7 @@ import { useObservable } from 'react-use';
import { Observable, of } from 'rxjs';
import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem, FrameMatcher } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import {
ScaleDimensionConfig,
ResourceDimensionConfig,
@ -54,6 +55,7 @@ export interface StyleEditorOptions {
type Props = StandardEditorProps<StyleConfig, StyleEditorOptions>;
export const StyleEditor = (props: Props) => {
const { t } = useTranslate();
const { value, onChange, item } = props;
const context = useMemo(() => {
if (!item.settings?.frameMatcher) {
@ -129,7 +131,7 @@ export const StyleEditor = (props: Props) => {
{featuresHavePoints && (
<>
<InlineFieldRow>
<InlineField label={'Symbol'}>
<InlineField label={t('geomap.style-editor.label-symbol', 'Symbol')}>
<ResourceDimensionEditor
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
@ -149,7 +151,7 @@ export const StyleEditor = (props: Props) => {
/>
</InlineField>
</InlineFieldRow>
<Field label={'Rotation angle'}>
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
<ScalarDimensionEditor
value={value?.rotation ?? defaultStyleConfig.rotation}
context={context}
@ -167,7 +169,7 @@ export const StyleEditor = (props: Props) => {
</>
)}
<InlineFieldRow>
<InlineField label="Color" labelWidth={10}>
<InlineField label={t('geomap.style-editor.label-color', 'Color')} labelWidth={10}>
<InlineLabel width={4}>
<ColorPicker
color={value?.color?.fixed ?? defaultStyleConfig.color.fixed}
@ -179,7 +181,7 @@ export const StyleEditor = (props: Props) => {
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField label="Opacity" labelWidth={10} grow>
<InlineField label={t('geomap.style-editor.label-opacity', 'Opacity')} labelWidth={10} grow>
<SliderValueEditor
value={value?.opacity ?? defaultStyleConfig.opacity}
context={context}
@ -202,7 +204,7 @@ export const StyleEditor = (props: Props) => {
return (
<>
<Field label={'Size'}>
<Field label={t('geomap.style-editor.label-size', 'Size')}>
<ScaleDimensionEditor
value={value?.size ?? defaultStyleConfig.size}
context={context}
@ -219,7 +221,7 @@ export const StyleEditor = (props: Props) => {
</Field>
{!settings?.hideSymbol && (
<>
<Field label={'Symbol'}>
<Field label={t('geomap.style-editor.label-symbol', 'Symbol')}>
<ResourceDimensionEditor
value={value?.symbol ?? defaultStyleConfig.symbol}
context={context}
@ -238,7 +240,7 @@ export const StyleEditor = (props: Props) => {
}
/>
</Field>
<Field label={'Symbol Vertical Align'}>
<Field label={t('geomap.style-editor.label-symbol-vertical-align', 'Symbol vertical align')}>
<RadioButtonGroup
value={value?.symbolAlign?.vertical ?? defaultStyleConfig.symbolAlign.vertical}
onChange={onAlignVerticalChange}
@ -249,7 +251,7 @@ export const StyleEditor = (props: Props) => {
]}
/>
</Field>
<Field label={'Symbol Horizontal Align'}>
<Field label={t('geomap.style-editor.label-symbol-horizontal-align', 'Symbol horizontal align')}>
<RadioButtonGroup
value={value?.symbolAlign?.horizontal ?? defaultStyleConfig.symbolAlign.horizontal}
onChange={onAlignHorizontalChange}
@ -262,7 +264,7 @@ export const StyleEditor = (props: Props) => {
</Field>
</>
)}
<Field label={'Color'}>
<Field label={t('geomap.style-editor.label-color', 'Color')}>
<ColorDimensionEditor
value={value?.color ?? defaultStyleConfig.color}
context={context}
@ -270,7 +272,7 @@ export const StyleEditor = (props: Props) => {
item={{} as StandardEditorsRegistryItem}
/>
</Field>
<Field label={'Fill opacity'}>
<Field label={t('geomap.style-editor.label-fill-opacity', 'Fill opacity')}>
<SliderValueEditor
value={value?.opacity ?? defaultStyleConfig.opacity}
context={context}
@ -287,7 +289,7 @@ export const StyleEditor = (props: Props) => {
/>
</Field>
{settings?.displayRotation && (
<Field label={'Rotation angle'}>
<Field label={t('geomap.style-editor.label-rotation-angle', 'Rotation angle')}>
<ScalarDimensionEditor
value={value?.rotation ?? defaultStyleConfig.rotation}
context={context}
@ -303,7 +305,7 @@ export const StyleEditor = (props: Props) => {
/>
</Field>
)}
<Field label={'Text label'}>
<Field label={t('geomap.style-editor.label-text-label', 'Text label')}>
<TextDimensionEditor
value={value?.text ?? defaultTextConfig}
context={context}
@ -315,7 +317,7 @@ export const StyleEditor = (props: Props) => {
{hasTextLabel && (
<>
<HorizontalGroup>
<Field label={'Font size'}>
<Field label={t('geomap.style-editor.label-font-size', 'Font size')}>
<NumberValueEditor
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
context={context}
@ -323,7 +325,7 @@ export const StyleEditor = (props: Props) => {
item={{} as FieldConfigPropertyItem}
/>
</Field>
<Field label={'X offset'}>
<Field label={t('geomap.style-editor.label-x-offset', 'X offset')}>
<NumberValueEditor
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
context={context}
@ -331,7 +333,7 @@ export const StyleEditor = (props: Props) => {
item={{} as FieldConfigPropertyItem}
/>
</Field>
<Field label={'Y offset'}>
<Field label={t('geomap.style-editor.label-y-offset', 'Y offset')}>
<NumberValueEditor
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
context={context}
@ -340,7 +342,7 @@ export const StyleEditor = (props: Props) => {
/>
</Field>
</HorizontalGroup>
<Field label={'Align'}>
<Field label={t('geomap.style-editor.label-align', 'Align')}>
<RadioButtonGroup
value={value?.textConfig?.textAlign ?? defaultStyleConfig.textConfig.textAlign}
onChange={onTextAlignChange}
@ -351,7 +353,7 @@ export const StyleEditor = (props: Props) => {
]}
/>
</Field>
<Field label={'Baseline'}>
<Field label={t('geomap.style-editor.label-baseline', 'Baseline')}>
<RadioButtonGroup
value={value?.textConfig?.textBaseline ?? defaultStyleConfig.textConfig.textBaseline}
onChange={onTextBaselineChange}

@ -5,6 +5,7 @@ import { useObservable } from 'react-use';
import { Observable } from 'rxjs';
import { GrafanaTheme2, SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ComparisonOperation } from '@grafana/schema';
import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
import { comparisonOperationOptions } from '@grafana/ui/internal';
@ -26,6 +27,7 @@ export interface StyleRuleEditorSettings {
type Props = StandardEditorProps<FeatureStyleConfig, StyleRuleEditorSettings, unknown>;
export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
const { t } = useTranslate();
const settings = item.settings;
if (!settings) {
// Shouldn't be possible to hit this block, but just in case
@ -132,13 +134,13 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
return (
<div className={styles.rule}>
<InlineFieldRow className={styles.row}>
<InlineField label="Rule" labelWidth={LABEL_WIDTH} grow={true}>
<InlineField label={t('geomap.style-rule-editor.label-rule', 'Rule')} labelWidth={LABEL_WIDTH} grow={true}>
<Select
placeholder={'Feature property'}
placeholder={t('geomap.style-rule-editor.placeholder-feature-property', 'Feature property')}
value={propv.current}
options={propv.options}
onChange={onChangeProperty}
aria-label={'Feature property'}
aria-label={t('geomap.style-rule-editor.aria-label-feature-property', 'Feature property')}
isClearable
allowCustomValue
/>
@ -148,7 +150,7 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
value={comparisonOperationOptions.find((v) => v.value === check.operation)}
options={comparisonOperationOptions}
onChange={onChangeComparison}
aria-label={'Comparison operator'}
aria-label={t('geomap.style-rule-editor.aria-label-comparison-operator', 'Comparison operator')}
width={8}
/>
</InlineField>
@ -156,11 +158,11 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
<div className={styles.flexRow}>
{(check.operation === ComparisonOperation.EQ || check.operation === ComparisonOperation.NEQ) && (
<Select
placeholder={'value'}
placeholder={t('geomap.style-rule-editor.placeholder-value', 'value')}
value={valuev.current}
options={valuev.options}
onChange={onChangeValue}
aria-label={'Comparison value'}
aria-label={t('geomap.style-rule-editor.aria-label-comparison-value', 'Comparison value')}
isClearable
allowCustomValue
/>
@ -169,7 +171,7 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
<NumberInput
key={`${check.property}/${check.operation}`}
value={!isNaN(Number(check.value)) ? Number(check.value) : 0}
placeholder="numeric value"
placeholder={t('geomap.style-rule-editor.placeholder-numeric-value', 'Numeric value')}
onChange={onChangeNumericValue}
/>
)}
@ -180,7 +182,7 @@ export const StyleRuleEditor = ({ value, onChange, item, context }: Props) => {
icon="trash-alt"
onClick={() => onDelete()}
variant="secondary"
aria-label={'Delete style rule'}
aria-label={t('geomap.style-rule-editor.aria-label-delete-style-rule', 'Delete style rule')}
className={styles.button}
></Button>
</InlineFieldRow>

@ -1,4 +1,5 @@
import { PanelPlugin } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { config } from '@grafana/runtime';
import { commonOptionsBuilder } from '@grafana/ui';
@ -72,7 +73,13 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel)
path: '',
name: '',
// eslint-disable-next-line react/display-name
editor: () => <div>The basemap layer is configured by the server admin.</div>,
editor: () => (
<div>
<Trans i18nKey="geomap.plugin.basemap-layer-configured-server-admin">
The basemap layer is configured by the server admin.
</Trans>
</div>
),
});
} else if (baselayer) {
builder.addNestedOptions(

@ -3,6 +3,8 @@ import { css, cx } from '@emotion/css';
import { PureComponent } from 'react';
import { PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { config, reportInteraction } from '@grafana/runtime';
import { Button, Spinner, stylesFactory } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
@ -89,19 +91,23 @@ export class GettingStarted extends PureComponent<PanelProps, State> {
<div className={styles.container}>
{!checksDone ? (
<div className={styles.loading}>
<div className={styles.loadingText}>Checking completed setup steps</div>
<div className={styles.loadingText}>
<Trans i18nKey="gettingstarted.getting-started.checking-completed-setup-steps">
Checking completed setup steps
</Trans>
</div>
<Spinner size="xl" inline />
</div>
) : (
<>
<Button variant="secondary" fill="text" className={styles.dismiss} onClick={this.dismiss}>
Remove this panel
<Trans i18nKey="gettingstarted.getting-started.remove-this-panel">Remove this panel</Trans>
</Button>
{currentStep === steps.length - 1 && (
<Button
className={cx(styles.backForwardButtons, styles.previous)}
onClick={this.onPreviousClick}
aria-label="To basic tutorials"
aria-label={t('gettingstarted.getting-started.aria-label-to-basic-tutorials', 'To basic tutorials')}
icon="angle-left"
variant="secondary"
/>
@ -113,7 +119,10 @@ export class GettingStarted extends PureComponent<PanelProps, State> {
<Button
className={cx(styles.backForwardButtons, styles.forward)}
onClick={this.onForwardClick}
aria-label="To advanced tutorials"
aria-label={t(
'gettingstarted.getting-started.aria-label-to-advanced-tutorials',
'To advanced tutorials'
)}
icon="angle-right"
variant="secondary"
/>

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { Icon, useStyles2 } from '@grafana/ui';
@ -13,6 +14,7 @@ interface Props {
}
export const DocsCard = ({ card }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles, card.done);
return (
@ -23,7 +25,9 @@ export const DocsCard = ({ card }: Props) => {
className={styles.url}
onClick={() => reportInteraction('grafana_getting_started_docs', { title: card.title, link: card.href })}
>
<div className={styles.heading}>{card.done ? 'complete' : card.heading}</div>
<div className={styles.heading}>
{card.done ? t('gettingstarted.docs-card.complete', 'complete') : card.heading}
</div>
<h4 className={styles.title}>{card.title}</h4>
</a>
</div>
@ -34,7 +38,8 @@ export const DocsCard = ({ card }: Props) => {
rel="noreferrer"
onClick={() => reportInteraction('grafana_getting_started_docs', { title: card.title, link: card.learnHref })}
>
Learn how in the docs <Icon name="external-link-alt" />
<Trans i18nKey="gettingstarted.docs-card.learn-how">Learn how in the docs</Trans>{' '}
<Icon name="external-link-alt" />
</a>
</div>
);

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { MouseEvent } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import store from 'app/core/store';
@ -15,6 +16,7 @@ interface Props {
}
export const TutorialCard = ({ card }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles, card.done);
return (
@ -27,7 +29,9 @@ export const TutorialCard = ({ card }: Props) => {
>
<div className={cardContent}>
<div className={styles.type}>{card.type}</div>
<div className={styles.heading}>{card.done ? 'complete' : card.heading}</div>
<div className={styles.heading}>
{card.done ? t('gettingstarted.tutorial-card.complete', 'complete') : card.heading}
</div>
<h4 className={styles.cardTitle}>{card.title}</h4>
<div className={styles.info}>{card.info}</div>
</div>

@ -9,6 +9,7 @@ import {
cacheFieldDisplayNames,
getHistogramFields,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { TooltipDisplayMode, TooltipPlugin2, useTheme2 } from '@grafana/ui';
import { TooltipHoverMode } from '@grafana/ui/internal';
@ -64,7 +65,11 @@ export const HistogramPanel = ({ data, options, width, height }: Props) => {
if (!histogram || !histogram.fields.length) {
return (
<div className="panel-empty">
<p>No histogram found in response</p>
<p>
<Trans i18nKey="histogram.histogram-panel.no-histogram-found-in-response">
No histogram found in response
</Trans>
</p>
</div>
);
}

@ -9,6 +9,7 @@ import {
GrafanaTheme2,
parseLiveChannelAddress,
} from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Select, Alert, Label, stylesFactory, Combobox } from '@grafana/ui';
import { config } from 'app/core/config';
import { discoveryResources, getAPIGroupDiscoveryList, GroupDiscoveryResource } from 'app/features/apiserver/discovery';
@ -27,6 +28,7 @@ const scopes: Array<SelectableValue<LiveChannelScope>> = [
];
export function LiveChannelEditor(props: Props) {
const { t } = useTranslate();
const [channels, setChannels] = useState<Array<SelectableValue<string>>>([]);
const [namespaces, paths] = useMemo(() => {
const namespaces: Array<SelectableValue<string>> = [];
@ -110,14 +112,18 @@ export function LiveChannelEditor(props: Props) {
return (
<>
<Alert title="Grafana Live" severity="info">
This supports real-time event streams in grafana core. This feature is under heavy development. Expect the
intefaces and structures to change as this becomes more production ready.
<Alert title={t('live.live-channel-editor.title-grafana-live', 'Grafana Live')} severity="info">
<Trans i18nKey="live.live-channel-editor.description-grafana-live">
This supports real-time event streams in Grafana core. This feature is under heavy development. Expect the
interfaces and structures to change as this becomes more production ready.
</Trans>
</Alert>
<div>
<div className={style.dropWrap}>
<Label>Scope</Label>
<Label>
<Trans i18nKey="live.live-channel-editor.scope">Scope</Trans>
</Label>
<Select options={scopes} value={scopes.find((s) => s.value === scope)} onChange={onScopeChanged} />
</div>
@ -125,7 +131,10 @@ export function LiveChannelEditor(props: Props) {
<div className={style.dropWrap}>
<Combobox
options={getWatchableResources}
placeholder="Select watchable resource"
placeholder={t(
'live.live-channel-editor.placeholder-select-watchable-resource',
'Select watchable resource'
)}
onChange={(v) => {
const resource = (v as any).resource as GroupDiscoveryResource;
if (resource) {
@ -142,7 +151,9 @@ export function LiveChannelEditor(props: Props) {
{scope && (
<div className={style.dropWrap}>
<Label>Namespace</Label>
<Label>
<Trans i18nKey="live.live-channel-editor.namespace">Namespace</Trans>
</Label>
<Select
options={namespaces}
value={
@ -159,7 +170,9 @@ export function LiveChannelEditor(props: Props) {
{scope && namespace && (
<div className={style.dropWrap}>
<Label>Path</Label>
<Label>
<Trans i18nKey="live.live-channel-editor.path">Path</Trans>
</Label>
<Select
options={paths}
value={findPathOption(paths, path)}

@ -18,6 +18,8 @@ import {
LiveChannelAddress,
StreamingDataFrame,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { t } from '@grafana/i18n/internal';
import { config, getGrafanaLiveSrv } from '@grafana/runtime';
import { Alert, stylesFactory, JSONFormatter, CustomScrollbar } from '@grafana/ui';
@ -125,9 +127,12 @@ export class LivePanel extends PureComponent<Props, State> {
const preformatted = `[feature_toggles]
enable = live`;
return (
<Alert title="Grafana Live" severity="info">
<p>Grafana live requires a feature flag to run</p>
<Alert title={t('live.live-panel.title-grafana-live', 'Grafana Live')} severity="info">
<p>
<Trans i18nKey="live.live-panel.grafana-requires-feature">Grafana live requires a feature flag to run</Trans>
</p>
{/* eslint-disable-next-line @grafana/i18n/no-untranslated-strings */}
<b>custom.ini:</b>
<pre>{preformatted}</pre>
</Alert>
@ -141,7 +146,9 @@ export class LivePanel extends PureComponent<Props, State> {
if (!message) {
return (
<div>
<h4>Waiting for data:</h4>
<h4>
<Trans i18nKey="live.live-panel.waiting-for-data">Waiting for data:</Trans>
</h4>
{options.channel?.scope}/{options.channel?.namespace}/{options.channel?.path}
</div>
);
@ -246,15 +253,17 @@ export class LivePanel extends PureComponent<Props, State> {
const { addr, error } = this.state;
if (!addr) {
return (
<Alert title="Grafana Live" severity="info">
Use the panel editor to pick a channel
<Alert title={t('live.live-panel.title-grafana-live', 'Grafana Live')} severity="info">
<Trans i18nKey="live.live-panel.panel-editor-channel">Use the panel editor to pick a channel</Trans>
</Alert>
);
}
if (error) {
return (
<div>
<h2>ERROR</h2>
<h2>
<Trans i18nKey="live.live-panel.error">Error</Trans>
</h2>
<div>{JSON.stringify(error)}</div>
</div>
);

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { LiveChannelAddress, isValidLiveChannelAddress } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { getBackendSrv, getGrafanaLiveSrv } from '@grafana/runtime';
import { CodeEditor, Button } from '@grafana/ui';
@ -60,7 +61,9 @@ export function LivePublish({ height, mode, body, addr, onSave }: Props) {
showLineNumbers={true}
/>
<div style={{ height: 32 }}>
<Button onClick={onPublishClicked}>Publish</Button>
<Button onClick={onPublishClicked}>
<Trans i18nKey="live.live-publish.publish">Publish</Trans>
</Button>
</div>
</>
);

@ -30,6 +30,7 @@ import {
LoadingState,
rangeUtil,
} from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { config, getAppEvents } from '@grafana/runtime';
import { ScrollContainer, usePanelContext, useStyles2 } from '@grafana/ui';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
@ -454,7 +455,9 @@ export const LogsPanel = ({
const renderCommonLabels = () => (
<div className={cx(style.labelContainer, isAscending && style.labelContainerAscending)}>
<span className={style.label}>Common labels:</span>
<span className={style.label}>
<Trans i18nKey="logs.logs-panel.render-common-labels.common-labels">Common labels:</Trans>
</span>
<LogLabels
labels={typeof commonLabels?.value === 'object' ? commonLabels?.value : noCommonLabels}
emptyMessage="(no common labels)"

@ -1,8 +1,9 @@
import { useEffect } from 'react';
import { PanelProps } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { RefreshEvent } from '@grafana/runtime';
import { Alert, Icon, ScrollContainer } from '@grafana/ui';
import { Alert, ScrollContainer, TextLink } from '@grafana/ui';
import { News } from './component/News';
import { DEFAULT_FEED_URL } from './constants';
@ -12,6 +13,7 @@ import { useNewsFeed } from './useNewsFeed';
interface NewsPanelProps extends PanelProps<Options> {}
export function NewsPanel(props: NewsPanelProps) {
const { t } = useTranslate();
const {
width,
options: { feedUrl = DEFAULT_FEED_URL, showImage },
@ -33,19 +35,22 @@ export function NewsPanel(props: NewsPanelProps) {
if (state.error) {
return (
<Alert title="Error loading RSS feed">
Make sure that the feed URL is correct and that CORS is configured correctly on the server. See{' '}
<a
style={{ textDecoration: 'underline' }}
href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/news/"
>
News panel documentation. <Icon name="external-link-alt" />
</a>
<Alert title={t('news.news-panel.title-error-loading-rss-feed', 'Error loading RSS feed')}>
<Trans i18nKey="news.news-panel.body-error-loading-rss-feed">
Make sure that the feed URL is correct and that CORS is configured correctly on the server. See{' '}
<TextLink href="https://grafana.com/docs/grafana/latest/panels-visualizations/visualizations/news/" external>
News panel documentation.
</TextLink>
</Trans>
</Alert>
);
}
if (state.loading) {
return <div>Loading...</div>;
return (
<div>
<Trans i18nKey="news.news-panel.loading">Loading...</Trans>
</div>
);
}
if (!state.value) {

@ -1,5 +1,7 @@
import { MouseEvent, memo } from 'react';
import { useTranslate } from '@grafana/i18n';
import { EdgeArrowMarker } from './EdgeArrowMarker';
import { computeNodeCircumferenceStrokeWidth, nodeR } from './Node';
import { EdgeDatumLayout, NodeDatum } from './types';
@ -19,6 +21,7 @@ interface Props {
}
export const Edge = memo(function Edge(props: Props) {
const { t } = useTranslate();
const { edge, onClick, onMouseEnter, onMouseLeave, hovering, svgIdNamespace, processedNodesLength } = props;
// Not great typing but after we do layout these properties are full objects not just references
@ -60,7 +63,10 @@ export const Edge = memo(function Edge(props: Props) {
key={`${edge.id}-${edge.source.y ?? ''}-${processedNodesLength}-g`}
onClick={(event) => onClick(event, edge)}
style={{ cursor: 'pointer' }}
aria-label={`Edge from: ${source.id} to: ${target.id}`}
aria-label={t('nodeGraph.edge.aria-label-from-to', 'Edge from: {{from}} to: {{to}}', {
from: source.id,
to: target.id,
})}
>
<line
strokeWidth={(hovering ? 1 : 0) + (edge.highlighted ? 1 : 0) + edge.thickness}

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { MouseEvent, memo } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
import { NodesMarker } from './types';
@ -32,6 +33,7 @@ export const Marker = memo(function Marker(props: {
marker: NodesMarker;
onClick?: (event: MouseEvent<SVGElement>, marker: NodesMarker) => void;
}) {
const { t } = useTranslate();
const { marker, onClick } = props;
const { node } = marker;
const styles = useStyles2(getStyles);
@ -47,14 +49,20 @@ export const Marker = memo(function Marker(props: {
onClick={(event) => {
onClick?.(event, marker);
}}
aria-label={`Hidden nodes marker: ${node.id}`}
aria-label={t('nodeGraph.marker.aria-label-hidden-marker', 'Hidden nodes marker: {{marker}}', {
marker: node.id,
})}
>
<circle className={styles.mainCircle} r={nodeR} cx={node.x} cy={node.y} />
<g>
<foreignObject x={node.x - 25} y={node.y - 25} width="50" height="50">
<div className={styles.text}>
{/* we limit the count to 101 so if we have more than 100 nodes we don't have exact count */}
<span>{marker.count > 100 ? '>100' : marker.count} nodes</span>
<span>
{marker.count > 100
? t('nodeGraph.marker.100-node-count', '>100 nodes')
: t('nodeGraph.marker.node-count', '{{count}} nodes', { count: marker.count })}
</span>
</div>
</foreignObject>
</g>

@ -4,6 +4,7 @@ import { MouseEvent, memo } from 'react';
import tinycolor from 'tinycolor2';
import { Field, getFieldColorModeForField, GrafanaTheme2 } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Icon, useTheme2 } from '@grafana/ui';
import { HoverState } from './NodeGraph';
@ -82,6 +83,7 @@ export const Node = memo(function Node(props: {
onMouseLeave: (id: string) => void;
onClick: (event: MouseEvent<SVGElement>, node: NodeDatum) => void;
}) {
const { t } = useTranslate();
const { node, onMouseEnter, onMouseLeave, onClick, hovering } = props;
const theme = useTheme2();
const styles = getStyles(theme, hovering);
@ -94,7 +96,11 @@ export const Node = memo(function Node(props: {
}
return (
<g data-node-id={node.id} className={styles.mainGroup} aria-label={`Node: ${node.title}`}>
<g
data-node-id={node.id}
className={styles.mainGroup}
aria-label={t('nodeGraph.node.aria-label-node-title', 'Node: {{nodeName}}', { nodeName: node.title })}
>
<circle
data-testid={`node-circle-${node.id}`}
className={node.highlighted ? styles.filledCircle : styles.mainCircle}

@ -4,6 +4,7 @@ import { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } f
import useMeasure from 'react-use/lib/useMeasure';
import { DataFrame, GrafanaTheme2, LinkModel } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Icon, RadioButtonGroup, Spinner, useStyles2 } from '@grafana/ui';
import { Edge } from './Edge';
@ -126,6 +127,7 @@ interface Props {
layoutAlgorithm?: LayoutAlgorithm;
}
export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode, layoutAlgorithm }: Props) {
const { t } = useTranslate();
const nodeCountLimit = nodeLimit || defaultNodeCountLimit;
const { edges: edgesDataFrames, nodes: nodesDataFrames } = useCategorizeFrames(dataFrames);
@ -243,7 +245,7 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode,
<div ref={topLevelRef} className={styles.wrapper}>
{loading ? (
<div className={styles.loadingWrapper}>
Computing layout&nbsp;
<Trans i18nKey="nodeGraph.node-graph.computing-layout">Computing layout</Trans>&nbsp;
<Spinner />
</div>
) : null}
@ -305,7 +307,9 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode,
</g>
</svg>
) : (
<div className={styles.noDataMsg}>No data</div>
<div className={styles.noDataMsg}>
<Trans i18nKey="nodeGraph.node-graph.no-data">No data</Trans>
</div>
)}
<div className={styles.viewControls}>
@ -342,9 +346,11 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode,
<div
className={styles.alert}
style={{ top: panelId ? '0px' : '40px' }} // panelId is undefined in Explore
aria-label={'Nodes hidden warning'}
aria-label={t('nodeGraph.node-graph.aria-label-nodes-hidden-warning', 'Nodes hidden warning')}
>
<Icon size="sm" name={'info-circle'} /> {hiddenNodesCount} nodes are hidden for performance reasons.
<Trans i18nKey="nodeGraph.node-graph.hidden-nodes" count={hiddenNodesCount}>
<Icon size="sm" name={'info-circle'} /> {'{{count}}'} nodes are hidden for performance reasons.
</Trans>
</div>
)}
@ -352,10 +358,14 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode,
<div
className={styles.alert}
style={{ top: panelId ? '30px' : '70px' }}
aria-label={'Layered layout performance warning'}
aria-label={t(
'nodeGraph.node-graph.aria-label-layered-layout-performance-warning',
'Layered layout performance warning'
)}
>
<Icon size="sm" name={'exclamation-triangle'} /> Layered layout may be slow with {processed.nodes.length}{' '}
nodes.
<Trans i18nKey="nodeGraph.node-graph.processed-nodes" count={processed.nodes.length}>
<Icon size="sm" name={'exclamation-triangle'} /> Layered layout may be slow with {'{{count}}'} nodes.
</Trans>
</div>
)}

@ -2,6 +2,7 @@ import memoizeOne from 'memoize-one';
import { useId } from 'react';
import { PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { useLinks } from '../../../features/explore/utils/links';
@ -16,7 +17,9 @@ export const NodeGraphPanel = ({ width, height, data, options }: PanelProps<Node
if (!data || !data.series.length) {
return (
<div className="panel-empty">
<p>No data found in response</p>
<p>
<Trans i18nKey="nodeGraph.node-graph-panel.no-data-found-in-response">No data found in response</Trans>
</p>
</div>
);
}

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { useState } from 'react';
import { useTranslate } from '@grafana/i18n';
import { Button, Stack, useStyles2 } from '@grafana/ui';
function getStyles() {
@ -26,6 +27,7 @@ interface Props<Config> {
* Control buttons for zoom but also some layout config inputs mainly for debugging.
*/
export function ViewControls<Config extends Record<string, any>>(props: Props<Config>) {
const { t } = useTranslate();
const { config, onConfigChange, onPlus, onMinus, disableZoomOut, disableZoomIn } = props;
const [showConfig, setShowConfig] = useState(false);
@ -41,7 +43,7 @@ export function ViewControls<Config extends Record<string, any>>(props: Props<Co
icon={'plus-circle'}
onClick={onPlus}
size={'md'}
title={'Zoom in'}
title={t('nodeGraph.view-controls.title-zoom-in', 'Zoom in')}
variant="secondary"
disabled={disableZoomIn}
/>
@ -49,7 +51,7 @@ export function ViewControls<Config extends Record<string, any>>(props: Props<Co
icon={'minus-circle'}
onClick={onMinus}
size={'md'}
title={'Zoom out'}
title={t('nodeGraph.view-controls.title-zoom-out', 'Zoom out')}
variant="secondary"
disabled={disableZoomOut}
/>
@ -58,7 +60,9 @@ export function ViewControls<Config extends Record<string, any>>(props: Props<Co
{allowConfiguration && (
<Button size={'xs'} fill="text" onClick={() => setShowConfig((showConfig) => !showConfig)}>
{showConfig ? 'Hide config' : 'Show config'}
{showConfig
? t('nodeGraph.view-controls.hide-config', 'Hide config')
: t('nodeGraph.view-controls.show-config', 'Show config')}
</Button>
)}

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { Field, GrafanaTheme2, StandardEditorProps } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Button, ColorPicker, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/internal';
@ -10,6 +11,7 @@ type Settings = { filter: (field: Field) => boolean };
type ArcOptionsEditorProps = StandardEditorProps<ArcOption[], Settings, NodeGraphOptions, undefined>;
export const ArcOptionsEditor = ({ value, onChange, context }: ArcOptionsEditorProps) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const addArc = () => {
@ -54,12 +56,18 @@ export const ArcOptionsEditor = ({ value, onChange, context }: ArcOptionsEditorP
updateField(i, 'color', val);
}}
/>
<Button size="sm" icon="minus" variant="secondary" onClick={() => removeArc(i)} title="Remove arc" />
<Button
size="sm"
icon="minus"
variant="secondary"
onClick={() => removeArc(i)}
title={t('nodeGraph.arc-options-editor.title-remove-arc', 'Remove arc')}
/>
</div>
);
})}
<Button size={'sm'} icon="plus" onClick={addArc} variant="secondary">
Add arc
<Trans i18nKey="nodeGraph.arc-options-editor.add-arc">Add arc</Trans>
</Button>
</>
);

@ -3,6 +3,7 @@ import { MouseEvent, useCallback, useState } from 'react';
import * as React from 'react';
import { DataFrame, Field, GrafanaTheme2, LinkModel, LinkTarget } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { ContextMenu, MenuGroup, MenuItem, useStyles2 } from '@grafana/ui';
import { Config } from './layout';
@ -199,6 +200,7 @@ function HeaderRow({ label, value }: { label: string; value: string }) {
* Shows some field values in a table on top of the context menu.
*/
function NodeHeader({ node, nodes }: { node: NodeDatum; nodes?: DataFrame }) {
const { t } = useTranslate();
const rows = [];
if (nodes) {
const fields = getNodeFields(nodes);
@ -210,10 +212,12 @@ function NodeHeader({ node, nodes }: { node: NodeDatum; nodes?: DataFrame }) {
} else {
// Fallback if we don't have nodes dataFrame. Can happen if we use just the edges frame to construct this.
if (node.title) {
rows.push(<HeaderRow key="title" label={'Title'} value={node.title} />);
rows.push(<HeaderRow key="title" label={t('nodeGraph.node-header.label-title', 'Title')} value={node.title} />);
}
if (node.subTitle) {
rows.push(<HeaderRow key="subtitle" label={'Subtitle'} value={node.subTitle} />);
rows.push(
<HeaderRow key="subtitle" label={t('nodeGraph.node-header.label-subtitle', 'Subtitle')} value={node.subTitle} />
);
}
}
@ -228,6 +232,7 @@ function NodeHeader({ node, nodes }: { node: NodeDatum; nodes?: DataFrame }) {
* Shows some of the field values in a table on top of the context menu.
*/
function EdgeHeader(props: { edge: EdgeDatumLayout; edges: DataFrame }) {
const { t } = useTranslate();
const index = props.edge.dataFrameRowIndex;
const fields = getEdgeFields(props.edges);
const valueSource = fields.source?.values[index] || '';
@ -235,7 +240,13 @@ function EdgeHeader(props: { edge: EdgeDatumLayout; edges: DataFrame }) {
const rows = [];
if (valueSource && valueTarget) {
rows.push(<HeaderRow key={'header-row'} label={'Source → Target'} value={`${valueSource}${valueTarget}`} />);
rows.push(
<HeaderRow
key={'header-row'}
label={t('nodeGraph.edge-header.label-source-target', 'Source → Target')}
value={`${valueSource}${valueTarget}`}
/>
);
}
for (const f of [fields.mainStat, fields.secondaryStat, ...fields.details]) {

@ -1,6 +1,7 @@
import { useMemo, useState } from 'react';
import { DashboardCursorSync, PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import {
AxisPlacement,
EventBusPlugin,
@ -79,8 +80,10 @@ export const StatusHistoryPanel = ({
return (
<div className="panel-empty">
<p>
Too many points to visualize properly. <br />
Update the query to return fewer points. <br />({paginatedFrames[0].length} points received)
<Trans i18nKey="status-history.status-history-panel.too-many-points" count={paginatedFrames[0].length}>
Too many points to visualize properly. <br />
Update the query to return fewer points. <br />({'{{count}}'} points received)
</Trans>
</p>
</div>
);

@ -1,3 +1,4 @@
import { useTranslate } from '@grafana/i18n';
import { TableAutoCellOptions, TableColorTextCellOptions } from '@grafana/schema';
import { Field, Switch } from '@grafana/ui';
@ -8,6 +9,7 @@ export const AutoCellOptionsEditor = ({
onChange,
}: TableCellEditorProps<TableAutoCellOptions | TableColorTextCellOptions>) => {
// Handle row coloring changes
const { t } = useTranslate();
const onWrapTextChange = () => {
cellOptions.wrapText = !cellOptions.wrapText;
onChange(cellOptions);
@ -15,8 +17,11 @@ export const AutoCellOptionsEditor = ({
return (
<Field
label="Wrap text"
description="If selected text will be wrapped to the width of text in the configured column"
label={t('table.auto-cell-options-editor.label-wrap-text', 'Wrap text')}
description={t(
'table.auto-cell-options-editor.description-wrap-text',
'If selected text will be wrapped to the width of text in the configured column'
)}
>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>

@ -1,4 +1,5 @@
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableBarGaugeCellOptions } from '@grafana/schema';
import { Field, RadioButtonGroup, Stack } from '@grafana/ui';
@ -8,6 +9,7 @@ type Props = TableCellEditorProps<TableBarGaugeCellOptions>;
export function BarGaugeCellOptionsEditor({ cellOptions, onChange }: Props) {
// Set the display mode on change
const { t } = useTranslate();
const onCellOptionsChange = (v: BarGaugeDisplayMode) => {
cellOptions.mode = v;
onChange(cellOptions);
@ -20,14 +22,14 @@ export function BarGaugeCellOptionsEditor({ cellOptions, onChange }: Props) {
return (
<Stack direction="column" gap={0}>
<Field label="Gauge display mode">
<Field label={t('table.bar-gauge-cell-options-editor.label-gauge-display-mode', 'Gauge display mode')}>
<RadioButtonGroup
value={cellOptions?.mode ?? BarGaugeDisplayMode.Gradient}
onChange={onCellOptionsChange}
options={barGaugeOpts}
/>
</Field>
<Field label="Value display">
<Field label={t('table.bar-gauge-cell-options-editor.label-value-display', 'Value display')}>
<RadioButtonGroup
value={cellOptions?.valueDisplayMode ?? BarGaugeValueMode.Text}
onChange={onValueModeChange}

@ -1,4 +1,5 @@
import { SelectableValue } from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
import { Field, RadioButtonGroup, Switch, Label, Badge } from '@grafana/ui';
@ -14,6 +15,7 @@ export const ColorBackgroundCellOptionsEditor = ({
onChange,
}: TableCellEditorProps<TableColoredBackgroundCellOptions>) => {
// Set the display mode on change
const { t } = useTranslate();
const onCellOptionsChange = (v: TableCellBackgroundDisplayMode) => {
cellOptions.mode = v;
onChange(cellOptions);
@ -32,15 +34,26 @@ export const ColorBackgroundCellOptionsEditor = ({
};
const label = (
<Label description="If selected text will be wrapped to the width of text in the configured column">
{'Wrap text '}
<Badge text="Alpha" color="blue" style={{ fontSize: '11px', marginLeft: '5px', lineHeight: '1.2' }} />
<Label
description={t(
'table.color-background-cell-options-editor.description-wrap-text',
'If selected text will be wrapped to the width of text in the configured column'
)}
>
<Trans i18nKey="table.color-background-cell-options-editor.wrap-text">Wrap text</Trans>{' '}
<Badge
text={t('table.color-background-cell-options-editor.label.text-alpha', 'Alpha')}
color="blue"
style={{ fontSize: '11px', marginLeft: '5px', lineHeight: '1.2' }}
/>
</Label>
);
return (
<>
<Field label="Background display mode">
<Field
label={t('table.color-background-cell-options-editor.label-background-display-mode', 'Background display mode')}
>
<RadioButtonGroup
value={cellOptions?.mode ?? TableCellBackgroundDisplayMode.Gradient}
onChange={onCellOptionsChange}
@ -48,8 +61,11 @@ export const ColorBackgroundCellOptionsEditor = ({
/>
</Field>
<Field
label="Apply to entire row"
description="If selected the entire row will be colored as this cell would be."
label={t('table.color-background-cell-options-editor.label-apply-to-entire-row', 'Apply to entire row')}
description={t(
'table.color-background-cell-options-editor.description-apply-to-entire-row',
'If selected the entire row will be colored as this cell would be.'
)}
>
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
</Field>

@ -1,11 +1,13 @@
import { FormEvent } from 'react';
import { useTranslate } from '@grafana/i18n';
import { TableImageCellOptions } from '@grafana/schema';
import { Field, Input } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEditorProps<TableImageCellOptions>) => {
const { t } = useTranslate();
const onAltChange = (e: FormEvent<HTMLInputElement>) => {
cellOptions.alt = e.currentTarget.value;
onChange(cellOptions);
@ -19,13 +21,22 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
return (
<>
<Field
label="Alt text"
description="Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
label={t('table.image-cell-options-editor.label-alt-text', 'Alt text')}
description={t(
'table.image-cell-options-editor.description-alt-text',
"Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
)}
>
<Input onChange={onAltChange} defaultValue={cellOptions.alt} />
</Field>
<Field label="Title text" description="Text that will be displayed when the image is hovered by a cursor">
<Field
label={t('table.image-cell-options-editor.label-title-text', 'Title text')}
description={t(
'table.image-cell-options-editor.description-title-text',
'Text that will be displayed when the image is hovered by a cursor'
)}
>
<Input onChange={onTitleChange} defaultValue={cellOptions.title} />
</Field>
</>

@ -1,3 +1,4 @@
import { useTranslate } from '@grafana/i18n';
import { TableAutoCellOptions, TableColorTextCellOptions } from '@grafana/schema';
import { Field, Switch } from '@grafana/ui';
@ -8,13 +9,14 @@ export const AutoCellOptionsEditor = ({
onChange,
}: TableCellEditorProps<TableAutoCellOptions | TableColorTextCellOptions>) => {
// Handle row coloring changes
const { t } = useTranslate();
const onWrapTextChange = () => {
cellOptions.wrapText = !cellOptions.wrapText;
onChange(cellOptions);
};
return (
<Field label="Wrap text">
<Field label={t('table.auto-cell-options-editor.label-wrap-text', 'Wrap text')}>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
);

@ -1,4 +1,5 @@
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { BarGaugeDisplayMode, BarGaugeValueMode, TableBarGaugeCellOptions } from '@grafana/schema';
import { Field, RadioButtonGroup, Stack } from '@grafana/ui';
@ -8,6 +9,7 @@ type Props = TableCellEditorProps<TableBarGaugeCellOptions>;
export function BarGaugeCellOptionsEditor({ cellOptions, onChange }: Props) {
// Set the display mode on change
const { t } = useTranslate();
const onCellOptionsChange = (v: BarGaugeDisplayMode) => {
cellOptions.mode = v;
onChange(cellOptions);
@ -20,14 +22,14 @@ export function BarGaugeCellOptionsEditor({ cellOptions, onChange }: Props) {
return (
<Stack direction="column" gap={0}>
<Field label="Gauge display mode">
<Field label={t('table.bar-gauge-cell-options-editor.label-gauge-display-mode', 'Gauge display mode')}>
<RadioButtonGroup
value={cellOptions?.mode ?? BarGaugeDisplayMode.Gradient}
onChange={onCellOptionsChange}
options={barGaugeOpts}
/>
</Field>
<Field label="Value display">
<Field label={t('table.bar-gauge-cell-options-editor.label-value-display', 'Value display')}>
<RadioButtonGroup
value={cellOptions?.valueDisplayMode ?? BarGaugeValueMode.Text}
onChange={onValueModeChange}

@ -1,4 +1,5 @@
import { SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { TableCellBackgroundDisplayMode, TableColoredBackgroundCellOptions } from '@grafana/schema';
import { Field, RadioButtonGroup, Switch } from '@grafana/ui';
@ -13,6 +14,7 @@ export const ColorBackgroundCellOptionsEditor = ({
onChange,
}: TableCellEditorProps<TableColoredBackgroundCellOptions>) => {
// Set the display mode on change
const { t } = useTranslate();
const onCellOptionsChange = (v: TableCellBackgroundDisplayMode) => {
cellOptions.mode = v;
onChange(cellOptions);
@ -31,7 +33,9 @@ export const ColorBackgroundCellOptionsEditor = ({
return (
<>
<Field label="Background display mode">
<Field
label={t('table.color-background-cell-options-editor.label-background-display-mode', 'Background display mode')}
>
<RadioButtonGroup
value={cellOptions?.mode ?? TableCellBackgroundDisplayMode.Gradient}
onChange={onCellOptionsChange}
@ -39,12 +43,15 @@ export const ColorBackgroundCellOptionsEditor = ({
/>
</Field>
<Field
label="Apply to entire row"
description="If selected the entire row will be colored as this cell would be."
label={t('table.color-background-cell-options-editor.label-apply-to-entire-row', 'Apply to entire row')}
description={t(
'table.color-background-cell-options-editor.description-apply-to-entire-row',
'If selected the entire row will be colored as this cell would be.'
)}
>
<Switch value={cellOptions.applyToRow} onChange={onColorRowChange} />
</Field>
<Field label="Wrap text">
<Field label={t('table.color-background-cell-options-editor.label-wrap-text', 'Wrap text')}>
<Switch value={cellOptions.wrapText} onChange={onWrapTextChange} />
</Field>
</>

@ -1,11 +1,13 @@
import { FormEvent } from 'react';
import { useTranslate } from '@grafana/i18n';
import { TableImageCellOptions } from '@grafana/schema';
import { Field, Input } from '@grafana/ui';
import { TableCellEditorProps } from '../TableCellOptionEditor';
export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEditorProps<TableImageCellOptions>) => {
const { t } = useTranslate();
const onAltChange = (e: FormEvent<HTMLInputElement>) => {
cellOptions.alt = e.currentTarget.value;
onChange(cellOptions);
@ -19,13 +21,22 @@ export const ImageCellOptionsEditor = ({ cellOptions, onChange }: TableCellEdito
return (
<>
<Field
label="Alt text"
description="Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
label={t('table.image-cell-options-editor.label-alt-text', 'Alt text')}
description={t(
'table.image-cell-options-editor.description-alt-text',
"Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader"
)}
>
<Input onChange={onAltChange} defaultValue={cellOptions.alt} />
</Field>
<Field label="Title text" description="Text that will be displayed when the image is hovered by a cursor">
<Field
label={t('table.image-cell-options-editor.label-title-text', 'Title text')}
description={t(
'table.image-cell-options-editor.description-title-text',
'Text that will be displayed when the image is hovered by a cursor'
)}
>
<Input onChange={onTitleChange} defaultValue={cellOptions.title} />
</Field>
</>

@ -1,6 +1,7 @@
import { useMemo } from 'react';
import { StandardEditorProps, SelectableValue } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { LineStyle } from '@grafana/schema';
import { HorizontalGroup, IconButton, RadioButtonGroup, Select } from '@grafana/ui';
@ -55,6 +56,7 @@ const dotOptions: Array<SelectableValue<string>> = [
type Props = StandardEditorProps<LineStyle, unknown>;
export const LineStyleEditor = ({ value, onChange }: Props) => {
const { t } = useTranslate();
const options = useMemo(() => (value?.fill === 'dash' ? dashOptions : dotOptions), [value]);
const current = useMemo(() => {
if (!value?.dash?.length) {
@ -108,12 +110,15 @@ export const LineStyleEditor = ({ value, onChange }: Props) => {
<div>
&nbsp;
<a
title="The input expects a segment list"
title={t(
'timeseries.line-style-editor.title-the-input-expects-a-segment-list',
'The input expects a segment list'
)}
href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash#Parameters"
target="_blank"
rel="noreferrer"
>
<IconButton name="question-circle" tooltip="Help" />
<IconButton name="question-circle" tooltip={t('timeseries.line-style-editor.tooltip-help', 'Help')} />
</a>
</div>
</>

@ -1,6 +1,7 @@
import * as React from 'react';
import { rangeUtil } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { Input } from '@grafana/ui';
export enum InputPrefix {
@ -16,6 +17,7 @@ type Props = {
};
export const NullsThresholdInput = ({ value, onChange, inputPrefix, isTime }: Props) => {
const { t } = useTranslate();
let defaultValue = rangeUtil.secondsToHms(value / 1000);
if (!isTime) {
defaultValue = '10';
@ -57,7 +59,7 @@ export const NullsThresholdInput = ({ value, onChange, inputPrefix, isTime }: Pr
return (
<Input
autoFocus={false}
placeholder="never"
placeholder={t('timeseries.nulls-threshold-input.placeholder-never', 'Never')}
width={10}
defaultValue={defaultValue}
onKeyDown={handleEnterKey}

@ -1,12 +1,14 @@
import { css } from '@emotion/css';
import { GrafanaTheme2, InternalTimeZones, StandardEditorProps } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { OptionsWithTimezones } from '@grafana/schema';
import { IconButton, TimeZonePicker, useStyles2 } from '@grafana/ui';
type Props = StandardEditorProps<string[], unknown, OptionsWithTimezones>;
export const TimezonesEditor = ({ value, onChange }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
if (!value || value.length < 1) {
@ -43,9 +45,17 @@ export const TimezonesEditor = ({ value, onChange }: Props) => {
value={tz ?? InternalTimeZones.default}
/>
{idx === value.length - 1 ? (
<IconButton name="plus" onClick={addTimezone} tooltip="Add timezone" />
<IconButton
name="plus"
onClick={addTimezone}
tooltip={t('timeseries.timezones-editor.tooltip-add-timezone', 'Add timezone')}
/>
) : (
<IconButton name="times" onClick={() => removeTimezone(idx)} tooltip="Remove timezone" />
<IconButton
name="times"
onClick={() => removeTimezone(idx)}
tooltip={t('timeseries.timezones-editor.tooltip-remove-timezone', 'Remove timezone')}
/>
)}
</li>
))}

@ -2,6 +2,7 @@ import { useLayoutEffect, useRef, useState } from 'react';
import uPlot, { TypedArray, Scale } from 'uplot';
import { AbsoluteTimeRange } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { UPlotConfigBuilder, Button } from '@grafana/ui';
interface ThresholdControlsPluginProps {
@ -70,13 +71,15 @@ export const OutsideRangePlugin = ({ config, onChangeTimeRange }: ThresholdContr
}}
>
<div>
<div>Data outside time range</div>
<div>
<Trans i18nKey="timeseries.outside-range-plugin.data-outside-time-range">Data outside time range</Trans>
</div>
<Button
onClick={(v) => onChangeTimeRange({ from: first, to: last })}
variant="secondary"
data-testid="time-series-zoom-to-data"
>
Zoom to data
<Trans i18nKey="timeseries.outside-range-plugin.zoom-to-data">Zoom to data</Trans>
</Button>
</div>
</div>

@ -4,6 +4,7 @@ import { Controller } from 'react-hook-form';
import { useAsyncFn, useClickAway } from 'react-use';
import { AnnotationEventUIModel, GrafanaTheme2, dateTimeFormat, systemDateFormats } from '@grafana/data';
import { useTranslate, Trans } from '@grafana/i18n';
import { Button, Field, Stack, TextArea, usePanelContext, useStyles2 } from '@grafana/ui';
import { Form } from 'app/core/components/Form/Form';
import { TagFilter } from 'app/core/components/TagFilter/TagFilter';
@ -22,6 +23,7 @@ interface AnnotationEditFormDTO {
}
export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...otherProps }: Props) => {
const { t } = useTranslate();
const styles = useStyles2(getStyles);
const { onAnnotationCreate, onAnnotationUpdate } = usePanelContext();
@ -70,7 +72,11 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
<div ref={clickAwayRef} className={styles.editor} {...otherProps}>
<div className={styles.header}>
<Stack justifyContent={'space-between'} alignItems={'center'}>
<div>{isUpdatingAnnotation ? 'Edit annotation' : 'Add annotation'}</div>
<div>
{isUpdatingAnnotation
? t('timeseries.annotation-editor2.edit-annotation', 'Edit annotation')
: t('timeseries.annotation-editor2.add-annotation', 'Add annotation')}
</div>
<div>{time}</div>
</Stack>
</div>
@ -82,7 +88,11 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
return (
<>
<div className={styles.content}>
<Field label={'Description'} invalid={!!errors.description} error={errors?.description?.message}>
<Field
label={t('timeseries.annotation-editor2.label-description', 'Description')}
invalid={!!errors.description}
error={errors?.description?.message}
>
<TextArea
className={styles.textarea}
{...register('description', {
@ -90,7 +100,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
})}
/>
</Field>
<Field label={'Tags'}>
<Field label={t('timeseries.annotation-editor2.label-tags', 'Tags')}>
<Controller
control={control}
name="tags"
@ -98,7 +108,7 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
return (
<TagFilter
allowCustomValue
placeholder="Add tags"
placeholder={t('timeseries.annotation-editor2.placeholder-add-tags', 'Add tags')}
onChange={onChange}
tagOptions={getAnnotationTags}
tags={field.value}
@ -111,10 +121,12 @@ export const AnnotationEditor2 = ({ annoVals, annoIdx, dismiss, timeZone, ...oth
<div className={styles.footer}>
<Stack justifyContent={'flex-end'}>
<Button size={'sm'} variant="secondary" onClick={dismiss} fill="outline">
Cancel
<Trans i18nKey="timeseries.annotation-editor2.cancel">Cancel</Trans>
</Button>
<Button size={'sm'} type={'submit'} disabled={stateIndicator?.loading}>
{stateIndicator?.loading ? 'Saving' : 'Save'}
{stateIndicator?.loading
? t('timeseries.annotation-editor2.saving', 'Saving')
: t('timeseries.annotation-editor2.save', 'Save')}
</Button>
</Stack>
</div>

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import * as React from 'react';
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, textUtil } from '@grafana/data';
import { useTranslate } from '@grafana/i18n';
import { HorizontalGroup, IconButton, Tag, usePanelContext, useStyles2 } from '@grafana/ui';
import alertDef from 'app/features/alerting/state/alertDef';
@ -15,6 +16,7 @@ interface Props {
const retFalse = () => false;
export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Props) => {
const { t } = useTranslate();
const annoId = annoVals.id?.[annoIdx];
const styles = useStyles2(getStyles);
@ -74,13 +76,20 @@ export const AnnotationTooltip2 = ({ annoVals, annoIdx, timeZone, onEdit }: Prop
</div>
{(canEdit || canDelete) && (
<div className={styles.editControls}>
{canEdit && <IconButton name={'pen'} size={'sm'} onClick={onEdit} tooltip="Edit" />}
{canEdit && (
<IconButton
name={'pen'}
size={'sm'}
onClick={onEdit}
tooltip={t('timeseries.annotation-tooltip2.tooltip-edit', 'Edit')}
/>
)}
{canDelete && (
<IconButton
name={'trash-alt'}
size={'sm'}
onClick={() => onAnnotationDelete(annoId)}
tooltip="Delete"
tooltip={t('timeseries.annotation-tooltip2.tooltip-delete', 'Delete')}
/>
)}
</div>

@ -3,6 +3,7 @@ import { useMemo, createRef } from 'react';
import { useAsync } from 'react-use';
import { Field, LinkModel, PanelProps } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { getDataSourceSrv } from '@grafana/runtime';
import { TraceView } from 'app/features/explore/TraceView/TraceView';
import { SpanLinkFunc } from 'app/features/explore/TraceView/components';
@ -34,7 +35,9 @@ export const TracesPanel = ({ data, options, replaceVariables }: PanelProps<Trac
if (!data || !data.series.length || !traceProp) {
return (
<div className="panel-empty">
<p>No data found in response</p>
<p>
<Trans i18nKey="traces.traces-panel.no-data-found-in-response">No data found in response</Trans>
</p>
</div>
);
}

@ -1,6 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { useStyles2 } from '@grafana/ui';
const helpOptions = [
@ -15,9 +16,13 @@ export const WelcomeBanner = () => {
return (
<div className={styles.container}>
<h1 className={styles.title}>Welcome to Grafana</h1>
<h1 className={styles.title}>
<Trans i18nKey="welcome.welcome-banner.welcome-to-grafana">Welcome to Grafana</Trans>
</h1>
<div className={styles.help}>
<h3 className={styles.helpText}>Need help?</h3>
<h3 className={styles.helpText}>
<Trans i18nKey="welcome.welcome-banner.need-help">Need help?</Trans>
</h3>
<div className={styles.helpLinks}>
{helpOptions.map((option, index) => {
return (

@ -12,6 +12,7 @@ import {
FieldType,
GrafanaTheme2,
} from '@grafana/data';
import { Trans, useTranslate } from '@grafana/i18n';
import { Button, Field, IconButton, Select, useStyles2 } from '@grafana/ui';
import { FieldNamePicker } from '@grafana/ui/internal';
import { LayerName } from 'app/core/components/Layers/LayerName';
@ -23,6 +24,7 @@ export const SeriesEditor = ({
onChange,
context,
}: StandardEditorProps<XYSeriesConfig[], unknown, Options>) => {
const { t } = useTranslate();
const style = useStyles2(getStyles);
// reset opts when mapping changes (no way to do this in panel opts builder?)
@ -81,7 +83,7 @@ export const SeriesEditor = ({
{mapping === SeriesMapping.Manual && (
<>
<Button icon="plus" size="sm" variant="secondary" onClick={addSeries} className={style.marginBot}>
Add series
<Trans i18nKey="xychart.series-editor.add-series">Add series</Trans>
</Button>
<div className={style.marginBot}>
@ -92,7 +94,9 @@ export const SeriesEditor = ({
className={index === selectedIdx ? `${style.row} ${style.sel}` : style.row}
onClick={() => setSelectedIdx(index)}
role="button"
aria-label={`Select series ${index + 1}`}
aria-label={t('xychart.series-editor.aria-label-select-series', 'Select series {{seriesNum}}', {
seriesNum: index + 1,
})}
tabIndex={0}
onKeyPress={(e) => {
if (e.key === 'Enter') {
@ -111,10 +115,9 @@ export const SeriesEditor = ({
/>
<IconButton
name="trash-alt"
title={'remove'}
className={cx(style.actionIcon)}
onClick={() => deleteSeries(index)}
tooltip="Delete series"
tooltip={t('xychart.series-editor.tooltip-delete-series', 'Delete series')}
/>
</div>
);
@ -125,9 +128,13 @@ export const SeriesEditor = ({
{selectedIdx >= 0 && series != null && (
<Fragment key={formKey}>
<Field label="Frame">
<Field label={t('xychart.series-editor.label-frame', 'Frame')}>
<Select
placeholder={mapping === SeriesMapping.Auto ? 'All frames' : 'Select frame'}
placeholder={
mapping === SeriesMapping.Auto
? t('xychart.series-editor.placeholder-all-frames', 'All frames')
: t('xychart.series-editor.placeholder-select-frame', 'Select frame')
}
isClearable={true}
options={context.data.map((frame, index) => ({
value: index,
@ -150,7 +157,7 @@ export const SeriesEditor = ({
}}
/>
</Field>
<Field label="X field">
<Field label={t('xychart.series-editor.label-x-field', 'X field')}>
<FieldNamePicker
value={series.x?.matcher.options as string}
context={context}
@ -184,7 +191,7 @@ export const SeriesEditor = ({
}}
/>
</Field>
<Field label="Y field">
<Field label={t('xychart.series-editor.label-y-field', 'Y field')}>
<FieldNamePicker
value={series.y?.matcher?.options as string}
context={context}
@ -219,7 +226,7 @@ export const SeriesEditor = ({
}}
/>
</Field>
<Field label="Size field">
<Field label={t('xychart.series-editor.label-size-field', 'Size field')}>
<FieldNamePicker
value={series.size?.matcher?.options as string}
context={context}
@ -255,7 +262,7 @@ export const SeriesEditor = ({
}}
/>
</Field>
<Field label="Color field">
<Field label={t('xychart.series-editor.label-color-field', 'Color field')}>
<FieldNamePicker
value={series.color?.matcher?.options as string}
context={context}

@ -2903,6 +2903,36 @@
"body": "The YAML content in the editor only contains alert rule configuration <1></1>To configure Prometheus, you need to provide the rest of the <4>configuration file content.</4>"
}
},
"alertlist": {
"group-by": {
"aria-label-group-by-label-keys": "group by label keys",
"placeholder-group-by": "Group by"
},
"ungrouped-mode-view": {
"active-for": "for <1>{{duration}}</1>",
"aria-label-view-alert-rule": "View alert rule",
"view-alert-rule": "View alert rule"
},
"unified-alert-list": {
"clear": "Clear",
"text-loading": "Loading..."
},
"unified-alert-list-panel": {
"body-permission-required": "Sorry, you do not have the required permissions to read alert rules.",
"title-permission-required": "Permission required"
}
},
"annolist": {
"anno-list-panel": {
"aria-label-remove-filter": "Remove filter: {{filterToRemove}}",
"filter": "Filter:",
"loading": "Loading...",
"no-annotations-found": "No annotations found"
},
"annotation-list-item": {
"tooltip-created-by": "Created by:<1></1> {{email}}"
}
},
"annotations": {
"annotation-field-mapper": {
"annotation": "Annotation",
@ -3154,6 +3184,12 @@
"auth-config-auth-config-page-unconnected": {
"subtitle": "Manage your auth settings and configure single sign-on. Find out more in our <2>documentation</2>."
},
"barchart": {
"tick-spacing-editor": {
"content-require-space-from-the-right-side": "Require space from the right side",
"label-rtl": "RTL"
}
},
"bookmarks-page": {
"empty": {
"message": "It looks like you haven’t created any bookmarks yet",
@ -3282,6 +3318,17 @@
"text-this-repository-is-read-only": "If you have direct access to the target, copy the JSON and paste it there."
},
"canvas": {
"apieditor": {
"label-endpoint": "Endpoint",
"label-header-parameters": "Header parameters",
"label-method": "Method",
"label-payload": "Payload",
"label-query-parameters": "Query parameters",
"render-test-apibutton": {
"test-api": "Test API",
"title-test-api": "Test API"
}
},
"button-item": {
"label": {
"center": "Center",
@ -3292,6 +3339,28 @@
"auto": "Auto"
}
},
"button-style-editor": {
"label-variant": "Variant"
},
"canvas-context-menu": {
"close-editor": "Close Editor",
"open-editor": "Open Editor",
"render-menu-items": {
"add-item-menu-item": {
"label-add-item": "Add item"
},
"edit-element-menu-item": {
"label-edit": "Edit"
},
"label-bring-to-front": "Bring to front",
"label-delete": "Delete",
"label-duplicate": "Duplicate",
"label-send-to-back": "Send to back",
"set-background-menu-item": {
"label-set-background": "Set background"
}
}
},
"cloud-item": {
"label": {
"bottom": "Bottom",
@ -3318,6 +3387,17 @@
"auto": "Auto"
}
},
"inline-edit": {
"canvas-inline-editor": "Canvas Inline Editor",
"tooltip-close-inline-editor": "Close inline editor"
},
"inline-edit-body": {
"label-add-item": "Add item",
"please-select-an-element": "Please select an element"
},
"line-style-editor": {
"label-animate": "Animate"
},
"metric-value-item": {
"label": {
"bottom": "Bottom",
@ -3334,6 +3414,14 @@
"not-found-display": {
"not-found": "<0>Not found: </0><config />"
},
"pan-zoom-help": {
"ctrl-right-mouse": "CTRL + right mouse",
"middle-mouse": "Middle mouse",
"pan-title": "Pan:",
"reset-double-click": "Reset: Double click",
"title-pan-and-zoom-controls": "Pan and zoom controls",
"zoom-scroll-wheel": "Zoom: Scroll wheel"
},
"parallelogram-item": {
"label": {
"bottom": "Bottom",
@ -3347,6 +3435,25 @@
"auto": "Auto"
}
},
"params-editor": {
"aria-label-add": "Add",
"aria-label-delete": "Delete",
"placeholder-key": "Key",
"placeholder-value": "Value"
},
"placement-editor": {
"label-constraints": "Constraints",
"label-position": "Position",
"loading": "Loading..."
},
"quick-positioning": {
"tooltip-align-bottom": "Align bottom",
"tooltip-align-horizontal-centers": "Align horizontal centers",
"tooltip-align-left": "Align left",
"tooltip-align-right": "Align right",
"tooltip-align-top": "Align top",
"tooltip-align-vertical-centers": "Align vertical centers"
},
"rectangle-item": {
"label": {
"bottom": "Bottom",
@ -3376,6 +3483,22 @@
"auto": "Auto"
}
},
"tree-navigation-editor": {
"clear-selection": "Clear selection",
"frame-selection": "Frame selection",
"label-add-item": "Add item",
"missing-layer": "Missing layer?",
"no-settings": "No settings",
"switcher-icon": {
"title-node-icon": "Node Icon"
}
},
"tree-node-title": {
"title-duplicate": "Duplicate",
"title-remove": "Remove",
"tooltip-duplicate": "Duplicate",
"tooltip-remove": "Remove"
},
"triangle-item": {
"label": {
"bottom": "Bottom",
@ -5484,6 +5607,25 @@
"message": "No data sources found"
}
},
"datagrid": {
"add-column": {
"placeholder-column-name": "Column name"
},
"datagrid-context-menu": {
"render-context-menu-items": {
"label-clear-column": "Clear column",
"label-clear-row": "Clear row",
"label-remove-all-data": "Remove all data",
"label-search": "Search..."
},
"render-header-menu-items": {
"label-clear-column": "Clear column",
"label-delete-column": "Delete column",
"label-rename-column": "Rename column",
"label-set-field-type": "Set field type"
}
}
},
"datasources": {
"add-new-data-source-button": {
"tooltip-no-permission": "You do not have permission to configure new data sources"
@ -5666,6 +5808,26 @@
}
}
},
"debug": {
"cursor-view": {
"no-events-yet": "No events yet"
},
"render-info-viewer": {
"data-counter": "Data: {{numDataChanges}} ",
"elapsed-time": "Time: {{elapsed}}ms",
"field": "Field",
"last": "Last",
"render-counter": "Render: {{numRenders}} ",
"schema-counter": "Schema: {{numSchemaChanges}} ",
"title-reset-counters": "Reset counters",
"tooltip-step-back": "Step back",
"type": "Type"
},
"state-view": {
"current-value": "Current value: {{currentValue}} ",
"label-state-name": "State name"
}
},
"dimensions": {
"file-dropzone-custom-children": {
"upload": "Upload"
@ -6515,10 +6677,102 @@
"incomplete-request-error": "Sorry, I was unable to complete your request. Please try again.",
"send-custom-feedback": "Send"
},
"geomap": {
"coordinates-map-view-editor": {
"label-latitude": "Latitude",
"label-longitude": "Longitude"
},
"debug-overlay": {
"center": "Center:",
"zoom": "Zoom:"
},
"fit-map-view-editor": {
"all-layers-editor-fragment": {
"label-layer": "Layer"
},
"label-data": "Data",
"last-only-editor-fragment": {
"label-padding": "Padding",
"tooltip-padding-relative-percent-beyond-extent": "Sets padding in relative percent beyond data extent"
}
},
"frame-multi-selection-editor": {
"placeholder-change-filter": "Change filter"
},
"frame-selection-editor": {
"placeholder-change-filter": "Change filter"
},
"geomap-panel": {
"aria-label-map": "Navigable map"
},
"geomap-style-rules-editor": {
"aria-label-add-geomap-style-rule": "Add geomap style rule"
},
"layers-editor": {
"label-add-layer": "Add layer",
"no-layers": "No layers?"
},
"map-view-editor": {
"label-max-zoom": "Max Zoom",
"label-view": "View",
"label-zoom": "Zoom",
"use-current-map-settings": "Use current map settings"
},
"markers-legend": {
"title-symbol": "Symbol"
},
"measure-overlay": {
"tooltip-show-measure-tools": "Show measure tools"
},
"plugin": {
"basemap-layer-configured-server-admin": "The basemap layer is configured by the server admin."
},
"style-editor": {
"label-align": "Align",
"label-baseline": "Baseline",
"label-color": "Color",
"label-fill-opacity": "Fill opacity",
"label-font-size": "Font size",
"label-opacity": "Opacity",
"label-rotation-angle": "Rotation angle",
"label-size": "Size",
"label-symbol": "Symbol",
"label-symbol-horizontal-align": "Symbol horizontal align",
"label-symbol-vertical-align": "Symbol vertical align",
"label-text-label": "Text label",
"label-x-offset": "X offset",
"label-y-offset": "Y offset"
},
"style-rule-editor": {
"aria-label-comparison-operator": "Comparison operator",
"aria-label-comparison-value": "Comparison value",
"aria-label-delete-style-rule": "Delete style rule",
"aria-label-feature-property": "Feature property",
"label-rule": "Rule",
"placeholder-feature-property": "Feature property",
"placeholder-numeric-value": "Numeric value",
"placeholder-value": "value"
}
},
"get-enterprise": {
"requires-license": "Requires a Grafana Enterprise license",
"title": "Enterprise"
},
"gettingstarted": {
"docs-card": {
"complete": "complete",
"learn-how": "Learn how in the docs"
},
"getting-started": {
"aria-label-to-advanced-tutorials": "To advanced tutorials",
"aria-label-to-basic-tutorials": "To basic tutorials",
"checking-completed-setup-steps": "Checking completed setup steps",
"remove-this-panel": "Remove this panel"
},
"tutorial-card": {
"complete": "complete"
}
},
"gops": {
"config-card": {
"text-loading-configuration": "Loading configuration...."
@ -7006,6 +7260,11 @@
"support-bundle": "You can also retrieve a support bundle containing information concerning your Grafana instance and configured datasources in the <1>support bundles section</1>.",
"troubleshooting-help": "To request troubleshooting help, send a snapshot of this panel to Grafana Labs Technical Support. The snapshot contains query response data and panel settings."
},
"histogram": {
"histogram-panel": {
"no-histogram-found-in-response": "No histogram found in response"
}
},
"inspector": {
"inspect-data-tab": {
"loading": "Loading",
@ -7317,8 +7576,26 @@
"discard-local-changes": "Discard local changes",
"title-dashboard-changed": "Dashboard changed"
},
"live-channel-editor": {
"description-grafana-live": "This supports real-time event streams in Grafana core. This feature is under heavy development. Expect the interfaces and structures to change as this becomes more production ready.",
"namespace": "Namespace",
"path": "Path",
"placeholder-select-watchable-resource": "Select watchable resource",
"scope": "Scope",
"title-grafana-live": "Grafana Live"
},
"live-connection-warning": {
"title-connection-to-server-is-lost": "Connection to server is lost..."
},
"live-panel": {
"error": "Error",
"grafana-requires-feature": "Grafana live requires a feature flag to run",
"panel-editor-channel": "Use the panel editor to pick a channel",
"title-grafana-live": "Grafana Live",
"waiting-for-data": "Waiting for data:"
},
"live-publish": {
"publish": "Publish"
}
},
"lock-icon": "lock icon",
@ -7491,6 +7768,11 @@
"scroll-top": "Scroll to top",
"start-of-range": "Start of range"
},
"logs-panel": {
"render-common-labels": {
"common-labels": "Common labels:"
}
},
"out-of-range-message": {
"end-of-the-selected-time-range": "End of the selected time range."
},
@ -8212,8 +8494,57 @@
"close": "Close drawer"
},
"link-title": "Go to Grafana labs blog",
"news-panel": {
"body-error-loading-rss-feed": "Make sure that the feed URL is correct and that CORS is configured correctly on the server. See <2>News panel documentation.</2>",
"loading": "Loading...",
"title-error-loading-rss-feed": "Error loading RSS feed"
},
"title": "Latest from the blog"
},
"nodeGraph": {
"arc-options-editor": {
"add-arc": "Add arc",
"title-remove-arc": "Remove arc"
},
"edge": {
"aria-label-from-to": "Edge from: {{from}} to: {{to}}"
},
"edge-header": {
"label-source-target": "Source → Target"
},
"marker": {
"100-node-count": ">100 nodes",
"aria-label-hidden-marker": "Hidden nodes marker: {{marker}}",
"node-count_one": "{{count}} node",
"node-count_other": "{{count}} nodes"
},
"node": {
"aria-label-node-title": "Node: {{nodeName}}"
},
"node-graph": {
"aria-label-layered-layout-performance-warning": "Layered layout performance warning",
"aria-label-nodes-hidden-warning": "Nodes hidden warning",
"computing-layout": "Computing layout",
"hidden-nodes_one": "<0></0> {{count}} node is hidden for performance reasons.",
"hidden-nodes_other": "<0></0> {{count}} nodes are hidden for performance reasons.",
"no-data": "No data",
"processed-nodes_one": "<0></0> Layered layout may be slow with {{count}} node.",
"processed-nodes_other": "<0></0> Layered layout may be slow with {{count}} nodes."
},
"node-graph-panel": {
"no-data-found-in-response": "No data found in response"
},
"node-header": {
"label-subtitle": "Subtitle",
"label-title": "Title"
},
"view-controls": {
"hide-config": "Hide config",
"show-config": "Show config",
"title-zoom-in": "Zoom in",
"title-zoom-out": "Zoom out"
}
},
"notifications": {
"empty-state": {
"description": "Notifications you have received will appear here",
@ -10002,6 +10333,12 @@
"sort-picker": {
"select-aria-label": "Sort"
},
"status-history": {
"status-history-panel": {
"too-many-points_one": "Too many points to visualize properly. <1></1>Update the query to return fewer points. <3></3>({{count}} point received)",
"too-many-points_other": "Too many points to visualize properly. <1></1>Update the query to return fewer points. <3></3>({{count}} points received)"
}
},
"support-bundles": {
"new-bundle-button": {
"new-support-bundle": "New support bundle"
@ -10027,10 +10364,35 @@
"login": "Login"
},
"table": {
"auto-cell-options-editor": {
"description-wrap-text": "If selected text will be wrapped to the width of text in the configured column",
"label-wrap-text": "Wrap text"
},
"bar-gauge-cell-options-editor": {
"label-gauge-display-mode": "Gauge display mode",
"label-value-display": "Value display"
},
"color-background-cell-options-editor": {
"description-apply-to-entire-row": "If selected the entire row will be colored as this cell would be.",
"description-wrap-text": "If selected text will be wrapped to the width of text in the configured column",
"label": {
"text-alpha": "Alpha"
},
"label-apply-to-entire-row": "Apply to entire row",
"label-background-display-mode": "Background display mode",
"label-wrap-text": "Wrap text",
"wrap-text": "Wrap text"
},
"container": {
"content": "Showing too many columns in a single table may impact performance and make data harder to read. Consider refining your queries.",
"show-all-series": "Show all columns",
"show-only-series": "Showing only {{MAX_NUMBER_OF_COLUMNS}} columns"
},
"image-cell-options-editor": {
"description-alt-text": "Alternative text that will be displayed if an image can't be displayed or for users who use a screen reader",
"description-title-text": "Text that will be displayed when the image is hovered by a cursor",
"label-alt-text": "Alt text",
"label-title-text": "Title text"
}
},
"tag-filter": {
@ -10189,6 +10551,42 @@
"select-search-input": "Type to search (country, city, abbreviation)"
}
},
"timeseries": {
"annotation-editor2": {
"add-annotation": "Add annotation",
"cancel": "Cancel",
"edit-annotation": "Edit annotation",
"label-description": "Description",
"label-tags": "Tags",
"placeholder-add-tags": "Add tags",
"save": "Save",
"saving": "Saving"
},
"annotation-tooltip2": {
"tooltip-delete": "Delete",
"tooltip-edit": "Edit"
},
"line-style-editor": {
"title-the-input-expects-a-segment-list": "The input expects a segment list",
"tooltip-help": "Help"
},
"nulls-threshold-input": {
"placeholder-never": "Never"
},
"outside-range-plugin": {
"data-outside-time-range": "Data outside time range",
"zoom-to-data": "Zoom to data"
},
"timezones-editor": {
"tooltip-add-timezone": "Add timezone",
"tooltip-remove-timezone": "Remove timezone"
}
},
"traces": {
"traces-panel": {
"no-data-found-in-response": "No data found in response"
}
},
"transformations": {
"empty": {
"add-transformation-body": "Transformations allow data to be changed in various ways before your visualization is shown.<1></1>This includes joining data together, renaming fields, making calculations, formatting data for display, and more.",
@ -10806,5 +11204,25 @@
"data-hover-view": {
"link": "Link"
}
},
"welcome": {
"welcome-banner": {
"need-help": "Need help?",
"welcome-to-grafana": "Welcome to Grafana"
}
},
"xychart": {
"series-editor": {
"add-series": "Add series",
"aria-label-select-series": "Select series {{seriesNum}}",
"label-color-field": "Color field",
"label-frame": "Frame",
"label-size-field": "Size field",
"label-x-field": "X field",
"label-y-field": "Y field",
"placeholder-all-frames": "All frames",
"placeholder-select-frame": "Select frame",
"tooltip-delete-series": "Delete series"
}
}
}

@ -12,7 +12,7 @@ module.exports = {
input: [
'../../public/**/*.{tsx,ts}',
'!../../public/app/extensions/**/*', // Don't extract from Enterprise
'!../../public/app/plugins/**/*', // Don't extract from core plugins
'!../../public/app/plugins/datasource/**/*', // Don't extract from datasource plugins
'../../packages/grafana-ui/**/*.{tsx,ts}',
],
output: './public/locales/$LOCALE/$NAMESPACE.json',

Loading…
Cancel
Save