mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-14 09:09:42 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 384cd63729 | |||
| 0898723168 | |||
| f7dc7faa65 | |||
| 03aa0f75f5 | |||
| a625112106 | |||
| 1bf3bd4294 | |||
| 9d62cb4bad | |||
| 8bc381d852 | |||
| a22923b56b | |||
| a73800e0ef | |||
| a0c1bdf58e | |||
| d685660e1d | |||
| f1d5123c0b | |||
| 679ea0706c | |||
| c366796756 | |||
| f35be52c80 | |||
| 848a39d1c8 | |||
| 9f447aab29 | |||
| 552538a0d9 | |||
| 5c4a808ba8 | |||
| ad6262b6f9 | |||
| 2137c2df7b | |||
| 990e702035 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Track which Ask AI entry point opened the chat in ask view analytics.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Update API
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix table of contents scrolling back to the current page when expanding another group.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Keep the docs embed open when switching section, variant or language.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix low-contrast button labels inside success, warning and danger hints.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Agents can now send a goal with any question they ask on the Markdown version of a page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Track server-side Insights events (markdown, llms.txt, RSS, ask) with the site's public URL instead of the proxy URL.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix OpenAPI schemas that reference themselves through a nullable union rendering forever when expanded, which crashed PDF exports.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show expression values in site previews that use a visitor segment.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Show site UI in French or Spanish for spaces set to Canadian French, Mexican Spanish or Latin American Spanish.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Let search buttons open search limited to the current space.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Render a single Ask AI paragraph button that moves to the hovered paragraph, instead of one hidden button per paragraph.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Skip rendering the Ask AI paragraph button inside table cells, where it is hidden, to keep pages with large tables within memory limits.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Upgrade object-identity to preserve value types when caching object arguments.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Read the production hostnames that receive the visitor country from the `GITBOOK_VISITOR_COUNTRY_HOSTNAMES` build-time env variable.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/react-contentkit": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Reserve the webframe's size in the server-rendered layout so anchor links to content below integration blocks no longer land too high.
|
||||
+46
-70
@@ -1,113 +1,89 @@
|
||||
# Welcome to GitBook's contributing guide!
|
||||
# Contributing to GitBook
|
||||
|
||||
> _For help, support, feature requests, and product questions - head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions) 🤖_
|
||||
> _For help, support and product questions, head to our [GitHub Community](https://github.com/orgs/GitbookIO/discussions)._
|
||||
|
||||
Thank you for investing your time in contributing to GitBook. Any contribution you make will be reviewed by our team. In this guide, you'll learn the different ways you can contribute.
|
||||
Thanks for helping make GitBook better. This repository holds the open source renderer behind every published GitBook site, and the team reads everything that comes in here.
|
||||
|
||||
## Types of Contributions
|
||||
## Ways to contribute
|
||||
|
||||
This repository contains code related to the rendering engine of GitBook's published content. Depending on what you'd like to contribute to, head to the section below to find the necessary steps.
|
||||
- **Open an issue.** Report a bug, suggest a feature, or tell us when something feels off. This is the most useful thing you can do.
|
||||
- **Join the conversation.** Share ideas and help others in [GitHub Community](https://github.com/orgs/GitbookIO/discussions).
|
||||
- **Build with GitBook.** Publish your docs, try new features early, and tell us what works and what doesn't.
|
||||
|
||||
### Add a feature
|
||||
## Issues instead of pull requests
|
||||
|
||||
Because this portion of GitBook is open source and available for you to use - if you think you can provide extra value through a new feature - you're welcome to add it! If you plan to distribute the code, keep the source code public to comply with GNU GPLv3. To clone in a private repository, you must first acquire a [commercial license](https://www.gitbook.com/pricing).
|
||||
Pull requests are limited to the GitBook team. If you've found a bug or want something to change, please [open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) instead.
|
||||
|
||||
### Create a new issue
|
||||
This isn't because we don't want your help. Every change here ships to every published GitBook site, so we need to own each one from start to finish. A clear issue often helps us more than a pull request: it tells us what's wrong and why, and lets us fix it in a way that fits the rest of the codebase.
|
||||
|
||||
If you spot a problem within a repository, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue here!Please make sure any added issues are
|
||||
A good issue includes:
|
||||
|
||||
- Descriptive
|
||||
- Thoughtful
|
||||
- Organized
|
||||
- What you expected to happen, and what happened instead
|
||||
- A link to a published GitBook site that shows the problem, if you have one
|
||||
- Steps to reproduce it, plus screenshots or a recording for anything visual
|
||||
- Your browser and OS, if it looks like it might matter
|
||||
|
||||
We recommend adding as many relevant links, minimal reproductions of the issue, and other materials that will help our team solve the issue fast.
|
||||
Before opening one, [search existing issues](https://github.com/GitbookIO/gitbook/issues). If someone has already reported it, add a 👍 or any extra details you have.
|
||||
|
||||
### Solve an issue
|
||||
Already fixed it in your fork? Link the branch or commit in your issue. We may open a pull request from it, and your commits keep your name.
|
||||
|
||||
If you're interested in solving an issue in our repository, start by scanning through it's exisiting issues to find one that you're interested in working on. If you find an issue to work on, you are welcome to open a PR with a fix. See the following sections below for more information on contributing for specific sections.
|
||||
If you open a pull request anyway, we'll close it and point you here. Nothing personal, and we'd still love the issue.
|
||||
|
||||
### Documentation
|
||||
## Reach out
|
||||
|
||||
The official documentation on GitBook open can be found directly in this Readme. Any updates or changes you would like to make, you can make directly to the README of this repository.
|
||||
Not sure if it's a bug? Have a half-formed idea? Open an issue or start a [discussion](https://github.com/orgs/GitbookIO/discussions) anyway. We'd much rather hear from you than not, and no report is too small.
|
||||
|
||||
## Contributing
|
||||
Spotted a wrong or missing translation? Open an issue with the string and the wording you'd suggest.
|
||||
|
||||
### Make changes locally
|
||||
## Using AI
|
||||
|
||||
Any contribution you make can be made to the code located in this repository. In order to contribute, you'll need to start off of a local version of this repository.
|
||||
You're welcome to use AI to help you write an issue. Please read and check what it produces before you post: reports that are long, vague or unverified take time away from fixing real problems, and we may close them without a reply.
|
||||
|
||||
#### 1. Fork the repository
|
||||
## Running locally
|
||||
|
||||
##### Using GitHub Desktop:
|
||||
You don't need a local setup to open an issue, but it can help you reproduce a problem or self-host the renderer.
|
||||
|
||||
- [Getting started with GitHub Desktop](https://docs.github.com/en/desktop/installing-and-configuring-github-desktop/getting-started-with-github-desktop) will guide you through setting up Desktop.
|
||||
- Once Desktop is set up, you can use it to [fork the repo](https://docs.github.com/en/desktop/contributing-and-collaborating-using-github-desktop/cloning-and-forking-repositories-from-github-desktop)!
|
||||
### Prerequisites
|
||||
|
||||
##### Using the command line:
|
||||
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||
|
||||
- [Fork the repo](https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#fork-an-example-repository) so that you can make your changes without affecting the original project until you're ready to merge them.
|
||||
### Setup
|
||||
|
||||
#### 2. Create a working branch and start with your changes
|
||||
1. Use the project's version of Node:
|
||||
|
||||
After forking this repository, you'll want to [create a branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/creating-a-branch-for-an-issue) to work off of.
|
||||
|
||||
#### 3. Install dependencies and run the project locally
|
||||
|
||||
##### Prerequisites:
|
||||
|
||||
- [Node.js](https://nodejs.org/en) (see "engines" in `package.json`)
|
||||
- [Bun](https://bun.sh/) (see "packageManager" in `package.json`)
|
||||
|
||||
##### Setup steps:
|
||||
|
||||
1. Ensure you are using the project's version of Node:
|
||||
```bash
|
||||
nvm use
|
||||
```
|
||||
|
||||
2. Install dependencies using Bun:
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Start the development server:
|
||||
|
||||
```bash
|
||||
bun dev
|
||||
```
|
||||
|
||||
Additional development commands:
|
||||
- `bun lint`: Lint the code using Oxlint
|
||||
- `bun lint:fix`: Automatically fix lint issues using Oxlint
|
||||
- `bun format`: Format the code using Oxfmt
|
||||
- `bun format:check`: Check formatting without changing files
|
||||
- `bun typecheck`: Run TypeScript type checking
|
||||
- `bun unit`: Run unit tests
|
||||
- `bun e2e`: Run end-to-end tests
|
||||
Other commands:
|
||||
|
||||
#### 4. Preview your changes
|
||||
- `bun lint`: Lint the code using Oxlint
|
||||
- `bun lint:fix`: Automatically fix lint issues using Oxlint
|
||||
- `bun format`: Format the code using Oxfmt
|
||||
- `bun format:check`: Check formatting without changing files
|
||||
- `bun typecheck`: Run TypeScript type checking
|
||||
- `bun unit`: Run unit tests
|
||||
- `bun e2e`: Run end-to-end tests
|
||||
|
||||
When running the development server, published GitBook sites can be rendered through your local version at `http://localhost:3000/url`.
|
||||
### Preview a site
|
||||
|
||||
For example, our published docs can be viewed using the local version by visiting `http://localhost:3000/url/gitbook.com/docs` after running the development server.
|
||||
With the development server running, any published GitBook site can be rendered through your local version at `http://localhost:3000/url/<published-url>`. For example, visit `http://localhost:3000/url/gitbook.com/docs` to see our own docs.
|
||||
|
||||
You can visit any published GitBook site behind your development server. Please make sure your site is [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to ensure you can view the site correctly in your development version.
|
||||
The site must be [published publicly](https://gitbook.com/docs/published-documentation/publish-your-content-as-a-docs-site) to render locally.
|
||||
|
||||
### Commit your update
|
||||
## License
|
||||
|
||||
[Commit your changes](https://github.com/git-guides/git-commit) once you are happy with them. See [Atom's contributing guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md#git-commit-messages) to know how to use emoji for commit messages!
|
||||
|
||||
Once your changes are ready, don't forget to self-review your code to double check that your chagnes are ready to be added.
|
||||
|
||||
### Pull Request
|
||||
|
||||
When you're finished with the changes, [create a pull request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request), also known as a PR.
|
||||
|
||||
- Don't forget to [link PR to issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue) if you are solving one.
|
||||
- Enable the checkbox to [allow maintainer edits](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork) so the branch can be updated for a merge. Once you submit your PR, a GitBook team member will review your proposal. We may ask questions or request for additional information.
|
||||
- We may ask for changes to be made before a PR can be merged, either using [suggested changes](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/incorporating-feedback-in-your-pull-request) or pull request comments. You can apply suggested changes directly through the UI. You can make any other changes in your fork, then commit them to your branch.
|
||||
- As you update your PR and apply changes, mark each conversation as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations).
|
||||
- If you run into any merge issues, checkout this [git tutorial](https://lab.github.com/githubtraining/managing-merge-conflicts) to help you resolve merge conflicts and other issues.
|
||||
|
||||
### Your PR is merged
|
||||
|
||||
Congratulations 🎉 Thank you for your contribution! Once your PR is merged, your contributions will be publicly visible on the relevant repository.
|
||||
The renderer is distributed under the [GNU GPLv3 License](https://github.com/GitbookIO/gitbook/blob/main/LICENSE). If you distribute the code, you must keep the source code public. To use it in a private repository, you need a [commercial license](https://www.gitbook.com/pricing).
|
||||
|
||||
@@ -78,6 +78,11 @@ runs:
|
||||
echo "GITBOOK_DISABLE_INSIGHTS=true" >> .vercel/.env.${{ inputs.environment }}.local
|
||||
echo "--- .vercel/.env.${{ inputs.environment }}.local after inject ---"
|
||||
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
|
||||
if: ${{ inputs.environment == 'staging' }}
|
||||
shell: bash
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
environment:
|
||||
name: 2v-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
@@ -31,6 +33,8 @@ jobs:
|
||||
environment:
|
||||
name: 2c-production
|
||||
url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
env:
|
||||
GITBOOK_VISITOR_COUNTRY_HOSTNAMES: ${{ vars.GITBOOK_VISITOR_COUNTRY_HOSTNAMES }}
|
||||
outputs:
|
||||
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
|
||||
steps:
|
||||
|
||||
@@ -85,7 +85,7 @@ GitBook Open uses fontawesome. For self-hosting and local development, for licen
|
||||
|
||||
## Contributing
|
||||
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn more about the workflow on adding your first Pull Request.
|
||||
GitBook's rendering engine is fully open source and built on top of [Next.js](https://nextjs.org/). Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to learn how to report bugs and suggest changes.
|
||||
|
||||
### Types of contributions
|
||||
|
||||
@@ -93,22 +93,22 @@ We encourage you to contribute to GitBook to help us build the best tool for doc
|
||||
|
||||
#### Translations
|
||||
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). We welcome all additional translations for the UI.
|
||||
The GitBook UI is rendered using a set of translation files found in [`packages/gitbook/src/intl/translations`](/packages/gitbook/src/intl/translations/). Spotted a wrong or missing translation? Open an issue and let us know.
|
||||
|
||||
#### Bugs
|
||||
|
||||
Encounter a bug or find an issue you'd like to fix? Helping us fix issues related to GitBook greatly improves the experience for everyone. Head to the issues section of this repository to learn more about the types of bugs you can already help out with.
|
||||
Encounter a bug? [Open an issue](https://github.com/GitbookIO/gitbook/issues/new/choose) with as much detail as you can. Clear reports help us fix things faster for everyone.
|
||||
|
||||
## Deployment
|
||||
|
||||
> [!WARNING]
|
||||
> While it is possible to self-host this project, we do not recommend this unless you are certain this option fits your need.
|
||||
>
|
||||
> _Looking to add a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to get started._
|
||||
> _Looking for a specific feature in GitBook? Head to our [contributing guide](https://github.com/GitbookIO/gitbook/blob/main/.github/CONTRIBUTING.md) to suggest it._
|
||||
>
|
||||
> Self-hosting this project puts the responsibility of maintaining and merging future updates on **you**. We cannot guarantee support, maintenance, or updates to forked and self-hosted instances of this project.
|
||||
>
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to contribute to our product directly instead of creating your own version.
|
||||
> We want to make it as easy as possible for our community to collaborate and push the future of GitBook, which is why we encourage you to share your ideas with us instead of creating your own version.
|
||||
|
||||
This project allows you to self-host the rendering portion of your GitBook published content. Self-hosting has pros and cons.
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
"object-identity": "^0.2.1",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.3",
|
||||
"p-retry": "^8.0.0",
|
||||
@@ -355,7 +355,7 @@
|
||||
},
|
||||
"catalog": {
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.204.0",
|
||||
"@gitbook/api": "0.206.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@tsconfig/node20": "^20.1.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=="],
|
||||
|
||||
"@gitbook/api": ["@gitbook/api@0.204.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-SJCe0Ipyt+V5HZX4EZpFO8AHTOWIMwj2ljQTbcqgU7rVOAyCNDzILp8mRk9stbboSi1pv09+rDnZK1ZguPvFTw=="],
|
||||
"@gitbook/api": ["@gitbook/api@0.206.0", "", { "dependencies": { "event-iterator": "^2.0.0", "eventsource-parser": "^3.0.0" } }, "sha512-ctgnDSQ26ZeZvdU/Zb1xxU97yMquKnhGiA0QblAIzKUmfi/3aYmWYHAwXwvEg0wtBrEtrjzme9pXgX/ACWnANA=="],
|
||||
|
||||
"@gitbook/browser-types": ["@gitbook/browser-types@workspace:packages/browser-types"],
|
||||
|
||||
@@ -2677,7 +2677,7 @@
|
||||
|
||||
"object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="],
|
||||
|
||||
"object-identity": ["object-identity@0.1.2", "", {}, "sha512-Px5puVllX5L2aBjbcfXpiG5xXeq6OE8RckryTeP2Zq+0PgYrCGJXmC6LblWgknKSJs11Je2W4U2NOWFj3t/QXQ=="],
|
||||
"object-identity": ["object-identity@0.2.1", "", {}, "sha512-itX2N4m0IvaLhFZMi9YkW4nFML6FshKCCpnn6iKJZVIkCNM/ZRaG4t1bzD2ncaW4r6cHJa59XY+7oZAH7tg8tA=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
|
||||
|
||||
@@ -2933,8 +2933,6 @@
|
||||
|
||||
"rolldown-plugin-dts": ["rolldown-plugin-dts@0.16.11", "", { "dependencies": { "@babel/generator": "^7.28.3", "@babel/parser": "^7.28.4", "@babel/types": "^7.28.4", "ast-kit": "^2.1.2", "birpc": "^2.6.1", "debug": "^4.4.3", "dts-resolver": "^2.1.2", "get-tsconfig": "^4.10.1", "magic-string": "^0.30.19" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.9", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-9IQDaPvPqTx3RjG2eQCK5GYZITo203BxKunGI80AGYicu1ySFTUyugicAaTZWRzFWh9DSnzkgNeMNbDWBbSs0w=="],
|
||||
|
||||
"rollup": ["rollup@3.29.5", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w=="],
|
||||
|
||||
"roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="],
|
||||
|
||||
"router": ["router@2.0.0", "", { "dependencies": { "array-flatten": "3.0.0", "is-promise": "4.0.0", "methods": "~1.1.2", "parseurl": "~1.3.3", "path-to-regexp": "^8.0.0", "setprototypeof": "1.2.0", "utils-merge": "1.0.1" } }, "sha512-dIM5zVoG8xhC6rnSN8uoAgFARwTE7BQs8YwHEvK0VCmfxQXMaOuA1uiR1IPwsW7JyK5iTt7Od/TC9StasS2NPQ=="],
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@
|
||||
"@tsconfig/strictest": "^2.0.6",
|
||||
"@tsconfig/node20": "^20.1.6",
|
||||
"@base-ui/react": "^1.7.0",
|
||||
"@gitbook/api": "0.204.0",
|
||||
"@gitbook/api": "0.206.0",
|
||||
"@scalar/api-client-react": "^1.3.46",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
|
||||
@@ -54,6 +54,7 @@ GitBook('configure', {
|
||||
icon: 'assistant' // 'assistant' | 'sparkle' | 'help' | 'book'
|
||||
},
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -107,6 +108,7 @@ frame.clearChat();
|
||||
// Configure the embed (see Configuration section for all options)
|
||||
frame.configure({
|
||||
tabs: ['assistant', 'search', 'docs'],
|
||||
defaultPage: '/getting-started',
|
||||
actions: [
|
||||
{
|
||||
icon: 'circle-question',
|
||||
@@ -142,6 +144,7 @@ import { GitBookProvider, GitBookFrame } from '@gitbook/embed/react';
|
||||
unsignedClaims: { userId: '123' } // Optional: custom claims for dynamic expressions
|
||||
}}
|
||||
tabs={['assistant', 'search', 'docs']}
|
||||
defaultPage="/getting-started"
|
||||
greeting={{ title: 'Welcome!', subtitle: 'How can I help?' }}
|
||||
assistantName="Support Assistant"
|
||||
suggestions={['What is GitBook?', 'How do I get started?']}
|
||||
@@ -249,6 +252,33 @@ Override which tabs are displayed. Defaults to your site's configuration.
|
||||
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`
|
||||
|
||||
Available in: Standalone script, NPM package, React components
|
||||
|
||||
@@ -62,6 +62,12 @@ export type GitBookEmbeddableConfiguration = {
|
||||
/** Tabs to display in the embed (if enabled on the site). */
|
||||
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. */
|
||||
actions: GitBookEmbeddableActionDefinition[];
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions = [],
|
||||
tools = [],
|
||||
tabs = ['assistant', 'search', 'docs'],
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
trademark = true,
|
||||
closeButton = false,
|
||||
assistantName,
|
||||
@@ -50,6 +52,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
useEffect(() => {
|
||||
gitbookFrame?.configure({
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
actions,
|
||||
greeting,
|
||||
suggestions,
|
||||
@@ -65,6 +69,8 @@ export function GitBookFrame(props: GitBookFrameProps) {
|
||||
suggestions,
|
||||
tools,
|
||||
tabs,
|
||||
defaultTab,
|
||||
defaultPage,
|
||||
closeButton,
|
||||
trademark,
|
||||
assistantName,
|
||||
|
||||
@@ -95,6 +95,8 @@ const nextConfig = {
|
||||
GITBOOK_BLOCK_SEARCH_INDEXATION: process.env.GITBOOK_BLOCK_SEARCH_INDEXATION,
|
||||
GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE: process.env.GITBOOK_ALLOW_CUSTOMIZATION_OVERRIDE,
|
||||
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_SERVER_ACTIONS_ENCRYPTION_KEY: process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY,
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nuqs": "^2.2.3",
|
||||
"object-hash": "^3.0.0",
|
||||
"object-identity": "^0.1.2",
|
||||
"object-identity": "^0.2.1",
|
||||
"openapi-types": "^12.1.3",
|
||||
"p-map": "^7.0.3",
|
||||
"p-retry": "^8.0.0",
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { CustomizationAIMode } from '@gitbook/api';
|
||||
import type { GitBookAssistant } from '@gitbook/browser-types';
|
||||
import { Icon, type IconName } from '@gitbook/icons';
|
||||
|
||||
import { useAIChatController, useAIChatState } from '.';
|
||||
import { type AskAITrigger, useAIChatController, useAIChatState } from '.';
|
||||
import { AIChatIcon, AISearchIcon, getAIChatName } from '../AIChat';
|
||||
import { useIntegrationAssistants } from '../Integrations';
|
||||
import { useSetSearchState } from '../Search/useSearch';
|
||||
@@ -26,7 +26,8 @@ export type AIConfig = {
|
||||
};
|
||||
};
|
||||
|
||||
export type Assistant = Omit<GitBookAssistant, 'icon'> & {
|
||||
export type Assistant = Omit<GitBookAssistant, 'icon' | 'open'> & {
|
||||
open: (query?: string, trigger?: AskAITrigger) => void;
|
||||
/**
|
||||
* Unique identifier for the assistant. Generated automatically using Crypto.randomUUID().
|
||||
* @example '123e4567-e89b-12d3-a456-426614174000'
|
||||
@@ -102,8 +103,8 @@ export function useAI(): AIContext {
|
||||
className="size-text-lg"
|
||||
/>
|
||||
),
|
||||
open: (query?: string) => {
|
||||
chatController.open();
|
||||
open: (query?: string, trigger?: AskAITrigger) => {
|
||||
chatController.open(trigger);
|
||||
if (query) {
|
||||
chatController.postMessage({ message: query });
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
type AIStreamResponseToolCallPending,
|
||||
type AIToolCallResult,
|
||||
SiteInsightsDisplayContext,
|
||||
type SiteInsightsEventAskView,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { getInsightsSession, useTrackEvent } from '../Insights';
|
||||
@@ -60,6 +61,8 @@ export type AIChatStatus =
|
||||
| 'error'
|
||||
| 'confirm';
|
||||
|
||||
export type AskAITrigger = NonNullable<SiteInsightsEventAskView['trigger']>;
|
||||
|
||||
export type AIChatState = {
|
||||
/**
|
||||
* If true, the chat is open.
|
||||
@@ -160,7 +163,7 @@ type AIChatEventListener = (input?: Omit<AIChatEvent, 'type'>) => void;
|
||||
|
||||
export type AIChatController = {
|
||||
/** Open the dialog */
|
||||
open: () => void;
|
||||
open: (trigger?: AskAITrigger) => void;
|
||||
/** Close the dialog */
|
||||
close: () => void;
|
||||
/** Post a message to the session */
|
||||
@@ -302,22 +305,28 @@ export function AIChatProvider(props: {
|
||||
const eventsRef = React.useRef<Map<AIChatEvent['type'], AIChatEventListener[]>>(new Map());
|
||||
|
||||
// Open AI chat and sync with search state
|
||||
const onOpen = React.useCallback(() => {
|
||||
setIntercomLauncherHidden(true);
|
||||
const onOpen = React.useCallback(
|
||||
(trigger?: AskAITrigger) => {
|
||||
setIntercomLauncherHidden(true);
|
||||
if (!renderMessageOptions?.asEmbeddable) {
|
||||
trackEvent({ type: 'ask_view', trigger });
|
||||
}
|
||||
|
||||
const { initialQuery } = globalState.getState();
|
||||
globalState.setState((state) => ({ ...state, opened: true }));
|
||||
const { initialQuery } = globalState.getState();
|
||||
globalState.setState((state) => ({ ...state, opened: true }));
|
||||
|
||||
// Update search state to show ask mode with first message or current ask value
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false, // Close search popover when opening chat
|
||||
}));
|
||||
// Update search state to show ask mode with first message or current ask value
|
||||
setSearchState((prev) => ({
|
||||
ask: prev?.ask ?? initialQuery ?? '',
|
||||
query: prev?.query ?? null,
|
||||
scope: prev?.scope ?? 'default',
|
||||
open: false, // Close search popover when opening chat
|
||||
}));
|
||||
|
||||
notify(eventsRef.current.get('open'), {});
|
||||
}, [setSearchState]);
|
||||
notify(eventsRef.current.get('open'), {});
|
||||
},
|
||||
[setSearchState, trackEvent, renderMessageOptions?.asEmbeddable]
|
||||
);
|
||||
|
||||
// Close AI chat and clear ask parameter
|
||||
const onClose = React.useCallback(() => {
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
EmbeddableFrameTitle,
|
||||
} from '../Embeddable/EmbeddableFrame';
|
||||
import { useIsMounted, useNow } from '../hooks';
|
||||
import { useTrackEvent } from '../Insights';
|
||||
import { Button } from '../primitives';
|
||||
import { ScrollContainer } from '../primitives/ScrollContainer';
|
||||
import { SideSheet } from '../primitives/SideSheet';
|
||||
@@ -68,16 +67,6 @@ 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 (
|
||||
<SideSheet
|
||||
side="right"
|
||||
|
||||
@@ -39,7 +39,7 @@ export function AIChatButton(props: {
|
||||
</div>
|
||||
}
|
||||
aria-label={tString(language, 'ai_chat_ask', assistant.label)}
|
||||
onClick={() => assistant.open()}
|
||||
onClick={() => assistant.open(undefined, 'site')}
|
||||
>
|
||||
{showLabel ? t(language, 'ask') : null}
|
||||
</Button>
|
||||
|
||||
+73
-19
@@ -1,29 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import fnv1a from '@sindresorhus/fnv1a';
|
||||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { AIChatIcon } from '../AIChatIcon';
|
||||
import { useAIChatController, useAIConfig } from '@/components/AI';
|
||||
import { Button } from '@/components/primitives';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
import { t, tString, useLanguage } from '@/intl/client';
|
||||
import { type ClassValue, tcls } from '@/lib/tailwind';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
/**
|
||||
* A small icon button revealed in the left margin of a top-level paragraph when it is hovered (on
|
||||
* devices with a fine pointer). Clicking it stages the paragraph's text as a reference and opens
|
||||
* the AI chat — a more discoverable variant of the text-selection "Ask" affordance.
|
||||
* A small icon button revealed in the left margin of a paragraph when it is hovered (on devices
|
||||
* with a fine pointer). Clicking it stages the paragraph's text as a reference and opens the AI
|
||||
* chat — a more discoverable variant of the text-selection "Ask" affordance.
|
||||
*
|
||||
* The button is absolutely positioned so it never affects the document flow, and its visibility is
|
||||
* driven purely by the `group/ask-ai` hover state of its paragraph wrapper.
|
||||
* A single instance is portaled into the last hovered paragraph wrapper, rather than rendering one
|
||||
* per paragraph, so pages with many paragraphs don't pay for markup that is hidden until hovered.
|
||||
* Its visibility is still driven purely by the `group/ask-ai` hover state of that wrapper.
|
||||
*/
|
||||
export function AskAIParagraphButton(props: { content: string; className?: ClassValue }) {
|
||||
const { content, className } = props;
|
||||
export function AskAIParagraphButton() {
|
||||
const config = useAIConfig();
|
||||
const language = useLanguage();
|
||||
const chatController = useAIChatController();
|
||||
const [target, setTarget] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
const enabled = isAIChatEnabled(config.aiMode);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onPointerOver = (event: PointerEvent) => {
|
||||
if (!(event.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const wrapper = event.target.closest<HTMLElement>('[data-ask-ai-paragraph]');
|
||||
if (wrapper) {
|
||||
setTarget(wrapper);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('pointerover', onPointerOver);
|
||||
return () => document.removeEventListener('pointerover', onPointerOver);
|
||||
}, [enabled]);
|
||||
|
||||
if (!enabled || !target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
const text = content.trim();
|
||||
const text = getParagraphText(target).trim();
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
@@ -33,12 +62,12 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
|
||||
id: `text-${fnv1a(text, { size: 32 })}`,
|
||||
content: text,
|
||||
});
|
||||
chatController.open();
|
||||
chatController.open('hover');
|
||||
chatController.setDraft(tString(language, 'ai_chat_paragraph_draft'));
|
||||
chatController.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
return createPortal(
|
||||
<div
|
||||
className={tcls(
|
||||
// Sit in the left margin, flush against the paragraph so there is no hover gap.
|
||||
@@ -52,13 +81,7 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
|
||||
'invisible opacity-0 transition-opacity duration-150',
|
||||
'hover:visible hover:opacity-100 group-hover/ask-ai:visible group-hover/ask-ai:opacity-100',
|
||||
// Never shown on touch / hover-less contexts.
|
||||
'not-pointer-fine:hidden',
|
||||
// Hidden where an overflow-clipped ancestor would cut it off (ARIA and native
|
||||
// tables, record cards).
|
||||
'in-[[role=table]]:hidden',
|
||||
'in-[table]:hidden',
|
||||
'in-[[data-card]]:hidden',
|
||||
className
|
||||
'not-pointer-fine:hidden'
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
@@ -75,6 +98,37 @@ export function AskAIParagraphButton(props: { content: string; className?: Class
|
||||
'in-[.hint.bg-danger]:bg-danger in-[.hint.bg-info]:bg-info in-[.hint.bg-success]:bg-success in-[.hint.bg-warning]:bg-warning'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
target
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the paragraph's text, skipping rendered math like `getNodeText` does.
|
||||
*/
|
||||
function getParagraphText(wrapper: HTMLElement): string {
|
||||
const paragraph = wrapper.querySelector(':scope > p');
|
||||
if (!paragraph) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
paragraph,
|
||||
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
||||
{
|
||||
acceptNode: (node) =>
|
||||
node instanceof Element &&
|
||||
node.matches('.katex, mjx-container, [aria-hidden="true"]')
|
||||
? NodeFilter.FILTER_REJECT
|
||||
: NodeFilter.FILTER_ACCEPT,
|
||||
}
|
||||
);
|
||||
|
||||
let text = '';
|
||||
while (walker.nextNode()) {
|
||||
if (walker.currentNode.nodeType === Node.TEXT_NODE) {
|
||||
text += walker.currentNode.textContent ?? '';
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function AskAITextSelection() {
|
||||
id: `text-${fnv1a(content, { size: 32 })}`,
|
||||
content,
|
||||
});
|
||||
chatController.open();
|
||||
chatController.open('selection');
|
||||
chatController.focus();
|
||||
clear();
|
||||
};
|
||||
|
||||
@@ -135,10 +135,10 @@ const HINT_STYLES: {
|
||||
header: 'bg-warning-active',
|
||||
body: [
|
||||
'text-neutral-strong',
|
||||
'links-default:[&_a]:text-warning',
|
||||
'links-default:[&_a:hover]:text-warning-strong',
|
||||
'links-default:[&_a]:decoration-warning/6',
|
||||
'links-accent:[&_a]:decoration-warning',
|
||||
'links-default:[&_a:not(.button)]:text-warning',
|
||||
'links-default:[&_a:not(.button):hover]:text-warning-strong',
|
||||
'links-default:[&_a:not(.button)]:decoration-warning/6',
|
||||
'links-accent:[&_a:not(.button)]:decoration-warning',
|
||||
'decoration-warning/6',
|
||||
'[&_.can-override-bg]:bg-warning-active',
|
||||
'[&_.can-override-text]:text-warning-strong',
|
||||
@@ -152,10 +152,10 @@ const HINT_STYLES: {
|
||||
header: 'bg-danger-active',
|
||||
body: [
|
||||
'text-neutral-strong',
|
||||
'links-default:[&_a]:text-danger',
|
||||
'links-default:[&_a:hover]:text-danger-strong',
|
||||
'links-default:[&_a]:decoration-danger/6',
|
||||
'links-accent:[&_a]:decoration-danger',
|
||||
'links-default:[&_a:not(.button)]:text-danger',
|
||||
'links-default:[&_a:not(.button):hover]:text-danger-strong',
|
||||
'links-default:[&_a:not(.button)]:decoration-danger/6',
|
||||
'links-accent:[&_a:not(.button)]:decoration-danger',
|
||||
'decoration-danger/6',
|
||||
'[&_.can-override-bg]:bg-danger-active',
|
||||
'[&_.can-override-text]:text-danger-strong',
|
||||
@@ -169,10 +169,10 @@ const HINT_STYLES: {
|
||||
header: 'bg-success-active',
|
||||
body: [
|
||||
'text-neutral-strong',
|
||||
'links-default:[&_a]:text-success',
|
||||
'links-default:[&_a:hover]:text-success-strong',
|
||||
'links-default:[&_a]:decoration-success/6',
|
||||
'links-accent:[&_a]:decoration-success',
|
||||
'links-default:[&_a:not(.button)]:text-success',
|
||||
'links-default:[&_a:not(.button):hover]:text-success-strong',
|
||||
'links-default:[&_a:not(.button)]:decoration-success/6',
|
||||
'links-accent:[&_a:not(.button)]:decoration-success',
|
||||
'decoration-success/6',
|
||||
'[&_.can-override-bg]:bg-success-active',
|
||||
'[&_.can-override-text]:text-success-strong',
|
||||
|
||||
@@ -13,9 +13,14 @@ const INPUT_SIZE_MAP: Record<NonNullable<ButtonProps['size']>, 'small' | 'medium
|
||||
};
|
||||
|
||||
export function InlineActionButton(
|
||||
props: { action: 'ask' | 'search'; query?: string } & { buttonProps: ButtonProps } // TODO: Type this properly: Pick<api.DocumentInlineButton, 'action' | 'query'> & { buttonProps: ButtonProps }
|
||||
props: {
|
||||
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, buttonProps } = props;
|
||||
const { action, query, scope, buttonProps } = props;
|
||||
|
||||
const { assistants } = useAI();
|
||||
const chatState = useAIChatState();
|
||||
@@ -29,7 +34,7 @@ export function InlineActionButton(
|
||||
setSearchState((prev) => ({
|
||||
...prev,
|
||||
ask: null,
|
||||
scope: 'default',
|
||||
scope: scope === 'space' ? 'current' : 'default',
|
||||
query: value ?? query,
|
||||
open: true,
|
||||
}));
|
||||
|
||||
@@ -47,6 +47,11 @@ export function InlineButton(props: InlineProps<api.DocumentInlineButton>) {
|
||||
<InlineActionButton
|
||||
action={inline.data.action.action}
|
||||
query={inline.data.action.query ?? ''}
|
||||
scope={
|
||||
inline.data.action.action === 'search'
|
||||
? inline.data.action.scope
|
||||
: undefined
|
||||
}
|
||||
buttonProps={buttonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -52,6 +52,14 @@
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.contentkit-webframe-container {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.contentkit-webframe-loading {
|
||||
@apply absolute inset-0 m-auto size-5 animate-spin;
|
||||
}
|
||||
|
||||
/** Card */
|
||||
.contentkit-card {
|
||||
@apply flex flex-col border rounded border-tint-subtle;
|
||||
|
||||
@@ -4,12 +4,11 @@ import { CustomizationAIMode } from '@gitbook/api';
|
||||
import type { BlockProps } from './Block';
|
||||
import { Inlines } from './Inlines';
|
||||
import { getTextAlignment } from './utils';
|
||||
import { AskAIParagraphButton } from '@/components/AIChat/AskAIParagraphButton';
|
||||
import { getNodeText } from '@/lib/document';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
const { block, style, ...contextProps } = props;
|
||||
const { block, style, ancestorBlocks, ...contextProps } = props;
|
||||
const { context } = contextProps;
|
||||
|
||||
const paragraph = (
|
||||
@@ -28,6 +27,8 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
const contentContext = context.contentContext;
|
||||
const aiAssistantEnabled =
|
||||
context.mode !== 'print' &&
|
||||
// Table cells would clip the margin button, and big tables hold thousands of paragraphs.
|
||||
!ancestorBlocks.some((ancestor) => ancestor.type === 'table') &&
|
||||
contentContext != null &&
|
||||
'customization' in contentContext &&
|
||||
contentContext.customization.ai.mode === CustomizationAIMode.Assistant;
|
||||
@@ -38,6 +39,8 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
// `self-center`/`self-end`) — otherwise centered paragraphs pin left on wide/no-TOC pages.
|
||||
return (
|
||||
<div
|
||||
// The shared `AskAIParagraphButton` portals itself into the hovered wrapper.
|
||||
data-ask-ai-paragraph=""
|
||||
className={tcls(
|
||||
'group/ask-ai relative',
|
||||
style,
|
||||
@@ -45,7 +48,6 @@ export function Paragraph(props: BlockProps<DocumentBlockParagraph>) {
|
||||
)}
|
||||
>
|
||||
{paragraph}
|
||||
<AskAIParagraphButton content={text} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
tag: Tag = 'div',
|
||||
ariaLabelledBy,
|
||||
block,
|
||||
ancestorBlocks,
|
||||
document,
|
||||
record,
|
||||
column,
|
||||
@@ -151,7 +152,7 @@ export async function RecordColumnValue<Tag extends React.ElementType = 'div'>(
|
||||
<Blocks
|
||||
tag={Tag}
|
||||
document={document}
|
||||
ancestorBlocks={[]}
|
||||
ancestorBlocks={[...ancestorBlocks, block]}
|
||||
nodes={fragment.nodes}
|
||||
style={[
|
||||
'blocks',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect, it } from 'bun:test';
|
||||
|
||||
import type { DocumentBlockParagraph, JSONDocument } from '@gitbook/api';
|
||||
import type { DocumentBlockParagraph, DocumentBlockTable, JSONDocument } from '@gitbook/api';
|
||||
|
||||
import { isBlockOffscreen } from './isBlockOffscreen';
|
||||
|
||||
@@ -42,3 +42,31 @@ it('should return true if block is offscreen', () => {
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should treat content in table cells as offscreen', () => {
|
||||
const cell: DocumentBlockParagraph = {
|
||||
object: 'block',
|
||||
type: 'paragraph',
|
||||
nodes: [],
|
||||
};
|
||||
|
||||
const table: DocumentBlockTable = {
|
||||
object: 'block',
|
||||
type: 'table',
|
||||
isVoid: true,
|
||||
data: {
|
||||
view: { type: 'cards', cardSize: 'medium', columns: ['column'] },
|
||||
records: {},
|
||||
definition: {},
|
||||
},
|
||||
fragments: [{ object: 'fragment', fragment: 'cell', nodes: [cell] }],
|
||||
};
|
||||
|
||||
const document: JSONDocument = {
|
||||
object: 'document',
|
||||
nodes: [table],
|
||||
data: {},
|
||||
};
|
||||
|
||||
expect(isBlockOffscreen({ document, block: cell, ancestorBlocks: [table] })).toBe(true);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,11 @@ export function isBlockOffscreen(
|
||||
},
|
||||
screenHeight = 500
|
||||
): boolean {
|
||||
// Cell content lives in fragments, not table nodes, so its row position can't be estimated.
|
||||
if (ancestorBlocks.some((ancestor) => ancestor.type === 'table')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let current = block;
|
||||
|
||||
const allAncestors = [document, ...ancestorBlocks];
|
||||
|
||||
@@ -61,9 +61,7 @@ export function EmbeddableAIChat(props: EmbeddableAIChatProps) {
|
||||
}
|
||||
|
||||
trackEvent(
|
||||
{
|
||||
type: 'ask_view',
|
||||
},
|
||||
{ type: 'ask_view', trigger: 'embed' },
|
||||
{
|
||||
pageId: null,
|
||||
displayContext: api.SiteInsightsDisplayContext.Embed,
|
||||
|
||||
@@ -97,6 +97,7 @@ export async function EmbeddableDocsPage(
|
||||
) ?? context.siteSpace
|
||||
}
|
||||
siteSpaces={variants.translations}
|
||||
asEmbeddable
|
||||
className="my-1.5 ml-2 self-start"
|
||||
/>
|
||||
) : null}
|
||||
@@ -125,6 +126,7 @@ export async function EmbeddableDocsPage(
|
||||
) ?? context.siteSpace
|
||||
}
|
||||
siteSpaces={variants.translations}
|
||||
asEmbeddable
|
||||
className="max-md:[&_.button-content]:block"
|
||||
/>
|
||||
) : null
|
||||
@@ -137,6 +139,7 @@ export async function EmbeddableDocsPage(
|
||||
context={context}
|
||||
siteSpace={context.siteSpace}
|
||||
siteSpaces={variants.generic}
|
||||
asEmbeddable
|
||||
className="w-full px-3"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { GitBookEmbeddableConfiguration, ParentToFrameMessage } from '@gitb
|
||||
import { integrationsAssistantTools } from '../Integrations';
|
||||
import { Button, LinkContext, type LinkContextType } from '../primitives';
|
||||
import { getChannel } from './channel';
|
||||
import { DEFAULT_EMBEDDABLE_TABS, getEmbeddableLandingTab } from './getEmbeddableLandingTab';
|
||||
import { resolveEmbedPageLink } from './server-actions';
|
||||
import { useAI, useAIChatController } from '@/components/AI';
|
||||
import { isAIChatEnabled } from '@/components/utils/isAIChatEnabled';
|
||||
@@ -23,6 +24,17 @@ const embeddableConfiguration = createStore<GitBookEmbeddableConfiguration>(() =
|
||||
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
|
||||
function log(...data: any[]) {
|
||||
// oxlint-disable-next-line no-console
|
||||
@@ -66,6 +78,15 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
const { baseURL, router, chatController } = refs.current;
|
||||
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);
|
||||
|
||||
switch (message.type) {
|
||||
@@ -83,29 +104,43 @@ export function EmbeddableIframeAPI(props: { baseURL: string }) {
|
||||
break;
|
||||
}
|
||||
case 'configure': {
|
||||
embeddableConfiguration.setState(message.settings);
|
||||
const { settings } = message;
|
||||
embeddableConfiguration.setState(settings);
|
||||
integrationsAssistantTools.setState({
|
||||
tools: message.settings.tools,
|
||||
tools: 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;
|
||||
}
|
||||
case 'navigateToPage': {
|
||||
// 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}`));
|
||||
navigate(resolvePageHref(message.pagePath, baseURL));
|
||||
break;
|
||||
}
|
||||
case 'navigateToAssistant': {
|
||||
@@ -132,7 +167,7 @@ export function useEmbeddableConfiguration<T = GitBookEmbeddableConfiguration>(
|
||||
|
||||
export function useEmbeddableTabs() {
|
||||
const configuredTabs = useEmbeddableConfiguration((state) => state.tabs);
|
||||
return configuredTabs.length > 0 ? configuredTabs : ['assistant', 'search', 'docs'];
|
||||
return configuredTabs.length > 0 ? configuredTabs : DEFAULT_EMBEDDABLE_TABS;
|
||||
}
|
||||
|
||||
export function useEmbeddableLinkContext() {
|
||||
@@ -200,6 +235,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
const { ref, active = 'assistant', baseURL, siteTitle, onNavigate } = props;
|
||||
const actions = useEmbeddableConfiguration((state) => state.actions);
|
||||
const tabs = useEmbeddableTabs();
|
||||
const docsHomeHref = useStore(docsHome, (state) => state.href);
|
||||
|
||||
const { assistants, config } = useAI();
|
||||
const language = useLanguage();
|
||||
@@ -228,7 +264,7 @@ export function EmbeddableIframeTabs(props: {
|
||||
key: 'docs',
|
||||
label: siteTitle,
|
||||
icon: 'book-open',
|
||||
href: `${baseURL}/page/`,
|
||||
href: docsHomeHref ?? `${baseURL}/page/`,
|
||||
}
|
||||
: null,
|
||||
].filter((tab) => tab !== null);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CustomizationDefaultThemeMode } from '@gitbook/api';
|
||||
import { SiteInsightsTrademarkPlacement } from '@gitbook/api';
|
||||
|
||||
import { AskAIParagraphButton } from '../AIChat';
|
||||
import { LoadIntegrations } from '../Integrations';
|
||||
import { NavigationLoader } from '../primitives/NavigationLoader';
|
||||
import { SpaceLayoutServerContext } from '../SpaceLayout';
|
||||
@@ -77,6 +78,7 @@ export async function EmbeddableRootLayout({
|
||||
}}
|
||||
>
|
||||
<NavigationLoader />
|
||||
<AskAIParagraphButton />
|
||||
<VisibilityProvider className="fixed inset-0 flex flex-col">
|
||||
{children}
|
||||
{context.customization.trademark.enabled ? (
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
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,9 +2,10 @@ import type { SiteSpace } from '@gitbook/api';
|
||||
import type { IconName } from '@gitbook/icons';
|
||||
|
||||
import type { ButtonProps } from '../primitives';
|
||||
import { getTargetURLForSiteSpace } from '../SiteSections/encodeClientSiteSections';
|
||||
import { SpacesDropdownClient } from './SpacesDropdownClient';
|
||||
import type { GitBookSiteContext } from '@/lib/context';
|
||||
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
|
||||
// Memoized regex for checking if a string starts with an emoji
|
||||
@@ -20,9 +21,18 @@ export function SpacesDropdown(
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
/** Keep the links inside the embed rather than sending readers to the full site. */
|
||||
asEmbeddable?: boolean;
|
||||
} & ButtonProps
|
||||
) {
|
||||
const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
|
||||
const {
|
||||
context,
|
||||
siteSpace,
|
||||
siteSpaces,
|
||||
className,
|
||||
asEmbeddable = false,
|
||||
...buttonProps
|
||||
} = props;
|
||||
const currentLanguage = context.locale;
|
||||
|
||||
const dropdownClassName = tcls(
|
||||
@@ -33,7 +43,7 @@ export function SpacesDropdown(
|
||||
const slimSpaces = siteSpaces.map((siteSp) => ({
|
||||
id: siteSp.id,
|
||||
title: getLocalizedTitle(siteSp, currentLanguage),
|
||||
url: getSiteSpaceURL(context, siteSp),
|
||||
url: getTargetURLForSiteSpace(context, siteSp, asEmbeddable),
|
||||
isActive: siteSp.id === siteSpace.id,
|
||||
spaceId: siteSp.space.id,
|
||||
}));
|
||||
@@ -57,9 +67,10 @@ export function TranslationsDropdown(
|
||||
siteSpace: SiteSpace;
|
||||
siteSpaces: SiteSpace[];
|
||||
className?: string;
|
||||
asEmbeddable?: boolean;
|
||||
} & ButtonProps
|
||||
) {
|
||||
const { context, siteSpace, siteSpaces, className, ...buttonProps } = props;
|
||||
const { context, siteSpace, siteSpaces, className, asEmbeddable, ...buttonProps } = props;
|
||||
|
||||
const title = getLocalizedTitle(siteSpace, context.locale);
|
||||
const hasEmojiPrefix = startsWithEmoji(title);
|
||||
@@ -70,6 +81,7 @@ export function TranslationsDropdown(
|
||||
context={context}
|
||||
siteSpace={siteSpace}
|
||||
siteSpaces={siteSpaces}
|
||||
asEmbeddable={asEmbeddable}
|
||||
variant="blank"
|
||||
className={tcls(
|
||||
'bg-transparent lg:max-w-64 max-md:[&_.button-content]:hidden',
|
||||
|
||||
@@ -67,7 +67,7 @@ export function ActionOpenAssistant(props: {
|
||||
href: page.href,
|
||||
});
|
||||
}
|
||||
assistant.open();
|
||||
assistant.open(undefined, 'page');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { JSONDocument, RevisionPageDocument, SiteInsightsDisplayContext } from '@gitbook/api';
|
||||
import {
|
||||
type JSONDocument,
|
||||
type RevisionPageDocument,
|
||||
SiteInsightsDisplayContext,
|
||||
} from '@gitbook/api';
|
||||
|
||||
import { DocumentView, DocumentViewSkeleton } from '../DocumentView';
|
||||
import { CurrentPageProvider } from '../hooks/useCurrentPage';
|
||||
@@ -127,6 +131,7 @@ export async function PageBody(props: {
|
||||
ancestors={ancestors}
|
||||
withRSSFeed={contentHasUpdates}
|
||||
hasAPIBlocks={hasAPIBlocks}
|
||||
asEmbeddable={insightsDisplayContext === SiteInsightsDisplayContext.Embed}
|
||||
/>
|
||||
{document && !isNodeEmpty(document) ? (
|
||||
<OptionalSuspense
|
||||
|
||||
@@ -19,14 +19,17 @@ import {
|
||||
import { PageAsideToggleButton } from '../PageAside/PageAsideButton';
|
||||
import { PageIcon } from '../PageIcon';
|
||||
import { getPDFURLSearchParams } from '../PDF';
|
||||
import { findBestTargetURL } from '../SiteSections/encodeClientSiteSections';
|
||||
import {
|
||||
findBestTargetURL,
|
||||
getTargetURLForSiteSpace,
|
||||
} from '../SiteSections/encodeClientSiteSections';
|
||||
import { categorizeVariants } from '../SpaceLayout/categorizeVariants';
|
||||
import { BreadcrumbItemDropdown, type BreadcrumbSibling } from './BreadcrumbItemDropdown';
|
||||
import { PageTags } from './PageTags';
|
||||
import type { GitBookSiteContext, SiteStructureNode } from '@/lib/context';
|
||||
import { hasAdaptiveMcpEndpoint } from '@/lib/mcp/endpoints';
|
||||
import { type AncestorRevisionPage, resolveFirstDocument } from '@/lib/pages';
|
||||
import { getLocalizedTitle, getSiteSpaceURL } from '@/lib/sites';
|
||||
import { getLocalizedTitle } from '@/lib/sites';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getPageRSSURL } from '@/routes/rss';
|
||||
|
||||
@@ -41,8 +44,10 @@ export async function PageHeader(props: {
|
||||
* exactly as-is.
|
||||
*/
|
||||
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 } = props;
|
||||
const { context, page, ancestors, withRSSFeed, hasAPIBlocks, asEmbeddable } = props;
|
||||
const { revision, linker } = context;
|
||||
|
||||
const hasAncestors = ancestors.length > 0;
|
||||
@@ -82,7 +87,10 @@ export async function PageHeader(props: {
|
||||
// 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
|
||||
// the reader on their current variant (same logic as the section tabs).
|
||||
href: node.object === 'site-section' ? findBestTargetURL(context, node) : undefined,
|
||||
href:
|
||||
node.object === 'site-section'
|
||||
? findBestTargetURL(context, node, asEmbeddable)
|
||||
: undefined,
|
||||
label: getLocalizedTitle(node, context.locale),
|
||||
icon: node.icon,
|
||||
siblings: siblings
|
||||
@@ -92,7 +100,7 @@ export async function PageHeader(props: {
|
||||
// Don't offer hidden sections/groups as switch targets.
|
||||
.filter((sibling) => !visibleSectionIds || visibleSectionIds.has(sibling.id))
|
||||
.map((sibling) => {
|
||||
const href = getSectionNodeURL(context, sibling);
|
||||
const href = getSectionNodeURL(context, sibling, asEmbeddable);
|
||||
// Keep siblings whose URL is "" (the site's first page); only drop the ones
|
||||
// with no resolvable URL at all.
|
||||
return href !== undefined
|
||||
@@ -112,7 +120,7 @@ export async function PageHeader(props: {
|
||||
if (currentSiteSpace) {
|
||||
contextCrumbs.push({
|
||||
key: `variant-${currentSiteSpace.id}`,
|
||||
href: getSiteSpaceURL(context, currentSiteSpace),
|
||||
href: getTargetURLForSiteSpace(context, currentSiteSpace, asEmbeddable),
|
||||
label: getLocalizedTitle(currentSiteSpace, context.locale),
|
||||
siblings: [],
|
||||
// Reuse the header's variant switcher rather than plain per-variant links, so each entry
|
||||
@@ -122,7 +130,7 @@ export async function PageHeader(props: {
|
||||
slimSpaces: variantSpaces.map((siteSpace) => ({
|
||||
id: siteSpace.id,
|
||||
title: getLocalizedTitle(siteSpace, context.locale),
|
||||
url: getSiteSpaceURL(context, siteSpace),
|
||||
url: getTargetURLForSiteSpace(context, siteSpace, asEmbeddable),
|
||||
isActive: siteSpace.id === currentSiteSpace.id,
|
||||
spaceId: siteSpace.space.id,
|
||||
})),
|
||||
@@ -448,9 +456,13 @@ function collectSectionNodeIds(list: SiteStructureNode[]): Set<string> {
|
||||
* 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.
|
||||
*/
|
||||
function getSectionNodeURL(context: GitBookSiteContext, node: SectionNode): string | undefined {
|
||||
function getSectionNodeURL(
|
||||
context: GitBookSiteContext,
|
||||
node: SectionNode,
|
||||
asEmbeddable: boolean
|
||||
): string | undefined {
|
||||
const section = findFirstSection(node);
|
||||
return section ? findBestTargetURL(context, section) : undefined;
|
||||
return section ? findBestTargetURL(context, section, asEmbeddable) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -284,11 +284,11 @@ export async function getSitePageData(props: SitePageProps) {
|
||||
if (pathname !== null) {
|
||||
// If the pathname was not normalized, redirect to the normalized version
|
||||
// before trying to resolve the page again
|
||||
redirect(context.linker.toPathInSpace(pathname));
|
||||
redirect(context.linker.toPathForPagePath({ path: pathname }));
|
||||
} else {
|
||||
// If the page is not found and we are in fallback mode, return a redirect to the basepath
|
||||
if (context.isFallback) {
|
||||
redirect(context.linker.toPathInSpace('/'));
|
||||
redirect(context.linker.toPathForPagePath({ path: '/' }));
|
||||
}
|
||||
notFound();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useSetSearchState } from '../Search';
|
||||
import { fetchSiteIndex } from '../Search/site-index';
|
||||
import { SiteAuthLoginButton } from '../SiteAuth/SiteAuthLoginLink';
|
||||
import {
|
||||
useIsLoggedInVisitor,
|
||||
useSiteAdaptiveAuthLoginHref,
|
||||
useSiteIndexURL,
|
||||
useSpaceBasePath,
|
||||
@@ -44,6 +45,9 @@ export function SitePageNotFound() {
|
||||
const setSearchState = useSetSearchState();
|
||||
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).
|
||||
const assistant = assistants.find((candidate) => candidate.mode !== 'search') ?? null;
|
||||
const inputLabel = tString(language, assistant ? 'search_or_ask' : 'search');
|
||||
@@ -124,21 +128,20 @@ export function SitePageNotFound() {
|
||||
'text-tint-strong'
|
||||
)}
|
||||
>
|
||||
{t(
|
||||
language,
|
||||
adaptiveAuthLoginHref
|
||||
? 'notfound_adaptive_title'
|
||||
: 'notfound_title'
|
||||
)}
|
||||
{t(language, copy.title)}
|
||||
</h1>
|
||||
<p className={tcls('text-base', 'text-tint')}>
|
||||
{t(
|
||||
language,
|
||||
adaptiveAuthLoginHref ? 'notfound_adaptive' : 'notfound'
|
||||
)}
|
||||
<p className={tcls('text-base', 'text-tint', 'text-center')}>
|
||||
{t(language, copy.message)}
|
||||
</p>
|
||||
</div>
|
||||
{adaptiveAuthLoginHref ? (
|
||||
{adaptiveAuthLoginHref && isAuthenticated ? (
|
||||
// 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">
|
||||
<SiteAuthLoginButton
|
||||
href={adaptiveAuthLoginHref}
|
||||
@@ -190,6 +193,19 @@ 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
|
||||
* there are no suggestions.
|
||||
|
||||
@@ -201,7 +201,7 @@ function getTargetURLForSection(
|
||||
return getSectionURL(context, section);
|
||||
}
|
||||
|
||||
function getTargetURLForSiteSpace(
|
||||
export function getTargetURLForSiteSpace(
|
||||
context: GitBookSiteContext,
|
||||
siteSpace: SiteSpace,
|
||||
asEmbeddable: boolean
|
||||
|
||||
@@ -5,7 +5,7 @@ import { CustomizationHeaderPreset, CustomizationSearchStyle } from '@gitbook/ap
|
||||
import { AdaptiveVisitorContextProvider } from '../Adaptive';
|
||||
import { AIChatProvider } from '../AI';
|
||||
import type { RenderAIMessageOptions } from '../AI';
|
||||
import { AIChat, AskAITextSelection } from '../AIChat';
|
||||
import { AIChat, AskAIParagraphButton, AskAITextSelection } from '../AIChat';
|
||||
import { Announcement } from '../Announcement';
|
||||
import { SpacesDropdown, TranslationsDropdown } from '../Header/SpacesDropdown';
|
||||
import { CurrentContentProvider } from '../hooks';
|
||||
@@ -72,6 +72,7 @@ export function SpaceLayoutServerContext(props: SpaceLayoutProps) {
|
||||
<SpaceLayoutContextProvider
|
||||
basePath={context.linker.toPathInSpace('')}
|
||||
siteAdaptiveAuthLoginHref={siteAdaptiveAuthLoginHref}
|
||||
isLoggedInVisitor={context.isLoggedInVisitor}
|
||||
siteIndexURL={context.linker.toPathInSite('~gitbook/site-index')}
|
||||
>
|
||||
<AdaptiveVisitorContextProvider
|
||||
@@ -137,6 +138,7 @@ export function SpaceLayout(props: SpaceLayoutProps) {
|
||||
<>
|
||||
<AIChat />
|
||||
<AskAITextSelection />
|
||||
<AskAIParagraphButton />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
const SpaceLayoutContext = React.createContext({
|
||||
basePath: '',
|
||||
siteAdaptiveAuthLoginHref: null as string | null,
|
||||
isLoggedInVisitor: false,
|
||||
siteIndexURL: '',
|
||||
});
|
||||
|
||||
@@ -15,14 +16,21 @@ export function SpaceLayoutContextProvider(
|
||||
props: React.PropsWithChildren<{
|
||||
basePath: string;
|
||||
siteAdaptiveAuthLoginHref?: string | null;
|
||||
isLoggedInVisitor?: boolean;
|
||||
siteIndexURL: string;
|
||||
}>
|
||||
) {
|
||||
const { basePath, siteAdaptiveAuthLoginHref = null, siteIndexURL, children } = props;
|
||||
const {
|
||||
basePath,
|
||||
siteAdaptiveAuthLoginHref = null,
|
||||
isLoggedInVisitor = false,
|
||||
siteIndexURL,
|
||||
children,
|
||||
} = props;
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({ basePath, siteAdaptiveAuthLoginHref, siteIndexURL }),
|
||||
[basePath, siteAdaptiveAuthLoginHref, siteIndexURL]
|
||||
() => ({ basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL }),
|
||||
[basePath, siteAdaptiveAuthLoginHref, isLoggedInVisitor, siteIndexURL]
|
||||
);
|
||||
|
||||
return <SpaceLayoutContext.Provider value={value}>{children}</SpaceLayoutContext.Provider>;
|
||||
@@ -50,6 +58,17 @@ export function useSiteAdaptiveAuthLoginHref() {
|
||||
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`).
|
||||
*/
|
||||
|
||||
@@ -100,11 +100,19 @@ export function ScrollContainer(props: ScrollContainerProps) {
|
||||
}
|
||||
|
||||
let frame = 0;
|
||||
let previousActiveItems = new Set(container.querySelectorAll(active));
|
||||
// Active items can mount only after a collapsed group expands.
|
||||
const observer = new MutationObserver(() => {
|
||||
cancelAnimationFrame(frame);
|
||||
frame = requestAnimationFrame(() => {
|
||||
for (const activeItem of container.querySelectorAll(active)) {
|
||||
const activeItems = new Set(container.querySelectorAll(active));
|
||||
const newActiveItems = [...activeItems].filter(
|
||||
(item) => !previousActiveItems.has(item)
|
||||
);
|
||||
previousActiveItems = activeItems;
|
||||
// Only follow newly active items so unrelated changes (e.g. expanding another
|
||||
// group) don't pull the user back to the current page.
|
||||
for (const activeItem of newActiveItems) {
|
||||
if (!isElementFullyVisibleInContainer(activeItem, container)) {
|
||||
scrollToElementInContainer(activeItem, container, 'smooth');
|
||||
return;
|
||||
|
||||
@@ -77,10 +77,14 @@ export const ar: TranslationLanguage = {
|
||||
notfound_title: 'الصفحة غير موجودة',
|
||||
notfound_link: 'يشير هذا الرابط إلى صفحة تمت إزالتها أو لم تعد موجودة.',
|
||||
notfound: 'الصفحة التي تبحث عنها غير موجودة.',
|
||||
notfound_adaptive_title: 'الصفحة غير متاحة',
|
||||
notfound_adaptive: 'قد تكون هذه الصفحة موجودة، لكن قد تحتاج إلى تسجيل الدخول للوصول إليها.',
|
||||
notfound_adaptive_title: 'سجّل الدخول لعرض هذه الصفحة',
|
||||
notfound_adaptive: 'قد تتطلب هذه الصفحة تسجيل الدخول. إذا اتبعت رابطًا، فتحقق من صحته.',
|
||||
notfound_adaptive_login: 'تسجيل الدخول',
|
||||
notfound_adaptive_registration_hint: 'قد يكون التسجيل متاحًا حسب المؤسسة.',
|
||||
notfound_adaptive_no_access_title: 'قد لا تملك صلاحية الوصول إلى هذه الصفحة',
|
||||
notfound_adaptive_no_access:
|
||||
'قد لا تكون هذه الصفحة موجودة، أو قد لا يملك حسابك صلاحية الوصول إليها. إذا اتبعت رابطًا، فتحقق من صحته.',
|
||||
notfound_adaptive_login_again: 'تسجيل الدخول مجددًا',
|
||||
notfound_goto_home: 'الانتقال إلى الصفحة الرئيسية',
|
||||
notfound_suggestions_title: 'ربما تبحث عن',
|
||||
unexpected_error_title: 'حدث خطأ',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const bg: TranslationLanguage = {
|
||||
notfound_title: 'Страницата не е намерена',
|
||||
notfound_link: 'Тази връзка сочи към страница, която е премахната или вече не съществува.',
|
||||
notfound: 'Страницата, която търсите, не съществува.',
|
||||
notfound_adaptive_title: 'Страницата не е налична',
|
||||
notfound_adaptive_title: 'Влезте, за да видите тази страница',
|
||||
notfound_adaptive:
|
||||
'Тази страница може да съществува, но може да трябва да влезете, за да я достъпите.',
|
||||
'Тази страница може да изисква вход. Ако сте последвали връзка, проверете дали е правилна.',
|
||||
notfound_adaptive_login: 'Вход',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Регистрацията може да е възможна в зависимост от организацията.',
|
||||
notfound_adaptive_no_access_title: 'Възможно е да нямате достъп до тази страница',
|
||||
notfound_adaptive_no_access:
|
||||
'Тази страница може да не съществува или профилът ви може да няма достъп до нея. Ако сте последвали връзка, проверете дали е правилна.',
|
||||
notfound_adaptive_login_again: 'Повторен вход',
|
||||
notfound_goto_home: 'Към началната страница',
|
||||
notfound_suggestions_title: 'Може би търсите',
|
||||
unexpected_error_title: 'Възникна грешка',
|
||||
|
||||
@@ -77,11 +77,15 @@ export const cs: TranslationLanguage = {
|
||||
notfound_title: 'Stránka nenalezena',
|
||||
notfound_link: 'Tento odkaz vede na stránku, která byla odstraněna nebo již neexistuje.',
|
||||
notfound: 'Stránka, kterou hledáte, neexistuje.',
|
||||
notfound_adaptive_title: 'Stránka není dostupná',
|
||||
notfound_adaptive_title: 'Pro zobrazení této stránky se přihlaste',
|
||||
notfound_adaptive:
|
||||
'Tato stránka může existovat, ale pro přístup se možná budete muset přihlásit.',
|
||||
'Tato stránka může vyžadovat přihlášení. Pokud jste přešli z odkazu, zkontrolujte, zda je správný.',
|
||||
notfound_adaptive_login: 'Přihlásit se',
|
||||
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_suggestions_title: 'Možná hledáte',
|
||||
unexpected_error_title: 'Došlo k chybě',
|
||||
|
||||
@@ -77,11 +77,15 @@ export const da: TranslationLanguage = {
|
||||
notfound_title: 'Siden blev ikke fundet',
|
||||
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_adaptive_title: 'Siden er ikke tilgængelig',
|
||||
notfound_adaptive_title: 'Log ind for at se denne side',
|
||||
notfound_adaptive:
|
||||
'Denne side findes muligvis, men du skal muligvis logge ind for at få adgang.',
|
||||
'Denne side kræver muligvis, at du logger ind. Hvis du har fulgt et link, så tjek, at det er korrekt.',
|
||||
notfound_adaptive_login: 'Log ind',
|
||||
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_suggestions_title: 'Du leder måske efter',
|
||||
unexpected_error_title: 'Der opstod en fejl',
|
||||
|
||||
@@ -80,12 +80,16 @@ export const de: TranslationLanguage = {
|
||||
notfound_link:
|
||||
'Dieser Link verweist auf eine Seite, die entfernt wurde oder nicht mehr existiert.',
|
||||
notfound: 'Die gesuchte Seite existiert nicht.',
|
||||
notfound_adaptive_title: 'Seite nicht verfügbar',
|
||||
notfound_adaptive_title: 'Melden Sie sich an, um diese Seite anzuzeigen',
|
||||
notfound_adaptive:
|
||||
'Diese Seite existiert möglicherweise, aber Sie müssen sich eventuell anmelden, um darauf zuzugreifen.',
|
||||
'Für diese Seite ist möglicherweise eine Anmeldung erforderlich. Wenn Sie einem Link gefolgt sind, prüfen Sie, ob er korrekt ist.',
|
||||
notfound_adaptive_login: 'Anmelden',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Vielleicht suchen Sie nach',
|
||||
unexpected_error_title: 'Ein Fehler ist aufgetreten',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const el: TranslationLanguage = {
|
||||
notfound_title: 'Η σελίδα δεν βρέθηκε',
|
||||
notfound_link: 'Αυτός ο σύνδεσμος οδηγεί σε μια σελίδα που έχει αφαιρεθεί ή δεν υπάρχει πλέον.',
|
||||
notfound: 'Η σελίδα που αναζητάτε δεν υπάρχει.',
|
||||
notfound_adaptive_title: 'Η σελίδα δεν είναι διαθέσιμη',
|
||||
notfound_adaptive_title: 'Συνδεθείτε για να δείτε αυτή τη σελίδα',
|
||||
notfound_adaptive:
|
||||
'Αυτή η σελίδα μπορεί να υπάρχει, αλλά ίσως χρειαστεί να συνδεθείτε για πρόσβαση.',
|
||||
'Αυτή η σελίδα ενδέχεται να απαιτεί σύνδεση. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.',
|
||||
notfound_adaptive_login: 'Σύνδεση',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Η εγγραφή ενδέχεται να είναι διαθέσιμη ανάλογα με τον οργανισμό.',
|
||||
notfound_adaptive_no_access_title: 'Ίσως δεν έχετε πρόσβαση σε αυτή τη σελίδα',
|
||||
notfound_adaptive_no_access:
|
||||
'Αυτή η σελίδα ενδέχεται να μην υπάρχει ή ο λογαριασμός σας ενδέχεται να μην έχει πρόσβαση σε αυτήν. Αν ακολουθήσατε έναν σύνδεσμο, ελέγξτε ότι είναι σωστός.',
|
||||
notfound_adaptive_login_again: 'Επανασύνδεση',
|
||||
notfound_goto_home: 'Μετάβαση στην αρχική σελίδα',
|
||||
notfound_suggestions_title: 'Ίσως αναζητάτε',
|
||||
unexpected_error_title: 'Παρουσιάστηκε σφάλμα',
|
||||
|
||||
@@ -75,11 +75,16 @@ export const en = {
|
||||
notfound_title: 'Page not found',
|
||||
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_adaptive_title: 'Page unavailable',
|
||||
notfound_adaptive: 'This page may exist, but you may need to log in to access it.',
|
||||
notfound_adaptive_title: 'Log in to view this page',
|
||||
notfound_adaptive:
|
||||
"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_registration_hint:
|
||||
'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_suggestions_title: 'You might be looking for',
|
||||
unexpected_error_title: 'An error occurred',
|
||||
|
||||
@@ -79,12 +79,16 @@ export const es: TranslationLanguage = {
|
||||
notfound_title: 'Página no encontrada',
|
||||
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_adaptive_title: 'Página no disponible',
|
||||
notfound_adaptive_title: 'Inicia sesión para ver esta página',
|
||||
notfound_adaptive:
|
||||
'Esta página puede existir, pero es posible que debas iniciar sesión para acceder.',
|
||||
'Es posible que esta página requiera iniciar sesión. Si has seguido un enlace, comprueba que sea correcto.',
|
||||
notfound_adaptive_login: 'Iniciar sesión',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Quizás estés buscando',
|
||||
unexpected_error_title: 'Ocurrió un error',
|
||||
|
||||
@@ -77,11 +77,16 @@ export const et: TranslationLanguage = {
|
||||
notfound_title: 'Lehte ei leitud',
|
||||
notfound_link: 'See link viitab lehele, mis on eemaldatud või mida enam ei eksisteeri.',
|
||||
notfound: 'Lehte, mida otsite, ei eksisteeri.',
|
||||
notfound_adaptive_title: 'Leht pole saadaval',
|
||||
notfound_adaptive: 'See leht võib olemas olla, kuid juurdepääsuks võib olla vaja sisse logida.',
|
||||
notfound_adaptive_title: 'Selle lehe vaatamiseks logi sisse',
|
||||
notfound_adaptive:
|
||||
'See leht võib nõuda sisselogimist. Kui järgisid linki, kontrolli, et see oleks õige.',
|
||||
notfound_adaptive_login: 'Logi sisse',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Võib-olla otsite',
|
||||
unexpected_error_title: 'Ilmnes tõrge',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const fi: TranslationLanguage = {
|
||||
notfound_title: 'Sivua ei löytynyt',
|
||||
notfound_link: 'Tämä linkki osoittaa sivulle, joka on poistettu tai jota ei enää ole.',
|
||||
notfound: 'Etsimääsi sivua ei ole olemassa.',
|
||||
notfound_adaptive_title: 'Sivu ei ole saatavilla',
|
||||
notfound_adaptive_title: 'Kirjaudu sisään nähdäksesi tämän sivun',
|
||||
notfound_adaptive:
|
||||
'Tämä sivu voi olla olemassa, mutta sen käyttö voi edellyttää kirjautumista.',
|
||||
'Tämä sivu saattaa vaatia kirjautumisen. Jos seurasit linkkiä, tarkista, että se on oikein.',
|
||||
notfound_adaptive_login: 'Kirjaudu sisään',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Saatat etsiä',
|
||||
unexpected_error_title: 'Tapahtui virhe',
|
||||
|
||||
@@ -78,11 +78,15 @@ export const fr: TranslationLanguage = {
|
||||
notfound_title: 'Page introuvable',
|
||||
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_adaptive_title: 'Page inaccessible',
|
||||
notfound_adaptive_title: 'Connectez-vous pour afficher cette page',
|
||||
notfound_adaptive:
|
||||
'Cette page existe peut-être, mais vous devez peut-être vous connecter pour y accéder.',
|
||||
"Cette page peut nécessiter une connexion. Si vous avez suivi un lien, vérifiez qu'il est correct.",
|
||||
notfound_adaptive_login: 'Se connecter',
|
||||
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_suggestions_title: 'Vous cherchez peut-être',
|
||||
unexpected_error_title: 'Erreur inattendue',
|
||||
|
||||
@@ -77,10 +77,14 @@ export const he: TranslationLanguage = {
|
||||
notfound_title: 'הדף לא נמצא',
|
||||
notfound_link: 'קישור זה מפנה לדף שהוסר או אינו קיים עוד.',
|
||||
notfound: 'הדף שחיפשת אינו קיים.',
|
||||
notfound_adaptive_title: 'הדף אינו זמין',
|
||||
notfound_adaptive: 'ייתכן שהדף קיים, אך ייתכן שעליך להתחבר כדי לגשת אליו.',
|
||||
notfound_adaptive_title: 'יש להתחבר כדי לצפות בדף זה',
|
||||
notfound_adaptive: 'ייתכן שדף זה דורש התחברות. אם הגעת דרך קישור, יש לוודא שהוא נכון.',
|
||||
notfound_adaptive_login: 'התחברות',
|
||||
notfound_adaptive_registration_hint: 'ייתכן שההרשמה זמינה בהתאם לארגון.',
|
||||
notfound_adaptive_no_access_title: 'ייתכן שאין לך גישה לדף זה',
|
||||
notfound_adaptive_no_access:
|
||||
'ייתכן שדף זה אינו קיים, או שלחשבון שלך אין גישה אליו. אם הגעת דרך קישור, יש לוודא שהוא נכון.',
|
||||
notfound_adaptive_login_again: 'התחברות מחדש',
|
||||
notfound_goto_home: 'מעבר לדף הבית',
|
||||
notfound_suggestions_title: 'אולי אתה מחפש',
|
||||
unexpected_error_title: 'אירעה שגיאה',
|
||||
|
||||
@@ -77,10 +77,15 @@ export const hi: TranslationLanguage = {
|
||||
notfound_title: 'पृष्ठ नहीं मिला',
|
||||
notfound_link: 'यह लिंक ऐसे पृष्ठ पर जाता है जिसे हटा दिया गया है या जो अब मौजूद नहीं है।',
|
||||
notfound: 'आप जिस पृष्ठ को खोज रहे हैं वह मौजूद नहीं है।',
|
||||
notfound_adaptive_title: 'पृष्ठ उपलब्ध नहीं',
|
||||
notfound_adaptive: 'यह पृष्ठ मौजूद हो सकता है, लेकिन इसे देखने के लिए आपको लॉग इन करना पड़ सकता है।',
|
||||
notfound_adaptive_title: 'यह पृष्ठ देखने के लिए लॉग इन करें',
|
||||
notfound_adaptive:
|
||||
'इस पृष्ठ के लिए लॉग इन करना आवश्यक हो सकता है। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
|
||||
notfound_adaptive_login: 'लॉग इन करें',
|
||||
notfound_adaptive_registration_hint: 'संगठन के आधार पर पंजीकरण उपलब्ध हो सकता है।',
|
||||
notfound_adaptive_no_access_title: 'हो सकता है कि आपके पास इस पृष्ठ की पहुंच न हो',
|
||||
notfound_adaptive_no_access:
|
||||
'हो सकता है कि यह पृष्ठ मौजूद न हो, या आपके खाते के पास इसकी पहुंच न हो। यदि आप किसी लिंक से आए हैं, तो जांच लें कि वह सही है।',
|
||||
notfound_adaptive_login_again: 'फिर से लॉग इन करें',
|
||||
notfound_goto_home: 'मुखपृष्ठ पर जाएं',
|
||||
notfound_suggestions_title: 'शायद आप यह ढूँढ रहे हैं',
|
||||
unexpected_error_title: 'त्रुटि हुई',
|
||||
|
||||
@@ -77,11 +77,15 @@ export const hr: TranslationLanguage = {
|
||||
notfound_title: 'Stranica nije pronađena',
|
||||
notfound_link: 'Ova poveznica vodi na stranicu koja je uklonjena ili više ne postoji.',
|
||||
notfound: 'Stranica koju tražite ne postoji.',
|
||||
notfound_adaptive_title: 'Stranica nije dostupna',
|
||||
notfound_adaptive_title: 'Prijavite se za prikaz ove stranice',
|
||||
notfound_adaptive:
|
||||
'Ova stranica možda postoji, ali možda se morate prijaviti da biste joj pristupili.',
|
||||
'Ova stranica možda zahtijeva prijavu. Ako ste slijedili poveznicu, provjerite je li ispravna.',
|
||||
notfound_adaptive_login: 'Prijava',
|
||||
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_suggestions_title: 'Možda tražite',
|
||||
unexpected_error_title: 'Došlo je do pogreške',
|
||||
|
||||
@@ -78,10 +78,15 @@ export const hu: TranslationLanguage = {
|
||||
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: 'A keresett oldal nem létezik.',
|
||||
notfound_adaptive_title: 'Az oldal nem érhető el',
|
||||
notfound_adaptive: 'Ez az oldal létezhet, de a hozzáféréshez lehet, hogy be kell jelentkeznie.',
|
||||
notfound_adaptive_title: 'Jelentkezzen be az oldal megtekintéséhez',
|
||||
notfound_adaptive:
|
||||
'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_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_suggestions_title: 'Talán ezt keresi',
|
||||
unexpected_error_title: 'Hiba történt',
|
||||
|
||||
@@ -77,11 +77,15 @@ export const id: TranslationLanguage = {
|
||||
notfound_title: 'Halaman tidak ditemukan',
|
||||
notfound_link: 'Tautan ini mengarah ke halaman yang telah dihapus atau sudah tidak ada.',
|
||||
notfound: 'Halaman yang Anda cari tidak ada.',
|
||||
notfound_adaptive_title: 'Halaman tidak tersedia',
|
||||
notfound_adaptive_title: 'Masuk untuk melihat halaman ini',
|
||||
notfound_adaptive:
|
||||
'Halaman ini mungkin ada, tetapi Anda mungkin perlu masuk untuk mengaksesnya.',
|
||||
'Halaman ini mungkin mengharuskan Anda masuk. Jika Anda mengikuti tautan, periksa apakah tautan tersebut benar.',
|
||||
notfound_adaptive_login: 'Masuk',
|
||||
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_suggestions_title: 'Mungkin Anda mencari',
|
||||
unexpected_error_title: 'Terjadi kesalahan',
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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,6 +25,10 @@ const languageDefinitions = {
|
||||
metadata: { locale: 'fr', language: 'Français', flag: '🇫🇷' },
|
||||
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: {
|
||||
metadata: { locale: 'de', language: 'Deutsch', flag: '🇩🇪' },
|
||||
load: () => import('./de').then((module) => module.de),
|
||||
@@ -33,6 +37,14 @@ const languageDefinitions = {
|
||||
metadata: { locale: 'es', language: 'Español', flag: '🇪🇸' },
|
||||
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: {
|
||||
metadata: { locale: 'it', language: 'Italiano', flag: '🇮🇹' },
|
||||
load: () => import('./it').then((module) => module.it),
|
||||
@@ -177,6 +189,14 @@ const 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(
|
||||
Object.entries(languageDefinitions).map(([locale, definition]) => [locale, definition.metadata])
|
||||
) as {
|
||||
|
||||
@@ -79,12 +79,16 @@ export const it: TranslationLanguage = {
|
||||
notfound_title: 'Pagina non trovata',
|
||||
notfound_link: 'Questo link punta a una pagina che è stata rimossa o non esiste più.',
|
||||
notfound: 'La pagina che cerchi non esiste.',
|
||||
notfound_adaptive_title: 'Pagina non disponibile',
|
||||
notfound_adaptive_title: 'Accedi per visualizzare questa pagina',
|
||||
notfound_adaptive:
|
||||
"Questa pagina potrebbe esistere, ma potrebbe essere necessario effettuare l'accesso per visualizzarla.",
|
||||
"Questa pagina potrebbe richiedere l'accesso. Se hai seguito un link, verifica che sia corretto.",
|
||||
notfound_adaptive_login: 'Accedi',
|
||||
notfound_adaptive_registration_hint:
|
||||
"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_suggestions_title: 'Forse stai cercando',
|
||||
unexpected_error_title: 'Si è verificato un errore',
|
||||
|
||||
@@ -78,11 +78,15 @@ export const ja: TranslationLanguage = {
|
||||
notfound_title: 'ページが見つかりません',
|
||||
notfound_link: 'このリンクは、削除されたか、もはや存在しないページを指しています。',
|
||||
notfound: 'お探しのページは存在しません。',
|
||||
notfound_adaptive_title: 'ページにアクセスできません',
|
||||
notfound_adaptive_title: 'このページを表示するにはログインしてください',
|
||||
notfound_adaptive:
|
||||
'このページは存在する可能性がありますが、アクセスするにはログインが必要な場合があります。',
|
||||
'このページの閲覧にはログインが必要な場合があります。リンクからアクセスした場合は、リンクが正しいか確認してください。',
|
||||
notfound_adaptive_login: 'ログイン',
|
||||
notfound_adaptive_registration_hint: '組織によっては登録が可能な場合があります。',
|
||||
notfound_adaptive_no_access_title: 'このページへのアクセス権がない可能性があります',
|
||||
notfound_adaptive_no_access:
|
||||
'このページが存在しないか、お使いのアカウントにアクセス権がない可能性があります。リンクからアクセスした場合は、リンクが正しいか確認してください。',
|
||||
notfound_adaptive_login_again: '再ログイン',
|
||||
notfound_goto_home: 'ホームへ移動',
|
||||
notfound_suggestions_title: 'お探しのページはこちらですか',
|
||||
unexpected_error_title: 'エラーが発生しました',
|
||||
|
||||
@@ -78,10 +78,15 @@ export const ko: TranslationLanguage = {
|
||||
notfound_title: '페이지를 찾을 수 없음',
|
||||
notfound_link: '이 링크는 삭제되었거나 더 이상 존재하지 않는 페이지를 가리킵니다.',
|
||||
notfound: '찾으시는 페이지가 존재하지 않습니다.',
|
||||
notfound_adaptive_title: '페이지에 접근할 수 없음',
|
||||
notfound_adaptive: '이 페이지는 존재할 수 있지만, 접근하려면 로그인해야 할 수 있습니다.',
|
||||
notfound_adaptive_title: '이 페이지를 보려면 로그인하세요',
|
||||
notfound_adaptive:
|
||||
'이 페이지는 로그인이 필요할 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
|
||||
notfound_adaptive_login: '로그인',
|
||||
notfound_adaptive_registration_hint: '조직에 따라 등록이 가능할 수 있습니다.',
|
||||
notfound_adaptive_no_access_title: '이 페이지에 대한 접근 권한이 없을 수 있습니다',
|
||||
notfound_adaptive_no_access:
|
||||
'이 페이지가 존재하지 않거나 계정에 이 페이지에 대한 접근 권한이 없을 수 있습니다. 링크를 통해 접속했다면 링크가 올바른지 확인하세요.',
|
||||
notfound_adaptive_login_again: '다시 로그인',
|
||||
notfound_goto_home: '홈으로 이동',
|
||||
notfound_suggestions_title: '찾고 계신 내용일 수 있어요',
|
||||
unexpected_error_title: '오류가 발생했습니다',
|
||||
|
||||
@@ -77,12 +77,16 @@ export const lt: TranslationLanguage = {
|
||||
notfound_title: 'Puslapis nerastas',
|
||||
notfound_link: 'Ši nuoroda veda į puslapį, kuris buvo pašalintas arba nebeegzistuoja.',
|
||||
notfound: 'Puslapis, kurio ieškote, neegzistuoja.',
|
||||
notfound_adaptive_title: 'Puslapis nepasiekiamas',
|
||||
notfound_adaptive_title: 'Prisijunkite, kad peržiūrėtumėte šį puslapį',
|
||||
notfound_adaptive:
|
||||
'Šis puslapis gali egzistuoti, bet norint jį pasiekti gali reikėti prisijungti.',
|
||||
'Šiam puslapiui gali reikėti prisijungti. Jei sekėte nuoroda, patikrinkite, ar ji teisinga.',
|
||||
notfound_adaptive_login: 'Prisijungti',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Galbūt ieškote',
|
||||
unexpected_error_title: 'Įvyko klaida',
|
||||
|
||||
@@ -77,10 +77,15 @@ export const lv: TranslationLanguage = {
|
||||
notfound_title: 'Lapa nav atrasta',
|
||||
notfound_link: 'Šī saite norāda uz lapu, kas ir noņemta vai vairs nepastāv.',
|
||||
notfound: 'Meklētā lapa nepastāv.',
|
||||
notfound_adaptive_title: 'Lapa nav pieejama',
|
||||
notfound_adaptive: 'Šī lapa var pastāvēt, bet, iespējams, jums jāpierakstās, lai tai piekļūtu.',
|
||||
notfound_adaptive_title: 'Pierakstieties, lai skatītu šo lapu',
|
||||
notfound_adaptive:
|
||||
'Š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_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_suggestions_title: 'Iespējams, meklējat',
|
||||
unexpected_error_title: 'Radās kļūda',
|
||||
|
||||
@@ -77,11 +77,15 @@ export const ms: TranslationLanguage = {
|
||||
notfound_title: 'Halaman tidak ditemui',
|
||||
notfound_link: 'Pautan ini menghala ke halaman yang telah dialih keluar atau tidak lagi wujud.',
|
||||
notfound: 'Halaman yang anda cari tidak wujud.',
|
||||
notfound_adaptive_title: 'Halaman tidak tersedia',
|
||||
notfound_adaptive_title: 'Log masuk untuk melihat halaman ini',
|
||||
notfound_adaptive:
|
||||
'Halaman ini mungkin wujud, tetapi anda mungkin perlu log masuk untuk mengaksesnya.',
|
||||
'Halaman ini mungkin memerlukan anda log masuk. Jika anda mengikuti pautan, semak sama ada pautan itu betul.',
|
||||
notfound_adaptive_login: 'Log masuk',
|
||||
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_suggestions_title: 'Mungkin anda mencari',
|
||||
unexpected_error_title: 'Ralat berlaku',
|
||||
|
||||
@@ -79,12 +79,16 @@ export const nl: TranslationLanguage = {
|
||||
notfound_title: 'Pagina niet gevonden',
|
||||
notfound_link: 'Deze link verwijst naar een pagina die is verwijderd of niet meer bestaat.',
|
||||
notfound: 'De pagina die je zoekt, bestaat niet.',
|
||||
notfound_adaptive_title: 'Pagina niet beschikbaar',
|
||||
notfound_adaptive_title: 'Log in om deze pagina te bekijken',
|
||||
notfound_adaptive:
|
||||
'Deze pagina bestaat mogelijk, maar je moet mogelijk inloggen om toegang te krijgen.',
|
||||
'Voor deze pagina moet je mogelijk inloggen. Als je een link hebt gevolgd, controleer dan of deze klopt.',
|
||||
notfound_adaptive_login: 'Inloggen',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Misschien zoek je',
|
||||
unexpected_error_title: 'Er is een fout opgetreden',
|
||||
|
||||
@@ -79,11 +79,16 @@ export const no: TranslationLanguage = {
|
||||
notfound_link:
|
||||
'Denne lenken peker til en side som har blitt fjernet eller ikke lenger eksisterer.',
|
||||
notfound: 'Siden du leter etter eksisterer ikke.',
|
||||
notfound_adaptive_title: 'Siden er ikke tilgjengelig',
|
||||
notfound_adaptive: 'Denne siden kan finnes, men du må kanskje logge inn for å få tilgang.',
|
||||
notfound_adaptive_title: 'Logg inn for å se denne siden',
|
||||
notfound_adaptive:
|
||||
'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_registration_hint:
|
||||
'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_suggestions_title: 'Kanskje du leter etter',
|
||||
unexpected_error_title: 'En feil oppstod',
|
||||
|
||||
@@ -77,11 +77,16 @@ export const pl: TranslationLanguage = {
|
||||
notfound_title: 'Nie znaleziono strony',
|
||||
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_adaptive_title: 'Strona niedostępna',
|
||||
notfound_adaptive: 'Ta strona może istnieć, ale dostęp do niej może wymagać zalogowania.',
|
||||
notfound_adaptive_title: 'Zaloguj się, aby wyświetlić tę stronę',
|
||||
notfound_adaptive:
|
||||
'Ta strona może wymagać zalogowania. Jeśli przechodzisz z linku, sprawdź, czy jest poprawny.',
|
||||
notfound_adaptive_login: 'Zaloguj się',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Być może szukasz',
|
||||
unexpected_error_title: 'Wystąpił błąd',
|
||||
|
||||
@@ -79,12 +79,16 @@ export const pt_br: TranslationLanguage = {
|
||||
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: 'A página que você está procurando não existe.',
|
||||
notfound_adaptive_title: 'Página indisponível',
|
||||
notfound_adaptive_title: 'Faça login para ver esta página',
|
||||
notfound_adaptive:
|
||||
'Esta página pode existir, mas talvez você precise fazer login para acessá-la.',
|
||||
'Esta página pode exigir login. Se você seguiu um link, verifique se ele está correto.',
|
||||
notfound_adaptive_login: 'Fazer login',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Você pode estar procurando por',
|
||||
unexpected_error_title: 'Aconteceu um erro',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const pt: TranslationLanguage = {
|
||||
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: 'A página que procura não existe.',
|
||||
notfound_adaptive_title: 'Página indisponível',
|
||||
notfound_adaptive_title: 'Inicie sessão para ver esta página',
|
||||
notfound_adaptive:
|
||||
'Esta página pode existir, mas talvez tenha de iniciar sessão para lhe aceder.',
|
||||
'Esta página pode exigir que inicie sessão. Se seguiu uma ligação, verifique se está correta.',
|
||||
notfound_adaptive_login: 'Iniciar sessão',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Talvez esteja à procura de',
|
||||
unexpected_error_title: 'Ocorreu um erro',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const ro: TranslationLanguage = {
|
||||
notfound_title: 'Pagina nu a fost găsită',
|
||||
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_adaptive_title: 'Pagina nu este disponibilă',
|
||||
notfound_adaptive_title: 'Autentifică-te pentru a vedea această pagină',
|
||||
notfound_adaptive:
|
||||
'Această pagină poate exista, dar este posibil să trebuiască să te autentifici pentru a o accesa.',
|
||||
'Această pagină poate necesita autentificare. Dacă ai urmat un link, verifică dacă este corect.',
|
||||
notfound_adaptive_login: 'Autentificare',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Î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_suggestions_title: 'Poate cauți',
|
||||
unexpected_error_title: 'A apărut o eroare',
|
||||
|
||||
@@ -79,12 +79,16 @@ export const ru: TranslationLanguage = {
|
||||
notfound_title: 'Страница не найдена',
|
||||
notfound_link: 'Эта ссылка ведёт на страницу, которая была удалена или больше не существует.',
|
||||
notfound: 'Страница, которую вы ищете, не существует.',
|
||||
notfound_adaptive_title: 'Страница недоступна',
|
||||
notfound_adaptive_title: 'Войдите, чтобы просмотреть эту страницу',
|
||||
notfound_adaptive:
|
||||
'Эта страница может существовать, но для доступа к ней может потребоваться вход.',
|
||||
'Для просмотра этой страницы может потребоваться вход. Если вы перешли по ссылке, проверьте, что она верна.',
|
||||
notfound_adaptive_login: 'Войти',
|
||||
notfound_adaptive_registration_hint:
|
||||
'Регистрация может быть доступна в зависимости от организации.',
|
||||
notfound_adaptive_no_access_title: 'Возможно, у вас нет доступа к этой странице',
|
||||
notfound_adaptive_no_access:
|
||||
'Возможно, эта страница не существует или у вашей учётной записи нет к ней доступа. Если вы перешли по ссылке, проверьте, что она верна.',
|
||||
notfound_adaptive_login_again: 'Войти снова',
|
||||
notfound_goto_home: 'Перейти на главную',
|
||||
notfound_suggestions_title: 'Возможно, вы ищете',
|
||||
unexpected_error_title: 'Произошла ошибка',
|
||||
|
||||
@@ -78,12 +78,16 @@ export const sk: TranslationLanguage = {
|
||||
notfound_title: 'Stránka sa nenašla',
|
||||
notfound_link: 'Tento odkaz smeruje na stránku, ktorá bola odstránená alebo už neexistuje.',
|
||||
notfound: 'Stránka, ktorú hľadáte, neexistuje.',
|
||||
notfound_adaptive_title: 'Stránka nie je dostupná',
|
||||
notfound_adaptive_title: 'Na zobrazenie tejto stránky sa prihláste',
|
||||
notfound_adaptive:
|
||||
'Táto stránka môže existovať, ale na prístup sa možno budete musieť prihlásiť.',
|
||||
'Táto stránka môže vyžadovať prihlásenie. Ak ste prešli z odkazu, skontrolujte, či je správny.',
|
||||
notfound_adaptive_login: 'Prihlásiť sa',
|
||||
notfound_adaptive_registration_hint:
|
||||
'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_suggestions_title: 'Možno hľadáte',
|
||||
unexpected_error_title: 'Vyskytla sa chyba',
|
||||
|
||||
@@ -78,10 +78,15 @@ export const sl: TranslationLanguage = {
|
||||
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: 'Stran, ki jo iščete, ne obstaja.',
|
||||
notfound_adaptive_title: 'Stran ni na voljo',
|
||||
notfound_adaptive: 'Ta stran morda obstaja, vendar se boste za dostop morda morali prijaviti.',
|
||||
notfound_adaptive_title: 'Za ogled te strani se prijavite',
|
||||
notfound_adaptive:
|
||||
'Ta stran morda zahteva prijavo. Če ste sledili povezavi, preverite, ali je pravilna.',
|
||||
notfound_adaptive_login: 'Prijava',
|
||||
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_suggestions_title: 'Morda iščete',
|
||||
unexpected_error_title: 'Prišlo je do napake',
|
||||
|
||||
@@ -77,11 +77,16 @@ export const sv: TranslationLanguage = {
|
||||
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: 'Sidan du letar efter finns inte.',
|
||||
notfound_adaptive_title: 'Sidan är inte tillgänglig',
|
||||
notfound_adaptive: 'Sidan kan finnas, men du kan behöva logga in för att få åtkomst.',
|
||||
notfound_adaptive_title: 'Logga in för att visa den här sidan',
|
||||
notfound_adaptive:
|
||||
'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_registration_hint:
|
||||
'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_suggestions_title: 'Du kanske letar efter',
|
||||
unexpected_error_title: 'Ett fel uppstod',
|
||||
|
||||
@@ -76,10 +76,14 @@ export const th: TranslationLanguage = {
|
||||
notfound_title: 'ไม่พบหน้า',
|
||||
notfound_link: 'ลิงก์นี้ชี้ไปยังหน้าที่ถูกลบหรือไม่มีอยู่อีกต่อไป',
|
||||
notfound: 'หน้าที่คุณกำลังค้นหาไม่มีอยู่',
|
||||
notfound_adaptive_title: 'หน้าไม่พร้อมใช้งาน',
|
||||
notfound_adaptive: 'หน้านี้อาจมีอยู่ แต่คุณอาจต้องเข้าสู่ระบบเพื่อเข้าถึง',
|
||||
notfound_adaptive_title: 'เข้าสู่ระบบเพื่อดูหน้านี้',
|
||||
notfound_adaptive: 'หน้านี้อาจต้องเข้าสู่ระบบ หากคุณเปิดจากลิงก์ โปรดตรวจสอบว่าลิงก์ถูกต้อง',
|
||||
notfound_adaptive_login: 'เข้าสู่ระบบ',
|
||||
notfound_adaptive_registration_hint: 'การลงทะเบียนอาจใช้ได้ ขึ้นอยู่กับองค์กร',
|
||||
notfound_adaptive_no_access_title: 'คุณอาจไม่มีสิทธิ์เข้าถึงหน้านี้',
|
||||
notfound_adaptive_no_access:
|
||||
'หน้านี้อาจไม่มีอยู่ หรือบัญชีของคุณอาจไม่มีสิทธิ์เข้าถึง หากคุณเปิดจากลิงก์ โปรดตรวจสอบว่าลิงก์ถูกต้อง',
|
||||
notfound_adaptive_login_again: 'เข้าสู่ระบบอีกครั้ง',
|
||||
notfound_goto_home: 'ไปที่หน้าแรก',
|
||||
notfound_suggestions_title: 'คุณอาจกำลังมองหา',
|
||||
unexpected_error_title: 'เกิดข้อผิดพลาด',
|
||||
|
||||
@@ -77,10 +77,15 @@ export const tr: TranslationLanguage = {
|
||||
notfound_title: 'Sayfa bulunamadı',
|
||||
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_adaptive_title: 'Sayfa kullanılamıyor',
|
||||
notfound_adaptive: 'Bu sayfa mevcut olabilir, ancak erişmek için oturum açmanız gerekebilir.',
|
||||
notfound_adaptive_title: 'Bu sayfayı görüntülemek için oturum açın',
|
||||
notfound_adaptive:
|
||||
'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_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_suggestions_title: 'Şunu arıyor olabilirsiniz',
|
||||
unexpected_error_title: 'Bir hata oluştu',
|
||||
|
||||
@@ -77,10 +77,15 @@ export const uk: TranslationLanguage = {
|
||||
notfound_title: 'Сторінку не знайдено',
|
||||
notfound_link: 'Це посилання веде на сторінку, яку видалено або якої більше не існує.',
|
||||
notfound: 'Сторінка, яку ви шукаєте, не існує.',
|
||||
notfound_adaptive_title: 'Сторінка недоступна',
|
||||
notfound_adaptive: 'Ця сторінка може існувати, але для доступу до неї може знадобитися вхід.',
|
||||
notfound_adaptive_title: 'Увійдіть, щоб переглянути цю сторінку',
|
||||
notfound_adaptive:
|
||||
'Для перегляду цієї сторінки може знадобитися вхід. Якщо ви перейшли за посиланням, перевірте, чи воно правильне.',
|
||||
notfound_adaptive_login: 'Увійти',
|
||||
notfound_adaptive_registration_hint: 'Реєстрація може бути доступною залежно від організації.',
|
||||
notfound_adaptive_no_access_title: 'Можливо, у вас немає доступу до цієї сторінки',
|
||||
notfound_adaptive_no_access:
|
||||
'Можливо, ця сторінка не існує або ваш обліковий запис не має до неї доступу. Якщо ви перейшли за посиланням, перевірте, чи воно правильне.',
|
||||
notfound_adaptive_login_again: 'Увійти знову',
|
||||
notfound_goto_home: 'Перейти на головну',
|
||||
notfound_suggestions_title: 'Можливо, ви шукаєте',
|
||||
unexpected_error_title: 'Сталася помилка',
|
||||
|
||||
@@ -77,10 +77,15 @@ export const vi: TranslationLanguage = {
|
||||
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: 'Trang bạn đang tìm kiếm không tồn tại.',
|
||||
notfound_adaptive_title: 'Trang không khả dụng',
|
||||
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.',
|
||||
notfound_adaptive_title: 'Đăng nhập để xem trang này',
|
||||
notfound_adaptive:
|
||||
'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_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_suggestions_title: 'Có thể bạn đang tìm',
|
||||
unexpected_error_title: 'Đã xảy ra lỗi',
|
||||
|
||||
@@ -75,10 +75,14 @@ export const yue: TranslationLanguage = {
|
||||
notfound_title: '搵唔到頁面',
|
||||
notfound_link: '呢條連結指向嘅頁面已被移除或已經唔存在。',
|
||||
notfound: '你搵緊嘅頁面唔存在。',
|
||||
notfound_adaptive_title: '頁面無法使用',
|
||||
notfound_adaptive: '呢個頁面可能存在,但你可能需要登入先可以存取。',
|
||||
notfound_adaptive_title: '登入先可以睇呢個頁面',
|
||||
notfound_adaptive: '呢個頁面可能需要登入。如果你係經連結過嚟,請檢查連結係咪正確。',
|
||||
notfound_adaptive_login: '登入',
|
||||
notfound_adaptive_registration_hint: '視乎機構,可能可以註冊。',
|
||||
notfound_adaptive_no_access_title: '你可能冇權限存取呢個頁面',
|
||||
notfound_adaptive_no_access:
|
||||
'呢個頁面可能唔存在,或者你嘅帳戶可能冇權限存取。如果你係經連結過嚟,請檢查連結係咪正確。',
|
||||
notfound_adaptive_login_again: '重新登入',
|
||||
notfound_goto_home: '前往首頁',
|
||||
notfound_suggestions_title: '你可能喺度搵緊',
|
||||
unexpected_error_title: '發生錯誤',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user