Merge pull request #29401 from mario-mui/feat/search-plugin-i18n

search plugin support i18n
This commit is contained in:
Alex Lorenzi
2025-05-07 12:42:34 -04:00
committed by GitHub
24 changed files with 263 additions and 68 deletions
+15
View File
@@ -15,6 +15,7 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
import { SearchFilterExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
const _default: FrontendPlugin<
@@ -247,5 +248,19 @@ export const searchPage: ExtensionDefinition<{
};
}>;
// @alpha (undocumented)
export const searchTranslationRef: TranslationRef<
'search',
{
readonly 'searchModal.viewFullResults': 'View Full Results';
readonly 'searchType.tabs.allTitle': 'All';
readonly 'searchType.allResults': 'All Results';
readonly 'searchType.accordion.collapse': 'Collapse';
readonly 'searchType.accordion.allTitle': 'All';
readonly 'searchType.accordion.numberOfResults': '{{number}} results';
readonly 'sidebarSearchModal.title': 'Search';
}
>;
// (No @packageDocumentation comment for this package)
```
+3
View File
@@ -284,3 +284,6 @@ export default createFrontendPlugin({
root: rootRouteRef,
}),
});
/** @alpha */
export { searchTranslationRef } from './translation';
@@ -39,6 +39,8 @@ import { useNavigate } from 'react-router-dom';
import { rootRouteRef } from '../../plugin';
import { SearchResultSet } from '@backstage/plugin-search-common';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
/**
* @public
@@ -121,6 +123,7 @@ export const Modal = ({
const navigate = useNavigate();
const { transitions } = useTheme();
const { focusContent } = useContent();
const { t } = useTranslationRef(searchTranslationRef);
const searchRootRoute = useRouteRef(rootRouteRef)();
const searchBarRef = useRef<HTMLInputElement | null>(null);
@@ -171,7 +174,7 @@ export const Modal = ({
onClick={handleSearchBarSubmit}
disableRipple
>
View Full Results
{t('searchModal.viewFullResults')}
</Button>
</Grid>
</Grid>
@@ -15,8 +15,12 @@
*/
import { ReactNode } from 'react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { act, render, waitFor } from '@testing-library/react';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { act, waitFor } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
searchApiRef,
@@ -81,7 +85,7 @@ describe('SearchType.Accordion', () => {
};
it('should render as expected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -103,7 +107,7 @@ describe('SearchType.Accordion', () => {
});
it('should set entire types array when a type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -115,7 +119,7 @@ describe('SearchType.Accordion', () => {
});
it('should reset types array when all is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion
name={expectedLabel}
@@ -131,7 +135,7 @@ describe('SearchType.Accordion', () => {
});
it('should reset page cursor when a new type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion name={expectedLabel} types={[expectedType]} />
</Wrapper>,
@@ -143,7 +147,7 @@ describe('SearchType.Accordion', () => {
});
it('should show result counts if enabled', async () => {
const { getAllByText } = render(
const { getAllByText } = await renderInTestApp(
<Wrapper>
<SearchType.Accordion
name={expectedLabel}
@@ -31,6 +31,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import Typography from '@material-ui/core/Typography';
import AllIcon from '@material-ui/icons/FontDownload';
import useAsync from 'react-use/esm/useAsync';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
icon: {
@@ -83,6 +85,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const searchApi = useApi(searchApiRef);
const [expanded, setExpanded] = useState(true);
const { defaultValue, name, showCounts, types: givenTypes } = props;
const { t } = useTranslationRef(searchTranslationRef);
const toggleExpanded = () => setExpanded(prevState => !prevState);
const handleClick = (type: string) => {
@@ -103,7 +106,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const definedTypes = [
{
value: '',
name: 'All',
name: t('searchType.accordion.allTitle'),
icon: <AllIcon />,
},
...givenTypes,
@@ -117,7 +120,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
const counts = await Promise.all(
definedTypes
.map(t => t.value)
.map(type => type.value)
.map(async type => {
const { numberOfResults } = await searchApi.query({
term,
@@ -130,9 +133,10 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
return [
type,
numberOfResults !== undefined
? `${
numberOfResults >= 10000 ? `>10000` : numberOfResults
} results`
? t('searchType.accordion.numberOfResults', {
number:
numberOfResults >= 10000 ? `>10000` : `${numberOfResults}`,
})
: ' -- ',
];
}),
@@ -160,8 +164,8 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
IconButtonProps={{ size: 'small' }}
>
{expanded
? 'Collapse'
: definedTypes.filter(t => t.value === selected)[0]!.name}
? t('searchType.accordion.collapse')
: definedTypes.filter(type => type.value === selected)[0]!.name}
</AccordionSummary>
<AccordionDetails classes={{ root: classes.accordionDetails }}>
<List
@@ -15,8 +15,12 @@
*/
import { ReactNode } from 'react';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { act } from '@testing-library/react';
import user from '@testing-library/user-event';
import {
SearchContextProvider,
@@ -69,7 +73,7 @@ describe('SearchType.Tabs', () => {
};
it('should render as expected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -85,7 +89,7 @@ describe('SearchType.Tabs', () => {
});
it('should set entire types array when a type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -97,7 +101,7 @@ describe('SearchType.Tabs', () => {
});
it('should reset types array when all is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs
defaultValue={expectedType.value}
@@ -112,7 +116,7 @@ describe('SearchType.Tabs', () => {
});
it('should reset page cursor when a new type is selected', async () => {
const { getByText } = render(
const { getByText } = await renderInTestApp(
<Wrapper>
<SearchType.Tabs types={[expectedType]} />
</Wrapper>,
@@ -20,6 +20,8 @@ import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import { makeStyles } from '@material-ui/core/styles';
import { Theme } from '@material-ui/core/styles';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles((theme: Theme) => ({
tabs: {
@@ -49,6 +51,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
const classes = useStyles();
const { setPageCursor, setTypes, types } = useSearch();
const { defaultValue, types: givenTypes } = props;
const { t } = useTranslationRef(searchTranslationRef);
const changeTab = (_: ChangeEvent<{}>, newType: string) => {
setTypes(newType !== '' ? [newType] : []);
@@ -66,7 +69,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => {
const definedTypes = [
{
value: '',
name: 'All',
name: t('searchType.tabs.allTitle'),
},
...givenTypes,
];
@@ -15,14 +15,18 @@
*/
import { configApiRef } from '@backstage/core-plugin-api';
import { render, screen, waitFor } from '@testing-library/react';
import { screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import {
SearchContextProvider,
searchApiRef,
} from '@backstage/plugin-search-react';
import { SearchType } from './SearchType';
import { mockApis, TestApiProvider } from '@backstage/test-utils';
import {
mockApis,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
describe('SearchType', () => {
const initialState = {
@@ -53,7 +57,7 @@ describe('SearchType', () => {
describe('Type Filter', () => {
it('Renders field name and values when provided as props', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -85,7 +89,7 @@ describe('SearchType', () => {
});
it('Renders correctly based on type filter state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -123,7 +127,7 @@ describe('SearchType', () => {
});
it('Renders correctly based on type filter defaultValue', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -156,7 +160,7 @@ describe('SearchType', () => {
});
it('Selecting a value sets type filter state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -199,7 +203,7 @@ describe('SearchType', () => {
});
it('Selecting none defaults to empty state', async () => {
render(
await renderInTestApp(
<TestApiProvider
apis={[
[configApiRef, configApiMock],
@@ -29,6 +29,8 @@ import {
} from './SearchType.Accordion';
import { SearchTypeTabs, SearchTypeTabsProps } from './SearchType.Tabs';
import { useSearch } from '@backstage/plugin-search-react';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
label: {
@@ -63,6 +65,7 @@ const SearchType = (props: SearchTypeProps) => {
const { className, defaultValue, name, values = [] } = props;
const classes = useStyles();
const { types, setTypes } = useSearch();
const { t } = useTranslationRef(searchTranslationRef);
useEffectOnce(() => {
if (!types.length) {
@@ -94,7 +97,7 @@ const SearchType = (props: SearchTypeProps) => {
variant="outlined"
value={types}
onChange={handleChange}
placeholder="All Results"
placeholder={t('searchType.allResults')}
renderValue={selected => (
<div className={classes.chips}>
{(selected as string[]).map(value => (
@@ -22,6 +22,8 @@ import {
SearchModalProvider,
useSearchModal,
} from '../SearchModal';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchTranslationRef } from '../../translation';
/**
* Props for {@link SidebarSearchModal}.
@@ -38,13 +40,14 @@ export type SidebarSearchModalProps = Pick<
const SidebarSearchModalContent = (props: SidebarSearchModalProps) => {
const { state, toggleModal } = useSearchModal();
const Icon = props.icon ? props.icon : SearchIcon;
const { t } = useTranslationRef(searchTranslationRef);
return (
<>
<SidebarItem
className="search-icon"
icon={Icon}
text="Search"
text={t('sidebarSearchModal.title')}
onClick={toggleModal}
/>
<SearchModal
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2025 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { createTranslationRef } from '@backstage/core-plugin-api/alpha';
/**
* @alpha
*/
export const searchTranslationRef = createTranslationRef({
id: 'search',
messages: {
searchModal: {
viewFullResults: 'View Full Results',
},
searchType: {
allResults: 'All Results',
tabs: {
allTitle: 'All',
},
accordion: {
allTitle: 'All',
collapse: 'Collapse',
numberOfResults: '{{number}} results',
},
},
sidebarSearchModal: {
title: 'Search',
},
},
});