Chore: Refactor TimeRangePicker for aria-label selectors (#78399)

* Change from aria-label to data-testid for e2e selectors

* translate

* update tests

* swap buttons:
pull/78544/head
Josh Hunt 2 years ago committed by GitHub
parent 02068662c1
commit 05070385cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      .betterer.results
  2. 10
      packages/grafana-e2e-selectors/src/selectors/components.ts
  3. 35
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarFooter.tsx
  4. 34
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx
  5. 79
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx
  6. 8
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx
  7. 2
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx
  8. 26
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx
  9. 14
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx
  10. 4
      public/app/core/components/TimePicker/TimePickerWithHistory.test.tsx
  11. 2
      public/locales/de-DE/grafana.json
  12. 2
      public/locales/en-US/grafana.json
  13. 2
      public/locales/es-ES/grafana.json
  14. 2
      public/locales/fr-FR/grafana.json
  15. 2
      public/locales/pseudo-LOCALE/grafana.json
  16. 2
      public/locales/zh-Hans/grafana.json

@ -752,20 +752,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx:5381": [ "packages/grafana-ui/src/components/DateTimePickers/TimeRangeInput.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
], ],
"packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/CalendarHeader.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerCalendar.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerFooter.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
],
"packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"],
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "2"]
],
"packages/grafana-ui/src/components/Drawer/Drawer.tsx:5381": [ "packages/grafana-ui/src/components/Drawer/Drawer.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"] [0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
], ],

