search plugin support i18n

Signed-off-by: mario ma <mario.ma.node@gmail.com>
This commit is contained in:
mario ma
2025-03-26 23:10:41 +08:00
parent 714e86684e
commit fa485943e4
18 changed files with 182 additions and 24 deletions
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-search-react': patch
'@backstage/plugin-search': patch
---
search plugin support i18n
+20
View File
@@ -8,6 +8,7 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ListItemProps } from '@material-ui/core/ListItem';
import { SearchDocument } from '@backstage/plugin-search-common';
import { SearchResult } from '@backstage/plugin-search-common';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
// @alpha (undocumented)
export type BaseSearchResultListItemProps<T = {}> = T & {
@@ -94,6 +95,25 @@ export interface SearchFilterResultTypeBlueprintParams {
value: string;
}
// Warning: (ae-missing-release-tag) "searchReactTranslationRef" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const searchReactTranslationRef: TranslationRef<
'search-react',
{
readonly 'searchBar.title': 'Search';
readonly 'searchBar.placeholder': 'Search in {{org}}';
readonly 'searchFilter.allOptionTitle': 'All';
readonly 'searchPagination.limitLabel': 'Results per page:';
readonly 'searchPagination.limitText': 'of {{num}}';
readonly noResultsDescription: 'Sorry, no results were found';
readonly 'searchResultGroup.linkTitle': 'See All';
readonly 'searchResultGroup.addFilterButtonTitle': 'Add filter';
readonly 'searchResultPager.next': 'Next';
readonly 'searchResultPager.previous': 'Previous';
}
>;
// @alpha (undocumented)
export type SearchResultItemExtensionComponent = <
P extends BaseSearchResultListItemProps,
+1
View File
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export * from './blueprints';
export { searchReactTranslationRef } from '../translation';
@@ -38,6 +38,8 @@ import {
} from 'react';
import useDebounce from 'react-use/esm/useDebounce';
import { SearchContextProvider, useSearch } from '../../context';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
/**
* Props for {@link SearchBarBase}.
@@ -81,6 +83,7 @@ export const SearchBarBase = forwardRef((props: SearchBarBaseProps, ref) => {
const configApi = useApi(configApiRef);
const [value, setValue] = useState<string>('');
const forwardedValueRef = useRef<string>('');
const { t } = useTranslationRef(searchReactTranslationRef);
useEffect(() => {
setValue(prevValue => {
@@ -129,12 +132,15 @@ export const SearchBarBase = forwardRef((props: SearchBarBaseProps, ref) => {
}
}, [onChange, onClear]);
const ariaLabel: string | undefined = label ? undefined : 'Search';
const ariaLabel: string | undefined = label
? undefined
: t('searchBar.title');
const inputPlaceholder =
placeholder ??
`Search in ${configApi.getOptionalString('app.title') || 'Backstage'}`;
t('searchBar.placeholder', {
org: configApi.getOptionalString('app.title') || 'Backstage',
});
const SearchIcon = useApp().getSystemIcon('search') || DefaultSearchIcon;
const startAdornment = (
@@ -31,6 +31,8 @@ import {
} from './SearchFilter.Autocomplete';
import { useAsyncFilterValues, useDefaultFilterValue } from './hooks';
import { ensureFilterValueWithLabel, FilterValue } from './types';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
const useStyles = makeStyles({
label: {
@@ -165,6 +167,7 @@ export const SelectFilter = (props: SearchFilterComponentProps) => {
values: givenValues,
valuesDebounceMs,
} = props;
const { t } = useTranslationRef(searchReactTranslationRef);
useDefaultFilterValue(name, defaultValue);
const asyncValues =
typeof givenValues === 'function' ? givenValues : undefined;
@@ -179,7 +182,10 @@ export const SelectFilter = (props: SearchFilterComponentProps) => {
valuesDebounceMs,
);
const allOptionValue = useRef(uuid());
const allOption = { value: allOptionValue.current, label: 'All' };
const allOption = {
value: allOptionValue.current,
label: t('searchFilter.allOptionTitle'),
};
const { filters, setFilters } = useSearch();
const handleChange = (value: SelectedItems) => {
@@ -23,6 +23,8 @@ import {
} from 'react';
import TablePagination from '@material-ui/core/TablePagination';
import { useSearch } from '../../context';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
const encodePageCursor = (pageCursor: number): string => {
return Buffer.from(pageCursor.toString(), 'utf-8').toString('base64');
@@ -119,15 +121,18 @@ export type SearchPaginationBaseProps = {
* @public
*/
export const SearchPaginationBase = (props: SearchPaginationBaseProps) => {
const { t } = useTranslationRef(searchReactTranslationRef);
const {
total: count = -1,
cursor: pageCursor,
hasNextPage,
onCursorChange: onPageCursorChange,
limit: rowsPerPage = 25,
limitLabel: labelRowsPerPage = 'Results per page:',
limitLabel: labelRowsPerPage = t('searchPagination.limitLabel'),
limitText: labelDisplayedRows = ({ from, to }) =>
count > 0 ? `of ${count}` : `${from}-${to}`,
count > 0
? t('searchPagination.limitText', { num: `${count}` })
: `${from}-${to}`,
limitOptions: rowsPerPageOptions,
onLimitChange: onPageLimitChange,
...rest
@@ -32,6 +32,8 @@ import {
SearchResultListItemExtensions,
SearchResultListItemExtensionsProps,
} from '../../extensions';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
/**
* Props for {@link SearchResultContext}
@@ -186,11 +188,12 @@ export type SearchResultProps = Pick<SearchResultStateProps, 'query'> &
* @public
*/
export const SearchResultComponent = (props: SearchResultProps) => {
const { t } = useTranslationRef(searchReactTranslationRef);
const {
query,
children,
noResultsComponent = (
<EmptyState missing="data" title="Sorry, no results were found" />
<EmptyState missing="data" title={t('noResultsDescription')} />
),
...rest
} = props;
@@ -51,6 +51,8 @@ import { useSearchResultListItemExtensions } from '../../extensions';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
import { searchReactTranslationRef } from '../../translation';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
const useStyles = makeStyles((theme: Theme) => ({
listSubheader: {
@@ -356,6 +358,7 @@ export function SearchResultGroupLayout<FilterOption>(
) {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const { t } = useTranslationRef(searchReactTranslationRef);
const {
error,
@@ -365,7 +368,7 @@ export function SearchResultGroupLayout<FilterOption>(
titleProps = {},
link = (
<>
See all
{t('searchResultGroup.linkTitle')}
<ArrowRightIcon className={classes.listSubheaderLinkIcon} />
</>
),
@@ -387,7 +390,7 @@ export function SearchResultGroupLayout<FilterOption>(
),
disableRenderingWithNoResults,
noResultsComponent = disableRenderingWithNoResults ? null : (
<EmptyState missing="data" title="Sorry, no results were found" />
<EmptyState missing="data" title={t('noResultsDescription')} />
),
...rest
} = props;
@@ -434,7 +437,7 @@ export function SearchResultGroupLayout<FilterOption>(
component="button"
icon={<AddIcon />}
variant="outlined"
label="Add filter"
label={t('searchResultGroup.addFilterButtonTitle')}
aria-controls="filters-menu"
aria-haspopup="true"
onClick={handleClick}
@@ -30,6 +30,8 @@ import { useSearchResultListItemExtensions } from '../../extensions';
import { DefaultResultListItem } from '../DefaultResultListItem';
import { SearchResultState, SearchResultStateProps } from '../SearchResult';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
/**
* Props for {@link SearchResultListLayout}
@@ -72,6 +74,7 @@ export type SearchResultListLayoutProps = ListProps & {
* @public
*/
export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
const { t } = useTranslationRef(searchReactTranslationRef);
const {
error,
loading,
@@ -84,7 +87,7 @@ export const SearchResultListLayout = (props: SearchResultListLayoutProps) => {
),
disableRenderingWithNoResults,
noResultsComponent = disableRenderingWithNoResults ? null : (
<EmptyState missing="data" title="Sorry, no results were found" />
<EmptyState missing="data" title={t('noResultsDescription')} />
),
...rest
} = props;
@@ -20,6 +20,8 @@ import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos';
import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos';
import { useSearch } from '../../context';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { searchReactTranslationRef } from '../../translation';
const useStyles = makeStyles(theme => ({
root: {
@@ -36,6 +38,7 @@ const useStyles = makeStyles(theme => ({
export const SearchResultPager = () => {
const { fetchNextPage, fetchPreviousPage } = useSearch();
const classes = useStyles();
const { t } = useTranslationRef(searchReactTranslationRef);
if (!fetchNextPage && !fetchPreviousPage) {
return <></>;
@@ -49,7 +52,7 @@ export const SearchResultPager = () => {
onClick={fetchPreviousPage}
startIcon={<ArrowBackIosIcon />}
>
Previous
{t('searchResultPager.previous')}
</Button>
<Button
@@ -58,7 +61,7 @@ export const SearchResultPager = () => {
onClick={fetchNextPage}
endIcon={<ArrowForwardIosIcon />}
>
Next
{t('searchResultPager.next')}
</Button>
</nav>
);
+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';
export const searchReactTranslationRef = createTranslationRef({
id: 'search-react',
messages: {
searchBar: {
title: 'Search',
placeholder: 'Search in {{org}}',
},
searchFilter: {
allOptionTitle: 'All',
},
searchPagination: {
limitLabel: 'Results per page:',
limitText: 'of {{num}}',
},
noResultsDescription: 'Sorry, no results were found',
searchResultGroup: {
linkTitle: 'See All',
addFilterButtonTitle: 'Add filter',
},
searchResultPager: {
previous: 'Previous',
next: 'Next',
},
},
});
+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>
@@ -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(_t => _t.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(_t => _t.value === selected)[0]!.name}
</AccordionSummary>
<AccordionDetails classes={{ root: classes.accordionDetails }}>
<List
@@ -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,
];
@@ -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
+40
View File
@@ -0,0 +1,40 @@
/*
* 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';
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',
},
},
});