Internationalisation: More markup for translations (#103392)

* some more translations

* more markup

* more translations

* fix tests
This commit is contained in:
Ashley Harrison
2025-04-07 09:48:03 +01:00
committed by GitHub
parent 3c3e5754b3
commit 0fc4f53fab
52 changed files with 391 additions and 301 deletions
+4 -172
View File
@@ -654,19 +654,15 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"packages/grafana-ui/src/components/Table/TableNG/Cells/TableCellNG.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"packages/grafana-ui/src/components/Table/TableNG/Filter/Filter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/components/Table/TableNG/Filter/FilterPopup.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"packages/grafana-ui/src/components/Table/TableNG/Filter/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@@ -685,8 +681,7 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Table/TableNG/TableNG.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"packages/grafana-ui/src/components/Table/TableNG/utils.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@@ -850,68 +845,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not re-export imported variable (\`updateConfigurationSubtitle\`)", "2"],
[0, 0, 0, "Do not re-export imported variable (\`updateNavIndex\`)", "3"]
],
"public/app/core/components/AccessControl/AddPermission.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
],
"public/app/core/components/AccessControl/PermissionListItem.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/AccessControl/index.ts:5381": [
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"]
],
"public/app/core/components/AppChrome/MegaMenu/MegaMenuItem.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/News/NewsDrawer.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/News/NewsWrapper.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/OrganizationSwitcher/OrganizationSelect.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/TopBar/ProfileButton.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/Breadcrumbs/Breadcrumbs.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/CloseButton/CloseButton.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/DynamicImports/SafeDynamicImport.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/core/components/FolderFilter/FolderFilter.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
],
"public/app/core/components/ForgottenPassword/ChangePassword.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
],
"public/app/core/components/ForgottenPassword/ForgottenPassword.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
],
"public/app/core/components/FormPrompt/FormPrompt.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/GraphNG/GraphNG.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@@ -920,103 +859,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/core/components/Layers/LayerDragDropList.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"]
],
"public/app/core/components/Layers/LayerName.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/LocalStorageValueProvider/index.tsx:5381": [
[0, 0, 0, "Do not re-export imported variable (\`./LocalStorageValueProvider\`)", "0"]
],
"public/app/core/components/Login/PasswordlessConfirmationForm.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/OptionsUI/color.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/OptionsUI/fieldColor.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/OptionsUI/units.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/Page/EditableTitle.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/PluginHelp/PluginHelp.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/QueryOperationRow/QueryOperationRowHeader.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/RolePicker/RoleMenuGroupOption.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/RolePicker/RoleMenuOption.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/RolePicker/RolePickerMenu.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/RolePicker/RolePickerSubMenu.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/RolePickerDrawer/RolePickerBadges.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/Select/OldFolderPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/Select/OrgPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/Select/ServiceAccountPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/Select/SortPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/core/components/Select/TeamPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/Select/UserPicker.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/components/Signup/SignupPage.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "8"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "9"]
],
"public/app/core/components/Signup/VerifyEmail.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"]
],
"public/app/core/components/TagFilter/TagFilter.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/core/components/TagFilter/TagOption.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/core/components/TimeSeries/utils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -1037,10 +884,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not re-export imported variable (\`profiler\`)", "6"],
[0, 0, 0, "Do not re-export imported variable (\`updateLegendValues\`)", "7"]
],
"public/app/core/navigation/GrafanaRouteError.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/core/navigation/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@@ -1136,10 +979,6 @@ exports[`better eslint`] = {
"public/app/features/actions/ParamsEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/admin/AdminFeatureTogglesTable.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
],
"public/app/features/admin/UpgradePage.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
@@ -1148,13 +987,6 @@ exports[`better eslint`] = {
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "4"],
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "5"]
],
"public/app/features/admin/Users/OrgUsersTable.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
],
"public/app/features/admin/Users/UsersTable.tsx:5381": [
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
],
"public/app/features/alerting/routes.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
+2 -2
View File
@@ -27,7 +27,7 @@ describe('Pin nav items', () => {
bookmarkPinIcon.click({ force: true });
// Check if the Administration menu item is visible in the Bookmarks section
cy.get('[aria-label="Expand section Bookmarks"]').click();
cy.get('[aria-label="Expand section: Bookmarks"]').click();
const bookmarks = cy.get('[href="/bookmarks"]').parentsUntil('li').siblings('ul');
bookmarks.within(() => {
cy.get('a').should('contain.text', 'Administration');
@@ -46,7 +46,7 @@ describe('Pin nav items', () => {
// Check if the Bookmark section is visible and open it
cy.get('[href="/bookmarks"]').should('be.visible');
cy.get('[aria-label="Expand section Bookmarks"]').click();
cy.get('[aria-label="Expand section: Bookmarks"]').click();
// Check if the Administration menu item is visible in the Bookmarks section
const bookmarks = cy.get('[href="/bookmarks"]').parentsUntil('li').siblings('ul').children();
@@ -186,7 +186,7 @@ export function TableCellNG(props: TableCellNGProps) {
{cellInspect && (
<IconButton
name="eye"
tooltip="Inspect value"
tooltip={t('grafana-ui.table.cell-inspect-tooltip', 'Inspect value')}
onClick={() => {
setContextMenuProps({
value: String(value ?? ''),
@@ -5,7 +5,7 @@ import { Field, GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Button, ButtonSelect, ClickOutsideWrapper, FilterInput, Label, Stack } from '../../..';
import { useStyles2, useTheme2 } from '../../../../themes';
import { Trans } from '../../../../utils/i18n';
import { t, Trans } from '../../../../utils/i18n';
import { FilterType } from '../types';
import { FilterList } from './FilterList';
@@ -118,7 +118,11 @@ export const FilterPopup = ({
<div className={styles.listDivider} />
<Stack gap={1}>
<FilterInput placeholder="Filter values" onChange={setSearchFilter} value={searchFilter} />
<FilterInput
placeholder={t('grafana-ui.table.filter-popup-input-placeholder', 'Filter values')}
onChange={setSearchFilter}
value={searchFilter}
/>
<Button
variant="secondary"
style={{ color: matchCase ? theme.colors.text.link : theme.colors.text.disabled }}
@@ -26,7 +26,7 @@ import {
import { TableCellDisplayMode } from '@grafana/schema';
import { useStyles2, useTheme2 } from '../../../themes';
import { Trans } from '../../../utils/i18n';
import { t, Trans } from '../../../utils/i18n';
import { ContextMenu } from '../../ContextMenu/ContextMenu';
import { MenuItem } from '../../Menu/MenuItem';
import { Pagination } from '../../Pagination/Pagination';
@@ -423,7 +423,7 @@ export function TableNG(props: TableNGProps) {
return (
<>
<MenuItem
label="Inspect value"
label={t('grafana-ui.table.inspect-menu-label', 'Inspect value')}
onClick={() => {
setIsInspecting(true);
}}
@@ -67,7 +67,10 @@ export const AddPermission = ({
(PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole));
return (
<div className="cta-form" aria-label="Permissions slider">
<div
className="cta-form"
aria-label={t('access-control.add-permission.permissions-aria-label', 'Permissions slider')}
>
<CloseButton onClick={onCancel} />
<h5>{title}</h5>
@@ -80,7 +83,7 @@ export const AddPermission = ({
>
<Stack gap={1} direction="row">
<Select
aria-label="Role to add new permission to"
aria-label={t('access-control.add-permission.role-select-aria-label', 'Role to add new permission to')}
value={target}
options={targetOptions}
onChange={(v) => setPermissionTarget(v.value!)}
@@ -98,7 +101,7 @@ export const AddPermission = ({
{target === PermissionTarget.BuiltInRole && (
<Select
aria-label={'Built-in role picker'}
aria-label={t('access-control.add-permission.built-in-aria-label', 'Built-in role picker')}
options={Object.values(OrgRole)
.filter((r) => r !== OrgRole.None)
.map((r) => ({ value: r, label: r }))}
@@ -108,7 +111,7 @@ export const AddPermission = ({
)}
<Select
aria-label="Permission Level"
aria-label={t('access-control.add-permission.level-aria-label', 'Permission level')}
width="auto"
value={permissions.find((p) => p === permission)}
options={permissions.map((p) => ({ label: p, value: p }))}
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { Box, Button, Icon, Select, Tooltip, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { ResourcePermission } from './types';
@@ -62,11 +62,21 @@ export const PermissionListItem = ({ item, permissionLevels, canSet, onRemove, o
variant="destructive"
disabled={!canSet}
onClick={() => onRemove(item)}
aria-label={`Remove permission for ${getName(item)}`}
aria-label={t(
'access-control.permission-list-item.remove-aria-label',
'Remove permission for {{identifier}}',
{
identifier: getName(item),
}
)}
/>
) : (
<Tooltip content={item.isInherited ? 'Inherited Permission' : 'Provisioned Permission'}>
<Button size="sm" icon="lock" aria-label="Locked permission indicator" />
<Button
size="sm"
icon="lock"
aria-label={t('access-control.permission-list-item.locked-aria-label', 'Locked permission indicator')}
/>
</Tooltip>
)}
</td>
@@ -48,16 +48,16 @@ describe('MegaMenu', () => {
it('should render children', async () => {
setup();
await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' }));
await userEvent.click(await screen.findByRole('button', { name: 'Expand section: Section name' }));
expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument();
});
it('should render grandchildren', async () => {
setup();
await userEvent.click(await screen.findByRole('button', { name: 'Expand section Section name' }));
await userEvent.click(await screen.findByRole('button', { name: 'Expand section: Section name' }));
expect(await screen.findByRole('link', { name: 'Child1' })).toBeInTheDocument();
await userEvent.click(await screen.findByRole('button', { name: 'Expand section Child1' }));
await userEvent.click(await screen.findByRole('button', { name: 'Expand section: Child1' }));
expect(await screen.findByRole('link', { name: 'Grandchild1' })).toBeInTheDocument();
expect(await screen.findByRole('link', { name: 'Child2' })).toBeInTheDocument();
});
@@ -8,6 +8,7 @@ import { FeatureState, GrafanaTheme2, NavModelItem, toIconName } from '@grafana/
import { useStyles2, Text, IconButton, Icon, Stack, FeatureBadge } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { t } from '../../../internationalization';
import { Indent } from '../../Indent/Indent';
import { FeatureHighlight } from './FeatureHighlight';
@@ -114,7 +115,15 @@ export function MegaMenuItem({ link, activeItem, level = 0, onClick, onPin, isPi
<div className={styles.collapseButtonWrapper}>
{showExpandButton && (
<IconButton
aria-label={`${sectionExpanded ? 'Collapse' : 'Expand'} section ${link.text}`}
aria-label={
sectionExpanded
? t('navigation.megamenu-item.collapse-aria-label', 'Collapse section: {{sectionName}}', {
sectionName: link.text,
})
: t('navigation.megamenu-item.expand-aria-label', 'Expand section: {{sectionName}}', {
sectionName: link.text,
})
}
className={styles.collapseButton}
onClick={() => setSectionExpanded(!sectionExpanded)}
name={getIconName(Boolean(sectionExpanded))}
@@ -25,7 +25,7 @@ export function NewsContainer({ onClose }: NewsContainerProps) {
href="https://grafana.com/blog/"
target="_blank"
rel="noreferrer"
title="Go to Grafana labs blog"
title={t('news.link-title', 'Go to Grafana labs blog')}
className={styles.grot}
>
<img src="public/img/grot-news.svg" alt="Grot reading news" />
@@ -36,7 +36,7 @@ export function NewsContainer({ onClose }: NewsContainerProps) {
variant="secondary"
onClick={onClose}
data-testid={selectors.components.Drawer.General.close}
tooltip={t(`news.drawer.close`, 'Close Drawer')}
tooltip={t(`news.drawer.close`, 'Close drawer')}
/>
</div>
</div>
@@ -7,6 +7,8 @@ import { useStyles2 } from '@grafana/ui';
import { News } from 'app/plugins/panel/news/component/News';
import { useNewsFeed } from 'app/plugins/panel/news/useNewsFeed';
import { t } from '../../../internationalization';
interface NewsWrapperProps {
feedUrl: string;
}
@@ -42,7 +44,12 @@ export function NewsWrapper({ feedUrl }: NewsWrapperProps) {
</>
)}
<div className={styles.grot}>
<a href="https://grafana.com/blog/" target="_blank" rel="noreferrer" title="Go to Grafana labs blog">
<a
href="https://grafana.com/blog/"
target="_blank"
rel="noreferrer"
title={t('news.link-title', 'Go to Grafana labs blog')}
>
<img src="public/img/grot-news.svg" alt="Grot reading news" />
</a>
</div>
@@ -6,6 +6,8 @@ import { Icon, Select, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { UserOrg } from 'app/types';
import { t } from '../../../internationalization';
import { OrganizationBaseProps } from './types';
export function OrganizationSelect({ orgs, onSelectChange }: OrganizationBaseProps) {
@@ -32,7 +34,7 @@ export function OrganizationSelect({ orgs, onSelectChange }: OrganizationBasePro
return (
<Select<UserOrg>
aria-label="Change organization"
aria-label={t('navigation.org-switcher.aria-label', 'Change organization')}
width={'auto'}
value={value}
prefix={<Icon className="prefix-icon" name="building" />}
@@ -7,6 +7,7 @@ import { Menu, Dropdown, useStyles2, useTheme2, ToolbarButton } from '@grafana/u
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
import { useSelector } from 'app/types';
import { t } from '../../../internationalization';
import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
import { findCreateActions } from './utils';
@@ -53,7 +54,7 @@ export const QuickAdd = ({}: Props) => {
iconOnly
icon={isSmallScreen ? 'plus-circle' : 'plus'}
isOpen={isSmallScreen ? undefined : isOpen}
aria-label="New"
aria-label={t('navigation.quick-add.aria-label', 'New')}
/>
</Dropdown>
<NavToolbarSeparator className={styles.separator} />
@@ -61,7 +61,7 @@ export function ProfileButton({ profileNode }: Props) {
className={styles.profileButton}
imgSrc={contextSrv.user.gravatarUrl}
imgAlt="User avatar"
aria-label="Profile"
aria-label={t('navigation.profile.aria-label', 'Profile')}
/>
</Dropdown>
{showNewsDrawer && <NewsContainer onClose={onToggleShowNewsDrawer} />}
@@ -78,14 +78,14 @@ export const SingleTopBar = memo(function SingleTopBar({
<QuickAdd />
{enrichedHelpNode && (
<Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
<ToolbarButton iconOnly icon="question-circle" aria-label="Help" />
<ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
</Dropdown>
)}
<ToolbarButton
icon="monitor"
className={styles.kioskToggle}
onClick={onToggleKioskMode}
tooltip="Enable kiosk mode"
tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
/>
{!contextSrv.user.isSignedIn && <SignInLink />}
{config.featureToggles.inviteUserExperimental && <InviteUserButton />}
@@ -3,6 +3,8 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
import { BreadcrumbItem } from './BreadcrumbItem';
import { Breadcrumb } from './types';
@@ -15,7 +17,7 @@ export function Breadcrumbs({ breadcrumbs, className }: Props) {
const styles = useStyles2(getStyles);
return (
<nav aria-label="Breadcrumbs" className={className}>
<nav aria-label={t('navigation.breadcrumbs.aria-label', 'Breadcrumbs')} className={className}>
<ol className={styles.breadcrumbs}>
{breadcrumbs.map((breadcrumb, index) => (
<BreadcrumbItem
@@ -4,6 +4,8 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
type Props = {
onClick: () => void;
'aria-label'?: string;
@@ -19,7 +21,7 @@ export const CloseButton = ({ onClick, 'aria-label': ariaLabel, style }: Props)
name="times"
onClick={onClick}
style={style}
tooltip="Close"
tooltip={t('close-button.tooltip', 'Close')}
/>
);
};
@@ -5,7 +5,7 @@ import { useCallback, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { AsyncMultiSelect, Icon, Button, useStyles2 } from '@grafana/ui';
import { config } from 'app/core/config';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
import { FolderInfo, PermissionLevelString } from 'app/types';
@@ -34,14 +34,7 @@ export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JS
return (
<div className={styles.container}>
{value.length > 0 && (
<Button
size="xs"
icon="trash-alt"
fill="text"
className={styles.clear}
onClick={() => onChange([])}
aria-label="Clear folders"
>
<Button size="xs" icon="trash-alt" fill="text" className={styles.clear} onClick={() => onChange([])}>
<Trans i18nKey="folder-filter.clear-folder-button">Clear folders</Trans>
</Button>
)}
@@ -51,10 +44,10 @@ export function FolderFilter({ onChange, maxMenuHeight }: FolderFilterProps): JS
isLoading={loading}
loadOptions={debouncedLoadOptions}
maxMenuHeight={maxMenuHeight}
placeholder="Filter by folder"
placeholder={t('folder-filter.select-placeholder', 'Filter by folder')}
noOptionsMessage="No folders found"
prefix={<Icon name="filter" />}
aria-label="Folder filter"
aria-label={t('folder-filter.select-aria-label', 'Folder filter')}
defaultOptions
/>
</div>
@@ -4,7 +4,7 @@ import { useForm } from 'react-hook-form';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { Tooltip, Field, Button, Alert, useStyles2, Stack } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { getStyles } from '../Login/LoginForm';
import { PasswordField } from '../PasswordField/PasswordField';
@@ -50,9 +50,19 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }:
return (
<form onSubmit={handleSubmit(submit)}>
{showDefaultPasswordWarning && (
<Alert severity="info" title="Continuing to use the default password exposes you to security risks." />
<Alert
severity="info"
title={t(
'forgot-password.change-password.default-password-alert',
'Continuing to use the default password exposes you to security risks.'
)}
/>
)}
<Field label="New password" invalid={!!errors.newPassword} error={errors?.newPassword?.message}>
<Field
label={t('forgot-password.change-password.new-password-label', 'New password')}
invalid={!!errors.newPassword}
error={errors?.newPassword?.message}
>
<PasswordField
onFocus={() => setDisplayValidationLabels(true)}
{...register('newPassword', {
@@ -72,7 +82,11 @@ export const ChangePassword = ({ onSubmit, onSkip, showDefaultPasswordWarning }:
strongPasswordValidations={strongPasswordValidations}
/>
)}
<Field label="Confirm new password" invalid={!!errors.confirmNew} error={errors?.confirmNew?.message}>
<Field
label={t('forgot-password.change-password.confirm-label', 'Confirm new password')}
invalid={!!errors.confirmNew}
error={errors?.confirmNew?.message}
>
<PasswordField
{...register('confirmNew', {
required: 'Confirmed Password is required',
@@ -6,7 +6,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Field, Input, Button, Legend, Container, useStyles2, LinkButton, Stack } from '@grafana/ui';
import config from 'app/core/config';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
interface EmailDTO {
userOrEmail: string;
@@ -59,14 +59,17 @@ export const ForgottenPassword = () => {
<Trans i18nKey="forgot-password.reset-password-header">Reset password</Trans>
</Legend>
<Field
label="User"
description="Enter your information to get a reset link sent to you"
label={t('forgot-password.user-field-label', 'User')}
description={t(
'forgot-password.user-field-description',
'Enter your information to get a reset link sent to you'
)}
invalid={!!errors.userOrEmail}
error={errors?.userOrEmail?.message}
>
<Input
id="user-input"
placeholder="Email or username"
placeholder={t('forgot-password.user-field-placeholder', 'Email or username')}
{...register('userOrEmail', { required: 'Email or username is required' })}
/>
</Field>
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom-v5-compat';
import { Button, Modal } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { Prompt } from './Prompt';
@@ -99,7 +99,7 @@ const UnsavedChangesModal = ({ onDiscard, onBackToForm, isOpen }: UnsavedChanges
return (
<Modal
isOpen={isOpen}
title="Leave page?"
title={t('form-prompt.title', 'Leave page?')}
onDismiss={onBackToForm}
icon="exclamation-triangle"
className={css({ width: '500px' })}
@@ -38,13 +38,13 @@ describe('LayerDragDropList', () => {
it('renders draggable icon', () => {
renderScenario({});
expect(screen.getAllByLabelText('Drag and drop icon').length).toEqual(2);
expect(screen.getAllByLabelText('Drag and drop to reorder').length).toEqual(2);
});
it('does not render draggable icon', () => {
renderScenario({ excludeBaseLayer: true });
expect(screen.queryAllByLabelText('Drag and drop icon').length).toEqual(0);
expect(screen.queryAllByLabelText('Drag and drop to reorder').length).toEqual(0);
});
function renderScenario(overrides: Partial<LayerDragDropListProps<testLayer>>) {
@@ -4,6 +4,8 @@ import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, IconButton, useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
import { LayerName } from './LayerName';
import { LayerElement } from './types';
@@ -81,7 +83,7 @@ export const LayerDragDropList = <T extends LayerElement>({
{onDuplicate ? (
<IconButton
name="copy"
tooltip="Duplicate"
tooltip={t('layers.layer-drag-drop-list.duplicate-tooltip', 'Duplicate')}
className={style.actionIcon}
onClick={() => onDuplicate(element)}
/>
@@ -89,7 +91,7 @@ export const LayerDragDropList = <T extends LayerElement>({
<IconButton
name="trash-alt"
tooltip="Remove"
tooltip={t('layers.layer-drag-drop-list.remove-tooltip', 'Remove')}
className={cx(style.actionIcon, style.dragIcon)}
onClick={() => onDelete(element)}
/>
@@ -97,8 +99,10 @@ export const LayerDragDropList = <T extends LayerElement>({
)}
{layers.length > shouldRenderDragIconLengthThreshold && (
<Icon
aria-label="Drag and drop icon"
title="Drag and drop to reorder"
aria-label={t(
'layers.layer-drag-drop-list.draggable-aria-label',
'Drag and drop to reorder'
)}
name="draggabledots"
size="lg"
className={style.dragIcon}
@@ -5,6 +5,8 @@ import * as React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, Input, FieldValidationMessage, useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
export interface LayerNameProps {
name: string;
onChange: (v: string) => void;
@@ -73,7 +75,7 @@ export const LayerName = ({ name, onChange, verifyLayerNameUniqueness, overrideS
{!isEditing && (
<button
className={styles.layerNameWrapper}
title="Edit layer name"
title={t('layers.layer-name.edit-layer-title', 'Edit layer name')}
onClick={onEditLayer}
data-testid="layer-name-div"
>
@@ -80,13 +80,17 @@ export const PasswordlessConfirmation = ({ onSubmit, isLoggingIn }: Props) => {
</Field>
{signup && (
<>
<Field label={'Username'} invalid={!!errors.code} error={errors.code?.message} hidden={true}>
<Field
label={t('login.form.passwordless-username-label', 'Username')}
invalid={!!errors.code}
error={errors.code?.message}
hidden={true}
>
<Input
{...register('username')}
id={usernameId}
autoFocus
autoCapitalize="none"
placeholder={'username'}
data-testid={selectors.pages.PasswordlessLogin.email}
hidden={true}
/>
@@ -97,7 +101,6 @@ export const PasswordlessConfirmation = ({ onSubmit, isLoggingIn }: Props) => {
id={nameId}
autoFocus
autoCapitalize="none"
placeholder={t('login.form.name-placeholder', 'name')}
data-testid={selectors.pages.PasswordlessLogin.email}
/>
</Field>
@@ -4,6 +4,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useTheme2, useStyles2, ColorPicker, IconButton } from '@grafana/ui';
import { ColorSwatch } from '@grafana/ui/internal';
import { t } from '../../internationalization';
export interface ColorValueEditorSettings {
placeholder?: string;
/** defaults to true */
@@ -57,7 +59,11 @@ export const ColorValueEditor = ({ value, settings, onChange, details }: Props)
</span>
)}
{settings?.isClearable && value && (
<IconButton name="times" onClick={() => onChange(undefined)} tooltip="Clear settings" />
<IconButton
name="times"
onClick={() => onChange(undefined)}
tooltip={t('options-ui.color.clear-tooltip', 'Clear settings')}
/>
)}
</>
)}
@@ -15,6 +15,8 @@ import {
} from '@grafana/data';
import { useStyles2, useTheme2, Field, RadioButtonGroup, Select } from '@grafana/ui';
import { t } from '../../internationalization';
import { ColorValueEditor } from './color';
type Props = StandardEditorProps<FieldColor | undefined, FieldColorConfigSettings>;
@@ -98,7 +100,7 @@ export const FieldColorEditor = ({ value, onChange, item, id }: Props) => {
<div style={{ marginBottom: theme.spacing(2) }}>
<Select minMenuHeight={200} options={options} value={mode} onChange={onModeChange} inputId={id} />
</div>
<Field label="Color series by">
<Field label={t('options-ui.field-color.color-by-label', 'Color series by')}>
<RadioButtonGroup value={value?.seriesBy ?? 'last'} options={seriesModes} onChange={onSeriesModeChange} />
</Field>
</>
@@ -3,6 +3,8 @@ import { css } from '@emotion/css';
import { StandardEditorProps, GrafanaTheme2, UnitFieldConfigSettings } from '@grafana/data';
import { IconButton, UnitPicker, useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
type Props = StandardEditorProps<string, UnitFieldConfigSettings>;
export function UnitValueEditor({ value, onChange, item }: Props) {
@@ -13,7 +15,11 @@ export function UnitValueEditor({ value, onChange, item }: Props) {
<span className={styles.first}>
<UnitPicker value={value} onChange={onChange} />
</span>
<IconButton name="times" onClick={() => onChange(undefined)} tooltip="Clear unit selection" />
<IconButton
name="times"
onClick={() => onChange(undefined)}
tooltip={t('options-ui.units.clear-tooltip', 'Clear unit selection')}
/>
</div>
);
}
@@ -6,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import { Field, IconButton, Input, useStyles2, Text } from '@grafana/ui';
import { t } from '../../internationalization';
export interface Props {
value: string;
onEdit: (newValue: string) => Promise<void>;
@@ -64,7 +66,12 @@ export const EditableTitle = ({ value, onEdit }: Props) => {
<Text element="h1" truncate>
{localValue}
</Text>
<IconButton name="pen" size="lg" tooltip="Edit title" onClick={() => setIsEditing(true)} />
<IconButton
name="pen"
size="lg"
tooltip={t('page.editable-title.edit-tooltip', 'Edit title')}
onClick={() => setIsEditing(true)}
/>
</div>
</div>
) : (
@@ -3,7 +3,7 @@ import { useCallback, useMemo, useState } from 'react';
import { GrafanaTheme2, PanelPluginMeta, SelectableValue } from '@grafana/data';
import { Icon, Button, MultiSelect, useStyles2 } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
export interface Props {
@@ -35,8 +35,8 @@ export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Prop
defaultOptions: true,
getOptionLabel: (i: SelectableValue<PanelPluginMeta>) => i.label,
getOptionValue: (i: SelectableValue<PanelPluginMeta>) => i.value,
noOptionsMessage: 'No Panel types found',
placeholder: 'Filter by type',
noOptionsMessage: t('panel-type-filter.select-no-options', 'No panel types found'),
placeholder: t('panel-type-filter.select-placeholder', 'Filter by type'),
maxMenuHeight,
options,
value,
@@ -46,18 +46,15 @@ export const PanelTypeFilter = ({ onChange: propsOnChange, maxMenuHeight }: Prop
return (
<div className={styles.container}>
{value.length > 0 && (
<Button
size="xs"
icon="trash-alt"
fill="text"
className={styles.clear}
onClick={() => onChange([])}
aria-label="Clear types"
>
<Button size="xs" icon="trash-alt" fill="text" className={styles.clear} onClick={() => onChange([])}>
<Trans i18nKey="panel-type-filter.clear-button">Clear types</Trans>
</Button>
)}
<MultiSelect<PanelPluginMeta> {...selectOptions} prefix={<Icon name="filter" />} aria-label="Panel Type filter" />
<MultiSelect<PanelPluginMeta>
{...selectOptions}
prefix={<Icon name="filter" />}
aria-label={t('panel-type-filter.select-aria-label', 'Panel type filter')}
/>
</div>
);
};
@@ -3,7 +3,7 @@ import { useAsync } from 'react-use';
import { renderMarkdown } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { LoadingPlaceholder } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
interface Props {
pluginId: string;
@@ -17,7 +17,7 @@ export function PluginHelp({ pluginId }: Props) {
const renderedMarkdown = renderMarkdown(value);
if (loading) {
return <LoadingPlaceholder text="Loading help..." />;
return <LoadingPlaceholder text={t('plugins.plugin-help.loading', 'Loading help...')} />;
}
if (error) {
@@ -71,7 +71,7 @@ export const QueryOperationRowHeader = ({
// disabling the a11y rules here as the IconButton above handles keyboard interactions
// this is just to provide a better experience for mouse users
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div className={styles.titleWrapper} onClick={onRowToggle} aria-label="Query operation row title">
<div className={styles.titleWrapper} onClick={onRowToggle}>
<div className={cx(styles.title, disabled && styles.disabled)}>{title}</div>
</div>
)}
@@ -5,6 +5,8 @@ import * as React from 'react';
import { Checkbox, Portal, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles } from '@grafana/ui/internal';
import { t } from '../../internationalization';
import { getStyles } from './styles';
interface RoleMenuGroupsOptionProps {
@@ -84,7 +86,12 @@ export const RoleMenuGroupOption = memo(
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{/* TODO: fix keyboard a11y */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onClickInternal}>
<div
ref={ref}
className={wrapperClassName}
aria-label={t('role-picker.menu-group-option-aria-label', 'Role picker option')}
onClick={onClickInternal}
>
<Checkbox
value={isSelected}
className={cx(customStyles.menuOptionCheckbox, {
@@ -5,6 +5,8 @@ import { Checkbox, Icon, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles } from '@grafana/ui/internal';
import { Role } from 'app/types';
import { t } from '../../internationalization';
import { getStyles } from './styles';
interface RoleMenuOptionProps {
@@ -49,7 +51,12 @@ export const RoleMenuOption = forwardRef<HTMLDivElement, React.PropsWithChildren
return (
// TODO: fix keyboard a11y
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div ref={ref} className={wrapperClassName} aria-label="Role picker option" onClick={onChangeInternal}>
<div
ref={ref}
className={wrapperClassName}
aria-label={t('role-picker.menu-option-aria-label', 'Role picker option')}
onClick={onChangeInternal}
>
<Checkbox
value={isSelected}
className={customStyles.menuOptionCheckbox}
@@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react';
import { Button, ScrollContainer, Stack, TextLink, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles } from '@grafana/ui/internal';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { OrgRole, Role } from 'app/types';
import { BuiltinRoleSelector } from './BuiltinRoleSelector';
@@ -237,7 +237,7 @@ export const RolePickerMenu = ({
})
)}
>
<div className={customStyles.menu} aria-label="Role picker menu">
<div className={customStyles.menu} aria-label={t('role-picker.menu-aria-label', 'Role picker menu')}>
<ScrollContainer
maxHeight={`${MENU_MAX_HEIGHT}px`}
// NOTE: this is a way to force hiding of the scrollbar
@@ -2,7 +2,7 @@ import { cx } from '@emotion/css';
import { Button, ScrollContainer, Stack, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles } from '@grafana/ui/internal';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { Role } from 'app/types';
import { RoleMenuOption } from './RoleMenuOption';
@@ -40,7 +40,7 @@ export const RolePickerSubMenu = ({
return (
<div
className={cx(customStyles.subMenu, { [customStyles.subMenuLeft]: showOnLeft })}
aria-label="Role picker submenu"
aria-label={t('role-picker.sub-menu-aria-label', 'Role picker submenu')}
>
<ScrollContainer maxHeight={`${MENU_MAX_HEIGHT}px`}>
<div className={styles.optionBody}>
@@ -6,6 +6,8 @@ import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2, Badge, Stack } from '@grafana/ui';
import { OrgUser } from 'app/types';
import { t } from '../../internationalization';
import { RolePickerDrawer } from './RolePickerDrawer';
export interface Props {
@@ -39,7 +41,12 @@ export const RolePickerBadges = ({ disabled, user }: Props) => {
<Stack gap={1}>
<Badge className={badgeStyle} color="blue" onClick={drawerControl} text={watch('role')} />
{user.roles && user.roles.length > 0 && (
<Badge className={badgeStyle} color="blue" onClick={drawerControl} text={`+${user.roles.length}`} />
<Badge
className={badgeStyle}
color="blue"
onClick={drawerControl}
text={t('role-picker-drawer.user-count', '+{{numUsers}}', { numUsers: user.roles.length })}
/>
)}
</Stack>
{isDrawerOpen && (
@@ -332,7 +332,7 @@ export function OldFolderPicker(props: Props) {
value={newFolderValue}
onChange={onNewFolderChange}
onKeyDown={onKeyDown}
placeholder="Press enter to confirm new folder."
placeholder={t('folder-picker.input-placeholder', 'Press enter to confirm new folder')}
onBlur={onBlur}
/>
</>
@@ -6,6 +6,8 @@ import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { Organization, UserOrg } from 'app/types';
import { t } from '../../internationalization';
export type OrgSelectItem = SelectableValue<Organization>;
export interface Props {
@@ -65,7 +67,7 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOr
setSelected(item);
}}
value={selected}
placeholder="Select organization"
placeholder={t('org-picker.select-placeholder', 'Select organization')}
noOptionsMessage="No organizations found"
/>
);
@@ -7,6 +7,8 @@ import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { ServiceAccountDTO, ServiceAccountsState } from 'app/types';
import { t } from '../../internationalization';
export interface Props {
onSelected: (user: SelectableValue<ServiceAccountDTO>) => void;
className?: string;
@@ -65,9 +67,9 @@ export class ServiceAccountPicker extends Component<Props, State> {
defaultOptions={true}
loadOptions={this.search}
onChange={onSelected}
placeholder="Start typing to search for service accounts"
placeholder={t('service-account-picker.select-placeholder', 'Start typing to search for service accounts')}
noOptionsMessage="No service accounts found"
aria-label="Service Account picker"
aria-label={t('service-account-picker.select-aria-label', 'Service account picker')}
/>
</div>
);
@@ -5,6 +5,8 @@ import { Icon, Select } from '@grafana/ui';
import { DEFAULT_SORT } from 'app/features/search/constants';
import { getGrafanaSearcher } from 'app/features/search/service/searcher';
import { t } from '../../internationalization';
export interface Props {
onChange: (sortValue: SelectableValue) => void;
value?: string;
@@ -40,7 +42,7 @@ export function SortPicker({ onChange, value, placeholder, filter, getSortOption
onChange={onChange}
value={options.value?.find((opt) => opt.value === value) ?? null}
options={options.value}
aria-label="Sort"
aria-label={t('sort-picker.select-aria-label', 'Sort')}
placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`}
prefix={<Icon name={isDesc ? 'sort-amount-down' : 'sort-amount-up'} />}
isClearable={isClearable}
@@ -7,6 +7,8 @@ import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { Team } from 'app/types';
import { t } from '../../internationalization';
export interface Props {
onSelected: (team: SelectableValue<Team>) => void;
className?: string;
@@ -82,9 +84,9 @@ export class TeamPicker extends Component<Props, State> {
value={value}
onChange={onSelected}
className={className}
placeholder="Select a team"
placeholder={t('team-picker.select-placeholder', 'Select a team')}
noOptionsMessage="No teams found"
aria-label="Team picker"
aria-label={t('team-picker.select-aria-label', 'Team picker')}
/>
</div>
);
@@ -7,6 +7,8 @@ import { getBackendSrv } from '@grafana/runtime';
import { AsyncSelect } from '@grafana/ui';
import { OrgUser } from 'app/types';
import { t } from '../../internationalization';
export interface Props {
onSelected: (user: SelectableValue<OrgUser>) => void;
className?: string;
@@ -65,9 +67,9 @@ export class UserPicker extends Component<Props, State> {
defaultOptions={true}
loadOptions={this.search}
onChange={onSelected}
placeholder="Start typing to search for user"
placeholder={t('user-picker.select-placeholder', 'Start typing to search for user')}
noOptionsMessage="No users found"
aria-label="User picker"
aria-label={t('user-picker.select-aria-label', 'User picker')}
/>
</div>
);
@@ -4,7 +4,7 @@ import { getBackendSrv } from '@grafana/runtime';
import { Field, Input, Button, LinkButton, Stack } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { useAppNotification } from 'app/core/copy/appNotification';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { w3cStandardEmailValidator } from 'app/features/admin/utils';
@@ -67,10 +67,14 @@ export const SignupPage = ({ queryParams }: Props) => {
<LoginLayout>
<InnerBox>
<form onSubmit={handleSubmit(onSubmit)} style={{ width: '100%' }}>
<Field label="Your name">
<Input id="user-name" {...register('name')} placeholder="(optional)" />
<Field label={t('sign-up.user-name-label', 'Your name')}>
<Input
id="user-name"
{...register('name')}
placeholder={t('sign-up.user-name-placeholder', '(optional)')}
/>
</Field>
<Field label="Email" invalid={!!errors.email} error={errors.email?.message}>
<Field label={t('sign-up.email-label', 'Email')} invalid={!!errors.email} error={errors.email?.message}>
<Input
id="email"
{...register('email', {
@@ -81,20 +85,23 @@ export const SignupPage = ({ queryParams }: Props) => {
},
})}
type="email"
placeholder="Email"
/>
</Field>
{!getConfig().autoAssignOrg && (
<Field label="Org. name">
<Input id="org-name" {...register('orgName')} placeholder="Org. name" />
<Field label={t('sign-up.org-name-label', 'Org. name')}>
<Input id="org-name" {...register('orgName')} />
</Field>
)}
{getConfig().verifyEmailEnabled && (
<Field label="Email verification code (sent to your email)">
<Input id="verification-code" {...register('code')} placeholder="Code" />
<Field label={t('sign-up.verification-code-label', 'Email verification code (sent to your email)')}>
<Input id="verification-code" {...register('code')} />
</Field>
)}
<Field label="Password" invalid={!!errors.password} error={errors?.password?.message}>
<Field
label={t('sign-up.password-label', 'Password')}
invalid={!!errors.password}
error={errors?.password?.message}
>
<PasswordField
id="new-password"
autoFocus
@@ -102,7 +109,11 @@ export const SignupPage = ({ queryParams }: Props) => {
{...register('password', { required: 'Password is required' })}
/>
</Field>
<Field label="Confirm password" invalid={!!errors.confirm} error={errors?.confirm?.message}>
<Field
label={t('sign-up.confirm-password-label', 'Confirm password')}
invalid={!!errors.confirm}
error={errors?.confirm?.message}
>
<PasswordField
id="confirm-new-password"
autoComplete="new-password"
@@ -5,7 +5,7 @@ import { getBackendSrv } from '@grafana/runtime';
import { Field, Input, Button, Legend, Container, LinkButton, Stack } from '@grafana/ui';
import { getConfig } from 'app/core/config';
import { useAppNotification } from 'app/core/copy/appNotification';
import { Trans } from 'app/core/internationalization';
import { t, Trans } from 'app/core/internationalization';
import { w3cStandardEmailValidator } from 'app/features/admin/utils';
interface EmailDTO {
@@ -55,8 +55,11 @@ export const VerifyEmail = () => {
<Trans i18nKey="sign-up.verify.header">Verify email</Trans>
</Legend>
<Field
label="Email"
description="Enter your email address to get a verification link sent to you"
label={t('sign-up.verify.email-label', 'Email')}
description={t(
'sign-up.verify.email-description',
'Enter your email address to get a verification link sent to you'
)}
invalid={!!errors.email}
error={errors.email?.message}
>
@@ -69,7 +72,6 @@ export const VerifyEmail = () => {
message: 'Email is invalid',
},
})}
placeholder="Email"
/>
</Field>
<Stack>
@@ -159,7 +159,12 @@ export const TagFilter = ({
<Trans i18nKey="tag-filter.clear-button">Clear tags</Trans>
</button>
)}
<MultiSelect key={selectKey} {...selectOptions} prefix={<Icon name="tag-alt" />} aria-label="Tag filter" />
<MultiSelect
key={selectKey}
{...selectOptions}
prefix={<Icon name="tag-alt" />}
aria-label={t('tag-filter.select-aria-label', 'Tag filter')}
/>
</div>
);
};
@@ -4,6 +4,8 @@ import { OptionProps } from 'react-select';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { t } from '../../internationalization';
import { TagBadge } from './TagBadge';
export interface TagSelectOption {
@@ -16,7 +18,11 @@ export const TagOption = ({ data, className, label, isFocused, innerProps }: Opt
const styles = useStyles2(getStyles);
return (
<div className={cx(styles.option, isFocused && styles.optionFocused)} aria-label="Tag option" {...innerProps}>
<div
className={cx(styles.option, isFocused && styles.optionFocused)}
aria-label={t('tag-filter.tag-option-label', 'Tag option')}
{...innerProps}
>
<div className={cx(styles.optionInner, className)}>
{typeof label === 'string' ? <TagBadge label={label} removeIcon={false} count={data.count ?? 0} /> : label}
</div>
@@ -6,7 +6,7 @@ import { GrafanaTheme2, locationUtil, PageLayoutType } from '@grafana/data';
import { Button, ErrorWithStack, useStyles2 } from '@grafana/ui';
import { Page } from '../components/Page/Page';
import { Trans } from '../internationalization';
import { t, Trans } from '../internationalization';
interface Props {
error: Error | null;
@@ -45,11 +45,15 @@ export function GrafanaRouteError({ error, errorInfo }: Props) {
<Button size="md" variant="secondary" icon="repeat" onClick={() => window.location.reload()}>
<Trans i18nKey="route-error.reload-button">Reload</Trans>
</Button>
<ErrorWithStack title={'Error details'} error={error} errorInfo={errorInfo} />
<ErrorWithStack title={t('route-error.error-title', 'Error details')} error={error} errorInfo={errorInfo} />
</div>
)}
{!isChunkLoadingError && (
<ErrorWithStack title={'An unexpected error happened'} error={error} errorInfo={errorInfo} />
<ErrorWithStack
title={t('route-error.error-unexpected-title', 'An unexpected error happened')}
error={error}
errorInfo={errorInfo}
/>
)}
</div>
</Page>
@@ -166,10 +166,16 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat
body={
<div>
<p>
Some features are stable (GA) and enabled by default, whereas some are currently in their preliminary
Beta phase, available for early adoption.
<Trans i18nKey="admin.admin-feature-toggles-table.confirm-modal-body-1">
Some features are stable (GA) and enabled by default, whereas some are currently in their
preliminary Beta phase, available for early adoption.
</Trans>
</p>
<p>
<Trans i18nKey="admin.admin-feature-toggles-table.confirm-modal-body-2">
We advise understanding the implications of each feature change before making modifications.
</Trans>
</p>
<p>We advise understanding the implications of each feature change before making modifications.</p>
</div>
}
confirmText="Save changes"
@@ -241,7 +241,9 @@ export const OrgUsersTable = ({
setUserToRemove(original);
}}
icon="times"
aria-label={`Delete user ${original.name}`}
aria-label={t('admin.org-users-table.delete-aria-label', 'Delete user: {{name}}', {
name: original.name,
})}
/>
)
);
@@ -174,8 +174,8 @@ export const UsersTable = ({
size="sm"
icon="pen"
href={`admin/users/edit/${original.uid}`}
aria-label={`Edit user ${original.name}`}
tooltip={'Edit user'}
aria-label={t('admin.users-table.edit-aria-label', 'Edit user: {{name}}', { name: original.name })}
tooltip={t('admin.users-table.edit-tooltip', 'Edit user')}
/>
);
},
+123 -9
View File
@@ -2,7 +2,11 @@
"_comment": "The code is the source of truth for English phrases. They should be updated in the components directly, and additional plurals specified in this file.",
"access-control": {
"add-permission": {
"built-in-aria-label": "Built-in role picker",
"level-aria-label": "Permission level",
"permissions-aria-label": "Permissions slider",
"role-label": "Role",
"role-select-aria-label": "Role to add new permission to",
"serviceaccount-label": "Service Account",
"team-label": "Team",
"title": "Add permission for",
@@ -15,7 +19,9 @@
"permission": "Permission"
},
"permission-list-item": {
"inherited": "Inherited from folder"
"inherited": "Inherited from folder",
"locked-aria-label": "Locked permission indicator",
"remove-aria-label": "Remove permission for {{identifier}}"
},
"permissions": {
"add-label": "Add a permission",
@@ -60,6 +66,8 @@
}
},
"admin-feature-toggles-table": {
"confirm-modal-body-1": "Some features are stable (GA) and enabled by default, whereas some are currently in their preliminary Beta phase, available for early adoption.",
"confirm-modal-body-2": "We advise understanding the implications of each feature change before making modifications.",
"get-stage-cell": {
"beta": "Beta",
"deprecated": "Deprecated",
@@ -167,6 +175,7 @@
"columns": {
"aria-label-role": "Role"
},
"delete-aria-label": "Delete user: {{name}}",
"title-delete": "Delete"
},
"orgs": {
@@ -267,6 +276,8 @@
"columns": {
"title-edit-user": "Edit user"
},
"edit-aria-label": "Edit user: {{name}}",
"edit-tooltip": "Edit user",
"last-seen-never": "Never",
"no-licensed-roles": "Not assigned"
}
@@ -2257,6 +2268,9 @@
"success": "Copied"
}
},
"close-button": {
"tooltip": "Close"
},
"combobox": {
"async": {
"error": "An error occurred while loading options."
@@ -4295,10 +4309,13 @@
}
},
"folder-filter": {
"clear-folder-button": "Clear folders"
"clear-folder-button": "Clear folders",
"select-aria-label": "Folder filter",
"select-placeholder": "Filter by folder"
},
"folder-picker": {
"create-instructions": "Press enter to create the new folder.",
"input-placeholder": "Press enter to confirm new folder",
"loading": "Loading folders..."
},
"folder-repo": {
@@ -4307,18 +4324,25 @@
"forgot-password": {
"back-button": "Back to login",
"change-password": {
"confirm-label": "Confirm new password",
"default-password-alert": "Continuing to use the default password exposes you to security risks.",
"new-password-label": "New password",
"skip-button": "Skip",
"submit-button": "Submit"
},
"contact-admin": "Did you forget your username or email? Contact your Grafana administrator.",
"email-sent": "An email with a reset link has been sent to the email address. You should receive it shortly.",
"reset-password-header": "Reset password",
"send-email-button": "Send reset email"
"send-email-button": "Send reset email",
"user-field-description": "Enter your information to get a reset link sent to you",
"user-field-label": "User",
"user-field-placeholder": "Email or username"
},
"form-prompt": {
"continue-button": "Continue editing",
"description": "Changes that you made may not be saved.",
"discard-button": "Discard unsaved changes"
"discard-button": "Discard unsaved changes",
"title": "Leave page?"
},
"frontendtests": {
"test-key": "test-key not found"
@@ -4588,6 +4612,7 @@
"cell-filter-on": "Filter for value",
"cell-filter-out": "Filter out value",
"cell-inspect": "Inspect value",
"cell-inspect-tooltip": "Inspect value",
"copy": "Copy to Clipboard",
"count": "Count",
"csv-counts": "Rows:{{rows}}, Columns:{{columns}} <5></5>",
@@ -4597,8 +4622,10 @@
"filter-popup-cancel": "Cancel",
"filter-popup-clear": "Clear filter",
"filter-popup-heading": "Filter by values:",
"filter-popup-input-placeholder": "Filter values",
"filter-popup-match-case": "Match case",
"inspect-drawer-title": "Inspect value",
"inspect-menu-label": "Inspect value",
"no-values-label": "No values",
"pagination-summary": "{{itemsRangeStart}} - {{displayedEnd}} of {{numRows}} rows"
},
@@ -4741,6 +4768,16 @@
"sign-up": "Sign up"
}
},
"layers": {
"layer-drag-drop-list": {
"draggable-aria-label": "Drag and drop to reorder",
"duplicate-tooltip": "Duplicate",
"remove-tooltip": "Remove"
},
"layer-name": {
"edit-layer-title": "Edit layer name"
}
},
"ldap-drawer": {
"attributes-section": {
"description": "Specify the LDAP attributes that map to the user's given name, surname, and email address, ensuring the application correctly retrieves and displays user information.",
@@ -4999,10 +5036,10 @@
"email-placeholder": "email",
"email-required": "Email is required",
"name-label": "Name",
"name-placeholder": "name",
"password-label": "Password",
"password-placeholder": "password",
"password-required": "Password is required",
"passwordless-username-label": "Username",
"submit-label": "Log in",
"submit-loading-label": "Logging in...",
"username-label": "Email or username",
@@ -5740,6 +5777,12 @@
}
},
"navigation": {
"breadcrumbs": {
"aria-label": "Breadcrumbs"
},
"help": {
"aria-label": "Help"
},
"invite-user": {
"invite-button": "Invite",
"invite-tooltip": "Invite new member"
@@ -5749,6 +5792,7 @@
"remove-bookmark": "Remove from Bookmarks"
},
"kiosk": {
"tooltip": "Enable kiosk mode",
"tv-alert": "Press ESC to exit kiosk mode"
},
"megamenu": {
@@ -5758,12 +5802,26 @@
"open": "Open menu",
"undock": "Undock menu"
},
"megamenu-item": {
"collapse-aria-label": "Collapse section: {{sectionName}}",
"expand-aria-label": "Expand section: {{sectionName}}"
},
"org-switcher": {
"aria-label": "Change organization"
},
"profile": {
"aria-label": "Profile"
},
"quick-add": {
"aria-label": "New"
},
"rss-button": "Latest from the blog"
},
"news": {
"drawer": {
"close": "Close Drawer"
"close": "Close drawer"
},
"link-title": "Go to Grafana labs blog",
"title": "Latest from the blog"
},
"notifications": {
@@ -5788,6 +5846,17 @@
"error": "Login provider denied login request"
}
},
"options-ui": {
"color": {
"clear-tooltip": "Clear settings"
},
"field-color": {
"color-by-label": "Color series by"
},
"units": {
"clear-tooltip": "Clear unit selection"
}
},
"org": {
"new-org-page": {
"create": "Create",
@@ -5809,6 +5878,14 @@
"submit": "Submit"
}
},
"org-picker": {
"select-placeholder": "Select organization"
},
"page": {
"editable-title": {
"edit-tooltip": "Edit title"
}
},
"panel": {
"header-menu": {
"copy": "Copy",
@@ -5856,7 +5933,10 @@
"unsupported-layout": "Unsupported layout"
},
"panel-type-filter": {
"clear-button": "Clear types"
"clear-button": "Clear types",
"select-aria-label": "Panel type filter",
"select-no-options": "No panel types found",
"select-placeholder": "Filter by type"
},
"playlist": {
"start-modal": {
@@ -6065,6 +6145,7 @@
},
"plugin-help": {
"error": "An error occurred when loading help.",
"loading": "Loading help...",
"not-found": "No query help could be found."
},
"plugin-installed-badge": {
@@ -6822,9 +6903,13 @@
"clear-button": "Clear all",
"tooltip": "You can now select the \"No basic role\" option and add permissions to your custom needs. You can find more information in <1>our documentation</1>."
},
"menu-aria-label": "Role picker menu",
"menu-group-option-aria-label": "Role picker option",
"menu-option-aria-label": "Role picker option",
"sub-menu": {
"clear-button": "Clear"
},
"sub-menu-aria-label": "Role picker submenu",
"title": {
"description": "Assign roles to users to ensure granular control over access to Grafana&lsquo;s features and resources. Find out more in our <2>documentation</2>."
}
@@ -6832,10 +6917,13 @@
"role-picker-drawer": {
"basic-roles": {
"label": "Basic Roles"
}
},
"user-count": "+{{numUsers}}"
},
"route-error": {
"description": "Grafana has likely been updated. Please try reloading the page.",
"error-title": "Error details",
"error-unexpected-title": "An unexpected error happened",
"reload-button": "Reload",
"title": "Unable to find application file"
},
@@ -6952,6 +7040,10 @@
"label": "Role"
}
},
"service-account-picker": {
"select-aria-label": "Service account picker",
"select-placeholder": "Start typing to search for service accounts"
},
"service-accounts": {
"empty-state": {
"button-title": "Add service account",
@@ -7220,10 +7312,19 @@
},
"sign-up": {
"back-button": "Back to login",
"confirm-password-label": "Confirm password",
"email-label": "Email",
"org-name-label": "Org. name",
"password-label": "Password",
"submit-button": "Submit",
"user-name-label": "Your name",
"user-name-placeholder": "(optional)",
"verification-code-label": "Email verification code (sent to your email)",
"verify": {
"back-button": "Back to login",
"complete-button": "Complete signup",
"email-description": "Enter your email address to get a verification link sent to you",
"email-label": "Email",
"header": "Verify email",
"info": "An email with a verification link has been sent to the email address. You should receive it shortly.",
"send-button": "Send verification email"
@@ -7281,6 +7382,9 @@
"url-column-header": "Snapshot url",
"view-button": "View"
},
"sort-picker": {
"select-aria-label": "Sort"
},
"support-bundles": {
"new-bundle-button": {
"new-support-bundle": "New support bundle"
@@ -7308,7 +7412,13 @@
"clear-button": "Clear tags",
"loading": "Loading...",
"no-tags": "No tags found",
"placeholder": "Filter by tag"
"placeholder": "Filter by tag",
"select-aria-label": "Tag filter",
"tag-option-label": "Tag option"
},
"team-picker": {
"select-aria-label": "Team picker",
"select-placeholder": "Select a team"
},
"teams": {
"create-team": {
@@ -7763,6 +7873,10 @@
"select-org-button": "Select organisation",
"title": "Organizations"
},
"user-picker": {
"select-aria-label": "User picker",
"select-placeholder": "Start typing to search for user"
},
"user-profile": {
"fields": {
"email-error": "Email is required",