From 83b05a2ea1b4553b8a95566003647e73de32011a Mon Sep 17 00:00:00 2001 From: weareoutman Date: Tue, 19 Dec 2023 18:54:06 +0800 Subject: [PATCH 1/2] feat: support i18n for search context labels --- README.md | 56 +++++++------- .../src/client/theme/SearchBar/SearchBar.tsx | 74 ++++++++++++------- .../client/theme/SearchPage/SearchPage.tsx | 37 +++++----- .../src/client/theme/hooks/useSearchQuery.ts | 18 ++++- .../client/utils/normalizeContextByPath.ts | 36 +++++++++ docusaurus-search-local/src/declarations.ts | 2 +- docusaurus-search-local/src/index.ts | 5 +- .../src/server/utils/validateOptions.ts | 10 ++- website-multi-docs/docusaurus.config.js | 13 +++- 9 files changed, 166 insertions(+), 85 deletions(-) create mode 100644 docusaurus-search-local/src/client/utils/normalizeContextByPath.ts diff --git a/README.md b/README.md index fd22c982..640dfb06 100644 --- a/README.md +++ b/README.md @@ -69,34 +69,34 @@ module.exports = { ## Theme Options -| Name | Type | Default | Description | -| --------------------------------- | ------------------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| indexDocs | boolean | `true` | Whether to index docs. | -| indexBlog | boolean | `true` | Whether to index blog. | -| indexPages | boolean | `false` | Whether to index pages. | -| docsRouteBasePath | string \| string[] | `"/docs"` | Base route path(s) of docs. Slash at beginning is not required. Note: for [docs-only mode](https://docusaurus.io/docs/docs-introduction#docs-only-mode), this needs to be the same as `routeBasePath` in your `@docusaurus/preset-classic` config e.g., `"/"`. | -| blogRouteBasePath | string \| string[] | `"/blog"` | Base route path(s) of blog. Slash at beginning is not required. | -| language | string \| string[] | `"en"` | All [lunr-languages](https://github.com/MihaiValentin/lunr-languages) supported languages, + `zh` 🔥. | -| hashed | boolean \| `"filename"` \| `"query"` | `false` | Whether to add a hashed query when fetching index (based on the content hash of all indexed `*.md` in `docsDir` and `blogDir` if applicable). Setting to `"filename"` will save hash in filename instead of query. | -| docsDir | string \| string[] | `"docs"` | The dir(s) of docs to get the content hash, it's relative to the dir of your project. | -| blogDir | string \| string[] | `"blog"` | Just like the `docsDir` but applied to blog. | -| removeDefaultStopWordFilter | boolean | `false` | Sometimes people (E.g., us) want to keep the English stop words as indexed, since they maybe are relevant in programming docs. | -| removeDefaultStemmer | boolean | `false` | Enable this if you want to be able to search for any partial word at the cost of search performance. | -| highlightSearchTermsOnTargetPage | boolean | `false` | Highlight search terms on target page. | -| searchResultLimits | number | `8` | Limit the search results. | -| searchResultContextMaxLength | number | `50` | Set the max length of characters of each search result to show. | -| explicitSearchResultPath | boolean | `false` | Whether an explicit path to a heading should be presented on a suggestion template. | -| ignoreFiles | string \| RegExp \| (string \| RegExp)[] | `[]` | Set the match rules to ignore some routes. Put a string if you want an exact match, or put a regex if you want a partial match. Note: without the website base url. | -| ignoreCssSelectors | string \| string[] | `[]` | A list of css selectors to ignore when indexing each page. | -| searchBarShortcut | boolean | `true` | Whether to enable keyboard shortcut to focus in search bar. | -| searchBarShortcutHint | boolean | `true` | Whether to show keyboard shortcut hint in search bar. Disable it if you need to hide the hint while shortcut is still enabled. | -| searchBarPosition | `"auto"` \| `"left"` \| `"right"` | `"auto"` | The side of the navbar the search bar should appear on. By default, it will try to autodetect based on your docusaurus config according to [the docs](https://docusaurus.io/docs/api/themes/configuration#navbar-search). | -| docsPluginIdForPreferredVersion | string | | When you're using multi-instance of docs, set the docs plugin id which you'd like to check the preferred version with, for the search index. | -| zhUserDict | string | | Provide your custom dict for language of zh, [see here](https://github.com/fxsjy/jieba#%E8%BD%BD%E5%85%A5%E8%AF%8D%E5%85%B8) | -| zhUserDictPath | string | | Provide the file path to your custom dict for language of zh, E.g.: `path.resolve("./src/zh-dict.txt")` | -| searchContextByPaths | `(string \| { label: string; path: string; } )[]` | `[]` | Provide an list of sub-paths as separate search context, E.g.: `["docs", "community", "legacy/resources"]`. It will create multiple search indexes by these paths. | -| hideSearchBarWithNoSearchContext | boolean | `false` | Whether to hide the search bar when no search context was matched. By default, if `searchContextByPaths` is set, pages which are not matched with it will be considered as with a search context of ROOT. By setting `hideSearchBarWithNoSearchContext` to false, these pages will be considered as with NO search context, and the search bar will be hidden. | -| useAllContextsWithNoSearchContext | boolean | `false` | Whether to show results from all the contexts if no context is provided. This option should not be used with `hideSearchBarWithNoSearchContext` set to `true` as this would show results when there is no search context. This will duplicate indexes and might have a performance cost depending on the index sizes. | +| Name | Type | Default | Description | +| --------------------------------- | --------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| indexDocs | boolean | `true` | Whether to index docs. | +| indexBlog | boolean | `true` | Whether to index blog. | +| indexPages | boolean | `false` | Whether to index pages. | +| docsRouteBasePath | string \| string[] | `"/docs"` | Base route path(s) of docs. Slash at beginning is not required. Note: for [docs-only mode](https://docusaurus.io/docs/docs-introduction#docs-only-mode), this needs to be the same as `routeBasePath` in your `@docusaurus/preset-classic` config e.g., `"/"`. | +| blogRouteBasePath | string \| string[] | `"/blog"` | Base route path(s) of blog. Slash at beginning is not required. | +| language | string \| string[] | `"en"` | All [lunr-languages](https://github.com/MihaiValentin/lunr-languages) supported languages, + `zh` 🔥. | +| hashed | boolean \| `"filename"` \| `"query"` | `false` | Whether to add a hashed query when fetching index (based on the content hash of all indexed `*.md` in `docsDir` and `blogDir` if applicable). Setting to `"filename"` will save hash in filename instead of query. | +| docsDir | string \| string[] | `"docs"` | The dir(s) of docs to get the content hash, it's relative to the dir of your project. | +| blogDir | string \| string[] | `"blog"` | Just like the `docsDir` but applied to blog. | +| removeDefaultStopWordFilter | boolean | `false` | Sometimes people (E.g., us) want to keep the English stop words as indexed, since they maybe are relevant in programming docs. | +| removeDefaultStemmer | boolean | `false` | Enable this if you want to be able to search for any partial word at the cost of search performance. | +| highlightSearchTermsOnTargetPage | boolean | `false` | Highlight search terms on target page. | +| searchResultLimits | number | `8` | Limit the search results. | +| searchResultContextMaxLength | number | `50` | Set the max length of characters of each search result to show. | +| explicitSearchResultPath | boolean | `false` | Whether an explicit path to a heading should be presented on a suggestion template. | +| ignoreFiles | string \| RegExp \| (string \| RegExp)[] | `[]` | Set the match rules to ignore some routes. Put a string if you want an exact match, or put a regex if you want a partial match. Note: without the website base url. | +| ignoreCssSelectors | string \| string[] | `[]` | A list of css selectors to ignore when indexing each page. | +| searchBarShortcut | boolean | `true` | Whether to enable keyboard shortcut to focus in search bar. | +| searchBarShortcutHint | boolean | `true` | Whether to show keyboard shortcut hint in search bar. Disable it if you need to hide the hint while shortcut is still enabled. | +| searchBarPosition | `"auto"` \| `"left"` \| `"right"` | `"auto"` | The side of the navbar the search bar should appear on. By default, it will try to autodetect based on your docusaurus config according to [the docs](https://docusaurus.io/docs/api/themes/configuration#navbar-search). | +| docsPluginIdForPreferredVersion | string | | When you're using multi-instance of docs, set the docs plugin id which you'd like to check the preferred version with, for the search index. | +| zhUserDict | string | | Provide your custom dict for language of zh, [see here](https://github.com/fxsjy/jieba#%E8%BD%BD%E5%85%A5%E8%AF%8D%E5%85%B8) | +| zhUserDictPath | string | | Provide the file path to your custom dict for language of zh, E.g.: `path.resolve("./src/zh-dict.txt")` | +| searchContextByPaths | `(string \| { label: string \| Record; path: string; } )[]` | `[]` | Provide an list of sub-paths as separate search context, E.g.: `["docs", "community", "legacy/resources"]`. It will create multiple search indexes by these paths. | +| hideSearchBarWithNoSearchContext | boolean | `false` | Whether to hide the search bar when no search context was matched. By default, if `searchContextByPaths` is set, pages which are not matched with it will be considered as with a search context of ROOT. By setting `hideSearchBarWithNoSearchContext: true`, these pages will be considered as with NO search context, and the search bar will be hidden. | +| useAllContextsWithNoSearchContext | boolean | `false` | Whether to show results from all the contexts if no context is provided. This option should not be used with `hideSearchBarWithNoSearchContext: true` as this would show results when there is no search context. This will duplicate indexes and might have a performance cost depending on the index sizes. | ### I18N diff --git a/docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx b/docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx index 5cc74382..5d601ee6 100644 --- a/docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx +++ b/docusaurus-search-local/src/client/theme/SearchBar/SearchBar.tsx @@ -31,10 +31,12 @@ import { indexDocs, searchContextByPaths, hideSearchBarWithNoSearchContext, + useAllContextsWithNoSearchContext, } from "../../utils/proxiedGenerated"; import LoadingRing from "../LoadingRing/LoadingRing"; import styles from "./SearchBar.module.css"; +import { normalizeContextByPath } from "../../utils/normalizeContextByPath"; async function fetchAutoCompleteJS(): Promise { const autoCompleteModule = await import("@easyops-cn/autocomplete.js"); @@ -62,6 +64,7 @@ export default function SearchBar({ const isBrowser = useIsBrowser(); const { siteConfig: { baseUrl }, + i18n: { currentLocale }, } = useDocusaurusContext(); // It returns undefined for non-docs pages @@ -161,39 +164,51 @@ export default function SearchBar({ const a = document.createElement("a"); const params = new URLSearchParams(); - const seeAllResultsText = translate({ - id: "theme.SearchBar.seeAll", - message: "See all results", - }); - - const seeAllResultsOutsideContextText = translate( - { - id: "theme.SearchBar.seeAllOutsideContext", - message: "See results outside {context}", - }, - { context: searchContext } - ); - - const seeAllResultsInContextText = translate( - { - id: "theme.SearchBar.searchInContext", - message: "See all results in {context}", - }, - { context: searchContext } - ); - params.set("q", query); let linkText; - if (searchContext && isEmpty) { - linkText = seeAllResultsOutsideContextText; - } else if (searchContext) { - linkText = seeAllResultsInContextText; + if (searchContext) { + const detailedSearchContext = + searchContext && Array.isArray(searchContextByPaths) + ? searchContextByPaths.find((item) => + typeof item === "string" + ? item === searchContext + : item.path === searchContext + ) + : searchContext; + const translatedSearchContext = detailedSearchContext + ? normalizeContextByPath(detailedSearchContext, currentLocale).label + : searchContext; + + if (useAllContextsWithNoSearchContext && isEmpty) { + linkText = translate( + { + id: "theme.SearchBar.seeAllOutsideContext", + message: "See results outside {context}", + }, + { context: translatedSearchContext } + ); + } else { + linkText = translate( + { + id: "theme.SearchBar.searchInContext", + message: "See all results in {context}", + }, + { context: translatedSearchContext } + ); + } } else { - linkText = seeAllResultsText; + linkText = translate({ + id: "theme.SearchBar.seeAll", + message: "See all results", + }); } - if (Array.isArray(searchContextByPaths) && !isEmpty) { + if ( + searchContext && + Array.isArray(searchContextByPaths) && + (!useAllContextsWithNoSearchContext || !isEmpty) + ) { params.set("ctx", searchContext); } @@ -250,7 +265,10 @@ export default function SearchBar({ suggestion: SuggestionTemplate, empty: EmptyTemplate, footer: ({ query, isEmpty }: any) => { - if (isEmpty && !searchContext) { + if ( + isEmpty && + (!searchContext || !useAllContextsWithNoSearchContext) + ) { return; } const a = searchFooterLinkElement({ query, isEmpty }); diff --git a/docusaurus-search-local/src/client/theme/SearchPage/SearchPage.tsx b/docusaurus-search-local/src/client/theme/SearchPage/SearchPage.tsx index 0088776e..f3cd9225 100644 --- a/docusaurus-search-local/src/client/theme/SearchPage/SearchPage.tsx +++ b/docusaurus-search-local/src/client/theme/SearchPage/SearchPage.tsx @@ -23,6 +23,7 @@ import { } from "../../utils/proxiedGenerated"; import styles from "./SearchPage.module.css"; +import { normalizeContextByPath } from "../../utils/normalizeContextByPath"; export default function SearchPage(): React.ReactElement { return ( @@ -35,6 +36,7 @@ export default function SearchPage(): React.ReactElement { function SearchPageContent(): React.ReactElement { const { siteConfig: { baseUrl }, + i18n: { currentLocale }, } = useDocusaurusContext(); const { selectMessage } = usePluralForm(); @@ -106,10 +108,10 @@ function SearchPageContent(): React.ReactElement { useEffect(() => { async function doFetchIndexes() { - const { wrappedIndexes, zhDictionary } = await fetchIndexes( - versionUrl, - searchContext - ); + const { wrappedIndexes, zhDictionary } = + searchContext || useAllContextsWithNoSearchContext + ? await fetchIndexes(versionUrl, searchContext) + : { wrappedIndexes: [], zhDictionary: [] }; setSearchSource(() => SearchSourceFactory(wrappedIndexes, zhDictionary, 100) ); @@ -166,22 +168,19 @@ function SearchPageContent(): React.ReactElement { value={searchContext} onChange={(e) => updateSearchContext(e.target.value)} > - + {useAllContextsWithNoSearchContext && ( + + )} {searchContextByPaths.map((context) => { - let label: string; - let path: string; - if (typeof context === "string") { - label = path = context; - } else { - ({ label, path } = context); - } + const { label, path } = normalizeContextByPath( + context, + currentLocale + ); return (