*/
import styled from '@emotion/styled';
import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
import tw from 'twin.macro';
import {
LAYOUT_BANNER_HEIGHT,
themeColor,
themeContrast,
} from '../helpers';
-import { translate } from '../helpers/l10n';
import { ThemeColors } from '../types';
import { InteractiveIconBase } from './InteractiveIcon';
import { CloseIcon, FlagErrorIcon, FlagInfoIcon, FlagSuccessIcon, FlagWarningIcon } from './icons';
export function Banner({ children, onDismiss, variant }: Props) {
const variantInfo = getVariantInfo(variant);
+ const intl = useIntl();
+
return (
<div role="alert" style={{ height: LAYOUT_BANNER_HEIGHT }}>
<BannerWrapper
{onDismiss && (
<BannerCloseIcon
Icon={CloseIcon}
- aria-label={translate('dismiss')}
+ aria-label={intl.formatMessage({ id: 'dismiss' })}
onClick={onDismiss}
size="small"
/>
import styled from '@emotion/styled';
import classNames from 'classnames';
import React from 'react';
+import { useIntl } from 'react-intl';
import tw from 'twin.macro';
import {
LAYOUT_VIEWPORT_MAX_WIDTH_LARGE,
themeColor,
themeContrast,
} from '../helpers';
-import { translate } from '../helpers/l10n';
import { useResizeObserver } from '../hooks/useResizeObserver';
import { Dropdown } from './Dropdown';
import { InteractiveIcon } from './InteractiveIcon';
} = props;
const [lengthOfChildren, setLengthOfChildren] = React.useState<number[]>([]);
+ const intl = useIntl();
+
const breadcrumbRef = React.useCallback((node: HTMLLIElement, index: number) => {
setLengthOfChildren((value) => {
if (value[index] === node.offsetWidth) {
return (
<BreadcrumbWrapper
- aria-label={ariaLabel ?? translate('breadcrumbs')}
+ aria-label={ariaLabel ?? intl.formatMessage({ id: 'breadcrumbs' })}
className={classNames('js-breadcrumbs', className)}
ref={innerRef}
>
>
<InteractiveIcon
Icon={ChevronDownIcon}
- aria-label={expandButtonLabel ?? translate('expand_breadcrumb')}
+ aria-label={expandButtonLabel ?? intl.formatMessage({ id: 'expand_breadcrumb' })}
className="sw-m-1 sw-mr-2"
size="small"
/>
*/
import { keyframes } from '@emotion/react';
import styled from '@emotion/styled';
-import React from 'react';
+import React, { DetailedHTMLProps, HTMLAttributes } from 'react';
+import { useIntl } from 'react-intl';
import tw from 'twin.macro';
-import { translate } from '../helpers/l10n';
import { themeColor } from '../helpers/theme';
interface Props {
if (customSpinner) {
return customSpinner;
}
- return <Spinner aria-label={ariaLabel} className={className} role="status" />;
+ // Overwrite aria-label only if defined
+ return <Spinner {...(ariaLabel ? { 'aria-label': ariaLabel } : {})} className={className} />;
}
if (children) {
return children;
}
`;
-export const Spinner = styled.div`
+/* Exported to allow styles to be overridden */
+export const StyledSpinner = styled.div`
border: 2px solid transparent;
background: linear-gradient(0deg, ${themeColor('primary')} 50%, transparent 50% 100%) border-box,
linear-gradient(90deg, ${themeColor('primary')} 25%, transparent 75% 100%) border-box;
${tw`sw-rounded-pill`}
`;
-Spinner.defaultProps = { 'aria-label': translate('loading'), role: 'status' };
+function Spinner(props: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>) {
+ const intl = useIntl();
+
+ return (
+ <StyledSpinner aria-label={intl.formatMessage({ id: 'loading' })} role="status" {...props} />
+ );
+}
const Placeholder = styled.div`
position: relative;
*/
import React from 'react';
-import { translate } from '../helpers/l10n';
+import { useIntl } from 'react-intl';
import { PopupPlacement, PopupZLevel } from '../helpers/positioning';
import { InputSizeKeys } from '../types/theme';
import { DropdownMenu } from './DropdownMenu';
export function ActionsDropdown(props: ActionsDropdownProps) {
const { children, buttonSize, ariaLabel, ...dropdownProps } = props;
+
+ const intl = useIntl();
+
return (
<Dropdown overlay={children} {...dropdownProps}>
<InteractiveIcon
Icon={MenuIcon}
- aria-label={ariaLabel ?? translate('menu')}
+ aria-label={ariaLabel ?? intl.formatMessage({ id: 'menu' })}
size={buttonSize}
stopPropagation={false}
/>
*/
import styled from '@emotion/styled';
import classNames from 'classnames';
+import { useIntl } from 'react-intl';
import tw from 'twin.macro';
-import { translate } from '../helpers/l10n';
import { themeBorder, themeColor, themeContrast, themeShadow } from '../helpers/theme';
import { LightLabel } from './Text';
import { RecommendedIcon } from './icons/RecommendedIcon';
vertical = false,
} = props;
const isActionable = Boolean(onClick);
+
+ const intl = useIntl();
+
return (
<StyledButton
aria-checked={selected}
<StyledRecommended>
<StyledRecommendedIcon className="sw-mr-1" />
<span className="sw-align-middle">
- <strong>{translate('recommended')}</strong> {recommendedReason}
+ <strong>{intl.formatMessage({ id: 'recommended' })}</strong> {recommendedReason}
</span>
</StyledRecommended>
)}
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { screen } from '@testing-library/react';
-import { render } from '../../helpers/testUtils';
+import { renderWithContext } from '../../helpers/testUtils';
import { FCProps } from '../../types/misc';
import { Banner } from '../Banner';
import { Note } from '../Text';
});
function setupWithProps(props: Partial<FCProps<typeof Banner>> = {}) {
- return render(
+ return renderWithContext(
<Banner {...props} variant="warning">
<Note className="sw-body-sm">{props.children ?? 'Test Message'}</Note>
</Banner>
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import { render, screen } from '@testing-library/react';
+import { IntlWrapper } from '../../helpers/testUtils';
import { DeferredSpinner } from '../DeferredSpinner';
beforeEach(() => {
}
function prepareDeferredSpinner(props: Partial<DeferredSpinner['props']> = {}) {
- return <DeferredSpinner {...props} />;
+ return (
+ <IntlWrapper>
+ <DeferredSpinner {...props} />
+ </IntlWrapper>
+ );
}
*/
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { render } from '../../helpers/testUtils';
+import { renderWithContext } from '../../helpers/testUtils';
import { FCProps } from '../../types/misc';
import { SelectionCard } from '../SelectionCard';
});
function renderSelectionCard(props: Partial<FCProps<typeof SelectionCard>> = {}) {
- return render(
+ return renderWithContext(
<SelectionCard
recommended
recommendedReason="Recommended for you"
import styled from '@emotion/styled';
import classNames from 'classnames';
-import {
- format,
- getYear,
- isSameMonth,
- isSameYear,
- setMonth,
- setYear,
- startOfMonth,
-} from 'date-fns';
-import { range } from 'lodash';
+import { format } from 'date-fns';
import * as React from 'react';
-import {
- ActiveModifiers,
- CaptionProps,
- Matcher,
- DayPicker as OriginalDayPicker,
- useNavigation as useCalendarNavigation,
- useDayPicker,
-} from 'react-day-picker';
+import { ActiveModifiers, Matcher, DayPicker as OriginalDayPicker } from 'react-day-picker';
import tw from 'twin.macro';
import { PopupPlacement, PopupZLevel, themeBorder, themeColor, themeContrast } from '../../helpers';
import { InputSizeKeys } from '../../types/theme';
import { FocusOutHandler } from '../FocusOutHandler';
import { InteractiveIcon } from '../InteractiveIcon';
import { OutsideClickHandler } from '../OutsideClickHandler';
-import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from '../icons';
+import { CalendarIcon } from '../icons';
import { CloseIcon } from '../icons/CloseIcon';
import { Popup } from '../popups';
+import { CustomCalendarNavigation } from './DatePickerCustomCalendarNavigation';
import { InputField } from './InputField';
-import { InputSelect } from './InputSelect';
// When no minDate is given, year dropdown will show year options up to PAST_MAX_YEARS in the past
const YEARS_TO_DISPLAY = 10;
-const MONTHS_IN_A_YEAR = 12;
interface Props {
alignRight?: boolean;
- ariaNextMonthLabel: string;
- ariaPreviousMonthLabel: string;
className?: string;
clearButtonLabel: string;
currentMonth?: Date;
render() {
const {
alignRight,
- ariaNextMonthLabel,
- ariaPreviousMonthLabel,
clearButtonLabel,
highlightFrom,
highlightTo,
captionLayout="dropdown-buttons"
className="sw-body-sm"
components={{
- Caption: getCustomCalendarNavigation({
- ariaNextMonthLabel,
- ariaPreviousMonthLabel,
- }),
+ Caption: CustomCalendarNavigation,
}}
disabled={{ after: maxDate, before: minDate }}
formatters={{
color: ${themeContrast('datePickerSelected')};
}
`;
-
-function getCustomCalendarNavigation({
- ariaNextMonthLabel,
- ariaPreviousMonthLabel,
-}: {
- ariaNextMonthLabel: string;
- ariaPreviousMonthLabel: string;
-}) {
- return function CalendarNavigation(props: CaptionProps) {
- const { displayMonth } = props;
- const { fromYear, toYear } = useDayPicker();
- const { goToMonth, nextMonth, previousMonth } = useCalendarNavigation();
-
- const baseDate = startOfMonth(displayMonth); // reference date
-
- const months = range(MONTHS_IN_A_YEAR).map((month) => {
- const monthValue = setMonth(baseDate, month);
-
- return {
- label: format(monthValue, 'MMM'),
- value: monthValue,
- };
- });
-
- const startYear = fromYear ?? getYear(Date.now()) - YEARS_TO_DISPLAY;
-
- const years = range(startYear, toYear ? toYear + 1 : undefined).map((year) => {
- const yearValue = setYear(baseDate, year);
-
- return {
- label: String(year),
- value: yearValue,
- };
- });
-
- return (
- <nav className="sw-flex sw-items-center sw-justify-between sw-py-1">
- <InteractiveIcon
- Icon={ChevronLeftIcon}
- aria-label={ariaPreviousMonthLabel}
- className="sw-mr-2"
- onClick={() => {
- if (previousMonth) {
- goToMonth(previousMonth);
- }
- }}
- size="small"
- />
-
- <span data-testid="month-select">
- <InputSelect
- isClearable={false}
- onChange={(value) => {
- if (value) {
- goToMonth(value.value);
- }
- }}
- options={months}
- size="full"
- value={months.find((m) => isSameMonth(m.value, displayMonth))}
- />
- </span>
-
- <span data-testid="year-select">
- <InputSelect
- className="sw-ml-1"
- data-testid="year-select"
- isClearable={false}
- onChange={(value) => {
- if (value) {
- goToMonth(value.value);
- }
- }}
- options={years}
- size="full"
- value={years.find((y) => isSameYear(y.value, displayMonth))}
- />
- </span>
-
- <InteractiveIcon
- Icon={ChevronRightIcon}
- aria-label={ariaNextMonthLabel}
- className="sw-ml-2"
- onClick={() => {
- if (nextMonth) {
- goToMonth(nextMonth);
- }
- }}
- size="small"
- />
- </nav>
- );
- };
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+import {
+ format,
+ getYear,
+ isSameMonth,
+ isSameYear,
+ setMonth,
+ setYear,
+ startOfMonth,
+} from 'date-fns';
+import { range } from 'lodash';
+import {
+ CaptionProps,
+ useNavigation as useCalendarNavigation,
+ useDayPicker,
+} from 'react-day-picker';
+import { useIntl } from 'react-intl';
+import { InteractiveIcon } from '../InteractiveIcon';
+import { ChevronLeftIcon, ChevronRightIcon } from '../icons';
+import { InputSelect } from './InputSelect';
+
+const YEARS_TO_DISPLAY = 10;
+const MONTHS_IN_A_YEAR = 12;
+
+export function CustomCalendarNavigation(props: CaptionProps) {
+ const { displayMonth } = props;
+ const { fromYear, toYear } = useDayPicker();
+ const { goToMonth, nextMonth, previousMonth } = useCalendarNavigation();
+
+ const intl = useIntl();
+
+ const baseDate = startOfMonth(displayMonth); // reference date
+
+ const months = range(MONTHS_IN_A_YEAR).map((month) => {
+ const monthValue = setMonth(baseDate, month);
+
+ return {
+ label: format(monthValue, 'MMM'),
+ value: monthValue,
+ };
+ });
+
+ const startYear = fromYear ?? getYear(Date.now()) - YEARS_TO_DISPLAY;
+
+ const years = range(startYear, toYear ? toYear + 1 : undefined).map((year) => {
+ const yearValue = setYear(baseDate, year);
+
+ return {
+ label: String(year),
+ value: yearValue,
+ };
+ });
+
+ return (
+ <nav className="sw-flex sw-items-center sw-justify-between sw-py-1">
+ <InteractiveIcon
+ Icon={ChevronLeftIcon}
+ aria-label={intl.formatMessage({ id: 'previous_' })}
+ className="sw-mr-2"
+ onClick={() => {
+ if (previousMonth) {
+ goToMonth(previousMonth);
+ }
+ }}
+ size="small"
+ />
+
+ <span data-testid="month-select">
+ <InputSelect
+ isClearable={false}
+ onChange={(value) => {
+ if (value) {
+ goToMonth(value.value);
+ }
+ }}
+ options={months}
+ size="full"
+ value={months.find((m) => isSameMonth(m.value, displayMonth))}
+ />
+ </span>
+
+ <span data-testid="year-select">
+ <InputSelect
+ className="sw-ml-1"
+ data-testid="year-select"
+ isClearable={false}
+ onChange={(value) => {
+ if (value) {
+ goToMonth(value.value);
+ }
+ }}
+ options={years}
+ size="full"
+ value={years.find((y) => isSameYear(y.value, displayMonth))}
+ />
+ </span>
+
+ <InteractiveIcon
+ Icon={ChevronRightIcon}
+ aria-label={intl.formatMessage({ id: 'next_' })}
+ className="sw-ml-2"
+ onClick={() => {
+ if (nextMonth) {
+ goToMonth(nextMonth);
+ }
+ }}
+ size="small"
+ />
+ </nav>
+ );
+}
interface Props {
alignEndDateCalandarRight?: boolean;
- ariaNextMonthLabel: string;
- ariaPreviousMonthLabel: string;
className?: string;
clearButtonLabel: string;
fromLabel: string;
render() {
const {
alignEndDateCalandarRight,
- ariaNextMonthLabel,
- ariaPreviousMonthLabel,
clearButtonLabel,
fromLabel,
minDate,
return (
<div className={classNames('sw-flex sw-items-center', this.props.className)}>
<DatePicker
- ariaNextMonthLabel={ariaNextMonthLabel}
- ariaPreviousMonthLabel={ariaPreviousMonthLabel}
clearButtonLabel={clearButtonLabel}
currentMonth={this.to}
data-test="from"
<LightLabel className="sw-mx-2">{separatorText ?? '–'}</LightLabel>
<DatePicker
alignRight={alignEndDateCalandarRight}
- ariaNextMonthLabel={ariaNextMonthLabel}
- ariaPreviousMonthLabel={ariaPreviousMonthLabel}
clearButtonLabel={clearButtonLabel}
currentMonth={this.from}
data-test="to"
import { themeBorder, themeColor, themeContrast } from '../../helpers/theme';
import { isDefined } from '../../helpers/types';
import { InputSizeKeys } from '../../types/theme';
-import { DeferredSpinner, Spinner } from '../DeferredSpinner';
+import { DeferredSpinner, StyledSpinner } from '../DeferredSpinner';
import { InteractiveIcon } from '../InteractiveIcon';
import { CloseIcon } from '../icons/CloseIcon';
import { SearchIcon } from '../icons/SearchIcon';
${tw`sw-align-middle`}
${tw`sw-h-control`}
- ${Spinner} {
+ ${StyledSpinner} {
top: calc((2.25rem - ${theme('spacing.4')}) / 2);
${tw`sw-left-3`};
${tw`sw-absolute`};
import classNames from 'classnames';
import { omit } from 'lodash';
import React, { RefObject } from 'react';
+import { useIntl } from 'react-intl';
import { GroupBase, InputProps, components } from 'react-select';
import AsyncSelect, { AsyncProps } from 'react-select/async';
import Select from 'react-select/dist/declarations/src/Select';
import { INPUT_SIZES } from '../../helpers';
import { Key } from '../../helpers/keyboard';
-import { translate } from '../../helpers/l10n';
import { InputSearch } from './InputSearch';
import { LabelValueSelectOption, SelectProps, selectStyle } from './InputSelect';
selectProps: { clearIconLabel, placeholder, isLoading, inputValue, minLength, tooShortText },
} = props;
+ const intl = useIntl();
+
const onChange = (v: string, prevValue = '') => {
props.selectProps.onInputChange(v, { action: 'input-change', prevInputValue: prevValue });
};
return (
<InputSearch
- clearIconAriaLabel={clearIconLabel ?? translate('clear')}
+ clearIconAriaLabel={clearIconLabel ?? intl.formatMessage({ id: 'clear' })}
loading={isLoading && inputValue.length >= (minLength ?? 0)}
minLength={minLength}
onChange={onChange}
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getMonth, getYear, parseISO } from 'date-fns';
-import { render } from '../../../helpers/testUtils';
+import { renderWithContext } from '../../../helpers/testUtils';
import { DatePicker } from '../DatePicker';
it('behaves correctly', async () => {
const nav = screen.getByRole('navigation');
expect(nav).toBeInTheDocument();
- await user.click(within(nav).getByRole('button', { name: 'previous' }));
+ await user.click(within(nav).getByRole('button', { name: 'previous_' }));
await user.click(screen.getByText('7'));
expect(onChange).toHaveBeenCalled();
* Then check that onChange was correctly called with a date in the following month
*/
await user.click(screen.getByRole('textbox'));
- const nextButton = screen.getByRole('button', { name: 'next' });
- await user.click(nextButton);
- await user.click(nextButton);
+ await user.click(screen.getByRole('button', { name: 'next_' }));
await user.click(screen.getByText('12'));
expect(onChange).toHaveBeenCalled();
function renderDatePicker(overrides: Partial<DatePicker['props']> = {}) {
const defaultFormatter = (date?: Date) => (date ? date.toISOString() : '');
- render(
+ renderWithContext(
<DatePicker
- ariaNextMonthLabel="next"
- ariaPreviousMonthLabel="previous"
clearButtonLabel="clear"
onChange={jest.fn()}
placeholder="placeholder"
import { screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { formatISO, parseISO } from 'date-fns';
-import { render } from '../../../helpers/testUtils';
+import { IntlWrapper, render } from '../../../helpers/testUtils';
import { DateRangePicker } from '../DateRangePicker';
beforeEach(() => {
date ? formatISO(date, { representation: 'date' }) : '';
render(
- <DateRangePicker
- ariaNextMonthLabel="next"
- ariaPreviousMonthLabel="previous"
- clearButtonLabel="clear"
- fromLabel="from"
- onChange={jest.fn()}
- toLabel="to"
- valueFormatter={defaultFormatter}
- {...overrides}
- />
+ <IntlWrapper messages={{ next_: 'next', previous_: 'previous' }}>
+ <DateRangePicker
+ clearButtonLabel="clear"
+ fromLabel="from"
+ onChange={jest.fn()}
+ toLabel="to"
+ valueFormatter={defaultFormatter}
+ {...overrides}
+ />
+ </IntlWrapper>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { render, screen } from '@testing-library/react';
+import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
+import { renderWithContext } from '../../../helpers/testUtils';
import { MultiSelectMenu } from '../MultiSelectMenu';
const elements = ['foo', 'bar', 'baz'];
});
function renderMultiselect(props: Partial<MultiSelectMenu['props']> = {}) {
- return render(
+ return renderWithContext(
<MultiSelectMenu
clearIconAriaLabel="clear"
createElementLabel="create thing"
*/
import { act, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { render } from '../../../helpers/testUtils';
+import { renderWithContext } from '../../../helpers/testUtils';
import { FCProps } from '../../../types/misc';
import { LabelValueSelectOption } from '../InputSelect';
import { SearchSelectDropdown } from '../SearchSelectDropdown';
});
function renderSearchSelectDropdown(props: Partial<FCProps<typeof SearchSelectDropdown>> = {}) {
- return render(
+ return renderWithContext(
<SearchSelectDropdown
aria-label="label"
controlLabel="not assigned"
import { Global, css, useTheme } from '@emotion/react';
import classNames from 'classnames';
import { ReactNode } from 'react';
+import { useIntl } from 'react-intl';
import ReactModal from 'react-modal';
import tw from 'twin.macro';
import { themeColor } from '../../helpers';
import { REACT_DOM_CONTAINER } from '../../helpers/constants';
-import { translate } from '../../helpers/l10n';
import { Theme } from '../../types/theme';
import { ButtonSecondary } from '../buttons';
import { ModalBody } from './ModalBody';
...props
}: Props) {
const theme = useTheme();
-
+ const intl = useIntl();
return (
<>
<Global styles={globalStyles({ theme })} />
onClick={onClose}
type="reset"
>
- {props.secondaryButtonLabel ?? translate('close')}
+ {props.secondaryButtonLabel ?? intl.formatMessage({ id: 'close' })}
</ButtonSecondary>
}
/>
*/
import { screen } from '@testing-library/react';
-import { render } from '../../../helpers/testUtils';
+import { renderWithContext } from '../../../helpers/testUtils';
import { Modal, PropsWithChildren, PropsWithSections } from '../Modal';
it('should render default modal with predefined content', async () => {
});
function setupPredefinedContent(props: Partial<PropsWithSections> = {}) {
- return render(
+ return renderWithContext(
<Modal
body="Body"
headerTitle="Hello"
}
function setupLooseContent(props: Partial<PropsWithChildren> = {}, children = <div />) {
- return render(
+ return renderWithContext(
<Modal onClose={jest.fn()} {...props}>
{children}
</Modal>
}
function setupLooseContentWithMultipleChildren(props: Partial<PropsWithChildren> = {}) {
- return render(
+ return renderWithContext(
<Modal onClose={jest.fn()} {...props}>
<div>Hello there!</div>
<div>How are you?</div>
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-/**
- * (!) Do not use this, it is left for legacy purposes and should be slowly replaced with react-intl
- */
-export function translate(keys: string): string {
- return keys;
-}
-
-export function translateWithParameters(
- messageKey: string,
- ...parameters: Array<string | number>
-): string {
- return `${messageKey}.${parameters.join('.')}`;
-}
import { identity, kebabCase } from 'lodash';
import React, { PropsWithChildren, ReactNode } from 'react';
import { HelmetProvider } from 'react-helmet-async';
-import { IntlProvider } from 'react-intl';
+import { IntlProvider, ReactIntlErrorCode } from 'react-intl';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
export function render(
function RouterWrapper({ children }: React.PropsWithChildren<object>) {
return (
<HelmetProvider>
- <MemoryRouter>
- <Routes>
- <Route element={children} path="/" />
- {additionalRoutes}
- </Routes>
- </MemoryRouter>
+ <IntlWrapper>
+ <MemoryRouter>
+ <Routes>
+ <Route element={children} path="/" />
+ {additionalRoutes}
+ </Routes>
+ </MemoryRouter>
+ </IntlWrapper>
</HelmetProvider>
);
}
return function ContextWrapper({ children }: React.PropsWithChildren<object>) {
return (
<HelmetProvider>
- <IntlProvider defaultLocale="en" locale="en">
- {children}
- </IntlProvider>
+ <IntlWrapper>{children}</IntlWrapper>
</HelmetProvider>
);
};
return debounced;
});
+
+export function IntlWrapper({
+ children,
+ messages = {},
+}: {
+ children: ReactNode;
+ messages?: Record<string, string>;
+}) {
+ return (
+ <IntlProvider
+ defaultLocale="en"
+ locale="en"
+ messages={messages}
+ onError={(e) => {
+ // ignore missing translations, there are none!
+ if (e.code !== ReactIntlErrorCode.MISSING_TRANSLATION) {
+ // eslint-disable-next-line no-console
+ console.error(e);
+ }
+ }}
+ >
+ {children}
+ </IntlProvider>
+ );
+}
});
const startReactApp = await import('./utils/startReactApp').then((i) => i.default);
- startReactApp(l10nBundle.locale, currentUser, appState, availableFeatures);
+ startReactApp(l10nBundle, currentUser, appState, availableFeatures);
}
function isMainApp() {
import * as React from 'react';
import { render } from 'react-dom';
import { Helmet, HelmetProvider } from 'react-helmet-async';
-import { IntlProvider } from 'react-intl';
+import { IntlShape, RawIntlProvider } from 'react-intl';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import accountRoutes from '../../apps/account/routes';
import auditLogsRoutes from '../../apps/audit-logs/routes';
const queryClient = new QueryClient();
export default function startReactApp(
- lang: string,
+ l10nBundle: IntlShape,
currentUser?: CurrentUser,
appState?: AppState,
availableFeatures?: Feature[]
<AppStateContextProvider appState={appState ?? DEFAULT_APP_STATE}>
<AvailableFeaturesContext.Provider value={availableFeatures ?? DEFAULT_AVAILABLE_FEATURES}>
<CurrentUserContextProvider currentUser={currentUser}>
- <IntlProvider defaultLocale={lang} locale={lang}>
+ <RawIntlProvider value={l10nBundle}>
<ThemeProvider theme={lightTheme}>
<QueryClientProvider client={queryClient}>
<GlobalMessagesContainer />
</BrowserRouter>
</QueryClientProvider>
</ThemeProvider>
- </IntlProvider>
+ </RawIntlProvider>
</CurrentUserContextProvider>
</AvailableFeaturesContext.Provider>
</AppStateContextProvider>
return (
<DateRangePicker
- ariaNextMonthLabel={translate('next_')}
- ariaPreviousMonthLabel={translate('previous_')}
clearButtonLabel={translate('clear')}
fromLabel={translate('start_date')}
onChange={this.handlePeriodChange}
return (
<div className="sw-flex">
<DateRangePicker
- ariaNextMonthLabel={translate('next_')}
- ariaPreviousMonthLabel={translate('previous_')}
className="sw-w-abs-350"
clearButtonLabel={translate('clear')}
fromLabel={translate('start_date')}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { IntlShape, createIntl, createIntlCache } from 'react-intl';
import { fetchL10nBundle } from '../api/l10n';
import { L10nBundle, L10nBundleRequestParams } from '../types/l10nBundle';
import { Dict } from '../types/types';
default_error_message: 'The request cannot be processed. Try again later.',
};
+let intl: IntlShape;
+
+export function getIntl() {
+ return intl;
+}
+
export function getMessages() {
return getL10nBundleFromCache().messages ?? DEFAULT_MESSAGES;
}
persistL10nBundleInCache(bundle);
- return bundle;
+ const cache = createIntlCache();
+
+ intl = createIntl(
+ {
+ locale: effectiveLocale,
+ messages,
+ },
+ cache
+ );
+
+ return intl;
}
function getPreferredLanguage() {
bold=Bold
branch=Branch
breadcrumbs=Breadcrumbs
+expand_breadcrumbs=Expand breadcrumbs
by_=by
calendar=Calendar
cancel=Cancel
max_results_reached=Only the first {0} results are displayed
me=Me
members=Members
+menu=Menu
min=Min
minor=Minor
more=More