Skip to content

Commit 7f5d5c0

Browse files
authored
chore(i18n): add ESLint rules and translate all user-facing strings (#725)
1 parent 779f150 commit 7f5d5c0

49 files changed

Lines changed: 502 additions & 172 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

eslint.config.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@ export default [
1414
},
1515
},
1616
{
17+
name: 'grafana/i18n-rules',
1718
files: ['src/**/*.{ts,tsx}'],
1819
ignores: ['src/**/*.test.{ts,tsx}', 'src/**/__tests__/**'],
1920
plugins: { '@grafana/i18n': grafanaI18nPlugin },
2021
rules: {
21-
'@grafana/i18n/no-untranslated-strings': ['warn', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
22+
'@grafana/i18n/no-untranslated-strings': ['error', { calleesToIgnore: ['^css$', 'use[A-Z].*'] }],
2223
'@grafana/i18n/no-translation-top-level': 'error',
2324
},
2425
},

src/addedComponents/InsightsTimelineWidget/InsightsTimelineWidget.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { memo, ReactElement } from 'react';
22

3+
import { t } from '@grafana/i18n';
34
import { usePluginComponent } from '@grafana/runtime';
45
import { InsightsTimelineWidgetProps } from '@grafana/plugin-types/grafana-asserts-app/';
56
import { MetricFunction } from 'utils/shared';
@@ -56,7 +57,7 @@ export const InsightsTimelineWidget = memo(function InsightsTimelineWidget({
5657
end={endTime}
5758
filterBySeverity={filterBySeverity}
5859
filterBySummaryKeywords={filterBySummaryKeywords}
59-
label={'Insights'}
60+
label={t('insights-timeline-widget.label', 'Insights')}
6061
/>
6162
</div>
6263
);

src/components/AppConfig/AppConfig.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState } from 'react';
22
import { Button, Field, useStyles2, FieldSet, Combobox } from '@grafana/ui';
3+
import { t, Trans } from '@grafana/i18n';
34
import { PluginConfigPageProps, AppPluginMeta, PluginMeta, GrafanaTheme2 } from '@grafana/data';
45
import { FetchResponse, getBackendSrv, locationService } from '@grafana/runtime';
56
import { css } from '@emotion/css';
@@ -41,10 +42,10 @@ const AppConfig = ({ plugin }: Props) => {
4142
return (
4243
<div data-testid={testIds.appConfig.container}>
4344
{/* ENABLE / DISABLE PLUGIN */}
44-
<FieldSet label="Enable / Disable">
45+
<FieldSet label={t('app-config.fieldset.enable-disable', 'Enable / Disable')}>
4546
{!enabled && (
4647
<>
47-
<div className={s.colorWeak}>The plugin is currently not enabled.</div>
48+
<div className={s.colorWeak}><Trans i18nKey="app-config.plugin-not-enabled">The plugin is currently not enabled.</Trans></div>
4849
<Button
4950
className={s.marginTop}
5051
variant="primary"
@@ -56,15 +57,15 @@ const AppConfig = ({ plugin }: Props) => {
5657
})
5758
}
5859
>
59-
Enable plugin
60+
<Trans i18nKey="app-config.enable-plugin">Enable plugin</Trans>
6061
</Button>
6162
</>
6263
)}
6364

6465
{/* Disable the plugin */}
6566
{enabled && (
6667
<>
67-
<div className={s.colorWeak}>The plugin is currently enabled.</div>
68+
<div className={s.colorWeak}><Trans i18nKey="app-config.plugin-enabled">The plugin is currently enabled.</Trans></div>
6869
<Button
6970
className={s.marginTop}
7071
variant="destructive"
@@ -76,17 +77,17 @@ const AppConfig = ({ plugin }: Props) => {
7677
})
7778
}
7879
>
79-
Disable plugin
80+
<Trans i18nKey="app-config.disable-plugin">Disable plugin</Trans>
8081
</Button>
8182
</>
8283
)}
8384
</FieldSet>
8485

