Scope search across sections and variants (#3640)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Zeno Kapitein
2025-09-16 12:07:29 +02:00
committed by GitHub
parent ca4a3e8c9a
commit b932e4e2e9
23 changed files with 367 additions and 100 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": minor
---
Scope search across sections and variants
+1 -1
View File
@@ -137,7 +137,7 @@ export function useAI(): AIContext {
setSearchState((prev) => ({
ask: null, // Reset ask as we assume the assistant will handle it
query: prev?.query ?? null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: false,
}));
assistant.open(query);
@@ -146,7 +146,7 @@ export function AIChatProvider(props: {
setSearchState((prev) => ({
ask: prev?.ask ?? initialQuery ?? '',
query: prev?.query ?? null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: false, // Close search popover when opening chat
}));
}, [setSearchState]);
@@ -159,7 +159,7 @@ export function AIChatProvider(props: {
setSearchState((prev) => ({
ask: null,
query: prev?.query ?? null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: false,
}));
}, [setSearchState]);
@@ -374,7 +374,7 @@ export function AIChatProvider(props: {
setSearchState((prev) => ({
ask: input.message,
query: prev?.query ?? null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: false,
}));
}
@@ -435,7 +435,7 @@ export function AIChatProvider(props: {
setSearchState((prev) => ({
ask: '',
query: prev?.query ?? null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: false,
}));
}, [setSearchState]);
@@ -120,9 +120,28 @@ export function Header(props: {
>
<SearchContainer
style={customization.styling.search}
isMultiVariants={siteSpaces.length > 1}
withVariants={withVariants === 'generic'}
withSiteVariants={
sections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.filter(
(s) => s.space.language === siteSpace.space.language
).length > 1
) ?? false
}
withSections={!!sections}
section={
sections
? // Client-encode to avoid a serialisation issue that was causing the language selector to disappear
encodeClientSiteSections(context, sections).current
: undefined
}
spaceTitle={siteSpace.title}
siteSpaceId={siteSpace.id}
siteSpaceIds={siteSpaces
.filter((s) => s.space.language === siteSpace.space.language)
.map((s) => s.id)}
viewport={!withTopHeader ? 'mobile' : undefined}
/>
</div>
@@ -1,6 +1,6 @@
'use client';
import { CustomizationSearchStyle } from '@gitbook/api';
import { CustomizationSearchStyle, type SiteSection } from '@gitbook/api';
import { useRouter } from 'next/navigation';
import React, { useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
@@ -18,9 +18,27 @@ import { SearchScopeToggle } from './SearchScopeToggle';
import { useSearch } from './useSearch';
interface SearchContainerProps {
/** The current site space id. */
siteSpaceId: string;
/** The title of the current space. */
spaceTitle: string;
isMultiVariants: boolean;
/** The ids of all spaces in the current section. */
siteSpaceIds: string[];
/** Whether there are sections on the site. */
withSections: boolean;
/** The current section, displayed in search scope toggle. */
section?: Pick<SiteSection, 'title' | 'icon'>;
/** Whether the current section has variants. */
withVariants: boolean;
/** Whether any section on the site has variants. */
withSiteVariants: boolean;
style: CustomizationSearchStyle;
className?: string;
viewport?: 'desktop' | 'mobile';
@@ -30,7 +48,18 @@ interface SearchContainerProps {
* Client component to render the search input and results.
*/
export function SearchContainer(props: SearchContainerProps) {
const { siteSpaceId, spaceTitle, isMultiVariants, style, className, viewport } = props;
const {
siteSpaceId,
spaceTitle,
section,
withVariants,
withSiteVariants,
withSections,
style,
className,
viewport,
siteSpaceIds,
} = props;
const { assistants } = useAI();
@@ -108,7 +137,7 @@ export function SearchContainer(props: SearchContainerProps) {
}
setSearchState((prev) => ({
ask: withAI ? (prev?.ask ?? null) : null,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
query: prev?.query ?? (withSearchAI || !withAI ? prev?.ask : null) ?? '',
open: true,
}));
@@ -148,7 +177,7 @@ export function SearchContainer(props: SearchContainerProps) {
setSearchState((prev) => ({
ask: withAI && !withSearchAI ? (prev?.ask ?? null) : null, // When typing, we reset ask to get back to normal search (unless non-search assistants are defined)
query: value,
global: prev?.global ?? false,
scope: prev?.scope ?? 'default',
open: true,
}));
};
@@ -168,15 +197,22 @@ export function SearchContainer(props: SearchContainerProps) {
// Only show content if there's a query or Ask is enabled
state?.query || withAI ? (
<React.Suspense fallback={null}>
{isMultiVariants && !showAsk ? (
<SearchScopeToggle spaceTitle={spaceTitle} />
{(withVariants || withSections) && !showAsk ? (
<SearchScopeToggle
section={section}
spaceTitle={spaceTitle}
withVariants={withVariants}
withSiteVariants={withSiteVariants}
withSections={withSections}
/>
) : null}
{state !== null && !showAsk ? (
<SearchResults
ref={resultsRef}
query={normalizedQuery}
global={state?.global ?? false}
scope={state?.scope ?? 'default'}
siteSpaceId={siteSpaceId}
siteSpaceIds={siteSpaceIds}
/>
) : null}
{showAsk ? <SearchAskAnswer query={normalizedAsk} /> : null}
@@ -194,7 +230,7 @@ export function SearchContainer(props: SearchContainerProps) {
onOpenAutoFocus: (event) => event.preventDefault(),
align: 'start',
className:
'bg-tint-base has-[.empty]:hidden gutter-stable scroll-py-2 w-128 p-2 pr-1 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
'@container bg-tint-base has-[.empty]:hidden scroll-py-2 w-128 p-2 max-h-[min(32rem,var(--radix-popover-content-available-height))] max-w-[min(var(--radix-popover-content-available-width),32rem)]',
onInteractOutside: (event) => {
// Don't close if clicking on the search input itself
if (searchInputRef.current?.contains(event.target as Node)) {
@@ -17,9 +17,11 @@ import { SearchSectionResultItem } from './SearchSectionResultItem';
import {
type OrderedComputedResult,
searchAllSiteContent,
searchSiteSpaceContent,
searchCurrentSiteSpaceContent,
searchSpecificSiteSpaceContent,
streamRecommendedQuestions,
} from './server-actions';
import type { SearchScope } from './useSearch';
export interface SearchResultsRef {
moveUp(): void;
@@ -50,12 +52,13 @@ export const SearchResults = React.forwardRef(function SearchResults(
props: {
children?: React.ReactNode;
query: string;
global: boolean;
scope: SearchScope;
siteSpaceId: string;
siteSpaceIds: string[];
},
ref: React.Ref<SearchResultsRef>
) {
const { children, query, global, siteSpaceId } = props;
const { children, query, scope, siteSpaceId, siteSpaceIds } = props;
const language = useLanguage();
const trackEvent = useTrackEvent();
@@ -133,9 +136,25 @@ export const SearchResults = React.forwardRef(function SearchResults(
setResultsState((prev) => ({ results: prev.results, fetching: true }));
let cancelled = false;
const timeout = setTimeout(async () => {
const results = await (global
? searchAllSiteContent(query)
: searchSiteSpaceContent(query));
const results = await (() => {
if (scope === 'all') {
// Search all content on the site
return searchAllSiteContent(query);
}
if (scope === 'default') {
// Search the current section's variant + matched/default variant for other sections
return searchCurrentSiteSpaceContent(query, siteSpaceId);
}
if (scope === 'extended') {
// Search all variants of the current section
return searchSpecificSiteSpaceContent(query, siteSpaceIds);
}
if (scope === 'current') {
// Search only the current section's current variant
return searchSpecificSiteSpaceContent(query, [siteSpaceId]);
}
throw new Error(`Unhandled search scope: ${scope}`);
})();
if (cancelled) {
return;
@@ -158,7 +177,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
cancelled = true;
clearTimeout(timeout);
};
}, [query, global, trackEvent, withAI, siteSpaceId]);
}, [query, scope, trackEvent, withAI, siteSpaceId, siteSpaceIds]);
const results: ResultType[] = React.useMemo(() => {
if (!withAI) {
@@ -1,13 +1,22 @@
'use client';
import { tString, useLanguage } from '@/intl/client';
import { Button } from '../primitives';
import type { SiteSection } from '@gitbook/api';
import { SegmentedControl, SegmentedControlItem } from '../primitives/SegmentedControl';
import { useSearch } from './useSearch';
/**
* Toolbar to toggle between search modes (global or scoped to a space).
* Only visible when the space is in a collection.
*/
export function SearchScopeToggle(props: { spaceTitle: string }) {
const { spaceTitle } = props;
export function SearchScopeToggle(props: {
spaceTitle: string;
section?: Pick<SiteSection, 'title' | 'icon'>;
withVariants: boolean;
withSiteVariants: boolean;
withSections: boolean;
}) {
const { spaceTitle, section, withVariants, withSections, withSiteVariants } = props;
const [state, setSearchState] = useSearch();
const language = useLanguage();
@@ -16,37 +25,82 @@ export function SearchScopeToggle(props: { spaceTitle: string }) {
}
return (
<div
role="toolbar"
aria-orientation="horizontal"
className="mb-2 flex flex-row flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint-subtle p-1"
>
<Button
variant="blank"
size="medium"
className="shrink grow justify-center whitespace-normal"
active={!state.global}
label={tString(language, 'search_scope_space', spaceTitle)}
onClick={() => {
setSearchState({
...state,
global: false,
});
}}
/>
<Button
variant="blank"
size="medium"
className="shrink grow justify-center whitespace-normal"
active={state.global}
label={tString(language, 'search_scope_all')}
onClick={() => {
setSearchState({
...state,
global: true,
});
}}
/>
</div>
<>
{withSections ? (
<SegmentedControl className="animate-scale-in">
{/* `Default` scope = current section's current variant + best match in other sections */}
<SegmentedControlItem
active={
withSiteVariants
? state.scope === 'default'
: ['default', 'all'].includes(state.scope)
}
label={
withSiteVariants
? tString(language, 'search_scope_default')
: tString(language, 'search_scope_all')
}
className={withSiteVariants ? '@max-md:basis-full' : ''}
icon={withSiteVariants ? 'bullseye-arrow' : 'infinity'}
onClick={() => setSearchState({ ...state, scope: 'default' })}
/>
{/* `Current` scope = current section's current variant (with further variant scope selection if necessary) */}
<SegmentedControlItem
active={state.scope === 'current' || state.scope === 'extended'}
icon={section?.icon ?? 'crosshairs'}
label={tString(language, 'search_scope_current', section?.title)}
onClick={() => setSearchState({ ...state, scope: 'current' })}
/>
{/* `All` scope = all content on the site. Only visible if site has variants, otherwise it's the same as default */}
{withSiteVariants ? (
<SegmentedControlItem
active={state.scope === 'all'}
label={tString(language, 'search_scope_all')}
icon="infinity"
onClick={() => setSearchState({ ...state, scope: 'all' })}
/>
) : null}
</SegmentedControl>
) : null}
{withVariants &&
(!withSections || state.scope === 'current' || state.scope === 'extended') ? (
<SegmentedControl className="animate-scale-in">
{/* `Current` scope = current section's current variant. `Default` on sites without sections. */}
<SegmentedControlItem
size={withSections ? 'small' : 'medium'}
active={
withSections
? state.scope === 'current'
: ['default', 'current'].includes(state.scope)
}
className="py-1"
label={tString(language, 'search_scope_current', spaceTitle)}
onClick={() =>
setSearchState({
...state,
scope: withSections ? 'current' : 'default',
})
}
/>
{/* `Extended` scope = all variants of the current section. `All` on sites without sections. */}
<SegmentedControlItem
size={withSections ? 'small' : 'medium'}
active={
withSections
? state.scope === 'extended'
: ['extended', 'all'].includes(state.scope)
}
className="py-1"
label={tString(language, 'search_scope_extended')}
onClick={() =>
setSearchState({ ...state, scope: withSections ? 'extended' : 'all' })
}
/>
</SegmentedControl>
) : null}
</>
);
}
@@ -81,18 +81,33 @@ export async function searchAllSiteContent(query: string): Promise<OrderedComput
/**
* Server action to search content in a space.
*/
export async function searchSiteSpaceContent(query: string): Promise<OrderedComputedResult[]> {
export async function searchCurrentSiteSpaceContent(
query: string,
siteSpaceId: string
): Promise<OrderedComputedResult[]> {
return traceErrorOnly('Search.searchSiteSpaceContent', async () => {
const context = await getServerActionBaseContext();
const siteURLData = await getSiteURLDataFromMiddleware();
return await searchSiteContent(context, {
query,
// If we have a siteSectionId that means its a sections site use `current` mode
// which searches in the current space + all default spaces of sections
scope: siteURLData.siteSection
? { mode: 'current', siteSpaceId: siteURLData.siteSpace }
: { mode: 'specific', siteSpaceIds: [siteURLData.siteSpace] },
scope: { mode: 'current', siteSpaceId },
});
});
}
/**
* Server action to search content in a specific space.
*/
export async function searchSpecificSiteSpaceContent(
query: string,
siteSpaceIds: string[]
): Promise<OrderedComputedResult[]> {
return traceErrorOnly('Search.searchSiteSpaceContent', async () => {
const context = await getServerActionBaseContext();
return await searchSiteContent(context, {
query,
scope: { mode: 'specific', siteSpaceIds },
});
});
}
@@ -1,14 +1,24 @@
'use client';
import { parseAsBoolean, parseAsString, useQueryStates } from 'nuqs';
import { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryStates } from 'nuqs';
import React from 'react';
import type { LinkProps } from '../primitives';
export type SearchScope =
/** Search all content on the site */
| 'all'
/** Search the current section's variant + matched/default variant for other sections */
| 'default'
/** Search all variants of the current section */
| 'extended'
/** Search only the current section's current variant */
| 'current';
export interface SearchState {
// URL-backed state
query: string | null;
ask: string | null;
global: boolean;
scope: SearchScope;
// Local UI state
open: boolean;
@@ -18,7 +28,8 @@ export interface SearchState {
const keyMap = {
q: parseAsString,
ask: parseAsString,
global: parseAsBoolean,
scope: parseAsStringLiteral(['all', 'default', 'extended', 'current']).withDefault('default'),
global: parseAsBoolean, // Legacy support for global=true
};
export type UpdateSearchState = (
@@ -41,14 +52,21 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
history: 'replace',
});
// Handle legacy ask=true format by converting it to the new format
React.useEffect(() => {
// Handle legacy ask=true format by converting it to the new format
if (rawState?.ask === 'true' && rawState?.q) {
// Convert legacy format: q=query&ask=true -> ask=query&q=null
setRawState({
q: null,
ask: rawState.q,
global: rawState.global,
});
}
// Handle legacy global=true
if (rawState?.global === true) {
setRawState({
scope: 'all',
global: null, // Remove the legacy parameter
});
}
}, [rawState, setRawState]);
@@ -65,7 +83,7 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
return {
query: rawState.q,
ask: rawState.ask,
global: !!rawState.global,
scope: rawState.scope,
open,
};
}, [rawState, open]);
@@ -85,14 +103,14 @@ export function SearchContextProvider(props: React.PropsWithChildren): React.Rea
if (update === null) {
setIsOpen(false);
return setRawState({ q: null, ask: null, global: null });
return setRawState({ q: null, ask: null, scope: 'default' });
}
setIsOpen(update.open);
return setRawState({
q: update.query,
ask: update.ask,
global: update.global ? true : null,
scope: update.scope,
});
},
[setRawState]
@@ -126,7 +144,7 @@ export function useSearchLink(): (
const searchParams = new URLSearchParams();
params.query ? searchParams.set('q', params.query) : searchParams.delete('q');
params.ask ? searchParams.set('ask', params.ask) : searchParams.delete('ask');
params.global ? searchParams.set('global', 'true') : searchParams.delete('global');
params.scope ? searchParams.set('scope', params.scope) : searchParams.delete('scope');
return {
href: `?${searchParams.toString()}`,
prefetch: false,
@@ -137,7 +155,7 @@ export function useSearchLink(): (
...prev,
query: params.query !== undefined ? params.query : null,
ask: params.ask !== undefined ? params.ask : null,
global: params.global !== undefined ? params.global : false,
scope: params.scope !== undefined ? params.scope : 'default',
open: params.open !== undefined ? params.open : false,
}));
},
@@ -180,9 +180,29 @@ export function SpaceLayout(props: SpaceLayoutProps) {
<div className="flex gap-2">
<SearchContainer
style={CustomizationSearchStyle.Subtle}
isMultiVariants={siteSpaces.length > 1}
withVariants={withVariants === 'generic'}
withSiteVariants={
sections?.list.some(
(s) =>
s.object === 'site-section' &&
s.siteSpaces.filter(
(s) =>
s.space.language ===
siteSpace.space.language
).length > 1
) ?? false
}
withSections={withSections}
section={sections?.current}
spaceTitle={siteSpace.title}
siteSpaceId={siteSpace.id}
siteSpaceIds={siteSpaces
.filter(
(s) =>
s.space.language ===
siteSpace.space.language
)
.map((s) => s.id)}
className="max-lg:hidden"
viewport="desktop"
/>
@@ -40,6 +40,7 @@ export const variantClasses = {
'bg-transparent',
'text-tint',
'border-0',
'contrast-more:border',
'shadow-none!',
'hover:bg-tint-hover',
'hover:text-tint-strong',
@@ -80,9 +81,9 @@ export const variantClasses = {
],
};
const activeClasses = {
export const activeClasses = {
primary: 'bg-primary-solid-hover',
blank: 'bg-primary-active disabled:bg-primary-active text-primary-strong font-medium hover:text-primary-strong disabled:text-primary-strong hover:bg-primary-active',
blank: 'bg-primary-active contrast-more:bg-primary-12 contrast-more:text-contrast-primary-12 disabled:bg-primary-active text-primary-strong font-medium hover:text-primary-strong disabled:text-primary-strong hover:bg-primary-active',
secondary: 'bg-tint-active disabled:bg-tint-active',
header: 'bg-header-link/3',
};
@@ -134,7 +135,7 @@ export const Button = React.forwardRef<
typeof icon === 'string' ? (
<Icon
icon={icon as IconName}
className={tcls('button-leading-icon size-[1em]')}
className={tcls('button-leading-icon size-[1em] shrink-0')}
/>
) : (
icon
@@ -154,6 +155,7 @@ export const Button = React.forwardRef<
aria-label={label?.toString()}
aria-pressed={active}
target={target}
data-active={active}
{...rest}
>
{content}
@@ -166,6 +168,7 @@ export const Button = React.forwardRef<
aria-label={label?.toString()}
aria-pressed={active}
disabled={disabled}
data-active={active}
{...rest}
>
{content}
@@ -1,6 +1,6 @@
'use client';
import { Icon } from '@gitbook/icons';
import { Icon, type IconName } from '@gitbook/icons';
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
import { createContext, useCallback, useContext, useState } from 'react';
@@ -146,10 +146,20 @@ export function DropdownMenuItem(
active?: boolean;
className?: ClassValue;
children: React.ReactNode;
leadingIcon?: IconName | React.ReactNode;
} & LinkInsightsProps &
RadixDropdownMenu.DropdownMenuItemProps
) {
const { children, active = false, href, className, insights, target, ...rest } = props;
const {
children,
active = false,
href,
className,
insights,
target,
leadingIcon,
...rest
} = props;
const itemClassName = tcls(
'rounded-xs straight-corners:rounded-xs circular-corners:rounded-lg px-3 py-1 text-sm flex gap-2 items-center',
@@ -161,10 +171,22 @@ export function DropdownMenuItem(
className
);
const icon = leadingIcon ? (
typeof leadingIcon === 'string' ? (
<Icon
icon={leadingIcon as IconName}
className={tcls('size-4 shrink-0', active ? 'text-primary' : 'text-tint-subtle')}
/>
) : (
leadingIcon
)
) : null;
if (href) {
return (
<RadixDropdownMenu.Item {...rest} asChild>
<Link href={href} insights={insights} className={itemClassName} target={target}>
{icon}
{children}
</Link>
</RadixDropdownMenu.Item>
@@ -173,6 +195,7 @@ export function DropdownMenuItem(
return (
<RadixDropdownMenu.Item {...rest} className={tcls('px-3 py-1', itemClassName, className)}>
{icon}
{children}
</RadixDropdownMenu.Item>
);
@@ -0,0 +1,35 @@
import { type ClassValue, tcls } from '@/lib/tailwind';
import { Button, type ButtonProps } from './Button';
export function SegmentedControl(props: { children: React.ReactNode; className?: ClassValue }) {
const { children, className } = props;
return (
<div
role="toolbar"
aria-orientation="horizontal"
className={tcls(
'mb-2 flex flex-wrap gap-1 circular-corners:rounded-3xl rounded-corners:rounded-lg bg-tint p-1',
className
)}
>
{children}
</div>
);
}
export function SegmentedControlItem(props: ButtonProps) {
const { size = 'medium', className, ...rest } = props;
return (
<Button
variant="blank"
size={size}
className={tcls(
'shrink grow justify-center whitespace-normal not-contrast-more:data-[active=true]:bg-tint-base',
className
)}
{...rest}
/>
);
}
+4 -2
View File
@@ -12,8 +12,10 @@ export const de = {
search_no_results_for: 'Keine Ergebnisse für "${1}".',
search_no_results: 'Keine Ergebnisse',
search_results_count: '${1} Ergebnisse',
search_scope_space: '${1}',
search_scope_all: 'Alle Inhalte',
search_scope_current: '${1}',
search_scope_extended: 'Alle Inhalte',
search_scope_default: 'Beste Match',
search_scope_all: 'Alles',
ask: 'Fragen',
search_ask: 'Fragen "${1}"',
search_ask_description: 'Finden Sie die Antwort mit ${1}',
+4 -2
View File
@@ -12,8 +12,10 @@ export const en = {
search_no_results_for: 'No results for "${1}".',
search_no_results: 'No results',
search_results_count: '${1} results',
search_scope_space: '${1}',
search_scope_all: 'All content',
search_scope_current: '${1}',
search_scope_extended: 'All content',
search_scope_default: 'Best match',
search_scope_all: 'Everything',
ask: 'Ask',
search_ask: 'Ask "${1}"',
search_ask_description: 'Find the answer with ${1}',
+4 -2
View File
@@ -14,8 +14,10 @@ export const es: TranslationLanguage = {
search_no_results_for: 'No hay resultados para "${1}".',
search_no_results: 'No hay resultados',
search_results_count: '${1} resultados',
search_scope_space: '${1}',
search_scope_all: 'Todo el contenido',
search_scope_current: '${1}',
search_scope_extended: 'Todo el contenido',
search_scope_default: 'Mejor match',
search_scope_all: 'Todo',
ask: 'Preguntar',
search_ask: 'Preguntar "${1}"',
search_ask_description: 'Encuentra la respuesta con ${1}',
+4 -2
View File
@@ -12,8 +12,10 @@ export const fr = {
search_no_results_for: 'Aucun résultat pour « ${1} ».',
search_no_results: 'Aucun résultat',
search_results_count: '${1} résultats',
search_scope_space: '${1}',
search_scope_all: 'Tous les contenus',
search_scope_current: '${1}',
search_scope_extended: 'Tous les contenus',
search_scope_default: 'Meilleur match',
search_scope_all: 'Tout',
ask: 'Poser une question',
search_ask: 'Demander « ${1} »',
search_ask_description: 'Trouvez la réponse avec ${1}',
+4 -2
View File
@@ -14,8 +14,10 @@ export const ja: TranslationLanguage = {
search_no_results_for: '"${1}" の結果はありません。',
search_no_results: '結果がありません',
search_results_count: '${1}件の結果',
search_scope_space: '${1}',
search_scope_all: '全てのコンテンツ',
search_scope_current: '${1}',
search_scope_extended: '全てのコンテンツ',
search_scope_default: '最適なマッチ',
search_scope_all: 'すべて',
ask: '質問する',
search_ask: '"${1}" を質問する',
search_ask_description: '${1}で答えを見つける',
+4 -2
View File
@@ -14,8 +14,10 @@ export const nl: TranslationLanguage = {
search_no_results_for: 'Geen resultaten voor "${1}".',
search_no_results: 'Geen resultaten',
search_results_count: '${1} resultaten',
search_scope_space: '${1}',
search_scope_all: 'Alle inhoud',
search_scope_current: '${1}',
search_scope_extended: 'Alle inhoud',
search_scope_default: 'Beste match',
search_scope_all: 'Alles',
ask: 'Vragen',
search_ask: 'Vraag "${1}"',
search_ask_description: 'Vind het antwoord met ${1}',
+4 -2
View File
@@ -14,8 +14,10 @@ export const no: TranslationLanguage = {
search_no_results_for: 'Ingen resultater for "${1}".',
search_no_results: 'Ingen resultater',
search_results_count: '${1} resultater',
search_scope_space: '${1}',
search_scope_all: 'Alt innhold',
search_scope_current: '${1}',
search_scope_extended: 'Alt innhold',
search_scope_default: 'Beste treff',
search_scope_all: 'Alt',
ask: 'Spør',
search_ask: 'Spør "${1}"',
search_ask_description: 'Finn svaret med ${1}',
@@ -12,8 +12,10 @@ export const pt_br = {
search_no_results_for: 'Sem resultados para "${1}".',
search_no_results: 'Sem resultados',
search_results_count: '${1} resultados',
search_scope_space: '${1}',
search_scope_all: 'Todo o conteúdo',
search_scope_current: '${1}',
search_scope_extended: 'Todo o conteúdo',
search_scope_default: 'Melhor match',
search_scope_all: 'Tudo',
ask: 'Perguntar',
search_ask: 'Perguntar "${1}"',
search_ask_description: 'Encontre a resposta com ${1}',
+4 -2
View File
@@ -12,8 +12,10 @@ export const ru = {
search_no_results_for: 'Нет результатов для "${1}".',
search_no_results: 'Нет результатов',
search_results_count: '${1} — число результатов',
search_scope_space: '${1}',
search_scope_all: 'Все материалы',
search_scope_current: '${1}',
search_scope_extended: 'Все материалы',
search_scope_default: 'Лучший матч',
search_scope_all: 'Всё',
ask: 'Спросить',
search_ask: 'Спросить "${1}"',
search_ask_description: 'Найти ответ с помощью ${1}',
+4 -2
View File
@@ -14,8 +14,10 @@ export const zh: TranslationLanguage = {
search_no_results_for: '没有找到"${1}"的结果。',
search_no_results: '没有找到结果',
search_results_count: '${1} 个结果',
search_scope_space: '${1}',
search_scope_all: '所有内容',
search_scope_current: '${1}',
search_scope_extended: '所有内容',
search_scope_default: '最佳匹配',
search_scope_all: '全部',
ask: '询问',
search_ask: '询问"${1}"',
search_ask_description: '利用${1}找到答案',