Compare commits

..

2 Commits

Author SHA1 Message Date
Peter White 0bd257d918 changeset 2026-10-06 16:02:05 +02:00
Peter White 8d238f76d4 Export subtree PDFs in batches of 20 with previous/next navigation 2026-10-06 16:02:05 +02:00
100 changed files with 725 additions and 1220 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Track which Ask AI entry point opened the chat in ask view analytics.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Keep the docs embed open when switching section, variant or language.
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/embed": minor
"gitbook": patch
---
Add `defaultTab` and `defaultPage` options to the Docs Embed to choose where it opens. `defaultTab` opens the embed on the assistant, search or docs tab. `defaultPage` sets the page the docs tab opens on in place of the site's home page, and the embed opens on that page unless `defaultTab` says otherwise. Both work with the standalone script's `configure`, `frame.configure` and `<GitBookFrame>` props.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Agents can now send a goal with any question they ask on the Markdown version of a page.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Track server-side Insights events (markdown, llms.txt, RSS, ask) with the site's public URL instead of the proxy URL.
-6
View File
@@ -1,6 +0,0 @@
---
"@gitbook/react-openapi": patch
"gitbook": patch
---
Fix OpenAPI schemas that reference themselves through a nullable union rendering forever when expanded, which crashed PDF exports.
+5
View File
@@ -0,0 +1,5 @@
---
"gitbook": patch
---
Export page subtrees as PDF in batches of 20 pages, with links to the previous and next batch, so large exports no longer crash.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Fix previewed customization and theme overrides being lost when navigating between pages inside the site preview. The preview cookie was scoped to the site's canonical base path instead of the preview route it is actually served under, so the browser dropped it on the next in-preview navigation.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Clarify the not-found page on sites with login-protected content: prompt anonymous visitors to log in, and tell logged-in visitors they may not have access.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Show site UI in French or Spanish for spaces set to Canadian French, Mexican Spanish or Latin American Spanish.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Let search buttons open search limited to the current space.
-5
View File
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Send the visitor country to resolvePublishedContentByUrl on dev, preview, staging and selected sites, using the forwarded `x-gitbook-country` header on revalidation requests.
@@ -1,5 +0,0 @@
---
"gitbook": patch
---
Read the production hostnames that receive the visitor country from the `GITBOOK_VISITOR_COUNTRY_HOSTNAMES` build-time env variable.
@@ -78,11 +78,6 @@ runs:
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---" echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
cat .vercel/.env.${{ inputs.environment }}.local cat .vercel/.env.${{ inputs.environment }}.local
- name: Inject visitor country hostnames
if: ${{ env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES != '' }}
shell: bash
run: |
echo "GITBOOK_VISITOR_COUNTRY_HOSTNAMES=$GITBOOK_VISITOR_COUNTRY_HOSTNAMES" >> .vercel/.env.${{ inputs.environment }}.local
- name: Inject staging build env vars - name: Inject staging build env vars
if: ${{ inputs.environment == 'staging' }} if: ${{ inputs.environment == 'staging' }}
shell: bash shell: bash
-4
View File
@@ -10,8 +10,6 @@ jobs:
environment: environment:
name: 2v-production name: 2v-production
url: ${{ steps.deploy.outputs.deployment-url }} url: ${{ steps.deploy.outputs.deployment-url }}
env:
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
@@ -33,8 +31,6 @@ jobs:
environment: environment:
name: 2c-production name: 2c-production
url: ${{ steps.deploy.outputs.deployment-url }} url: ${{ steps.deploy.outputs.deployment-url }}
env:
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
outputs: outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }} deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps: steps:
+2 -2
View File
@@ -355,7 +355,7 @@
}, },
"catalog": { "catalog": {
"@base-ui/react": "^1.7.0", "@base-ui/react": "^1.7.0",
"@gitbook/api": "0.205.0", "@gitbook/api": "0.204.0",
"@scalar/api-client-react": "^1.3.46", "@scalar/api-client-react": "^1.3.46",
"@tsconfig/node20": "^20.1.6", "@tsconfig/node20": "^20.1.6",
"@tsconfig/strictest": "^2.0.6", "@tsconfig/strictest": "^2.0.6",
@@ -727,7 +727,7 @@
"@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="], "@fortawesome/fontawesome-svg-core": ["@fortawesome/fontawesome-svg-core@7.2.0", "", { "dependencies": { "@fortawesome/fontawesome-common-types": "7.2.0" } }, "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q=="],
"@gitbook/api": ["@gitbook/api@0.205.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-jjkPAhPw6tja1N0ZPKqVsaV+opN8IS7yP1oZHHbcacZ477angPCo9YqiJJYWAl0tQ5OcKTuQbrsqhZSuwKM2Kw=="], "@gitbook/api": ["@gitbook/api@0.204.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-SJCe0Ipyt+V5HZX4EZpFO8AHTOWIMwj2ljQTbcqgU7rVOAyCNDzILp8mRk9stbboSi1pv09+rDnZK1ZguPvFTw=="],
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"], "@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
+1 -1
View File
@@ -48,7 +48,7 @@
"@tsconfig/strictest": "^2.0.6", "@tsconfig/strictest": "^2.0.6",
"@tsconfig/node20": "^20.1.6", "@tsconfig/node20": "^20.1.6",
"@base-ui/react": "^1.7.0", "@base-ui/react": "^1.7.0",
"@gitbook/api": "0.205.0", "@gitbook/api": "0.204.0",
"@scalar/api-client-react": "^1.3.46", "@scalar/api-client-react": "^1.3.46",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0", "@types/react-dom": "^19.0.0",
-30
View File
@@ -54,7 +54,6 @@ GitBook('configure', {
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book' icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
}, },
tabs: ['assistant', 'search', 'docs'], tabs: ['assistant', 'search', 'docs'],
defaultPage: '/getting-started',
actions: [ actions: [
{ {
icon: 'circle-question', icon: 'circle-question',
@@ -108,7 +107,6 @@ frame.clearChat();
// Configure the embed (see Configuration section for all options) // Configure the embed (see Configuration section for all options)
frame.configure({ frame.configure({
tabs: ['assistant', 'search', 'docs'], tabs: ['assistant', 'search', 'docs'],
defaultPage: '/getting-started',
actions: [ actions: [
{ {
icon: 'circle-question', icon: 'circle-question',
@@ -144,7 +142,6 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
}} }}
tabs={['assistant', 'search', 'docs']} tabs={['assistant', 'search', 'docs']}
defaultPage="/getting-started"
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }} greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
assistantName="Support Assistant" assistantName="Support Assistant"
suggestions={['What is GitBook?', 'How do I get started?']} suggestions={['What is GitBook?', 'How do I get started?']}
@@ -252,33 +249,6 @@ Override which tabs are displayed. Defaults to your site's configuration.
tabs: ['assistant', 'search', 'docs'] tabs: ['assistant', 'search', 'docs']
``` ```
### `defaultTab`
Available in: Standalone script, NPM package, React components
The tab the embed opens on. Without it, the embed opens on `defaultPage` if set, otherwise on the assistant (or the docs when the assistant isn't available). The tab must be one of the enabled `tabs`.
- **Type**: `'assistant' | 'search' | 'docs'`
```javascript
tabs: ['assistant', 'search', 'docs'],
defaultTab: 'search'
```
`defaultTab` and `defaultPage` apply when the embed first loads: the standalone widget keeps its place when it is closed and reopened. To move it later, use `navigateToPage` or `navigateToAssistant`.
### `defaultPage`
Available in: Standalone script, NPM package, React components
The page the docs tab opens on, in place of your site's home page. Accepts the same references as `navigateToPage`: the page's path within the site, an absolute path, or its full published URL. Unless `defaultTab` says otherwise, the embed opens on this page.
- **Type**: `string`
```javascript
defaultPage: '/getting-started/quickstart'
```
### `closeButton` ### `closeButton`
Available in: Standalone script, NPM package, React components Available in: Standalone script, NPM package, React components
-6
View File
@@ -62,12 +62,6 @@ export type GitBookEmbeddableConfiguration = {
/** Tabs to display in the embed (if enabled on the site). */ /** Tabs to display in the embed (if enabled on the site). */
tabs: ('assistant' | 'docs' | 'search')[]; tabs: ('assistant' | 'docs' | 'search')[];
/** Tab to open the embed on. */
defaultTab?: 'assistant' | 'docs' | 'search';
/** Page to open the docs tab on, instead of the site's home page. */
defaultPage?: string;
/** Additional buttons to be displayed in the header of the GitBook embed. */ /** Additional buttons to be displayed in the header of the GitBook embed. */
actions: GitBookEmbeddableActionDefinition[]; actions: GitBookEmbeddableActionDefinition[];
@@ -27,8 +27,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
suggestions = [], suggestions = [],
tools = [], tools = [],
tabs = ['assistant', 'search', 'docs'], tabs = ['assistant', 'search', 'docs'],
defaultTab,
defaultPage,
trademark = true, trademark = true,
closeButton = false, closeButton = false,
assistantName, assistantName,
@@ -52,8 +50,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
useEffect(() => { useEffect(() => {
gitbookFrame?.configure({ gitbookFrame?.configure({
tabs, tabs,
defaultTab,
defaultPage,
actions, actions,
greeting, greeting,
suggestions, suggestions,
@@ -69,8 +65,6 @@ export function GitBookFrame(props: GitBookFrameProps) {
suggestions, suggestions,
tools, tools,
tabs, tabs,
defaultTab,
defaultPage,
closeButton, closeButton,
trademark, trademark,
assistantName, assistantName,
-2
View File
@@ -95,8 +95,6 @@ const nextConfig = {
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION, GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE, GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
GITBOOK_DISABLE_INSIGHTS: process.env.GITBOOK_DISABLE_INSIGHTS, GITBOOK_DISABLE_INSIGHTS: process.env.GITBOOK_DISABLE_INSIGHTS,
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: process.env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES,
VERCEL_TARGET_ENV: process.env.VERCEL_TARGET_ENV,
// Next.js envs // Next.js envs
NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY, NEXT_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
+4 -5
View File
@@ -7,7 +7,7 @@ import type { CustomizationAIMode } from '@gitbook/api';
import type { GitBookAssistant } from '@gitbook/browser-types'; import type { GitBookAssistant } from '@gitbook/browser-types';
import { Icon, type IconName } from '@gitbook/icons'; import { Icon, type IconName } from '@gitbook/icons';
import { type AskAITrigger, useAIChatController, useAIChatState } from '.'; import { useAIChatController, useAIChatState } from '.';
import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat'; import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat';
import { useIntegrationAssistants } from '../Integrations'; import { useIntegrationAssistants } from '../Integrations';
import { useSetSearchState } from '../Search/useSearch'; import { useSetSearchState } from '../Search/useSearch';
@@ -26,8 +26,7 @@ export type AIConfig = {
}; };
}; };
export type Assistant = Omit<GitBookAssistant, 'icon' | 'open'> & { export type Assistant = Omit<GitBookAssistant, 'icon'> & {
open: (query?: string, trigger?: AskAITrigger) => void;
/** /**
* Unique identifier for the assistant. Generated automatically using Crypto.randomUUID(). * Unique identifier for the assistant. Generated automatically using Crypto.randomUUID().
* @example '123e4567-e89b-12d3-a456-426614174000' * @example '123e4567-e89b-12d3-a456-426614174000'
@@ -103,8 +102,8 @@ export function useAI(): AIContext {
className="size-text-lg" className="size-text-lg"
/> />
), ),
open: (query?: string, trigger?: AskAITrigger) => { open: (query?: string) => {
chatController.open(trigger); chatController.open();
if (query) { if (query) {
chatController.postMessage({ message: query }); chatController.postMessage({ message: query });
} }
@@ -11,7 +11,6 @@ import {
type AIStreamResponseToolCallPending, type AIStreamResponseToolCallPending,
type AIToolCallResult, type AIToolCallResult,
SiteInsightsDisplayContext, SiteInsightsDisplayContext,
type SiteInsightsEventAskView,
} from '@gitbook/api'; } from '@gitbook/api';
import { getInsightsSession, useTrackEvent } from '../Insights'; import { getInsightsSession, useTrackEvent } from '../Insights';
@@ -61,8 +60,6 @@ export type AIChatStatus =
| 'error' | 'error'
| 'confirm'; | 'confirm';
export type AskAITrigger = NonNullable<SiteInsightsEventAskView['trigger']>;
export type AIChatState = { export type AIChatState = {
/** /**
* If true, the chat is open. * If true, the chat is open.
@@ -163,7 +160,7 @@ type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
export type AIChatController = { export type AIChatController = {
/** Open the dialog */ /** Open the dialog */
open: (trigger?: AskAITrigger) => void; open: () => void;
/** Close the dialog */ /** Close the dialog */
close: () => void; close: () => void;
/** Post a message to the session */ /** Post a message to the session */
@@ -305,28 +302,22 @@ export function AIChatProvider(props: {
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map()); const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
// Open AI chat and sync with search state // Open AI chat and sync with search state
const onOpen = React.useCallback( const onOpen = React.useCallback(() => {
(trigger?: AskAITrigger) => { setIntercomLauncherHidden(true);
setIntercomLauncherHidden(true);
if (!renderMessageOptions?.asEmbeddable) {
trackEvent({ type: 'ask_view', trigger });
}
const { initialQuery } = globalState.getState(); const { initialQuery } = globalState.getState();
globalState.setState((state) => ({ ...state, opened: true })); globalState.setState((state) => ({ ...state, opened: true }));
// Update search state to show ask mode with first message or current ask value // Update search state to show ask mode with first message or current ask value
setSearchState((prev) => ({ setSearchState((prev) => ({
ask: prev?.ask ?? initialQuery ?? '', ask: prev?.ask ?? initialQuery ?? '',
query: prev?.query ?? null, query: prev?.query ?? null,
scope: prev?.scope ?? 'default', scope: prev?.scope ?? 'default',
open: false, // Close search popover when opening chat open: false, // Close search popover when opening chat
})); }));
notify(eventsRef.current.get('open'), {}); notify(eventsRef.current.get('open'), {});
}, }, [setSearchState]);
[setSearchState, trackEvent, renderMessageOptions?.asEmbeddable]
);
// Close AI chat and clear ask parameter // Close AI chat and clear ask parameter
const onClose = React.useCallback(() => { const onClose = React.useCallback(() => {
@@ -24,6 +24,7 @@ import {
EmbeddableFrameTitle, EmbeddableFrameTitle,
} from '../Embeddable/EmbeddableFrame'; } from '../Embeddable/EmbeddableFrame';
import { useIsMounted, useNow } from '../hooks'; import { useIsMounted, useNow } from '../hooks';
import { useTrackEvent } from '../Insights';
import { Button } from '../primitives'; import { Button } from '../primitives';
import { ScrollContainer } from '../primitives/ScrollContainer'; import { ScrollContainer } from '../primitives/ScrollContainer';
import { SideSheet } from '../primitives/SideSheet'; import { SideSheet } from '../primitives/SideSheet';
@@ -67,6 +68,16 @@ export function AIChat() {
} }
); );
// Track the view of the AI chat
const trackEvent = useTrackEvent();
React.useEffect(() => {
if (chat.opened) {
trackEvent({
type: 'ask_view',
});
}
}, [chat.opened, trackEvent]);
return ( return (
<SideSheet <SideSheet
side="right" side="right"
@@ -39,7 +39,7 @@ export function AIChatButton(props: {
</div> </div>
} }
aria-label={tString(language, 'ai_chat_ask', assistant.label)} aria-label={tString(language, 'ai_chat_ask', assistant.label)}
onClick={() => assistant.open(undefined, 'site')} onClick={() => assistant.open()}
> >
{showLabel ? t(language, 'ask') : null} {showLabel ? t(language, 'ask') : null}
</Button> </Button>
@@ -33,7 +33,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
id: `text-${fnv1a(text, { size: 32 })}`, id: `text-${fnv1a(text, { size: 32 })}`,
content: text, content: text,
}); });
chatController.open('hover'); chatController.open();
chatController.setDraft(tString(language, 'ai_chat_paragraph_draft')); chatController.setDraft(tString(language, 'ai_chat_paragraph_draft'));
chatController.focus(); chatController.focus();
}; };
@@ -84,7 +84,7 @@ export function AskAITextSelection() {
id: `text-${fnv1a(content, { size: 32 })}`, id: `text-${fnv1a(content, { size: 32 })}`,
content, content,
}); });
chatController.open('selection'); chatController.open();
chatController.focus(); chatController.focus();
clear(); clear();
}; };
@@ -13,14 +13,9 @@ const INPUT_SIZE_MAP: Record<NonNullable<ButtonProps['size']>, 'small' | 'medium
}; };
export function InlineActionButton( export function InlineActionButton(
props: { props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
action: 'ask' | 'search';
query?: string;
/** Where a search runs: the whole site, or only the space the visitor is reading. */
scope?: 'site' | 'space';
} & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
) { ) {
const { action, query, scope, buttonProps } = props; const { action, query, buttonProps } = props;
const { assistants } = useAI(); const { assistants } = useAI();
const chatState = useAIChatState(); const chatState = useAIChatState();
@@ -34,7 +29,7 @@ export function InlineActionButton(
setSearchState((prev) => ({ setSearchState((prev) => ({
...prev, ...prev,
ask: null, ask: null,
scope: scope === 'space' ? 'current' : 'default', scope: 'default',
query: value ?? query, query: value ?? query,
open: true, open: true,
})); }));
@@ -47,11 +47,6 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
<InlineActionButton <InlineActionButton
action={inline.data.action.action} action={inline.data.action.action}
query={inline.data.action.query ?? ''} query={inline.data.action.query ?? ''}
scope={
inline.data.action.action === 'search'
? inline.data.action.scope
: undefined
}
buttonProps={buttonProps} buttonProps={buttonProps}
/> />
); );
@@ -61,7 +61,9 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
} }
trackEvent( trackEvent(
{ type: 'ask_view', trigger: 'embed' }, {
type: 'ask_view',
},
{ {
pageId: null, pageId: null,
displayContext: api.SiteInsightsDisplayContext.Embed, displayContext: api.SiteInsightsDisplayContext.Embed,
@@ -97,7 +97,6 @@ export async function EmbeddableDocsPage(
) ?? context.siteSpace ) ?? context.siteSpace
} }
siteSpaces={variants.translations} siteSpaces={variants.translations}
asEmbeddable
className="my-1.5 ml-2 self-start" className="my-1.5 ml-2 self-start"
/> />
) : null} ) : null}
@@ -126,7 +125,6 @@ export async function EmbeddableDocsPage(
) ?? context.siteSpace ) ?? context.siteSpace
} }
siteSpaces={variants.translations} siteSpaces={variants.translations}
asEmbeddable
className="max-md:[&_.button-content]:block" className="max-md:[&_.button-content]:block"
/> />
) : null ) : null
@@ -139,7 +137,6 @@ export async function EmbeddableDocsPage(
context={context} context={context}
siteSpace={context.siteSpace} siteSpace={context.siteSpace}
siteSpaces={variants.generic} siteSpaces={variants.generic}
asEmbeddable
className="w-full px-3" className="w-full px-3"
/> />
) : null} ) : null}
@@ -9,7 +9,6 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
import { integrationsAssistantTools } from '../Integrations'; import { integrationsAssistantTools } from '../Integrations';
import { Button, LinkContext, type LinkContextType } from '../primitives'; import { Button, LinkContext, type LinkContextType } from '../primitives';
import { getChannel } from './channel'; import { getChannel } from './channel';
import { DEFAULT_EMBEDDABLE_TABS, getEmbeddableLandingTab } from './getEmbeddableLandingTab';
import { resolveEmbedPageLink } from './server-actions'; import { resolveEmbedPageLink } from './server-actions';
import { useAI, useAIChatController } from '@/components/AI'; import { useAI, useAIChatController } from '@/components/AI';
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled'; import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
@@ -24,17 +23,6 @@ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() =
trademark: true, trademark: true,
})); }));
const docsHome = createStore<{ reference?: string; href?: string }>(() => ({}));
// Module-level so it survives the layout remounting on a cross-space navigation.
let landed = false;
function resolvePageHref(pagePath: string, baseURL: string): Promise<string> {
return resolveEmbedPageLink(pagePath)
.then((resolved) => ('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`))
.catch(() => `${baseURL}/page/${pagePath}`);
}
// oxlint-disable-next-line typescript/no-explicit-any // oxlint-disable-next-line typescript/no-explicit-any
function log(...data: any[]) { function log(...data: any[]) {
// oxlint-disable-next-line no-console // oxlint-disable-next-line no-console
@@ -78,15 +66,6 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
const { baseURL, router, chatController } = refs.current; const { baseURL, router, chatController } = refs.current;
const message = payload as ParentToFrameMessage; const message = payload as ParentToFrameMessage;
const navigate = (href: Promise<string>) => {
const token = ++navToken.current;
href.then((href) => {
if (navToken.current === token) {
refs.current.router.push(href);
}
});
};
log('[gitbook] received message', message); log('[gitbook] received message', message);
switch (message.type) { switch (message.type) {
@@ -104,43 +83,29 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
break; break;
} }
case 'configure': { case 'configure': {
const { settings } = message; embeddableConfiguration.setState(message.settings);
embeddableConfiguration.setState(settings);
integrationsAssistantTools.setState({ integrationsAssistantTools.setState({
tools: settings.tools, tools: message.settings.tools,
}); });
const defaultPage =
typeof settings.defaultPage === 'string' && settings.defaultPage
? settings.defaultPage
: undefined;
let docsHomeHref: Promise<string> | undefined;
if (defaultPage !== docsHome.getState().reference) {
docsHome.setState({ reference: defaultPage, href: undefined });
if (defaultPage) {
docsHomeHref = resolvePageHref(defaultPage, baseURL);
docsHomeHref.then((href) => {
if (docsHome.getState().reference === defaultPage) {
docsHome.setState({ href });
}
});
}
}
if (!landed) {
landed = true;
const tab = getEmbeddableLandingTab(settings);
if (tab === 'docs') {
navigate(docsHomeHref ?? Promise.resolve(`${baseURL}/page/`));
} else if (tab) {
navToken.current++;
router.push(`${baseURL}/${tab}`);
}
}
break; break;
} }
case 'navigateToPage': { case 'navigateToPage': {
navigate(resolvePageHref(message.pagePath, baseURL)); // Resolve the target server-side: on a multi-space site the page may
// live in another space/section, whose base must go before
// `~gitbook/embed/page`. Fall back to a same-space push on failure.
const { pagePath } = message;
const token = ++navToken.current;
// Ignore the result if a later navigation has since superseded this one.
const push = (href: string) => {
if (navToken.current === token) {
router.push(href);
}
};
resolveEmbedPageLink(pagePath)
.then((resolved) =>
push('href' in resolved ? resolved.href : `${baseURL}/page/${pagePath}`)
)
.catch(() => push(`${baseURL}/page/${pagePath}`));
break; break;
} }
case 'navigateToAssistant': { case 'navigateToAssistant': {
@@ -167,7 +132,7 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
export function useEmbeddableTabs() { export function useEmbeddableTabs() {
const configuredTabs = useEmbeddableConfiguration((state) => state.tabs); const configuredTabs = useEmbeddableConfiguration((state) => state.tabs);
return configuredTabs.length > 0 ? configuredTabs : DEFAULT_EMBEDDABLE_TABS; return configuredTabs.length > 0 ? configuredTabs : ['assistant', 'search', 'docs'];
} }
export function useEmbeddableLinkContext() { export function useEmbeddableLinkContext() {
@@ -235,7 +200,6 @@ export function EmbeddableIframeTabs(props: {
const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props; const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props;
const actions = useEmbeddableConfiguration((state) => state.actions); const actions = useEmbeddableConfiguration((state) => state.actions);
const tabs = useEmbeddableTabs(); const tabs = useEmbeddableTabs();
const docsHomeHref = useStore(docsHome, (state) => state.href);
const { assistants, config } = useAI(); const { assistants, config } = useAI();
const language = useLanguage(); const language = useLanguage();
@@ -264,7 +228,7 @@ export function EmbeddableIframeTabs(props: {
key: 'docs', key: 'docs',
label: siteTitle, label: siteTitle,
icon: 'book-open', icon: 'book-open',
href: docsHomeHref ?? `${baseURL}/page/`, href: `${baseURL}/page/`,
} }
: null, : null,
].filter((tab) => tab !== null); ].filter((tab) => tab !== null);
@@ -1,39 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { getEmbeddableLandingTab } from './getEmbeddableLandingTab';
describe('getEmbeddableLandingTab', () => {
it('keeps the embed default when nothing is configured', () => {
expect(getEmbeddableLandingTab({})).toBeNull();
expect(getEmbeddableLandingTab({ tabs: ['docs', 'search'] })).toBeNull();
});
it('opens on the configured tab', () => {
expect(getEmbeddableLandingTab({ defaultTab: 'search' })).toBe('search');
expect(
getEmbeddableLandingTab({ tabs: ['assistant', 'search', 'docs'], defaultTab: 'search' })
).toBe('search');
});
it('opens on the docs when only a default page is set', () => {
expect(getEmbeddableLandingTab({ defaultPage: 'getting-started' })).toBe('docs');
});
it('prefers the configured tab over the default page', () => {
expect(
getEmbeddableLandingTab({ defaultTab: 'assistant', defaultPage: 'getting-started' })
).toBe('assistant');
});
it('ignores a tab that is not enabled', () => {
expect(getEmbeddableLandingTab({ tabs: ['assistant'], defaultTab: 'search' })).toBeNull();
expect(
getEmbeddableLandingTab({ tabs: ['assistant', 'search'], defaultPage: 'quickstart' })
).toBeNull();
});
it('ignores an unknown tab', () => {
// @ts-expect-error - the parent window is plain JS and can send anything.
expect(getEmbeddableLandingTab({ defaultTab: 'settings' })).toBeNull();
});
});
@@ -1,17 +0,0 @@
import type { GitBookEmbeddableConfiguration } from '@gitbook/embed';
export type EmbeddableTab = GitBookEmbeddableConfiguration['tabs'][number];
export const DEFAULT_EMBEDDABLE_TABS: EmbeddableTab[] = ['assistant', 'search', 'docs'];
export function getEmbeddableLandingTab(
settings: Partial<Pick<GitBookEmbeddableConfiguration, 'tabs' | 'defaultTab' | 'defaultPage'>>
): EmbeddableTab | null {
const tab = settings.defaultTab ?? (settings.defaultPage ? 'docs' : undefined);
if (!tab || !DEFAULT_EMBEDDABLE_TABS.includes(tab)) {
return null;
}
const tabs = settings.tabs?.length ? settings.tabs : DEFAULT_EMBEDDABLE_TABS;
return tabs.includes(tab) ? tab : null;
}
@@ -2,10 +2,9 @@ import type { SiteSpace } from '@gitbook/api';
import type { IconName } from '@gitbook/icons'; import type { IconName } from '@gitbook/icons';
import type { ButtonProps } from '../primitives'; import type { ButtonProps } from '../primitives';
import { getTargetURLForSiteSpace } from '../SiteSections/encodeClientSiteSections';
import { SpacesDropdownClient } from './SpacesDropdownClient'; import { SpacesDropdownClient } from './SpacesDropdownClient';
import type { GitBookSiteContext } from '@/lib/context'; import type { GitBookSiteContext } from '@/lib/context';
import { getLocalizedTitle } from '@/lib/sites'; import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
// Memoized regex for checking if a string starts with an emoji // Memoized regex for checking if a string starts with an emoji
@@ -21,18 +20,9 @@ export function SpacesDropdown(
siteSpace: SiteSpace; siteSpace: SiteSpace;
siteSpaces: SiteSpace[]; siteSpaces: SiteSpace[];
className?: string; className?: string;
/** Keep the links inside the embed rather than sending readers to the full site. */
asEmbeddable?: boolean;
} & ButtonProps } & ButtonProps
) { ) {
const { const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
context,
siteSpace,
siteSpaces,
className,
asEmbeddable = false,
...buttonProps
} = props;
const currentLanguage = context.locale; const currentLanguage = context.locale;
const dropdownClassName = tcls( const dropdownClassName = tcls(
@@ -43,7 +33,7 @@ export function SpacesDropdown(
const slimSpaces = siteSpaces.map((siteSp) => ({ const slimSpaces = siteSpaces.map((siteSp) => ({
id: siteSp.id, id: siteSp.id,
title: getLocalizedTitle(siteSp, currentLanguage), title: getLocalizedTitle(siteSp, currentLanguage),
url: getTargetURLForSiteSpace(context, siteSp, asEmbeddable), url: getSiteSpaceURL(context, siteSp),
isActive: siteSp.id === siteSpace.id, isActive: siteSp.id === siteSpace.id,
spaceId: siteSp.space.id, spaceId: siteSp.space.id,
})); }));
@@ -67,10 +57,9 @@ export function TranslationsDropdown(
siteSpace: SiteSpace; siteSpace: SiteSpace;
siteSpaces: SiteSpace[]; siteSpaces: SiteSpace[];
className?: string; className?: string;
asEmbeddable?: boolean;
} & ButtonProps } & ButtonProps
) { ) {
const { context, siteSpace, siteSpaces, className, asEmbeddable, ...buttonProps } = props; const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
const title = getLocalizedTitle(siteSpace, context.locale); const title = getLocalizedTitle(siteSpace, context.locale);
const hasEmojiPrefix = startsWithEmoji(title); const hasEmojiPrefix = startsWithEmoji(title);
@@ -81,7 +70,6 @@ export function TranslationsDropdown(
context={context} context={context}
siteSpace={siteSpace} siteSpace={siteSpace}
siteSpaces={siteSpaces} siteSpaces={siteSpaces}
asEmbeddable={asEmbeddable}
variant="blank" variant="blank"
className={tcls( className={tcls(
'bg-transparent lg:max-w-64 max-md:[&_.button-content]:hidden', 'bg-transparent lg:max-w-64 max-md:[&_.button-content]:hidden',
@@ -3,6 +3,13 @@ import { describe, expect, it } from 'bun:test';
import { RevisionPageType } from '@gitbook/api'; import { RevisionPageType } from '@gitbook/api';
import { createPDFLinker } from './linker'; import { createPDFLinker } from './linker';
import { selectPages } from './selectPages';
import {
PDF_DEFAULT_LIMIT,
PDF_SUBTREE_DEFAULT_LIMIT,
getPDFSearchParams,
getPDFURLSearchParams,
} from './urls';
import { createLinker, linkerWithAbsoluteURLs } from '@/lib/links'; import { createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
import { getLinkerForSiteSpace } from '@/lib/sites'; import { getLinkerForSiteSpace } from '@/lib/sites';
@@ -15,6 +22,10 @@ function createDocumentPage(id: string, path: string) {
} as any; } as any;
} }
function createDocumentPageWithChildren(id: string, children: any[]) {
return { ...createDocumentPage(id, id), pages: children };
}
function createGroupPage(id: string, path: string) { function createGroupPage(id: string, path: string) {
return { return {
id, id,
@@ -208,3 +219,183 @@ describe('createPDFLinker', () => {
).toBe('https://open-2v.gitbook.com/~space/HJ1ltuWFvsArFWtevnRn~gitbook/pdf/respond'); ).toBe('https://open-2v.gitbook.com/~space/HJ1ltuWFvsArFWtevnRn~gitbook/pdf/respond');
}); });
}); });
describe('createPDFLinker with batches', () => {
const baseLinker = createLinker({
host: 'open-2v.gitbook.com',
siteBasePath: '/~space/HJ1ltuWFvsArFWtevnRn',
spaceBasePath: '/~space/HJ1ltuWFvsArFWtevnRn',
});
const getBatchURL = (page: { id: string }) =>
page.id === 'other-batch'
? 'https://open-2v.gitbook.com/~gitbook/pdf?offset=20'
: undefined;
it('prefers the published URL for documents exported in another batch', () => {
const linker = createPDFLinker(
baseLinker,
[{ page: createDocumentPage('included', '') }],
createPublishedLinker(),
{ getBatchURL }
);
expect(
linker.toPathForPage({
pages: [
createDocumentPage('included', ''),
createDocumentPage('other-batch', 'respond'),
],
page: createDocumentPage('other-batch', 'respond'),
})
).toBe('https://docs.vectra.ai/deployment/respond');
});
it('links to the batch containing a page when there is no published URL', () => {
const linker = createPDFLinker(
baseLinker,
[{ page: createDocumentPage('included', '') }],
undefined,
{ getBatchURL }
);
expect(
linker.toPathForPage({
pages: [createDocumentPage('other-batch', 'respond')],
page: createDocumentPage('other-batch', 'respond'),
anchor: 'faq',
})
).toBe('https://open-2v.gitbook.com/~gitbook/pdf?offset=20#page-other-batch-faq');
});
it('links group pages to the batch containing them', () => {
const linker = createPDFLinker(
baseLinker,
[{ page: createDocumentPage('included', '') }],
createPublishedLinker(),
{ getBatchURL }
);
expect(
linker.toPathForPage({
pages: [createGroupPage('other-batch', 'group')],
page: createGroupPage('other-batch', 'group'),
})
).toBe('https://open-2v.gitbook.com/~gitbook/pdf?offset=20#page-other-batch');
});
});
describe('getPDFSearchParams', () => {
it('defaults to a small batch for subtree exports', () => {
expect(getPDFSearchParams(new URLSearchParams('page=abc&only=yes'))).toEqual({
page: 'abc',
only: true,
limit: PDF_SUBTREE_DEFAULT_LIMIT,
offset: 0,
});
expect(PDF_SUBTREE_DEFAULT_LIMIT).toBe(20);
});
it('keeps the larger default for whole-space and single-page exports', () => {
expect(getPDFSearchParams(new URLSearchParams('')).limit).toBe(PDF_DEFAULT_LIMIT);
expect(getPDFSearchParams(new URLSearchParams('page=abc')).limit).toBe(PDF_DEFAULT_LIMIT);
expect(PDF_DEFAULT_LIMIT).toBe(100);
});
it('parses an explicit limit and offset', () => {
const params = getPDFSearchParams(
new URLSearchParams('page=abc&only=yes&limit=5&offset=10')
);
expect(params.limit).toBe(5);
expect(params.offset).toBe(10);
});
it('ignores invalid limit and offset values', () => {
const params = getPDFSearchParams(
new URLSearchParams('page=abc&only=yes&limit=nope&offset=-3')
);
expect(params.limit).toBe(PDF_SUBTREE_DEFAULT_LIMIT);
expect(params.offset).toBe(0);
expect(getPDFSearchParams(new URLSearchParams('limit=0&offset=1.5'))).toMatchObject({
limit: PDF_DEFAULT_LIMIT,
offset: 0,
});
});
it('round-trips through getPDFURLSearchParams', () => {
const params = { page: 'abc', only: true, limit: 20, offset: 40, back: 'false' };
expect(getPDFSearchParams(getPDFURLSearchParams(params))).toEqual(params);
});
it('omits offset and limit when not set', () => {
expect(getPDFURLSearchParams({ page: 'abc', only: true }).toString()).toBe(
'page=abc&only=yes'
);
});
});
describe('selectPages', () => {
const children = Array.from({ length: 45 }, (_, index) =>
createDocumentPage(`child-${index}`, `child-${index}`)
);
const rootPages = [createDocumentPageWithChildren('root', children)];
const ids = (pages: { page: { id: string } }[] | undefined) =>
(pages ?? []).map(({ page }) => page.id);
const childIds = (from: number, to: number) => children.slice(from, to).map((page) => page.id);
it('renders the first batch of a subtree by default', () => {
const selection = selectPages(
rootPages,
getPDFSearchParams(new URLSearchParams('page=root&only=yes'))
);
expect(selection?.total).toBe(46);
expect(selection?.offset).toBe(0);
expect(ids(selection?.pages)).toEqual(['root', ...childIds(0, 19)]);
expect(selection?.allPages).toHaveLength(46);
});
it('slices the batch from offset to offset + limit', () => {
const selection = selectPages(rootPages, {
page: 'root',
only: true,
limit: 20,
offset: 20,
});
expect(selection?.offset).toBe(20);
expect(ids(selection?.pages)).toEqual(childIds(19, 39));
});
it('returns a shorter last batch', () => {
const selection = selectPages(rootPages, {
page: 'root',
only: true,
limit: 20,
offset: 40,
});
expect(ids(selection?.pages)).toEqual(childIds(39, 45));
});
it('falls back to the last batch when offset is past the end', () => {
const selection = selectPages(rootPages, {
page: 'root',
only: true,
limit: 20,
offset: 100,
});
expect(selection?.offset).toBe(26);
expect(selection?.pages).toHaveLength(20);
});
it('only selects the page itself without only', () => {
const selection = selectPages(rootPages, { page: 'root', limit: 100, offset: 0 });
expect(ids(selection?.pages)).toEqual(['root']);
expect(selection?.total).toBe(1);
});
it('returns null for an unknown page', () => {
expect(selectPages(rootPages, { page: 'missing', limit: 100, offset: 0 })).toBeNull();
});
});
+30 -73
View File
@@ -3,7 +3,6 @@ import { notFound } from 'next/navigation';
import * as React from 'react'; import * as React from 'react';
import { import {
type Revision,
type RevisionPageDocument, type RevisionPageDocument,
type RevisionPageGroup, type RevisionPageGroup,
type SiteCustomizationSettings, type SiteCustomizationSettings,
@@ -16,7 +15,8 @@ import { ImagesLoadingStatus } from './ImagesLoadingStatus';
import { createPDFLinker, getPagePDFContainerId } from './linker'; import { createPDFLinker, getPagePDFContainerId } from './linker';
import { PageControlButtons } from './PageControlButtons'; import { PageControlButtons } from './PageControlButtons';
import { PDFPrintControls } from './PDFPrintControls'; import { PDFPrintControls } from './PDFPrintControls';
import { type PDFSearchParams, getPDFSearchParams } from './urls'; import { selectPages } from './selectPages';
import { getPDFSearchParams, getPDFURLSearchParams } from './urls';
import { DocumentView } from '@/components/DocumentView'; import { DocumentView } from '@/components/DocumentView';
import { Trademark } from '@/components/TableOfContents/Trademark'; import { Trademark } from '@/components/TableOfContents/Trademark';
import type { PolymorphicComponentProp } from '@/components/utils/types'; import type { PolymorphicComponentProp } from '@/components/utils/types';
@@ -27,13 +27,10 @@ import type { GitBookSiteContext, GitBookSpaceContext } from '@/lib/context';
import { getPageDocument } from '@/lib/data'; import { getPageDocument } from '@/lib/data';
import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links'; import { type GitBookLinker, createLinker, linkerWithAbsoluteURLs } from '@/lib/links';
import './pdf.css'; import './pdf.css';
import { resolvePageId } from '@/lib/pages';
import { getLinkerForSiteSpace } from '@/lib/sites'; import { getLinkerForSiteSpace } from '@/lib/sites';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { defaultCustomization } from '@/lib/utils'; import { defaultCustomization } from '@/lib/utils';
const DEFAULT_LIMIT = 100;
/** /**
* Generate the metadata for the PDF page. * Generate the metadata for the PDF page.
*/ */
@@ -62,13 +59,37 @@ export async function PDFPage(props: {
const language = await getSpaceLanguage(baseContext); const language = await getSpaceLanguage(baseContext);
// Compute the pages to render // Compute the pages to render
const { pages, total } = selectPages(baseContext.revision.pages, pdfParams); const selection = selectPages(baseContext.revision.pages, pdfParams);
if (!selection) {
notFound();
}
const { pages, allPages, offset, total } = selection;
const batchParams = { ...pdfParams, offset };
const pageIds = pages.map( const pageIds = pages.map(
({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string] ({ page }) => [page.id, getPagePDFContainerId(page)] as [string, string]
); );
// Build a linker that create anchor links for the pages rendered in the PDF page. // Build a linker that create anchor links for the pages rendered in the PDF page.
const linker = createPDFLinker(baseContext.linker, pages, getPublishedLinker(baseContext)); const pageIndexes = new Map(allPages.map(({ page }, index) => [page.id, index]));
const linker = createPDFLinker(baseContext.linker, pages, getPublishedLinker(baseContext), {
getBatchURL: (page) => {
const index = pageIndexes.get(page.id);
if (index === undefined) {
return undefined;
}
// Align on the current batch so these links match the previous/next controls.
const batchOffset = Math.max(
0,
offset + Math.floor((index - offset) / pdfParams.limit) * pdfParams.limit
);
return baseContext.linker.toAbsoluteURL(
baseContext.linker.toPathInSpace(
`~gitbook/pdf?${getPDFURLSearchParams({ ...pdfParams, offset: batchOffset }).toString()}`
)
);
},
});
const context: GitBookSpaceContext = { const context: GitBookSpaceContext = {
...baseContext, ...baseContext,
@@ -113,7 +134,7 @@ export async function PDFPage(props: {
</div> </div>
<PageControlButtons <PageControlButtons
params={pdfParams} params={batchParams}
pageIds={pageIds} pageIds={pageIds}
total={total} total={total}
trademark={ trademark={
@@ -126,7 +147,7 @@ export async function PDFPage(props: {
} }
/> />
{pdfParams.only ? null : ( {pdfParams.only || offset > 0 ? null : (
<PDFSpaceIntro space={context.space} customization={customization} /> <PDFSpaceIntro space={context.space} customization={customization} />
)} )}
{pages.map(({ page }) => {pages.map(({ page }) =>
@@ -273,67 +294,3 @@ function PrintPage(
</div> </div>
); );
} }
type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: number };
/**
* Compute the ordered flat set of pages to render.
*/
function selectPages(
rootPages: Revision['pages'],
params: PDFSearchParams
): { pages: FlatPageEntry[]; total: number } {
const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup,
depth: number
): FlatPageEntry[] => {
return [
{ page, depth },
...page.pages.flatMap((child) => {
if (child.type !== 'document') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
];
};
const limitTo = (entries: FlatPageEntry[]) => {
return {
// Apply a soft-limit, the limit can be controlled by the URL to allow testing
pages: entries.slice(0, params.limit ?? DEFAULT_LIMIT),
total: entries.length,
};
};
if (params.page) {
const found = resolvePageId(rootPages, params.page);
if (!found) {
notFound();
}
if (!params.only) {
return limitTo([{ page: found.page, depth: 0 }]);
}
return limitTo(flattenPage(found.page, 0));
}
const allPages = rootPages.flatMap((page) => {
if (page.type !== 'document' && page.type !== 'group') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
return limitTo(allPages);
}
@@ -10,8 +10,6 @@ import { Button } from '@/components/primitives';
import { t, tString, useLanguage } from '@/intl/client'; import { t, tString, useLanguage } from '@/intl/client';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
const limitExtend = 50;
/** /**
* Dynamic controls to show active page and to let the user select between modes. * Dynamic controls to show active page and to let the user select between modes.
*/ */
@@ -19,7 +17,7 @@ export function PageControlButtons(props: {
params: PDFSearchParams; params: PDFSearchParams;
/** Array of the [pageId, divId] */ /** Array of the [pageId, divId] */
pageIds: [string, string][]; pageIds: [string, string][];
/** Total number of pages targetted by the generation */ /** Total number of pages targetted by the generation, across all batches */
total: number; total: number;
/** Trademark to display */ /** Trademark to display */
trademark?: React.ReactNode; trademark?: React.ReactNode;
@@ -38,6 +36,10 @@ export function PageControlButtons(props: {
const activeIndex = (activeDivId ? divIds.indexOf(activeDivId) : 0) + 1; const activeIndex = (activeDivId ? divIds.indexOf(activeDivId) : 0) + 1;
const activePageId = pageIds[activeIndex - 1]?.[0]; const activePageId = pageIds[activeIndex - 1]?.[0];
const batchEnd = params.offset + pageIds.length;
const previousCount = Math.min(params.limit, params.offset);
const nextCount = Math.min(params.limit, total - batchEnd);
return ( return (
<> <>
<div <div
@@ -58,6 +60,8 @@ export function PageControlButtons(props: {
...params, ...params,
page: activePageId, page: activePageId,
only: true, only: true,
limit: undefined,
offset: undefined,
}).toString()}`} }).toString()}`}
variant="secondary" variant="secondary"
label={tString(language, 'pdf_mode_only_page')} label={tString(language, 'pdf_mode_only_page')}
@@ -68,6 +72,8 @@ export function PageControlButtons(props: {
...params, ...params,
page: undefined, page: undefined,
only: false, only: false,
limit: undefined,
offset: undefined,
}).toString()}`} }).toString()}`}
variant="secondary" variant="secondary"
label={tString(language, 'pdf_mode_all')} label={tString(language, 'pdf_mode_all')}
@@ -89,7 +95,7 @@ export function PageControlButtons(props: {
'z-50' 'z-50'
)} )}
> >
{total !== pageIds.length ? ( {previousCount > 0 || nextCount > 0 ? (
<div <div
role="banner" role="banner"
className={tcls( className={tcls(
@@ -112,19 +118,32 @@ export function PageControlButtons(props: {
className={tcls('size-6', 'mr-3', 'mt-1')} className={tcls('size-6', 'mr-3', 'mt-1')}
/>{' '} />{' '}
<div> <div>
<div>{t(language, 'pdf_limit_reached', total, pageIds.length)}</div>
<div> <div>
<a {t(language, 'pdf_batch_range', params.offset + 1, batchEnd, total)}
href={`?${getPDFURLSearchParams({ </div>
...params, <div className={tcls('flex', 'flex-row', 'flex-wrap', 'gap-x-3')}>
page: undefined, {previousCount > 0 ? (
only: false, <a
limit: params.limit + limitExtend, href={`?${getPDFURLSearchParams({
}).toString()}`} ...params,
className={tcls('underline')} offset: params.offset - previousCount,
> }).toString()}`}
{t(language, 'pdf_limit_reached_continue', limitExtend)} className={tcls('underline')}
</a> >
{t(language, 'pdf_batch_previous', previousCount)}
</a>
) : null}
{nextCount > 0 ? (
<a
href={`?${getPDFURLSearchParams({
...params,
offset: batchEnd,
}).toString()}`}
className={tcls('underline')}
>
{t(language, 'pdf_batch_next', nextCount)}
</a>
) : null}
</div> </div>
</div> </div>
</div> </div>
@@ -146,7 +165,7 @@ export function PageControlButtons(props: {
'border' 'border'
)} )}
> >
{t(language, 'pdf_page_of', activeIndex, pageIds.length)} {t(language, 'pdf_page_of', params.offset + activeIndex, total)}
</div> </div>
</div> </div>
</> </>
+21 -18
View File
@@ -20,36 +20,39 @@ export function getPagePDFContainerId(
/** /**
* Create a custom linker for PDF exports. * Create a custom linker for PDF exports.
* *
* This linker generates in-document anchor links for pages that are included * Pages rendered in the current batch get in-document anchors. Other pages link to the
* in the current PDF export, using `getPagePDFContainerId` to build the * published site when possible; pages exported in another batch otherwise link to that batch.
* target element ID. For pages that are not part of the exported PDF, it
* falls back to URLs pointing to the published site if `publishedLinker` is
* provided, preserving navigability for external content. Otherwise, it uses
* absolute URLs from the base linker.
*
* @param baseLinker - The base GitBook linker used to resolve standard paths and URLs.
* @param pages - The list of pages that are included in the current PDF export.
* @param publishedLinker - Optional linker for the published space for external page links.
* @returns A `GitBookLinker` configured to generate PDF-friendly links.
*/ */
export function createPDFLinker( export function createPDFLinker(
baseLinker: GitBookLinker, baseLinker: GitBookLinker,
pages: { page: Revision['pages'][number] }[], pages: { page: Revision['pages'][number] }[],
publishedLinker?: GitBookLinker publishedLinker?: GitBookLinker,
options: {
/** URL of the export batch rendering a page, for pages outside the current batch. */
getBatchURL?: (page: RevisionPageDocument | RevisionPageGroup) => string | undefined;
} = {}
): GitBookLinker { ): GitBookLinker {
const pageIds = new Set(pages.map((p) => p.page.id));
return { return {
...baseLinker, ...baseLinker,
toPathForPage(input) { toPathForPage(input) {
if (pages.some((p) => p.page.id === input.page.id)) { if (pageIds.has(input.page.id)) {
return `#${getPagePDFContainerId(input.page, input.anchor)}`; return `#${getPagePDFContainerId(input.page, input.anchor)}`;
} }
if (input.page.type === RevisionPageType.Group) {
return '#'; // A published page outlives the export URL, so prefer it for documents.
if (input.page.type !== RevisionPageType.Group && publishedLinker) {
return publishedLinker.toPathForPage(input);
} }
// For pages that are not embedded in this PDF export, keep links on the published site. const batchURL = options.getBatchURL?.(input.page);
if (publishedLinker) { if (batchURL) {
return publishedLinker.toPathForPage(input); return `${batchURL}#${getPagePDFContainerId(input.page, input.anchor)}`;
}
if (input.page.type === RevisionPageType.Group) {
return '#';
} }
return baseLinker.toAbsoluteURL(baseLinker.toPathForPage(input)); return baseLinker.toAbsoluteURL(baseLinker.toPathForPage(input));
@@ -0,0 +1,92 @@
import type { Revision, RevisionPageDocument, RevisionPageGroup } from '@gitbook/api';
import type { PDFSearchParams } from './urls';
import { resolvePageId } from '@/lib/pages';
export type FlatPageEntry = { page: RevisionPageDocument | RevisionPageGroup; depth: number };
export interface PDFPageSelection {
/** Pages rendered in the current batch */
pages: FlatPageEntry[];
/** Every page targeted by the export, across all batches */
allPages: FlatPageEntry[];
/** Index in `allPages` of the first page of the batch */
offset: number;
/** Total number of pages targeted by the export */
total: number;
}
/**
* Compute the ordered flat set of pages targeted by the export, and the batch to render.
* Returns `null` if the requested page doesn't exist.
*/
export function selectPages(
rootPages: Revision['pages'],
params: Pick<PDFSearchParams, 'page' | 'only' | 'limit' | 'offset'>
): PDFPageSelection | null {
const allPages = selectAllPages(rootPages, params);
if (!allPages) {
return null;
}
const total = allPages.length;
// Fall back to the last batch rather than rendering an empty one.
const offset = params.offset < total ? params.offset : Math.max(0, total - params.limit);
return {
pages: allPages.slice(offset, offset + params.limit),
allPages,
offset,
total,
};
}
function selectAllPages(
rootPages: Revision['pages'],
params: Pick<PDFSearchParams, 'page' | 'only'>
): FlatPageEntry[] | null {
if (params.page) {
const found = resolvePageId(rootPages, params.page);
if (!found) {
return null;
}
if (!params.only) {
return [{ page: found.page, depth: 0 }];
}
return flattenPage(found.page, 0);
}
return rootPages.flatMap((page) => {
if (page.type !== 'document' && page.type !== 'group') {
return [];
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
}
function flattenPage(
page: RevisionPageDocument | RevisionPageGroup,
depth: number
): FlatPageEntry[] {
return [
{ page, depth },
...page.pages.flatMap((child) => {
if (child.type !== 'document') {
return [];
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
];
}
+56 -12
View File
@@ -1,33 +1,61 @@
const DEFAULT_LIMIT = 100; /** Default number of pages rendered per request when exporting the whole space. */
export const PDF_DEFAULT_LIMIT = 100;
/**
* Default number of pages rendered per request when exporting a page and its subtree.
* Subtrees are often OpenAPI references, which are much heavier to render than prose
* and get the request killed for CPU/memory at higher limits.
*/
export const PDF_SUBTREE_DEFAULT_LIMIT = 20;
export interface PDFSearchParams { export interface PDFSearchParams {
/** Page to export. If none is passed, all pages are exported. */ /** Page to export. If none is passed, all pages are exported. */
page?: string; page?: string;
/** If true, only the `page` is exported, and not its descendant */ /** If true, the `page` is exported with its descendants */
only?: boolean; only?: boolean;
/** Limit the number of pages */ /** Maximum number of pages to render in this batch */
limit: number; limit: number;
/** Index of the first page to render in this batch */
offset: number;
/** URL to redirect back to */ /** URL to redirect back to */
back?: string; back?: string;
} }
/**
* Params accepted when building a PDF export URL. `limit` and `offset` fall back to the defaults.
*/
export type PDFURLParams = Omit<PDFSearchParams, 'limit' | 'offset'> & {
limit?: number;
offset?: number;
};
/**
* Default batch size for a given PDF export.
*/
export function getDefaultPDFLimit(params: Pick<PDFSearchParams, 'page' | 'only'>): number {
return params.page && params.only ? PDF_SUBTREE_DEFAULT_LIMIT : PDF_DEFAULT_LIMIT;
}
/** /**
* Get the PDF export params from the URL serch params. * Get the PDF export params from the URL serch params.
*/ */
export function getPDFSearchParams(searchParams: URLSearchParams): PDFSearchParams { export function getPDFSearchParams(searchParams: URLSearchParams): PDFSearchParams {
const page = searchParams.has('page') ? (searchParams.get('page') ?? '') : undefined;
const only = searchParams.has('only') ? true : undefined;
const params: PDFSearchParams = { const params: PDFSearchParams = {
limit: DEFAULT_LIMIT, limit:
parseInteger(searchParams.get('limit'), { min: 1 }) ??
getDefaultPDFLimit({ page, only }),
offset: parseInteger(searchParams.get('offset'), { min: 0 }) ?? 0,
}; };
if (searchParams.has('page')) { if (page !== undefined) {
params.page = searchParams.get('page') ?? ''; params.page = page;
} }
if (searchParams.has('only')) { if (only) {
params.only = true; params.only = true;
} }
if (searchParams.has('limit')) {
params.limit = Number(searchParams.get('limit'));
}
if (searchParams.has('back')) { if (searchParams.has('back')) {
params.back = searchParams.get('back') ?? ''; params.back = searchParams.get('back') ?? '';
} }
@@ -39,7 +67,7 @@ export function getPDFSearchParams(searchParams: URLSearchParams): PDFSearchPara
* Get the URL search params to use for a PDF export. * Get the URL search params to use for a PDF export.
*/ */
export function getPDFURLSearchParams( export function getPDFURLSearchParams(
params: PDFSearchParams, params: PDFURLParams,
searchParams = new URLSearchParams({}) searchParams = new URLSearchParams({})
): URLSearchParams { ): URLSearchParams {
if (params?.page) { if (params?.page) {
@@ -53,9 +81,16 @@ export function getPDFURLSearchParams(
searchParams.delete('only'); searchParams.delete('only');
} }
// Persist limit and back // Persist limit, offset and back
if (params?.limit) { if (params?.limit) {
searchParams.set('limit', String(params.limit)); searchParams.set('limit', String(params.limit));
} else {
searchParams.delete('limit');
}
if (params?.offset) {
searchParams.set('offset', String(params.offset));
} else {
searchParams.delete('offset');
} }
if (params?.back) { if (params?.back) {
searchParams.set('back', String(params.back)); searchParams.set('back', String(params.back));
@@ -63,3 +98,12 @@ export function getPDFURLSearchParams(
return searchParams; return searchParams;
} }
function parseInteger(value: string | null, options: { min: number }): number | undefined {
if (value === null || value.trim() === '') {
return undefined;
}
const parsed = Number(value);
return Number.isInteger(parsed) && parsed >= options.min ? parsed : undefined;
}
@@ -67,7 +67,7 @@ export function ActionOpenAssistant(props: {
href: page.href, href: page.href,
}); });
} }
assistant.open(undefined, 'page'); assistant.open();
}} }}
/> />
); );
@@ -1,8 +1,4 @@
import { import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
type JSONDocument,
type RevisionPageDocument,
SiteInsightsDisplayContext,
} from '@gitbook/api';
import { DocumentView, DocumentViewSkeleton } from '../DocumentView'; import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
import { CurrentPageProvider } from '../hooks/useCurrentPage'; import { CurrentPageProvider } from '../hooks/useCurrentPage';
@@ -131,7 +127,6 @@ export async function PageBody(props: {
ancestors={ancestors} ancestors={ancestors}
withRSSFeed={contentHasUpdates} withRSSFeed={contentHasUpdates}
hasAPIBlocks={hasAPIBlocks} hasAPIBlocks={hasAPIBlocks}
asEmbeddable={insightsDisplayContext === SiteInsightsDisplayContext.Embed}
/> />
{document && !isNodeEmpty(document) ? ( {document && !isNodeEmpty(document) ? (
<OptionalSuspense <OptionalSuspense
@@ -19,17 +19,14 @@ import {
import { PageAsideToggleButton } from '../PageAside/PageAsideButton'; import { PageAsideToggleButton } from '../PageAside/PageAsideButton';
import { PageIcon } from '../PageIcon'; import { PageIcon } from '../PageIcon';
import { getPDFURLSearchParams } from '../PDF'; import { getPDFURLSearchParams } from '../PDF';
import { import { findBestTargetURL } from '../SiteSections/encodeClientSiteSections';
findBestTargetURL,
getTargetURLForSiteSpace,
} from '../SiteSections/encodeClientSiteSections';
import { categorizeVariants } from '../SpaceLayout/categorizeVariants'; import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown'; import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown';
import { PageTags } from './PageTags'; import { PageTags } from './PageTags';
import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context'; import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context';
import { hasAdaptiveMcpEndpoint } from '@/lib/mcp/endpoints'; import { hasAdaptiveMcpEndpoint } from '@/lib/mcp/endpoints';
import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages'; import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
import { getLocalizedTitle } from '@/lib/sites'; import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { getPageRSSURL } from '@/routes/rss'; import { getPageRSSURL } from '@/routes/rss';
@@ -44,10 +41,8 @@ export async function PageHeader(props: {
* exactly as-is. * exactly as-is.
*/ */
hasAPIBlocks: boolean; hasAPIBlocks: boolean;
/** Keep the context crumbs' links inside the embed rather than sending readers to the full site. */
asEmbeddable: boolean;
}) { }) {
const { context, page, ancestors, withRSSFeed, hasAPIBlocks, asEmbeddable } = props; const { context, page, ancestors, withRSSFeed, hasAPIBlocks } = props;
const { revision, linker } = context; const { revision, linker } = context;
const hasAncestors = ancestors.length > 0; const hasAncestors = ancestors.length > 0;
@@ -87,10 +82,7 @@ export async function PageHeader(props: {
// Section groups have no URL of their own; only sections are navigable. Resolve the // Section groups have no URL of their own; only sections are navigable. Resolve the
// section to the site space matching the current variant so switching sections keeps // section to the site space matching the current variant so switching sections keeps
// the reader on their current variant (same logic as the section tabs). // the reader on their current variant (same logic as the section tabs).
href: href: node.object === 'site-section' ? findBestTargetURL(context, node) : undefined,
node.object === 'site-section'
? findBestTargetURL(context, node, asEmbeddable)
: undefined,
label: getLocalizedTitle(node, context.locale), label: getLocalizedTitle(node, context.locale),
icon: node.icon, icon: node.icon,
siblings: siblings siblings: siblings
@@ -100,7 +92,7 @@ export async function PageHeader(props: {
// Don't offer hidden sections/groups as switch targets. // Don't offer hidden sections/groups as switch targets.
.filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id)) .filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id))
.map((sibling) => { .map((sibling) => {
const href = getSectionNodeURL(context, sibling, asEmbeddable); const href = getSectionNodeURL(context, sibling);
// Keep siblings whose URL is "" (the site's first page); only drop the ones // Keep siblings whose URL is "" (the site's first page); only drop the ones
// with no resolvable URL at all. // with no resolvable URL at all.
return href !== undefined return href !== undefined
@@ -120,7 +112,7 @@ export async function PageHeader(props: {
if (currentSiteSpace) { if (currentSiteSpace) {
contextCrumbs.push({ contextCrumbs.push({
key: `variant-${currentSiteSpace.id}`, key: `variant-${currentSiteSpace.id}`,
href: getTargetURLForSiteSpace(context, currentSiteSpace, asEmbeddable), href: getSiteSpaceURL(context, currentSiteSpace),
label: getLocalizedTitle(currentSiteSpace, context.locale), label: getLocalizedTitle(currentSiteSpace, context.locale),
siblings: [], siblings: [],
// Reuse the header's variant switcher rather than plain per-variant links, so each entry // Reuse the header's variant switcher rather than plain per-variant links, so each entry
@@ -130,7 +122,7 @@ export async function PageHeader(props: {
slimSpaces: variantSpaces.map((siteSpace) => ({ slimSpaces: variantSpaces.map((siteSpace) => ({
id: siteSpace.id, id: siteSpace.id,
title: getLocalizedTitle(siteSpace, context.locale), title: getLocalizedTitle(siteSpace, context.locale),
url: getTargetURLForSiteSpace(context, siteSpace, asEmbeddable), url: getSiteSpaceURL(context, siteSpace),
isActive: siteSpace.id === currentSiteSpace.id, isActive: siteSpace.id === currentSiteSpace.id,
spaceId: siteSpace.space.id, spaceId: siteSpace.space.id,
})), })),
@@ -456,13 +448,9 @@ function collectSectionNodeIds(list: SiteStructureNode[]): Set<string> {
* The URL resolves to the site space matching the current variant (same logic as the section tabs), * The URL resolves to the site space matching the current variant (same logic as the section tabs),
* so switching sections keeps the reader on their current variant. * so switching sections keeps the reader on their current variant.
*/ */
function getSectionNodeURL( function getSectionNodeURL(context: GitBookSiteContext, node: SectionNode): string | undefined {
context: GitBookSiteContext,
node: SectionNode,
asEmbeddable: boolean
): string | undefined {
const section = findFirstSection(node); const section = findFirstSection(node);
return section ? findBestTargetURL(context, section, asEmbeddable) : undefined; return section ? findBestTargetURL(context, section) : undefined;
} }
/** /**
@@ -545,7 +533,6 @@ function getPageActionsURLs({
`~gitbook/pdf?${getPDFURLSearchParams({ `~gitbook/pdf?${getPDFURLSearchParams({
page: page.id, page: page.id,
only: true, only: true,
limit: 100,
}).toString()}` }).toString()}`
) )
: undefined, : undefined,
@@ -284,11 +284,11 @@ export async function getSitePageData(props: SitePageProps) {
if (pathname !== null) { if (pathname !== null) {
// If the pathname was not normalized, redirect to the normalized version // If the pathname was not normalized, redirect to the normalized version
// before trying to resolve the page again // before trying to resolve the page again
redirect(context.linker.toPathForPagePath({ path: pathname })); redirect(context.linker.toPathInSpace(pathname));
} else { } else {
// If the page is not found and we are in fallback mode, return a redirect to the basepath // If the page is not found and we are in fallback mode, return a redirect to the basepath
if (context.isFallback) { if (context.isFallback) {
redirect(context.linker.toPathForPagePath({ path: '/' })); redirect(context.linker.toPathInSpace('/'));
} }
notFound(); notFound();
} }
@@ -16,7 +16,6 @@ import { useSetSearchState } from '../Search';
import { fetchSiteIndex } from '../Search/site-index'; import { fetchSiteIndex } from '../Search/site-index';
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink'; import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
import { import {
useIsLoggedInVisitor,
useSiteAdaptiveAuthLoginHref, useSiteAdaptiveAuthLoginHref,
useSiteIndexURL, useSiteIndexURL,
useSpaceBasePath, useSpaceBasePath,
@@ -45,9 +44,6 @@ export function SitePageNotFound() {
const setSearchState = useSetSearchState(); const setSearchState = useSetSearchState();
const { assistants } = useAI(); const { assistants } = useAI();
const isAuthenticated = useIsLoggedInVisitor();
const copy = getNotFoundCopy({ hasAdaptiveLogin: !!adaptiveAuthLoginHref, isAuthenticated });
// Show the assistant input when a non-search assistant is available (i.e. not just ask-AI). // Show the assistant input when a non-search assistant is available (i.e. not just ask-AI).
const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null; const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null;
const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search'); const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search');
@@ -128,20 +124,21 @@ export function SitePageNotFound() {
'text-tint-strong' 'text-tint-strong'
)} )}
> >
{t(language, copy.title)} {t(
language,
adaptiveAuthLoginHref
? 'notfound_adaptive_title'
: 'notfound_title'
)}
</h1> </h1>
<p className={tcls('text-base', 'text-tint', 'text-center')}> <p className={tcls('text-base', 'text-tint')}>
{t(language, copy.message)} {t(
language,
adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound'
)}
</p> </p>
</div> </div>
{adaptiveAuthLoginHref && isAuthenticated ? ( {adaptiveAuthLoginHref ? (
// Re-running login refreshes the visitor's claims, e.g. after a role change.
<SiteAuthLoginButton
href={adaptiveAuthLoginHref}
variant="secondary"
label={t(language, 'notfound_adaptive_login_again')}
/>
) : adaptiveAuthLoginHref ? (
<div className="flex flex-col items-center gap-4"> <div className="flex flex-col items-center gap-4">
<SiteAuthLoginButton <SiteAuthLoginButton
href={adaptiveAuthLoginHref} href={adaptiveAuthLoginHref}
@@ -193,19 +190,6 @@ export function SitePageNotFound() {
); );
} }
function getNotFoundCopy(args: { hasAdaptiveLogin: boolean; isAuthenticated: boolean }) {
if (!args.hasAdaptiveLogin) {
return { title: 'notfound_title', message: 'notfound' } as const;
}
if (args.isAuthenticated) {
return {
title: 'notfound_adaptive_no_access_title',
message: 'notfound_adaptive_no_access',
} as const;
}
return { title: 'notfound_adaptive_title', message: 'notfound_adaptive' } as const;
}
/** /**
* List of related pages, with skeleton rows while they load. Renders nothing once we know * List of related pages, with skeleton rows while they load. Renders nothing once we know
* there are no suggestions. * there are no suggestions.
@@ -201,7 +201,7 @@ function getTargetURLForSection(
return getSectionURL(context, section); return getSectionURL(context, section);
} }
export function getTargetURLForSiteSpace( function getTargetURLForSiteSpace(
context: GitBookSiteContext, context: GitBookSiteContext,
siteSpace: SiteSpace, siteSpace: SiteSpace,
asEmbeddable: boolean asEmbeddable: boolean
@@ -72,7 +72,6 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
<SpaceLayoutContextProvider <SpaceLayoutContextProvider
basePath={context.linker.toPathInSpace('')} basePath={context.linker.toPathInSpace('')}
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref} siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
isLoggedInVisitor={context.isLoggedInVisitor}
siteIndexURL={context.linker.toPathInSite('~gitbook/site-index')} siteIndexURL={context.linker.toPathInSite('~gitbook/site-index')}
> >
<AdaptiveVisitorContextProvider <AdaptiveVisitorContextProvider
@@ -5,7 +5,6 @@ import React from 'react';
const SpaceLayoutContext = React.createContext({ const SpaceLayoutContext = React.createContext({
basePath: '', basePath: '',
siteAdaptiveAuthLoginHref: null as string | null, siteAdaptiveAuthLoginHref: null as string | null,
isLoggedInVisitor: false,
siteIndexURL: '', siteIndexURL: '',
}); });
@@ -16,21 +15,14 @@ export function SpaceLayoutContextProvider(
props: React.PropsWithChildren<{ props: React.PropsWithChildren<{
basePath: string; basePath: string;
siteAdaptiveAuthLoginHref?: string | null; siteAdaptiveAuthLoginHref?: string | null;
isLoggedInVisitor?: boolean;
siteIndexURL: string; siteIndexURL: string;
}> }>
) { ) {
const { const { basePath, siteAdaptiveAuthLoginHref = null, siteIndexURL, children } = props;
basePath,
siteAdaptiveAuthLoginHref = null,
isLoggedInVisitor = false,
siteIndexURL,
children,
} = props;
const value = React.useMemo( const value = React.useMemo(
() => ({ basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL }), () => ({ basePath, siteAdaptiveAuthLoginHref, siteIndexURL }),
[basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL] [basePath, siteAdaptiveAuthLoginHref, siteIndexURL]
); );
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>; return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
@@ -58,17 +50,6 @@ export function useSiteAdaptiveAuthLoginHref() {
return context.siteAdaptiveAuthLoginHref; return context.siteAdaptiveAuthLoginHref;
} }
/**
* Return whether the visitor has a visitor auth token, with or without claims.
*/
export function useIsLoggedInVisitor() {
const context = React.useContext(SpaceLayoutContext);
if (!context) {
throw new Error('SpaceLayoutContext not found');
}
return context.isLoggedInVisitor;
}
/** /**
* Return the URL of the site search index (`~gitbook/site-index`). * Return the URL of the site search index (`~gitbook/site-index`).
*/ */
+5 -8
View File
@@ -77,14 +77,10 @@ export const ar: TranslationLanguage = {
notfound_title: 'الصفحة غير موجودة', notfound_title: 'الصفحة غير موجودة',
notfound_link: 'يشير هذا الرابط إلى صفحة تمت إزالتها أو لم تعد موجودة.', notfound_link: 'يشير هذا الرابط إلى صفحة تمت إزالتها أو لم تعد موجودة.',
notfound: 'الصفحة التي تبحث عنها غير موجودة.', notfound: 'الصفحة التي تبحث عنها غير موجودة.',
notfound_adaptive_title: 'سجّل الدخول لعرض هذه الصفحة', notfound_adaptive_title: 'الصفحة غير متاحة',
notfound_adaptive: 'قد تتطلب هذه الصفحة تسجيل الدخول. إذا اتبعت رابطًا، فتحقق من صحته.', notfound_adaptive: 'قد تكون هذه الصفحة موجودة، لكن قد تحتاج إلى تسجيل الدخول للوصول إليها.',
notfound_adaptive_login: 'تسجيل الدخول', notfound_adaptive_login: 'تسجيل الدخول',
notfound_adaptive_registration_hint: 'قد يكون التسجيل متاحًا حسب المؤسسة.', notfound_adaptive_registration_hint: 'قد يكون التسجيل متاحًا حسب المؤسسة.',
notfound_adaptive_no_access_title: 'قد لا تملك صلاحية الوصول إلى هذه الصفحة',
notfound_adaptive_no_access:
'قد لا تكون هذه الصفحة موجودة، أو قد لا يملك حسابك صلاحية الوصول إليها. إذا اتبعت رابطًا، فتحقق من صحته.',
notfound_adaptive_login_again: 'تسجيل الدخول مجددًا',
notfound_goto_home: 'الانتقال إلى الصفحة الرئيسية', notfound_goto_home: 'الانتقال إلى الصفحة الرئيسية',
notfound_suggestions_title: 'ربما تبحث عن', notfound_suggestions_title: 'ربما تبحث عن',
unexpected_error_title: 'حدث خطأ', unexpected_error_title: 'حدث خطأ',
@@ -100,8 +96,9 @@ export const ar: TranslationLanguage = {
pdf_page_of: '${1} من ${2}', pdf_page_of: '${1} من ${2}',
pdf_mode_only_page: 'هذه الصفحة فقط', pdf_mode_only_page: 'هذه الصفحة فقط',
pdf_mode_all: 'كل الصفحات', pdf_mode_all: 'كل الصفحات',
pdf_limit_reached: 'تعذر إنشاء ملف PDF لـ ${1} صفحات، وتوقف الإنشاء عند ${2}.', pdf_batch_range: 'عرض الصفحات ${1}–${2} من ${3}.',
pdf_limit_reached_continue: 'التوسيع بإضافة ${1} صفحات أخرى.', pdf_batch_previous: 'الصفحات الـ ${1} السابقة',
pdf_batch_next: 'الصفحات الـ ${1} التالية',
more: 'المزيد', more: 'المزيد',
link_tooltip_external_link: 'رابط خارجي إلى', link_tooltip_external_link: 'رابط خارجي إلى',
link_tooltip_email: 'إرسال بريد إلكتروني إلى', link_tooltip_email: 'إرسال بريد إلكتروني إلى',
+5 -9
View File
@@ -78,16 +78,12 @@ export const bg: TranslationLanguage = {
notfound_title: 'Страницата не е намерена', notfound_title: 'Страницата не е намерена',
notfound_link: 'Тази връзка сочи към страница, която е премахната или вече не съществува.', notfound_link: 'Тази връзка сочи към страница, която е премахната или вече не съществува.',
notfound: 'Страницата, която търсите, не съществува.', notfound: 'Страницата, която търсите, не съществува.',
notfound_adaptive_title: 'Влезте, за да видите тази страница', notfound_adaptive_title: 'Страницата не е налична',
notfound_adaptive: notfound_adaptive:
'Тази страница може да изисква вход. Ако сте последвали връзка, проверете дали е правилна.', 'Тази страница може да съществува, но може да трябва да влезете, за да я достъпите.',
notfound_adaptive_login: 'Вход', notfound_adaptive_login: 'Вход',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Регистрацията може да е възможна в зависимост от организацията.', 'Регистрацията може да е възможна в зависимост от организацията.',
notfound_adaptive_no_access_title: 'Възможно е да нямате достъп до тази страница',
notfound_adaptive_no_access:
'Тази страница може да не съществува или профилът ви може да няма достъп до нея. Ако сте последвали връзка, проверете дали е правилна.',
notfound_adaptive_login_again: 'Повторен вход',
notfound_goto_home: 'Към началната страница', notfound_goto_home: 'Към началната страница',
notfound_suggestions_title: 'Може би търсите', notfound_suggestions_title: 'Може би търсите',
unexpected_error_title: 'Възникна грешка', unexpected_error_title: 'Възникна грешка',
@@ -103,9 +99,9 @@ export const bg: TranslationLanguage = {
pdf_page_of: '${1} от ${2}', pdf_page_of: '${1} от ${2}',
pdf_mode_only_page: 'Само тази страница', pdf_mode_only_page: 'Само тази страница',
pdf_mode_all: 'Всички страници', pdf_mode_all: 'Всички страници',
pdf_limit_reached: pdf_batch_range: 'Показани са страници ${1}–${2} от ${3}.',
'PDF файлът за ${1} страници не можа да бъде генериран, генерирането спря на ${2}.', pdf_batch_previous: 'Предишни ${1} страници',
pdf_limit_reached_continue: 'Разширяване с още ${1} страници.', pdf_batch_next: 'Следващи ${1} страници',
more: 'Още', more: 'Още',
link_tooltip_external_link: 'Външна връзка към', link_tooltip_external_link: 'Външна връзка към',
link_tooltip_email: 'Изпращане на имейл до', link_tooltip_email: 'Изпращане на имейл до',
+5 -9
View File
@@ -77,15 +77,11 @@ export const cs: TranslationLanguage = {
notfound_title: 'Stránka nenalezena', notfound_title: 'Stránka nenalezena',
notfound_link: 'Tento odkaz vede na stránku, která byla odstraněna nebo již neexistuje.', notfound_link: 'Tento odkaz vede na stránku, která byla odstraněna nebo již neexistuje.',
notfound: 'Stránka, kterou hledáte, neexistuje.', notfound: 'Stránka, kterou hledáte, neexistuje.',
notfound_adaptive_title: 'Pro zobrazení této stránky se přihlaste', notfound_adaptive_title: 'Stránka není dostupná',
notfound_adaptive: notfound_adaptive:
'Tato stránka může vyžadovat přihlášení. Pokud jste přešli z odkazu, zkontrolujte, zda je správný.', 'Tato stránka může existovat, ale pro přístup se možná budete muset přihlásit.',
notfound_adaptive_login: 'Přihlásit se', notfound_adaptive_login: 'Přihlásit se',
notfound_adaptive_registration_hint: 'Registrace může být dostupná v závislosti na organizaci.', notfound_adaptive_registration_hint: 'Registrace může být dostupná v závislosti na organizaci.',
notfound_adaptive_no_access_title: 'K této stránce možná nemáte přístup',
notfound_adaptive_no_access:
'Tato stránka možná neexistuje nebo k ní váš účet nemá přístup. Pokud jste přešli z odkazu, zkontrolujte, zda je správný.',
notfound_adaptive_login_again: 'Přihlásit se znovu',
notfound_goto_home: 'Přejít na domovskou stránku', notfound_goto_home: 'Přejít na domovskou stránku',
notfound_suggestions_title: 'Možná hledáte', notfound_suggestions_title: 'Možná hledáte',
unexpected_error_title: 'Došlo k chybě', unexpected_error_title: 'Došlo k chybě',
@@ -101,9 +97,9 @@ export const cs: TranslationLanguage = {
pdf_page_of: '${1} z ${2}', pdf_page_of: '${1} z ${2}',
pdf_mode_only_page: 'Pouze tato stránka', pdf_mode_only_page: 'Pouze tato stránka',
pdf_mode_all: 'Všechny stránky', pdf_mode_all: 'Všechny stránky',
pdf_limit_reached: pdf_batch_range: 'Zobrazeny stránky ${1}–${2} z ${3}.',
'Nepodařilo se vygenerovat PDF pro ${1} stránek, generování se zastavilo na ${2}.', pdf_batch_previous: 'Předchozích ${1} stránek',
pdf_limit_reached_continue: 'Rozšířit o dalších ${1} stránek.', pdf_batch_next: 'Dalších ${1} stránek',
more: 'Více', more: 'Více',
link_tooltip_external_link: 'Externí odkaz na', link_tooltip_external_link: 'Externí odkaz na',
link_tooltip_email: 'Odeslat e-mail na', link_tooltip_email: 'Odeslat e-mail na',
+5 -8
View File
@@ -77,15 +77,11 @@ export const da: TranslationLanguage = {
notfound_title: 'Siden blev ikke fundet', notfound_title: 'Siden blev ikke fundet',
notfound_link: 'Dette link peger på en side, der er blevet fjernet eller ikke længere findes.', notfound_link: 'Dette link peger på en side, der er blevet fjernet eller ikke længere findes.',
notfound: 'Siden, du leder efter, findes ikke.', notfound: 'Siden, du leder efter, findes ikke.',
notfound_adaptive_title: 'Log ind for at se denne side', notfound_adaptive_title: 'Siden er ikke tilgængelig',
notfound_adaptive: notfound_adaptive:
'Denne side kræver muligvis, at du logger ind. Hvis du har fulgt et link, så tjek, at det er korrekt.', 'Denne side findes muligvis, men du skal muligvis logge ind for at få adgang.',
notfound_adaptive_login: 'Log ind', notfound_adaptive_login: 'Log ind',
notfound_adaptive_registration_hint: 'Registrering kan være mulig afhængigt af organisationen.', notfound_adaptive_registration_hint: 'Registrering kan være mulig afhængigt af organisationen.',
notfound_adaptive_no_access_title: 'Du har muligvis ikke adgang til denne side',
notfound_adaptive_no_access:
'Denne side findes muligvis ikke, eller din konto har muligvis ikke adgang til den. Hvis du har fulgt et link, så tjek, at det er korrekt.',
notfound_adaptive_login_again: 'Log ind igen',
notfound_goto_home: 'Gå til forsiden', notfound_goto_home: 'Gå til forsiden',
notfound_suggestions_title: 'Du leder måske efter', notfound_suggestions_title: 'Du leder måske efter',
unexpected_error_title: 'Der opstod en fejl', unexpected_error_title: 'Der opstod en fejl',
@@ -101,8 +97,9 @@ export const da: TranslationLanguage = {
pdf_page_of: '${1} af ${2}', pdf_page_of: '${1} af ${2}',
pdf_mode_only_page: 'Kun denne side', pdf_mode_only_page: 'Kun denne side',
pdf_mode_all: 'Alle sider', pdf_mode_all: 'Alle sider',
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, genereringen stoppede ved ${2}.', pdf_batch_range: 'Viser sider ${1}–${2} af ${3}.',
pdf_limit_reached_continue: 'Udvid med ${1} flere sider.', pdf_batch_previous: 'Forrige ${1} sider',
pdf_batch_next: 'Næste ${1} sider',
more: 'Mere', more: 'Mere',
link_tooltip_external_link: 'Eksternt link til', link_tooltip_external_link: 'Eksternt link til',
link_tooltip_email: 'Send e-mail til', link_tooltip_email: 'Send e-mail til',
+5 -9
View File
@@ -80,16 +80,12 @@ export const de: TranslationLanguage = {
notfound_link: notfound_link:
'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.', 'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.',
notfound: 'Die gesuchte Seite existiert nicht.', notfound: 'Die gesuchte Seite existiert nicht.',
notfound_adaptive_title: 'Melden Sie sich an, um diese Seite anzuzeigen', notfound_adaptive_title: 'Seite nicht verfügbar',
notfound_adaptive: notfound_adaptive:
'Für diese Seite ist möglicherweise eine Anmeldung erforderlich. Wenn Sie einem Link gefolgt sind, prüfen Sie, ob er korrekt ist.', 'Diese Seite existiert möglicherweise, aber Sie müssen sich eventuell anmelden, um darauf zuzugreifen.',
notfound_adaptive_login: 'Anmelden', notfound_adaptive_login: 'Anmelden',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Die Registrierung ist je nach Organisation möglicherweise verfügbar.', 'Die Registrierung ist je nach Organisation möglicherweise verfügbar.',
notfound_adaptive_no_access_title: 'Sie haben möglicherweise keinen Zugriff auf diese Seite',
notfound_adaptive_no_access:
'Diese Seite existiert möglicherweise nicht, oder Ihr Konto hat keinen Zugriff darauf. Wenn Sie einem Link gefolgt sind, prüfen Sie, ob er korrekt ist.',
notfound_adaptive_login_again: 'Erneut anmelden',
notfound_goto_home: 'Zur Startseite', notfound_goto_home: 'Zur Startseite',
notfound_suggestions_title: 'Vielleicht suchen Sie nach', notfound_suggestions_title: 'Vielleicht suchen Sie nach',
unexpected_error_title: 'Ein Fehler ist aufgetreten', unexpected_error_title: 'Ein Fehler ist aufgetreten',
@@ -106,9 +102,9 @@ export const de: TranslationLanguage = {
pdf_page_of: '${1} von ${2}', pdf_page_of: '${1} von ${2}',
pdf_mode_only_page: 'Nur diese Seite', pdf_mode_only_page: 'Nur diese Seite',
pdf_mode_all: 'Alle Seiten', pdf_mode_all: 'Alle Seiten',
pdf_limit_reached: pdf_batch_range: 'Seiten ${1}–${2} von ${3}.',
'Das PDF konnte für ${1} Seiten nicht generiert werden, Generierung wurde bei ${2} gestoppt.', pdf_batch_previous: 'Vorherige ${1} Seiten',
pdf_limit_reached_continue: 'Mit ${1} weiteren Seiten erweitern.', pdf_batch_next: 'Nächste ${1} Seiten',
more: 'Mehr', more: 'Mehr',
link_tooltip_external_link: 'Externer Link zu', link_tooltip_external_link: 'Externer Link zu',
link_tooltip_email: 'E-Mail senden an', link_tooltip_email: 'E-Mail senden an',
+5 -9
View File
@@ -78,16 +78,12 @@ export const el: TranslationLanguage = {
notfound_title: 'Η σελίδα δεν βρέθηκε', notfound_title: 'Η σελίδα δεν βρέθηκε',
notfound_link: 'Αυτός ο σύνδεσμος οδηγεί σε μια σελίδα που έχει αφαιρεθεί ή δεν υπάρχει πλέον.', notfound_link: 'Αυτός ο σύνδεσμος οδηγεί σε μια σελίδα που έχει αφαιρεθεί ή δεν υπάρχει πλέον.',
notfound: 'Η σελίδα που αναζητάτε δεν υπάρχει.', notfound: 'Η σελίδα που αναζητάτε δεν υπάρχει.',
notfound_adaptive_title: 'Συνδεθείτε για να δείτε αυτή τη σελίδα', notfound_adaptive_title: 'Η σελίδα δεν είναι διαθέσιμη',
notfound_adaptive: notfound_adaptive:
'Αυτή η σελίδα ενδέχεται να απαιτεί σύνδεση. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.', 'Αυτή η σελίδα μπορεί να υπάρχει, αλλά ίσως χρειαστεί να συνδεθείτε για πρόσβαση.',
notfound_adaptive_login: 'Σύνδεση', notfound_adaptive_login: 'Σύνδεση',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Η εγγραφή ενδέχεται να είναι διαθέσιμη ανάλογα με τον οργανισμό.', 'Η εγγραφή ενδέχεται να είναι διαθέσιμη ανάλογα με τον οργανισμό.',
notfound_adaptive_no_access_title: 'Ίσως δεν έχετε πρόσβαση σε αυτή τη σελίδα',
notfound_adaptive_no_access:
'Αυτή η σελίδα ενδέχεται να μην υπάρχει ή ο λογαριασμός σας ενδέχεται να μην έχει πρόσβαση σε αυτήν. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.',
notfound_adaptive_login_again: 'Επανασύνδεση',
notfound_goto_home: 'Μετάβαση στην αρχική σελίδα', notfound_goto_home: 'Μετάβαση στην αρχική σελίδα',
notfound_suggestions_title: 'Ίσως αναζητάτε', notfound_suggestions_title: 'Ίσως αναζητάτε',
unexpected_error_title: 'Παρουσιάστηκε σφάλμα', unexpected_error_title: 'Παρουσιάστηκε σφάλμα',
@@ -104,9 +100,9 @@ export const el: TranslationLanguage = {
pdf_page_of: '${1} από ${2}', pdf_page_of: '${1} από ${2}',
pdf_mode_only_page: 'Μόνο αυτή η σελίδα', pdf_mode_only_page: 'Μόνο αυτή η σελίδα',
pdf_mode_all: 'Όλες οι σελίδες', pdf_mode_all: 'Όλες οι σελίδες',
pdf_limit_reached: pdf_batch_range: 'Εμφάνιση σελίδων ${1}–${2} από ${3}.',
'Δεν ήταν δυνατή η δημιουργία PDF για ${1} σελίδες, η δημιουργία σταμάτησε στο ${2}.', pdf_batch_previous: 'Προηγούμενες ${1} σελίδες',
pdf_limit_reached_continue: 'Επέκταση με ${1} ακόμη σελίδες.', pdf_batch_next: 'Επόμενες ${1} σελίδες',
more: 'Περισσότερα', more: 'Περισσότερα',
link_tooltip_external_link: 'Εξωτερικός σύνδεσμος προς', link_tooltip_external_link: 'Εξωτερικός σύνδεσμος προς',
link_tooltip_email: 'Αποστολή email σε', link_tooltip_email: 'Αποστολή email σε',
+5 -9
View File
@@ -75,16 +75,11 @@ export const en = {
notfound_title: 'Page not found', notfound_title: 'Page not found',
notfound_link: 'This link points to a page that has been removed or no longer exists.', notfound_link: 'This link points to a page that has been removed or no longer exists.',
notfound: "The page you're looking for doesn't exist.", notfound: "The page you're looking for doesn't exist.",
notfound_adaptive_title: 'Log in to view this page', notfound_adaptive_title: 'Page unavailable',
notfound_adaptive: notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
"This page may require you to log in. If you followed a link, check that it's correct.",
notfound_adaptive_login: 'Log in', notfound_adaptive_login: 'Log in',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registration may be available depending on the organization.', 'Registration may be available depending on the organization.',
notfound_adaptive_no_access_title: 'You may not have access to this page',
notfound_adaptive_no_access:
"This page may not exist, or your account may not have access to it. If you followed a link, check that it's correct.",
notfound_adaptive_login_again: 'Log in again',
notfound_goto_home: 'Go to homepage', notfound_goto_home: 'Go to homepage',
notfound_suggestions_title: 'You might be looking for', notfound_suggestions_title: 'You might be looking for',
unexpected_error_title: 'An error occurred', unexpected_error_title: 'An error occurred',
@@ -100,8 +95,9 @@ export const en = {
pdf_page_of: '${1} of ${2}', pdf_page_of: '${1} of ${2}',
pdf_mode_only_page: 'Only this page', pdf_mode_only_page: 'Only this page',
pdf_mode_all: 'All pages', pdf_mode_all: 'All pages',
pdf_limit_reached: "Couldn't generate the PDF for ${1} pages, generation stopped at ${2}.", pdf_batch_range: 'Showing pages ${1}–${2} of ${3}.',
pdf_limit_reached_continue: 'Extend with ${1} more pages.', pdf_batch_previous: 'Previous ${1} pages',
pdf_batch_next: 'Next ${1} pages',
more: 'More', more: 'More',
link_tooltip_external_link: 'External link to', link_tooltip_external_link: 'External link to',
link_tooltip_email: 'Send an email to', link_tooltip_email: 'Send an email to',
+5 -9
View File
@@ -79,16 +79,12 @@ export const es: TranslationLanguage = {
notfound_title: 'Página no encontrada', notfound_title: 'Página no encontrada',
notfound_link: 'Este enlace apunta a una página que ha sido eliminada o ya no existe.', notfound_link: 'Este enlace apunta a una página que ha sido eliminada o ya no existe.',
notfound: 'La página que buscas no existe.', notfound: 'La página que buscas no existe.',
notfound_adaptive_title: 'Inicia sesión para ver esta página', notfound_adaptive_title: 'Página no disponible',
notfound_adaptive: notfound_adaptive:
'Es posible que esta página requiera iniciar sesión. Si has seguido un enlace, comprueba que sea correcto.', 'Esta página puede existir, pero es posible que debas iniciar sesión para acceder.',
notfound_adaptive_login: 'Iniciar sesión', notfound_adaptive_login: 'Iniciar sesión',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'El registro puede estar disponible según la organización.', 'El registro puede estar disponible según la organización.',
notfound_adaptive_no_access_title: 'Es posible que no tengas acceso a esta página',
notfound_adaptive_no_access:
'Es posible que esta página no exista o que tu cuenta no tenga acceso a ella. Si has seguido un enlace, comprueba que sea correcto.',
notfound_adaptive_login_again: 'Volver a iniciar sesión',
notfound_goto_home: 'Ir al inicio', notfound_goto_home: 'Ir al inicio',
notfound_suggestions_title: 'Quizás estés buscando', notfound_suggestions_title: 'Quizás estés buscando',
unexpected_error_title: 'Ocurrió un error', unexpected_error_title: 'Ocurrió un error',
@@ -105,9 +101,9 @@ export const es: TranslationLanguage = {
pdf_page_of: '${1} de ${2}', pdf_page_of: '${1} de ${2}',
pdf_mode_only_page: 'Solo esta página', pdf_mode_only_page: 'Solo esta página',
pdf_mode_all: 'Todas las páginas', pdf_mode_all: 'Todas las páginas',
pdf_limit_reached: pdf_batch_range: 'Mostrando las páginas ${1}–${2} de ${3}.',
'No se pudo generar el PDF para ${1} páginas, la generación se detuvo en ${2}.', pdf_batch_previous: '${1} páginas anteriores',
pdf_limit_reached_continue: 'Extender con ${1} páginas más.', pdf_batch_next: '${1} páginas siguientes',
more: 'Más', more: 'Más',
link_tooltip_external_link: 'Enlace externo a', link_tooltip_external_link: 'Enlace externo a',
link_tooltip_email: 'Enviar un correo electrónico a', link_tooltip_email: 'Enviar un correo electrónico a',
+5 -9
View File
@@ -77,16 +77,11 @@ export const et: TranslationLanguage = {
notfound_title: 'Lehte ei leitud', notfound_title: 'Lehte ei leitud',
notfound_link: 'See link viitab lehele, mis on eemaldatud või mida enam ei eksisteeri.', notfound_link: 'See link viitab lehele, mis on eemaldatud või mida enam ei eksisteeri.',
notfound: 'Lehte, mida otsite, ei eksisteeri.', notfound: 'Lehte, mida otsite, ei eksisteeri.',
notfound_adaptive_title: 'Selle lehe vaatamiseks logi sisse', notfound_adaptive_title: 'Leht pole saadaval',
notfound_adaptive: notfound_adaptive: 'See leht võib olemas olla, kuid juurdepääsuks võib olla vaja sisse logida.',
'See leht võib nõuda sisselogimist. Kui järgisid linki, kontrolli, et see oleks õige.',
notfound_adaptive_login: 'Logi sisse', notfound_adaptive_login: 'Logi sisse',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registreerimine võib olla saadaval olenevalt organisatsioonist.', 'Registreerimine võib olla saadaval olenevalt organisatsioonist.',
notfound_adaptive_no_access_title: 'Sul ei pruugi olla sellele lehele juurdepääsu',
notfound_adaptive_no_access:
'Seda lehte ei pruugi olla olemas või sinu kontol ei pruugi olla sellele juurdepääsu. Kui järgisid linki, kontrolli, et see oleks õige.',
notfound_adaptive_login_again: 'Logi uuesti sisse',
notfound_goto_home: 'Mine avalehele', notfound_goto_home: 'Mine avalehele',
notfound_suggestions_title: 'Võib-olla otsite', notfound_suggestions_title: 'Võib-olla otsite',
unexpected_error_title: 'Ilmnes tõrge', unexpected_error_title: 'Ilmnes tõrge',
@@ -102,8 +97,9 @@ export const et: TranslationLanguage = {
pdf_page_of: '${1}/${2}', pdf_page_of: '${1}/${2}',
pdf_mode_only_page: 'Ainult see leht', pdf_mode_only_page: 'Ainult see leht',
pdf_mode_all: 'Kõik lehed', pdf_mode_all: 'Kõik lehed',
pdf_limit_reached: 'PDF-i ei saanud luua ${1} lehe jaoks, loomine peatus kohas ${2}.', pdf_batch_range: 'Kuvatakse lehti ${1}–${2}/${3}.',
pdf_limit_reached_continue: 'Laienda veel ${1} lehe võrra.', pdf_batch_previous: 'Eelmised ${1} lehte',
pdf_batch_next: 'Järgmised ${1} lehte',
more: 'Rohkem', more: 'Rohkem',
link_tooltip_external_link: 'Väline link aadressile', link_tooltip_external_link: 'Väline link aadressile',
link_tooltip_email: 'Saada e-kiri aadressile', link_tooltip_email: 'Saada e-kiri aadressile',
+5 -8
View File
@@ -78,16 +78,12 @@ export const fi: TranslationLanguage = {
notfound_title: 'Sivua ei löytynyt', notfound_title: 'Sivua ei löytynyt',
notfound_link: 'Tämä linkki osoittaa sivulle, joka on poistettu tai jota ei enää ole.', notfound_link: 'Tämä linkki osoittaa sivulle, joka on poistettu tai jota ei enää ole.',
notfound: 'Etsimääsi sivua ei ole olemassa.', notfound: 'Etsimääsi sivua ei ole olemassa.',
notfound_adaptive_title: 'Kirjaudu sisään nähdäksesi tämän sivun', notfound_adaptive_title: 'Sivu ei ole saatavilla',
notfound_adaptive: notfound_adaptive:
'Tämä sivu saattaa vaatia kirjautumisen. Jos seurasit linkkiä, tarkista, että se on oikein.', 'Tämä sivu voi olla olemassa, mutta sen käyttö voi edellyttää kirjautumista.',
notfound_adaptive_login: 'Kirjaudu sisään', notfound_adaptive_login: 'Kirjaudu sisään',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Rekisteröityminen voi olla mahdollista organisaatiosta riippuen.', 'Rekisteröityminen voi olla mahdollista organisaatiosta riippuen.',
notfound_adaptive_no_access_title: 'Sinulla ei ehkä ole pääsyä tälle sivulle',
notfound_adaptive_no_access:
'Tätä sivua ei ehkä ole olemassa, tai tililläsi ei ehkä ole pääsyä siihen. Jos seurasit linkkiä, tarkista, että se on oikein.',
notfound_adaptive_login_again: 'Kirjaudu uudelleen',
notfound_goto_home: 'Siirry etusivulle', notfound_goto_home: 'Siirry etusivulle',
notfound_suggestions_title: 'Saatat etsiä', notfound_suggestions_title: 'Saatat etsiä',
unexpected_error_title: 'Tapahtui virhe', unexpected_error_title: 'Tapahtui virhe',
@@ -103,8 +99,9 @@ export const fi: TranslationLanguage = {
pdf_page_of: '${1}/${2}', pdf_page_of: '${1}/${2}',
pdf_mode_only_page: 'Vain tämä sivu', pdf_mode_only_page: 'Vain tämä sivu',
pdf_mode_all: 'Kaikki sivut', pdf_mode_all: 'Kaikki sivut',
pdf_limit_reached: 'PDF-tiedostoa ei voitu luoda ${1} sivulle, luonti pysähtyi kohtaan ${2}.', pdf_batch_range: 'Näytetään sivut ${1}–${2}/${3}.',
pdf_limit_reached_continue: 'Laajenna ${1} sivulla.', pdf_batch_previous: 'Edelliset ${1} sivua',
pdf_batch_next: 'Seuraavat ${1} sivua',
more: 'Lisää', more: 'Lisää',
link_tooltip_external_link: 'Ulkoinen linkki kohteeseen', link_tooltip_external_link: 'Ulkoinen linkki kohteeseen',
link_tooltip_email: 'Lähetä sähköpostia osoitteeseen', link_tooltip_email: 'Lähetä sähköpostia osoitteeseen',
+5 -8
View File
@@ -78,15 +78,11 @@ export const fr: TranslationLanguage = {
notfound_title: 'Page introuvable', notfound_title: 'Page introuvable',
notfound_link: "Ce lien pointe vers une page qui a été supprimée ou n'existe plus.", notfound_link: "Ce lien pointe vers une page qui a été supprimée ou n'existe plus.",
notfound: 'La page que vous cherchez n’existe pas.', notfound: 'La page que vous cherchez n’existe pas.',
notfound_adaptive_title: 'Connectez-vous pour afficher cette page', notfound_adaptive_title: 'Page inaccessible',
notfound_adaptive: notfound_adaptive:
"Cette page peut nécessiter une connexion. Si vous avez suivi un lien, vérifiez qu'il est correct.", 'Cette page existe peut-être, mais vous devez peut-être vous connecter pour y accéder.',
notfound_adaptive_login: 'Se connecter', notfound_adaptive_login: 'Se connecter',
notfound_adaptive_registration_hint: "L'inscription peut être disponible selon l'organisation.", notfound_adaptive_registration_hint: "L'inscription peut être disponible selon l'organisation.",
notfound_adaptive_no_access_title: "Vous n'avez peut-être pas accès à cette page",
notfound_adaptive_no_access:
"Cette page n'existe peut-être pas, ou votre compte n'y a peut-être pas accès. Si vous avez suivi un lien, vérifiez qu'il est correct.",
notfound_adaptive_login_again: 'Se reconnecter',
notfound_goto_home: "Aller à l'accueil", notfound_goto_home: "Aller à l'accueil",
notfound_suggestions_title: 'Vous cherchez peut-être', notfound_suggestions_title: 'Vous cherchez peut-être',
unexpected_error_title: 'Erreur inattendue', unexpected_error_title: 'Erreur inattendue',
@@ -102,8 +98,9 @@ export const fr: TranslationLanguage = {
pdf_page_of: '${1} sur ${2}', pdf_page_of: '${1} sur ${2}',
pdf_mode_only_page: 'Cette page uniquement', pdf_mode_only_page: 'Cette page uniquement',
pdf_mode_all: 'Toutes les pages', pdf_mode_all: 'Toutes les pages',
pdf_limit_reached: 'Impossible de générer le PDF pour ${1} pages, arrêté à ${2}.', pdf_batch_range: 'Pages ${1} à ${2} sur ${3}.',
pdf_limit_reached_continue: 'Ajouter ${1} pages supplémentaires', pdf_batch_previous: '${1} pages précédentes',
pdf_batch_next: '${1} pages suivantes',
more: 'Plus', more: 'Plus',
link_tooltip_external_link: 'Lien externe vers', link_tooltip_external_link: 'Lien externe vers',
link_tooltip_email: 'Envoyer un e-mail à', link_tooltip_email: 'Envoyer un e-mail à',
+5 -8
View File
@@ -77,14 +77,10 @@ export const he: TranslationLanguage = {
notfound_title: 'הדף לא נמצא', notfound_title: 'הדף לא נמצא',
notfound_link: 'קישור זה מפנה לדף שהוסר או אינו קיים עוד.', notfound_link: 'קישור זה מפנה לדף שהוסר או אינו קיים עוד.',
notfound: 'הדף שחיפשת אינו קיים.', notfound: 'הדף שחיפשת אינו קיים.',
notfound_adaptive_title: 'יש להתחבר כדי לצפות בדף זה', notfound_adaptive_title: 'הדף אינו זמין',
notfound_adaptive: 'ייתכן שדף זה דורש התחברות. אם הגעת דרך קישור, יש לוודא שהוא נכון.', notfound_adaptive: 'ייתכן שהדף קיים, אך ייתכן שעליך להתחבר כדי לגשת אליו.',
notfound_adaptive_login: 'התחברות', notfound_adaptive_login: 'התחברות',
notfound_adaptive_registration_hint: 'ייתכן שההרשמה זמינה בהתאם לארגון.', notfound_adaptive_registration_hint: 'ייתכן שההרשמה זמינה בהתאם לארגון.',
notfound_adaptive_no_access_title: 'ייתכן שאין לך גישה לדף זה',
notfound_adaptive_no_access:
'ייתכן שדף זה אינו קיים, או שלחשבון שלך אין גישה אליו. אם הגעת דרך קישור, יש לוודא שהוא נכון.',
notfound_adaptive_login_again: 'התחברות מחדש',
notfound_goto_home: 'מעבר לדף הבית', notfound_goto_home: 'מעבר לדף הבית',
notfound_suggestions_title: 'אולי אתה מחפש', notfound_suggestions_title: 'אולי אתה מחפש',
unexpected_error_title: 'אירעה שגיאה', unexpected_error_title: 'אירעה שגיאה',
@@ -100,8 +96,9 @@ export const he: TranslationLanguage = {
pdf_page_of: '${1} מתוך ${2}', pdf_page_of: '${1} מתוך ${2}',
pdf_mode_only_page: 'רק הדף הזה', pdf_mode_only_page: 'רק הדף הזה',
pdf_mode_all: 'כל הדפים', pdf_mode_all: 'כל הדפים',
pdf_limit_reached: 'לא ניתן ליצור PDF עבור ${1} דפים, היצירה נעצרה ב-${2}.', pdf_batch_range: 'מוצגים דפים ${1}–${2} מתוך ${3}.',
pdf_limit_reached_continue: 'הרחבה עם עוד ${1} דפים.', pdf_batch_previous: '${1} הדפים הקודמים',
pdf_batch_next: '${1} הדפים הבאים',
more: 'עוד', more: 'עוד',
link_tooltip_external_link: 'קישור חיצוני אל', link_tooltip_external_link: 'קישור חיצוני אל',
link_tooltip_email: 'שליחת אימייל אל', link_tooltip_email: 'שליחת אימייל אל',
+5 -9
View File
@@ -77,15 +77,10 @@ export const hi: TranslationLanguage = {
notfound_title: 'पृष्ठ नहीं मिला', notfound_title: 'पृष्ठ नहीं मिला',
notfound_link: 'यह लिंक ऐसे पृष्ठ पर जाता है जिसे हटा दिया गया है या जो अब मौजूद नहीं है।', notfound_link: 'यह लिंक ऐसे पृष्ठ पर जाता है जिसे हटा दिया गया है या जो अब मौजूद नहीं है।',
notfound: 'आप जिस पृष्ठ को खोज रहे हैं वह मौजूद नहीं है।', notfound: 'आप जिस पृष्ठ को खोज रहे हैं वह मौजूद नहीं है।',
notfound_adaptive_title: 'यह पृष्ठ देखने के लिए लॉग इन करें', notfound_adaptive_title: 'पृष्ठ उपलब्ध नहीं',
notfound_adaptive: notfound_adaptive: 'यह पृष्ठ मौजूद हो सकता है, लेकिन इसे देखने के लिए आपको लॉग इन करना पड़ सकता है।',
'इस पृष्ठ के लिए लॉग इन करना आवश्यक हो सकता है। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
notfound_adaptive_login: 'लॉग इन करें', notfound_adaptive_login: 'लॉग इन करें',
notfound_adaptive_registration_hint: 'संगठन के आधार पर पंजीकरण उपलब्ध हो सकता है।', notfound_adaptive_registration_hint: 'संगठन के आधार पर पंजीकरण उपलब्ध हो सकता है।',
notfound_adaptive_no_access_title: 'हो सकता है कि आपके पास इस पृष्ठ की पहुंच न हो',
notfound_adaptive_no_access:
'हो सकता है कि यह पृष्ठ मौजूद न हो, या आपके खाते के पास इसकी पहुंच न हो। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
notfound_adaptive_login_again: 'फिर से लॉग इन करें',
notfound_goto_home: 'मुखपृष्ठ पर जाएं', notfound_goto_home: 'मुखपृष्ठ पर जाएं',
notfound_suggestions_title: 'शायद आप यह ढूँढ रहे हैं', notfound_suggestions_title: 'शायद आप यह ढूँढ रहे हैं',
unexpected_error_title: 'त्रुटि हुई', unexpected_error_title: 'त्रुटि हुई',
@@ -101,8 +96,9 @@ export const hi: TranslationLanguage = {
pdf_page_of: '${1} में से ${2}', pdf_page_of: '${1} में से ${2}',
pdf_mode_only_page: 'केवल यह पृष्ठ', pdf_mode_only_page: 'केवल यह पृष्ठ',
pdf_mode_all: 'सभी पृष्ठ', pdf_mode_all: 'सभी पृष्ठ',
pdf_limit_reached: '${1} पृष्ठों के लिए PDF नहीं बन सका, जनरेशन ${2} पर रुक गया।', pdf_batch_range: '${3} में से पृष्ठ ${1}–${2} दिखाए जा रहे हैं।',
pdf_limit_reached_continue: '${1} और पृष्ठों के साथ बढ़ाएं।', pdf_batch_previous: 'पिछले ${1} पृष्ठ',
pdf_batch_next: 'अगले ${1} पृष्ठ',
more: 'अधिक', more: 'अधिक',
link_tooltip_external_link: 'बाहरी लिंक', link_tooltip_external_link: 'बाहरी लिंक',
link_tooltip_email: 'ईमेल भेजें', link_tooltip_email: 'ईमेल भेजें',
+5 -9
View File
@@ -77,15 +77,11 @@ export const hr: TranslationLanguage = {
notfound_title: 'Stranica nije pronađena', notfound_title: 'Stranica nije pronađena',
notfound_link: 'Ova poveznica vodi na stranicu koja je uklonjena ili više ne postoji.', notfound_link: 'Ova poveznica vodi na stranicu koja je uklonjena ili više ne postoji.',
notfound: 'Stranica koju tražite ne postoji.', notfound: 'Stranica koju tražite ne postoji.',
notfound_adaptive_title: 'Prijavite se za prikaz ove stranice', notfound_adaptive_title: 'Stranica nije dostupna',
notfound_adaptive: notfound_adaptive:
'Ova stranica možda zahtijeva prijavu. Ako ste slijedili poveznicu, provjerite je li ispravna.', 'Ova stranica možda postoji, ali možda se morate prijaviti da biste joj pristupili.',
notfound_adaptive_login: 'Prijava', notfound_adaptive_login: 'Prijava',
notfound_adaptive_registration_hint: 'Registracija može biti dostupna ovisno o organizaciji.', notfound_adaptive_registration_hint: 'Registracija može biti dostupna ovisno o organizaciji.',
notfound_adaptive_no_access_title: 'Možda nemate pristup ovoj stranici',
notfound_adaptive_no_access:
'Ova stranica možda ne postoji ili vaš račun nema pristup njoj. Ako ste slijedili poveznicu, provjerite je li ispravna.',
notfound_adaptive_login_again: 'Ponovna prijava',
notfound_goto_home: 'Idi na početnu stranicu', notfound_goto_home: 'Idi na početnu stranicu',
notfound_suggestions_title: 'Možda tražite', notfound_suggestions_title: 'Možda tražite',
unexpected_error_title: 'Došlo je do pogreške', unexpected_error_title: 'Došlo je do pogreške',
@@ -101,9 +97,9 @@ export const hr: TranslationLanguage = {
pdf_page_of: '${1} od ${2}', pdf_page_of: '${1} od ${2}',
pdf_mode_only_page: 'Samo ova stranica', pdf_mode_only_page: 'Samo ova stranica',
pdf_mode_all: 'Sve stranice', pdf_mode_all: 'Sve stranice',
pdf_limit_reached: pdf_batch_range: 'Prikazane stranice ${1}–${2} od ${3}.',
'Nije moguće generirati PDF za ${1} stranica, generiranje je zaustavljeno na ${2}.', pdf_batch_previous: 'Prethodnih ${1} stranica',
pdf_limit_reached_continue: 'Proširi za još ${1} stranica.', pdf_batch_next: 'Sljedećih ${1} stranica',
more: 'Više', more: 'Više',
link_tooltip_external_link: 'Vanjska poveznica na', link_tooltip_external_link: 'Vanjska poveznica na',
link_tooltip_email: 'Pošalji e-poštu na', link_tooltip_email: 'Pošalji e-poštu na',
+5 -10
View File
@@ -78,15 +78,10 @@ export const hu: TranslationLanguage = {
notfound_title: 'Az oldal nem található', notfound_title: 'Az oldal nem található',
notfound_link: 'Ez a hivatkozás egy eltávolított vagy már nem létező oldalra mutat.', notfound_link: 'Ez a hivatkozás egy eltávolított vagy már nem létező oldalra mutat.',
notfound: 'A keresett oldal nem létezik.', notfound: 'A keresett oldal nem létezik.',
notfound_adaptive_title: 'Jelentkezzen be az oldal megtekintéséhez', notfound_adaptive_title: 'Az oldal nem érhető el',
notfound_adaptive: notfound_adaptive: 'Ez az oldal létezhet, de a hozzáféréshez lehet, hogy be kell jelentkeznie.',
'Ehhez az oldalhoz bejelentkezés szükséges lehet. Ha egy hivatkozást követett, ellenőrizze, hogy helyes-e.',
notfound_adaptive_login: 'Bejelentkezés', notfound_adaptive_login: 'Bejelentkezés',
notfound_adaptive_registration_hint: 'A regisztráció a szervezettől függően elérhető lehet.', notfound_adaptive_registration_hint: 'A regisztráció a szervezettől függően elérhető lehet.',
notfound_adaptive_no_access_title: 'Lehet, hogy nincs hozzáférése ehhez az oldalhoz',
notfound_adaptive_no_access:
'Lehet, hogy ez az oldal nem létezik, vagy a fiókja nem fér hozzá. Ha egy hivatkozást követett, ellenőrizze, hogy helyes-e.',
notfound_adaptive_login_again: 'Újbóli bejelentkezés',
notfound_goto_home: 'Ugrás a kezdőlapra', notfound_goto_home: 'Ugrás a kezdőlapra',
notfound_suggestions_title: 'Talán ezt keresi', notfound_suggestions_title: 'Talán ezt keresi',
unexpected_error_title: 'Hiba történt', unexpected_error_title: 'Hiba történt',
@@ -102,9 +97,9 @@ export const hu: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: 'Csak ez az oldal', pdf_mode_only_page: 'Csak ez az oldal',
pdf_mode_all: 'Minden oldal', pdf_mode_all: 'Minden oldal',
pdf_limit_reached: pdf_batch_range: '${1}–${2}. oldal, összesen ${3}.',
'Nem sikerült PDF-et létrehozni ${1} oldalhoz, a létrehozás itt megállt: ${2}.', pdf_batch_previous: 'Előző ${1} oldal',
pdf_limit_reached_continue: 'Bővítés további ${1} oldallal.', pdf_batch_next: 'Következő ${1} oldal',
more: 'Továbbiak', more: 'Továbbiak',
link_tooltip_external_link: 'Külső hivatkozás ide:', link_tooltip_external_link: 'Külső hivatkozás ide:',
link_tooltip_email: 'E-mail küldése ide:', link_tooltip_email: 'E-mail küldése ide:',
+5 -8
View File
@@ -77,15 +77,11 @@ export const id: TranslationLanguage = {
notfound_title: 'Halaman tidak ditemukan', notfound_title: 'Halaman tidak ditemukan',
notfound_link: 'Tautan ini mengarah ke halaman yang telah dihapus atau sudah tidak ada.', notfound_link: 'Tautan ini mengarah ke halaman yang telah dihapus atau sudah tidak ada.',
notfound: 'Halaman yang Anda cari tidak ada.', notfound: 'Halaman yang Anda cari tidak ada.',
notfound_adaptive_title: 'Masuk untuk melihat halaman ini', notfound_adaptive_title: 'Halaman tidak tersedia',
notfound_adaptive: notfound_adaptive:
'Halaman ini mungkin mengharuskan Anda masuk. Jika Anda mengikuti tautan, periksa apakah tautan tersebut benar.', 'Halaman ini mungkin ada, tetapi Anda mungkin perlu masuk untuk mengaksesnya.',
notfound_adaptive_login: 'Masuk', notfound_adaptive_login: 'Masuk',
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia tergantung pada organisasi.', notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia tergantung pada organisasi.',
notfound_adaptive_no_access_title: 'Anda mungkin tidak memiliki akses ke halaman ini',
notfound_adaptive_no_access:
'Halaman ini mungkin tidak ada, atau akun Anda mungkin tidak memiliki akses ke halaman ini. Jika Anda mengikuti tautan, periksa apakah tautan tersebut benar.',
notfound_adaptive_login_again: 'Masuk lagi',
notfound_goto_home: 'Buka beranda', notfound_goto_home: 'Buka beranda',
notfound_suggestions_title: 'Mungkin Anda mencari', notfound_suggestions_title: 'Mungkin Anda mencari',
unexpected_error_title: 'Terjadi kesalahan', unexpected_error_title: 'Terjadi kesalahan',
@@ -101,8 +97,9 @@ export const id: TranslationLanguage = {
pdf_page_of: '${1} dari ${2}', pdf_page_of: '${1} dari ${2}',
pdf_mode_only_page: 'Hanya halaman ini', pdf_mode_only_page: 'Hanya halaman ini',
pdf_mode_all: 'Semua halaman', pdf_mode_all: 'Semua halaman',
pdf_limit_reached: 'Tidak dapat membuat PDF untuk ${1} halaman, pembuatan berhenti di ${2}.', pdf_batch_range: 'Menampilkan halaman ${1}–${2} dari ${3}.',
pdf_limit_reached_continue: 'Perluas dengan ${1} halaman lagi.', pdf_batch_previous: '${1} halaman sebelumnya',
pdf_batch_next: '${1} halaman berikutnya',
more: 'Lainnya', more: 'Lainnya',
link_tooltip_external_link: 'Tautan eksternal ke', link_tooltip_external_link: 'Tautan eksternal ke',
link_tooltip_email: 'Kirim email ke', link_tooltip_email: 'Kirim email ke',
@@ -1,20 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { es } from './es';
import { fr } from './fr';
import { isAvailableLanguage, languages, loadLanguage } from './index';
describe('regional language variants', () => {
it.each([
['fr-ca', fr],
['es-mx', es],
['es-419', es],
] as const)('loads %s with its own metadata and the base strings', async (locale, base) => {
expect(isAvailableLanguage(locale)).toBe(true);
const language = await loadLanguage(locale);
expect(language).toEqual({ ...base, ...languages[locale] });
expect(language.locale).toBe(locale);
});
});
@@ -25,10 +25,6 @@ const languageDefinitions = {
metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' }, metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' },
load: () => import('./fr').then((module) => module.fr), load: () => import('./fr').then((module) => module.fr),
}, },
'fr-ca': {
metadata: { locale: 'fr-ca', language: 'Français (Canada)', flag: '🇨🇦' },
load: () => import('./fr').then((module) => withMetadata(module.fr, 'fr-ca')),
},
de: { de: {
metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' }, metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' },
load: () => import('./de').then((module) => module.de), load: () => import('./de').then((module) => module.de),
@@ -37,14 +33,6 @@ const languageDefinitions = {
metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' }, metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' },
load: () => import('./es').then((module) => module.es), load: () => import('./es').then((module) => module.es),
}, },
'es-mx': {
metadata: { locale: 'es-mx', language: 'Español (México)', flag: '🇲🇽' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-mx')),
},
'es-419': {
metadata: { locale: 'es-419', language: 'Español (Latinoamérica)', flag: '🌎' },
load: () => import('./es').then((module) => withMetadata(module.es, 'es-419')),
},
it: { it: {
metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' }, metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' },
load: () => import('./it').then((module) => module.it), load: () => import('./it').then((module) => module.it),
@@ -189,14 +177,6 @@ const languageDefinitions = {
export type TranslationLocale = keyof typeof languageDefinitions; export type TranslationLocale = keyof typeof languageDefinitions;
// Regional variants reuse their base language's strings but keep their own locale, name and flag.
function withMetadata(
base: TranslationLanguage,
locale: 'fr-ca' | 'es-mx' | 'es-419'
): TranslationLanguage {
return { ...base, ...languageDefinitions[locale].metadata };
}
export const languages = Object.fromEntries( export const languages = Object.fromEntries(
Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata]) Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata])
) as { ) as {
+5 -9
View File
@@ -79,16 +79,12 @@ export const it: TranslationLanguage = {
notfound_title: 'Pagina non trovata', notfound_title: 'Pagina non trovata',
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.', notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
notfound: 'La pagina che cerchi non esiste.', notfound: 'La pagina che cerchi non esiste.',
notfound_adaptive_title: 'Accedi per visualizzare questa pagina', notfound_adaptive_title: 'Pagina non disponibile',
notfound_adaptive: notfound_adaptive:
"Questa pagina potrebbe richiedere l'accesso. Se hai seguito un link, verifica che sia corretto.", "Questa pagina potrebbe esistere, ma potrebbe essere necessario effettuare l'accesso per visualizzarla.",
notfound_adaptive_login: 'Accedi', notfound_adaptive_login: 'Accedi',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
"La registrazione potrebbe essere disponibile a seconda dell'organizzazione.", "La registrazione potrebbe essere disponibile a seconda dell'organizzazione.",
notfound_adaptive_no_access_title: 'Potresti non avere accesso a questa pagina',
notfound_adaptive_no_access:
'Questa pagina potrebbe non esistere, oppure il tuo account potrebbe non avervi accesso. Se hai seguito un link, verifica che sia corretto.',
notfound_adaptive_login_again: 'Accedi di nuovo',
notfound_goto_home: 'Vai alla home', notfound_goto_home: 'Vai alla home',
notfound_suggestions_title: 'Forse stai cercando', notfound_suggestions_title: 'Forse stai cercando',
unexpected_error_title: 'Si è verificato un errore', unexpected_error_title: 'Si è verificato un errore',
@@ -104,9 +100,9 @@ export const it: TranslationLanguage = {
pdf_page_of: '${1} di ${2}', pdf_page_of: '${1} di ${2}',
pdf_mode_only_page: 'Solo questa pagina', pdf_mode_only_page: 'Solo questa pagina',
pdf_mode_all: 'Tutte le pagine', pdf_mode_all: 'Tutte le pagine',
pdf_limit_reached: pdf_batch_range: 'Pagine ${1}–${2} di ${3}.',
'Impossibile generare il PDF per ${1} pagine, la generazione si è fermata a ${2}.', pdf_batch_previous: '${1} pagine precedenti',
pdf_limit_reached_continue: 'Estendi con altre ${1} pagine.', pdf_batch_next: '${1} pagine successive',
more: 'Altro', more: 'Altro',
link_tooltip_external_link: 'Link esterno a', link_tooltip_external_link: 'Link esterno a',
link_tooltip_email: "Invia un'email a", link_tooltip_email: "Invia un'email a",
+5 -8
View File
@@ -78,15 +78,11 @@ export const ja: TranslationLanguage = {
notfound_title: 'ページが見つかりません', notfound_title: 'ページが見つかりません',
notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。', notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。',
notfound: 'お探しのページは存在しません。', notfound: 'お探しのページは存在しません。',
notfound_adaptive_title: 'このページを表示するにはログインしてください', notfound_adaptive_title: 'ページにアクセスできません',
notfound_adaptive: notfound_adaptive:
'このページの閲覧にはログインが必要な場合があります。リンクからアクセスした場合は、リンクが正しいか確認してください。', 'このページは存在する可能性がありますが、アクセスするにはログインが必要な場合があります。',
notfound_adaptive_login: 'ログイン', notfound_adaptive_login: 'ログイン',
notfound_adaptive_registration_hint: '組織によっては登録が可能な場合があります。', notfound_adaptive_registration_hint: '組織によっては登録が可能な場合があります。',
notfound_adaptive_no_access_title: 'このページへのアクセス権がない可能性があります',
notfound_adaptive_no_access:
'このページが存在しないか、お使いのアカウントにアクセス権がない可能性があります。リンクからアクセスした場合は、リンクが正しいか確認してください。',
notfound_adaptive_login_again: '再ログイン',
notfound_goto_home: 'ホームへ移動', notfound_goto_home: 'ホームへ移動',
notfound_suggestions_title: 'お探しのページはこちらですか', notfound_suggestions_title: 'お探しのページはこちらですか',
unexpected_error_title: 'エラーが発生しました', unexpected_error_title: 'エラーが発生しました',
@@ -102,8 +98,9 @@ export const ja: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: 'このページのみ', pdf_mode_only_page: 'このページのみ',
pdf_mode_all: '全てのページ', pdf_mode_all: '全てのページ',
pdf_limit_reached: '${1}ページのPDFを生成できませんでした、${2}で生成が停止しました。', pdf_batch_range: '${3}ページ中 ${1}–${2}ページを表示しています。',
pdf_limit_reached_continue: 'さらに${1}ページで拡張', pdf_batch_previous: '前の${1}ページ',
pdf_batch_next: '次の${1}ページ',
more: 'もっと見る', more: 'もっと見る',
link_tooltip_external_link: '外部リンクへ', link_tooltip_external_link: '外部リンクへ',
link_tooltip_email: 'メールを送信', link_tooltip_email: 'メールを送信',
+5 -9
View File
@@ -78,15 +78,10 @@ export const ko: TranslationLanguage = {
notfound_title: '페이지를 찾을 수 없음', notfound_title: '페이지를 찾을 수 없음',
notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.', notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.',
notfound: '찾으시는 페이지가 존재하지 않습니다.', notfound: '찾으시는 페이지가 존재하지 않습니다.',
notfound_adaptive_title: '이 페이지를 보려면 로그인하세요', notfound_adaptive_title: '페이지에 접근할 수 없음',
notfound_adaptive: notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
'이 페이지는 로그인이 필요할 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
notfound_adaptive_login: '로그인', notfound_adaptive_login: '로그인',
notfound_adaptive_registration_hint: '조직에 따라 등록이 가능할 수 있습니다.', notfound_adaptive_registration_hint: '조직에 따라 등록이 가능할 수 있습니다.',
notfound_adaptive_no_access_title: '이 페이지에 대한 접근 권한이 없을 수 있습니다',
notfound_adaptive_no_access:
'이 페이지가 존재하지 않거나 계정에 이 페이지에 대한 접근 권한이 없을 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
notfound_adaptive_login_again: '다시 로그인',
notfound_goto_home: '홈으로 이동', notfound_goto_home: '홈으로 이동',
notfound_suggestions_title: '찾고 계신 내용일 수 있어요', notfound_suggestions_title: '찾고 계신 내용일 수 있어요',
unexpected_error_title: '오류가 발생했습니다', unexpected_error_title: '오류가 발생했습니다',
@@ -102,8 +97,9 @@ export const ko: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: '이 페이지만', pdf_mode_only_page: '이 페이지만',
pdf_mode_all: '모든 페이지', pdf_mode_all: '모든 페이지',
pdf_limit_reached: '${1}페이지에 대한 PDF를 생성할 수 없어 ${2}에서 생성이 중지되었습니다.', pdf_batch_range: '전체 ${3}페이지 중 ${1}–${2}페이지를 표시하고 있습니다.',
pdf_limit_reached_continue: '${1}페이지 더 확장하기.', pdf_batch_previous: '이전 ${1}페이지',
pdf_batch_next: '다음 ${1}페이지',
more: '더 보기', more: '더 보기',
link_tooltip_external_link: '외부 링크', link_tooltip_external_link: '외부 링크',
link_tooltip_email: '이메일 보내기', link_tooltip_email: '이메일 보내기',
+5 -8
View File
@@ -77,16 +77,12 @@ export const lt: TranslationLanguage = {
notfound_title: 'Puslapis nerastas', notfound_title: 'Puslapis nerastas',
notfound_link: 'Ši nuoroda veda į puslapį, kuris buvo pašalintas arba nebeegzistuoja.', notfound_link: 'Ši nuoroda veda į puslapį, kuris buvo pašalintas arba nebeegzistuoja.',
notfound: 'Puslapis, kurio ieškote, neegzistuoja.', notfound: 'Puslapis, kurio ieškote, neegzistuoja.',
notfound_adaptive_title: 'Prisijunkite, kad peržiūrėtumėte šį puslapį', notfound_adaptive_title: 'Puslapis nepasiekiamas',
notfound_adaptive: notfound_adaptive:
'Šiam puslapiui gali reikėti prisijungti. Jei sekėte nuoroda, patikrinkite, ar ji teisinga.', 'Šis puslapis gali egzistuoti, bet norint jį pasiekti gali reikėti prisijungti.',
notfound_adaptive_login: 'Prisijungti', notfound_adaptive_login: 'Prisijungti',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registracija gali būti galima priklausomai nuo organizacijos.', 'Registracija gali būti galima priklausomai nuo organizacijos.',
notfound_adaptive_no_access_title: 'Galbūt neturite prieigos prie šio puslapio',
notfound_adaptive_no_access:
'Šis puslapis gali neegzistuoti arba jūsų paskyra gali neturėti prieigos prie jo. Jei sekėte nuoroda, patikrinkite, ar ji teisinga.',
notfound_adaptive_login_again: 'Prisijungti iš naujo',
notfound_goto_home: 'Eiti į pagrindinį puslapį', notfound_goto_home: 'Eiti į pagrindinį puslapį',
notfound_suggestions_title: 'Galbūt ieškote', notfound_suggestions_title: 'Galbūt ieškote',
unexpected_error_title: 'Įvyko klaida', unexpected_error_title: 'Įvyko klaida',
@@ -102,8 +98,9 @@ export const lt: TranslationLanguage = {
pdf_page_of: '${1} iš ${2}', pdf_page_of: '${1} iš ${2}',
pdf_mode_only_page: 'Tik šis puslapis', pdf_mode_only_page: 'Tik šis puslapis',
pdf_mode_all: 'Visi puslapiai', pdf_mode_all: 'Visi puslapiai',
pdf_limit_reached: 'Nepavyko sugeneruoti PDF ${1} puslapiams, generavimas sustojo ties ${2}.', pdf_batch_range: 'Rodomi puslapiai ${1}–${2} iš ${3}.',
pdf_limit_reached_continue: 'Išplėsti dar ${1} puslapiais.', pdf_batch_previous: 'Ankstesni ${1} puslapiai',
pdf_batch_next: 'Kiti ${1} puslapiai',
more: 'Daugiau', more: 'Daugiau',
link_tooltip_external_link: 'Išorinė nuoroda į', link_tooltip_external_link: 'Išorinė nuoroda į',
link_tooltip_email: 'Siųsti el. laišką į', link_tooltip_email: 'Siųsti el. laišką į',
+5 -9
View File
@@ -77,15 +77,10 @@ export const lv: TranslationLanguage = {
notfound_title: 'Lapa nav atrasta', notfound_title: 'Lapa nav atrasta',
notfound_link: 'Šī saite norāda uz lapu, kas ir noņemta vai vairs nepastāv.', notfound_link: 'Šī saite norāda uz lapu, kas ir noņemta vai vairs nepastāv.',
notfound: 'Meklētā lapa nepastāv.', notfound: 'Meklētā lapa nepastāv.',
notfound_adaptive_title: 'Pierakstieties, lai skatītu šo lapu', notfound_adaptive_title: 'Lapa nav pieejama',
notfound_adaptive: notfound_adaptive: 'Šī lapa var pastāvēt, bet, iespējams, jums jāpierakstās, lai tai piekļūtu.',
'Šai lapai, iespējams, nepieciešams pierakstīties. Ja sekojāt saitei, pārbaudiet, vai tā ir pareiza.',
notfound_adaptive_login: 'Pierakstīties', notfound_adaptive_login: 'Pierakstīties',
notfound_adaptive_registration_hint: 'Reģistrācija var būt pieejama atkarībā no organizācijas.', notfound_adaptive_registration_hint: 'Reģistrācija var būt pieejama atkarībā no organizācijas.',
notfound_adaptive_no_access_title: 'Iespējams, jums nav piekļuves šai lapai',
notfound_adaptive_no_access:
'Šī lapa, iespējams, neeksistē, vai arī jūsu kontam nav piekļuves tai. Ja sekojāt saitei, pārbaudiet, vai tā ir pareiza.',
notfound_adaptive_login_again: 'Pierakstīties vēlreiz',
notfound_goto_home: 'Doties uz sākumlapu', notfound_goto_home: 'Doties uz sākumlapu',
notfound_suggestions_title: 'Iespējams, meklējat', notfound_suggestions_title: 'Iespējams, meklējat',
unexpected_error_title: 'Radās kļūda', unexpected_error_title: 'Radās kļūda',
@@ -101,8 +96,9 @@ export const lv: TranslationLanguage = {
pdf_page_of: '${1} no ${2}', pdf_page_of: '${1} no ${2}',
pdf_mode_only_page: 'Tikai šī lapa', pdf_mode_only_page: 'Tikai šī lapa',
pdf_mode_all: 'Visas lapas', pdf_mode_all: 'Visas lapas',
pdf_limit_reached: 'Neizdevās ģenerēt PDF ${1} lapām, ģenerēšana apstājās pie ${2}.', pdf_batch_range: 'Rāda lapas ${1}–${2} no ${3}.',
pdf_limit_reached_continue: 'Paplašināt ar vēl ${1} lapām.', pdf_batch_previous: 'Iepriekšējās ${1} lapas',
pdf_batch_next: 'Nākamās ${1} lapas',
more: 'Vairāk', more: 'Vairāk',
link_tooltip_external_link: 'Ārēja saite uz', link_tooltip_external_link: 'Ārēja saite uz',
link_tooltip_email: 'Sūtīt e-pastu uz', link_tooltip_email: 'Sūtīt e-pastu uz',
+5 -8
View File
@@ -77,15 +77,11 @@ export const ms: TranslationLanguage = {
notfound_title: 'Halaman tidak ditemui', notfound_title: 'Halaman tidak ditemui',
notfound_link: 'Pautan ini menghala ke halaman yang telah dialih keluar atau tidak lagi wujud.', notfound_link: 'Pautan ini menghala ke halaman yang telah dialih keluar atau tidak lagi wujud.',
notfound: 'Halaman yang anda cari tidak wujud.', notfound: 'Halaman yang anda cari tidak wujud.',
notfound_adaptive_title: 'Log masuk untuk melihat halaman ini', notfound_adaptive_title: 'Halaman tidak tersedia',
notfound_adaptive: notfound_adaptive:
'Halaman ini mungkin memerlukan anda log masuk. Jika anda mengikuti pautan, semak sama ada pautan itu betul.', 'Halaman ini mungkin wujud, tetapi anda mungkin perlu log masuk untuk mengaksesnya.',
notfound_adaptive_login: 'Log masuk', notfound_adaptive_login: 'Log masuk',
notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia bergantung pada organisasi.', notfound_adaptive_registration_hint: 'Pendaftaran mungkin tersedia bergantung pada organisasi.',
notfound_adaptive_no_access_title: 'Anda mungkin tidak mempunyai akses ke halaman ini',
notfound_adaptive_no_access:
'Halaman ini mungkin tidak wujud, atau akaun anda mungkin tidak mempunyai akses kepadanya. Jika anda mengikuti pautan, semak sama ada pautan itu betul.',
notfound_adaptive_login_again: 'Log masuk semula',
notfound_goto_home: 'Pergi ke halaman utama', notfound_goto_home: 'Pergi ke halaman utama',
notfound_suggestions_title: 'Mungkin anda mencari', notfound_suggestions_title: 'Mungkin anda mencari',
unexpected_error_title: 'Ralat berlaku', unexpected_error_title: 'Ralat berlaku',
@@ -101,8 +97,9 @@ export const ms: TranslationLanguage = {
pdf_page_of: '${1} daripada ${2}', pdf_page_of: '${1} daripada ${2}',
pdf_mode_only_page: 'Halaman ini sahaja', pdf_mode_only_page: 'Halaman ini sahaja',
pdf_mode_all: 'Semua halaman', pdf_mode_all: 'Semua halaman',
pdf_limit_reached: 'Tidak dapat menjana PDF untuk ${1} halaman, penjanaan berhenti pada ${2}.', pdf_batch_range: 'Memaparkan halaman ${1}–${2} daripada ${3}.',
pdf_limit_reached_continue: 'Lanjutkan dengan ${1} halaman lagi.', pdf_batch_previous: '${1} halaman sebelumnya',
pdf_batch_next: '${1} halaman seterusnya',
more: 'Lagi', more: 'Lagi',
link_tooltip_external_link: 'Pautan luaran ke', link_tooltip_external_link: 'Pautan luaran ke',
link_tooltip_email: 'Hantar e-mel kepada', link_tooltip_email: 'Hantar e-mel kepada',
+5 -8
View File
@@ -79,16 +79,12 @@ export const nl: TranslationLanguage = {
notfound_title: 'Pagina niet gevonden', notfound_title: 'Pagina niet gevonden',
notfound_link: 'Deze link verwijst naar een pagina die is verwijderd of niet meer bestaat.', notfound_link: 'Deze link verwijst naar een pagina die is verwijderd of niet meer bestaat.',
notfound: 'De pagina die je zoekt, bestaat niet.', notfound: 'De pagina die je zoekt, bestaat niet.',
notfound_adaptive_title: 'Log in om deze pagina te bekijken', notfound_adaptive_title: 'Pagina niet beschikbaar',
notfound_adaptive: notfound_adaptive:
'Voor deze pagina moet je mogelijk inloggen. Als je een link hebt gevolgd, controleer dan of deze klopt.', 'Deze pagina bestaat mogelijk, maar je moet mogelijk inloggen om toegang te krijgen.',
notfound_adaptive_login: 'Inloggen', notfound_adaptive_login: 'Inloggen',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registratie is mogelijk beschikbaar, afhankelijk van de organisatie.', 'Registratie is mogelijk beschikbaar, afhankelijk van de organisatie.',
notfound_adaptive_no_access_title: 'Je hebt mogelijk geen toegang tot deze pagina',
notfound_adaptive_no_access:
'Deze pagina bestaat mogelijk niet, of je account heeft er geen toegang toe. Als je een link hebt gevolgd, controleer dan of deze klopt.',
notfound_adaptive_login_again: 'Opnieuw inloggen',
notfound_goto_home: 'Naar startpagina', notfound_goto_home: 'Naar startpagina',
notfound_suggestions_title: 'Misschien zoek je', notfound_suggestions_title: 'Misschien zoek je',
unexpected_error_title: 'Er is een fout opgetreden', unexpected_error_title: 'Er is een fout opgetreden',
@@ -104,8 +100,9 @@ export const nl: TranslationLanguage = {
pdf_page_of: '${1} van ${2}', pdf_page_of: '${1} van ${2}',
pdf_mode_only_page: 'Alleen deze pagina', pdf_mode_only_page: 'Alleen deze pagina',
pdf_mode_all: "Alle pagina's", pdf_mode_all: "Alle pagina's",
pdf_limit_reached: "Kon de PDF niet genereren voor ${1} pagina's, generatie gestopt bij ${2}.", pdf_batch_range: "Pagina's ${1}–${2} van ${3}.",
pdf_limit_reached_continue: "Verleng met ${1} extra pagina's.", pdf_batch_previous: "Vorige ${1} pagina's",
pdf_batch_next: "Volgende ${1} pagina's",
more: 'Meer', more: 'Meer',
link_tooltip_external_link: 'Externe link naar', link_tooltip_external_link: 'Externe link naar',
link_tooltip_email: 'E-mail versturen naar', link_tooltip_email: 'E-mail versturen naar',
+5 -9
View File
@@ -79,16 +79,11 @@ export const no: TranslationLanguage = {
notfound_link: notfound_link:
'Denne lenken peker til en side som har blitt fjernet eller ikke lenger eksisterer.', 'Denne lenken peker til en side som har blitt fjernet eller ikke lenger eksisterer.',
notfound: 'Siden du leter etter eksisterer ikke.', notfound: 'Siden du leter etter eksisterer ikke.',
notfound_adaptive_title: 'Logg inn for å se denne siden', notfound_adaptive_title: 'Siden er ikke tilgjengelig',
notfound_adaptive: notfound_adaptive: 'Denne siden kan finnes, men du må kanskje logge inn for å få tilgang.',
'Denne siden kan kreve at du logger inn. Hvis du fulgte en lenke, sjekk at den er riktig.',
notfound_adaptive_login: 'Logg inn', notfound_adaptive_login: 'Logg inn',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registrering kan være tilgjengelig avhengig av organisasjonen.', 'Registrering kan være tilgjengelig avhengig av organisasjonen.',
notfound_adaptive_no_access_title: 'Du har kanskje ikke tilgang til denne siden',
notfound_adaptive_no_access:
'Denne siden finnes kanskje ikke, eller kontoen din har kanskje ikke tilgang til den. Hvis du fulgte en lenke, sjekk at den er riktig.',
notfound_adaptive_login_again: 'Logg inn på nytt',
notfound_goto_home: 'Gå til forsiden', notfound_goto_home: 'Gå til forsiden',
notfound_suggestions_title: 'Kanskje du leter etter', notfound_suggestions_title: 'Kanskje du leter etter',
unexpected_error_title: 'En feil oppstod', unexpected_error_title: 'En feil oppstod',
@@ -104,8 +99,9 @@ export const no: TranslationLanguage = {
pdf_page_of: '${1} av ${2}', pdf_page_of: '${1} av ${2}',
pdf_mode_only_page: 'Kun denne siden', pdf_mode_only_page: 'Kun denne siden',
pdf_mode_all: 'Alle sider', pdf_mode_all: 'Alle sider',
pdf_limit_reached: 'Kunne ikke generere PDF for ${1} sider, generering stoppet ved ${2}.', pdf_batch_range: 'Viser sider ${1}–${2} av ${3}.',
pdf_limit_reached_continue: 'Utvid med ${1} flere sider.', pdf_batch_previous: 'Forrige ${1} sider',
pdf_batch_next: 'Neste ${1} sider',
more: 'Mer', more: 'Mer',
link_tooltip_external_link: 'Ekstern lenke til', link_tooltip_external_link: 'Ekstern lenke til',
link_tooltip_email: 'Send e-post til', link_tooltip_email: 'Send e-post til',
+5 -10
View File
@@ -77,16 +77,11 @@ export const pl: TranslationLanguage = {
notfound_title: 'Nie znaleziono strony', notfound_title: 'Nie znaleziono strony',
notfound_link: 'Ten link prowadzi do strony, która została usunięta lub już nie istnieje.', notfound_link: 'Ten link prowadzi do strony, która została usunięta lub już nie istnieje.',
notfound: 'Strona, której szukasz, nie istnieje.', notfound: 'Strona, której szukasz, nie istnieje.',
notfound_adaptive_title: 'Zaloguj się, aby wyświetlić tę stronę', notfound_adaptive_title: 'Strona niedostępna',
notfound_adaptive: notfound_adaptive: 'Ta strona może istnieć, ale dostęp do niej może wymagać zalogowania.',
'Ta strona może wymagać zalogowania. Jeśli przechodzisz z linku, sprawdź, czy jest poprawny.',
notfound_adaptive_login: 'Zaloguj się', notfound_adaptive_login: 'Zaloguj się',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Rejestracja może być dostępna w zależności od organizacji.', 'Rejestracja może być dostępna w zależności od organizacji.',
notfound_adaptive_no_access_title: 'Możesz nie mieć dostępu do tej strony',
notfound_adaptive_no_access:
'Ta strona może nie istnieć lub Twoje konto może nie mieć do niej dostępu. Jeśli przechodzisz z linku, sprawdź, czy jest poprawny.',
notfound_adaptive_login_again: 'Zaloguj się ponownie',
notfound_goto_home: 'Przejdź do strony głównej', notfound_goto_home: 'Przejdź do strony głównej',
notfound_suggestions_title: 'Być może szukasz', notfound_suggestions_title: 'Być może szukasz',
unexpected_error_title: 'Wystąpił błąd', unexpected_error_title: 'Wystąpił błąd',
@@ -102,9 +97,9 @@ export const pl: TranslationLanguage = {
pdf_page_of: '${1} z ${2}', pdf_page_of: '${1} z ${2}',
pdf_mode_only_page: 'Tylko ta strona', pdf_mode_only_page: 'Tylko ta strona',
pdf_mode_all: 'Wszystkie strony', pdf_mode_all: 'Wszystkie strony',
pdf_limit_reached: pdf_batch_range: 'Strony ${1}–${2} z ${3}.',
'Nie udało się wygenerować PDF dla ${1} stron, generowanie zatrzymało się na ${2}.', pdf_batch_previous: 'Poprzednie ${1} stron',
pdf_limit_reached_continue: 'Rozszerz o kolejne ${1} stron.', pdf_batch_next: 'Następne ${1} stron',
more: 'Więcej', more: 'Więcej',
link_tooltip_external_link: 'Link zewnętrzny do', link_tooltip_external_link: 'Link zewnętrzny do',
link_tooltip_email: 'Wyślij e-mail do', link_tooltip_email: 'Wyślij e-mail do',
@@ -79,16 +79,12 @@ export const pt_br: TranslationLanguage = {
notfound_title: 'Página não encontrada', notfound_title: 'Página não encontrada',
notfound_link: 'Este link aponta para uma página que foi removida ou não existe mais.', notfound_link: 'Este link aponta para uma página que foi removida ou não existe mais.',
notfound: 'A página que você está procurando não existe.', notfound: 'A página que você está procurando não existe.',
notfound_adaptive_title: 'Faça login para ver esta página', notfound_adaptive_title: 'Página indisponível',
notfound_adaptive: notfound_adaptive:
'Esta página pode exigir login. Se você seguiu um link, verifique se ele está correto.', 'Esta página pode existir, mas talvez você precise fazer login para acessá-la.',
notfound_adaptive_login: 'Fazer login', notfound_adaptive_login: 'Fazer login',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'O registro pode estar disponível dependendo da organização.', 'O registro pode estar disponível dependendo da organização.',
notfound_adaptive_no_access_title: 'Talvez você não tenha acesso a esta página',
notfound_adaptive_no_access:
'Esta página pode não existir ou sua conta pode não ter acesso a ela. Se você seguiu um link, verifique se ele está correto.',
notfound_adaptive_login_again: 'Fazer login novamente',
notfound_goto_home: 'Ir para a página inicial', notfound_goto_home: 'Ir para a página inicial',
notfound_suggestions_title: 'Você pode estar procurando por', notfound_suggestions_title: 'Você pode estar procurando por',
unexpected_error_title: 'Aconteceu um erro', unexpected_error_title: 'Aconteceu um erro',
@@ -105,9 +101,9 @@ export const pt_br: TranslationLanguage = {
pdf_page_of: '${1} de ${2}', pdf_page_of: '${1} de ${2}',
pdf_mode_only_page: 'Somente esta página', pdf_mode_only_page: 'Somente esta página',
pdf_mode_all: 'Todas as páginas', pdf_mode_all: 'Todas as páginas',
pdf_limit_reached: pdf_batch_range: 'Mostrando as páginas ${1}–${2} de ${3}.',
'Não foi possível gerar o PDF para ${1} páginas, a geração foi interrompida em ${2}.', pdf_batch_previous: '${1} páginas anteriores',
pdf_limit_reached_continue: 'Estender com mais ${1} páginas.', pdf_batch_next: 'Próximas ${1} páginas',
more: 'Mais', more: 'Mais',
link_tooltip_external_link: 'Link externo para', link_tooltip_external_link: 'Link externo para',
link_tooltip_email: 'Enviar e-mail para', link_tooltip_email: 'Enviar e-mail para',
+5 -8
View File
@@ -78,16 +78,12 @@ export const pt: TranslationLanguage = {
notfound_title: 'Página não encontrada', notfound_title: 'Página não encontrada',
notfound_link: 'Esta ligação aponta para uma página que foi removida ou já não existe.', notfound_link: 'Esta ligação aponta para uma página que foi removida ou já não existe.',
notfound: 'A página que procura não existe.', notfound: 'A página que procura não existe.',
notfound_adaptive_title: 'Inicie sessão para ver esta página', notfound_adaptive_title: 'Página indisponível',
notfound_adaptive: notfound_adaptive:
'Esta página pode exigir que inicie sessão. Se seguiu uma ligação, verifique se está correta.', 'Esta página pode existir, mas talvez tenha de iniciar sessão para lhe aceder.',
notfound_adaptive_login: 'Iniciar sessão', notfound_adaptive_login: 'Iniciar sessão',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'O registo pode estar disponível dependendo da organização.', 'O registo pode estar disponível dependendo da organização.',
notfound_adaptive_no_access_title: 'Pode não ter acesso a esta página',
notfound_adaptive_no_access:
'Esta página pode não existir ou a sua conta pode não ter acesso à mesma. Se seguiu uma ligação, verifique se está correta.',
notfound_adaptive_login_again: 'Iniciar sessão novamente',
notfound_goto_home: 'Ir para a página inicial', notfound_goto_home: 'Ir para a página inicial',
notfound_suggestions_title: 'Talvez esteja à procura de', notfound_suggestions_title: 'Talvez esteja à procura de',
unexpected_error_title: 'Ocorreu um erro', unexpected_error_title: 'Ocorreu um erro',
@@ -103,8 +99,9 @@ export const pt: TranslationLanguage = {
pdf_page_of: '${1} de ${2}', pdf_page_of: '${1} de ${2}',
pdf_mode_only_page: 'Apenas esta página', pdf_mode_only_page: 'Apenas esta página',
pdf_mode_all: 'Todas as páginas', pdf_mode_all: 'Todas as páginas',
pdf_limit_reached: 'Não foi possível gerar o PDF para ${1} páginas; a geração parou em ${2}.', pdf_batch_range: 'A mostrar as páginas ${1}–${2} de ${3}.',
pdf_limit_reached_continue: 'Expandir com mais ${1} páginas.', pdf_batch_previous: '${1} páginas anteriores',
pdf_batch_next: '${1} páginas seguintes',
more: 'Mais', more: 'Mais',
link_tooltip_external_link: 'Ligação externa para', link_tooltip_external_link: 'Ligação externa para',
link_tooltip_email: 'Enviar e-mail para', link_tooltip_email: 'Enviar e-mail para',
+5 -9
View File
@@ -78,16 +78,12 @@ export const ro: TranslationLanguage = {
notfound_title: 'Pagina nu a fost găsită', notfound_title: 'Pagina nu a fost găsită',
notfound_link: 'Acest link indică o pagină care a fost eliminată sau nu mai există.', notfound_link: 'Acest link indică o pagină care a fost eliminată sau nu mai există.',
notfound: 'Pagina pe care o cauți nu există.', notfound: 'Pagina pe care o cauți nu există.',
notfound_adaptive_title: 'Autentifică-te pentru a vedea această pagină', notfound_adaptive_title: 'Pagina nu este disponibilă',
notfound_adaptive: notfound_adaptive:
'Această pagină poate necesita autentificare. Dacă ai urmat un link, verifică dacă este corect.', 'Această pagină poate exista, dar este posibil să trebuiască să te autentifici pentru a o accesa.',
notfound_adaptive_login: 'Autentificare', notfound_adaptive_login: 'Autentificare',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Înregistrarea poate fi disponibilă în funcție de organizație.', 'Înregistrarea poate fi disponibilă în funcție de organizație.',
notfound_adaptive_no_access_title: 'Este posibil să nu ai acces la această pagină',
notfound_adaptive_no_access:
'Este posibil ca această pagină să nu existe sau contul tău să nu aibă acces la ea. Dacă ai urmat un link, verifică dacă este corect.',
notfound_adaptive_login_again: 'Reautentificare',
notfound_goto_home: 'Mergi la pagina principală', notfound_goto_home: 'Mergi la pagina principală',
notfound_suggestions_title: 'Poate cauți', notfound_suggestions_title: 'Poate cauți',
unexpected_error_title: 'A apărut o eroare', unexpected_error_title: 'A apărut o eroare',
@@ -104,9 +100,9 @@ export const ro: TranslationLanguage = {
pdf_page_of: '${1} din ${2}', pdf_page_of: '${1} din ${2}',
pdf_mode_only_page: 'Doar această pagină', pdf_mode_only_page: 'Doar această pagină',
pdf_mode_all: 'Toate paginile', pdf_mode_all: 'Toate paginile',
pdf_limit_reached: pdf_batch_range: 'Se afișează paginile ${1}–${2} din ${3}.',
'Nu s-a putut genera PDF-ul pentru ${1} pagini, generarea s-a oprit la ${2}.', pdf_batch_previous: '${1} pagini anterioare',
pdf_limit_reached_continue: 'Extinde cu încă ${1} pagini.', pdf_batch_next: 'Următoarele ${1} pagini',
more: 'Mai multe', more: 'Mai multe',
link_tooltip_external_link: 'Link extern către', link_tooltip_external_link: 'Link extern către',
link_tooltip_email: 'Trimite e-mail către', link_tooltip_email: 'Trimite e-mail către',
+5 -9
View File
@@ -79,16 +79,12 @@ export const ru: TranslationLanguage = {
notfound_title: 'Страница не найдена', notfound_title: 'Страница не найдена',
notfound_link: 'Эта ссылка ведёт на страницу, которая была удалена или больше не существует.', notfound_link: 'Эта ссылка ведёт на страницу, которая была удалена или больше не существует.',
notfound: 'Страница, которую вы ищете, не существует.', notfound: 'Страница, которую вы ищете, не существует.',
notfound_adaptive_title: 'Войдите, чтобы просмотреть эту страницу', notfound_adaptive_title: 'Страница недоступна',
notfound_adaptive: notfound_adaptive:
'Для просмотра этой страницы может потребоваться вход. Если вы перешли по ссылке, проверьте, что она верна.', 'Эта страница может существовать, но для доступа к ней может потребоваться вход.',
notfound_adaptive_login: 'Войти', notfound_adaptive_login: 'Войти',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Регистрация может быть доступна в зависимости от организации.', 'Регистрация может быть доступна в зависимости от организации.',
notfound_adaptive_no_access_title: 'Возможно, у вас нет доступа к этой странице',
notfound_adaptive_no_access:
'Возможно, эта страница не существует или у вашей учётной записи нет к ней доступа. Если вы перешли по ссылке, проверьте, что она верна.',
notfound_adaptive_login_again: 'Войти снова',
notfound_goto_home: 'Перейти на главную', notfound_goto_home: 'Перейти на главную',
notfound_suggestions_title: 'Возможно, вы ищете', notfound_suggestions_title: 'Возможно, вы ищете',
unexpected_error_title: 'Произошла ошибка', unexpected_error_title: 'Произошла ошибка',
@@ -104,9 +100,9 @@ export const ru: TranslationLanguage = {
pdf_page_of: '${1} из ${2}', pdf_page_of: '${1} из ${2}',
pdf_mode_only_page: 'Только эта страница', pdf_mode_only_page: 'Только эта страница',
pdf_mode_all: 'Все страницы', pdf_mode_all: 'Все страницы',
pdf_limit_reached: pdf_batch_range: 'Страницы ${1}–${2} из ${3}.',
'Не удалось создать PDF-файл для ${1} страниц, генерация остановлена на ${2}.', pdf_batch_previous: 'Предыдущие ${1} страниц',
pdf_limit_reached_continue: 'Расширьте ещё на ${1} страниц.', pdf_batch_next: 'Следующие ${1} страниц',
more: 'Ещё', more: 'Ещё',
link_tooltip_external_link: 'Внешняя ссылка на', link_tooltip_external_link: 'Внешняя ссылка на',
link_tooltip_email: 'Отправить письмо на', link_tooltip_email: 'Отправить письмо на',
+5 -9
View File
@@ -78,16 +78,12 @@ export const sk: TranslationLanguage = {
notfound_title: 'Stránka sa nenašla', notfound_title: 'Stránka sa nenašla',
notfound_link: 'Tento odkaz smeruje na stránku, ktorá bola odstránená alebo už neexistuje.', notfound_link: 'Tento odkaz smeruje na stránku, ktorá bola odstránená alebo už neexistuje.',
notfound: 'Stránka, ktorú hľadáte, neexistuje.', notfound: 'Stránka, ktorú hľadáte, neexistuje.',
notfound_adaptive_title: 'Na zobrazenie tejto stránky sa prihláste', notfound_adaptive_title: 'Stránka nie je dostupná',
notfound_adaptive: notfound_adaptive:
'Táto stránka môže vyžadovať prihlásenie. Ak ste prešli z odkazu, skontrolujte, či je správny.', 'Táto stránka môže existovať, ale na prístup sa možno budete musieť prihlásiť.',
notfound_adaptive_login: 'Prihlásiť sa', notfound_adaptive_login: 'Prihlásiť sa',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registrácia môže byť dostupná v závislosti od organizácie.', 'Registrácia môže byť dostupná v závislosti od organizácie.',
notfound_adaptive_no_access_title: 'K tejto stránke možno nemáte prístup',
notfound_adaptive_no_access:
'Táto stránka možno neexistuje alebo k nej váš účet nemá prístup. Ak ste prešli z odkazu, skontrolujte, či je správny.',
notfound_adaptive_login_again: 'Prihlásiť sa znova',
notfound_goto_home: 'Prejsť na domovskú stránku', notfound_goto_home: 'Prejsť na domovskú stránku',
notfound_suggestions_title: 'Možno hľadáte', notfound_suggestions_title: 'Možno hľadáte',
unexpected_error_title: 'Vyskytla sa chyba', unexpected_error_title: 'Vyskytla sa chyba',
@@ -103,9 +99,9 @@ export const sk: TranslationLanguage = {
pdf_page_of: '${1} z ${2}', pdf_page_of: '${1} z ${2}',
pdf_mode_only_page: 'Iba táto stránka', pdf_mode_only_page: 'Iba táto stránka',
pdf_mode_all: 'Všetky stránky', pdf_mode_all: 'Všetky stránky',
pdf_limit_reached: pdf_batch_range: 'Zobrazené stránky ${1}–${2} z ${3}.',
'Nepodarilo sa vygenerovať PDF pre ${1} stránok, generovanie sa zastavilo na ${2}.', pdf_batch_previous: 'Predchádzajúcich ${1} stránok',
pdf_limit_reached_continue: 'Rozšíriť o ďalších ${1} stránok.', pdf_batch_next: 'Ďalších ${1} stránok',
more: 'Viac', more: 'Viac',
link_tooltip_external_link: 'Externý odkaz na', link_tooltip_external_link: 'Externý odkaz na',
link_tooltip_email: 'Odoslať e-mail na', link_tooltip_email: 'Odoslať e-mail na',
+5 -10
View File
@@ -78,15 +78,10 @@ export const sl: TranslationLanguage = {
notfound_title: 'Strani ni bilo mogoče najti', notfound_title: 'Strani ni bilo mogoče najti',
notfound_link: 'Ta povezava kaže na stran, ki je bila odstranjena ali ne obstaja več.', notfound_link: 'Ta povezava kaže na stran, ki je bila odstranjena ali ne obstaja več.',
notfound: 'Stran, ki jo iščete, ne obstaja.', notfound: 'Stran, ki jo iščete, ne obstaja.',
notfound_adaptive_title: 'Za ogled te strani se prijavite', notfound_adaptive_title: 'Stran ni na voljo',
notfound_adaptive: notfound_adaptive: 'Ta stran morda obstaja, vendar se boste za dostop morda morali prijaviti.',
'Ta stran morda zahteva prijavo. Če ste sledili povezavi, preverite, ali je pravilna.',
notfound_adaptive_login: 'Prijava', notfound_adaptive_login: 'Prijava',
notfound_adaptive_registration_hint: 'Registracija je morda na voljo, odvisno od organizacije.', notfound_adaptive_registration_hint: 'Registracija je morda na voljo, odvisno od organizacije.',
notfound_adaptive_no_access_title: 'Morda nimate dostopa do te strani',
notfound_adaptive_no_access:
'Ta stran morda ne obstaja ali pa vaš račun nima dostopa do nje. Če ste sledili povezavi, preverite, ali je pravilna.',
notfound_adaptive_login_again: 'Ponovna prijava',
notfound_goto_home: 'Pojdi na domačo stran', notfound_goto_home: 'Pojdi na domačo stran',
notfound_suggestions_title: 'Morda iščete', notfound_suggestions_title: 'Morda iščete',
unexpected_error_title: 'Prišlo je do napake', unexpected_error_title: 'Prišlo je do napake',
@@ -102,9 +97,9 @@ export const sl: TranslationLanguage = {
pdf_page_of: '${1} od ${2}', pdf_page_of: '${1} od ${2}',
pdf_mode_only_page: 'Samo ta stran', pdf_mode_only_page: 'Samo ta stran',
pdf_mode_all: 'Vse strani', pdf_mode_all: 'Vse strani',
pdf_limit_reached: pdf_batch_range: 'Prikazane strani ${1}–${2} od ${3}.',
'PDF-ja ni bilo mogoče ustvariti za ${1} strani, ustvarjanje se je ustavilo pri ${2}.', pdf_batch_previous: 'Prejšnjih ${1} strani',
pdf_limit_reached_continue: 'Razširi še za ${1} strani.', pdf_batch_next: 'Naslednjih ${1} strani',
more: 'Več', more: 'Več',
link_tooltip_external_link: 'Zunanja povezava do', link_tooltip_external_link: 'Zunanja povezava do',
link_tooltip_email: 'Pošlji e-pošto na', link_tooltip_email: 'Pošlji e-pošto na',
+5 -10
View File
@@ -77,16 +77,11 @@ export const sv: TranslationLanguage = {
notfound_title: 'Sidan hittades inte', notfound_title: 'Sidan hittades inte',
notfound_link: 'Den här länken pekar på en sida som har tagits bort eller inte längre finns.', notfound_link: 'Den här länken pekar på en sida som har tagits bort eller inte längre finns.',
notfound: 'Sidan du letar efter finns inte.', notfound: 'Sidan du letar efter finns inte.',
notfound_adaptive_title: 'Logga in för att visa den här sidan', notfound_adaptive_title: 'Sidan är inte tillgänglig',
notfound_adaptive: notfound_adaptive: 'Sidan kan finnas, men du kan behöva logga in för att få åtkomst.',
'Den här sidan kan kräva att du loggar in. Om du följde en länk, kontrollera att den är korrekt.',
notfound_adaptive_login: 'Logga in', notfound_adaptive_login: 'Logga in',
notfound_adaptive_registration_hint: notfound_adaptive_registration_hint:
'Registrering kan vara tillgänglig beroende på organisationen.', 'Registrering kan vara tillgänglig beroende på organisationen.',
notfound_adaptive_no_access_title: 'Du kanske inte har åtkomst till den här sidan',
notfound_adaptive_no_access:
'Den här sidan kanske inte finns, eller så har ditt konto inte åtkomst till den. Om du följde en länk, kontrollera att den är korrekt.',
notfound_adaptive_login_again: 'Logga in igen',
notfound_goto_home: 'Gå till startsidan', notfound_goto_home: 'Gå till startsidan',
notfound_suggestions_title: 'Du kanske letar efter', notfound_suggestions_title: 'Du kanske letar efter',
unexpected_error_title: 'Ett fel uppstod', unexpected_error_title: 'Ett fel uppstod',
@@ -102,9 +97,9 @@ export const sv: TranslationLanguage = {
pdf_page_of: '${1} av ${2}', pdf_page_of: '${1} av ${2}',
pdf_mode_only_page: 'Endast denna sida', pdf_mode_only_page: 'Endast denna sida',
pdf_mode_all: 'Alla sidor', pdf_mode_all: 'Alla sidor',
pdf_limit_reached: pdf_batch_range: 'Visar sidorna ${1}–${2} av ${3}.',
'Det gick inte att generera PDF för ${1} sidor, genereringen stoppades vid ${2}.', pdf_batch_previous: 'Föregående ${1} sidor',
pdf_limit_reached_continue: 'Utöka med ytterligare ${1} sidor.', pdf_batch_next: 'Nästa ${1} sidor',
more: 'Mer', more: 'Mer',
link_tooltip_external_link: 'Extern länk till', link_tooltip_external_link: 'Extern länk till',
link_tooltip_email: 'Skicka e-post till', link_tooltip_email: 'Skicka e-post till',
+5 -8
View File
@@ -76,14 +76,10 @@ export const th: TranslationLanguage = {
notfound_title: 'ไม่พบหน้า', notfound_title: 'ไม่พบหน้า',
notfound_link: 'ลิงก์นี้ชี้ไปยังหน้าที่ถูกลบหรือไม่มีอยู่อีกต่อไป', notfound_link: 'ลิงก์นี้ชี้ไปยังหน้าที่ถูกลบหรือไม่มีอยู่อีกต่อไป',
notfound: 'หน้าที่คุณกำลังค้นหาไม่มีอยู่', notfound: 'หน้าที่คุณกำลังค้นหาไม่มีอยู่',
notfound_adaptive_title: 'เข้าสู่ระบบเพื่อดูหน้านี้', notfound_adaptive_title: 'หน้าไม่พร้อมใช้งาน',
notfound_adaptive: 'หน้านี้อาจต้องเข้าสู่ระบบ หากคุณเปิดจากลิงก์ โปรดตรวจสอบว่าลิงก์ถูกต้อง', notfound_adaptive: 'หน้านี้อาจมีอยู่ แต่คุณอาจต้องเข้าสู่ระบบเพื่อเข้าถึง',
notfound_adaptive_login: 'เข้าสู่ระบบ', notfound_adaptive_login: 'เข้าสู่ระบบ',
notfound_adaptive_registration_hint: 'การลงทะเบียนอาจใช้ได้ ขึ้นอยู่กับองค์กร', notfound_adaptive_registration_hint: 'การลงทะเบียนอาจใช้ได้ ขึ้นอยู่กับองค์กร',
notfound_adaptive_no_access_title: 'คุณอาจไม่มีสิทธิ์เข้าถึงหน้านี้',
notfound_adaptive_no_access:
'หน้านี้อาจไม่มีอยู่ หรือบัญชีของคุณอาจไม่มีสิทธิ์เข้าถึง หากคุณเปิดจากลิงก์ โปรดตรวจสอบว่าลิงก์ถูกต้อง',
notfound_adaptive_login_again: 'เข้าสู่ระบบอีกครั้ง',
notfound_goto_home: 'ไปที่หน้าแรก', notfound_goto_home: 'ไปที่หน้าแรก',
notfound_suggestions_title: 'คุณอาจกำลังมองหา', notfound_suggestions_title: 'คุณอาจกำลังมองหา',
unexpected_error_title: 'เกิดข้อผิดพลาด', unexpected_error_title: 'เกิดข้อผิดพลาด',
@@ -99,8 +95,9 @@ export const th: TranslationLanguage = {
pdf_page_of: '${1} จาก ${2}', pdf_page_of: '${1} จาก ${2}',
pdf_mode_only_page: 'เฉพาะหน้านี้', pdf_mode_only_page: 'เฉพาะหน้านี้',
pdf_mode_all: 'ทุกหน้า', pdf_mode_all: 'ทุกหน้า',
pdf_limit_reached: 'ไม่สามารถสร้าง PDF สำหรับ ${1} หน้าได้ การสร้างหยุดที่ ${2}', pdf_batch_range: 'แสดงหน้า ${1}–${2} จาก ${3}',
pdf_limit_reached_continue: 'ขยายอีก ${1} หน้า', pdf_batch_previous: '${1} หน้าก่อนหน้า',
pdf_batch_next: '${1} หน้าถัดไป',
more: 'เพิ่มเติม', more: 'เพิ่มเติม',
link_tooltip_external_link: 'ลิงก์ภายนอกไปยัง', link_tooltip_external_link: 'ลิงก์ภายนอกไปยัง',
link_tooltip_email: 'ส่งอีเมลไปยัง', link_tooltip_email: 'ส่งอีเมลไปยัง',
+5 -9
View File
@@ -77,15 +77,10 @@ export const tr: TranslationLanguage = {
notfound_title: 'Sayfa bulunamadı', notfound_title: 'Sayfa bulunamadı',
notfound_link: 'Bu bağlantı kaldırılmış veya artık mevcut olmayan bir sayfaya işaret ediyor.', notfound_link: 'Bu bağlantı kaldırılmış veya artık mevcut olmayan bir sayfaya işaret ediyor.',
notfound: 'Aradığınız sayfa mevcut değil.', notfound: 'Aradığınız sayfa mevcut değil.',
notfound_adaptive_title: 'Bu sayfayı görüntülemek için oturum açın', notfound_adaptive_title: 'Sayfa kullanılamıyor',
notfound_adaptive: notfound_adaptive: 'Bu sayfa mevcut olabilir, ancak erişmek için oturum açmanız gerekebilir.',
'Bu sayfa oturum açmanızı gerektirebilir. Bir bağlantıyı izlediyseniz doğru olduğunu kontrol edin.',
notfound_adaptive_login: 'Oturum aç', notfound_adaptive_login: 'Oturum aç',
notfound_adaptive_registration_hint: 'Kayıt, kuruluşa bağlı olarak mevcut olabilir.', notfound_adaptive_registration_hint: 'Kayıt, kuruluşa bağlı olarak mevcut olabilir.',
notfound_adaptive_no_access_title: 'Bu sayfaya erişiminiz olmayabilir',
notfound_adaptive_no_access:
'Bu sayfa mevcut olmayabilir veya hesabınızın bu sayfaya erişimi olmayabilir. Bir bağlantıyı izlediyseniz doğru olduğunu kontrol edin.',
notfound_adaptive_login_again: 'Tekrar oturum aç',
notfound_goto_home: 'Ana sayfaya git', notfound_goto_home: 'Ana sayfaya git',
notfound_suggestions_title: 'Şunu arıyor olabilirsiniz', notfound_suggestions_title: 'Şunu arıyor olabilirsiniz',
unexpected_error_title: 'Bir hata oluştu', unexpected_error_title: 'Bir hata oluştu',
@@ -101,8 +96,9 @@ export const tr: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: 'Yalnızca bu sayfa', pdf_mode_only_page: 'Yalnızca bu sayfa',
pdf_mode_all: 'Tüm sayfalar', pdf_mode_all: 'Tüm sayfalar',
pdf_limit_reached: '${1} sayfa için PDF oluşturulamadı, oluşturma ${2} konumunda durdu.', pdf_batch_range: '${3} sayfadan ${1}–${2} arası gösteriliyor.',
pdf_limit_reached_continue: '${1} sayfa daha ekleyerek genişlet.', pdf_batch_previous: 'Önceki ${1} sayfa',
pdf_batch_next: 'Sonraki ${1} sayfa',
more: 'Daha fazla', more: 'Daha fazla',
link_tooltip_external_link: 'Harici bağlantı', link_tooltip_external_link: 'Harici bağlantı',
link_tooltip_email: 'E-posta gönder', link_tooltip_email: 'E-posta gönder',
+5 -9
View File
@@ -77,15 +77,10 @@ export const uk: TranslationLanguage = {
notfound_title: 'Сторінку не знайдено', notfound_title: 'Сторінку не знайдено',
notfound_link: 'Це посилання веде на сторінку, яку видалено або якої більше не існує.', notfound_link: 'Це посилання веде на сторінку, яку видалено або якої більше не існує.',
notfound: 'Сторінка, яку ви шукаєте, не існує.', notfound: 'Сторінка, яку ви шукаєте, не існує.',
notfound_adaptive_title: 'Увійдіть, щоб переглянути цю сторінку', notfound_adaptive_title: 'Сторінка недоступна',
notfound_adaptive: notfound_adaptive: 'Ця сторінка може існувати, але для доступу до неї може знадобитися вхід.',
'Для перегляду цієї сторінки може знадобитися вхід. Якщо ви перейшли за посиланням, перевірте, чи воно правильне.',
notfound_adaptive_login: 'Увійти', notfound_adaptive_login: 'Увійти',
notfound_adaptive_registration_hint: 'Реєстрація може бути доступною залежно від організації.', notfound_adaptive_registration_hint: 'Реєстрація може бути доступною залежно від організації.',
notfound_adaptive_no_access_title: 'Можливо, у вас немає доступу до цієї сторінки',
notfound_adaptive_no_access:
'Можливо, ця сторінка не існує або ваш обліковий запис не має до неї доступу. Якщо ви перейшли за посиланням, перевірте, чи воно правильне.',
notfound_adaptive_login_again: 'Увійти знову',
notfound_goto_home: 'Перейти на головну', notfound_goto_home: 'Перейти на головну',
notfound_suggestions_title: 'Можливо, ви шукаєте', notfound_suggestions_title: 'Можливо, ви шукаєте',
unexpected_error_title: 'Сталася помилка', unexpected_error_title: 'Сталася помилка',
@@ -101,8 +96,9 @@ export const uk: TranslationLanguage = {
pdf_page_of: '${1} з ${2}', pdf_page_of: '${1} з ${2}',
pdf_mode_only_page: 'Лише ця сторінка', pdf_mode_only_page: 'Лише ця сторінка',
pdf_mode_all: 'Усі сторінки', pdf_mode_all: 'Усі сторінки',
pdf_limit_reached: 'Не вдалося створити PDF для ${1} сторінок, створення зупинилося на ${2}.', pdf_batch_range: 'Сторінки ${1}–${2} з ${3}.',
pdf_limit_reached_continue: 'Розширити ще на ${1} сторінок.', pdf_batch_previous: 'Попередні ${1} сторінок',
pdf_batch_next: 'Наступні ${1} сторінок',
more: 'Більше', more: 'Більше',
link_tooltip_external_link: 'Зовнішнє посилання на', link_tooltip_external_link: 'Зовнішнє посилання на',
link_tooltip_email: 'Надіслати електронний лист на', link_tooltip_email: 'Надіслати електронний лист на',
+5 -9
View File
@@ -77,15 +77,10 @@ export const vi: TranslationLanguage = {
notfound_title: 'Không tìm thấy trang', notfound_title: 'Không tìm thấy trang',
notfound_link: 'Liên kết này trỏ đến một trang đã bị xóa hoặc không còn tồn tại.', notfound_link: 'Liên kết này trỏ đến một trang đã bị xóa hoặc không còn tồn tại.',
notfound: 'Trang bạn đang tìm kiếm không tồn tại.', notfound: 'Trang bạn đang tìm kiếm không tồn tại.',
notfound_adaptive_title: 'Đăng nhập để xem trang này', notfound_adaptive_title: 'Trang không khả dụng',
notfound_adaptive: notfound_adaptive: 'Trang này có thể tồn tại, nhưng bạn có thể cần đăng nhập để truy cập.',
'Trang này có thể yêu cầu bạn đăng nhập. Nếu bạn truy cập qua một liên kết, hãy kiểm tra xem liên kết đó có đúng không.',
notfound_adaptive_login: 'Đăng nhập', notfound_adaptive_login: 'Đăng nhập',
notfound_adaptive_registration_hint: 'Việc đăng ký có thể khả dụng tùy theo tổ chức.', notfound_adaptive_registration_hint: 'Việc đăng ký có thể khả dụng tùy theo tổ chức.',
notfound_adaptive_no_access_title: 'Có thể bạn không có quyền truy cập trang này',
notfound_adaptive_no_access:
'Trang này có thể không tồn tại hoặc tài khoản của bạn có thể không có quyền truy cập. Nếu bạn truy cập qua một liên kết, hãy kiểm tra xem liên kết đó có đúng không.',
notfound_adaptive_login_again: 'Đăng nhập lại',
notfound_goto_home: 'Đi tới trang chủ', notfound_goto_home: 'Đi tới trang chủ',
notfound_suggestions_title: 'Có thể bạn đang tìm', notfound_suggestions_title: 'Có thể bạn đang tìm',
unexpected_error_title: 'Đã xảy ra lỗi', unexpected_error_title: 'Đã xảy ra lỗi',
@@ -101,8 +96,9 @@ export const vi: TranslationLanguage = {
pdf_page_of: '${1} trên ${2}', pdf_page_of: '${1} trên ${2}',
pdf_mode_only_page: 'Chỉ trang này', pdf_mode_only_page: 'Chỉ trang này',
pdf_mode_all: 'Tất cả trang', pdf_mode_all: 'Tất cả trang',
pdf_limit_reached: 'Không thể tạo PDF cho ${1} trang, quá trình tạo đã dừng tại ${2}.', pdf_batch_range: 'Đang hiển thị trang ${1}–${2} trên ${3}.',
pdf_limit_reached_continue: 'Mở rộng thêm ${1} trang.', pdf_batch_previous: '${1} trang trước',
pdf_batch_next: '${1} trang tiếp theo',
more: 'Thêm', more: 'Thêm',
link_tooltip_external_link: 'Liên kết ngoài tới', link_tooltip_external_link: 'Liên kết ngoài tới',
link_tooltip_email: 'Gửi email tới', link_tooltip_email: 'Gửi email tới',
@@ -75,14 +75,10 @@ export const yue: TranslationLanguage = {
notfound_title: '搵唔到頁面', notfound_title: '搵唔到頁面',
notfound_link: '呢條連結指向嘅頁面已被移除或已經唔存在。', notfound_link: '呢條連結指向嘅頁面已被移除或已經唔存在。',
notfound: '你搵緊嘅頁面唔存在。', notfound: '你搵緊嘅頁面唔存在。',
notfound_adaptive_title: '登入先可以睇呢個頁面', notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '呢個頁面可能需要登入。如果你係經連結過嚟,請檢查連結係咪正確。', notfound_adaptive: '呢個頁面可能存在,但你可能需要登入先可以存取。',
notfound_adaptive_login: '登入', notfound_adaptive_login: '登入',
notfound_adaptive_registration_hint: '視乎機構,可能可以註冊。', notfound_adaptive_registration_hint: '視乎機構,可能可以註冊。',
notfound_adaptive_no_access_title: '你可能冇權限存取呢個頁面',
notfound_adaptive_no_access:
'呢個頁面可能唔存在,或者你嘅帳戶可能冇權限存取。如果你係經連結過嚟,請檢查連結係咪正確。',
notfound_adaptive_login_again: '重新登入',
notfound_goto_home: '前往首頁', notfound_goto_home: '前往首頁',
notfound_suggestions_title: '你可能喺度搵緊', notfound_suggestions_title: '你可能喺度搵緊',
unexpected_error_title: '發生錯誤', unexpected_error_title: '發生錯誤',
@@ -98,8 +94,9 @@ export const yue: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: '只限呢頁', pdf_mode_only_page: '只限呢頁',
pdf_mode_all: '所有頁面', pdf_mode_all: '所有頁面',
pdf_limit_reached: '無法為 ${1} 頁產生 PDF,已喺 ${2} 停止。', pdf_batch_range: '而家顯示緊第 ${1}–${2} 頁,總共 ${3} 頁。',
pdf_limit_reached_continue: '再加 ${1} 頁。', pdf_batch_previous: '上 ${1} 頁',
pdf_batch_next: '下 ${1} 頁',
more: '更多', more: '更多',
link_tooltip_external_link: '外部連結到', link_tooltip_external_link: '外部連結到',
link_tooltip_email: '傳送電郵到', link_tooltip_email: '傳送電郵到',
@@ -75,14 +75,10 @@ export const zh_tw: TranslationLanguage = {
notfound_title: '找不到頁面', notfound_title: '找不到頁面',
notfound_link: '此連結指向的頁面已被移除或不再存在。', notfound_link: '此連結指向的頁面已被移除或不再存在。',
notfound: '您要尋找的頁面不存在。', notfound: '您要尋找的頁面不存在。',
notfound_adaptive_title: '登入以檢視此頁面', notfound_adaptive_title: '頁面無法使用',
notfound_adaptive: '此頁面可能需要登入。如果您是透過連結前往,請確認連結是否正確。', notfound_adaptive: '此頁面可能存在,但您可能需要登入才能存取。',
notfound_adaptive_login: '登入', notfound_adaptive_login: '登入',
notfound_adaptive_registration_hint: '視組織而定,可能可以註冊。', notfound_adaptive_registration_hint: '視組織而定,可能可以註冊。',
notfound_adaptive_no_access_title: '您可能沒有此頁面的存取權限',
notfound_adaptive_no_access:
'此頁面可能不存在,或您的帳戶可能沒有存取權限。如果您是透過連結前往,請確認連結是否正確。',
notfound_adaptive_login_again: '重新登入',
notfound_goto_home: '前往首頁', notfound_goto_home: '前往首頁',
notfound_suggestions_title: '您可能在尋找', notfound_suggestions_title: '您可能在尋找',
unexpected_error_title: '發生錯誤', unexpected_error_title: '發生錯誤',
@@ -98,8 +94,9 @@ export const zh_tw: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: '僅此頁面', pdf_mode_only_page: '僅此頁面',
pdf_mode_all: '所有頁面', pdf_mode_all: '所有頁面',
pdf_limit_reached: '無法為 ${1} 頁產生 PDF,已在 ${2} 停止產生。', pdf_batch_range: '正在顯示第 ${1}–${2} 頁,共 ${3} 頁。',
pdf_limit_reached_continue: '再擴展 ${1} 頁。', pdf_batch_previous: '上 ${1} 頁',
pdf_batch_next: '下 ${1} 頁',
more: '更多', more: '更多',
link_tooltip_external_link: '外部連結至', link_tooltip_external_link: '外部連結至',
link_tooltip_email: '傳送電子郵件至', link_tooltip_email: '傳送電子郵件至',
+5 -8
View File
@@ -76,14 +76,10 @@ export const zh: TranslationLanguage = {
notfound_title: '页面未找到', notfound_title: '页面未找到',
notfound_link: '此链接指向已被删除或不再存在的页面。', notfound_link: '此链接指向已被删除或不再存在的页面。',
notfound: '您要找的页面不存在。', notfound: '您要找的页面不存在。',
notfound_adaptive_title: '登录以查看此页面', notfound_adaptive_title: '页面无法访问',
notfound_adaptive: '此页面可能需要登录。如果您是通过链接访问的,请检查链接是否正确。', notfound_adaptive: '该页面可能存在,但您可能需要登录后才能访问。',
notfound_adaptive_login: '登录', notfound_adaptive_login: '登录',
notfound_adaptive_registration_hint: '视组织而定,可能可以注册。', notfound_adaptive_registration_hint: '视组织而定,可能可以注册。',
notfound_adaptive_no_access_title: '您可能没有此页面的访问权限',
notfound_adaptive_no_access:
'此页面可能不存在,或您的账户可能没有访问权限。如果您是通过链接访问的,请检查链接是否正确。',
notfound_adaptive_login_again: '重新登录',
notfound_goto_home: '前往首页', notfound_goto_home: '前往首页',
notfound_suggestions_title: '您可能在寻找', notfound_suggestions_title: '您可能在寻找',
unexpected_error_title: '发生错误', unexpected_error_title: '发生错误',
@@ -99,8 +95,9 @@ export const zh: TranslationLanguage = {
pdf_page_of: '${1} / ${2}', pdf_page_of: '${1} / ${2}',
pdf_mode_only_page: '仅本页', pdf_mode_only_page: '仅本页',
pdf_mode_all: '所有页面', pdf_mode_all: '所有页面',
pdf_limit_reached: '无法为 ${1} 页生成 PDF,生成在 ${2} 页时停止。', pdf_batch_range: '正在显示第 ${1}–${2} 页,共 ${3} 页。',
pdf_limit_reached_continue: '再扩展 ${1} 页。', pdf_batch_previous: '上 ${1} 页',
pdf_batch_next: '下 ${1} 页',
more: '更多', more: '更多',
link_tooltip_external_link: '外部链接到', link_tooltip_external_link: '外部链接到',
link_tooltip_email: '发送邮件到', link_tooltip_email: '发送邮件到',
@@ -105,30 +105,3 @@ describe('preview auth redirects', () => {
} }
); );
}); });
describe('visitor payload', () => {
it('forwards the visitor country to the API', async () => {
const calls: { visitor?: unknown }[] = [];
const apiClientSpy = spyOn(api, 'apiClient').mockReturnValue({
urls: {
async resolvePublishedContentByUrl(body: { visitor?: unknown }) {
calls.push(body);
return { data: { target: 'external', redirect: 'https://example.com' } };
},
},
} as unknown as ReturnType<typeof api.apiClient>);
try {
await lookupPublishedContentByUrl({
url: 'https://docs.example.com',
apiToken: null,
redirectOnError: false,
visitorPayload: { type: 'human', country: 'FR' },
});
} finally {
apiClientSpy.mockRestore();
}
expect(calls[0]?.visitor).toEqual({ type: 'human', country: 'FR' });
});
});
-9
View File
@@ -14,15 +14,6 @@ export const GITBOOK_RUNTIME = (process.env.GITBOOK_RUNTIME ?? 'unknown') as
| 'cloudflare' | 'cloudflare'
| 'unknown'; | 'unknown';
/**
* Deployment stage (`STAGE` from the Cloudflare wrangler configs, the target env on Vercel),
* defaulting to `dev` locally.
*/
export const GITBOOK_STAGE =
process.env.STAGE ??
process.env.VERCEL_TARGET_ENV ??
(process.env.NODE_ENV === 'development' ? 'dev' : undefined);
/** /**
* Main host on which GitBook is running. * Main host on which GitBook is running.
*/ */
+1 -43
View File
@@ -1,8 +1,6 @@
import { describe, expect, it } from 'bun:test'; import { describe, expect, it } from 'bun:test';
import type { PublishedSiteContent } from '@gitbook/api'; import { getPreviewRequestIdentifier, isPreviewRequest } from './preview';
import { getPreviewCookieResponse, getPreviewRequestIdentifier, isPreviewRequest } from './preview';
describe('isPreviewRequest', () => { describe('isPreviewRequest', () => {
it('should return true for preview requests', () => { it('should return true for preview requests', () => {
@@ -25,43 +23,3 @@ describe('getPreviewRequestIdentifier', () => {
expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo'); expect(getPreviewRequestIdentifier(previewRequestURL)).toBe('site_foo');
}); });
}); });
describe('getPreviewCookieResponse', () => {
// The site's canonical base path — deliberately different from the preview route prefix so the
// test fails if the cookie is scoped to it instead of the preview route.
const siteURLData = { siteBasePath: '/docs/' } as unknown as PublishedSiteContent;
it('scopes the cookie to the preview route prefix in url-host mode, not the site base path', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url-host',
siteRequestURL: new URL('https://sites.gitbook.com/preview/site_foo/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/preview/site_foo');
expect(cookie.options?.sameSite).toBe('lax');
});
it('scopes the cookie to the proxied preview route prefix in url mode', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url',
siteRequestURL: new URL('https://sites.gitbook.com/preview/site_foo/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/url/sites.gitbook.com/preview/site_foo');
});
it('falls back to the site base path for a non-preview url-host request', () => {
const cookie = getPreviewCookieResponse({
name: 'gitbook-customization',
value: 'v',
mode: 'url-host',
siteRequestURL: new URL('https://example.com/docs/hello/world'),
siteURLData,
});
expect(cookie.options?.path).toBe('/docs/');
});
});
+2 -15
View File
@@ -51,21 +51,8 @@ export function getPreviewCookieResponse(args: {
gitbookPreviewBaseURL.host + gitbookPreviewBaseURL.pathname.replace(/\/$/, ''); gitbookPreviewBaseURL.host + gitbookPreviewBaseURL.pathname.replace(/\/$/, '');
return `/url/${gitbookPreviewHostPath}/${getPreviewRequestIdentifier(siteRequestURL)}`; return `/url/${gitbookPreviewHostPath}/${getPreviewRequestIdentifier(siteRequestURL)}`;
} }
case 'url-host': { case 'url-host':
// The whole site preview (all variants/sections/pages) is served under the preview return siteURLData.siteBasePath;
// route prefix `<previewBasePath>/<siteId>` (e.g. `/preview/<siteId>`). Scope the
// cookie to that prefix, derived from the request, so it is resent on every
// in-preview navigation. The resolved `siteBasePath` is the site's canonical base
// path and does not prefix the preview route, so relying on it makes the browser
// drop the cookie on the next page — the actual cause of the override being lost.
// Mirrors getVisitorAuthBasePath, which uses the request identifier for proxy
// requests. Fall back to `siteBasePath` for any preview not served under the route.
if (!isPreviewRequest(siteRequestURL)) {
return siteURLData.siteBasePath;
}
const gitbookPreviewBaseURL = new URL(GITBOOK_PREVIEW_BASE_URL);
return `${gitbookPreviewBaseURL.pathname.replace(/\/$/, '')}/${getPreviewRequestIdentifier(siteRequestURL)}`;
}
default: default:
assertNever(mode); assertNever(mode);
} }
-92
View File
@@ -7,14 +7,11 @@ import {
getVisitorAuthCookieMaxAge, getVisitorAuthCookieMaxAge,
getVisitorAuthCookieName, getVisitorAuthCookieName,
getVisitorAuthCookieValue, getVisitorAuthCookieValue,
getVisitorCountry,
getVisitorToken, getVisitorToken,
getVisitorType, getVisitorType,
getVisitorUnsignedClaims, getVisitorUnsignedClaims,
isRevalidationRequest, isRevalidationRequest,
normalizeVisitorURL, normalizeVisitorURL,
parseHostnameList,
shouldSendVisitorCountry,
} from './visitors'; } from './visitors';
describe('getVisitorAuthToken', () => { describe('getVisitorAuthToken', () => {
@@ -602,92 +599,3 @@ describe('isRevalidationRequest', () => {
expect(isRevalidationRequest(new Headers())).toBe(false); expect(isRevalidationRequest(new Headers())).toBe(false);
}); });
}); });
describe('getVisitorCountry', () => {
const requestWith = (headers: Record<string, string>) => ({ headers: new Headers(headers) });
it('should prefer the OpenNext country header', () => {
expect(
getVisitorCountry(
requestWith({ 'x-open-next-country': 'FR', 'x-vercel-ip-country': 'US' })
)
).toBe('FR');
});
it('should fall back to the Vercel country header', () => {
expect(getVisitorCountry(requestWith({ 'x-vercel-ip-country': 'US' }))).toBe('US');
});
it('should normalize the country code to uppercase', () => {
expect(getVisitorCountry(requestWith({ 'x-open-next-country': ' fr ' }))).toBe('FR');
});
it.each(['XX', 'T1', 'FRA', 'F', ''])('should ignore the invalid country code %p', (value) => {
expect(getVisitorCountry(requestWith({ 'x-open-next-country': value }))).toBeUndefined();
});
it('should return undefined when no country header is present', () => {
expect(getVisitorCountry(requestWith({}))).toBeUndefined();
});
it('should prefer the forwarded country header on revalidation requests', () => {
expect(
getVisitorCountry(
requestWith({
'user-agent': 'gitbook-open-revalidation-worker',
'x-gitbook-country': 'DE',
'x-open-next-country': 'FR',
})
)
).toBe('DE');
});
it('should ignore the forwarded country header outside revalidation requests', () => {
expect(
getVisitorCountry(
requestWith({ 'x-gitbook-country': 'DE', 'x-open-next-country': 'FR' })
)
).toBe('FR');
expect(getVisitorCountry(requestWith({ 'x-gitbook-country': 'DE' }))).toBeUndefined();
});
it('should fall back to geolocation headers on revalidation requests without a forwarded country', () => {
expect(
getVisitorCountry(
requestWith({
'user-agent': 'gitbook-open-revalidation-worker',
'x-open-next-country': 'FR',
})
)
).toBe('FR');
});
});
describe('shouldSendVisitorCountry', () => {
it.each(['dev', 'preview', 'staging'])('should be enabled on the %p stage', (stage) => {
expect(shouldSendVisitorCountry('docs.example.com', stage)).toBe(true);
});
it.each(['production', undefined])(
'should be disabled for other hostnames on the %p stage',
(stage) => {
expect(shouldSendVisitorCountry('docs.example.com', stage)).toBe(false);
}
);
});
describe('parseHostnameList', () => {
it.each([undefined, '', ' , '])('should return an empty set for %p', (value) => {
expect(parseHostnameList(value).size).toBe(0);
});
it('should parse a single entry', () => {
expect(parseHostnameList('docs.foo.com')).toEqual(new Set(['docs.foo.com']));
});
it('should trim, lowercase and drop empty entries', () => {
expect(parseHostnameList(' Docs.Foo.com , ,bar.io')).toEqual(
new Set(['docs.foo.com', 'bar.io'])
);
});
});
-50
View File
@@ -106,56 +106,6 @@ export function getVisitorType(request: {
return detection.detected && detection.method !== 'heuristic' ? 'agent' : 'human'; return detection.detected && detection.method !== 'heuristic' ? 'agent' : 'human';
} }
// Production hostnames for which the visitor country is sent while the feature is rolled out.
// Set at build time from a GitHub Actions variable to keep customer hostnames out of the code.
const VISITOR_COUNTRY_HOSTNAMES = parseHostnameList(process.env.GITBOOK_VISITOR_COUNTRY_HOSTNAMES);
const VISITOR_COUNTRY_STAGES = new Set(['dev', 'preview', 'staging']);
/**
* Parse a comma-separated list of hostnames.
*/
export function parseHostnameList(value: string | undefined): Set<string> {
return new Set(
(value ?? '')
.split(',')
.map((hostname) => hostname.trim().toLowerCase())
.filter(Boolean)
);
}
/**
* Whether the visitor country should be sent when resolving the site URL.
*/
export function shouldSendVisitorCountry(hostname: string, stage: string | undefined): boolean {
return (
(!!stage && VISITOR_COUNTRY_STAGES.has(stage)) ||
VISITOR_COUNTRY_HOSTNAMES.has(hostname.toLowerCase())
);
}
/**
* Get the ISO 3166-1 alpha-2 country code of the visitor from the geolocation headers,
* or from `x-gitbook-country` when the request comes from the revalidation worker.
*/
export function getVisitorCountry(request: { headers: Headers }): string | undefined {
// The revalidation worker forwards the original visitor country; only trust it from there.
const country = (
(isRevalidationRequest(request.headers) && request.headers.get('x-gitbook-country')) ||
request.headers.get('x-open-next-country') ||
request.headers.get('x-vercel-ip-country') ||
''
)
.trim()
.toUpperCase();
// Cloudflare uses XX for unknown and T1 for Tor, which are not ISO codes.
if (!/^[A-Z]{2}$/.test(country) || country === 'XX' || country === 'T1') {
return undefined;
}
return country;
}
/** /**
* Get the visitor data for the request potentially including: * Get the visitor data for the request potentially including:
* - a JWT token that may contain signed claims or can be used for VA authentication. * - a JWT token that may contain signed claims or can be used for VA authentication.
+2 -16
View File
@@ -35,12 +35,7 @@ import {
normalizeRequestURL, normalizeRequestURL,
throwIfDataError, throwIfDataError,
} from '@/lib/data'; } from '@/lib/data';
import { import { GITBOOK_DISABLE_INSIGHTS, isGitBookAssetsHostURL, isGitBookHostURL } from '@/lib/env';
GITBOOK_DISABLE_INSIGHTS,
GITBOOK_STAGE,
isGitBookAssetsHostURL,
isGitBookHostURL,
} from '@/lib/env';
import { getImageResizingContextId } from '@/lib/images'; import { getImageResizingContextId } from '@/lib/images';
import { isAITrainingOrIndexingRequest } from '@/lib/indexing-crawlers'; import { isAITrainingOrIndexingRequest } from '@/lib/indexing-crawlers';
import { MCP_SERVER_CARD_PATH, MCP_SERVER_CARD_WELL_KNOWN_PATH } from '@/lib/mcp/paths'; import { MCP_SERVER_CARD_PATH, MCP_SERVER_CARD_WELL_KNOWN_PATH } from '@/lib/mcp/paths';
@@ -61,13 +56,11 @@ import {
type ResponseCookies, type ResponseCookies,
getPathScopedCookieName, getPathScopedCookieName,
getResponseCookiesForVisitorAuth, getResponseCookiesForVisitorAuth,
getVisitorCountry,
getVisitorData, getVisitorData,
getVisitorType, getVisitorType,
isRevalidationRequest, isRevalidationRequest,
normalizeVisitorURL, normalizeVisitorURL,
serveVisitorClaimsDataRequest, serveVisitorClaimsDataRequest,
shouldSendVisitorCountry,
} from '@/lib/visitors'; } from '@/lib/visitors';
import { waitUntil } from '@/lib/waitUntil'; import { waitUntil } from '@/lib/waitUntil';
import { serveResizedImage } from '@/routes/image'; import { serveResizedImage } from '@/routes/image';
@@ -227,10 +220,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
// //
request.headers.delete('x-gitbook-disable-tracking'); request.headers.delete('x-gitbook-disable-tracking');
const visitorCountry = shouldSendVisitorCountry(siteRequestURL.hostname, GITBOOK_STAGE)
? getVisitorCountry(request)
: undefined;
const withAPIToken = async (apiToken: string | null) => { const withAPIToken = async (apiToken: string | null) => {
const siteURLData = await throwIfDataError( const siteURLData = await throwIfDataError(
lookupPublishedContentByUrl({ lookupPublishedContentByUrl({
@@ -239,7 +228,6 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
jwtToken: visitorToken?.token ?? undefined, jwtToken: visitorToken?.token ?? undefined,
unsignedClaims, unsignedClaims,
type: getVisitorType(request), type: getVisitorType(request),
...(visitorCountry ? { country: visitorCountry } : {}),
}, },
// When the visitor auth token is pulled from the cookie, set redirectOnError when calling resolvePublishedContentByUrl to allow // When the visitor auth token is pulled from the cookie, set redirectOnError when calling resolvePublishedContentByUrl to allow
// redirecting when the token is invalid as we could be dealing with stale token stored in the cookie. // redirecting when the token is invalid as we could be dealing with stale token stored in the cookie.
@@ -520,8 +508,7 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
siteId: siteURLData.site, siteId: siteURLData.site,
events, events,
request: { request: {
// A proxied site's request URL is the proxy's (`/sites/site_xxx/...`), not its public one. url: siteRequestURL.toString(),
url: siteCanonicalURL.toString(),
headers: requestHeaders, headers: requestHeaders,
}, },
}) })
@@ -954,7 +941,6 @@ function encodePathInSiteContent(
{ {
type: 'ask_question', type: 'ask_question',
query: ask, query: ask,
goal: typeof goal === 'string' ? goal : undefined,
location: { location: {
displayContext: SiteInsightsDisplayContext.Server, displayContext: SiteInsightsDisplayContext.Server,
}, },
@@ -1,69 +0,0 @@
import { describe, expect, it } from 'bun:test';
import { renderToReadableStream } from 'react-dom/server';
import { parseOpenAPI } from '@gitbook/openapi-parser';
import type { OpenAPIClientContext } from './context';
import { OpenAPIRootSchema } from './OpenAPISchemaServer';
import { resolveOpenAPISchemas } from './schemas/resolveOpenAPISchemas';
import { translations } from './translations';
const context: OpenAPIClientContext = {
translation: translations.en,
icons: {
chevronDown: null,
chevronRight: null,
plus: null,
copy: null,
check: null,
lock: null,
mcp: null,
hashtag: null,
},
expandAllModelSections: true,
scalarRuntimeURL: '',
$$isClientContext$$: true,
};
describe('OpenAPIRootSchema', () => {
it('should stop at a self-reference behind a nullable union when expanded', async () => {
const { filesystem } = await parseOpenAPI({
value: JSON.stringify({
openapi: '3.1.0',
info: { title: 'Test', version: '1.0.0' },
paths: {},
components: {
schemas: {
FilterClause: {
type: 'object',
properties: {
AND: {
anyOf: [
{
type: 'array',
items: { $ref: '#/components/schemas/FilterClause' },
},
{ type: 'null' },
],
},
},
},
},
},
}),
rootURL: 'memory://spec.json',
});
const resolved = await resolveOpenAPISchemas(filesystem, { schemas: ['FilterClause'] });
const schema = resolved?.schemas[0]?.schema;
if (!schema) {
throw new Error('FilterClause not resolved');
}
const stream = await renderToReadableStream(
<OpenAPIRootSchema schema={schema} context={context} />
);
const html = await new Response(stream).text();
expect(html).toContain('AND');
});
});
-8
View File
@@ -190,14 +190,6 @@ describe('extractNonNullTypes', () => {
}); });
describe('normalizeNullableUnion', () => { describe('normalizeNullableUnion', () => {
it('should return the same object for the same input', () => {
const schema: OpenAPIV3_1.SchemaObject = {
anyOf: [{ type: 'array', items: { type: 'string' } }, { type: 'null' }],
};
expect(normalizeNullableUnion(schema)).toBe(normalizeNullableUnion(schema));
});
it('should collapse anyOf with a single non-null member into a nullable schema', () => { it('should collapse anyOf with a single non-null member into a nullable schema', () => {
const schema: OpenAPIV3_1.SchemaObject = { const schema: OpenAPIV3_1.SchemaObject = {
anyOf: [{ type: 'string' }, { type: 'null' }], anyOf: [{ type: 'string' }, { type: 'null' }],
-14
View File
@@ -287,10 +287,6 @@ function isNullSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject
return type === 'null'; return type === 'null';
} }
// Circular-ref tracking compares schemas by identity: a fresh copy on every render lets a
// self-referencing nullable property (`anyOf: [{ items: Self }, null]`) recurse forever when expanded.
const normalizedNullableUnions = new WeakMap<OpenAPIV3.SchemaObject, OpenAPIV3.SchemaObject>();
/** /**
* Normalize the OpenAPI 3.1+ idiom of expressing nullability through `anyOf`/`oneOf` * Normalize the OpenAPI 3.1+ idiom of expressing nullability through `anyOf`/`oneOf`
* (e.g. `anyOf: [{ type: 'string' }, { type: 'null' }]`) into a regular nullable schema, * (e.g. `anyOf: [{ type: 'string' }, { type: 'null' }]`) into a regular nullable schema,
@@ -305,17 +301,7 @@ export function normalizeNullableUnion(
schema: OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject schema: OpenAPIV3.SchemaObject | OpenAPIV3_1.SchemaObject
): OpenAPIV3.SchemaObject { ): OpenAPIV3.SchemaObject {
const typed = schema as OpenAPIV3.SchemaObject; const typed = schema as OpenAPIV3.SchemaObject;
const cached = normalizedNullableUnions.get(typed);
if (cached) {
return cached;
}
const normalized = normalizeNullableUnionUncached(typed);
normalizedNullableUnions.set(typed, normalized);
return normalized;
}
function normalizeNullableUnionUncached(typed: OpenAPIV3.SchemaObject): OpenAPIV3.SchemaObject {
const isAnyOf = Array.isArray(typed.anyOf); const isAnyOf = Array.isArray(typed.anyOf);
const isOneOf = !isAnyOf && Array.isArray(typed.oneOf); const isOneOf = !isAnyOf && Array.isArray(typed.oneOf);
if (!isAnyOf && !isOneOf) { if (!isAnyOf && !isOneOf) {