8586
{/* CUSTOM SETTINGS */}
86-
<FieldSet label="Time Seeker Settings" className={s.marginTopXl}>
87+
<FieldSet label={t('app-config.fieldset.time-seeker-settings', 'Time Seeker Settings')} className={s.marginTopXl}>
8788
<Field
88-
label="Query Range"
89-
description="The time range for each cached batch in the Time Seeker. Larger values mean fewer queries but more data per query."
89+
label={t('app-config.field.query-range', 'Query Range')}
90+
description={t('app-config.field.query-range-description', 'The time range for each cached batch in the Time Seeker. Larger values mean fewer queries but more data per query.')}
9091
>
9192
<Combobox<number>
9293
width={30}
@@ -112,7 +113,7 @@ const AppConfig = ({ plugin }: Props) => {
112113
})
113114
}
114115
>
115-
Save settings
116+
<Trans i18nKey="app-config.save-settings">Save settings</Trans>
116117
</Button>
117118
</div>
118119
</FieldSet>

src/components/Explore/AttributesSidebar.tsx

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useMemo, useState, useEffect, useCallback } from 'react';
33

44
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
55
import { TabsBar, Tab, Input, Icon, IconButton, useStyles2, Badge, Checkbox, Button, useTheme2 } from '@grafana/ui';
6+
import { t, Trans } from '@grafana/i18n';
67
import { RESOURCE_ATTR, SPAN_ATTR, ignoredAttributes } from 'utils/shared';
78
import { getFiltersVariable } from 'utils/utils';
89
import { SceneObject } from '@grafana/scenes';
@@ -300,13 +301,13 @@ export function AttributesSidebar({
300301
};
301302

302303
const scopeButtons = [
303-
{ label: 'All', value: 'All' as ScopeType },
304-
{ label: 'Resource', value: 'Resource' as ScopeType },
305-
{ label: 'Span', value: 'Span' as ScopeType },
304+
{ label: t('attributes-sidebar.scope.all', 'All'), value: 'All' as ScopeType },
305+
{ label: t('attributes-sidebar.scope.resource', 'Resource'), value: 'Resource' as ScopeType },
306+
{ label: t('attributes-sidebar.scope.span', 'Span'), value: 'Span' as ScopeType },
306307
];
307308

308309
if (showFavorites) {
309-
scopeButtons.unshift({ label: 'Favorites', value: 'Favorites' as ScopeType });
310+
scopeButtons.unshift({ label: t('attributes-sidebar.scope.favorites', 'Favorites'), value: 'Favorites' as ScopeType });
310311
}
311312

312313
return (
@@ -319,18 +320,18 @@ export function AttributesSidebar({
319320
<div className={styles.selectedAttributeLabel}>
320321
{isMulti ? (
321322
<>
322-
<strong>Selected ({getSelectedAttributes().length}):</strong>{' '}
323-
{getSelectedAttributes().length > 0 ? getSelectedAttributes().join(', ') : 'None'}
323+
<strong>{t('attributes-sidebar.selected-count', 'Selected ({{count}}):', { count: getSelectedAttributes().length })}</strong>{' '}
324+
{getSelectedAttributes().length > 0 ? getSelectedAttributes().join(', ') : t('attributes-sidebar.none', 'None')}
324325
</>
325326
) : (
326327
<>
327-
<strong>Selected:</strong> {selected}
328+
<strong><Trans i18nKey="attributes-sidebar.selected">Selected:</Trans></strong> {selected}
328329
</>
329330
)}
330331
</div>
331332
{allowAllOption && selected !== 'All' && (
332333
<Button variant="secondary" size="sm" onClick={() => handleAttributeSelect('All')}>
333-
All
334+
<Trans i18nKey="attributes-sidebar.all-button">All</Trans>
334335
</Button>
335336
)}
336337
</div>
@@ -340,7 +341,7 @@ export function AttributesSidebar({
340341
<Input
341342
className={styles.searchInput}
342343
prefix={<Icon name="search" />}
343-
placeholder="Search attributes..."
344+
placeholder={t('attributes-sidebar.search-placeholder', 'Search attributes...')}
344345
value={searchValue}
345346
onChange={(e) => setSearchValue(e.currentTarget.value)}
346347
onKeyDown={handleSearchKeyDown}
@@ -349,7 +350,7 @@ export function AttributesSidebar({
349350
<IconButton
350351
name="times"
351352
variant="secondary"
352-
tooltip="Clear search"
353+
tooltip={t('attributes-sidebar.clear-search', 'Clear search')}
353354
onClick={() => setSearchValue('')}
354355
/>
355356
)
@@ -377,7 +378,7 @@ export function AttributesSidebar({
377378
<ul className={styles.attributesList} onDragOver={handleListDragOver} onDragLeave={handleListDragLeave}>
378379
{filteredAttributes.length === 0 ? (
379380
<div className={styles.emptyState}>
380-
{searchValue || selectedScope !== 'All' ? 'No attributes match your criteria' : 'No attributes available'}
381+
{searchValue || selectedScope !== 'All' ? t('attributes-sidebar.no-match', 'No attributes match your criteria') : t('attributes-sidebar.no-available', 'No attributes available')}
381382
</div>
382383
) : (
383384
filteredAttributes.map((attribute, index) => {
@@ -394,7 +395,7 @@ export function AttributesSidebar({
394395
{/* Ghost element above */}
395396
{showGhostAbove && (
396397
<li className={styles.ghostElement} onDrop={() => handleDrop(index)}>
397-
<div className={styles.ghostContent}>Drop here</div>
398+
<div className={styles.ghostContent}><Trans i18nKey="attributes-sidebar.drop-here">Drop here</Trans></div>
398399
</li>
399400
)}
400401

@@ -436,7 +437,7 @@ export function AttributesSidebar({
436437
variant="secondary"
437438
size="sm"
438439
className={`${styles.starButton} ${isFavorites ? styles.starButtonActive : ''}`}
439-
tooltip={isFavorites ? 'Remove from favorites' : 'Add to favorites'}
440+
tooltip={isFavorites ? t('attributes-sidebar.remove-from-favorites', 'Remove from favorites') : t('attributes-sidebar.add-to-favorites', 'Add to favorites')}
440441
onClick={(event) => toggleStar(attribute.value, event)}
441442
/>
442443
)}
@@ -445,7 +446,7 @@ export function AttributesSidebar({
445446
{/* Ghost element below */}
446447
{showGhostBelow && (
447448
<li className={styles.ghostElement} onDrop={() => handleDrop(index)}>
448-
<div className={styles.ghostContent}>Drop here</div>
449+
<div className={styles.ghostContent}><Trans i18nKey="attributes-sidebar.drop-here-below">Drop here</Trans></div>
449450
</li>
450451
)}
451452
</React.Fragment>

src/components/Explore/ByFrameRepeater.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22

33
import { DataFrame, FieldType, GrafanaTheme2, LoadingState, PanelData, sortDataFrame } from '@grafana/data';
4+
import { t } from '@grafana/i18n';
45
import {
56
SceneComponentProps,
67
SceneCSSGridLayout,
@@ -127,7 +128,7 @@ export class ByFrameRepeater extends SceneObjectBase<ByFrameRepeaterState> {
127128
children: [
128129
new SceneFlexItem({
129130
body: new EmptyStateScene({
130-
message: 'No data for search term',
131+
message: t('by-frame-repeater.no-data-for-search', 'No data for search term'),
131132
padding: '32px',
132133
}),
133134
}),

src/components/Explore/LayoutSwitcher.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22

33
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
4+
import { Trans } from '@grafana/i18n';
45
import { SceneComponentProps, SceneObject, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
56
import { Label, RadioButtonGroup, Stack, useStyles2 } from '@grafana/ui';
67
import { reportAppInteraction, USER_EVENTS_ACTIONS, USER_EVENTS_PAGES } from '../../utils/analytics';
@@ -21,7 +22,7 @@ export class LayoutSwitcher extends SceneObjectBase<LayoutSwitcherState> {
2122

2223
return (
2324
<Stack>
24-
<Label className={styles.label}>View</Label>
25+
<Label className={styles.label}><Trans i18nKey="layout-switcher.view-label">View</Trans></Label>
2526
<RadioButtonGroup options={options} value={active} onChange={model.onLayoutChange} />
2627
</Stack>
2728
);

src/components/Explore/SavedSearches/LoadSearchModal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { css } from '@emotion/css';
55
import { dateTime, GrafanaTheme2 } from '@grafana/data';
66
import { reportInteraction } from '@grafana/runtime';
77
import { SceneObject } from '@grafana/scenes';
8+
import { t, Trans } from '@grafana/i18n';
89
import { Modal, Box, useStyles2, Stack, Text, Divider, ScrollContainer, IconButton, Button } from '@grafana/ui';
910

1011
import { SavedSearch, useSavedSearches, applySavedSearchToScene } from './saveSearch';
@@ -64,10 +65,10 @@ export function LoadSearchModal({ onClose, sceneRef }: Props) {
6465
}, [onClose, sceneRef, selectedSearch]);
6566

6667
return (
67-
<Modal title="Load a previously saved search" isOpen={true} onDismiss={onClose}>
68+
<Modal title={t('load-search-modal.title', 'Load a previously saved search')} isOpen={true} onDismiss={onClose}>
6869
{!isLoading && searches.length === 0 && (
6970
<Box backgroundColor="secondary" padding={1.5} marginBottom={2}>
70-
<Text variant="body">No saved searches to display.</Text>
71+
<Text variant="body"><Trans i18nKey="load-search-modal.no-saved-searches">No saved searches to display.</Trans></Text>
7172
</Box>
7273
)}
7374
{searches.length > 0 && (
@@ -113,10 +114,10 @@ export function LoadSearchModal({ onClose, sceneRef }: Props) {
113114
size="xl"
114115
name="trash-alt"
115116
onClick={onDelete}
116-
tooltip="Remove"
117+
tooltip={t('load-search-modal.remove-tooltip', 'Remove')}
117118
/>
118119
<Button onClick={onSelectClick} variant="primary">
119-
Select
120+
<Trans i18nKey="load-search-modal.select-button">Select</Trans>
120121
</Button>
121122
</Stack>
122123
</Box>

src/components/Explore/SavedSearches/LoadSearchScene.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { css } from '@emotion/css';
55
import { AppEvents, GrafanaTheme2 } from '@grafana/data';
66
import { getAppEvents, reportInteraction, usePluginComponent } from '@grafana/runtime';
77
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
8+
import { t } from '@grafana/i18n';
89
import { ToolbarButton, useStyles2 } from '@grafana/ui';
910

1011
import { LoadSearchModal } from './LoadSearchModal';
@@ -78,7 +79,7 @@ export class LoadSearchScene extends SceneObjectBase<LoadSearchSceneState> {
7879
onClick={model.toggleOpen}
7980
className={styles.button}
8081
tooltip={
81-
hasSavedSearches ? 'Load saved search' : 'No saved searches to load'
82+
hasSavedSearches ? t('load-search-scene.load-saved-search', 'Load saved search') : t('load-search-scene.no-saved-searches', 'No saved searches to load')
8283
}
8384
/>
8485
{isOpen && <LoadSearchModal sceneRef={model} onClose={model.toggleClosed} />}
@@ -125,7 +126,7 @@ export class LoadSearchScene extends SceneObjectBase<LoadSearchSceneState> {
125126
datasourceFilters={[dsName]}
126127
icon="folder-open"
127128
onSelectQuery={onSelectQuery}
128-
tooltip="Load saved query"
129+
tooltip={t('load-search-scene.load-saved-query', 'Load saved query')}
129130
/>
130131
);
131132
};

src/components/Explore/SavedSearches/SaveSearchButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useMemo, useState } from 'react';
33
import { usePluginComponent } from '@grafana/runtime';
44
import { SceneObject } from '@grafana/scenes';
55
import { DataQuery } from '@grafana/schema';
6+
import { t } from '@grafana/i18n';
67
import { ToolbarButton } from '@grafana/ui';
78

89
import { getDatasourceVariable, getFiltersVariable, getTraceExplorationScene } from '../../../utils/utils';
@@ -42,7 +43,7 @@ export function SaveSearchButton({ sceneRef }: Props) {
4243
variant="canvas"
4344
icon="save"
4445
onClick={() => setSaving(true)}
45-
tooltip="Save search"
46+
tooltip={t('save-search-button.save-search', 'Save search')}
4647
/>
4748
{saving && <SaveSearchModal dsUid={dsUid} sceneRef={sceneRef} onClose={() => setSaving(false)} />}
4849
</>
@@ -81,7 +82,7 @@ export function SaveSearchButton({ sceneRef }: Props) {
8182
<OpenQueryLibraryComponent
8283
datasourceFilters={[dsName]}
8384
query={query}
84-
tooltip="Save in Saved Queries"
85+
tooltip={t('save-search-button.save-in-saved-queries', 'Save in Saved Queries')}
8586
/>
8687
);
8788
}

src/components/Explore/SavedSearches/SaveSearchModal.tsx

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppEvents, GrafanaTheme2 } from '@grafana/data';
66
import { getAppEvents, reportInteraction } from '@grafana/runtime';
77
import { SceneObject } from '@grafana/scenes';
88
import { Modal, Button, Box, Field, Input, Stack, useStyles2, Alert } from '@grafana/ui';
9+
import { t, Trans } from '@grafana/i18n';
910

1011
import { getFiltersVariable, getTraceExplorationScene } from '../../../utils/utils';
1112
import { renderTraceQLLabelFilters } from '../../../utils/filters-renderer';
@@ -69,9 +70,9 @@ export function SaveSearchModal({ dsUid, onClose, sceneRef }: Props) {
6970
);
7071

7172
return (
72-
<Modal title="Save current search" isOpen={true} onDismiss={onClose}>
73+
<Modal title={t('save-search-modal.title', 'Save current search')} isOpen={true} onDismiss={onClose}>
7374
<Alert title="" severity="info">
74-
Saved searches are stored locally in your browser and will only be available on this device.
75+
<Trans i18nKey="save-search-modal.info-alert">Saved searches are stored locally in your browser and will only be available on this device.</Trans>
7576
</Alert>
7677
<Box marginBottom={2}>
7778
<code className={styles.query}>{query}</code>
@@ -82,10 +83,10 @@ export function SaveSearchModal({ dsUid, onClose, sceneRef }: Props) {
8283
<Box flex={1} marginBottom={2}>
8384
{existingSearch && (
8485
<Alert title="" severity="warning">
85-
There is a previously saved search with the same query: {existingSearch.title}
86+
<Trans i18nKey="save-search-modal.existing-search-warning">There is a previously saved search with the same query: {{ title: existingSearch.title }}</Trans>
8687
</Alert>
8788
)}
88-
<Field label="Title" noMargin htmlFor="save-search-title">
89+
<Field label={t('save-search-modal.field.title', 'Title')} noMargin htmlFor="save-search-title">
8990
<Input
9091
id="save-search-title"
9192
required
@@ -96,7 +97,7 @@ export function SaveSearchModal({ dsUid, onClose, sceneRef }: Props) {
9697
</Field>
9798
</Box>
9899
<Box flex={1} marginBottom={2}>
99-
<Field label="Description" noMargin htmlFor="save-search-description">
100+
<Field label={t('save-search-modal.field.description', 'Description')} noMargin htmlFor="save-search-description">
100101
<Input
101102
id="save-search-description"
102103
value={description}
@@ -108,21 +109,21 @@ export function SaveSearchModal({ dsUid, onClose, sceneRef }: Props) {
108109
</Stack>
109110
<Modal.ButtonRow>
110111
<Button variant="secondary" fill="outline" onClick={onClose} disabled={state === 'saving'}>
111-
Cancel
112+
<Trans i18nKey="save-search-modal.cancel">Cancel</Trans>
112113
</Button>
113114
<Button type="submit" disabled={!title || state === 'saving'}>
114-
Save
115+
<Trans i18nKey="save-search-modal.save">Save</Trans>
115116
</Button>
116117
</Modal.ButtonRow>
117118
</form>
118119
) : (
119120
<>
120-
<Alert title="Success" severity="success">
121-
Search successfully saved.
121+
<Alert title={t('save-search-modal.success-title', 'Success')} severity="success">
122+
<Trans i18nKey="save-search-modal.success-message">Search successfully saved.</Trans>
122123
</Alert>
123124
<Modal.ButtonRow>
124125
<Button variant="secondary" fill="outline" onClick={onClose}>
125-
Close
126+
<Trans i18nKey="save-search-modal.close">Close</Trans>
126127
</Button>
127128
</Modal.ButtonRow>
128129
</>

0 commit comments

Comments
 (0)