diff --git a/.changeset/wicked-clubs-dream.md b/.changeset/wicked-clubs-dream.md new file mode 100644 index 0000000000..c866ac6312 --- /dev/null +++ b/.changeset/wicked-clubs-dream.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-react': patch +'@backstage/plugin-search': patch +--- + +search plugin support i18n diff --git a/plugins/search-react/report-alpha.api.md b/plugins/search-react/report-alpha.api.md index 98a3c533db..afe08b0f64 100644 --- a/plugins/search-react/report-alpha.api.md +++ b/plugins/search-react/report-alpha.api.md @@ -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 & { @@ -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, diff --git a/plugins/search-react/src/alpha/index.ts b/plugins/search-react/src/alpha/index.ts index 239ffa89dd..7c44f59d56 100644 --- a/plugins/search-react/src/alpha/index.ts +++ b/plugins/search-react/src/alpha/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export * from './blueprints'; +export { searchReactTranslationRef } from '../translation'; diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.tsx index 98bfe81b32..d2a1d08283 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.tsx @@ -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(''); const forwardedValueRef = useRef(''); + 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 = ( diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx index da96c711c4..1d0ce79f75 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.tsx @@ -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) => { diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx index 8906b82e79..571fa3f1a1 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.tsx @@ -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 diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.tsx index b0f4570885..99c7f1fb3a 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.tsx @@ -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 & * @public */ export const SearchResultComponent = (props: SearchResultProps) => { + const { t } = useTranslationRef(searchReactTranslationRef); const { query, children, noResultsComponent = ( - + ), ...rest } = props; diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx index 4c2d300314..9a9290bcbc 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.tsx @@ -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( ) { const classes = useStyles(); const [anchorEl, setAnchorEl] = useState(null); + const { t } = useTranslationRef(searchReactTranslationRef); const { error, @@ -365,7 +368,7 @@ export function SearchResultGroupLayout( titleProps = {}, link = ( <> - See all + {t('searchResultGroup.linkTitle')} ), @@ -387,7 +390,7 @@ export function SearchResultGroupLayout( ), disableRenderingWithNoResults, noResultsComponent = disableRenderingWithNoResults ? null : ( - + ), ...rest } = props; @@ -434,7 +437,7 @@ export function SearchResultGroupLayout( component="button" icon={} variant="outlined" - label="Add filter" + label={t('searchResultGroup.addFilterButtonTitle')} aria-controls="filters-menu" aria-haspopup="true" onClick={handleClick} diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx index 48ed8ac92f..a5459d8619 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.tsx @@ -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 : ( - + ), ...rest } = props; diff --git a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx index 764bb28129..d1dca8bb95 100644 --- a/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx +++ b/plugins/search-react/src/components/SearchResultPager/SearchResultPager.tsx @@ -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={} > - Previous + {t('searchResultPager.previous')} ); diff --git a/plugins/search-react/src/translation.ts b/plugins/search-react/src/translation.ts new file mode 100644 index 0000000000..be73ac981e --- /dev/null +++ b/plugins/search-react/src/translation.ts @@ -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', + }, + }, +}); diff --git a/plugins/search/src/alpha.tsx b/plugins/search/src/alpha.tsx index fed0124fa7..5f55b46028 100644 --- a/plugins/search/src/alpha.tsx +++ b/plugins/search/src/alpha.tsx @@ -284,3 +284,6 @@ export default createFrontendPlugin({ root: rootRouteRef, }), }); + +/** @alpha */ +export { searchTranslationRef } from './translation'; diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index 454d2eae96..af7e1c6e10 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -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(null); @@ -171,7 +174,7 @@ export const Modal = ({ onClick={handleSearchBarSubmit} disableRipple > - View Full Results + {t('searchModal.viewFullResults')} diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 7726ee4565..b709f3b12c 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -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: , }, ...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} ({ 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, ]; diff --git a/plugins/search/src/components/SearchType/SearchType.tsx b/plugins/search/src/components/SearchType/SearchType.tsx index 48b78f00d3..b40aa431a1 100644 --- a/plugins/search/src/components/SearchType/SearchType.tsx +++ b/plugins/search/src/components/SearchType/SearchType.tsx @@ -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 => (
{(selected as string[]).map(value => ( diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index ad687a35db..5dde4f385b 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -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 ( <>