From c5d549c2c356fd9f67c08d18f28af884106abeee Mon Sep 17 00:00:00 2001 From: Jeremy Davis Date: Tue, 8 Aug 2023 18:12:01 +0200 Subject: [PATCH] SONAR-19688 Use react-intl for translations --- .../design-system/src/components/Banner.tsx | 6 +- .../src/components/Breadcrumbs.tsx | 8 +- .../src/components/DeferredSpinner.tsx | 18 ++- .../design-system/src/components/Dropdown.tsx | 7 +- .../src/components/SelectionCard.tsx | 7 +- .../src/components/__tests__/Banner-test.tsx | 4 +- .../__tests__/DeferredSpinner-test.tsx | 7 +- .../__tests__/SelectionCard-test.tsx | 4 +- .../src/components/input/DatePicker.tsx | 128 +---------------- .../DatePickerCustomCalendarNavigation.tsx | 130 ++++++++++++++++++ .../src/components/input/DateRangePicker.tsx | 8 -- .../src/components/input/InputSearch.tsx | 4 +- .../src/components/input/SearchSelect.tsx | 6 +- .../input/__tests__/DatePicker-test.tsx | 12 +- .../input/__tests__/DateRangePicker-test.tsx | 22 +-- .../input/__tests__/MultiSelectMenu-test.tsx | 5 +- .../__tests__/SearchSelectDropdown-test.tsx | 4 +- .../src/components/modal/Modal.tsx | 6 +- .../components/modal/__tests__/Modal-test.tsx | 8 +- .../design-system/src/helpers/l10n.ts | 33 ----- .../design-system/src/helpers/testUtils.tsx | 45 ++++-- server/sonar-web/src/main/js/app/index.ts | 2 +- .../src/main/js/app/utils/startReactApp.tsx | 8 +- .../apps/issues/sidebar/CreationDateFacet.tsx | 2 - .../components/ProjectActivityDateInput.tsx | 2 - .../src/main/js/helpers/l10nBundle.ts | 19 ++- .../resources/org/sonar/l10n/core.properties | 2 + 27 files changed, 270 insertions(+), 237 deletions(-) create mode 100644 server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx delete mode 100644 server/sonar-web/design-system/src/helpers/l10n.ts diff --git a/server/sonar-web/design-system/src/components/Banner.tsx b/server/sonar-web/design-system/src/components/Banner.tsx index 90a6cf70576..61a6975b125 100644 --- a/server/sonar-web/design-system/src/components/Banner.tsx +++ b/server/sonar-web/design-system/src/components/Banner.tsx @@ -19,6 +19,7 @@ */ import styled from '@emotion/styled'; import { ReactNode } from 'react'; +import { useIntl } from 'react-intl'; import tw from 'twin.macro'; import { LAYOUT_BANNER_HEIGHT, @@ -26,7 +27,6 @@ import { 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'; @@ -69,6 +69,8 @@ function getVariantInfo(variant: Variant) { export function Banner({ children, onDismiss, variant }: Props) { const variantInfo = getVariantInfo(variant); + const intl = useIntl(); + return (
diff --git a/server/sonar-web/design-system/src/components/Breadcrumbs.tsx b/server/sonar-web/design-system/src/components/Breadcrumbs.tsx index f367e38965e..c4a4c77693a 100644 --- a/server/sonar-web/design-system/src/components/Breadcrumbs.tsx +++ b/server/sonar-web/design-system/src/components/Breadcrumbs.tsx @@ -20,6 +20,7 @@ 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, @@ -28,7 +29,6 @@ import { themeColor, themeContrast, } from '../helpers'; -import { translate } from '../helpers/l10n'; import { useResizeObserver } from '../hooks/useResizeObserver'; import { Dropdown } from './Dropdown'; import { InteractiveIcon } from './InteractiveIcon'; @@ -61,6 +61,8 @@ export function Breadcrumbs(props: Props) { } = props; const [lengthOfChildren, setLengthOfChildren] = React.useState([]); + const intl = useIntl(); + const breadcrumbRef = React.useCallback((node: HTMLLIElement, index: number) => { setLengthOfChildren((value) => { if (value[index] === node.offsetWidth) { @@ -138,7 +140,7 @@ export function Breadcrumbs(props: Props) { return ( @@ -155,7 +157,7 @@ export function Breadcrumbs(props: Props) { > diff --git a/server/sonar-web/design-system/src/components/DeferredSpinner.tsx b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx index b5fcd1067f1..b6179452123 100644 --- a/server/sonar-web/design-system/src/components/DeferredSpinner.tsx +++ b/server/sonar-web/design-system/src/components/DeferredSpinner.tsx @@ -19,9 +19,9 @@ */ 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 { @@ -83,7 +83,8 @@ export class DeferredSpinner extends React.PureComponent { if (customSpinner) { return customSpinner; } - return ; + // Overwrite aria-label only if defined + return ; } if (children) { return children; @@ -105,7 +106,8 @@ const spinAnimation = keyframes` } `; -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; @@ -120,7 +122,13 @@ export const Spinner = styled.div` ${tw`sw-rounded-pill`} `; -Spinner.defaultProps = { 'aria-label': translate('loading'), role: 'status' }; +function Spinner(props: DetailedHTMLProps, HTMLDivElement>) { + const intl = useIntl(); + + return ( + + ); +} const Placeholder = styled.div` position: relative; diff --git a/server/sonar-web/design-system/src/components/Dropdown.tsx b/server/sonar-web/design-system/src/components/Dropdown.tsx index c7d7b530e3e..6fb33b5853b 100644 --- a/server/sonar-web/design-system/src/components/Dropdown.tsx +++ b/server/sonar-web/design-system/src/components/Dropdown.tsx @@ -19,7 +19,7 @@ */ 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'; @@ -147,11 +147,14 @@ interface ActionsDropdownProps extends Omit { export function ActionsDropdown(props: ActionsDropdownProps) { const { children, buttonSize, ariaLabel, ...dropdownProps } = props; + + const intl = useIntl(); + return ( diff --git a/server/sonar-web/design-system/src/components/SelectionCard.tsx b/server/sonar-web/design-system/src/components/SelectionCard.tsx index 41c5541a3ea..9f14e1df853 100644 --- a/server/sonar-web/design-system/src/components/SelectionCard.tsx +++ b/server/sonar-web/design-system/src/components/SelectionCard.tsx @@ -19,8 +19,8 @@ */ 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'; @@ -53,6 +53,9 @@ export function SelectionCard(props: SelectionCardProps) { vertical = false, } = props; const isActionable = Boolean(onClick); + + const intl = useIntl(); + return ( - {translate('recommended')} {recommendedReason} + {intl.formatMessage({ id: 'recommended' })} {recommendedReason} )} diff --git a/server/sonar-web/design-system/src/components/__tests__/Banner-test.tsx b/server/sonar-web/design-system/src/components/__tests__/Banner-test.tsx index f07e4d6daf4..7f8acf474b5 100644 --- a/server/sonar-web/design-system/src/components/__tests__/Banner-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/Banner-test.tsx @@ -18,7 +18,7 @@ * 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'; @@ -42,7 +42,7 @@ it('should render with close button', async () => { }); function setupWithProps(props: Partial> = {}) { - return render( + return renderWithContext( {props.children ?? 'Test Message'} diff --git a/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx b/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx index 9e5b35753b2..d8dd6ab7bf0 100644 --- a/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/DeferredSpinner-test.tsx @@ -18,6 +18,7 @@ * 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(() => { @@ -65,5 +66,9 @@ function renderDeferredSpinner(props: Partial = {}) { } function prepareDeferredSpinner(props: Partial = {}) { - return ; + return ( + + + + ); } diff --git a/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx b/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx index 36f248ebe9b..9a48c7fbe72 100644 --- a/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx +++ b/server/sonar-web/design-system/src/components/__tests__/SelectionCard-test.tsx @@ -19,7 +19,7 @@ */ 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'; @@ -67,7 +67,7 @@ it('should not be actionnable when no click handler', () => { }); function renderSelectionCard(props: Partial> = {}) { - return render( + return renderWithContext( { render() { const { alignRight, - ariaNextMonthLabel, - ariaPreviousMonthLabel, clearButtonLabel, highlightFrom, highlightTo, @@ -181,10 +160,7 @@ export class DatePicker extends React.PureComponent { captionLayout="dropdown-buttons" className="sw-body-sm" components={{ - Caption: getCustomCalendarNavigation({ - ariaNextMonthLabel, - ariaPreviousMonthLabel, - }), + Caption: CustomCalendarNavigation, }} disabled={{ after: maxDate, before: minDate }} formatters={{ @@ -320,97 +296,3 @@ const DayPicker = styled(OriginalDayPicker)` 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 ( - - ); - }; -} diff --git a/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx b/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx new file mode 100644 index 00000000000..6ce279b7ae3 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/DatePickerCustomCalendarNavigation.tsx @@ -0,0 +1,130 @@ +/* + * 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 ( + + ); +} diff --git a/server/sonar-web/design-system/src/components/input/DateRangePicker.tsx b/server/sonar-web/design-system/src/components/input/DateRangePicker.tsx index f3f65ea29c1..393946732ea 100644 --- a/server/sonar-web/design-system/src/components/input/DateRangePicker.tsx +++ b/server/sonar-web/design-system/src/components/input/DateRangePicker.tsx @@ -31,8 +31,6 @@ interface DateRange { interface Props { alignEndDateCalandarRight?: boolean; - ariaNextMonthLabel: string; - ariaPreviousMonthLabel: string; className?: string; clearButtonLabel: string; fromLabel: string; @@ -75,8 +73,6 @@ export class DateRangePicker extends React.PureComponent { render() { const { alignEndDateCalandarRight, - ariaNextMonthLabel, - ariaPreviousMonthLabel, clearButtonLabel, fromLabel, minDate, @@ -90,8 +86,6 @@ export class DateRangePicker extends React.PureComponent { return (
{ {separatorText ?? '–'} { props.selectProps.onInputChange(v, { action: 'input-change', prevInputValue: prevValue }); }; @@ -107,7 +109,7 @@ export function SearchSelectInput< return ( = (minLength ?? 0)} minLength={minLength} onChange={onChange} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx index 81d9f167caf..dd68da9b975 100644 --- a/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx +++ b/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx @@ -21,7 +21,7 @@ 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 () => { @@ -40,7 +40,7 @@ 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(); @@ -54,9 +54,7 @@ it('behaves correctly', async () => { * 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(); @@ -131,10 +129,8 @@ it.each([ function renderDatePicker(overrides: Partial = {}) { const defaultFormatter = (date?: Date) => (date ? date.toISOString() : ''); - render( + renderWithContext( { @@ -79,15 +79,15 @@ function renderDateRangePicker(overrides: Partial = {} date ? formatISO(date, { representation: 'date' }) : ''; render( - + + + ); } diff --git a/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx index a1c7b5800ea..7035ce401f2 100644 --- a/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx +++ b/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx @@ -17,8 +17,9 @@ * 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']; @@ -94,7 +95,7 @@ it('should show no results', () => { }); function renderMultiselect(props: Partial = {}) { - return render( + return renderWithContext( { }); function renderSearchSelectDropdown(props: Partial> = {}) { - return render( + return renderWithContext( @@ -118,7 +118,7 @@ export function Modal({ onClick={onClose} type="reset" > - {props.secondaryButtonLabel ?? translate('close')} + {props.secondaryButtonLabel ?? intl.formatMessage({ id: 'close' })} } /> diff --git a/server/sonar-web/design-system/src/components/modal/__tests__/Modal-test.tsx b/server/sonar-web/design-system/src/components/modal/__tests__/Modal-test.tsx index 86085f547dd..185f69f006b 100644 --- a/server/sonar-web/design-system/src/components/modal/__tests__/Modal-test.tsx +++ b/server/sonar-web/design-system/src/components/modal/__tests__/Modal-test.tsx @@ -19,7 +19,7 @@ */ 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 () => { @@ -61,7 +61,7 @@ it('should request close when pressing esc on loose content', async () => { }); function setupPredefinedContent(props: Partial = {}) { - return render( + return renderWithContext( = {}) { } function setupLooseContent(props: Partial = {}, children =
) { - return render( + return renderWithContext( {children} @@ -81,7 +81,7 @@ function setupLooseContent(props: Partial = {}, children = = {}) { - return render( + return renderWithContext(
Hello there!
How are you?
diff --git a/server/sonar-web/design-system/src/helpers/l10n.ts b/server/sonar-web/design-system/src/helpers/l10n.ts deleted file mode 100644 index 3505a1423e5..00000000000 --- a/server/sonar-web/design-system/src/helpers/l10n.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 { - return `${messageKey}.${parameters.join('.')}`; -} diff --git a/server/sonar-web/design-system/src/helpers/testUtils.tsx b/server/sonar-web/design-system/src/helpers/testUtils.tsx index 516c427ba51..da50b78db85 100644 --- a/server/sonar-web/design-system/src/helpers/testUtils.tsx +++ b/server/sonar-web/design-system/src/helpers/testUtils.tsx @@ -24,7 +24,7 @@ import { InitialEntry } from 'history'; 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( @@ -60,12 +60,14 @@ export function renderWithRouter( function RouterWrapper({ children }: React.PropsWithChildren) { return ( - - - - {additionalRoutes} - - + + + + + {additionalRoutes} + + + ); } @@ -77,9 +79,7 @@ function getContextWrapper() { return function ContextWrapper({ children }: React.PropsWithChildren) { return ( - - {children} - + {children} ); }; @@ -113,3 +113,28 @@ export const debounceTimer = jest return debounced; }); + +export function IntlWrapper({ + children, + messages = {}, +}: { + children: ReactNode; + messages?: Record; +}) { + return ( + { + // ignore missing translations, there are none! + if (e.code !== ReactIntlErrorCode.MISSING_TRANSLATION) { + // eslint-disable-next-line no-console + console.error(e); + } + }} + > + {children} + + ); +} diff --git a/server/sonar-web/src/main/js/app/index.ts b/server/sonar-web/src/main/js/app/index.ts index 81e528d133a..8dfc13fcf50 100644 --- a/server/sonar-web/src/main/js/app/index.ts +++ b/server/sonar-web/src/main/js/app/index.ts @@ -48,7 +48,7 @@ async function initApplication() { }); const startReactApp = await import('./utils/startReactApp').then((i) => i.default); - startReactApp(l10nBundle.locale, currentUser, appState, availableFeatures); + startReactApp(l10nBundle, currentUser, appState, availableFeatures); } function isMainApp() { diff --git a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx index def7cb998a2..370f86af3fa 100644 --- a/server/sonar-web/src/main/js/app/utils/startReactApp.tsx +++ b/server/sonar-web/src/main/js/app/utils/startReactApp.tsx @@ -23,7 +23,7 @@ import { lightTheme } from 'design-system'; 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'; @@ -177,7 +177,7 @@ function renderRedirects() { const queryClient = new QueryClient(); export default function startReactApp( - lang: string, + l10nBundle: IntlShape, currentUser?: CurrentUser, appState?: AppState, availableFeatures?: Feature[] @@ -191,7 +191,7 @@ export default function startReactApp( - + @@ -266,7 +266,7 @@ export default function startReactApp( - + diff --git a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx index 39730c1831e..e585b8fb015 100644 --- a/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx +++ b/server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx @@ -177,8 +177,6 @@ export class CreationDateFacetClass extends React.PureComponent return (
= { 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; } @@ -77,7 +84,17 @@ export async function loadL10nBundle() { persistL10nBundleInCache(bundle); - return bundle; + const cache = createIntlCache(); + + intl = createIntl( + { + locale: effectiveLocale, + messages, + }, + cache + ); + + return intl; } function getPreferredLanguage() { diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index 6dd26e61136..4272c60e148 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -31,6 +31,7 @@ blocker=Blocker bold=Bold branch=Branch breadcrumbs=Breadcrumbs +expand_breadcrumbs=Expand breadcrumbs by_=by calendar=Calendar cancel=Cancel @@ -124,6 +125,7 @@ max=Max max_results_reached=Only the first {0} results are displayed me=Me members=Members +menu=Menu min=Min minor=Minor more=More -- 2.39.5