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:
Zeno Kapitein
2026-07-03 12:36:07 +02:00
committed by GitHub
parent 6102413d6b
commit 62ca6d3ce5
50 changed files with 440 additions and 29 deletions
+5
View File
@@ -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: '是的,很有帮助!',
+27
View File
@@ -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.
*/