@ -16,13 +16,13 @@ export const Components = {
TimePicker: { TimePicker: {
openButton: 'data-testid TimePicker Open Button', openButton: 'data-testid TimePicker Open Button',
overlayContent: 'data-testid TimePicker Overlay Content', overlayContent: 'data-testid TimePicker Overlay Content',
fromField: 'Time Range from field', fromField: 'data-testid Time Range from field',
toField: 'Time Range to field', toField: 'data-testid Time Range to field',
applyTimeRange: 'data-testid TimePicker submit button', applyTimeRange: 'data-testid TimePicker submit button',
calendar: { calendar: {
label: 'Time Range calendar', label: 'data-testid Time Range calendar',
openButton: 'Open time range calendar', openButton: 'data-testid Open time range calendar',
closeButton: 'Close time range Calendar', closeButton: 'data-testid Close time range Calendar',
}, },
absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow', absoluteTimeRangeTitle: 'data-testid-absolute-time-range-narrow',
}, },

@ -1,44 +1,23 @@
import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '../../../themes';
import { Trans } from '../../../utils/i18n'; import { Trans } from '../../../utils/i18n';
import { Button } from '../../Button'; import { Button } from '../../Button';
import { Stack } from '../../Layout/Stack/Stack';
import { TimePickerCalendarProps } from './TimePickerCalendar'; import { TimePickerCalendarProps } from './TimePickerCalendar';
export function Footer({ onClose, onApply }: TimePickerCalendarProps) { export function Footer({ onClose, onApply }: TimePickerCalendarProps) {
const styles = useStyles2(getFooterStyles);
return ( return (
<div className={styles.container}> <Stack gap={2} justifyContent="space-between">
<Button className={styles.apply} onClick={onApply}>
<Trans i18nKey="time-picker.calendar.apply-button">Apply time range</Trans>
</Button>
<Button variant="secondary" onClick={onClose}> <Button variant="secondary" onClick={onClose}>
<Trans i18nKey="time-picker.calendar.cancel-button">Cancel</Trans> <Trans i18nKey="time-picker.calendar.cancel-button">Cancel</Trans>
</Button> </Button>
</div>
<Button onClick={onApply}>
<Trans i18nKey="time-picker.calendar.apply-button">Apply time range</Trans>
</Button>
</Stack>
); );
} }
Footer.displayName = 'Footer'; Footer.displayName = 'Footer';
const getFooterStyles = (theme: GrafanaTheme2) => {
return {
container: css({
backgroundColor: theme.colors.background.primary,
display: 'flex',
justifyContent: 'center',
padding: '10px',
alignItems: 'stretch',
}),
apply: css({
marginRight: '4px',
width: '100%',
justifyContent: 'center',
}),
};
};

@ -1,44 +1,30 @@
import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../../themes'; import { Trans, t } from '../../../utils/i18n';
import { Trans } from '../../../utils/i18n'; import { IconButton } from '../../IconButton/IconButton';
import { Button } from '../../Button'; import { Stack } from '../../Layout/Stack/Stack';
import { TimePickerCalendarProps } from './TimePickerCalendar'; import { TimePickerCalendarProps } from './TimePickerCalendar';
import { TimePickerTitle } from './TimePickerTitle'; import { TimePickerTitle } from './TimePickerTitle';
export function Header({ onClose }: TimePickerCalendarProps) { export function Header({ onClose }: TimePickerCalendarProps) {
const styles = useStyles2(getHeaderStyles);
return ( return (
<div className={styles.container}> <Stack justifyContent="space-between">
<TimePickerTitle> <TimePickerTitle>
<Trans i18nKey="time-picker.calendar.select-time">Select a time range</Trans> <Trans i18nKey="time-picker.calendar.select-time">Select a time range</Trans>
</TimePickerTitle> </TimePickerTitle>
<Button
aria-label={selectors.components.TimePicker.calendar.closeButton} <IconButton
icon="times" data-testid={selectors.components.TimePicker.calendar.closeButton}
tooltip={t(`time-picker.calendar.close`, 'Close calendar')}
name="times"
variant="secondary" variant="secondary"
onClick={onClose} onClick={onClose}
/> />
</div> </Stack>
); );
} }
Header.displayName = 'Header'; Header.displayName = 'Header';
const getHeaderStyles = (theme: GrafanaTheme2) => {
return {
container: css({
backgroundColor: theme.colors.background.primary,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '7px',
}),
};
};

@ -19,27 +19,28 @@ export const getStyles = (theme: GrafanaTheme2, isReversed = false) => {
container: css({ container: css({
top: 0, top: 0,
position: 'absolute', position: 'absolute',
[`${isReversed ? 'left' : 'right'}`]: '544px', [`${isReversed ? 'left' : 'right'}`]: '546px', // lmao
}),
modalContainer: css({
label: 'modalContainer',
margin: '0 auto',
}),
calendar: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
padding: theme.spacing(1),
label: 'calendar',
boxShadow: theme.shadows.z3, boxShadow: theme.shadows.z3,
backgroundColor: theme.colors.background.primary, backgroundColor: theme.colors.background.primary,
zIndex: -1,
border: `1px solid ${theme.colors.border.weak}`, border: `1px solid ${theme.colors.border.weak}`,
borderTopLeftRadius: theme.shape.radius.default, borderRadius: theme.shape.radius.default,
borderBottomLeftRadius: theme.shape.radius.default,
'&:after': {
display: 'block',
backgroundColor: theme.colors.background.primary,
width: '19px',
height: '100%',
content: `${!isReversed ? '" "' : '""'}`,
position: 'absolute',
top: 0,
right: '-19px',
borderLeft: `1px solid ${theme.colors.border.weak}`,
},
}), }),
modal: css({ modal: css({
label: 'modal',
boxShadow: theme.shadows.z3, boxShadow: theme.shadows.z3,
left: '50%', left: '50%',
position: 'fixed', position: 'fixed',
@ -47,10 +48,6 @@ export const getStyles = (theme: GrafanaTheme2, isReversed = false) => {
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: theme.zIndex.modal, zIndex: theme.zIndex.modal,
}), }),
content: css({
margin: '0 auto',
width: '268px',
}),
}; };
}; };
@ -61,6 +58,11 @@ export interface TimePickerCalendarProps {
onClose: () => void; onClose: () => void;
onApply: (e: FormEvent<HTMLButtonElement>) => void; onApply: (e: FormEvent<HTMLButtonElement>) => void;
onChange: (from: DateTime, to: DateTime) => void; onChange: (from: DateTime, to: DateTime) => void;
/**
* When true, the calendar is rendered as a floating "tooltip" next to the input.
* When false, the calendar is rendered "fullscreen" in a modal. Yes. Don't ask.
*/
isFullscreen: boolean; isFullscreen: boolean;
timeZone?: TimeZone; timeZone?: TimeZone;
isReversed?: boolean; isReversed?: boolean;
@ -70,7 +72,7 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
const theme = useTheme2(); const theme = useTheme2();
const { modalBackdrop } = useStyles2(getModalStyles); const { modalBackdrop } = useStyles2(getModalStyles);
const styles = getStyles(theme, props.isReversed); const styles = getStyles(theme, props.isReversed);
const { isOpen, isFullscreen, onClose } = props; const { isOpen, isFullscreen: isFullscreenProp, onClose } = props;
const ref = React.createRef<HTMLElement>(); const ref = React.createRef<HTMLElement>();
const { dialogProps } = useDialog( const { dialogProps } = useDialog(
{ {
@ -87,17 +89,31 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
ref ref
); );
// This prop is confusingly worded, so rename it to something more intuitive.
const showInModal = !isFullscreenProp;
if (!isOpen) { if (!isOpen) {
return null; return null;
} }
if (isFullscreen) { const calendar = (
<section
className={styles.calendar}
ref={ref}
{...overlayProps}
{...dialogProps}
data-testid={selectors.components.TimePicker.calendar.label}
>
<Header {...props} />
<Body {...props} />
{showInModal && <Footer {...props} />}
</section>
);
if (!showInModal) {
return ( return (
<FocusScope contain restoreFocus autoFocus> <FocusScope contain restoreFocus autoFocus>
<section className={styles.container} ref={ref} {...overlayProps} {...dialogProps}> <div className={styles.container}>{calendar}</div>
<Header {...props} />
<Body {...props} />
</section>
</FocusScope> </FocusScope>
); );
} }
@ -105,14 +121,11 @@ function TimePickerCalendar(props: TimePickerCalendarProps) {
return ( return (
<OverlayContainer> <OverlayContainer>
<div className={modalBackdrop} /> <div className={modalBackdrop} />
<FocusScope contain autoFocus restoreFocus> <FocusScope contain autoFocus restoreFocus>
<section className={styles.modal} ref={ref} {...overlayProps} {...dialogProps}> <div className={styles.modal}>
<div className={styles.content} aria-label={selectors.components.TimePicker.calendar.label}> <div className={styles.modalContainer}>{calendar}</div>
<Header {...props} /> </div>
<Body {...props} />
<Footer {...props} />
</div>
</section>
</FocusScope> </FocusScope>
</OverlayContainer> </OverlayContainer>
); );

@ -96,22 +96,22 @@ describe('TimePickerContent', () => {
it('renders with absolute picker when absolute value and quick ranges are visible', () => { it('renders with absolute picker when absolute value and quick ranges are visible', () => {
renderComponent({ value: absoluteValue, isFullscreen: false }); renderComponent({ value: absoluteValue, isFullscreen: false });
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); expect(screen.queryByLabelText('From')).toBeInTheDocument();
}); });
it('renders with absolute picker when absolute value and quick ranges are hidden', () => { it('renders with absolute picker when absolute value and quick ranges are hidden', () => {
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true }); renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); expect(screen.queryByLabelText('From')).toBeInTheDocument();
}); });
it('renders without absolute picker when narrow screen and quick ranges are visible', () => { it('renders without absolute picker when narrow screen and quick ranges are visible', () => {
renderComponent({ value: relativeValue, isFullscreen: false }); renderComponent({ value: relativeValue, isFullscreen: false });
expect(screen.queryByLabelText(/time range from field/i)).not.toBeInTheDocument(); expect(screen.queryByLabelText('From')).not.toBeInTheDocument();
}); });
it('renders with absolute picker when narrow screen and quick ranges are hidden', () => { it('renders with absolute picker when narrow screen and quick ranges are hidden', () => {
renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true }); renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText(/time range from field/i)).toBeInTheDocument(); expect(screen.queryByLabelText('From')).toBeInTheDocument();
}); });
it('renders without timezone picker', () => { it('renders without timezone picker', () => {

@ -109,7 +109,7 @@ export const TimePickerFooter = (props: Props) => {
</section> </section>
) : ( ) : (
<section <section
aria-label={selectors.components.TimeZonePicker.containerV2} data-testid={selectors.components.TimeZonePicker.containerV2}
className={cx(style.timeZoneContainer, style.timeSettingContainer)} className={cx(style.timeZoneContainer, style.timeSettingContainer)}
> >
<Field <Field

@ -36,18 +36,17 @@ function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRan
describe('TimeRangeForm', () => { describe('TimeRangeForm', () => {
it('should render form correcty', () => { it('should render form correcty', () => {
const { getByLabelText, getByText, getAllByRole } = setup(); const { getByLabelText, getByText, getAllByRole } = setup();
const { TimePicker } = selectors.components;
expect(getByText('Apply time range')).toBeInTheDocument(); expect(getByText('Apply time range')).toBeInTheDocument();
expect(getAllByRole('button', { name: TimePicker.calendar.openButton })).toHaveLength(2); expect(getAllByRole('button', { name: 'Open calendar' })).toHaveLength(2);
expect(getByLabelText(TimePicker.fromField)).toBeInTheDocument(); expect(getByLabelText('From')).toBeInTheDocument();
expect(getByLabelText(TimePicker.toField)).toBeInTheDocument(); expect(getByLabelText('To')).toBeInTheDocument();
}); });
it('should display calendar when clicking the calendar icon', () => { it('should display calendar when clicking the calendar icon', () => {
const { getByLabelText, getAllByRole } = setup(); const { getByLabelText, getAllByRole } = setup();
const { TimePicker } = selectors.components; const { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]); fireEvent.click(openCalendarButton[0]);
expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument(); expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument();
@ -55,24 +54,23 @@ describe('TimeRangeForm', () => {
it('should have passed time range entered in form', () => { it('should have passed time range entered in form', () => {
const { getByLabelText } = setup(); const { getByLabelText } = setup();
const { TimePicker } = selectors.components;
const fromValue = defaultTimeRange.raw.from as string; const fromValue = defaultTimeRange.raw.from as string;
const toValue = defaultTimeRange.raw.to as string; const toValue = defaultTimeRange.raw.to as string;
expect(getByLabelText(TimePicker.fromField)).toHaveValue(fromValue); expect(getByLabelText('From')).toHaveValue(fromValue);
expect(getByLabelText(TimePicker.toField)).toHaveValue(toValue); expect(getByLabelText('To')).toHaveValue(toValue);
}); });
it('should close calendar when clicking the close icon', () => { it('should close calendar when clicking the close icon', () => {
const { queryByLabelText, getAllByRole, getByRole } = setup(); const { queryByLabelText, getAllByRole, getByRole } = setup();
const { TimePicker } = selectors.components; const { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton }); const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]); fireEvent.click(openCalendarButton[0]);
expect(getByRole('button', { name: TimePicker.calendar.closeButton })).toBeInTheDocument(); expect(getByRole('button', { name: 'Close calendar' })).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: TimePicker.calendar.closeButton })); fireEvent.click(getByRole('button', { name: 'Close calendar' }));
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); expect(queryByLabelText(TimePicker.calendar.label)).toBeNull();
}); });
@ -85,8 +83,7 @@ describe('TimeRangeForm', () => {
it('should have passed time range selected in calendar', () => { it('should have passed time range selected in calendar', () => {
const { getAllByRole, getCalendarDayByLabelText } = setup(); const { getAllByRole, getCalendarDayByLabelText } = setup();
const { TimePicker } = selectors.components; const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
fireEvent.click(openCalendarButton[0]); fireEvent.click(openCalendarButton[0]);
const from = getCalendarDayByLabelText('June 17, 2021'); const from = getCalendarDayByLabelText('June 17, 2021');
@ -98,8 +95,7 @@ describe('TimeRangeForm', () => {
it('should select correct time range in calendar when having a custom time zone', () => { it('should select correct time range in calendar when having a custom time zone', () => {
const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo'); const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo');
const { TimePicker } = selectors.components; const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
const openCalendarButton = getAllByRole('button', { name: TimePicker.calendar.openButton });
fireEvent.click(openCalendarButton[1]); fireEvent.click(openCalendarButton[1]);
const from = getCalendarDayByLabelText('June 17, 2021'); const from = getCalendarDayByLabelText('June 17, 2021');

@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React, { FormEvent, useCallback, useEffect, useState } from 'react'; import React, { FormEvent, useCallback, useEffect, useId, useState } from 'react';
import { import {
DateTime, DateTime,
@ -55,6 +55,9 @@ export const TimeRangeContent = (props: Props) => {
const [to, setTo] = useState<InputState>(toValue); const [to, setTo] = useState<InputState>(toValue);
const [isOpen, setOpen] = useState(false); const [isOpen, setOpen] = useState(false);
const fromFieldId = useId();
const toFieldId = useId();
// Synchronize internal state with external value // Synchronize internal state with external value
useEffect(() => { useEffect(() => {
const [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone); const [fromValue, toValue] = valueToState(value.raw.from, value.raw.to, timeZone);
@ -113,7 +116,8 @@ export const TimeRangeContent = (props: Props) => {
const icon = ( const icon = (
<Button <Button
aria-label={selectors.components.TimePicker.calendar.openButton} aria-label={t('time-picker.range-content.open-input-calendar', 'Open calendar')}
data-testid={selectors.components.TimePicker.calendar.openButton}
icon="calendar-alt" icon="calendar-alt"
variant="secondary" variant="secondary"
type="button" type="button"
@ -130,11 +134,12 @@ export const TimeRangeContent = (props: Props) => {
error={from.errorMessage} error={from.errorMessage}
> >
<Input <Input
id={fromFieldId}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
onChange={(event) => onChange(event.currentTarget.value, to.value)} onChange={(event) => onChange(event.currentTarget.value, to.value)}
addonAfter={icon} addonAfter={icon}
onKeyDown={submitOnEnter} onKeyDown={submitOnEnter}
aria-label={selectors.components.TimePicker.fromField} data-testid={selectors.components.TimePicker.fromField}
value={from.value} value={from.value}
/> />
</Field> </Field>
@ -143,11 +148,12 @@ export const TimeRangeContent = (props: Props) => {
<div className={style.fieldContainer}> <div className={style.fieldContainer}>
<Field label={t('time-picker.range-content.to-input', 'To')} invalid={to.invalid} error={to.errorMessage}> <Field label={t('time-picker.range-content.to-input', 'To')} invalid={to.invalid} error={to.errorMessage}>
<Input <Input
id={toFieldId}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
onChange={(event) => onChange(from.value, event.currentTarget.value)} onChange={(event) => onChange(from.value, event.currentTarget.value)}
addonAfter={icon} addonAfter={icon}
onKeyDown={submitOnEnter} onKeyDown={submitOnEnter}
aria-label={selectors.components.TimePicker.toField} data-testid={selectors.components.TimePicker.toField}
value={to.value} value={to.value}
/> />
</Field> </Field>

@ -9,8 +9,8 @@ import { TimePickerWithHistory } from './TimePickerWithHistory';
describe('TimePickerWithHistory', () => { describe('TimePickerWithHistory', () => {
// In some of the tests we close and re-open the picker. When we do that we must re-find these inputs // In some of the tests we close and re-open the picker. When we do that we must re-find these inputs
// as new elements will have been mounted // as new elements will have been mounted
const getFromField = () => screen.getByLabelText('Time Range from field'); const getFromField = () => screen.getByLabelText('From');
const getToField = () => screen.getByLabelText('Time Range to field'); const getToField = () => screen.getByLabelText('To');
const getApplyButton = () => screen.getByRole('button', { name: 'Apply time range' }); const getApplyButton = () => screen.getByRole('button', { name: 'Apply time range' });
const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history'; const LOCAL_STORAGE_KEY = 'grafana.dashboard.timepicker.history';

@ -1233,6 +1233,7 @@
"calendar": { "calendar": {
"apply-button": "Zeitbereich anwenden", "apply-button": "Zeitbereich anwenden",
"cancel-button": "Abbrechen", "cancel-button": "Abbrechen",
"close": "",
"select-time": "Einen Zeitbereich auswählen" "select-time": "Einen Zeitbereich auswählen"
}, },
"content": { "content": {
@ -1252,6 +1253,7 @@
"default-error": "Ein vergangenes Datum oder \"heutiges\" eingeben", "default-error": "Ein vergangenes Datum oder \"heutiges\" eingeben",
"fiscal-year": "Geschäftsjahr", "fiscal-year": "Geschäftsjahr",
"from-input": "Von", "from-input": "Von",
"open-input-calendar": "",
"range-error": "„Von“ darf nicht nach „Bis“ sein", "range-error": "„Von“ darf nicht nach „Bis“ sein",
"to-input": "Bis" "to-input": "Bis"
}, },

@ -1233,6 +1233,7 @@
"calendar": { "calendar": {
"apply-button": "Apply time range", "apply-button": "Apply time range",
"cancel-button": "Cancel", "cancel-button": "Cancel",
"close": "Close calendar",
"select-time": "Select a time range" "select-time": "Select a time range"
}, },
"content": { "content": {
@ -1252,6 +1253,7 @@
"default-error": "Please enter a past date or \"now\"", "default-error": "Please enter a past date or \"now\"",
"fiscal-year": "Fiscal year", "fiscal-year": "Fiscal year",
"from-input": "From", "from-input": "From",
"open-input-calendar": "Open calendar",
"range-error": "\"From\" can't be after \"To\"", "range-error": "\"From\" can't be after \"To\"",
"to-input": "To" "to-input": "To"
}, },

@ -1239,6 +1239,7 @@
"calendar": { "calendar": {
"apply-button": "Aplicar intervalo de tiempo", "apply-button": "Aplicar intervalo de tiempo",
"cancel-button": "Cancelar", "cancel-button": "Cancelar",
"close": "",
"select-time": "Seleccionar un intervalo de tiempo" "select-time": "Seleccionar un intervalo de tiempo"
}, },
"content": { "content": {
@ -1258,6 +1259,7 @@
"default-error": "Introducir una fecha anterior o «ahora»", "default-error": "Introducir una fecha anterior o «ahora»",
"fiscal-year": "Ejercicio fiscal", "fiscal-year": "Ejercicio fiscal",
"from-input": "Desde", "from-input": "Desde",
"open-input-calendar": "",
"range-error": "«Desde» no puede ser posterior a «hasta»", "range-error": "«Desde» no puede ser posterior a «hasta»",
"to-input": "Hasta" "to-input": "Hasta"
}, },

@ -1239,6 +1239,7 @@
"calendar": { "calendar": {
"apply-button": "Appliquer la plage de temps", "apply-button": "Appliquer la plage de temps",
"cancel-button": "Annuler", "cancel-button": "Annuler",
"close": "",
"select-time": "Sélectionner une plage de temps" "select-time": "Sélectionner une plage de temps"
}, },
"content": { "content": {
@ -1258,6 +1259,7 @@
"default-error": "Entrez une date dans le passé ou « maintenant »", "default-error": "Entrez une date dans le passé ou « maintenant »",
"fiscal-year": "Exercice fiscal", "fiscal-year": "Exercice fiscal",
"from-input": "De", "from-input": "De",
"open-input-calendar": "",
"range-error": "« De » ne peut pas être ultérieur à « À »", "range-error": "« De » ne peut pas être ultérieur à « À »",
"to-input": "À" "to-input": "À"
}, },

@ -1233,6 +1233,7 @@
"calendar": { "calendar": {
"apply-button": "Åppľy ŧįmę řäʼnģę", "apply-button": "Åppľy ŧįmę řäʼnģę",
"cancel-button": "Cäʼnčęľ", "cancel-button": "Cäʼnčęľ",
"close": "Cľőşę čäľęʼnđäř",
"select-time": "Ŝęľęčŧ ä ŧįmę řäʼnģę" "select-time": "Ŝęľęčŧ ä ŧįmę řäʼnģę"
}, },
"content": { "content": {
@ -1252,6 +1253,7 @@
"default-error": "Pľęäşę ęʼnŧęř ä päşŧ đäŧę őř \"ʼnőŵ\"", "default-error": "Pľęäşę ęʼnŧęř ä päşŧ đäŧę őř \"ʼnőŵ\"",
"fiscal-year": "Fįşčäľ yęäř", "fiscal-year": "Fįşčäľ yęäř",
"from-input": "Fřőm", "from-input": "Fřőm",
"open-input-calendar": "Øpęʼn čäľęʼnđäř",
"range-error": "\"Fřőm\" čäʼn'ŧ þę äƒŧęř \"Ŧő\"", "range-error": "\"Fřőm\" čäʼn'ŧ þę äƒŧęř \"Ŧő\"",
"to-input": "Ŧő" "to-input": "Ŧő"
}, },

@ -1227,6 +1227,7 @@
"calendar": { "calendar": {
"apply-button": "应用时间范围", "apply-button": "应用时间范围",
"cancel-button": "取消", "cancel-button": "取消",
"close": "",
"select-time": "选择时间范围" "select-time": "选择时间范围"
}, },
"content": { "content": {
@ -1246,6 +1247,7 @@
"default-error": "请输入一个过去的日期或\"现在\"", "default-error": "请输入一个过去的日期或\"现在\"",
"fiscal-year": "财政年度", "fiscal-year": "财政年度",
"from-input": "发件人", "from-input": "发件人",
"open-input-calendar": "",
"range-error": "“发件人”不能在“收件人”之后", "range-error": "“发件人”不能在“收件人”之后",
"to-input": "结束" "to-input": "结束"
}, },

Loading…
Cancel
Save