mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Show a "Back to [space]" shortcut when a link leads to another space (#4358)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show a "Back to [space]" shortcut at the top of the table of contents when a reader follows a link into a different space, so they don't lose track of where they were browsing from.
|
||||
@@ -1,7 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { joinPath } from '@/lib/paths';
|
||||
import { useCurrentPageMetadata, useCurrentPagePath } from '../hooks';
|
||||
import {
|
||||
markSpaceNavigationFromPickerOnClick,
|
||||
useCurrentPageMetadata,
|
||||
useCurrentPagePath,
|
||||
} from '../hooks';
|
||||
import { DropdownMenuItem } from '../primitives/DropdownMenu';
|
||||
|
||||
interface VariantSpace {
|
||||
@@ -60,7 +64,18 @@ export function SpacesDropdownMenuItem(props: {
|
||||
const variantHref = useVariantSpaceHref(variantSpace, currentSpacePath, active);
|
||||
|
||||
return (
|
||||
<DropdownMenuItem key={variantSpace.id} href={variantHref} active={active}>
|
||||
<DropdownMenuItem
|
||||
key={variantSpace.id}
|
||||
href={variantHref}
|
||||
active={active}
|
||||
onClick={(event) => {
|
||||
// Switching variant/translation through the picker shouldn't offer to
|
||||
// navigate "back" to the space we're leaving.
|
||||
if (!active) {
|
||||
markSpaceNavigationFromPickerOnClick(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{variantSpace.title}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '@/lib/icons/inline';
|
||||
import { getResizedImageURL } from '@/lib/images';
|
||||
import { resolveContentRef } from '@/lib/references';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { getSiteStructureTitle } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getPageRSSURL } from '@/routes/rss';
|
||||
import { PageContextProvider } from '../PageContext';
|
||||
@@ -149,31 +149,6 @@ export async function generateSitePageViewport(context: GitBookSiteContext): Pro
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A string concatenation of the site structure (sections and variants) titles.
|
||||
*/
|
||||
function getSiteStructureTitle(context: GitBookSiteContext): string | null {
|
||||
const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const title = [];
|
||||
if (
|
||||
sections &&
|
||||
sections.current.default === false && // Only if the current section is not the default one
|
||||
sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections
|
||||
) {
|
||||
title.push(getLocalizedTitle(sections.current, currentLanguage));
|
||||
}
|
||||
if (
|
||||
siteSpaces.length > 1 && // Only if there are multiple variants
|
||||
siteSpace.default === false && // Only if the variant is not the default one
|
||||
siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions.
|
||||
) {
|
||||
title.push(getLocalizedTitle(siteSpace, siteSpace.space.language));
|
||||
}
|
||||
return title.join(' ');
|
||||
}
|
||||
|
||||
export async function generateSitePageMetadata(props: SitePageProps): Promise<Metadata> {
|
||||
const { context, pageTarget, pageMetaLinks } = await getPageDataWithFallback({
|
||||
context: props.context,
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from 'react';
|
||||
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { findSectionInGroup } from '@/lib/utils';
|
||||
import { useToggleAnimation } from '../hooks';
|
||||
import { markSpaceNavigationFromPickerOnClick, useToggleAnimation } from '../hooks';
|
||||
import { Link, ToggleChevron } from '../primitives';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SectionIcon } from './SectionIcon';
|
||||
@@ -31,6 +31,7 @@ export function SiteSectionList(props: { sections: ClientSiteSections; className
|
||||
sectionsAndGroups.length > 0 && (
|
||||
<nav
|
||||
aria-label="Sections"
|
||||
onClickCapture={markSpaceNavigationFromPickerOnClick}
|
||||
className={tcls(
|
||||
'-mx-5 before:contents[] relative border-tint-subtle border-b from-transparent sidebar-filled:to-tint-subtle theme-muted:to-tint-subtle to-tint-base text-sm text-tint before:pointer-events-none before:absolute before:right-2 before:bottom-0 before:left-0 before:h-12 before:bg-linear-to-b [html.sidebar-filled.theme-bold.tint_&]:to-tint-base [html.sidebar-filled.theme-bold.tint_&]:to-tint-subtle [html.sidebar-filled.theme-muted_&]:to-tint-base',
|
||||
className
|
||||
|
||||
@@ -7,6 +7,7 @@ import React from 'react';
|
||||
import { Button, Link, ToggleChevron } from '@/components/primitives';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { findSectionInGroup } from '@/lib/utils';
|
||||
import { markSpaceNavigationFromPickerOnClick } from '../hooks';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { CONTAINER_STYLE } from '../layout';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
@@ -64,6 +65,7 @@ export function SiteSectionTabs(props: {
|
||||
className
|
||||
)}
|
||||
ref={containerRef}
|
||||
onClickCapture={markSpaceNavigationFromPickerOnClick}
|
||||
style={
|
||||
{
|
||||
'--site-section-column-width': COLUMN_WIDTH,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { Icon } from '@gitbook/icons';
|
||||
|
||||
import { t, useLanguage } from '@/intl/client';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
import { useBackToSpace } from '../hooks';
|
||||
import { Link } from '../primitives';
|
||||
|
||||
/**
|
||||
* Shortcut displayed at the top of the ToC to navigate back to the space the reader
|
||||
* came from, when they followed a link into a different space.
|
||||
*
|
||||
* It renders nothing until a cross-space navigation is detected client-side (see
|
||||
* {@link useBackToSpace}), so it has no effect on the vast majority of page views.
|
||||
*/
|
||||
export function BackToSpaceButton(props: { spaceId: string; spaceTitle: string }) {
|
||||
const { spaceId, spaceTitle } = props;
|
||||
const language = useLanguage();
|
||||
const backToSpace = useBackToSpace({ spaceId, spaceTitle });
|
||||
|
||||
if (!backToSpace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-2 px-2">
|
||||
<Link
|
||||
href={backToSpace.url}
|
||||
className={tcls(
|
||||
'group/back-to-space',
|
||||
'flex items-center gap-2',
|
||||
'px-3 py-2',
|
||||
'circular-corners:rounded-xl rounded-md straight-corners:rounded-none',
|
||||
'text-sm text-tint',
|
||||
'bg-tint-base ring-1 ring-tint-subtle ring-inset',
|
||||
'transition-colors',
|
||||
'hover:bg-tint-hover hover:text-tint-strong',
|
||||
'contrast-more:ring-tint'
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon="arrow-left"
|
||||
className={tcls(
|
||||
'size-3 shrink-0 text-tint-subtle transition-transform',
|
||||
'group-hover/back-to-space:-translate-x-0.5 group-hover/back-to-space:text-current'
|
||||
)}
|
||||
/>
|
||||
<span className="truncate">
|
||||
{t(language, 'toc_back_to_space', backToSpace.spaceTitle)}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
import type React from 'react';
|
||||
|
||||
import { getSiteStructureTitle } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SideSheet } from '../primitives/SideSheet';
|
||||
import { BackToSpaceButton } from './BackToSpaceButton';
|
||||
import { PagesList } from './PagesList';
|
||||
import { TableOfContentsScript } from './TableOfContentsScript';
|
||||
import { Trademark } from './Trademark';
|
||||
@@ -119,6 +121,12 @@ export async function TableOfContents(props: {
|
||||
'[html.sidebar-filled.circular-corners_&]:layout-wide:rounded-4xl'
|
||||
)}
|
||||
>
|
||||
<BackToSpaceButton
|
||||
spaceId={context.space.id}
|
||||
// Use the section/variant title as shown in the site navigation,
|
||||
// falling back to the site title for the default space.
|
||||
spaceTitle={getSiteStructureTitle(context) || context.site.title}
|
||||
/>
|
||||
{innerHeader}
|
||||
<ScrollContainer
|
||||
data-testid="toc-scroll-container"
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './useCurrentPage';
|
||||
export * from './useNow';
|
||||
export * from './useListOverflow';
|
||||
export * from './useCurrentPageMetadata';
|
||||
export * from './useBackToSpace';
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
|
||||
import { type SpaceLocation, resolveBackToSpace } from './useBackToSpace';
|
||||
|
||||
const gettingStarted: SpaceLocation = {
|
||||
spaceId: 'space_gs',
|
||||
spaceTitle: 'Getting Started',
|
||||
url: '/docs/getting-started',
|
||||
};
|
||||
const programmersGuide: SpaceLocation = {
|
||||
spaceId: 'space_pg',
|
||||
spaceTitle: "Programmer's Guide",
|
||||
url: '/docs/programmers-guide',
|
||||
};
|
||||
const apiReference: SpaceLocation = {
|
||||
spaceId: 'space_api',
|
||||
spaceTitle: 'API Reference',
|
||||
url: '/docs/api',
|
||||
};
|
||||
|
||||
describe('resolveBackToSpace', () => {
|
||||
it('offers nothing on the first navigation of a session', () => {
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: gettingStarted,
|
||||
lastLocation: null,
|
||||
storedBack: null,
|
||||
fromPicker: false,
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('offers to go back after following a link into a different space', () => {
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: programmersGuide,
|
||||
lastLocation: gettingStarted,
|
||||
storedBack: null,
|
||||
fromPicker: false,
|
||||
})
|
||||
).toEqual(gettingStarted);
|
||||
});
|
||||
|
||||
it('keeps the back target while browsing within the destination space', () => {
|
||||
const deeperInGuide: SpaceLocation = {
|
||||
...programmersGuide,
|
||||
url: '/docs/programmers-guide/chapter-2',
|
||||
};
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: deeperInGuide,
|
||||
lastLocation: programmersGuide,
|
||||
storedBack: gettingStarted,
|
||||
fromPicker: false,
|
||||
})
|
||||
).toEqual(gettingStarted);
|
||||
});
|
||||
|
||||
it('does not offer to go back when the space was changed through the picker', () => {
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: programmersGuide,
|
||||
lastLocation: gettingStarted,
|
||||
storedBack: null,
|
||||
fromPicker: true,
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('clears the back target once the reader returns to the space they came from', () => {
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: gettingStarted,
|
||||
lastLocation: programmersGuide,
|
||||
storedBack: gettingStarted,
|
||||
fromPicker: false,
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('points back to the most recent space when chaining cross-space links', () => {
|
||||
// Was in Getting Started (stored back), followed a link from the Programmer's
|
||||
// Guide into the API Reference: back should now point to the Programmer's Guide.
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: apiReference,
|
||||
lastLocation: programmersGuide,
|
||||
storedBack: gettingStarted,
|
||||
fromPicker: false,
|
||||
})
|
||||
).toEqual(programmersGuide);
|
||||
});
|
||||
|
||||
it('resets the back target when switching space through the picker even if one was set', () => {
|
||||
expect(
|
||||
resolveBackToSpace({
|
||||
current: apiReference,
|
||||
lastLocation: programmersGuide,
|
||||
storedBack: gettingStarted,
|
||||
fromPicker: true,
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
import * as React from 'react';
|
||||
|
||||
import {
|
||||
getSessionStorageItem,
|
||||
removeSessionStorageItem,
|
||||
setSessionStorageItem,
|
||||
} from '@/lib/browser';
|
||||
|
||||
/**
|
||||
* A location visited by the reader, scoped to the space it belongs to.
|
||||
*/
|
||||
export type SpaceLocation = {
|
||||
/** Identifier of the space. */
|
||||
spaceId: string;
|
||||
/** Localized title of the space, used as the label of the "back" shortcut. */
|
||||
spaceTitle: string;
|
||||
/** Path (with query string) of the last page visited in the space. */
|
||||
url: string;
|
||||
};
|
||||
|
||||
// Keys used to persist the navigation state for the current tab session.
|
||||
const LAST_LOCATION_KEY = 'gitbook-space-navigation:last';
|
||||
const BACK_TO_SPACE_KEY = 'gitbook-space-navigation:back';
|
||||
const FROM_PICKER_KEY = 'gitbook-space-navigation:from-picker';
|
||||
|
||||
/**
|
||||
* Mark that the navigation about to happen was initiated from the section/variant
|
||||
* picker (spaces dropdown, translations dropdown or section switcher).
|
||||
*
|
||||
* Such navigations are deliberate moves within the site's own structure and should
|
||||
* not surface a "Back to space" shortcut. Set synchronously when a picker link is
|
||||
* activated, so the flag is in place before the destination page reads it.
|
||||
*/
|
||||
function markSpaceNavigationFromPicker() {
|
||||
setSessionStorageItem(FROM_PICKER_KEY, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a click opens its link in a new tab or window instead of navigating the
|
||||
* current tab (a modifier key or a non-primary button). These must not mark a picker
|
||||
* navigation: the current tab stays put, so the flag would otherwise linger and be
|
||||
* mis-attributed to a later, unrelated navigation.
|
||||
*/
|
||||
function opensInNewTab(event: React.MouseEvent): boolean {
|
||||
return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click handler for the section/variant/translation pickers that marks the upcoming
|
||||
* navigation as picker-initiated, so arriving in the target space doesn't surface a
|
||||
* "Back to space" shortcut.
|
||||
*
|
||||
* Attach it with `onClickCapture` on a picker container (section list/tabs) or as the
|
||||
* `onClick` of a picker menu item (spaces dropdown). It intentionally does nothing for
|
||||
* clicks that leave the current tab unchanged: re-selecting the already-active item,
|
||||
* or opening the link in a new tab/window.
|
||||
*/
|
||||
export function markSpaceNavigationFromPickerOnClick(event: React.MouseEvent<HTMLElement>) {
|
||||
if (opensInNewTab(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = target.closest('a[href]');
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip the currently active item: re-selecting it doesn't navigate. The sidebar
|
||||
// section list marks the active link with `aria-current`, while the header section
|
||||
// tabs (rendered via the Button primitive) use `aria-pressed`.
|
||||
if (
|
||||
link.getAttribute('aria-current') === 'page' ||
|
||||
link.getAttribute('aria-pressed') === 'true'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
markSpaceNavigationFromPicker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which space (if any) the reader should be offered to navigate back to,
|
||||
* given the current location and the state persisted from the previous navigation.
|
||||
*
|
||||
* This is the pure decision logic behind {@link useBackToSpace}, extracted so it can
|
||||
* be unit-tested without a DOM.
|
||||
*/
|
||||
export function resolveBackToSpace(input: {
|
||||
current: SpaceLocation;
|
||||
lastLocation: SpaceLocation | null;
|
||||
storedBack: SpaceLocation | null;
|
||||
fromPicker: boolean;
|
||||
}): SpaceLocation | null {
|
||||
const { current, lastLocation, storedBack, fromPicker } = input;
|
||||
|
||||
// No previous navigation this session, or we're still browsing the same space:
|
||||
// keep whatever "back" target we already had (it persists while browsing).
|
||||
if (!lastLocation || lastLocation.spaceId === current.spaceId) {
|
||||
return storedBack && storedBack.spaceId !== current.spaceId ? storedBack : null;
|
||||
}
|
||||
|
||||
// The reader moved to a different space since the last navigation.
|
||||
|
||||
// Switching space through the picker is a deliberate navigation within the
|
||||
// site structure: reset the context instead of offering to go back.
|
||||
if (fromPicker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The reader returned to the space they originally came from.
|
||||
if (storedBack && storedBack.spaceId === current.spaceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The reader followed a link into a different space: offer to go back to the
|
||||
// page they were on.
|
||||
return lastLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track navigation between spaces and return the space to offer navigating back to.
|
||||
*
|
||||
* When a reader follows a link (in the ToC or in the content) into a space different
|
||||
* from the one they were browsing, we remember where they came from so a "Back to
|
||||
* [space]" shortcut can be surfaced. The shortcut persists while they browse the
|
||||
* destination space and is cleared once they return or switch space through the
|
||||
* section/variant picker.
|
||||
*
|
||||
* The state is kept in `sessionStorage` so it survives both client-side and full-page
|
||||
* navigations within the tab, without leaking across tabs or sessions.
|
||||
*/
|
||||
export function useBackToSpace(current: {
|
||||
spaceId: string;
|
||||
spaceTitle: string;
|
||||
}): SpaceLocation | null {
|
||||
const { spaceId, spaceTitle } = current;
|
||||
const pathname = usePathname();
|
||||
const [backToSpace, setBackToSpace] = React.useState<SpaceLocation | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const currentLocation: SpaceLocation = {
|
||||
spaceId,
|
||||
spaceTitle,
|
||||
// `pathname` (from Next) is the app-relative path used for client navigation,
|
||||
// paired with the current query string so we return to the exact same page.
|
||||
url: `${pathname}${window.location.search}`,
|
||||
};
|
||||
|
||||
const fromPicker = getSessionStorageItem<boolean>(FROM_PICKER_KEY, false);
|
||||
removeSessionStorageItem(FROM_PICKER_KEY);
|
||||
|
||||
const back = resolveBackToSpace({
|
||||
current: currentLocation,
|
||||
lastLocation: getSessionStorageItem<SpaceLocation | null>(LAST_LOCATION_KEY, null),
|
||||
storedBack: getSessionStorageItem<SpaceLocation | null>(BACK_TO_SPACE_KEY, null),
|
||||
fromPicker,
|
||||
});
|
||||
|
||||
if (back) {
|
||||
setSessionStorageItem(BACK_TO_SPACE_KEY, back);
|
||||
} else {
|
||||
removeSessionStorageItem(BACK_TO_SPACE_KEY);
|
||||
}
|
||||
setSessionStorageItem(LAST_LOCATION_KEY, currentLocation);
|
||||
|
||||
setBackToSpace(back);
|
||||
}, [pathname, spaceId, spaceTitle]);
|
||||
|
||||
return backToSpace;
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export const ar: TranslationLanguage = {
|
||||
on_this_page: 'في هذه الصفحة',
|
||||
next_page: 'التالي',
|
||||
previous_page: 'السابق',
|
||||
toc_back_to_space: 'العودة إلى ${1}',
|
||||
page_last_modified: 'آخر تحديث ${1}',
|
||||
was_this_helpful: 'هل كان هذا مفيدا؟',
|
||||
was_this_helpful_positive: 'نعم، كان مفيدا!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const bg: TranslationLanguage = {
|
||||
on_this_page: 'На тази страница',
|
||||
next_page: 'Следваща',
|
||||
previous_page: 'Предишна',
|
||||
toc_back_to_space: 'Обратно към ${1}',
|
||||
page_last_modified: 'Последна актуализация ${1}',
|
||||
was_this_helpful: 'Беше ли полезно?',
|
||||
was_this_helpful_positive: 'Да, беше!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const cs: TranslationLanguage = {
|
||||
on_this_page: 'Na této stránce',
|
||||
next_page: 'Další',
|
||||
previous_page: 'Předchozí',
|
||||
toc_back_to_space: 'Zpět na ${1}',
|
||||
page_last_modified: 'Poslední aktualizace ${1}',
|
||||
was_this_helpful: 'Bylo to užitečné?',
|
||||
was_this_helpful_positive: 'Ano, bylo!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const da: TranslationLanguage = {
|
||||
on_this_page: 'På denne side',
|
||||
next_page: 'Næste',
|
||||
previous_page: 'Forrige',
|
||||
toc_back_to_space: 'Tilbage til ${1}',
|
||||
page_last_modified: 'Sidst opdateret ${1}',
|
||||
was_this_helpful: 'Var dette nyttigt?',
|
||||
was_this_helpful_positive: 'Ja, det var det!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const de: TranslationLanguage = {
|
||||
on_this_page: 'Auf dieser Seite',
|
||||
next_page: 'Nächste',
|
||||
previous_page: 'Vorherige',
|
||||
toc_back_to_space: 'Zurück zu ${1}',
|
||||
page_last_modified: 'Zuletzt aktualisiert ${1}',
|
||||
was_this_helpful: 'War das hilfreich?',
|
||||
was_this_helpful_positive: 'Ja, das war es!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const el: TranslationLanguage = {
|
||||
on_this_page: 'Σε αυτήν τη σελίδα',
|
||||
next_page: 'Επόμενη',
|
||||
previous_page: 'Προηγούμενη',
|
||||
toc_back_to_space: 'Επιστροφή σε ${1}',
|
||||
page_last_modified: 'Τελευταία ενημέρωση ${1}',
|
||||
was_this_helpful: 'Ήταν χρήσιμο αυτό;',
|
||||
was_this_helpful_positive: 'Ναι, ήταν!',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const en = {
|
||||
on_this_page: 'On this page',
|
||||
next_page: 'Next',
|
||||
previous_page: 'Previous',
|
||||
toc_back_to_space: 'Back to ${1}',
|
||||
page_last_modified: 'Last updated ${1}',
|
||||
was_this_helpful: 'Was this helpful?',
|
||||
was_this_helpful_positive: 'Yes, it was!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const es: TranslationLanguage = {
|
||||
on_this_page: 'En esta página',
|
||||
next_page: 'Siguiente',
|
||||
previous_page: 'Anterior',
|
||||
toc_back_to_space: 'Volver a ${1}',
|
||||
page_last_modified: 'Última actualización ${1}',
|
||||
was_this_helpful: '¿Te fue útil?',
|
||||
was_this_helpful_positive: '¡Sí, lo fue!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const et: TranslationLanguage = {
|
||||
on_this_page: 'Sellel lehel',
|
||||
next_page: 'Järgmine',
|
||||
previous_page: 'Eelmine',
|
||||
toc_back_to_space: 'Tagasi: ${1}',
|
||||
page_last_modified: 'Viimati uuendatud ${1}',
|
||||
was_this_helpful: 'Kas sellest oli abi?',
|
||||
was_this_helpful_positive: 'Jah, oli!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const fi: TranslationLanguage = {
|
||||
on_this_page: 'Tällä sivulla',
|
||||
next_page: 'Seuraava',
|
||||
previous_page: 'Edellinen',
|
||||
toc_back_to_space: 'Takaisin: ${1}',
|
||||
page_last_modified: 'Päivitetty viimeksi ${1}',
|
||||
was_this_helpful: 'Oliko tästä apua?',
|
||||
was_this_helpful_positive: 'Kyllä oli!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const fr: TranslationLanguage = {
|
||||
on_this_page: 'Sur cette page',
|
||||
next_page: 'Suivant',
|
||||
previous_page: 'Précédent',
|
||||
toc_back_to_space: 'Retour à ${1}',
|
||||
page_last_modified: 'Mis à jour ${1}',
|
||||
was_this_helpful: 'Ce contenu vous a-t-il été utile ?',
|
||||
was_this_helpful_positive: 'Oui, tout à fait !',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const he: TranslationLanguage = {
|
||||
on_this_page: 'בדף הזה',
|
||||
next_page: 'הבא',
|
||||
previous_page: 'הקודם',
|
||||
toc_back_to_space: 'חזרה אל ${1}',
|
||||
page_last_modified: 'עודכן לאחרונה ${1}',
|
||||
was_this_helpful: 'האם זה היה מועיל?',
|
||||
was_this_helpful_positive: 'כן, זה עזר!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const hi: TranslationLanguage = {
|
||||
on_this_page: 'इस पृष्ठ पर',
|
||||
next_page: 'अगला',
|
||||
previous_page: 'पिछला',
|
||||
toc_back_to_space: '${1} पर वापस जाएँ',
|
||||
page_last_modified: 'अंतिम अपडेट ${1}',
|
||||
was_this_helpful: 'क्या यह उपयोगी था?',
|
||||
was_this_helpful_positive: 'हां, था!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const hr: TranslationLanguage = {
|
||||
on_this_page: 'Na ovoj stranici',
|
||||
next_page: 'Sljedeća',
|
||||
previous_page: 'Prethodna',
|
||||
toc_back_to_space: 'Natrag na ${1}',
|
||||
page_last_modified: 'Zadnje ažurirano ${1}',
|
||||
was_this_helpful: 'Je li ovo bilo korisno?',
|
||||
was_this_helpful_positive: 'Da, bilo je!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const hu: TranslationLanguage = {
|
||||
on_this_page: 'Ezen az oldalon',
|
||||
next_page: 'Következő',
|
||||
previous_page: 'Előző',
|
||||
toc_back_to_space: 'Vissza ide: ${1}',
|
||||
page_last_modified: 'Utoljára frissítve: ${1}',
|
||||
was_this_helpful: 'Hasznos volt ez?',
|
||||
was_this_helpful_positive: 'Igen, az volt!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const id: TranslationLanguage = {
|
||||
on_this_page: 'Di halaman ini',
|
||||
next_page: 'Berikutnya',
|
||||
previous_page: 'Sebelumnya',
|
||||
toc_back_to_space: 'Kembali ke ${1}',
|
||||
page_last_modified: 'Terakhir diperbarui ${1}',
|
||||
was_this_helpful: 'Apakah ini membantu?',
|
||||
was_this_helpful_positive: 'Ya, membantu!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const it: TranslationLanguage = {
|
||||
on_this_page: 'In questa pagina',
|
||||
next_page: 'Successivo',
|
||||
previous_page: 'Precedente',
|
||||
toc_back_to_space: 'Torna a ${1}',
|
||||
page_last_modified: 'Ultimo aggiornamento ${1}',
|
||||
was_this_helpful: 'È stato utile?',
|
||||
was_this_helpful_positive: 'Sì, lo è stato!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ja: TranslationLanguage = {
|
||||
on_this_page: 'このページ内',
|
||||
next_page: '次へ',
|
||||
previous_page: '前へ',
|
||||
toc_back_to_space: '${1}に戻る',
|
||||
page_last_modified: '最終更新 ${1}',
|
||||
was_this_helpful: '役に立ちましたか?',
|
||||
was_this_helpful_positive: 'はい、役に立ちました!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ko: TranslationLanguage = {
|
||||
on_this_page: '이 페이지에서',
|
||||
next_page: '다음',
|
||||
previous_page: '이전',
|
||||
toc_back_to_space: '${1}(으)로 돌아가기',
|
||||
page_last_modified: '마지막 업데이트 ${1}',
|
||||
was_this_helpful: '도움이 되었나요?',
|
||||
was_this_helpful_positive: '네, 도움이 되었어요!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const lt: TranslationLanguage = {
|
||||
on_this_page: 'Šiame puslapyje',
|
||||
next_page: 'Kitas',
|
||||
previous_page: 'Ankstesnis',
|
||||
toc_back_to_space: 'Atgal į ${1}',
|
||||
page_last_modified: 'Paskutinį kartą atnaujinta ${1}',
|
||||
was_this_helpful: 'Ar tai buvo naudinga?',
|
||||
was_this_helpful_positive: 'Taip, buvo!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const lv: TranslationLanguage = {
|
||||
on_this_page: 'Šajā lapā',
|
||||
next_page: 'Nākamā',
|
||||
previous_page: 'Iepriekšējā',
|
||||
toc_back_to_space: 'Atpakaļ uz ${1}',
|
||||
page_last_modified: 'Pēdoreiz atjaunināts ${1}',
|
||||
was_this_helpful: 'Vai tas bija noderīgi?',
|
||||
was_this_helpful_positive: 'Jā, bija!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const ms: TranslationLanguage = {
|
||||
on_this_page: 'Pada halaman ini',
|
||||
next_page: 'Seterusnya',
|
||||
previous_page: 'Sebelumnya',
|
||||
toc_back_to_space: 'Kembali ke ${1}',
|
||||
page_last_modified: 'Terakhir dikemas kini ${1}',
|
||||
was_this_helpful: 'Adakah ini membantu?',
|
||||
was_this_helpful_positive: 'Ya, membantu!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const nl: TranslationLanguage = {
|
||||
on_this_page: 'Op deze pagina',
|
||||
next_page: 'Volgende',
|
||||
previous_page: 'Vorige',
|
||||
toc_back_to_space: 'Terug naar ${1}',
|
||||
page_last_modified: 'Laatst bijgewerkt ${1}',
|
||||
was_this_helpful: 'Was dit nuttig?',
|
||||
was_this_helpful_positive: 'Ja, het was nuttig!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const no: TranslationLanguage = {
|
||||
on_this_page: 'På denne siden',
|
||||
next_page: 'Neste',
|
||||
previous_page: 'Forrige',
|
||||
toc_back_to_space: 'Tilbake til ${1}',
|
||||
page_last_modified: 'Sist oppdatert ${1}',
|
||||
was_this_helpful: 'Var dette nyttig?',
|
||||
was_this_helpful_positive: 'Ja, det var det!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const pl: TranslationLanguage = {
|
||||
on_this_page: 'Na tej stronie',
|
||||
next_page: 'Dalej',
|
||||
previous_page: 'Wstecz',
|
||||
toc_back_to_space: 'Powrót do ${1}',
|
||||
page_last_modified: 'Ostatnia aktualizacja ${1}',
|
||||
was_this_helpful: 'Czy to było pomocne?',
|
||||
was_this_helpful_positive: 'Tak!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const pt_br: TranslationLanguage = {
|
||||
on_this_page: 'Nesta página',
|
||||
next_page: 'Próximo',
|
||||
previous_page: 'Anterior',
|
||||
toc_back_to_space: 'Voltar para ${1}',
|
||||
page_last_modified: 'Atualizado ${1}',
|
||||
was_this_helpful: 'Isto foi útil?',
|
||||
was_this_helpful_positive: 'Sim, foi!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const pt: TranslationLanguage = {
|
||||
on_this_page: 'Nesta página',
|
||||
next_page: 'Seguinte',
|
||||
previous_page: 'Anterior',
|
||||
toc_back_to_space: 'Voltar para ${1}',
|
||||
page_last_modified: 'Última atualização ${1}',
|
||||
was_this_helpful: 'Isto foi útil?',
|
||||
was_this_helpful_positive: 'Sim, foi!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const ro: TranslationLanguage = {
|
||||
on_this_page: 'Pe această pagină',
|
||||
next_page: 'Următoarea',
|
||||
previous_page: 'Anterioara',
|
||||
toc_back_to_space: 'Înapoi la ${1}',
|
||||
page_last_modified: 'Ultima actualizare ${1}',
|
||||
was_this_helpful: 'A fost util?',
|
||||
was_this_helpful_positive: 'Da, a fost!',
|
||||
|
||||
@@ -46,6 +46,7 @@ export const ru: TranslationLanguage = {
|
||||
on_this_page: 'На этой странице',
|
||||
next_page: 'Следующая',
|
||||
previous_page: 'Предыдущая',
|
||||
toc_back_to_space: 'Назад к ${1}',
|
||||
page_last_modified: 'Последнее обновление ${1}',
|
||||
was_this_helpful: 'Это было полезно?',
|
||||
was_this_helpful_positive: 'Да, было!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const sk: TranslationLanguage = {
|
||||
on_this_page: 'Na tejto stránke',
|
||||
next_page: 'Ďalej',
|
||||
previous_page: 'Predchádzajúca',
|
||||
toc_back_to_space: 'Späť na ${1}',
|
||||
page_last_modified: 'Naposledy aktualizované ${1}',
|
||||
was_this_helpful: 'Bolo to užitočné?',
|
||||
was_this_helpful_positive: 'Áno, bolo!',
|
||||
|
||||
@@ -45,6 +45,7 @@ export const sl: TranslationLanguage = {
|
||||
on_this_page: 'Na tej strani',
|
||||
next_page: 'Naslednja',
|
||||
previous_page: 'Prejšnja',
|
||||
toc_back_to_space: 'Nazaj na ${1}',
|
||||
page_last_modified: 'Nazadnje posodobljeno ${1}',
|
||||
was_this_helpful: 'Je bilo to koristno?',
|
||||
was_this_helpful_positive: 'Da, bilo je!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const sv: TranslationLanguage = {
|
||||
on_this_page: 'På den här sidan',
|
||||
next_page: 'Nästa',
|
||||
previous_page: 'Föregående',
|
||||
toc_back_to_space: 'Tillbaka till ${1}',
|
||||
page_last_modified: 'Senast uppdaterad ${1}',
|
||||
was_this_helpful: 'Var detta till hjälp?',
|
||||
was_this_helpful_positive: 'Ja, det var det!',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const th: TranslationLanguage = {
|
||||
on_this_page: 'ในหน้านี้',
|
||||
next_page: 'ถัดไป',
|
||||
previous_page: 'ก่อนหน้า',
|
||||
toc_back_to_space: 'กลับไปที่ ${1}',
|
||||
page_last_modified: 'อัปเดตล่าสุด ${1}',
|
||||
was_this_helpful: 'สิ่งนี้มีประโยชน์หรือไม่?',
|
||||
was_this_helpful_positive: 'ใช่ มีประโยชน์!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const tr: TranslationLanguage = {
|
||||
on_this_page: 'Bu sayfada',
|
||||
next_page: 'Sonraki',
|
||||
previous_page: 'Önceki',
|
||||
toc_back_to_space: 'Şuraya dön: ${1}',
|
||||
page_last_modified: 'Son güncelleme ${1}',
|
||||
was_this_helpful: 'Bu yararlı oldu mu?',
|
||||
was_this_helpful_positive: 'Evet, oldu!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const uk: TranslationLanguage = {
|
||||
on_this_page: 'На цій сторінці',
|
||||
next_page: 'Наступна',
|
||||
previous_page: 'Попередня',
|
||||
toc_back_to_space: 'Назад до ${1}',
|
||||
page_last_modified: 'Останнє оновлення ${1}',
|
||||
was_this_helpful: 'Це було корисно?',
|
||||
was_this_helpful_positive: 'Так, було!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const vi: TranslationLanguage = {
|
||||
on_this_page: 'Trên trang này',
|
||||
next_page: 'Tiếp theo',
|
||||
previous_page: 'Trước',
|
||||
toc_back_to_space: 'Quay lại ${1}',
|
||||
page_last_modified: 'Cập nhật lần cuối ${1}',
|
||||
was_this_helpful: 'Nội dung này có hữu ích không?',
|
||||
was_this_helpful_positive: 'Có, hữu ích!',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const yue: TranslationLanguage = {
|
||||
on_this_page: '本頁內容',
|
||||
next_page: '下一頁',
|
||||
previous_page: '上一頁',
|
||||
toc_back_to_space: '返去${1}',
|
||||
page_last_modified: '最後更新 ${1}',
|
||||
was_this_helpful: '呢個有冇幫助?',
|
||||
was_this_helpful_positive: '有,幫到手!',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const zh_tw: TranslationLanguage = {
|
||||
on_this_page: '本頁內容',
|
||||
next_page: '下一頁',
|
||||
previous_page: '上一頁',
|
||||
toc_back_to_space: '返回${1}',
|
||||
page_last_modified: '最後更新於 ${1}',
|
||||
was_this_helpful: '這有幫助嗎?',
|
||||
was_this_helpful_positive: '有,很有幫助!',
|
||||
|
||||
@@ -44,6 +44,7 @@ export const zh: TranslationLanguage = {
|
||||
on_this_page: '在本页',
|
||||
next_page: '下一页',
|
||||
previous_page: '上一页',
|
||||
toc_back_to_space: '返回${1}',
|
||||
page_last_modified: '最后更新于 ${1}',
|
||||
was_this_helpful: '这有帮助吗?',
|
||||
was_this_helpful_positive: '是的,很有帮助!',
|
||||
|
||||
@@ -259,6 +259,33 @@ export function getLocalizedTitle(
|
||||
return getLocalizedField(entity.localizedTitle, currentLanguage) ?? entity.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* A string concatenation of the site structure (sections and variants) titles for the
|
||||
* current context, as shown in the site's navigation. Returns an empty string when the
|
||||
* current space is the default section and variant (i.e. it has no distinguishing label).
|
||||
*/
|
||||
export function getSiteStructureTitle(context: GitBookSiteContext): string {
|
||||
const { visibleSections: sections, siteSpace, visibleSiteSpaces: siteSpaces } = context;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const title = [];
|
||||
if (
|
||||
sections &&
|
||||
sections.current.default === false && // Only if the current section is not the default one
|
||||
sections.list.filter((section) => section.object === 'site-section').length > 1 // Only if there are multiple sections
|
||||
) {
|
||||
title.push(getLocalizedTitle(sections.current, currentLanguage));
|
||||
}
|
||||
if (
|
||||
siteSpaces.length > 1 && // Only if there are multiple variants
|
||||
siteSpace.default === false && // Only if the variant is not the default one
|
||||
siteSpaces.filter((space) => space.space.language === siteSpace.space.language).length > 1 // Only if there are multiple variants *for the current language*. This filters out spaces that are "just" translations of each other, not versions.
|
||||
) {
|
||||
title.push(getLocalizedTitle(siteSpace, siteSpace.space.language));
|
||||
}
|
||||
return title.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the localized description for a site entity.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user