From e3fcd26f12ccda464cb22bc4e95bad108878bca0 Mon Sep 17 00:00:00 2001 From: Claudio Valdez Date: Wed, 11 Mar 2026 12:30:59 -0300 Subject: [PATCH 01/74] Add mfa reset button for admin s on user profile edit --- app/Users/Controllers/UserController.php | 13 +++++++++++++ lang/en/settings.php | 5 +++++ resources/views/users/edit.blade.php | 20 ++++++++++++++++++++ routes/web.php | 1 + 4 files changed, 39 insertions(+) diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index 494221b14..6d1a47c0d 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -208,4 +208,17 @@ class UserController extends Controller return redirect('/settings/users'); } + + /** + * Reset MFA for the specified user. + */ + public function resetMfa(Request $request, int $id) + { + $this->checkPermission(Permission::UsersManage); + $user = $this->userRepo->getById($id); + // Resetear el 2FA del usuario + $user->mfaValues()->delete(); + session()->flash('success', trans('settings.users_mfa_reset_success', ['userName' => $user->name])); + return redirect()->back(); + } } diff --git a/lang/en/settings.php b/lang/en/settings.php index c4d1eb136..8499aed6f 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -263,6 +263,11 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset 2FA', + 'users_mfa_reset_desc' => 'Reset and clear all configured MFA methods for :userName. They will be prompted to reconfigure on next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset 2FA for :userName?', + 'users_mfa_reset_success' => '2FA has been reset for :userName', + 'users_mfa_reset_error' => 'Failed to reset 2FA for :userName', // API Tokens 'user_api_token_create' => 'Create API Token', diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 611653d6a..64d45f503 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -71,6 +71,26 @@ + @if(user()->hasSystemRole('admin')) +
+
+
+
+ {{ trans('settings.users_mfa_reset') }} +

{{ trans('settings.users_mfa_reset_desc', ['userName' => $user->name]) }}

+
+
+
id}/reset-mfa") }}" method="POST" style="display: inline;"> + @csrf + +
+
+
+
+ @endif @if(count($activeSocialDrivers) > 0) diff --git a/routes/web.php b/routes/web.php index a20c0a3d3..2571da2f3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -251,6 +251,7 @@ Route::middleware('auth')->group(function () { Route::get('/settings/users/{id}', [UserControllers\UserController::class, 'edit']); Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']); Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']); + Route::post('/settings/users/{id}/reset-mfa', [UserControllers\UserController::class, 'resetMfa']); // User Account Route::get('/my-account', [UserControllers\UserAccountController::class, 'redirect']); From 0b659671fe08da3a699f0b4ddd93e77e18d13b11 Mon Sep 17 00:00:00 2001 From: ololukaszuk <47779810+ololukaszuk@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:23:15 +0100 Subject: [PATCH 02/74] Fix PDF heading font fallback for export --- resources/sass/export-styles.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/sass/export-styles.scss b/resources/sass/export-styles.scss index 8dd7be375..22f15d1b8 100644 --- a/resources/sass/export-styles.scss +++ b/resources/sass/export-styles.scss @@ -64,6 +64,11 @@ body.export-format-pdf { font-size: 14px; line-height: 1.2; + // Ensure heading glyph coverage for PDF engines that don't handle CSS vars well. + h1, h2, h3, h4, h5, h6 { + font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + } + h1, h2, h3, h4, h5, h6 { line-height: 1.2; } @@ -100,4 +105,4 @@ body.export-format-pdf.export-engine-dompdf { .page-content td a > img { max-width: 100%; } -} \ No newline at end of file +} From e42fda893c62286d2948b0f18f4dd2fd846de098 Mon Sep 17 00:00:00 2001 From: Zhey Date: Thu, 26 Mar 2026 12:00:08 +0100 Subject: [PATCH 03/74] Add CSP controls for image and CSS sources --- .env.example.complete | 12 +++++++ app/Config/app.php | 10 ++++++ app/Util/CspService.php | 65 ++++++++++++++++++++++++++++++++++++ dev/docs/development.md | 42 +++++++++++++++++++++++ readme.md | 1 + tests/SecurityHeaderTest.php | 36 ++++++++++++++++++++ 6 files changed, 166 insertions(+) diff --git a/.env.example.complete b/.env.example.complete index ebebaf9e3..6a7f9db65 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -395,6 +395,18 @@ ALLOWED_IFRAME_HOSTS=null # Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured. ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com" +# A list of sources/hostnames that can be loaded as CSS styles within BookStack. +# Space separated if multiple. BookStack host domain is auto-inferred. +# Defaults to a permissive set if not provided. +# Example: ALLOWED_CSS_SOURCES="https://fonts.googleapis.com" +ALLOWED_CSS_SOURCES=null + +# A list of sources/hostnames that can be loaded as image content within BookStack. +# Space separated if multiple. BookStack host domain is auto-inferred. +# Defaults to a permissive set if not provided. +# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com data:" +ALLOWED_IMAGE_SOURCES=null + # A list of the sources/hostnames that can be reached by application SSR calls. # This is used wherever users can provide URLs/hosts in-platform, like for webhooks. # Host-specific functionality (usually controlled via other options) like auth diff --git a/app/Config/app.php b/app/Config/app.php index a476fdfea..5536e9abd 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -72,6 +72,16 @@ return [ // Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured. 'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'), + // A list of sources/hostnames that can be loaded as CSS styles within BookStack. + // Space separated if multiple. BookStack host domain is auto-inferred. + // If not set, a permissive default set is used to reduce potential breakage. + 'css_sources' => env('ALLOWED_CSS_SOURCES', null), + + // A list of sources/hostnames that can be loaded as image content within BookStack. + // Space separated if multiple. BookStack host domain is auto-inferred. + // If not set, a permissive default set is used to reduce potential breakage. + 'image_sources' => env('ALLOWED_IMAGE_SOURCES', null), + // A list of the sources/hostnames that can be reached by application SSR calls. // This is used wherever users can provide URLs/hosts in-platform, like for webhooks. // Host-specific functionality (usually controlled via other options) like auth diff --git a/app/Util/CspService.php b/app/Util/CspService.php index 466acb491..a0e1faadf 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -30,6 +30,8 @@ class CspService $this->getFrameAncestors(), $this->getFrameSrc(), $this->getScriptSrc(), + $this->getStyleSrc(), + $this->getImgSrc(), $this->getObjectSrc(), $this->getBaseUri(), ]; @@ -45,6 +47,8 @@ class CspService $headers = [ $this->getFrameSrc(), $this->getScriptSrc(), + $this->getStyleSrc(), + $this->getImgSrc(), $this->getObjectSrc(), $this->getBaseUri(), ]; @@ -115,6 +119,22 @@ class CspService return "object-src 'self'"; } + /** + * Creates CSP 'style-src' rule to restrict where styles can be loaded from. + */ + protected function getStyleSrc(): string + { + return 'style-src ' . implode(' ', $this->getAllowedStyleSources()); + } + + /** + * Creates CSP 'img-src' rule to restrict where images can be loaded from. + */ + protected function getImgSrc(): string + { + return 'img-src ' . implode(' ', $this->getAllowedImageSources()); + } + /** * Creates CSP 'base-uri' rule to restrict what base tags can be set on * the page to prevent manipulation of relative links. @@ -144,6 +164,51 @@ class CspService return array_filter($sources); } + /** + * Get allowed style sources for the style-src directive. + */ + protected function getAllowedStyleSources(): array + { + $configured = config('app.css_sources'); + + if (is_string($configured)) { + $sources = array_filter(explode(' ', $configured)); + array_unshift($sources, "'self'"); + + return array_values(array_unique($sources)); + } + + return [ + "'self'", + "'unsafe-inline'", + 'http:', + 'https:', + ]; + } + + /** + * Get allowed image sources for the img-src directive. + */ + protected function getAllowedImageSources(): array + { + $configured = config('app.image_sources'); + + if (is_string($configured)) { + $sources = array_filter(explode(' ', $configured)); + array_unshift($sources, "'self'"); + + return array_values(array_unique($sources)); + } + + return [ + "'self'", + 'data:', + 'blob:', + 'http:', + 'https:', + ]; + } + /** * Extract the host name of the configured drawio URL for use in CSP. * Returns empty string if not in use. diff --git a/dev/docs/development.md b/dev/docs/development.md index 2c73a0256..16f168f9c 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -31,6 +31,48 @@ BookStack has a large suite of PHP tests to cover application functionality. We For details about setting-up, running and writing tests please see the [php-testing.md document](php-testing.md). +## Content Security Policy Controls + +BookStack enforces a Content Security Policy (CSP) response header to reduce risk from injected content and untrusted embeds. + +For backward compatibility, image and CSS controls are intentionally permissive by default, but can be tightened via environment options. + +### Related Environment Options + +These values are defined in `.env.example.complete`: + +- `ALLOWED_CSS_SOURCES` + - Controls allowed `style-src` sources. + - Defaults to a permissive fallback if unset. +- `ALLOWED_IMAGE_SOURCES` + - Controls allowed `img-src` sources. + - Defaults to a permissive fallback if unset. + +Values should be space-separated source expressions. + +### Example Configurations + +Allow Google Fonts CSS and local styles only: + +```bash +ALLOWED_CSS_SOURCES="https://fonts.googleapis.com" +``` + +Allow local images, embedded data images, and a dedicated image CDN: + +```bash +ALLOWED_IMAGE_SOURCES="data: https://images.example.com" +``` + +### Tightening Guidance + +When hardening a deployment: + +1. Start with defaults to avoid unexpected breakage. +2. Set explicit `ALLOWED_CSS_SOURCES` and `ALLOWED_IMAGE_SOURCES` values for the domains you actually use. +3. Test key workflows (editor, page display, theme assets, external embeds) and browser console CSP warnings. +4. Remove unnecessary protocols and hosts over time. + ## Code Standards We use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools. diff --git a/readme.md b/readme.md index 00eb13504..672cff55a 100644 --- a/readme.md +++ b/readme.md @@ -102,6 +102,7 @@ Big thanks to these companies for supporting the project. ## 🛠️ Development & Testing Please see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code. +For details on Content Security Policy controls (including image and CSS source options), see the **Content Security Policy Controls** section in the [development docs](dev/docs/development.md). If you're just looking to customize or extend your own BookStack instance, take a look at our [Hacking BookStack documentation page](https://www.bookstackapp.com/docs/admin/hacking-bookstack/) for details on various options to achieve this without altering the BookStack source code. diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index 3f4b7d193..126a85f23 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -151,6 +151,42 @@ class SecurityHeaderTest extends TestCase $this->assertEquals('frame-src \'self\' https://example.com https://diagrams.example.com:8080', $scriptHeader); } + public function test_style_src_csp_header_set_to_permissive_defaults_when_not_configured() + { + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'style-src'); + + $this->assertEquals("style-src 'self' 'unsafe-inline' http: https:", $header); + } + + public function test_style_src_csp_header_can_be_overridden_by_config() + { + config()->set('app.css_sources', 'https://fonts.example.com'); + + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'style-src'); + + $this->assertEquals("style-src 'self' https://fonts.example.com", $header); + } + + public function test_img_src_csp_header_set_to_permissive_defaults_when_not_configured() + { + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'img-src'); + + $this->assertEquals("img-src 'self' data: blob: http: https:", $header); + } + + public function test_img_src_csp_header_can_be_overridden_by_config() + { + config()->set('app.image_sources', 'https://images.example.com data:'); + + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'img-src'); + + $this->assertEquals("img-src 'self' https://images.example.com data:", $header); + } + public function test_cache_control_headers_are_set_on_responses() { // Public access From c7d3775bb999bda0d134de900b28f2ebeb4cec67 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 5 Apr 2026 00:05:10 +0100 Subject: [PATCH 04/74] Plain text: Created a new HTML to plain text converter To centralise logic to be more consistent, and to have smarter logic which avoids just following newline format from input, preventing smushing HTML elements (like list elements) next to eachother --- app/Activity/Models/Comment.php | 7 +++ .../Messages/CommentCreationNotification.php | 2 +- .../Messages/CommentMentionNotification.php | 2 +- app/Entities/Repos/BaseRepo.php | 4 +- app/Entities/Tools/PageContent.php | 5 +- app/Util/HtmlToPlainText.php | 47 ++++++++++++++ tests/Util/HtmlToPlainTextTest.php | 63 +++++++++++++++++++ 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 app/Util/HtmlToPlainText.php create mode 100644 tests/Util/HtmlToPlainTextTest.php diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index ab7d91772..3faa76657 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -9,6 +9,7 @@ use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\OwnableInterface; use BookStack\Util\HtmlContentFilter; use BookStack\Util\HtmlContentFilterConfig; +use BookStack\Util\HtmlToPlainText; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -87,6 +88,12 @@ class Comment extends Model implements Loggable, OwnableInterface return $filter->filterString($this->html ?? ''); } + public function getPlainText(): string + { + $converter = new HtmlToPlainText(); + return $converter->convert($this->html ?? ''); + } + public function jointPermissions(): HasMany { return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id') diff --git a/app/Activity/Notifications/Messages/CommentCreationNotification.php b/app/Activity/Notifications/Messages/CommentCreationNotification.php index 30d0ffa2b..d739f4aab 100644 --- a/app/Activity/Notifications/Messages/CommentCreationNotification.php +++ b/app/Activity/Notifications/Messages/CommentCreationNotification.php @@ -24,7 +24,7 @@ class CommentCreationNotification extends BaseActivityNotification $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page), $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable), $locale->trans('notifications.detail_commenter') => $this->user->name, - $locale->trans('notifications.detail_comment') => strip_tags($comment->html), + $locale->trans('notifications.detail_comment') => $comment->getPlainText(), ]); return $this->newMailMessage($locale) diff --git a/app/Activity/Notifications/Messages/CommentMentionNotification.php b/app/Activity/Notifications/Messages/CommentMentionNotification.php index de9e71963..4c8ee5bab 100644 --- a/app/Activity/Notifications/Messages/CommentMentionNotification.php +++ b/app/Activity/Notifications/Messages/CommentMentionNotification.php @@ -24,7 +24,7 @@ class CommentMentionNotification extends BaseActivityNotification $locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page), $locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable), $locale->trans('notifications.detail_commenter') => $this->user->name, - $locale->trans('notifications.detail_comment') => strip_tags($comment->html), + $locale->trans('notifications.detail_comment') => $comment->getPlainText(), ]); return $this->newMailMessage($locale) diff --git a/app/Entities/Repos/BaseRepo.php b/app/Entities/Repos/BaseRepo.php index 717e9c9f8..44baeaccf 100644 --- a/app/Entities/Repos/BaseRepo.php +++ b/app/Entities/Repos/BaseRepo.php @@ -16,6 +16,7 @@ use BookStack\References\ReferenceUpdater; use BookStack\Sorting\BookSorter; use BookStack\Uploads\ImageRepo; use BookStack\Util\HtmlDescriptionFilter; +use BookStack\Util\HtmlToPlainText; use Illuminate\Http\UploadedFile; class BaseRepo @@ -151,9 +152,10 @@ class BaseRepo } if (isset($input['description_html'])) { + $plainTextConverter = new HtmlToPlainText(); $entity->descriptionInfo()->set( HtmlDescriptionFilter::filterFromString($input['description_html']), - html_entity_decode(strip_tags($input['description_html'])) + $plainTextConverter->convert($input['description_html']), ); } else if (isset($input['description'])) { $entity->descriptionInfo()->set('', $input['description']); diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 8d89a86cf..b86fbbe8b 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -16,6 +16,7 @@ use BookStack\Users\Models\User; use BookStack\Util\HtmlContentFilter; use BookStack\Util\HtmlContentFilterConfig; use BookStack\Util\HtmlDocument; +use BookStack\Util\HtmlToPlainText; use BookStack\Util\WebSafeMimeSniffer; use Closure; use DOMElement; @@ -303,8 +304,8 @@ class PageContent public function toPlainText(): string { $html = $this->render(true); - - return html_entity_decode(strip_tags($html)); + $converter = new HtmlToPlainText(); + return $converter->convert($html); } /** diff --git a/app/Util/HtmlToPlainText.php b/app/Util/HtmlToPlainText.php new file mode 100644 index 000000000..79da9e3d8 --- /dev/null +++ b/app/Util/HtmlToPlainText.php @@ -0,0 +1,47 @@ +nodeToText($doc->getBody()); + + // Remove repeated newlines + $text = preg_replace('/\n+/', "\n", $text); + // Remove leading/trailing whitespace + $text = trim($text); + + return $text; + } + + protected function nodeToText(\DOMNode $node): string + { + if ($node->nodeType === XML_TEXT_NODE) { + return $node->textContent; + } + + $text = ''; + if (!in_array($node->nodeName, $this->inlineTags)) { + $text .= "\n"; + } + + foreach ($node->childNodes as $childNode) { + $text .= $this->nodeToText($childNode); + } + + return $text; + } +} diff --git a/tests/Util/HtmlToPlainTextTest.php b/tests/Util/HtmlToPlainTextTest.php new file mode 100644 index 000000000..e522e4863 --- /dev/null +++ b/tests/Util/HtmlToPlainTextTest.php @@ -0,0 +1,63 @@ +This is a test

+
    +
  • Item 1
  • +
  • Item 2
  • +
+

A Header

+

more <©> text with bold

+HTML; + $expected = << text with bold +TEXT; + + $this->runTest($html, $expected); + } + + public function test_adjacent_list_items_are_separated_by_newline() + { + $html = <<
  • Item A
  • Item B
  • +HTML; + $expected = <<runTest($html, $expected); + } + + public function test_inline_formats_dont_cause_newlines() + { + $html = <<Hello

    +HTML; + $expected = <<runTest($html, $expected); + } + + protected function runTest(string $html, string $expected): void + { + $converter = new HtmlToPlainText(); + $result = $converter->convert(trim($html)); + $this->assertEquals(trim($expected), $result); + } +} From abed4eae0c541a44990d260796e21e420d8243ad Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 5 Apr 2026 17:51:19 +0100 Subject: [PATCH 05/74] Exports: Updated plaintext export to use new converter --- app/Exports/ExportFormatter.php | 20 +++++--------------- tests/Exports/TextExportTest.php | 4 ++-- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/app/Exports/ExportFormatter.php b/app/Exports/ExportFormatter.php index c5973eace..dec8aa23d 100644 --- a/app/Exports/ExportFormatter.php +++ b/app/Exports/ExportFormatter.php @@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PageContent; use BookStack\Uploads\ImageService; use BookStack\Util\CspService; use BookStack\Util\HtmlDocument; +use BookStack\Util\HtmlToPlainText; use DOMElement; use Exception; use Throwable; @@ -242,24 +243,13 @@ class ExportFormatter /** * Converts the page contents into simple plain text. - * This method filters any bad looking content to provide a nice final output. + * We re-generate the plain text from HTML at this point, post-page-content rendering. */ public function pageToPlainText(Page $page, bool $pageRendered = false, bool $fromParent = false): string { $html = $pageRendered ? $page->html : (new PageContent($page))->render(); - // Add proceeding spaces before tags so spaces remain between - // text within elements after stripping tags. - $html = str_replace('<', " <", $html); - $text = trim(strip_tags($html)); - // Replace multiple spaces with single spaces - $text = preg_replace('/ {2,}/', ' ', $text); - // Reduce multiple horrid whitespace characters. - $text = preg_replace('/(\x0A|\xA0|\x0A|\r|\n){2,}/su', "\n\n", $text); - $text = html_entity_decode($text); - // Add title - $text = $page->name . ($fromParent ? "\n" : "\n\n") . $text; - - return $text; + $contentText = (new HtmlToPlainText())->convert($html); + return $page->name . ($fromParent ? "\n" : "\n\n") . $contentText; } /** @@ -267,7 +257,7 @@ class ExportFormatter */ public function chapterToPlainText(Chapter $chapter): string { - $text = $chapter->name . "\n" . $chapter->description; + $text = $chapter->name . "\n" . $chapter->descriptionInfo()->getPlain(); $text = trim($text) . "\n\n"; $parts = []; diff --git a/tests/Exports/TextExportTest.php b/tests/Exports/TextExportTest.php index 4b2d62887..26298c185 100644 --- a/tests/Exports/TextExportTest.php +++ b/tests/Exports/TextExportTest.php @@ -52,7 +52,7 @@ class TextExportTest extends TestCase $resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext')); $expected = "Export Book\nThis is a book with stuff to export\n\nExport chapter\nA test chapter to be exported\nIt has loads of info within\n\n"; - $expected .= "My wonderful page!\nMy great page Full of great stuff"; + $expected .= "My wonderful page!\nMy great page\nFull of great stuff"; $resp->assertSee($expected); } @@ -82,7 +82,7 @@ class TextExportTest extends TestCase $resp = $this->asEditor()->get($entities['book']->getUrl('/export/plaintext')); $expected = "Export chapter\nA test chapter to be exported\nIt has loads of info within\n\n"; - $expected .= "My wonderful page!\nMy great page Full of great stuff"; + $expected .= "My wonderful page!\nMy great page\nFull of great stuff"; $resp->assertSee($expected); } } From c33853ed84bcefdd41fd12013451f14a69efc0d3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 8 Apr 2026 21:02:20 +0100 Subject: [PATCH 06/74] Maintenance: Updated NPM packages (#6090) * Maintenance: Updated NPM packages Includes typescript update to 6. Needed to update some typescript config to align with actual module environment used and built by esbuild. * Maintenance: Fixed testing issues after NPM dep version changes * Maintenance: Updated JS test workflow step version * Maintenance: Updated approach used for TS config in jest config --- .github/workflows/test-js.yml | 2 +- jest.config.ts | 14 +- package-lock.json | 1279 ++++++++++++++++++--------------- package.json | 24 +- tsconfig.json | 15 +- 5 files changed, 720 insertions(+), 614 deletions(-) diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml index 13f9a8a98..379f1ebfa 100644 --- a/.github/workflows/test-js.yml +++ b/.github/workflows/test-js.yml @@ -17,7 +17,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install NPM deps run: npm ci diff --git a/jest.config.ts b/jest.config.ts index 53bfceb05..f7b5596de 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -5,7 +5,15 @@ import type {Config} from 'jest'; import {pathsToModuleNameMapper} from "ts-jest"; -import { compilerOptions } from './tsconfig.json'; +import fs from "node:fs"; + +const { compilerOptions } = JSON.parse(fs.readFileSync('./tsconfig.json', 'utf8')); +const compilerPaths = compilerOptions.paths as Record; +const cleanedPaths: Record = {}; +Object.keys(compilerPaths).forEach((key) => { + const paths = compilerPaths[key]; + cleanedPaths[key] = paths.map(p => p.replace('./', '')); +}); const config: Config = { // All imported modules in your tests should be mocked automatically @@ -98,7 +106,7 @@ const config: Config = { // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module moduleNameMapper: { 'lexical/shared/invariant': 'resources/js/wysiwyg/lexical/core/shared/__mocks__/invariant', - ...pathsToModuleNameMapper(compilerOptions.paths), + ...pathsToModuleNameMapper(cleanedPaths), }, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader @@ -111,7 +119,7 @@ const config: Config = { // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration - // preset: undefined, + preset: 'ts-jest', // Run tests from one or more projects // projects: undefined, diff --git a/package-lock.json b/package-lock.json index b6508f1e9..d239426ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "packages": { "": { "dependencies": { - "@codemirror/commands": "^6.10.2", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.5", @@ -13,15 +13,14 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.12.2", + "@codemirror/language": "^6.12.3", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/state": "^6.5.4", + "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.39.16", + "@codemirror/view": "^6.41.0", "@lezer/highlight": "^1.2.3", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@types/jest": "^30.0.0", "codemirror": "^6.0.2", "idb-keyval": "^6.2.2", "markdown-it": "^14.1.1", @@ -32,19 +31,20 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@lezer/generator": "^1.8.0", + "@types/jest": "^30.0.0", "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.9", "chokidar-cli": "^3.0", - "esbuild": "^0.27.3", - "eslint": "^10.0.2", + "esbuild": "^0.28.0", + "eslint": "^10.2.0", "globals": "^17.4.0", - "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0", + "jest": "^30.3.0", + "jest-environment-jsdom": "^30.3.0", "npm-run-all": "^4.1.5", - "sass": "^1.97.3", - "ts-jest": "^29.4.6", + "sass": "^1.99.0", + "ts-jest": "^29.4.9", "ts-node": "^10.9.2", - "typescript": "5.9.*" + "typescript": "6.0.*" } }, "node_modules/@asamuzakjp/css-color": { @@ -72,6 +72,7 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -223,6 +224,7 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -239,23 +241,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { @@ -575,13 +577,13 @@ } }, "node_modules/@codemirror/commands": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.2.tgz", - "integrity": "sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "license": "MIT", "dependencies": { "@codemirror/language": "^6.0.0", - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.6.0", "@codemirror/view": "^6.27.0", "@lezer/common": "^1.1.0" } @@ -684,9 +686,9 @@ } }, "node_modules/@codemirror/language": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.2.tgz", - "integrity": "sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.0.0", @@ -729,9 +731,9 @@ } }, "node_modules/@codemirror/state": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.4.tgz", - "integrity": "sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" @@ -750,12 +752,12 @@ } }, "node_modules/@codemirror/view": { - "version": "6.39.16", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.39.16.tgz", - "integrity": "sha512-m6S22fFpKtOWhq8HuhzsI1WzUP/hB9THbDj0Tl5KX4gbO6Y91hwBl7Yky33NdvB6IffuRFiBxf1R8kJMyXmA4Q==", + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.0.tgz", + "integrity": "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA==", "license": "MIT", "dependencies": { - "@codemirror/state": "^6.5.0", + "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" @@ -901,21 +903,21 @@ } }, "node_modules/@emnapi/core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", - "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", "dev": true, "license": "MIT", "optional": true, @@ -924,9 +926,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, @@ -935,9 +937,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -952,9 +954,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -969,9 +971,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -986,9 +988,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -1003,9 +1005,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -1020,9 +1022,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -1037,9 +1039,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -1054,9 +1056,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -1071,9 +1073,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -1088,9 +1090,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -1105,9 +1107,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -1122,9 +1124,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -1139,9 +1141,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -1156,9 +1158,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -1173,9 +1175,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -1190,9 +1192,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -1207,9 +1209,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -1224,9 +1226,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -1241,9 +1243,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -1258,9 +1260,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -1275,9 +1277,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1292,9 +1294,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -1309,9 +1311,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -1326,9 +1328,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -1343,9 +1345,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -1360,9 +1362,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -1419,37 +1421,37 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1481,9 +1483,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1491,13 +1493,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -1658,17 +1660,17 @@ } }, "node_modules/@jest/console": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz", + "integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -1676,39 +1678,38 @@ } }, "node_modules/@jest/core": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", + "integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", + "@jest/console": "30.3.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/reporters": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.2.0", - "jest-config": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", + "jest-changed-files": "30.3.0", + "jest-config": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-resolve-dependencies": "30.2.0", - "jest-runner": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "jest-watcher": "30.2.0", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "jest-resolve": "30.3.0", + "jest-resolve-dependencies": "30.3.0", + "jest-runner": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", + "jest-watcher": "30.3.0", + "pretty-format": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -1724,44 +1725,45 @@ } }, "node_modules/@jest/diff-sequences": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", - "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "30.2.0" + "jest-mock": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/environment-jsdom-abstract": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", - "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1777,23 +1779,24 @@ } }, "node_modules/@jest/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.2.0", - "jest-snapshot": "30.2.0" + "expect": "30.3.0", + "jest-snapshot": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", + "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0" @@ -1803,18 +1806,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", - "@sinonjs/fake-timers": "^13.0.0", + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1824,22 +1827,23 @@ "version": "30.1.0", "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/globals": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz", + "integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/types": "30.2.0", - "jest-mock": "30.2.0" + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/types": "30.3.0", + "jest-mock": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -1849,6 +1853,7 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -1859,32 +1864,32 @@ } }, "node_modules/@jest/reporters": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz", + "integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", "collect-v8-coverage": "^1.0.2", "exit-x": "^0.2.2", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -1905,6 +1910,7 @@ "version": "30.0.5", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.34.0" @@ -1914,13 +1920,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz", + "integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -1945,14 +1951,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz", + "integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/types": "30.3.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -1961,15 +1967,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz", + "integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", + "@jest/test-result": "30.3.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "slash": "^3.0.0" }, "engines": { @@ -1977,24 +1983,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz", + "integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@jridgewell/trace-mapping": "^0.3.25", "babel-plugin-istanbul": "^7.0.1", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", "pirates": "^4.0.7", "slash": "^3.0.0", "write-file-atomic": "^5.0.1" @@ -2004,9 +2009,10 @@ } }, "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, "license": "MIT", "dependencies": { "@jest/pattern": "30.0.1", @@ -2072,15 +2078,15 @@ } }, "node_modules/@lezer/common": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.1.tgz", - "integrity": "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "license": "MIT" }, "node_modules/@lezer/css": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.1.tgz", - "integrity": "sha512-PYAKeUVBo3HFThruRyp/iK91SwiZJnzXh8QzkQlwijB5y+N5iB28+iLk78o2zmKqqV0uolNhCwFqB8LA7b0Svg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", "license": "MIT", "dependencies": { "@lezer/common": "^1.2.0", @@ -2333,6 +2339,9 @@ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2354,6 +2363,9 @@ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2375,6 +2387,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2396,6 +2411,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2417,6 +2435,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2438,6 +2459,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2515,9 +2539,9 @@ } }, "node_modules/@parcel/watcher/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "optional": true, @@ -2553,9 +2577,10 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.34.48", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", - "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, "license": "MIT" }, "node_modules/@sinonjs/commons": { @@ -2569,9 +2594,9 @@ } }, "node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.3.0.tgz", + "integrity": "sha512-m2xozxSfCIxjDdvbhIWazlP2i2aha/iUmbl94alpsIbd3iLTfeXgfBVbwyWogB6l++istyGZqamgA/EcqYf+Bg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2697,12 +2722,14 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" @@ -2712,6 +2739,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" @@ -2721,6 +2749,7 @@ "version": "30.0.0", "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, "license": "MIT", "dependencies": { "expect": "^30.0.0", @@ -2772,9 +2801,10 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -2791,6 +2821,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, "license": "MIT" }, "node_modules/@types/tough-cookie": { @@ -2804,6 +2835,7 @@ "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, "license": "MIT", "dependencies": { "@types/yargs-parser": "*" @@ -2813,6 +2845,7 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, "license": "MIT" }, "node_modules/@ungap/structured-clone": { @@ -2928,6 +2961,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2942,6 +2978,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2956,6 +2995,9 @@ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2970,6 +3012,9 @@ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -2984,6 +3029,9 @@ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -2998,6 +3046,9 @@ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3012,6 +3063,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3026,6 +3080,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3187,6 +3244,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3295,16 +3353,16 @@ } }, "node_modules/babel-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz", + "integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.2.0", + "@jest/transform": "30.3.0", "@types/babel__core": "^7.20.5", "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.2.0", + "babel-preset-jest": "30.3.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -3337,9 +3395,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz", + "integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==", "dev": true, "license": "MIT", "dependencies": { @@ -3377,13 +3435,13 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz", + "integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.2.0", + "babel-plugin-jest-hoist": "30.3.0", "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { @@ -3404,9 +3462,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3430,9 +3488,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3446,6 +3504,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3455,9 +3514,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -3475,11 +3534,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3589,9 +3648,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001777", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", - "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "dev": true, "funding": [ { @@ -3613,6 +3672,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3683,6 +3743,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, "funding": [ { "type": "github", @@ -3850,6 +3911,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3862,6 +3924,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/concat-map": { @@ -4145,9 +4208,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.307", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", - "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==", "dev": true, "license": "ISC" }, @@ -4194,9 +4257,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -4330,9 +4393,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4343,32 +4406,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -4395,18 +4458,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -4415,9 +4478,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -4428,7 +4491,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4451,9 +4514,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4496,9 +4559,9 @@ } }, "node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -4615,17 +4678,18 @@ } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4679,6 +4743,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -4719,9 +4784,9 @@ } }, "node_modules/flatted": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.4.tgz", - "integrity": "sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -4974,9 +5039,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -5046,12 +5111,13 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5087,6 +5153,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5592,6 +5659,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5894,16 +5962,16 @@ } }, "node_modules/jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz", + "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.3.0", + "@jest/types": "30.3.0", "import-local": "^3.2.0", - "jest-cli": "30.2.0" + "jest-cli": "30.3.0" }, "bin": { "jest": "bin/jest.js" @@ -5921,14 +5989,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz", + "integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "p-limit": "^3.1.0" }, "engines": { @@ -5936,29 +6004,29 @@ } }, "node_modules/jest-circus": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz", + "integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/expect": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-each": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-runtime": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "p-limit": "^3.1.0", - "pretty-format": "30.2.0", + "pretty-format": "30.3.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -5968,21 +6036,21 @@ } }, "node_modules/jest-cli": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", + "integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-config": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "yargs": "^17.7.2" }, "bin": { @@ -6108,34 +6176,33 @@ } }, "node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz", + "integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", + "@jest/test-sequencer": "30.3.0", + "@jest/types": "30.3.0", + "babel-jest": "30.3.0", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", + "jest-circus": "30.3.0", "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", + "jest-environment-node": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "micromatch": "^4.0.8", + "jest-resolve": "30.3.0", + "jest-runner": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "parse-json": "^5.2.0", - "pretty-format": "30.2.0", + "pretty-format": "30.3.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -6160,15 +6227,16 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.0.1", + "@jest/diff-sequences": "30.3.0", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6188,33 +6256,31 @@ } }, "node_modules/jest-each": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz", + "integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "chalk": "^4.1.2", - "jest-util": "30.2.0", - "pretty-format": "30.2.0" + "jest-util": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-jsdom": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", - "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/environment-jsdom-abstract": "30.2.0", - "@types/jsdom": "^21.1.7", - "@types/node": "*", + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", "jsdom": "^26.1.0" }, "engines": { @@ -6230,40 +6296,40 @@ } }, "node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz", + "integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" + "jest-mock": "30.3.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz", + "integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", - "micromatch": "^4.0.8", + "jest-util": "30.3.0", + "jest-worker": "30.3.0", + "picomatch": "^4.0.3", "walker": "^1.0.8" }, "engines": { @@ -6273,48 +6339,63 @@ "fsevents": "^2.3.3" } }, + "node_modules/jest-haste-map/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-leak-detector": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz", + "integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", + "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -6322,15 +6403,29 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6358,24 +6453,25 @@ "version": "30.0.1", "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-resolve": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz", + "integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.3.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-util": "30.3.0", + "jest-validate": "30.3.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -6384,46 +6480,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz", + "integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.2.0" + "jest-snapshot": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", + "integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/environment": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.3.0", + "@jest/environment": "30.3.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-leak-detector": "30.2.0", - "jest-message-util": "30.2.0", - "jest-resolve": "30.2.0", - "jest-runtime": "30.2.0", - "jest-util": "30.2.0", - "jest-watcher": "30.2.0", - "jest-worker": "30.2.0", + "jest-environment-node": "30.3.0", + "jest-haste-map": "30.3.0", + "jest-leak-detector": "30.3.0", + "jest-message-util": "30.3.0", + "jest-resolve": "30.3.0", + "jest-runtime": "30.3.0", + "jest-util": "30.3.0", + "jest-watcher": "30.3.0", + "jest-worker": "30.3.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -6432,32 +6528,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz", + "integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/globals": "30.2.0", + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/globals": "30.3.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", - "glob": "^10.3.10", + "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", + "jest-haste-map": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-resolve": "30.3.0", + "jest-snapshot": "30.3.0", + "jest-util": "30.3.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -6466,9 +6562,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz", + "integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6477,20 +6573,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.3.0", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.2.0", + "expect": "30.3.0", "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -6512,26 +6608,28 @@ } }, "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", "graceful-fs": "^4.2.11", - "picomatch": "^4.0.2" + "picomatch": "^4.0.3" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -6541,18 +6639,18 @@ } }, "node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", + "integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.3.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.3.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -6572,19 +6670,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", + "integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.3.0", + "@jest/types": "30.3.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "string-length": "^4.0.2" }, "engines": { @@ -6592,15 +6690,15 @@ } }, "node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", + "integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", + "jest-util": "30.3.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -6628,6 +6726,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -6989,19 +7088,6 @@ "dev": true, "license": "MIT" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -7013,13 +7099,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -7108,9 +7194,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "dev": true, "license": "MIT" }, @@ -7194,9 +7280,9 @@ "license": "MIT" }, "node_modules/npm-run-all/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7653,12 +7739,14 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -7790,9 +7878,10 @@ } }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, "license": "MIT", "dependencies": { "@jest/schemas": "30.0.5", @@ -7807,6 +7896,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -7855,6 +7945,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, "license": "MIT" }, "node_modules/read-pkg": { @@ -8060,14 +8151,14 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "dev": true, "license": "MIT", "dependencies": { "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -8246,14 +8337,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -8318,6 +8409,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8416,6 +8508,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" @@ -8428,6 +8521,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8709,6 +8803,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -8776,9 +8871,9 @@ "license": "MIT" }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8852,6 +8947,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -8887,19 +8983,19 @@ } }, "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "version": "29.4.9", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", + "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.9", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.3", + "semver": "^7.7.4", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -8916,7 +9012,7 @@ "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -9132,9 +9228,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -9188,6 +9284,7 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -9622,9 +9719,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 45c275863..80810fff1 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,23 @@ "devDependencies": { "@eslint/js": "^10.0.1", "@lezer/generator": "^1.8.0", + "@types/jest": "^30.0.0", "@types/markdown-it": "^14.1.2", "@types/sortablejs": "^1.15.9", "chokidar-cli": "^3.0", - "esbuild": "^0.27.3", - "eslint": "^10.0.2", + "esbuild": "^0.28.0", + "eslint": "^10.2.0", "globals": "^17.4.0", - "jest": "^30.2.0", - "jest-environment-jsdom": "^30.2.0", + "jest": "^30.3.0", + "jest-environment-jsdom": "^30.3.0", "npm-run-all": "^4.1.5", - "sass": "^1.97.3", - "ts-jest": "^29.4.6", + "sass": "^1.99.0", + "ts-jest": "^29.4.9", "ts-node": "^10.9.2", - "typescript": "5.9.*" + "typescript": "6.0.*" }, "dependencies": { - "@codemirror/commands": "^6.10.2", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-css": "^6.3.1", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-javascript": "^6.2.5", @@ -43,15 +44,14 @@ "@codemirror/lang-markdown": "^6.5.0", "@codemirror/lang-php": "^6.0.2", "@codemirror/lang-xml": "^6.1.0", - "@codemirror/language": "^6.12.2", + "@codemirror/language": "^6.12.3", "@codemirror/legacy-modes": "^6.5.2", - "@codemirror/state": "^6.5.4", + "@codemirror/state": "^6.6.0", "@codemirror/theme-one-dark": "^6.1.3", - "@codemirror/view": "^6.39.16", + "@codemirror/view": "^6.41.0", "@lezer/highlight": "^1.2.3", "@ssddanbrown/codemirror-lang-smarty": "^1.0.0", "@ssddanbrown/codemirror-lang-twig": "^1.0.0", - "@types/jest": "^30.0.0", "codemirror": "^6.0.2", "idb-keyval": "^6.2.2", "markdown-it": "^14.1.1", diff --git a/tsconfig.json b/tsconfig.json index dacaefea2..55b61674c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,15 +2,16 @@ "include": ["resources/js/**/*"], "exclude": ["resources/js/wysiwyg/lexical/yjs/*"], "compilerOptions": { - "target": "es2022", - "module": "commonjs", + "target": "es2023", + "module": "esnext", + "moduleResolution": "bundler", "rootDir": "./resources/js/", - "baseUrl": "./", + "types": ["jest", "node"], "paths": { - "@icons/*": ["resources/icons/*"], - "lexical": ["resources/js/wysiwyg/lexical/core/index.ts"], - "lexical/*": ["resources/js/wysiwyg/lexical/core/*"], - "@lexical/*": ["resources/js/wysiwyg/lexical/*"] + "@icons/*": ["./resources/icons/*"], + "lexical": ["./resources/js/wysiwyg/lexical/core/index.ts"], + "lexical/*": ["./resources/js/wysiwyg/lexical/core/*"], + "@lexical/*": ["./resources/js/wysiwyg/lexical/*"] }, "resolveJsonModule": true, "allowJs": true, From 5e78dc6ed54aeb6d3d2565334074286a0e249b78 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 8 Apr 2026 21:03:20 +0100 Subject: [PATCH 07/74] Maintenance: Updated PHPStan to Level 4 (#6085) --- .gitignore | 1 + app/Access/EmailConfirmationService.php | 2 ++ app/Access/LoginService.php | 2 +- app/Access/Mfa/MfaValue.php | 5 ++- app/Access/Oidc/OidcJwtSigningKey.php | 17 ++++------ app/Access/Oidc/OidcJwtWithClaims.php | 4 +-- app/Access/Oidc/OidcUserDetails.php | 2 +- app/Access/Saml2Service.php | 4 +-- app/Access/SocialAuthService.php | 4 +-- .../Notifications/NotificationManager.php | 14 ++++---- app/Api/ApiDocsGenerator.php | 10 ++++-- app/Api/ApiEntityListFormatter.php | 7 ++-- app/Api/ApiTokenGuard.php | 33 +++++-------------- .../Commands/AssignSortRuleCommand.php | 2 +- .../Commands/CopyShelfPermissionsCommand.php | 17 ++++++---- app/Entities/Models/Book.php | 2 +- app/Entities/Models/Entity.php | 1 + app/Entities/Models/Page.php | 2 +- app/Entities/Repos/PageRepo.php | 2 +- app/Entities/Tools/PageContent.php | 2 +- app/Entities/Tools/PermissionsUpdater.php | 2 +- app/Exports/ExportFormatter.php | 4 +-- app/Exports/ZipExports/ZipImportRunner.php | 14 ++++---- app/Exports/ZipExports/ZipReferenceParser.php | 4 --- app/Http/Controller.php | 2 +- app/Permissions/JointPermissionBuilder.php | 3 +- app/Search/SearchOptions.php | 12 +++---- app/Sorting/BookSorter.php | 3 +- app/Uploads/ImageRepo.php | 2 +- app/Uploads/UserAvatars.php | 2 +- app/Users/Models/User.php | 3 +- phpstan.neon.dist | 4 +-- tests/Api/SearchApiTest.php | 1 + .../CopyShelfPermissionsCommandTest.php | 18 ++++++++++ 34 files changed, 105 insertions(+), 102 deletions(-) diff --git a/.gitignore b/.gitignore index b545d161f..06a8723c5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /node_modules /.vscode /composer +/composer.phar /coverage Homestead.yaml .env diff --git a/app/Access/EmailConfirmationService.php b/app/Access/EmailConfirmationService.php index 1a5156d3e..e950c5504 100644 --- a/app/Access/EmailConfirmationService.php +++ b/app/Access/EmailConfirmationService.php @@ -5,6 +5,7 @@ namespace BookStack\Access; use BookStack\Access\Notifications\ConfirmEmailNotification; use BookStack\Exceptions\ConfirmationEmailException; use BookStack\Users\Models\User; +use Exception; class EmailConfirmationService extends UserTokenService { @@ -16,6 +17,7 @@ class EmailConfirmationService extends UserTokenService * Also removes any existing old ones. * * @throws ConfirmationEmailException + * @throws Exception */ public function sendConfirmation(User $user): void { diff --git a/app/Access/LoginService.php b/app/Access/LoginService.php index c81e95572..0c32b538f 100644 --- a/app/Access/LoginService.php +++ b/app/Access/LoginService.php @@ -71,7 +71,7 @@ class LoginService } $lastLoginDetails = $this->getLastLoginAttemptDetails(); - $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember'] ?? false); + $this->login($user, $lastLoginDetails['method'], $lastLoginDetails['remember']); } /** diff --git a/app/Access/Mfa/MfaValue.php b/app/Access/Mfa/MfaValue.php index dd3e04618..b0f08e826 100644 --- a/app/Access/Mfa/MfaValue.php +++ b/app/Access/Mfa/MfaValue.php @@ -48,17 +48,16 @@ class MfaValue extends Model } /** - * Easily get the decrypted MFA value for the given user and method. + * Get the decrypted MFA value for the given user and method. */ public static function getValueForUser(User $user, string $method): ?string { - /** @var MfaValue $mfaVal */ $mfaVal = static::query() ->where('user_id', '=', $user->id) ->where('method', '=', $method) ->first(); - return $mfaVal ? $mfaVal->getValue() : null; + return $mfaVal?->getValue(); } /** diff --git a/app/Access/Oidc/OidcJwtSigningKey.php b/app/Access/Oidc/OidcJwtSigningKey.php index 3dab3e442..3edba12b3 100644 --- a/app/Access/Oidc/OidcJwtSigningKey.php +++ b/app/Access/Oidc/OidcJwtSigningKey.php @@ -9,10 +9,7 @@ use phpseclib3\Math\BigInteger; class OidcJwtSigningKey { - /** - * @var PublicKey - */ - protected $key; + protected PublicKey $key; /** * Can be created either from a JWK parameter array or local file path to load a certificate from. @@ -20,15 +17,13 @@ class OidcJwtSigningKey * 'file:///var/www/cert.pem' * ['kty' => 'RSA', 'alg' => 'RS256', 'n' => 'abc123...']. * - * @param array|string $jwkOrKeyPath - * * @throws OidcInvalidKeyException */ - public function __construct($jwkOrKeyPath) + public function __construct(array|string $jwkOrKeyPath) { if (is_array($jwkOrKeyPath)) { $this->loadFromJwkArray($jwkOrKeyPath); - } elseif (is_string($jwkOrKeyPath) && strpos($jwkOrKeyPath, 'file://') === 0) { + } elseif (str_starts_with($jwkOrKeyPath, 'file://')) { $this->loadFromPath($jwkOrKeyPath); } else { throw new OidcInvalidKeyException('Unexpected type of key value provided'); @@ -38,7 +33,7 @@ class OidcJwtSigningKey /** * @throws OidcInvalidKeyException */ - protected function loadFromPath(string $path) + protected function loadFromPath(string $path): void { try { $key = PublicKeyLoader::load( @@ -58,7 +53,7 @@ class OidcJwtSigningKey /** * @throws OidcInvalidKeyException */ - protected function loadFromJwkArray(array $jwk) + protected function loadFromJwkArray(array $jwk): void { // 'alg' is optional for a JWK, but we will still attempt to validate if // it exists otherwise presume it will be compatible. @@ -82,7 +77,7 @@ class OidcJwtSigningKey throw new OidcInvalidKeyException('A "n" parameter on the provided key is expected'); } - $n = strtr($jwk['n'] ?? '', '-_', '+/'); + $n = strtr($jwk['n'], '-_', '+/'); try { $key = PublicKeyLoader::load([ diff --git a/app/Access/Oidc/OidcJwtWithClaims.php b/app/Access/Oidc/OidcJwtWithClaims.php index 06c04d81e..9d7eeead1 100644 --- a/app/Access/Oidc/OidcJwtWithClaims.php +++ b/app/Access/Oidc/OidcJwtWithClaims.php @@ -102,12 +102,12 @@ class OidcJwtWithClaims implements ProvidesClaims protected function validateTokenStructure(): void { foreach (['header', 'payload'] as $prop) { - if (empty($this->$prop) || !is_array($this->$prop)) { + if (empty($this->$prop)) { throw new OidcInvalidTokenException("Could not parse out a valid {$prop} within the provided token"); } } - if (empty($this->signature) || !is_string($this->signature)) { + if (empty($this->signature)) { throw new OidcInvalidTokenException('Could not parse out a valid signature within the provided token'); } } diff --git a/app/Access/Oidc/OidcUserDetails.php b/app/Access/Oidc/OidcUserDetails.php index 7a422a58d..b1736f97d 100644 --- a/app/Access/Oidc/OidcUserDetails.php +++ b/app/Access/Oidc/OidcUserDetails.php @@ -39,7 +39,7 @@ class OidcUserDetails ): void { $this->externalId = $claims->getClaim($idClaim) ?? $this->externalId; $this->email = $claims->getClaim('email') ?? $this->email; - $this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name; + $this->name = static::getUserDisplayName($displayNameClaims, $claims) ?: $this->name; $this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups; $this->picture = static::getPicture($claims) ?: $this->picture; } diff --git a/app/Access/Saml2Service.php b/app/Access/Saml2Service.php index 106a7a229..5572d2104 100644 --- a/app/Access/Saml2Service.php +++ b/app/Access/Saml2Service.php @@ -266,7 +266,7 @@ class Saml2Service /** * Extract the details of a user from a SAML response. * - * @return array{external_id: string, name: string, email: string, saml_id: string} + * @return array{external_id: string, name: string, email: string|null, saml_id: string} */ protected function getUserDetails(string $samlID, $samlAttributes): array { @@ -357,7 +357,7 @@ class Saml2Service ]); } - if ($userDetails['email'] === null) { + if (empty($userDetails['email'])) { throw new SamlException(trans('errors.saml_no_email_address')); } diff --git a/app/Access/SocialAuthService.php b/app/Access/SocialAuthService.php index c3c20587d..bdcfb45c8 100644 --- a/app/Access/SocialAuthService.php +++ b/app/Access/SocialAuthService.php @@ -117,14 +117,14 @@ class SocialAuthService } // When a user is logged in and the social account exists and is already linked to the current user. - if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) { + if ($isLoggedIn && $socialAccount->user->id === $currentUser->id) { session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver])); return redirect('/my-account/auth#social_accounts'); } // When a user is logged in, A social account exists but the users do not match. - if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) { + if ($isLoggedIn && $socialAccount->user->id != $currentUser->id) { session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver])); return redirect('/my-account/auth#social_accounts'); diff --git a/app/Activity/Notifications/NotificationManager.php b/app/Activity/Notifications/NotificationManager.php index 8a6c26ffb..38da2c552 100644 --- a/app/Activity/Notifications/NotificationManager.php +++ b/app/Activity/Notifications/NotificationManager.php @@ -15,14 +15,14 @@ use BookStack\Users\Models\User; class NotificationManager { /** - * @var class-string[] + * @var array[]> */ - protected array $handlers = []; + protected array $handlersByActivity = []; public function handle(Activity $activity, string|Loggable $detail, User $user): void { $activityType = $activity->type; - $handlersToRun = $this->handlers[$activityType] ?? []; + $handlersToRun = $this->handlersByActivity[$activityType] ?? []; foreach ($handlersToRun as $handlerClass) { /** @var NotificationHandler $handler */ $handler = new $handlerClass(); @@ -35,12 +35,12 @@ class NotificationManager */ public function registerHandler(string $activityType, string $handlerClass): void { - if (!isset($this->handlers[$activityType])) { - $this->handlers[$activityType] = []; + if (!isset($this->handlersByActivity[$activityType])) { + $this->handlersByActivity[$activityType] = []; } - if (!in_array($handlerClass, $this->handlers[$activityType])) { - $this->handlers[$activityType][] = $handlerClass; + if (!in_array($handlerClass, $this->handlersByActivity[$activityType])) { + $this->handlersByActivity[$activityType][] = $handlerClass; } } diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php index eb8f5508c..a59cb8198 100644 --- a/app/Api/ApiDocsGenerator.php +++ b/app/Api/ApiDocsGenerator.php @@ -17,7 +17,14 @@ use ReflectionMethod; class ApiDocsGenerator { + /** + * @var array + */ protected array $reflectionClasses = []; + + /** + * @var array + */ protected array $controllerClasses = []; /** @@ -107,7 +114,6 @@ class ApiDocsGenerator */ protected function getBodyParamsFromClass(string $className, string $methodName): ?array { - /** @var ApiController $class */ $class = $this->controllerClasses[$className] ?? null; if ($class === null) { $class = app()->make($className); @@ -153,7 +159,7 @@ class ApiDocsGenerator $matches = []; preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches); - $text = implode(' ', $matches[1] ?? []); + $text = implode(' ', $matches[1]); return str_replace(' ', "\n", $text); } diff --git a/app/Api/ApiEntityListFormatter.php b/app/Api/ApiEntityListFormatter.php index 3c94d96ee..23073bfc2 100644 --- a/app/Api/ApiEntityListFormatter.php +++ b/app/Api/ApiEntityListFormatter.php @@ -74,18 +74,21 @@ class ApiEntityListFormatter /** * Include parent book/chapter info in the formatted data. + * These functions are careful to not load the relation themselves, since they should + * have already been loaded in a more efficient manner, with permissions applied, by the time + * the parent fields are handled here. */ public function withParents(): self { $this->withField('book', function (Entity $entity) { - if ($entity instanceof BookChild && $entity->book) { + if ($entity instanceof BookChild && $entity->relationLoaded('book') && $entity->getRelationValue('book')) { return $entity->book->only(['id', 'name', 'slug']); } return null; }); $this->withField('chapter', function (Entity $entity) { - if ($entity instanceof Page && $entity->chapter) { + if ($entity instanceof Page && $entity->relationLoaded('chapter') && $entity->getRelationValue('chapter')) { return $entity->chapter->only(['id', 'name', 'slug']); } return null; diff --git a/app/Api/ApiTokenGuard.php b/app/Api/ApiTokenGuard.php index 9f4537b29..f1a3f0dc8 100644 --- a/app/Api/ApiTokenGuard.php +++ b/app/Api/ApiTokenGuard.php @@ -16,30 +16,15 @@ class ApiTokenGuard implements Guard { use GuardHelpers; - /** - * The request instance. - */ - protected $request; - - /** - * @var LoginService - */ - protected $loginService; - /** * The last auth exception thrown in this request. - * - * @var ApiAuthException */ - protected $lastAuthException; + protected ApiAuthException|null $lastAuthException = null; - /** - * ApiTokenGuard constructor. - */ - public function __construct(Request $request, LoginService $loginService) - { - $this->request = $request; - $this->loginService = $loginService; + public function __construct( + protected Request $request, + protected LoginService $loginService + ) { } /** @@ -67,7 +52,7 @@ class ApiTokenGuard implements Guard } /** - * Determine if current user is authenticated. If not, throw an exception. + * Determine if the current user is authenticated. If not, throw an exception. * * @throws ApiAuthException * @@ -121,7 +106,7 @@ class ApiTokenGuard implements Guard throw new ApiAuthException(trans('errors.api_no_authorization_found')); } - if (strpos($authToken, ':') === false || strpos($authToken, 'Token ') !== 0) { + if (!str_contains($authToken, ':') || !str_starts_with($authToken, 'Token ')) { throw new ApiAuthException(trans('errors.api_bad_authorization_format')); } } @@ -155,7 +140,7 @@ class ApiTokenGuard implements Guard /** * {@inheritdoc} */ - public function validate(array $credentials = []) + public function validate(array $credentials = []): bool { if (empty($credentials['id']) || empty($credentials['secret'])) { return false; @@ -175,7 +160,7 @@ class ApiTokenGuard implements Guard /** * "Log out" the currently authenticated user. */ - public function logout() + public function logout(): void { $this->user = null; } diff --git a/app/Console/Commands/AssignSortRuleCommand.php b/app/Console/Commands/AssignSortRuleCommand.php index c438d0783..f00df8383 100644 --- a/app/Console/Commands/AssignSortRuleCommand.php +++ b/app/Console/Commands/AssignSortRuleCommand.php @@ -32,7 +32,7 @@ class AssignSortRuleCommand extends Command */ public function handle(BookSorter $sorter): int { - $sortRuleId = intval($this->argument('sort-rule')) ?? 0; + $sortRuleId = intval($this->argument('sort-rule')); if ($sortRuleId === 0) { return $this->listSortRules(); } diff --git a/app/Console/Commands/CopyShelfPermissionsCommand.php b/app/Console/Commands/CopyShelfPermissionsCommand.php index c5e2d504e..1207621de 100644 --- a/app/Console/Commands/CopyShelfPermissionsCommand.php +++ b/app/Console/Commands/CopyShelfPermissionsCommand.php @@ -32,6 +32,7 @@ class CopyShelfPermissionsCommand extends Command { $shelfSlug = $this->option('slug'); $cascadeAll = $this->option('all'); + $noInteraction = boolval($this->option('no-interaction')); $shelves = null; if (!$cascadeAll && !$shelfSlug) { @@ -41,14 +42,16 @@ class CopyShelfPermissionsCommand extends Command } if ($cascadeAll) { - $continue = $this->confirm( - 'Permission settings for all shelves will be cascaded. ' . - 'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' . - 'Are you sure you want to proceed?' - ); + if (!$noInteraction) { + $continue = $this->confirm( + 'Permission settings for all shelves will be cascaded. ' . + 'Books assigned to multiple shelves will receive only the permissions of it\'s last processed shelf. ' . + 'Are you sure you want to proceed?', + ); - if (!$continue && !$this->hasOption('no-interaction')) { - return 0; + if (!$continue) { + return 0; + } } $shelves = $queries->start()->get(['id']); diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 1909dbd56..10f04695a 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -17,7 +17,7 @@ use Illuminate\Support\Collection; * * @property string $description * @property string $description_html - * @property int $image_id + * @property ?int $image_id * @property ?int $default_template_id * @property ?int $sort_rule_id * @property \Illuminate\Database\Eloquent\Collection $chapters diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 47e134626..27cfccaa8 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -479,6 +479,7 @@ abstract class Entity extends Model implements 'chapter' => new Chapter(), 'book' => new Book(), 'bookshelf' => new Bookshelf(), + default => throw new \InvalidArgumentException("Invalid entity type: {$type}"), }; } } diff --git a/app/Entities/Models/Page.php b/app/Entities/Models/Page.php index a1d3fc7b4..d3a392da6 100644 --- a/app/Entities/Models/Page.php +++ b/app/Entities/Models/Page.php @@ -23,7 +23,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne; * @property bool $draft * @property int $revision_count * @property string $editor - * @property Chapter $chapter + * @property Chapter|null $chapter * @property Collection $attachments * @property Collection $revisions * @property PageRevision $currentRevision diff --git a/app/Entities/Repos/PageRepo.php b/app/Entities/Repos/PageRepo.php index bc590785d..375bf1d2b 100644 --- a/app/Entities/Repos/PageRepo.php +++ b/app/Entities/Repos/PageRepo.php @@ -60,7 +60,7 @@ class PageRepo $page->book_id = $parent->id; } - $defaultTemplate = $page->chapter?->defaultTemplate()->get() ?? $page->book?->defaultTemplate()->get(); + $defaultTemplate = $page->chapter?->defaultTemplate()->get() ?? $page->book->defaultTemplate()->get(); if ($defaultTemplate) { $page->forceFill([ 'html' => $defaultTemplate->html, diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 8d89a86cf..6a7649900 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -359,7 +359,7 @@ class PageContent { $contentHash = md5($html); $contentId = $this->page->id; - $contentTime = $this->page->updated_at?->timestamp ?? time(); + $contentTime = $this->page->updated_at->timestamp ?? time(); $appVersion = AppVersion::get(); $filterConfig = config('app.content_filtering') ?? ''; return "page-content-cache::{$filterConfig}::{$appVersion}::{$contentId}::{$contentTime}::{$contentHash}"; diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index fa9ae753c..f3165b603 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -47,7 +47,7 @@ class PermissionsUpdater { if (isset($data['role_permissions'])) { $entity->permissions()->where('role_id', '!=', 0)->delete(); - $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false); + $rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'], false); $entity->permissions()->createMany($rolePermissionData); } diff --git a/app/Exports/ExportFormatter.php b/app/Exports/ExportFormatter.php index c5973eace..6bf0a05ad 100644 --- a/app/Exports/ExportFormatter.php +++ b/app/Exports/ExportFormatter.php @@ -208,7 +208,7 @@ class ExportFormatter preg_match_all("/\/i", $htmlContent, $imageTagsOutput); // Replace image src with base64 encoded image strings - if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) { + if (count($imageTagsOutput[0]) > 0) { foreach ($imageTagsOutput[0] as $index => $imgMatch) { $oldImgTagString = $imgMatch; $srcString = $imageTagsOutput[2][$index]; @@ -225,7 +225,7 @@ class ExportFormatter preg_match_all("/\/i", $htmlContent, $linksOutput); // Update relative links to be absolute, with instance url - if (isset($linksOutput[0]) && count($linksOutput[0]) > 0) { + if (count($linksOutput[0]) > 0) { foreach ($linksOutput[0] as $index => $linkMatch) { $oldLinkString = $linkMatch; $srcString = $linksOutput[2][$index]; diff --git a/app/Exports/ZipExports/ZipImportRunner.php b/app/Exports/ZipExports/ZipImportRunner.php index 382e4073e..9fa7dec3a 100644 --- a/app/Exports/ZipExports/ZipImportRunner.php +++ b/app/Exports/ZipExports/ZipImportRunner.php @@ -82,10 +82,8 @@ class ZipImportRunner $entity = $this->importBook($exportModel, $reader); } else if ($exportModel instanceof ZipExportChapter) { $entity = $this->importChapter($exportModel, $parent, $reader); - } else if ($exportModel instanceof ZipExportPage) { - $entity = $this->importPage($exportModel, $parent, $reader); } else { - throw new ZipImportException(['No importable data found in import data.']); + $entity = $this->importPage($exportModel, $parent, $reader); } $this->references->replaceReferences(); @@ -132,7 +130,7 @@ class ZipImportRunner 'name' => $exportBook->name, 'description_html' => $exportBook->description_html ?? '', 'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null, - 'tags' => $this->exportTagsToInputArray($exportBook->tags ?? []), + 'tags' => $this->exportTagsToInputArray($exportBook->tags), ]); if ($book->coverInfo()->getImage()) { @@ -151,7 +149,7 @@ class ZipImportRunner foreach ($children as $child) { if ($child instanceof ZipExportChapter) { $this->importChapter($child, $book, $reader); - } else if ($child instanceof ZipExportPage) { + } else { $this->importPage($child, $book, $reader); } } @@ -166,7 +164,7 @@ class ZipImportRunner $chapter = $this->chapterRepo->create([ 'name' => $exportChapter->name, 'description_html' => $exportChapter->description_html ?? '', - 'tags' => $this->exportTagsToInputArray($exportChapter->tags ?? []), + 'tags' => $this->exportTagsToInputArray($exportChapter->tags), ], $parent); $exportPages = $exportChapter->pages; @@ -199,7 +197,7 @@ class ZipImportRunner 'name' => $exportPage->name, 'markdown' => $exportPage->markdown ?? '', 'html' => $exportPage->html ?? '', - 'tags' => $this->exportTagsToInputArray($exportPage->tags ?? []), + 'tags' => $this->exportTagsToInputArray($exportPage->tags), ]); $this->references->addPage($page, $exportPage); @@ -302,7 +300,7 @@ class ZipImportRunner array_push($chapters, ...$exportModel->chapters); } else if ($exportModel instanceof ZipExportChapter) { $chapters[] = $exportModel; - } else if ($exportModel instanceof ZipExportPage) { + } else { $pages[] = $exportModel; } diff --git a/app/Exports/ZipExports/ZipReferenceParser.php b/app/Exports/ZipExports/ZipReferenceParser.php index a6560e3f2..9bb069ab7 100644 --- a/app/Exports/ZipExports/ZipReferenceParser.php +++ b/app/Exports/ZipExports/ZipReferenceParser.php @@ -68,10 +68,6 @@ class ZipReferenceParser $matches = []; preg_match_all($referenceRegex, $content, $matches); - if (count($matches) < 3) { - return $content; - } - for ($i = 0; $i < count($matches[0]); $i++) { $referenceText = $matches[0][$i]; $type = strtolower($matches[1][$i]); diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 1a0f5932e..796505795 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -62,7 +62,7 @@ abstract class Controller extends BaseController */ protected function checkPermission(string|Permission $permission): void { - if (!user() || !user()->can($permission)) { + if (!user()->can($permission)) { $this->showPermissionError(); } } diff --git a/app/Permissions/JointPermissionBuilder.php b/app/Permissions/JointPermissionBuilder.php index 56b22ad16..94f18916d 100644 --- a/app/Permissions/JointPermissionBuilder.php +++ b/app/Permissions/JointPermissionBuilder.php @@ -61,8 +61,7 @@ class JointPermissionBuilder return; } - /** @var BookChild $entity */ - if ($entity->book) { + if ($entity instanceof BookChild) { $entities[] = $entity->book; } diff --git a/app/Search/SearchOptions.php b/app/Search/SearchOptions.php index 83af2d043..cfd068386 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -121,13 +121,11 @@ class SearchOptions foreach ($patterns as $termType => $pattern) { $matches = []; preg_match_all($pattern, $searchString, $matches); - if (count($matches) > 0) { - foreach ($matches[1] as $index => $value) { - $negated = str_starts_with($matches[0][$index], '-'); - $terms[$termType][] = $constructors[$termType]($value, $negated); - } - $searchString = preg_replace($pattern, '', $searchString); + foreach ($matches[1] as $index => $value) { + $negated = str_starts_with($matches[0][$index], '-'); + $terms[$termType][] = $constructors[$termType]($value, $negated); } + $searchString = preg_replace($pattern, '', $searchString); } // Unescape exacts and backslash escapes @@ -261,7 +259,7 @@ class SearchOptions $userFilters = ['updated_by', 'created_by', 'owned_by']; $unsupportedFilters = ['is_template', 'sort_by']; foreach ($this->filters->all() as $filter) { - if (in_array($filter->getKey(), $userFilters, true) && $filter->value !== null && $filter->value !== 'me') { + if (in_array($filter->getKey(), $userFilters, true) && $filter->value && $filter->value !== 'me') { $options[] = $filter; } else if (in_array($filter->getKey(), $unsupportedFilters, true)) { $options[] = $filter; diff --git a/app/Sorting/BookSorter.php b/app/Sorting/BookSorter.php index b4f93d47b..0862aaa88 100644 --- a/app/Sorting/BookSorter.php +++ b/app/Sorting/BookSorter.php @@ -125,9 +125,8 @@ class BookSorter */ protected function applySortUpdates(BookSortMapItem $sortMapItem, array $modelMap): void { - /** @var BookChild $model */ $model = $modelMap[$sortMapItem->type . ':' . $sortMapItem->id] ?? null; - if (!$model) { + if (!($model instanceof BookChild)) { return; } diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index a16b87bd7..e87e22b3a 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -91,7 +91,7 @@ class ImageRepo $parentFilter = function (Builder $query) use ($filterType, $contextPage) { if ($filterType === 'page') { $query->where('uploaded_to', '=', $contextPage->id); - } else if ($filterType === 'book') { + } else { $validPageIds = $contextPage->book->pages() ->scopes('visible') ->pluck('id') diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index 0cc640f22..8fcf63580 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -148,7 +148,7 @@ class UserAvatars $responseCount++; $isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302); $url = $response->getHeader('Location')[0] ?? ''; - } while ($responseCount < 3 && $isRedirect && is_string($url) && str_starts_with($url, 'http')); + } while ($responseCount < 3 && $isRedirect && str_starts_with($url, 'http')); if ($responseCount === 3) { throw new HttpFetchException("Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}"); diff --git a/app/Users/Models/User.php b/app/Users/Models/User.php index 50efdcdad..b9289a5a7 100644 --- a/app/Users/Models/User.php +++ b/app/Users/Models/User.php @@ -222,8 +222,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon public function getAvatar(int $size = 50): string { $default = url('/user_avatar.png'); - $imageId = $this->image_id; - if ($imageId === 0 || $imageId === '0' || $imageId === null) { + if ($this->image_id === 0) { return $default; } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 72189222f..bab28ea0e 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,11 +7,11 @@ parameters: - app # The level 8 is the highest level - level: 3 + level: 4 phpVersion: min: 80200 - max: 80400 + max: 80500 bootstrapFiles: - bootstrap/phpstan.php diff --git a/tests/Api/SearchApiTest.php b/tests/Api/SearchApiTest.php index 517c5d8e4..5d0ce53ec 100644 --- a/tests/Api/SearchApiTest.php +++ b/tests/Api/SearchApiTest.php @@ -106,6 +106,7 @@ class SearchApiTest extends TestCase $this->permissions->setEntityPermissions($page, ['view'], [$editor->roles()->first()]); $resp = $this->getJson($this->baseEndpoint . '?query=superextrauniquevalue'); + $resp->assertOk(); $resp->assertJsonPath('data.0.id', $page->id); $resp->assertJsonPath('data.0.book.name', $book->name); $resp->assertJsonMissingPath('data.0.chapter'); diff --git a/tests/Commands/CopyShelfPermissionsCommandTest.php b/tests/Commands/CopyShelfPermissionsCommandTest.php index 5c21a2e34..d5f9677a2 100644 --- a/tests/Commands/CopyShelfPermissionsCommandTest.php +++ b/tests/Commands/CopyShelfPermissionsCommandTest.php @@ -2,6 +2,7 @@ namespace Tests\Commands; +use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Bookshelf; use Tests\TestCase; @@ -61,4 +62,21 @@ class CopyShelfPermissionsCommandTest extends TestCase 'view' => true, 'update' => true, 'create' => false, 'delete' => false, ]); } + + public function test_copy_shelf_permissions_command_using_slug_without_interaction() + { + $shelf = $this->entities->shelfHasBooks(); + $editorRole = $this->users->editor()->roles()->first(); + /** @var Book $child */ + $child = $shelf->books()->first(); + $child->shelves()->where('id', '!=', $shelf->id)->delete(); + + $this->assertFalse($child->hasPermissions()); + + $this->permissions->setEntityPermissions($shelf, ['view', 'update'], [$editorRole]); + $this->artisan('bookstack:copy-shelf-permissions --all --no-interaction'); + + $child->refresh(); + $this->assertTrue($child->hasPermissions(), 'Child book should now be restricted'); + } } From 3d9d5fef51c29328aa071c906ccfda9dd77fed7b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 11 Apr 2026 14:34:54 +0100 Subject: [PATCH 08/74] Theme Modules: Updated install command to handle nested folder Theme module ZIPs will now support their files being in a single nested directory within a ZIP, to support common ZIP structure approaches. Added test to cover. For #6066 --- app/Theming/ThemeModuleZip.php | 55 ++++++++++++++++++++- dev/docs/theme-system-modules.md | 1 + tests/Commands/InstallModuleCommandTest.php | 29 +++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/app/Theming/ThemeModuleZip.php b/app/Theming/ThemeModuleZip.php index 7029fa0c6..4785abadb 100644 --- a/app/Theming/ThemeModuleZip.php +++ b/app/Theming/ThemeModuleZip.php @@ -15,7 +15,41 @@ readonly class ThemeModuleZip { $zip = new ZipArchive(); $zip->open($this->path); - $zip->extractTo($destinationPath); + $prefix = $this->getZipContentPrefix($zip); + + for ($i = 0; $i < $zip->numFiles; $i++) { + $name = $zip->getNameIndex($i); + $entryIsDir = str_ends_with($name, "/"); + if ($entryIsDir) { + continue; + } + + $stream = $zip->getStreamIndex($i); + + if ($prefix) { + if (!str_starts_with($name, $prefix) || $name === $prefix) { + continue; + } + $name = str_replace($prefix, '', $name); + } + + $targetPath = $destinationPath . DIRECTORY_SEPARATOR . $name; + $targetPathDir = dirname($targetPath); + if (!is_dir($targetPathDir)) { + $dirCreated = mkdir($targetPathDir, 0777, true); + if (!$dirCreated) { + throw new ThemeModuleException("Failed to create directory {$targetPathDir} when extracting module files"); + } + } + + $targetFile = fopen($targetPath, 'w'); + $written = stream_copy_to_stream($stream, $targetFile); + if (!$written) { + throw new ThemeModuleException("Failed to write to {$targetPath} when extracting module files"); + } + fclose($targetFile); + } + $zip->close(); } @@ -31,7 +65,8 @@ readonly class ThemeModuleZip throw new ThemeModuleException("Unable to open zip file at {$this->path}"); } - $moduleJsonText = $zip->getFromName('bookstack-module.json'); + $prefix = $this->getZipContentPrefix($zip); + $moduleJsonText = $zip->getFromName("{$prefix}bookstack-module.json"); $zip->close(); if ($moduleJsonText === false) { @@ -95,4 +130,20 @@ readonly class ThemeModuleZip return $totalSize; } + + protected function getZipContentPrefix(ZipArchive $zip): string + { + $index = $zip->locateName('bookstack-module.json', ZipArchive::FL_NODIR); + if ($index === false) { + return ''; + } + + $location = $zip->getNameIndex($index); + $pathParts = explode('/', $location); + if (count($pathParts) !== 2) { + return ''; + } + + return $pathParts[0] . '/'; + } } diff --git a/dev/docs/theme-system-modules.md b/dev/docs/theme-system-modules.md index 8aa9370ed..0086ac9c0 100644 --- a/dev/docs/theme-system-modules.md +++ b/dev/docs/theme-system-modules.md @@ -66,6 +66,7 @@ Here are some general best practices when it comes to creating modules: ### Distribution Format Modules are expected to be distributed as a compressed ZIP file, where the ZIP contents follow that of a module folder. +Contents may optionally be placed within a nested folder inside the ZIP. BookStack provides a `php artisan bookstack:install-module` command which allows modules to be installed from these ZIP files, either from a local path or from a web URL. Currently, there's a hardcoded total filesize limit of 50MB for module contents installed via this method. diff --git a/tests/Commands/InstallModuleCommandTest.php b/tests/Commands/InstallModuleCommandTest.php index 8ffc4ead3..ee8da1128 100644 --- a/tests/Commands/InstallModuleCommandTest.php +++ b/tests/Commands/InstallModuleCommandTest.php @@ -175,6 +175,35 @@ class InstallModuleCommandTest extends TestCase ->assertExitCode(1); } + public function test_module_zip_when_files_in_nested_directory() + { + $this->usingThemeFolder(function ($themeFolder) { + $zip = new ZipArchive(); + $zipFile = tempnam(sys_get_temp_dir(), 'bs-test-module'); + $zip->open($zipFile, ZipArchive::CREATE); + + $zip->addEmptyDir('mod'); + $zip->addFromString('mod/bookstack-module.json', json_encode($metadata ?? [ + 'name' => 'Test Module', + 'description' => 'A test module for BookStack', + 'version' => '1.0.0', + ])); + $zip->addFromString('mod/functions.php', 'addEmptyDir('mod/a'); + $zip->addFromString('mod/a/cat.txt', 'Meow'); + $zip->close(); + + $this->artisan('bookstack:install-module', ['location' => $zipFile]) + ->expectsConfirmation('Are you sure you want to install this module?', 'yes') + ->assertExitCode(0); + + $modulePath = glob(theme_path('modules/*'), GLOB_ONLYDIR)[0]; + $this->assertFileExists($modulePath . '/a/cat.txt'); + $contents = file_get_contents($modulePath . '/a/cat.txt'); + $this->assertEquals('Meow', $contents); + }); + } + public function test_local_module_install_without_active_theme_can_setup_theme_folder() { $zip = $this->getModuleZipPath(); From 5fbaab474058c9c90496649d53dd002e25419884 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 11 Apr 2026 17:23:11 +0100 Subject: [PATCH 09/74] Theme modules: Allowed cross-origin redirects on download With a prompt to the user to confirm they trust the origin. For #6066 Added tests to cover. --- app/Console/Commands/InstallModuleCommand.php | 22 ++++++--- tests/Commands/InstallModuleCommandTest.php | 48 ++++++++++++++++++- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/app/Console/Commands/InstallModuleCommand.php b/app/Console/Commands/InstallModuleCommand.php index 20252525d..114bfb105 100644 --- a/app/Console/Commands/InstallModuleCommand.php +++ b/app/Console/Commands/InstallModuleCommand.php @@ -213,15 +213,23 @@ class InstallModuleCommand extends Command $redirectLocation = $resp->getHeaderLine('Location'); if ($redirectLocation) { $redirectUrl = parse_url($redirectLocation); - if ( - ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '') + $redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '') && ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '') - && ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? '') - ) { - $currentLocation = $redirectLocation; - $redirectCount++; - continue; + && ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? ''); + + if (!$redirectOriginMatches) { + $redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : ''); + $this->info("The download URL is redirecting to a different site: {$redirectOrigin}"); + $shouldContinue = $this->confirm("Do you trust downloading the module from this site?"); + if (!$shouldContinue) { + $this->error("Stopping module installation"); + return null; + } } + + $currentLocation = $redirectLocation; + $redirectCount++; + continue; } } diff --git a/tests/Commands/InstallModuleCommandTest.php b/tests/Commands/InstallModuleCommandTest.php index ee8da1128..c085c4907 100644 --- a/tests/Commands/InstallModuleCommandTest.php +++ b/tests/Commands/InstallModuleCommandTest.php @@ -96,18 +96,44 @@ class InstallModuleCommandTest extends TestCase }); } - public function test_remote_module_install_does_not_follow_redirects_to_different_origin() + public function test_remote_module_install_prompts_on_following_redirects_to_different_origin() { $this->usingThemeFolder(function () { $zip = $this->getModuleZipPath(); $http = $this->mockHttpClient([ new Response(302, ['Location' => 'http://example.com/a-test-module.zip']), + new Response(301, ['Location' => 'https://a.example.com:8080/a-test-module.zip']), new Response(200, ['Content-Length' => filesize($zip)], file_get_contents($zip)) ]); $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip']) ->expectsConfirmation('Are you sure you trust this source?', 'yes') + ->expectsOutput('The download URL is redirecting to a different site: http://example.com') + ->expectsConfirmation('Do you trust downloading the module from this site?', 'yes') + ->expectsOutput('The download URL is redirecting to a different site: https://a.example.com:8080') + ->expectsConfirmation('Do you trust downloading the module from this site?', 'yes') + ->assertExitCode(0); + + $this->assertEquals(3, $http->requestCount()); + $this->assertEquals('https', $http->requestAt(0)->getUri()->getScheme()); + $this->assertEquals('http', $http->requestAt(1)->getUri()->getScheme()); + $this->assertEquals('a.example.com', $http->requestAt(2)->getUri()->getHost()); + }); + } + + public function test_remote_module_install_redirect_origin_prompt_rejection() + { + $this->usingThemeFolder(function () { + $http = $this->mockHttpClient([ + new Response(302, ['Location' => 'http://example.com/a-test-module.zip']), + new Response(301, ['Location' => 'https://a.example.com:8080/a-test-module.zip']), + ]); + + $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip']) + ->expectsConfirmation('Are you sure you trust this source?', 'yes') + ->expectsOutput('The download URL is redirecting to a different site: http://example.com') + ->expectsConfirmation('Do you trust downloading the module from this site?', 'no') ->assertExitCode(1); $this->assertEquals(1, $http->requestCount()); @@ -115,6 +141,26 @@ class InstallModuleCommandTest extends TestCase }); } + public function test_remote_module_install_has_redirect_limit() + { + $this->usingThemeFolder(function () { + $http = $this->mockHttpClient([ + new Response(302, ['Location' => 'https://example.com/a-test-module.zip']), + new Response(302, ['Location' => 'https://example.com/b-test-module.zip']), + new Response(302, ['Location' => 'https://example.com/c-test-module.zip']), + new Response(302, ['Location' => 'https://example.com/d-test-module.zip']), + ]); + + $this->artisan('bookstack:install-module', ['location' => 'https://example.com/test-module.zip']) + ->expectsConfirmation('Are you sure you trust this source?', 'yes') + ->expectsOutput('ERROR: Failed to download module from https://example.com/test-module.zip') + ->assertExitCode(1); + + $this->assertEquals(4, $http->requestCount()); + $this->assertEquals('/c-test-module.zip', $http->requestAt(3)->getUri()->getPath()); + }); + } + public function test_remote_module_install_download_failures_are_announced_to_user() { $this->usingThemeFolder(function () { From 684a94c4195e7d0c7b10268cd4a99717ad637710 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 11 Apr 2026 18:49:34 +0100 Subject: [PATCH 10/74] Theme Modules: Prevented zip-slip in new module extraction method Updated the new (development only) approach which could result in zip-slip causing trouble. This adds path normalisation, and testing to cover. --- app/Theming/ThemeModuleManager.php | 9 ++++++++- app/Theming/ThemeModuleZip.php | 8 +++++++- tests/Commands/InstallModuleCommandTest.php | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/app/Theming/ThemeModuleManager.php b/app/Theming/ThemeModuleManager.php index 86362a2f3..ed33014eb 100644 --- a/app/Theming/ThemeModuleManager.php +++ b/app/Theming/ThemeModuleManager.php @@ -51,7 +51,14 @@ class ThemeModuleManager } $folderPath = $this->modulesFolderPath . DIRECTORY_SEPARATOR . $folderName; - $zip->extractTo($folderPath); + try { + $zip->extractTo($folderPath); + } catch (ThemeModuleException $exception) { + if (is_dir($folderPath)) { + $this->deleteDirectoryRecursively($folderPath); + } + throw new ThemeModuleException("Failed to load extract files from module ZIP with error: {$exception->getMessage()}"); + } $module = $this->loadFromFolder($folderName); if (!$module) { diff --git a/app/Theming/ThemeModuleZip.php b/app/Theming/ThemeModuleZip.php index 4785abadb..7e94074c8 100644 --- a/app/Theming/ThemeModuleZip.php +++ b/app/Theming/ThemeModuleZip.php @@ -2,6 +2,7 @@ namespace BookStack\Theming; +use BookStack\Util\FilePathNormalizer; use ZipArchive; readonly class ThemeModuleZip @@ -33,7 +34,12 @@ readonly class ThemeModuleZip $name = str_replace($prefix, '', $name); } - $targetPath = $destinationPath . DIRECTORY_SEPARATOR . $name; + try { + $targetPath = $destinationPath . DIRECTORY_SEPARATOR . FilePathNormalizer::normalize($name); + } catch (\Exception $exception) { + throw new ThemeModuleException("Bad file path found in module ZIP file: {$name}"); + } + $targetPathDir = dirname($targetPath); if (!is_dir($targetPathDir)) { $dirCreated = mkdir($targetPathDir, 0777, true); diff --git a/tests/Commands/InstallModuleCommandTest.php b/tests/Commands/InstallModuleCommandTest.php index c085c4907..e96fc02c1 100644 --- a/tests/Commands/InstallModuleCommandTest.php +++ b/tests/Commands/InstallModuleCommandTest.php @@ -250,6 +250,23 @@ class InstallModuleCommandTest extends TestCase }); } + public function test_module_install_negates_zip_slip() + { + $this->usingThemeFolder(function () { + $zip = $this->getModuleZipPath(null, [ + '../parent.txt' => str_repeat('dog', 10) + ]); + + $expectedInstallPath = theme_path('modules/test-module'); + $this->artisan('bookstack:install-module', ['location' => $zip]) + ->expectsConfirmation('Are you sure you want to install this module?', 'yes') + ->expectsOutput("ERROR: Failed to install module with error: Failed to load extract files from module ZIP with error: Bad file path found in module ZIP file: ../parent.txt") + ->assertExitCode(1); + + $this->assertDirectoryDoesNotExist($expectedInstallPath); + }); + } + public function test_local_module_install_without_active_theme_can_setup_theme_folder() { $zip = $this->getModuleZipPath(); From 4e3fa4822ff747d7a13c69c6d5d6a04899b33565 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Apr 2026 14:31:40 +0100 Subject: [PATCH 11/74] Sort Rules: Added creation hints to sort rule selection To help direct/indicate how rules can be created. For #5967 --- lang/en/entities.php | 1 + resources/views/books/sort.blade.php | 12 +++++++++++- .../views/settings/categories/sorting.blade.php | 7 +++++-- tests/Sorting/BookSortTest.php | 15 +++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lang/en/entities.php b/lang/en/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/resources/views/books/sort.blade.php b/resources/views/books/sort.blade.php index e090708b1..37a87d9ab 100644 --- a/resources/views/books/sort.blade.php +++ b/resources/views/books/sort.blade.php @@ -20,7 +20,12 @@

    {{ trans('entities.books_sort') }}

    -

    {{ trans('entities.books_sort_desc') }}

    +
    +

    {{ trans('entities.books_sort_desc') }}

    + @if(!userCan(\BookStack\Permissions\Permission::SettingsManage)) +

    {{ trans('entities.books_sort_auto_sort_creation_hint') }}

    + @endif +
    @php $autoSortVal = intval(old('auto-sort') ?? $book->sort_rule_id ?? 0); @@ -41,6 +46,11 @@ @endforeach + @if(userCan(\BookStack\Permissions\Permission::SettingsManage)) +

    + {{ trans('settings.sort_rule_create') }} +

    + @endif
    diff --git a/resources/views/settings/categories/sorting.blade.php b/resources/views/settings/categories/sorting.blade.php index 0c9dc1f95..5678434e0 100644 --- a/resources/views/settings/categories/sorting.blade.php +++ b/resources/views/settings/categories/sorting.blade.php @@ -38,9 +38,9 @@
    -

    {{ trans('settings.sorting_book_default_desc') }}

    +

    {{ trans('settings.sorting_book_default_desc') }}

    -
    +
    +

    + {{ trans('settings.sort_rule_create') }} +

    diff --git a/tests/Sorting/BookSortTest.php b/tests/Sorting/BookSortTest.php index 7f31f9c27..33a10609a 100644 --- a/tests/Sorting/BookSortTest.php +++ b/tests/Sorting/BookSortTest.php @@ -271,6 +271,21 @@ class BookSortTest extends TestCase $this->withHtml($resp)->assertElementExists('select[name="auto-sort"] option[value="' . $sort->id . '"]'); } + public function test_auto_sort_rule_create_hint_shown_on_sort_page() + { + $book = $this->entities->book(); + $hintText = 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.'; + + // Admin users see link for creating new rule + $resp = $this->asAdmin()->get($book->getUrl('/sort')); + $this->withHtml($resp)->assertLinkExists(url('/settings/sorting/rules/new'), 'Create Sort Rule'); + $resp->assertDontSee($hintText); + + // Non-admin users see help text + $resp = $this->asEditor()->get($book->getUrl('/sort')); + $resp->assertSee($hintText); + } + public function test_auto_sort_option_submit_saves_to_book() { $sort = SortRule::factory()->create(); From c7e2b487c14133ef3fa62321fb226501a0f3fc88 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Apr 2026 15:17:31 +0100 Subject: [PATCH 12/74] Attachments: Aligned ZipExportAttachment link validation With controller routes. Don't consider this as a security issue, since the filtered URLs by that validation are very likely to be blocked by browser security or CSP, and there's a level of assumed privilege to the users that are able to create such attachments links already. Closes #6093 --- .../ZipExports/Models/ZipExportAttachment.php | 2 +- tests/Exports/ZipExportValidatorTest.php | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/app/Exports/ZipExports/Models/ZipExportAttachment.php b/app/Exports/ZipExports/Models/ZipExportAttachment.php index 97995738f..88c20e4d3 100644 --- a/app/Exports/ZipExports/Models/ZipExportAttachment.php +++ b/app/Exports/ZipExports/Models/ZipExportAttachment.php @@ -45,7 +45,7 @@ final class ZipExportAttachment extends ZipExportModel $rules = [ 'id' => ['nullable', 'int', $context->uniqueIdRule('attachment')], 'name' => ['required', 'string', 'min:1'], - 'link' => ['required_without:file', 'nullable', 'string'], + 'link' => ['required_without:file', 'nullable', 'string', 'safe_url'], 'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()], ]; diff --git a/tests/Exports/ZipExportValidatorTest.php b/tests/Exports/ZipExportValidatorTest.php index c453ef294..e801705be 100644 --- a/tests/Exports/ZipExportValidatorTest.php +++ b/tests/Exports/ZipExportValidatorTest.php @@ -90,4 +90,29 @@ class ZipExportValidatorTest extends TestCase $this->assertEquals('The file needs to reference a file of type image/png,image/jpeg,image/gif,image/webp, found text/plain.', $results['page.images.0.file']); } + + public function test_page_link_attachments_cant_be_data_or_js() + { + $validateResultCountByLink = [ + 'data:text/html,

    hi

    ' => 1, + 'javascript:alert(\'hi\')' => 1, + 'mailto:email@example.com' => 0, + ]; + + foreach ($validateResultCountByLink as $link => $count) { + $validator = $this->getValidatorForData([ + 'page' => [ + 'id' => 4, + 'name' => 'My page', + 'markdown' => 'hello', + 'attachments' => [ + ['id' => 4, 'name' => 'Attachment A', 'link' => $link], + ], + ] + ]); + + $results = $validator->validate(); + $this->assertCount($count, $results); + } + } } From 4feb50e7ee06206b2ef037090673cabb70a17c73 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Apr 2026 15:29:00 +0100 Subject: [PATCH 13/74] Attachments: Aligned attachment validation a little more --- app/Exports/ZipExports/Models/ZipExportAttachment.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Exports/ZipExports/Models/ZipExportAttachment.php b/app/Exports/ZipExports/Models/ZipExportAttachment.php index 88c20e4d3..161186687 100644 --- a/app/Exports/ZipExports/Models/ZipExportAttachment.php +++ b/app/Exports/ZipExports/Models/ZipExportAttachment.php @@ -45,7 +45,7 @@ final class ZipExportAttachment extends ZipExportModel $rules = [ 'id' => ['nullable', 'int', $context->uniqueIdRule('attachment')], 'name' => ['required', 'string', 'min:1'], - 'link' => ['required_without:file', 'nullable', 'string', 'safe_url'], + 'link' => ['required_without:file', 'nullable', 'string', 'max:2000', 'safe_url'], 'file' => ['required_without:link', 'nullable', 'string', $context->fileReferenceRule()], ]; From f14fc68b6697c203f4012c5bf537dcb49db85f87 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Apr 2026 18:26:00 +0100 Subject: [PATCH 14/74] API: Added new tags API endpoints --- app/Activity/Controllers/TagApiController.php | 51 +++++++++++++++++++ app/Activity/Controllers/TagController.php | 4 +- app/Activity/TagRepo.php | 34 ++++++++++--- app/Api/ListingResponseBuilder.php | 32 +++++++++--- app/Http/ApiController.php | 6 ++- routes/api.php | 4 ++ 6 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 app/Activity/Controllers/TagApiController.php diff --git a/app/Activity/Controllers/TagApiController.php b/app/Activity/Controllers/TagApiController.php new file mode 100644 index 000000000..c1945ba5c --- /dev/null +++ b/app/Activity/Controllers/TagApiController.php @@ -0,0 +1,51 @@ +tagRepo + ->queryWithTotalsForApi(''); + + return $this->apiListingResponse($tagQuery, [ + 'name', 'values', 'usages', 'page_count', 'chapter_count', 'book_count', 'shelf_count', + ], [], [ + 'name' + ]); + } + + /** + * Get a list of tag values used in the system, which have been used for the given tag name. + * You'll only see results based on tags applied to content you have access to. + * Only the value field can be used in filters. + */ + public function listValues(string $name): JsonResponse + { + $tagQuery = $this->tagRepo + ->queryWithTotalsForApi($name); + + return $this->apiListingResponse($tagQuery, [ + 'name', 'value', 'usages', 'page_count', 'chapter_count', 'book_count', 'shelf_count', + ], [], [ + 'name', 'value', + ]); + } +} diff --git a/app/Activity/Controllers/TagController.php b/app/Activity/Controllers/TagController.php index 0af8835ca..723dc4ab4 100644 --- a/app/Activity/Controllers/TagController.php +++ b/app/Activity/Controllers/TagController.php @@ -24,9 +24,9 @@ class TagController extends Controller 'usages' => trans('entities.tags_usages'), ]); - $nameFilter = $request->get('name', ''); + $nameFilter = $request->input('name', ''); $tags = $this->tagRepo - ->queryWithTotals($listOptions, $nameFilter) + ->queryWithTotalsForList($listOptions, $nameFilter) ->paginate(50) ->appends(array_filter(array_merge($listOptions->getPaginationAppends(), [ 'name' => $nameFilter, diff --git a/app/Activity/TagRepo.php b/app/Activity/TagRepo.php index 82c26b00e..3e8d5545a 100644 --- a/app/Activity/TagRepo.php +++ b/app/Activity/TagRepo.php @@ -18,9 +18,10 @@ class TagRepo } /** - * Start a query against all tags in the system. + * Start a query against all tags in the system, with total counts for their usage, + * suitable for a system interface list with listing options. */ - public function queryWithTotals(SimpleListOptions $listOptions, string $nameFilter): Builder + public function queryWithTotalsForList(SimpleListOptions $listOptions, string $nameFilter): Builder { $searchTerm = $listOptions->getSearch(); $sort = $listOptions->getSort(); @@ -28,17 +29,34 @@ class TagRepo $sort = 'value'; } + $query = $this->baseQueryWithTotals($nameFilter, $searchTerm) + ->orderBy($sort, $listOptions->getOrder()); + + return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type'); + } + + /** + * Start a query against all tags in the system, with total counts for their usage, + * which can be used via the API. + */ + public function queryWithTotalsForApi(string $nameFilter): Builder + { + $query = $this->baseQueryWithTotals($nameFilter, ''); + return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type'); + } + + protected function baseQueryWithTotals(string $nameFilter, string $searchTerm): Builder + { $query = Tag::query() ->select([ 'name', ($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'), DB::raw('COUNT(id) as usages'), - DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'), - DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'), - DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'), - DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'), + DB::raw('CAST(SUM(IF(entity_type = \'page\', 1, 0)) as UNSIGNED) as page_count'), + DB::raw('CAST(SUM(IF(entity_type = \'chapter\', 1, 0)) as UNSIGNED) as chapter_count'), + DB::raw('CAST(SUM(IF(entity_type = \'book\', 1, 0)) as UNSIGNED) as book_count'), + DB::raw('CAST(SUM(IF(entity_type = \'bookshelf\', 1, 0)) as UNSIGNED) as shelf_count'), ]) - ->orderBy($sort, $listOptions->getOrder()) ->whereHas('entity'); if ($nameFilter) { @@ -57,7 +75,7 @@ class TagRepo }); } - return $this->permissions->restrictEntityRelationQuery($query, 'tags', 'entity_id', 'entity_type'); + return $query; } /** diff --git a/app/Api/ListingResponseBuilder.php b/app/Api/ListingResponseBuilder.php index 44117bad9..6b9cfdd7d 100644 --- a/app/Api/ListingResponseBuilder.php +++ b/app/Api/ListingResponseBuilder.php @@ -18,6 +18,13 @@ class ListingResponseBuilder */ protected array $fields; + /** + * Which fields are filterable. + * When null, the $fields above are used instead (Allow all fields). + * @var string[]|null + */ + protected array|null $filterableFields = null; + /** * @var array */ @@ -54,7 +61,7 @@ class ListingResponseBuilder { $filteredQuery = $this->filterQuery($this->query); - $total = $filteredQuery->count(); + $total = $filteredQuery->getCountForPagination(); $data = $this->fetchData($filteredQuery)->each(function ($model) { foreach ($this->resultModifiers as $modifier) { $modifier($model); @@ -77,6 +84,14 @@ class ListingResponseBuilder $this->resultModifiers[] = $modifier; } + /** + * Limit filtering to just the given set of fields. + */ + public function setFilterableFields(array $fields): void + { + $this->filterableFields = $fields; + } + /** * Fetch the data to return within the response. */ @@ -94,7 +109,7 @@ class ListingResponseBuilder protected function filterQuery(Builder $query): Builder { $query = clone $query; - $requestFilters = $this->request->get('filter', []); + $requestFilters = $this->request->input('filter', []); if (!is_array($requestFilters)) { return $query; } @@ -114,10 +129,11 @@ class ListingResponseBuilder protected function requestFilterToQueryFilter($fieldKey, $value): ?array { $splitKey = explode(':', $fieldKey); - $field = $splitKey[0]; + $field = strtolower($splitKey[0]); $filterOperator = $splitKey[1] ?? 'eq'; - if (!in_array($field, $this->fields)) { + $filterFields = $this->filterableFields ?? $this->fields; + if (!in_array($field, $filterFields)) { return null; } @@ -140,8 +156,8 @@ class ListingResponseBuilder $defaultSortName = $this->fields[0]; $direction = 'asc'; - $sort = $this->request->get('sort', ''); - if (strpos($sort, '-') === 0) { + $sort = $this->request->input('sort', ''); + if (str_starts_with($sort, '-')) { $direction = 'desc'; } @@ -160,9 +176,9 @@ class ListingResponseBuilder protected function countAndOffsetQuery(Builder $query): Builder { $query = clone $query; - $offset = max(0, $this->request->get('offset', 0)); + $offset = max(0, $this->request->input('offset', 0)); $maxCount = config('api.max_item_count'); - $count = $this->request->get('count', config('api.default_item_count')); + $count = $this->request->input('count', config('api.default_item_count')); $count = max(min($maxCount, $count), 1); return $query->skip($offset)->take($count); diff --git a/app/Http/ApiController.php b/app/Http/ApiController.php index 8c0f206d0..f1b74783f 100644 --- a/app/Http/ApiController.php +++ b/app/Http/ApiController.php @@ -20,10 +20,14 @@ abstract class ApiController extends Controller * Provide a paginated listing JSON response in a standard format * taking into account any pagination parameters passed by the user. */ - protected function apiListingResponse(Builder $query, array $fields, array $modifiers = []): JsonResponse + protected function apiListingResponse(Builder $query, array $fields, array $modifiers = [], array $filterableFields = []): JsonResponse { $listing = new ListingResponseBuilder($query, request(), $fields); + if (count($filterableFields) > 0) { + $listing->setFilterableFields($filterableFields); + } + foreach ($modifiers as $modifier) { $listing->modifyResults($modifier); } diff --git a/routes/api.php b/routes/api.php index 308a95d8c..9f45cefb9 100644 --- a/routes/api.php +++ b/routes/api.php @@ -7,6 +7,7 @@ */ use BookStack\Activity\Controllers as ActivityControllers; +use BookStack\Activity\Controllers\TagApiController; use BookStack\Api\ApiDocsController; use BookStack\App\SystemApiController; use BookStack\Entities\Controllers as EntityControllers; @@ -109,6 +110,9 @@ Route::get('search', [SearchApiController::class, 'all']); Route::get('system', [SystemApiController::class, 'read']); +Route::get('tags/names', [TagApiController::class, 'listNames']); +Route::get('tags/name/{name}/values', [TagApiController::class, 'listValues']); + Route::get('users', [UserApiController::class, 'list']); Route::post('users', [UserApiController::class, 'create']); Route::get('users/{id}', [UserApiController::class, 'read']); From 1c1ad1d1b739d2558d31478610b1ca4af553f3eb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 12 Apr 2026 20:45:18 +0100 Subject: [PATCH 15/74] Tags API: Reviewed docs and added examples --- app/Activity/Controllers/TagApiController.php | 10 ++++-- app/Api/ApiDocsGenerator.php | 7 ++-- dev/api/responses/tags-list-names.json | 32 +++++++++++++++++++ dev/api/responses/tags-list-values.json | 32 +++++++++++++++++++ .../views/api-docs/parts/endpoint.blade.php | 2 +- .../api-docs/parts/getting-started.blade.php | 2 +- 6 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 dev/api/responses/tags-list-names.json create mode 100644 dev/api/responses/tags-list-values.json diff --git a/app/Activity/Controllers/TagApiController.php b/app/Activity/Controllers/TagApiController.php index c1945ba5c..1fdffa007 100644 --- a/app/Activity/Controllers/TagApiController.php +++ b/app/Activity/Controllers/TagApiController.php @@ -8,6 +8,12 @@ use BookStack\Activity\TagRepo; use BookStack\Http\ApiController; use Illuminate\Http\JsonResponse; +/** + * Endpoints to query data about tags in the system. + * You'll only see results based on tags applied to content you have access to. + * There are no general create/update/delete endpoints here since tags do not exist + * by themselves, they are managed via the items they are assigned to. + */ class TagApiController extends ApiController { public function __construct( @@ -17,7 +23,6 @@ class TagApiController extends ApiController /** * Get a list of tag names used in the system. - * You'll only see results based on tags applied to content you have access to. * Only the name field can be used in filters. */ public function listNames(): JsonResponse @@ -33,8 +38,7 @@ class TagApiController extends ApiController } /** - * Get a list of tag values used in the system, which have been used for the given tag name. - * You'll only see results based on tags applied to content you have access to. + * Get a list of tag values, which have been set for the given tag name. * Only the value field can be used in filters. */ public function listValues(string $name): JsonResponse diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php index a59cb8198..53cb2890a 100644 --- a/app/Api/ApiDocsGenerator.php +++ b/app/Api/ApiDocsGenerator.php @@ -195,11 +195,12 @@ class ApiDocsGenerator protected function getFlatApiRoutes(): Collection { return collect(Route::getRoutes()->getRoutes())->filter(function ($route) { - return strpos($route->uri, 'api/') === 0; + return str_starts_with($route->uri, 'api/'); })->map(function ($route) { [$controller, $controllerMethod] = explode('@', $route->action['uses']); $baseModelName = explode('.', explode('/', $route->uri)[1])[0]; - $shortName = $baseModelName . '-' . $controllerMethod; + $controllerMethodKebab = Str::kebab($controllerMethod); + $shortName = $baseModelName . '-' . $controllerMethodKebab; return [ 'name' => $shortName, @@ -207,7 +208,7 @@ class ApiDocsGenerator 'method' => $route->methods[0], 'controller' => $controller, 'controller_method' => $controllerMethod, - 'controller_method_kebab' => Str::kebab($controllerMethod), + 'controller_method_kebab' => $controllerMethodKebab, 'base_model' => $baseModelName, ]; }); diff --git a/dev/api/responses/tags-list-names.json b/dev/api/responses/tags-list-names.json new file mode 100644 index 000000000..c0c8e7b22 --- /dev/null +++ b/dev/api/responses/tags-list-names.json @@ -0,0 +1,32 @@ +{ + "data": [ + { + "name": "Category", + "values": 8, + "usages": 184, + "page_count": 3, + "chapter_count": 8, + "book_count": 171, + "shelf_count": 2 + }, + { + "name": "Review Due", + "values": 2, + "usages": 2, + "page_count": 1, + "chapter_count": 0, + "book_count": 1, + "shelf_count": 0 + }, + { + "name": "Type", + "values": 2, + "usages": 2, + "page_count": 0, + "chapter_count": 1, + "book_count": 1, + "shelf_count": 0 + } + ], + "total": 3 +} \ No newline at end of file diff --git a/dev/api/responses/tags-list-values.json b/dev/api/responses/tags-list-values.json new file mode 100644 index 000000000..37926b846 --- /dev/null +++ b/dev/api/responses/tags-list-values.json @@ -0,0 +1,32 @@ +{ + "data": [ + { + "name": "Category", + "value": "Cool Stuff", + "usages": 3, + "page_count": 1, + "chapter_count": 0, + "book_count": 2, + "shelf_count": 0 + }, + { + "name": "Category", + "value": "Top Content", + "usages": 168, + "page_count": 0, + "chapter_count": 3, + "book_count": 165, + "shelf_count": 0 + }, + { + "name": "Category", + "value": "Learning", + "usages": 2, + "page_count": 0, + "chapter_count": 0, + "book_count": 0, + "shelf_count": 2 + } + ], + "total": 3 +} \ No newline at end of file diff --git a/resources/views/api-docs/parts/endpoint.blade.php b/resources/views/api-docs/parts/endpoint.blade.php index 024a5ecdf..543ef092e 100644 --- a/resources/views/api-docs/parts/endpoint.blade.php +++ b/resources/views/api-docs/parts/endpoint.blade.php @@ -1,7 +1,7 @@
    {{ $endpoint['method'] }}
    - @if($endpoint['controller_method_kebab'] === 'list') + @if(str_starts_with($endpoint['controller_method_kebab'], 'list') && !str_contains($endpoint['uri'], '{')) {{ url($endpoint['uri']) }} @else {{ url($endpoint['uri']) }} diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php index 663389047..ebe3838ef 100644 --- a/resources/views/api-docs/parts/getting-started.blade.php +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -2,7 +2,7 @@

    This documentation covers use of the REST API.
    - Examples of API usage, in a variety of programming languages, can be found in the BookStack api-scripts repo on GitHub. + Examples of API usage, in a variety of programming languages, can be found in the BookStack api-scripts repo on Codeberg.

    Some alternative options for extension and customization can be found below: From 346dc27979a05a7b82e7fef5f32c00be2578f77f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Apr 2026 11:31:34 +0100 Subject: [PATCH 16/74] API: Added testing to cover tags API endpoints --- tests/Api/TagsApiTest.php | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/Api/TagsApiTest.php diff --git a/tests/Api/TagsApiTest.php b/tests/Api/TagsApiTest.php new file mode 100644 index 000000000..baf7b0859 --- /dev/null +++ b/tests/Api/TagsApiTest.php @@ -0,0 +1,109 @@ + 'MyGreatApiTag', 'value' => 'cat']; + $pagesToTag = Page::query()->take(10)->get(); + $booksToTag = Book::query()->take(3)->get(); + $chaptersToTag = Chapter::query()->take(5)->get(); + $pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag($tagInfo))); + $booksToTag->each(fn (Book $book) => $book->tags()->save(new Tag($tagInfo))); + $chaptersToTag->each(fn (Chapter $chapter) => $chapter->tags()->save(new Tag($tagInfo))); + + $resp = $this->actingAsApiEditor()->getJson('api/tags/names?filter[name]=MyGreatApiTag'); + $resp->assertStatus(200); + $resp->assertJson([ + 'data' => [ + [ + 'name' => 'MyGreatApiTag', + 'values' => 1, + 'usages' => 18, + 'page_count' => 10, + 'book_count' => 3, + 'chapter_count' => 5, + 'shelf_count' => 0, + ] + ], + 'total' => 1, + ]); + } + + public function test_list_names_is_limited_by_permission_visibility(): void + { + $pagesToTag = Page::query()->take(10)->get(); + $pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyGreatApiTag', 'value' => 'cat' . $page->id]))); + + $this->permissions->disableEntityInheritedPermissions($pagesToTag[3]); + $this->permissions->disableEntityInheritedPermissions($pagesToTag[6]); + + $resp = $this->actingAsApiEditor()->getJson('api/tags/names?filter[name]=MyGreatApiTag'); + $resp->assertStatus(200); + $resp->assertJson([ + 'data' => [ + [ + 'name' => 'MyGreatApiTag', + 'values' => 8, + 'usages' => 8, + 'page_count' => 8, + 'book_count' => 0, + 'chapter_count' => 0, + 'shelf_count' => 0, + ] + ], + 'total' => 1, + ]); + } + + public function test_list_values_returns_values_for_set_tag() + { + $pagesToTag = Page::query()->take(10)->get(); + $booksToTag = Book::query()->take(3)->get(); + $chaptersToTag = Chapter::query()->take(5)->get(); + $pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-page' . $page->id]))); + $booksToTag->each(fn (Book $book) => $book->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-book' . $book->id]))); + $chaptersToTag->each(fn (Chapter $chapter) => $chapter->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-chapter' . $chapter->id]))); + + $resp = $this->actingAsApiEditor()->getJson('api/tags/name/MyValueApiTag/values'); + + $resp->assertStatus(200); + $resp->assertJson(['total' => 18]); + $resp->assertJsonFragment([ + [ + 'name' => 'MyValueApiTag', + 'value' => 'tag-page' . $pagesToTag[0]->id, + 'usages' => 1, + 'page_count' => 1, + 'book_count' => 0, + 'chapter_count' => 0, + 'shelf_count' => 0, + ] + ]); + } + + public function test_list_values_is_limited_by_permission_visibility(): void + { + $pagesToTag = Page::query()->take(10)->get(); + $pagesToTag->each(fn (Page $page) => $page->tags()->save(new Tag(['name' => 'MyGreatApiTag', 'value' => 'cat' . $page->id]))); + + $this->permissions->disableEntityInheritedPermissions($pagesToTag[3]); + $this->permissions->disableEntityInheritedPermissions($pagesToTag[6]); + + $resp = $this->actingAsApiEditor()->getJson('api/tags/name/MyGreatApiTag/values'); + $resp->assertStatus(200); + $resp->assertJson(['total' => 8]); + $resp->assertJsonMissing(['value' => 'cat' . $pagesToTag[3]->id]); + } +} From 208629ee1fce280c31f9baf30a6ed79c3cd17df0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 14 Apr 2026 12:03:29 +0100 Subject: [PATCH 17/74] API: Some changes to tag API endpoints - Updated tag values endpoint to use query param instead of path argument, so a better range of values can be provided (including those with slashes). - Updated image gallery example request to align with docs use changes. --- app/Activity/Controllers/TagApiController.php | 23 +++++++++++++++---- ...p => image-gallery-read-data-for-url.http} | 0 dev/api/requests/tags-list-values.http | 1 + routes/api.php | 2 +- tests/Api/TagsApiTest.php | 4 ++-- 5 files changed, 22 insertions(+), 8 deletions(-) rename dev/api/requests/{image-gallery-readDataForUrl.http => image-gallery-read-data-for-url.http} (100%) create mode 100644 dev/api/requests/tags-list-values.http diff --git a/app/Activity/Controllers/TagApiController.php b/app/Activity/Controllers/TagApiController.php index 1fdffa007..f5c5e95d4 100644 --- a/app/Activity/Controllers/TagApiController.php +++ b/app/Activity/Controllers/TagApiController.php @@ -7,6 +7,7 @@ namespace BookStack\Activity\Controllers; use BookStack\Activity\TagRepo; use BookStack\Http\ApiController; use Illuminate\Http\JsonResponse; +use Illuminate\Http\Request; /** * Endpoints to query data about tags in the system. @@ -21,6 +22,15 @@ class TagApiController extends ApiController ) { } + protected function rules(): array + { + return [ + 'listValues' => [ + 'name' => ['required', 'string'], + ], + ]; + } + /** * Get a list of tag names used in the system. * Only the name field can be used in filters. @@ -38,18 +48,21 @@ class TagApiController extends ApiController } /** - * Get a list of tag values, which have been set for the given tag name. + * Get a list of tag values, which have been set for the given tag name, + * which must be provided as a query parameter on the request. * Only the value field can be used in filters. */ - public function listValues(string $name): JsonResponse + public function listValues(Request $request): JsonResponse { - $tagQuery = $this->tagRepo - ->queryWithTotalsForApi($name); + $data = $this->validate($request, $this->rules()['listValues']); + $name = $data['name']; + + $tagQuery = $this->tagRepo->queryWithTotalsForApi($name); return $this->apiListingResponse($tagQuery, [ 'name', 'value', 'usages', 'page_count', 'chapter_count', 'book_count', 'shelf_count', ], [], [ - 'name', 'value', + 'value', ]); } } diff --git a/dev/api/requests/image-gallery-readDataForUrl.http b/dev/api/requests/image-gallery-read-data-for-url.http similarity index 100% rename from dev/api/requests/image-gallery-readDataForUrl.http rename to dev/api/requests/image-gallery-read-data-for-url.http diff --git a/dev/api/requests/tags-list-values.http b/dev/api/requests/tags-list-values.http new file mode 100644 index 000000000..6dd3f49fc --- /dev/null +++ b/dev/api/requests/tags-list-values.http @@ -0,0 +1 @@ +GET /api/tags/values-for-name?name=Category diff --git a/routes/api.php b/routes/api.php index 9f45cefb9..5a9df3cc4 100644 --- a/routes/api.php +++ b/routes/api.php @@ -111,7 +111,7 @@ Route::get('search', [SearchApiController::class, 'all']); Route::get('system', [SystemApiController::class, 'read']); Route::get('tags/names', [TagApiController::class, 'listNames']); -Route::get('tags/name/{name}/values', [TagApiController::class, 'listValues']); +Route::get('tags/values-for-name', [TagApiController::class, 'listValues']); Route::get('users', [UserApiController::class, 'list']); Route::post('users', [UserApiController::class, 'create']); diff --git a/tests/Api/TagsApiTest.php b/tests/Api/TagsApiTest.php index baf7b0859..a079fa639 100644 --- a/tests/Api/TagsApiTest.php +++ b/tests/Api/TagsApiTest.php @@ -76,7 +76,7 @@ class TagsApiTest extends TestCase $booksToTag->each(fn (Book $book) => $book->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-book' . $book->id]))); $chaptersToTag->each(fn (Chapter $chapter) => $chapter->tags()->save(new Tag(['name' => 'MyValueApiTag', 'value' => 'tag-chapter' . $chapter->id]))); - $resp = $this->actingAsApiEditor()->getJson('api/tags/name/MyValueApiTag/values'); + $resp = $this->actingAsApiEditor()->getJson('api/tags/values-for-name?name=MyValueApiTag'); $resp->assertStatus(200); $resp->assertJson(['total' => 18]); @@ -101,7 +101,7 @@ class TagsApiTest extends TestCase $this->permissions->disableEntityInheritedPermissions($pagesToTag[3]); $this->permissions->disableEntityInheritedPermissions($pagesToTag[6]); - $resp = $this->actingAsApiEditor()->getJson('api/tags/name/MyGreatApiTag/values'); + $resp = $this->actingAsApiEditor()->getJson('api/tags/values-for-name?name=MyGreatApiTag'); $resp->assertStatus(200); $resp->assertJson(['total' => 8]); $resp->assertJsonMissing(['value' => 'cat' . $pagesToTag[3]->id]); From 18364d1e6e235ab129a03e77279201436bcd954a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 16 Apr 2026 11:11:06 +0100 Subject: [PATCH 18/74] WYSIWYG: Added inline code support to minimal editor Used for comments and descriptions. Also updated shortcut handling that we're not registering shortcuts for edits which can't use the related formatting types. For #6003 --- app/Util/HtmlDescriptionFilter.php | 1 + resources/js/wysiwyg/index.ts | 6 +- resources/js/wysiwyg/services/shortcuts.ts | 59 +++++++++++--------- resources/js/wysiwyg/ui/defaults/toolbars.ts | 1 + tests/Entity/BookTest.php | 4 +- 5 files changed, 40 insertions(+), 31 deletions(-) diff --git a/app/Util/HtmlDescriptionFilter.php b/app/Util/HtmlDescriptionFilter.php index 1baa11ffc..ba1454603 100644 --- a/app/Util/HtmlDescriptionFilter.php +++ b/app/Util/HtmlDescriptionFilter.php @@ -27,6 +27,7 @@ class HtmlDescriptionFilter 'span' => [], 'em' => [], 'br' => [], + 'code' => [], ]; public static function filterFromString(string $html): string diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index 01964b066..dc0ea211f 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -59,7 +59,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st mergeRegister( registerRichText(editor), registerHistory(editor, createEmptyHistoryState(), 300), - registerShortcuts(context), + registerShortcuts(context, true), registerKeyboardHandling(context), registerMouseHandling(context), registerSelectionHandling(context), @@ -123,7 +123,7 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s const editorTeardown = mergeRegister( registerRichText(editor), registerHistory(editor, createEmptyHistoryState(), 300), - registerShortcuts(context), + registerShortcuts(context, false), registerAutoLinks(editor), ); @@ -157,7 +157,7 @@ export function createCommentEditorInstance(container: HTMLElement, htmlContent: const editorTeardown = mergeRegister( registerRichText(editor), registerHistory(editor, createEmptyHistoryState(), 300), - registerShortcuts(context), + registerShortcuts(context, false), registerAutoLinks(editor), registerMentions(context), ); diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts index c4be0f3cf..00abe0c6d 100644 --- a/resources/js/wysiwyg/services/shortcuts.ts +++ b/resources/js/wysiwyg/services/shortcuts.ts @@ -38,29 +38,9 @@ type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boole * List of action functions by their shortcut combo. * We use "meta" as an abstraction for ctrl/cmd depending on platform. */ -const actionsByKeys: Record = { - 'meta+s': () => { - window.$events.emit('editor-save-draft'); - return true; - }, - 'meta+enter': () => { - window.$events.emit('editor-save-page'); - return true; - }, - 'meta+1': (editor, context) => headerHandler(context, 'h2'), - 'meta+2': (editor, context) => headerHandler(context, 'h3'), - 'meta+3': (editor, context) => headerHandler(context, 'h4'), - 'meta+4': (editor, context) => headerHandler(context, 'h5'), - 'meta+5': wrapFormatAction(toggleSelectionAsParagraph), - 'meta+d': wrapFormatAction(toggleSelectionAsParagraph), - 'meta+6': wrapFormatAction(toggleSelectionAsBlockquote), - 'meta+q': wrapFormatAction(toggleSelectionAsBlockquote), - 'meta+7': wrapFormatAction(formatCodeBlock), - 'meta+e': wrapFormatAction(formatCodeBlock), +const baseActionsByKeys: Record = { 'meta+8': toggleInlineCode, 'meta+shift+e': toggleInlineCode, - 'meta+9': wrapFormatAction(cycleSelectionCalloutFormats), - 'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')), 'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')), 'meta+k': (editor, context) => { @@ -87,12 +67,39 @@ const actionsByKeys: Record = { }, }; -function createKeyDownListener(context: EditorUiContext): (e: KeyboardEvent) => void { +/** + * An extended set of the above, used for fuller-featured editors with heavier block-level formatting. + */ +const extendedActionsByKeys: Record = { + ...baseActionsByKeys, + 'meta+s': () => { + window.$events.emit('editor-save-draft'); + return true; + }, + 'meta+enter': () => { + window.$events.emit('editor-save-page'); + return true; + }, + 'meta+1': (editor, context) => headerHandler(context, 'h2'), + 'meta+2': (editor, context) => headerHandler(context, 'h3'), + 'meta+3': (editor, context) => headerHandler(context, 'h4'), + 'meta+4': (editor, context) => headerHandler(context, 'h5'), + 'meta+5': wrapFormatAction(toggleSelectionAsParagraph), + 'meta+d': wrapFormatAction(toggleSelectionAsParagraph), + 'meta+6': wrapFormatAction(toggleSelectionAsBlockquote), + 'meta+7': wrapFormatAction(formatCodeBlock), + 'meta+e': wrapFormatAction(formatCodeBlock), + 'meta+q': wrapFormatAction(toggleSelectionAsBlockquote), + 'meta+9': wrapFormatAction(cycleSelectionCalloutFormats), +}; + +function createKeyDownListener(context: EditorUiContext, useExtended: boolean): (e: KeyboardEvent) => void { + const keySetToUse = useExtended ? extendedActionsByKeys : baseActionsByKeys; return (event: KeyboardEvent) => { const combo = keyboardEventToKeyComboString(event); // console.log(`pressed: ${combo}`); - if (actionsByKeys[combo]) { - const handled = actionsByKeys[combo](context.editor, context); + if (keySetToUse[combo]) { + const handled = keySetToUse[combo](context.editor, context); if (handled) { event.stopPropagation(); event.preventDefault(); @@ -127,8 +134,8 @@ function overrideDefaultCommands(editor: LexicalEditor) { }, COMMAND_PRIORITY_HIGH); } -export function registerShortcuts(context: EditorUiContext) { - const listener = createKeyDownListener(context); +export function registerShortcuts(context: EditorUiContext, useExtended: boolean) { + const listener = createKeyDownListener(context, useExtended); overrideDefaultCommands(context.editor); return context.editor.registerRootListener((rootElement: null | HTMLElement, prevRootElement: null | HTMLElement) => { diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index d6af99638..a3ada5c89 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -227,6 +227,7 @@ export function getBasicEditorToolbar(context: EditorUiContext): EditorContainer new EditorButton(bold), new EditorButton(italic), new EditorButton(link), + new EditorButton(code), new EditorButton(bulletList), new EditorButton(numberList), ]) diff --git a/tests/Entity/BookTest.php b/tests/Entity/BookTest.php index 6082c59de..c0d4fbc63 100644 --- a/tests/Entity/BookTest.php +++ b/tests/Entity/BookTest.php @@ -256,8 +256,8 @@ class BookTest extends TestCase { $book = $this->entities->book(); - $input = '

    Test

    Contenta

    Hello

    '; - $expected = '

    Contenta

    '; + $input = '

    Test

    Contenta

    Hello
    code

    '; + $expected = '

    Contentacode

    '; $this->asEditor()->put($book->getUrl(), [ 'name' => $book->name, From a2bb5bdf10bbee40a6eb12e40101ea61f0a92021 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 17 Apr 2026 21:22:04 +0100 Subject: [PATCH 19/74] Meta: Updated COC, templates, PR template for community rules Added reference to new community rules page where sensible. --- .github/CODE_OF_CONDUCT.md | 86 +--------------------- .github/ISSUE_TEMPLATE/feature_request.yml | 10 +++ .github/pull_request_template.md | 11 +++ readme.md | 9 ++- 4 files changed, 28 insertions(+), 88 deletions(-) create mode 100644 .github/pull_request_template.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 2c6317af6..7a0265672 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -1,84 +1,2 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, race, -religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -### Project Maintainer Standards - -Project maintainers should generally follow these additional standards: - -* Avoid using a negative or harsh tone in communication, Even if the other party -is being negative themselves. -* When providing criticism, try to make it constructive to lead the other person -down the correct path. -* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition) -in mind when deciding what's in scope of the Project. - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. In addition, Project -maintainers are responsible for following the standards themselves. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org +Please find our community rules on our website here: +https://www.bookstackapp.com/about/community-rules/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 0ebb8e72f..ca1f2b830 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -56,3 +56,13 @@ body: description: Add any other context or screenshots about the feature request here. validations: required: false + - type: checkboxes + id: ai-thoughts + attributes: + label: Have you used generative AI/LLMs to create any thoughts in this request? + description: | + We ask that no machine generated thoughts or ideas are provided, to avoid us spending time considering the ideas + of a machine instead of a human. Further guidance on this can be found [in the BookStack community rules](https://www.bookstackapp.com/about/community-rules/#use-of-llmsai). + options: + - label: This request only contains the thoughts & ideas of a human + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..70f105874 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,11 @@ +## Details + + + + +## Checklist + + + +- [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/). +- [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above. diff --git a/readme.md b/readme.md index 00eb13504..d3a408ad0 100644 --- a/readme.md +++ b/readme.md @@ -9,8 +9,9 @@
    [![Alternate Source](https://img.shields.io/static/v1?label=Alt+Source&message=Git&color=ef391a&logo=git)](https://source.bookstackapp.com/) [![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) -[![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://www.bookstackapp.com/links/discord) +[![Community Discussions](https://img.shields.io/static/v1?label=Community&message=Discussions&color=4d36c4&logo=zulip)](https://community.bookstackapp.com/) [![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://www.bookstackapp.com/links/mastodon) +[![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://www.bookstackapp.com/links/discord)
    [![PeerTube](https://img.shields.io/static/v1?label=PeerTube&message=bookstack@foss.video&color=f2690d&logo=peertube)](https://foss.video/c/bookstack) [![YouTube](https://img.shields.io/static/v1?label=YouTube&message=bookstackapp&color=ff0000&logo=youtube)](https://www.youtube.com/bookstackapp) @@ -20,11 +21,10 @@ A platform for storing and organising information and documentation. Details for * [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation) * [Documentation](https://www.bookstackapp.com/docs) * [Demo Instance](https://demo.bookstackapp.com) - * [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password) * [Screenshots](https://www.bookstackapp.com/#screenshots) * [BookStack Blog](https://www.bookstackapp.com/blog) * [Issue List](https://github.com/BookStackApp/BookStack/issues) -* [Discord Chat](https://www.bookstackapp.com/links/discord) +* [Community Discussions](https://community.bookstackapp.com/) * [Support Options](https://www.bookstackapp.com/support/) ## 📚 Project Definition @@ -124,8 +124,9 @@ Feel free to [create issues](https://github.com/BookStackApp/BookStack/issues/ne Pull requests are welcome but, unless it's a small tweak, it may be best to open the pull request early or create an issue for your intended change to discuss how it will fit into the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project. Pull requests should be created from the `development` branch since they will be merged back into `development` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases. If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly. +See the [Development & Testing](#-development--testing) section above for further development guidance. -The project's code of conduct [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/CODE_OF_CONDUCT.md). +The project's community rules, including those for raising issues and making code contributions, [can be found here](https://www.bookstackapp.com/about/community-rules/). ## 🔒 Security From 083fb1a600f1eeb8acb719675bdeb089c86ceee1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 18 Apr 2026 20:43:27 +0100 Subject: [PATCH 20/74] Maintenance: Updated $request->get instance to use input --- .../Controllers/ForgotPasswordController.php | 4 ++-- app/Access/Controllers/LoginController.php | 8 ++++---- .../Controllers/MfaBackupCodesController.php | 2 +- app/Access/Controllers/MfaController.php | 4 ++-- .../Controllers/ResetPasswordController.php | 4 ++-- app/Access/Controllers/Saml2Controller.php | 4 ++-- app/Access/Controllers/SocialController.php | 2 +- .../Controllers/UserInviteController.php | 2 +- .../Controllers/AuditLogController.php | 14 ++++++------- .../Controllers/FavouriteController.php | 2 +- app/Activity/Controllers/TagController.php | 6 +++--- app/Api/UserApiTokenController.php | 8 ++++---- app/Entities/Controllers/BookController.php | 4 ++-- .../Controllers/BookshelfApiController.php | 4 ++-- .../Controllers/BookshelfController.php | 4 ++-- .../Controllers/ChapterApiController.php | 2 +- .../Controllers/ChapterController.php | 6 +++--- .../Controllers/PageApiController.php | 8 ++++---- app/Entities/Controllers/PageController.php | 8 ++++---- .../Controllers/PageTemplateController.php | 4 ++-- app/Entities/Tools/PermissionsUpdater.php | 4 ++-- app/Search/SearchApiController.php | 6 +++--- app/Search/SearchController.php | 20 +++++++++---------- app/Search/SearchOptions.php | 2 +- app/Settings/AppSettingsStore.php | 4 ++-- app/Settings/MaintenanceController.php | 2 +- app/Sorting/BookSortController.php | 4 ++-- .../Controllers/AttachmentApiController.php | 4 ++-- .../Controllers/AttachmentController.php | 16 +++++++-------- .../Controllers/DrawioImageController.php | 12 +++++------ .../Controllers/GalleryImageController.php | 10 +++++----- app/Users/Controllers/RoleController.php | 4 ++-- .../Controllers/UserAccountController.php | 6 +++--- app/Users/Controllers/UserApiController.php | 2 +- app/Users/Controllers/UserController.php | 4 ++-- .../Controllers/UserPreferencesController.php | 8 ++++---- .../Controllers/UserSearchController.php | 4 ++-- app/Util/SimpleListOptions.php | 2 +- 38 files changed, 107 insertions(+), 107 deletions(-) diff --git a/app/Access/Controllers/ForgotPasswordController.php b/app/Access/Controllers/ForgotPasswordController.php index 36dd97755..e8127e617 100644 --- a/app/Access/Controllers/ForgotPasswordController.php +++ b/app/Access/Controllers/ForgotPasswordController.php @@ -45,11 +45,11 @@ class ForgotPasswordController extends Controller ); if ($response === Password::RESET_LINK_SENT) { - $this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email')); + $this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->input('email')); } if (in_array($response, [Password::RESET_LINK_SENT, Password::INVALID_USER, Password::RESET_THROTTLED])) { - $message = trans('auth.reset_password_sent', ['email' => $request->get('email')]); + $message = trans('auth.reset_password_sent', ['email' => $request->input('email')]); $this->showSuccessNotification($message); return redirect('/password/email')->with('status', trans($response)); diff --git a/app/Access/Controllers/LoginController.php b/app/Access/Controllers/LoginController.php index ce872ba88..4694f22e4 100644 --- a/app/Access/Controllers/LoginController.php +++ b/app/Access/Controllers/LoginController.php @@ -32,12 +32,12 @@ class LoginController extends Controller { $socialDrivers = $this->socialDriverManager->getActive(); $authMethod = config('auth.method'); - $preventInitiation = $request->get('prevent_auto_init') === 'true'; + $preventInitiation = $request->input('prevent_auto_init') === 'true'; if ($request->has('email')) { session()->flashInput([ - 'email' => $request->get('email'), - 'password' => (config('app.env') === 'demo') ? $request->get('password', '') : '', + 'email' => $request->input('email'), + 'password' => (config('app.env') === 'demo') ? $request->input('password', '') : '', ]); } @@ -62,7 +62,7 @@ class LoginController extends Controller public function login(Request $request) { $this->validateLogin($request); - $username = $request->get($this->username()); + $username = $request->input($this->username()); // Check login throttling attempts to see if they've gone over the limit if ($this->hasTooManyLoginAttempts($request)) { diff --git a/app/Access/Controllers/MfaBackupCodesController.php b/app/Access/Controllers/MfaBackupCodesController.php index 5c334674e..0a6416a17 100644 --- a/app/Access/Controllers/MfaBackupCodesController.php +++ b/app/Access/Controllers/MfaBackupCodesController.php @@ -84,7 +84,7 @@ class MfaBackupCodesController extends Controller ], ]); - $updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes); + $updatedCodes = $codeService->removeInputCodeFromSet($request->input('code'), $codes); MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes); $mfaSession->markVerifiedForUser($user); diff --git a/app/Access/Controllers/MfaController.php b/app/Access/Controllers/MfaController.php index c9100ef91..181cfc0b8 100644 --- a/app/Access/Controllers/MfaController.php +++ b/app/Access/Controllers/MfaController.php @@ -51,14 +51,14 @@ class MfaController extends Controller */ public function verify(Request $request) { - $desiredMethod = $request->get('method'); + $desiredMethod = $request->input('method'); $userMethods = $this->currentOrLastAttemptedUser() ->mfaValues() ->get(['id', 'method']) ->groupBy('method'); // Basic search for the default option for a user. - // (Prioritises totp over backup codes) + // (Prioritises TOTP over backup codes) $method = $userMethods->has($desiredMethod) ? $desiredMethod : $userMethods->keys()->sort()->reverse()->first(); $otherMethods = $userMethods->keys()->filter(function ($userMethod) use ($method) { return $method !== $userMethod; diff --git a/app/Access/Controllers/ResetPasswordController.php b/app/Access/Controllers/ResetPasswordController.php index 3af65d17f..e81c98b28 100644 --- a/app/Access/Controllers/ResetPasswordController.php +++ b/app/Access/Controllers/ResetPasswordController.php @@ -48,7 +48,7 @@ class ResetPasswordController extends Controller // Here we will attempt to reset the user's password. If it is successful we // will update the password on an actual user model and persist it to the - // database. Otherwise we will parse the error and return the response. + // database. Otherwise, we will parse the error and return the response. $credentials = $request->only('email', 'password', 'password_confirmation', 'token'); $response = Password::broker()->reset($credentials, function (User $user, string $password) { $user->password = Hash::make($password); @@ -63,7 +63,7 @@ class ResetPasswordController extends Controller // redirect them back to where they came from with their error message. return $response === Password::PASSWORD_RESET ? $this->sendResetResponse() - : $this->sendResetFailedResponse($request, $response, $request->get('token')); + : $this->sendResetFailedResponse($request, $response, $request->input('token')); } /** diff --git a/app/Access/Controllers/Saml2Controller.php b/app/Access/Controllers/Saml2Controller.php index 6f802370e..39598b143 100644 --- a/app/Access/Controllers/Saml2Controller.php +++ b/app/Access/Controllers/Saml2Controller.php @@ -78,7 +78,7 @@ class Saml2Controller extends Controller */ public function startAcs(Request $request) { - $samlResponse = $request->get('SAMLResponse', null); + $samlResponse = $request->input('SAMLResponse', null); if (empty($samlResponse)) { $this->showErrorNotification(trans('errors.saml_fail_authed', ['system' => config('saml2.name')])); @@ -100,7 +100,7 @@ class Saml2Controller extends Controller */ public function processAcs(Request $request) { - $acsId = $request->get('id', null); + $acsId = $request->input('id', null); $cacheKey = 'saml2_acs:' . $acsId; $samlResponse = null; diff --git a/app/Access/Controllers/SocialController.php b/app/Access/Controllers/SocialController.php index 07f57062d..5a090c7ca 100644 --- a/app/Access/Controllers/SocialController.php +++ b/app/Access/Controllers/SocialController.php @@ -67,7 +67,7 @@ class SocialController extends Controller if ($request->has('error') && $request->has('error_description')) { throw new SocialSignInException(trans('errors.social_login_bad_response', [ 'socialAccount' => $socialDriver, - 'error' => $request->get('error_description'), + 'error' => $request->input('error_description'), ]), '/login'); } diff --git a/app/Access/Controllers/UserInviteController.php b/app/Access/Controllers/UserInviteController.php index 9ee05b84f..091b68e55 100644 --- a/app/Access/Controllers/UserInviteController.php +++ b/app/Access/Controllers/UserInviteController.php @@ -67,7 +67,7 @@ class UserInviteController extends Controller } $user = $this->userRepo->getById($userId); - $user->password = Hash::make($request->get('password')); + $user->password = Hash::make($request->input('password')); $user->email_confirmed = true; $user->save(); diff --git a/app/Activity/Controllers/AuditLogController.php b/app/Activity/Controllers/AuditLogController.php index c4f9b91ed..ed1421c0d 100644 --- a/app/Activity/Controllers/AuditLogController.php +++ b/app/Activity/Controllers/AuditLogController.php @@ -17,19 +17,19 @@ class AuditLogController extends Controller $this->checkPermission(Permission::SettingsManage); $this->checkPermission(Permission::UsersManage); - $sort = $request->get('sort', 'activity_date'); - $order = $request->get('order', 'desc'); + $sort = $request->input('sort', 'activity_date'); + $order = $request->input('order', 'desc'); $listOptions = (new SimpleListOptions('', $sort, $order))->withSortOptions([ 'created_at' => trans('settings.audit_table_date'), 'type' => trans('settings.audit_table_event'), ]); $filters = [ - 'event' => $request->get('event', ''), - 'date_from' => $request->get('date_from', ''), - 'date_to' => $request->get('date_to', ''), - 'user' => $request->get('user', ''), - 'ip' => $request->get('ip', ''), + 'event' => $request->input('event', ''), + 'date_from' => $request->input('date_from', ''), + 'date_to' => $request->input('date_to', ''), + 'user' => $request->input('user', ''), + 'ip' => $request->input('ip', ''), ]; $query = Activity::query() diff --git a/app/Activity/Controllers/FavouriteController.php b/app/Activity/Controllers/FavouriteController.php index deeb4b0af..65bae276d 100644 --- a/app/Activity/Controllers/FavouriteController.php +++ b/app/Activity/Controllers/FavouriteController.php @@ -20,7 +20,7 @@ class FavouriteController extends Controller public function index(Request $request, QueryTopFavourites $topFavourites) { $viewCount = 20; - $page = intval($request->get('page', 1)); + $page = intval($request->input('page', 1)); $favourites = $topFavourites->run($viewCount + 1, (($page - 1) * $viewCount)); $hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null; diff --git a/app/Activity/Controllers/TagController.php b/app/Activity/Controllers/TagController.php index 723dc4ab4..b57c79825 100644 --- a/app/Activity/Controllers/TagController.php +++ b/app/Activity/Controllers/TagController.php @@ -46,7 +46,7 @@ class TagController extends Controller */ public function getNameSuggestions(Request $request) { - $searchTerm = $request->get('search', ''); + $searchTerm = $request->input('search', ''); $suggestions = $this->tagRepo->getNameSuggestions($searchTerm); return response()->json($suggestions); @@ -57,8 +57,8 @@ class TagController extends Controller */ public function getValueSuggestions(Request $request) { - $searchTerm = $request->get('search', ''); - $tagName = $request->get('name', ''); + $searchTerm = $request->input('search', ''); + $tagName = $request->input('name', ''); $suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName); return response()->json($suggestions); diff --git a/app/Api/UserApiTokenController.php b/app/Api/UserApiTokenController.php index 2ca9e2235..2894ede3a 100644 --- a/app/Api/UserApiTokenController.php +++ b/app/Api/UserApiTokenController.php @@ -48,11 +48,11 @@ class UserApiTokenController extends Controller $secret = Str::random(32); $token = (new ApiToken())->forceFill([ - 'name' => $request->get('name'), + 'name' => $request->input('name'), 'token_id' => Str::random(32), 'secret' => Hash::make($secret), 'user_id' => $user->id, - 'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(), + 'expires_at' => $request->input('expires_at') ?: ApiToken::defaultExpiry(), ]); while (ApiToken::query()->where('token_id', '=', $token->token_id)->exists()) { @@ -100,8 +100,8 @@ class UserApiTokenController extends Controller [$user, $token] = $this->checkPermissionAndFetchUserToken($userId, $tokenId); $token->fill([ - 'name' => $request->get('name'), - 'expires_at' => $request->get('expires_at') ?: ApiToken::defaultExpiry(), + 'name' => $request->input('name'), + 'expires_at' => $request->input('expires_at') ?: ApiToken::defaultExpiry(), ])->save(); $this->logActivity(ActivityType::API_TOKEN_UPDATE, $token); diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index fca530f8a..98470d91c 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -144,7 +144,7 @@ class BookController extends Controller View::incrementFor($book); if ($request->has('shelf')) { - $this->shelfContext->setShelfContext(intval($request->get('shelf'))); + $this->shelfContext->setShelfContext(intval($request->input('shelf'))); } $this->setPageTitle($book->getShortName()); @@ -263,7 +263,7 @@ class BookController extends Controller $this->checkOwnablePermission(Permission::BookView, $book); $this->checkPermission(Permission::BookCreateAll); - $newName = $request->get('name') ?: $book->name; + $newName = $request->input('name') ?: $book->name; $bookCopy = $cloner->cloneBook($book, $newName); $this->showSuccessNotification(trans('entities.books_copy_success')); diff --git a/app/Entities/Controllers/BookshelfApiController.php b/app/Entities/Controllers/BookshelfApiController.php index 735742060..e620eb59c 100644 --- a/app/Entities/Controllers/BookshelfApiController.php +++ b/app/Entities/Controllers/BookshelfApiController.php @@ -49,7 +49,7 @@ class BookshelfApiController extends ApiController $this->checkPermission(Permission::BookshelfCreateAll); $requestData = $this->validate($request, $this->rules()['create']); - $bookIds = $request->get('books', []); + $bookIds = $request->input('books', []); $shelf = $this->bookshelfRepo->create($requestData, $bookIds); return response()->json($this->forJsonDisplay($shelf)); @@ -88,7 +88,7 @@ class BookshelfApiController extends ApiController $this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf); $requestData = $this->validate($request, $this->rules()['update']); - $bookIds = $request->get('books', null); + $bookIds = $request->input('books', null); $shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds); diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index f5f4a90bf..1e8b26b51 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -94,7 +94,7 @@ class BookshelfController extends Controller 'tags' => ['array'], ]); - $bookIds = explode(',', $request->get('books', '')); + $bookIds = explode(',', $request->input('books', '')); $shelf = $this->shelfRepo->create($validated, $bookIds); return redirect($shelf->getUrl()); @@ -196,7 +196,7 @@ class BookshelfController extends Controller unset($validated['image']); } - $bookIds = explode(',', $request->get('books', '')); + $bookIds = explode(',', $request->input('books', '')); $shelf = $this->shelfRepo->update($shelf, $validated, $bookIds); return redirect($shelf->getUrl()); diff --git a/app/Entities/Controllers/ChapterApiController.php b/app/Entities/Controllers/ChapterApiController.php index 6aa62f887..9e0c69b17 100644 --- a/app/Entities/Controllers/ChapterApiController.php +++ b/app/Entities/Controllers/ChapterApiController.php @@ -64,7 +64,7 @@ class ChapterApiController extends ApiController { $requestData = $this->validate($request, $this->rules['create']); - $bookId = $request->get('book_id'); + $bookId = $request->input('book_id'); $book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId)); $this->checkOwnablePermission(Permission::ChapterCreate, $book); diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index 878ee42b5..db2391599 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -203,7 +203,7 @@ class ChapterController extends Controller $this->checkOwnablePermission(Permission::ChapterUpdate, $chapter); $this->checkOwnablePermission(Permission::ChapterDelete, $chapter); - $entitySelection = $request->get('entity_selection', null); + $entitySelection = $request->input('entity_selection', null); if ($entitySelection === null || $entitySelection === '') { return redirect($chapter->getUrl()); } @@ -248,7 +248,7 @@ class ChapterController extends Controller { $chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug); - $entitySelection = $request->get('entity_selection') ?: null; + $entitySelection = $request->input('entity_selection') ?: null; $newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent(); if (!$newParentBook instanceof Book) { @@ -259,7 +259,7 @@ class ChapterController extends Controller $this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook); - $newName = $request->get('name') ?: $chapter->name; + $newName = $request->input('name') ?: $chapter->name; $chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName); $this->showSuccessNotification(trans('entities.chapters_copy_success')); diff --git a/app/Entities/Controllers/PageApiController.php b/app/Entities/Controllers/PageApiController.php index 197018cca..38042e670 100644 --- a/app/Entities/Controllers/PageApiController.php +++ b/app/Entities/Controllers/PageApiController.php @@ -74,9 +74,9 @@ class PageApiController extends ApiController $this->validate($request, $this->rules['create']); if ($request->has('chapter_id')) { - $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id'))); + $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->input('chapter_id'))); } else { - $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id'))); + $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->input('book_id'))); } $this->checkOwnablePermission(Permission::PageCreate, $parent); @@ -133,9 +133,9 @@ class PageApiController extends ApiController $parent = null; if ($request->has('chapter_id')) { - $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->get('chapter_id'))); + $parent = $this->entityQueries->chapters->findVisibleByIdOrFail(intval($request->input('chapter_id'))); } elseif ($request->has('book_id')) { - $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id'))); + $parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->input('book_id'))); } if ($parent && !$parent->matches($page->getParent())) { diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index 8778560e2..82edfbc27 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -88,7 +88,7 @@ class PageController extends Controller $page = $this->pageRepo->getNewDraftPage($parent); $this->pageRepo->publishDraft($page, [ - 'name' => $request->get('name'), + 'name' => $request->input('name'), ]); return redirect($page->getUrl('/edit')); @@ -408,7 +408,7 @@ class PageController extends Controller $this->checkOwnablePermission(Permission::PageUpdate, $page); $this->checkOwnablePermission(Permission::PageDelete, $page); - $entitySelection = $request->get('entity_selection', null); + $entitySelection = $request->input('entity_selection', null); if ($entitySelection === null || $entitySelection === '') { return redirect($page->getUrl()); } @@ -453,7 +453,7 @@ class PageController extends Controller $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkOwnablePermission(Permission::PageView, $page); - $entitySelection = $request->get('entity_selection') ?: null; + $entitySelection = $request->input('entity_selection') ?: null; $newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent(); if (!$newParent instanceof Book && !$newParent instanceof Chapter) { @@ -464,7 +464,7 @@ class PageController extends Controller $this->checkOwnablePermission(Permission::PageCreate, $newParent); - $newName = $request->get('name') ?: $page->name; + $newName = $request->input('name') ?: $page->name; $pageCopy = $cloner->clonePage($page, $newParent, $newName); $this->showSuccessNotification(trans('entities.pages_copy_success')); diff --git a/app/Entities/Controllers/PageTemplateController.php b/app/Entities/Controllers/PageTemplateController.php index c0b972148..9ff2fe029 100644 --- a/app/Entities/Controllers/PageTemplateController.php +++ b/app/Entities/Controllers/PageTemplateController.php @@ -21,8 +21,8 @@ class PageTemplateController extends Controller */ public function list(Request $request) { - $page = $request->get('page', 1); - $search = $request->get('search', ''); + $page = $request->input('page', 1); + $search = $request->input('search', ''); $count = 10; $query = $this->pageQueries->visibleTemplates() diff --git a/app/Entities/Tools/PermissionsUpdater.php b/app/Entities/Tools/PermissionsUpdater.php index f3165b603..5770d02f1 100644 --- a/app/Entities/Tools/PermissionsUpdater.php +++ b/app/Entities/Tools/PermissionsUpdater.php @@ -20,8 +20,8 @@ class PermissionsUpdater */ public function updateFromPermissionsForm(Entity $entity, Request $request): void { - $permissions = $request->get('permissions', null); - $ownerId = $request->get('owned_by', null); + $permissions = $request->input('permissions', null); + $ownerId = $request->input('owned_by', null); $entity->permissions()->delete(); diff --git a/app/Search/SearchApiController.php b/app/Search/SearchApiController.php index 5de7a5110..3ecb955ae 100644 --- a/app/Search/SearchApiController.php +++ b/app/Search/SearchApiController.php @@ -40,9 +40,9 @@ class SearchApiController extends ApiController { $this->validate($request, $this->rules['all']); - $options = SearchOptions::fromString($request->get('query') ?? ''); - $page = intval($request->get('page', '0')) ?: 1; - $count = min(intval($request->get('count', '0')) ?: 20, 100); + $options = SearchOptions::fromString($request->input('query') ?? ''); + $page = intval($request->input('page', '0')) ?: 1; + $count = min(intval($request->input('count', '0')) ?: 20, 100); $results = $this->searchRunner->searchEntities($options, 'all', $page, $count); $this->resultsFormatter->format($results['results']->all(), $options); diff --git a/app/Search/SearchController.php b/app/Search/SearchController.php index 348d44a42..50a73910a 100644 --- a/app/Search/SearchController.php +++ b/app/Search/SearchController.php @@ -24,7 +24,7 @@ class SearchController extends Controller { $searchOpts = SearchOptions::fromRequest($request); $fullSearchString = $searchOpts->toString(); - $page = intval($request->get('page', '0')) ?: 1; + $page = intval($request->input('page', '0')) ?: 1; $count = setting()->getInteger('lists-page-count-search', 18, 1, 1000); $results = $this->searchRunner->searchEntities($searchOpts, 'all', $page, $count); @@ -49,7 +49,7 @@ class SearchController extends Controller */ public function searchBook(Request $request, int $bookId) { - $term = $request->get('term', ''); + $term = $request->input('term', ''); $results = $this->searchRunner->searchBook($bookId, $term); return view('entities.list', ['entities' => $results]); @@ -60,7 +60,7 @@ class SearchController extends Controller */ public function searchChapter(Request $request, int $chapterId) { - $term = $request->get('term', ''); + $term = $request->input('term', ''); $results = $this->searchRunner->searchChapter($chapterId, $term); return view('entities.list', ['entities' => $results]); @@ -72,9 +72,9 @@ class SearchController extends Controller */ public function searchForSelector(Request $request, QueryPopular $queryPopular) { - $entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book']; - $searchTerm = $request->get('term', false); - $permission = $request->get('permission', 'view'); + $entityTypes = $request->filled('types') ? explode(',', $request->input('types')) : ['page', 'chapter', 'book']; + $searchTerm = $request->input('term', false); + $permission = $request->input('permission', 'view'); // Search for entities otherwise show most popular if ($searchTerm !== false) { @@ -93,7 +93,7 @@ class SearchController extends Controller */ public function templatesForSelector(Request $request) { - $searchTerm = $request->get('term', false); + $searchTerm = $request->input('term', false); if ($searchTerm !== false) { $searchOptions = SearchOptions::fromString($searchTerm); @@ -119,7 +119,7 @@ class SearchController extends Controller */ public function searchSuggestions(Request $request) { - $searchTerm = $request->get('term', ''); + $searchTerm = $request->input('term', ''); $entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 5)['results']; foreach ($entities as $entity) { @@ -136,8 +136,8 @@ class SearchController extends Controller */ public function searchSiblings(Request $request, SiblingFetcher $siblingFetcher) { - $type = $request->get('entity_type', null); - $id = $request->get('entity_id', null); + $type = $request->input('entity_type', null); + $id = $request->input('entity_id', null); $entities = $siblingFetcher->fetch($type, $id); diff --git a/app/Search/SearchOptions.php b/app/Search/SearchOptions.php index cfd068386..f3eb58b6b 100644 --- a/app/Search/SearchOptions.php +++ b/app/Search/SearchOptions.php @@ -51,7 +51,7 @@ class SearchOptions } if ($request->has('term')) { - return static::fromString($request->get('term')); + return static::fromString($request->input('term')); } $instance = new SearchOptions(); diff --git a/app/Settings/AppSettingsStore.php b/app/Settings/AppSettingsStore.php index e098d87f8..9c370ca27 100644 --- a/app/Settings/AppSettingsStore.php +++ b/app/Settings/AppSettingsStore.php @@ -44,7 +44,7 @@ class AppSettingsStore } // Clear icon image if requested - if ($request->get('app_icon_reset')) { + if ($request->input('app_icon_reset')) { $this->destroyExistingSettingImage('app-icon'); setting()->remove('app-icon'); foreach ($sizes as $size) { @@ -67,7 +67,7 @@ class AppSettingsStore } // Clear logo image if requested - if ($request->get('app_logo_reset')) { + if ($request->input('app_logo_reset')) { $this->destroyExistingSettingImage('app-logo'); setting()->remove('app-logo'); } diff --git a/app/Settings/MaintenanceController.php b/app/Settings/MaintenanceController.php index b2b2226bf..64e6c0111 100644 --- a/app/Settings/MaintenanceController.php +++ b/app/Settings/MaintenanceController.php @@ -38,7 +38,7 @@ class MaintenanceController extends Controller $this->checkPermission(Permission::SettingsManage); $this->logActivity(ActivityType::MAINTENANCE_ACTION_RUN, 'cleanup-images'); - $checkRevisions = !($request->get('ignore_revisions', 'false') === 'true'); + $checkRevisions = !($request->input('ignore_revisions', 'false') === 'true'); $dryRun = !($request->has('confirm')); $imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun); diff --git a/app/Sorting/BookSortController.php b/app/Sorting/BookSortController.php index 7e2ee5465..4ddbb14bc 100644 --- a/app/Sorting/BookSortController.php +++ b/app/Sorting/BookSortController.php @@ -58,7 +58,7 @@ class BookSortController extends Controller // Sort via map if ($request->filled('sort-tree')) { (new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) { - $sortMap = BookSortMap::fromJson($request->get('sort-tree')); + $sortMap = BookSortMap::fromJson($request->input('sort-tree')); $booksInvolved = $sorter->sortUsingMap($sortMap); // Add activity for involved books. @@ -72,7 +72,7 @@ class BookSortController extends Controller } if ($request->filled('auto-sort')) { - $sortSetId = intval($request->get('auto-sort')) ?: null; + $sortSetId = intval($request->input('auto-sort')) ?: null; if ($sortSetId && SortRule::query()->find($sortSetId) === null) { $sortSetId = null; } diff --git a/app/Uploads/Controllers/AttachmentApiController.php b/app/Uploads/Controllers/AttachmentApiController.php index ea3c4a962..2448b79b5 100644 --- a/app/Uploads/Controllers/AttachmentApiController.php +++ b/app/Uploads/Controllers/AttachmentApiController.php @@ -50,7 +50,7 @@ class AttachmentApiController extends ApiController $this->checkPermission(Permission::AttachmentCreateAll); $requestData = $this->validate($request, $this->rules()['create']); - $pageId = $request->get('uploaded_to'); + $pageId = $request->input('uploaded_to'); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); $this->checkOwnablePermission(Permission::PageUpdate, $page); @@ -134,7 +134,7 @@ class AttachmentApiController extends ApiController $page = $attachment->page; if ($requestData['uploaded_to'] ?? false) { - $pageId = $request->get('uploaded_to'); + $pageId = $request->input('uploaded_to'); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); $attachment->uploaded_to = $requestData['uploaded_to']; } diff --git a/app/Uploads/Controllers/AttachmentController.php b/app/Uploads/Controllers/AttachmentController.php index 9c60fa415..edcf066ac 100644 --- a/app/Uploads/Controllers/AttachmentController.php +++ b/app/Uploads/Controllers/AttachmentController.php @@ -39,7 +39,7 @@ class AttachmentController extends Controller 'file' => array_merge(['required'], $this->attachmentService->getFileValidationRules()), ]); - $pageId = $request->get('uploaded_to'); + $pageId = $request->input('uploaded_to'); $page = $this->pageQueries->findVisibleByIdOrFail($pageId); $this->checkPermission(Permission::AttachmentCreateAll); @@ -125,8 +125,8 @@ class AttachmentController extends Controller $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); $attachment = $this->attachmentService->updateFile($attachment, [ - 'name' => $request->get('attachment_edit_name'), - 'link' => $request->get('attachment_edit_url'), + 'name' => $request->input('attachment_edit_name'), + 'link' => $request->input('attachment_edit_url'), ]); return view('attachments.manager-edit-form', [ @@ -141,7 +141,7 @@ class AttachmentController extends Controller */ public function attachLink(Request $request) { - $pageId = $request->get('attachment_link_uploaded_to'); + $pageId = $request->input('attachment_link_uploaded_to'); try { $this->validate($request, [ @@ -161,8 +161,8 @@ class AttachmentController extends Controller $this->checkPermission(Permission::AttachmentCreateAll); $this->checkOwnablePermission(Permission::PageUpdate, $page); - $attachmentName = $request->get('attachment_link_name'); - $link = $request->get('attachment_link_url'); + $attachmentName = $request->input('attachment_link_name'); + $link = $request->input('attachment_link_url'); $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId)); return view('attachments.manager-link-form', [ @@ -198,7 +198,7 @@ class AttachmentController extends Controller $page = $this->pageQueries->findVisibleByIdOrFail($pageId); $this->checkOwnablePermission(Permission::PageUpdate, $page); - $attachmentOrder = $request->get('order'); + $attachmentOrder = $request->input('order'); $this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId); return response()->json(['message' => trans('entities.attachments_order_updated')]); @@ -231,7 +231,7 @@ class AttachmentController extends Controller $attachmentStream = $this->attachmentService->streamAttachmentFromStorage($attachment); $attachmentSize = $this->attachmentService->getAttachmentFileSize($attachment); - if ($request->get('open') === 'true') { + if ($request->input('open') === 'true') { return $this->download()->streamedInline($attachmentStream, $fileName, $attachmentSize); } diff --git a/app/Uploads/Controllers/DrawioImageController.php b/app/Uploads/Controllers/DrawioImageController.php index f44acd997..8295febc1 100644 --- a/app/Uploads/Controllers/DrawioImageController.php +++ b/app/Uploads/Controllers/DrawioImageController.php @@ -24,10 +24,10 @@ class DrawioImageController extends Controller */ public function list(Request $request, ImageResizer $resizer) { - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - $uploadedToFilter = $request->get('uploaded_to', null); - $parentTypeFilter = $request->get('filter_type', null); + $page = $request->input('page', 1); + $searchTerm = $request->input('search', null); + $uploadedToFilter = $request->input('uploaded_to', null); + $parentTypeFilter = $request->input('filter_type', null); $imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm); $viewData = [ @@ -59,10 +59,10 @@ class DrawioImageController extends Controller ]); $this->checkPermission(Permission::ImageCreateAll); - $imageBase64Data = $request->get('image'); + $imageBase64Data = $request->input('image'); try { - $uploadedTo = $request->get('uploaded_to', 0); + $uploadedTo = $request->input('uploaded_to', 0); $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); diff --git a/app/Uploads/Controllers/GalleryImageController.php b/app/Uploads/Controllers/GalleryImageController.php index 745efcde8..908322be0 100644 --- a/app/Uploads/Controllers/GalleryImageController.php +++ b/app/Uploads/Controllers/GalleryImageController.php @@ -24,10 +24,10 @@ class GalleryImageController extends Controller */ public function list(Request $request, ImageResizer $resizer) { - $page = $request->get('page', 1); - $searchTerm = $request->get('search', null); - $uploadedToFilter = $request->get('uploaded_to', null); - $parentTypeFilter = $request->get('filter_type', null); + $page = $request->input('page', 1); + $searchTerm = $request->input('search', null); + $uploadedToFilter = $request->input('uploaded_to', null); + $parentTypeFilter = $request->input('filter_type', null); $imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 30, $uploadedToFilter, $searchTerm); $viewData = [ @@ -69,7 +69,7 @@ class GalleryImageController extends Controller try { $imageUpload = $request->file('file'); - $uploadedTo = $request->get('uploaded_to', 0); + $uploadedTo = $request->input('uploaded_to', 0); $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); diff --git a/app/Users/Controllers/RoleController.php b/app/Users/Controllers/RoleController.php index 549f6e0ac..b9f06dace 100644 --- a/app/Users/Controllers/RoleController.php +++ b/app/Users/Controllers/RoleController.php @@ -55,7 +55,7 @@ class RoleController extends Controller /** @var ?Role $role */ $role = null; if ($request->has('copy_from')) { - $role = Role::query()->find($request->get('copy_from')); + $role = Role::query()->find($request->input('copy_from')); } if ($role) { @@ -150,7 +150,7 @@ class RoleController extends Controller $this->checkPermission(Permission::UserRolesManage); try { - $migrateRoleId = intval($request->get('migrate_role_id') ?: "0"); + $migrateRoleId = intval($request->input('migrate_role_id') ?: "0"); $this->permissionsRepo->deleteRole($id, $migrateRoleId); } catch (PermissionsException $e) { $this->showErrorNotification($e->getMessage()); diff --git a/app/Users/Controllers/UserAccountController.php b/app/Users/Controllers/UserAccountController.php index a8baba529..21816d5b8 100644 --- a/app/Users/Controllers/UserAccountController.php +++ b/app/Users/Controllers/UserAccountController.php @@ -106,8 +106,8 @@ class UserAccountController extends Controller */ public function updateShortcuts(Request $request) { - $enabled = $request->get('enabled') === 'true'; - $providedShortcuts = $request->get('shortcut', []); + $enabled = $request->input('enabled') === 'true'; + $providedShortcuts = $request->input('shortcut', []); $shortcuts = new UserShortcutMap($providedShortcuts); setting()->putForCurrentUser('ui-shortcuts', $shortcuts->toJson()); @@ -218,7 +218,7 @@ class UserAccountController extends Controller { $this->preventAccessInDemoMode(); - $requestNewOwnerId = intval($request->get('new_owner_id')) ?: null; + $requestNewOwnerId = intval($request->input('new_owner_id')) ?: null; $newOwnerId = userCan(Permission::UsersManage) ? $requestNewOwnerId : null; $this->userRepo->destroy(user(), $newOwnerId); diff --git a/app/Users/Controllers/UserApiController.php b/app/Users/Controllers/UserApiController.php index 25753280f..ebc17e262 100644 --- a/app/Users/Controllers/UserApiController.php +++ b/app/Users/Controllers/UserApiController.php @@ -141,7 +141,7 @@ class UserApiController extends ApiController public function delete(Request $request, string $id) { $user = $this->userRepo->getById($id); - $newOwnerId = $request->get('migrate_ownership_id', null); + $newOwnerId = $request->input('migrate_ownership_id', null); $this->userRepo->destroy($user, $newOwnerId); diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index 494221b14..f93c00a89 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -77,7 +77,7 @@ class UserController extends Controller $this->checkPermission(Permission::UsersManage); $authMethod = config('auth.method'); - $sendInvite = ($request->get('send_invite', 'false') === 'true'); + $sendInvite = ($request->input('send_invite', 'false') === 'true'); $externalAuth = $authMethod === 'ldap' || $authMethod === 'saml2' || $authMethod === 'oidc'; $passwordRequired = ($authMethod === 'standard' && !$sendInvite); @@ -202,7 +202,7 @@ class UserController extends Controller $this->checkPermission(Permission::UsersManage); $user = $this->userRepo->getById($id); - $newOwnerId = intval($request->get('new_owner_id')) ?: null; + $newOwnerId = intval($request->input('new_owner_id')) ?: null; $this->userRepo->destroy($user, $newOwnerId); diff --git a/app/Users/Controllers/UserPreferencesController.php b/app/Users/Controllers/UserPreferencesController.php index 0bed2d22a..f4a56b7bf 100644 --- a/app/Users/Controllers/UserPreferencesController.php +++ b/app/Users/Controllers/UserPreferencesController.php @@ -23,7 +23,7 @@ class UserPreferencesController extends Controller return $this->redirectToRequest($request); } - $view = $request->get('view'); + $view = $request->input('view'); if (!in_array($view, ['grid', 'list'])) { $view = 'list'; } @@ -44,8 +44,8 @@ class UserPreferencesController extends Controller return $this->redirectToRequest($request); } - $sort = substr($request->get('sort') ?: 'name', 0, 50); - $order = $request->get('order') === 'desc' ? 'desc' : 'asc'; + $sort = substr($request->input('sort') ?: 'name', 0, 50); + $order = $request->input('order') === 'desc' ? 'desc' : 'asc'; $sortKey = $type . '_sort'; $orderKey = $type . '_sort_order'; @@ -76,7 +76,7 @@ class UserPreferencesController extends Controller return response('Invalid key', 500); } - $newState = $request->get('expand', 'false'); + $newState = $request->input('expand', 'false'); setting()->putForCurrentUser('section_expansion#' . $type, $newState); return response('', 204); diff --git a/app/Users/Controllers/UserSearchController.php b/app/Users/Controllers/UserSearchController.php index bc0543cab..9734255e7 100644 --- a/app/Users/Controllers/UserSearchController.php +++ b/app/Users/Controllers/UserSearchController.php @@ -26,7 +26,7 @@ class UserSearchController extends Controller $this->showPermissionError(); } - $search = $request->get('search', ''); + $search = $request->input('search', ''); $query = User::query() ->orderBy('name', 'asc') ->take(20); @@ -58,7 +58,7 @@ class UserSearchController extends Controller $this->showPermissionError(); } - $search = $request->get('search', ''); + $search = $request->input('search', ''); $query = User::query() ->orderBy('name', 'asc') ->take(20); diff --git a/app/Util/SimpleListOptions.php b/app/Util/SimpleListOptions.php index 81d8a5876..9fb1b98ae 100644 --- a/app/Util/SimpleListOptions.php +++ b/app/Util/SimpleListOptions.php @@ -30,7 +30,7 @@ class SimpleListOptions */ public static function fromRequest(Request $request, string $typeKey, bool $sortDescDefault = false): self { - $search = $request->get('search', ''); + $search = $request->input('search', ''); $sort = setting()->getForCurrentUser($typeKey . '_sort', ''); $order = setting()->getForCurrentUser($typeKey . '_sort_order', $sortDescDefault ? 'desc' : 'asc'); From befa3a8fbb23cb8026fc646b9c8e8617e095b91e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 12:41:11 +0100 Subject: [PATCH 21/74] Permissions: Started addition of revision-view permission --- .../Controllers/PageRevisionController.php | 7 ++++++ app/Permissions/Permission.php | 2 ++ lang/en/settings.php | 1 + resources/views/entities/meta.blade.php | 2 +- .../show-sidebar-section-actions.blade.php | 10 +++++---- .../views/settings/roles/parts/form.blade.php | 1 + .../parts/revisions-permissions-row.blade.php | 22 +++++++++++++++++++ 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 resources/views/settings/roles/parts/revisions-permissions-row.blade.php diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 4bc15e6e9..0d690cb2c 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -34,6 +34,7 @@ class PageRevisionController extends Controller */ public function index(Request $request, string $bookSlug, string $pageSlug) { + $this->checkPermission(Permission::RevisionViewAll); $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $listOptions = SimpleListOptions::fromRequest($request, 'page_revisions', true)->withSortOptions([ 'id' => trans('entities.pages_revisions_sort_number') @@ -65,6 +66,8 @@ class PageRevisionController extends Controller */ public function show(string $bookSlug, string $pageSlug, int $revisionId) { + $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); /** @var ?PageRevision $revision */ $revision = $page->revisions()->where('id', '=', $revisionId)->first(); @@ -94,6 +97,8 @@ class PageRevisionController extends Controller */ public function changes(string $bookSlug, string $pageSlug, int $revisionId) { + $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); /** @var ?PageRevision $revision */ $revision = $page->revisions()->where('id', '=', $revisionId)->first(); @@ -130,6 +135,7 @@ class PageRevisionController extends Controller public function restore(string $bookSlug, string $pageSlug, int $revisionId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); + $this->checkPermission(Permission::RevisionViewAll); $this->checkOwnablePermission(Permission::PageUpdate, $page); $page = $this->pageRepo->restoreRevision($page, $revisionId); @@ -145,6 +151,7 @@ class PageRevisionController extends Controller public function destroy(string $bookSlug, string $pageSlug, int $revId) { $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); + $this->checkPermission(Permission::RevisionViewAll); $this->checkOwnablePermission(Permission::PageDelete, $page); $revision = $page->revisions()->where('id', '=', $revId)->first(); diff --git a/app/Permissions/Permission.php b/app/Permissions/Permission.php index 04878ada0..0fbe9693d 100644 --- a/app/Permissions/Permission.php +++ b/app/Permissions/Permission.php @@ -118,6 +118,8 @@ enum Permission: string case PageViewAll = 'page-view-all'; case PageViewOwn = 'page-view-own'; + case RevisionViewAll = 'revision-view-all'; + /** * Get the generic permissions which may be queried for entities. */ diff --git a/lang/en/settings.php b/lang/en/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/resources/views/entities/meta.blade.php b/resources/views/entities/meta.blade.php index 060c197a4..6c425a240 100644 --- a/resources/views/entities/meta.blade.php +++ b/resources/views/entities/meta.blade.php @@ -9,7 +9,7 @@
    @endif - @if ($entity->isA('page')) + @if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll)) @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} diff --git a/resources/views/pages/parts/show-sidebar-section-actions.blade.php b/resources/views/pages/parts/show-sidebar-section-actions.blade.php index ae115b69e..94061ecb3 100644 --- a/resources/views/pages/parts/show-sidebar-section-actions.blade.php +++ b/resources/views/pages/parts/show-sidebar-section-actions.blade.php @@ -24,10 +24,12 @@ @endif @endif - - @icon('history') - {{ trans('entities.revisions') }} - + @if(userCan(\BookStack\Permissions\Permission::RevisionViewAll)) + + @icon('history') + {{ trans('entities.revisions') }} + + @endif @if(userCan(\BookStack\Permissions\Permission::RestrictionsManage, $page)) @icon('lock') diff --git a/resources/views/settings/roles/parts/form.blade.php b/resources/views/settings/roles/parts/form.blade.php index 5a9eca7d2..890f79057 100644 --- a/resources/views/settings/roles/parts/form.blade.php +++ b/resources/views/settings/roles/parts/form.blade.php @@ -79,6 +79,7 @@ @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.books'), 'permissionPrefix' => 'book']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.chapters'), 'permissionPrefix' => 'chapter']) @include('settings.roles.parts.asset-permissions-row', ['title' => trans('entities.pages'), 'permissionPrefix' => 'page']) + @include('settings.roles.parts.revisions-permissions-row', ['title' => trans('entities.revisions'), 'permissionPrefix' => 'revision']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.images'), 'permissionPrefix' => 'image']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.attachments'), 'permissionPrefix' => 'attachment']) @include('settings.roles.parts.related-asset-permissions-row', ['title' => trans('entities.comments'), 'permissionPrefix' => 'comment']) diff --git a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php new file mode 100644 index 000000000..fe886a5d0 --- /dev/null +++ b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php @@ -0,0 +1,22 @@ +
    + +
    + {{ trans('common.create') }}
    + - +
    +
    + {{ trans('common.view') }}
    + @include('settings.roles.parts.checkbox', ['permission' => $permissionPrefix . '-view-all', 'label' => trans('settings.role_all')]) +
    +
    + {{ trans('common.edit') }}
    + - +
    +
    + {{ trans('common.delete') }}
    + {{ trans('settings.role_controlled_by_page_delete') }} +
    +
    \ No newline at end of file From 1339f668ebfd0155c15c122f4257e435d23f9a11 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 15:32:10 +0100 Subject: [PATCH 22/74] Permissions: Added revision-view-all addition migration --- ...41616_add_revision_view_all_permission.php | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 database/migrations/2026_04_19_141616_add_revision_view_all_permission.php diff --git a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php new file mode 100644 index 000000000..5a0b9a09b --- /dev/null +++ b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php @@ -0,0 +1,67 @@ +insertGetId([ + 'name' => 'revision-view-all', + 'created_at' => Carbon::now()->toDateTimeString(), + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + + // Get ids of page view permissions + $pageViewPermissions = DB::table('role_permissions') + ->whereIn('name', [ + 'page-view-own', + 'page-view-all', + ])->get(); + + if (!$pageViewPermissions->count() === 0) { + return; + } + + // Get role ids which have page view permission + $applicableRoleIds = DB::table('permission_role') + ->whereIn('permission_id', $pageViewPermissions->pluck('id')) + ->pluck('role_id') + ->unique() + ->all(); + + // Assign the new permission to relevant roles + $newPermissionRoles = array_values(array_map(function (int $roleId) use ($permissionId) { + return [ + 'role_id' => $roleId, + 'permission_id' => $permissionId, + ]; + }, $applicableRoleIds)); + + DB::table('permission_role')->insert($newPermissionRoles); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Get the permission to remove + $revisionViewPermission = DB::table('role_permissions') + ->where('name', '=', 'revision-view-all') + ->first(); + + if (!$revisionViewPermission) { + return; + } + + // Remove the permission, and its use on roles, from the database + DB::table('permission_role')->where('permission_id', '=', $revisionViewPermission->id)->delete(); + DB::table('role_permissions')->where('id', '=', $revisionViewPermission->id)->delete(); + } +}; From e7e019d3d44b263031d4a63f91e4c6bdf3b424eb Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 15:56:54 +0100 Subject: [PATCH 23/74] Permissions: Added testing coverage for revision-view-all --- tests/Entity/PageRevisionTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index 132a10fa4..8b46e84a6 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -4,6 +4,8 @@ namespace Tests\Entity; use BookStack\Activity\ActivityType; use BookStack\Entities\Models\Page; +use BookStack\Entities\Models\PageRevision; +use BookStack\Permissions\Permission; use Tests\TestCase; class PageRevisionTest extends TestCase @@ -257,6 +259,33 @@ class PageRevisionTest extends TestCase $revisionView->assertDontSee('dontwantthishere'); } + public function test_access_to_revision_operation_requires_revision_view_all_permission() + { + $editor = $this->users->editor(); + $this->actingAs($editor); + + $page = $this->entities->page(); + $this->createRevisions($page, 3); + /** @var PageRevision $revision */ + $revision = $page->revisions()->orderBy('id', 'desc')->first(); + + $this->get($page->getUrl())->assertSee($page->getUrl('/revisions'), false); + $this->get($page->getUrl('/revisions'))->assertOk(); + $this->get($revision->getUrl())->assertOk(); + $this->get($revision->getUrl('/changes'))->assertOk(); + $this->put($revision->getUrl('/restore'))->assertRedirect($page->getUrl()); + $this->delete($revision->getUrl('/delete'))->assertRedirect($page->getUrl('/revisions')); + + $this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]); + + $this->get($page->getUrl())->assertDontSee($page->getUrl('/revisions'), false); + $this->assertPermissionError($this->get($page->getUrl('/revisions'))); + $this->assertPermissionError($this->get($revision->getUrl())); + $this->assertPermissionError($this->get($revision->getUrl('/changes'))); + $this->assertPermissionError($this->put($revision->getUrl('/restore'))); + $this->assertPermissionError($this->delete($revision->getUrl('/delete'))); + } + public function test_revision_restore_action_only_visible_with_permission() { $page = $this->entities->page(); From ec0b0384a20f10a5ec44197a3fd5ca8f9fc543aa Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 16:06:31 +0100 Subject: [PATCH 24/74] Permissions: Tweaks/fixed during review of revision-view-all changes --- app/Entities/Controllers/PageRevisionController.php | 4 ++-- .../2026_04_19_141616_add_revision_view_all_permission.php | 2 +- .../settings/roles/parts/revisions-permissions-row.blade.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 0d690cb2c..cc6b79bfe 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -134,8 +134,8 @@ class PageRevisionController extends Controller */ public function restore(string $bookSlug, string $pageSlug, int $revisionId) { - $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkOwnablePermission(Permission::PageUpdate, $page); $page = $this->pageRepo->restoreRevision($page, $revisionId); @@ -150,8 +150,8 @@ class PageRevisionController extends Controller */ public function destroy(string $bookSlug, string $pageSlug, int $revId) { - $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkPermission(Permission::RevisionViewAll); + $page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug); $this->checkOwnablePermission(Permission::PageDelete, $page); $revision = $page->revisions()->where('id', '=', $revId)->first(); diff --git a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php index 5a0b9a09b..e4b51ff70 100644 --- a/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php +++ b/database/migrations/2026_04_19_141616_add_revision_view_all_permission.php @@ -24,7 +24,7 @@ return new class extends Migration 'page-view-all', ])->get(); - if (!$pageViewPermissions->count() === 0) { + if ($pageViewPermissions->count() === 0) { return; } diff --git a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php index fe886a5d0..326925ef9 100644 --- a/resources/views/settings/roles/parts/revisions-permissions-row.blade.php +++ b/resources/views/settings/roles/parts/revisions-permissions-row.blade.php @@ -19,4 +19,4 @@ {{ trans('common.delete') }}
    {{ trans('settings.role_controlled_by_page_delete') }} - \ No newline at end of file + From 426f9ac4934308da9f57580bc0e2fe399346cbb1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 19 Apr 2026 16:23:16 +0100 Subject: [PATCH 25/74] Permissions: Prevent export revision metadata view without permission --- resources/views/exports/parts/meta.blade.php | 2 +- tests/Exports/HtmlExportTest.php | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/resources/views/exports/parts/meta.blade.php b/resources/views/exports/parts/meta.blade.php index 00117f4a1..07eff14a4 100644 --- a/resources/views/exports/parts/meta.blade.php +++ b/resources/views/exports/parts/meta.blade.php @@ -1,5 +1,5 @@
    - @if ($entity->isA('page')) + @if ($entity->isA('page') && userCan(\BookStack\Permissions\Permission::RevisionViewAll)) @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
    @endif diff --git a/tests/Exports/HtmlExportTest.php b/tests/Exports/HtmlExportTest.php index f23352e0e..223a8c922 100644 --- a/tests/Exports/HtmlExportTest.php +++ b/tests/Exports/HtmlExportTest.php @@ -5,6 +5,7 @@ namespace Tests\Exports; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Page; +use BookStack\Permissions\Permission; use Illuminate\Support\Facades\Storage; use Tests\TestCase; @@ -229,6 +230,20 @@ class HtmlExportTest extends TestCase $resp->assertDontSee('ExportWizardTheFifth'); } + public function test_page_export_only_includes_revision_count_if_user_has_revision_view_permissions() + { + $editor = $this->users->editor(); + $page = $this->entities->page(); + + $resp = $this->actingAs($editor)->get($page->getUrl('/export/html')); + $resp->assertSee('Revision #'); + + $this->permissions->removeUserRolePermissions($editor, [Permission::RevisionViewAll]); + + $resp = $this->actingAs($editor)->get($page->getUrl('/export/html')); + $resp->assertDontSee('Revision #'); + } + public function test_html_exports_contain_csp_meta_tag() { $entities = [ From 4f370ccddb4f5fd8eddff5915a9413786ae2a2c7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 20 Apr 2026 14:32:13 +0100 Subject: [PATCH 26/74] Styles: Aligned fonts set on content and headers for exports During review of #6069 --- resources/sass/export-styles.scss | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/sass/export-styles.scss b/resources/sass/export-styles.scss index 22f15d1b8..36901c0f1 100644 --- a/resources/sass/export-styles.scss +++ b/resources/sass/export-styles.scss @@ -12,12 +12,16 @@ html, body { } body { - font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; margin: 0; padding: 0; display: block; } +// Set fonts to common system fonts, starting with DejaVu Sans due to support in DOMPDF +body, h1, h2, h3, h4, h5, h6 { + font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; +} + table { border-spacing: 0; border-collapse: collapse; @@ -64,11 +68,6 @@ body.export-format-pdf { font-size: 14px; line-height: 1.2; - // Ensure heading glyph coverage for PDF engines that don't handle CSS vars well. - h1, h2, h3, h4, h5, h6 { - font-family: 'DejaVu Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Roboto", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - } - h1, h2, h3, h4, h5, h6 { line-height: 1.2; } From e91747785b1950598812348ce9b8da89c7da9e22 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 20 Apr 2026 15:42:28 +0100 Subject: [PATCH 27/74] PDF: Started building system to allow custom DOMPDF font loading --- app/Config/exports.php | 4 +-- app/Exports/PdfGenerator.php | 47 +++++++++++++++++++++++++++ storage/fonts/.gitignore | 6 +++- storage/fonts/dompdf/.gitignore | 3 ++ storage/fonts/dompdf/cache/.gitignore | 2 ++ 5 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 storage/fonts/dompdf/.gitignore create mode 100644 storage/fonts/dompdf/cache/.gitignore diff --git a/app/Config/exports.php b/app/Config/exports.php index 2e22bc759..f48fe0a67 100644 --- a/app/Config/exports.php +++ b/app/Config/exports.php @@ -68,7 +68,7 @@ return [ * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, * Symbol, ZapfDingbats. */ - 'font_dir' => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) + 'font_dir' => storage_path('fonts/dompdf'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) /** * The location of the DOMPDF font cache directory. @@ -78,7 +78,7 @@ return [ * * Note: This directory must exist and be writable by the webserver process. */ - 'font_cache' => storage_path('fonts/'), + 'font_cache' => storage_path('fonts/dompdf/cache'), /** * The location of a temporary directory. diff --git a/app/Exports/PdfGenerator.php b/app/Exports/PdfGenerator.php index f31d8aad0..df40bf44f 100644 --- a/app/Exports/PdfGenerator.php +++ b/app/Exports/PdfGenerator.php @@ -4,6 +4,8 @@ namespace BookStack\Exports; use BookStack\Exceptions\PdfExportException; use Dompdf\Dompdf; +use FontLib\Font; +use Illuminate\Support\Str; use Knp\Snappy\Pdf as SnappyPdf; use Symfony\Component\Process\Exception\ProcessTimedOutException; use Symfony\Component\Process\Process; @@ -60,12 +62,57 @@ class PdfGenerator $domPdf = new Dompdf($options); $domPdf->setBasePath(base_path('public')); + $fontMetrics = $domPdf->getFontMetrics(); + $userFontfamilies = $this->getUserDomPdfFontFamilies(); + foreach ($userFontfamilies as $fontFamily => $fonts) { + $fontMetrics->setFontFamily($fontFamily, $fonts); + } + +// dd($userFontfamilies, $fontMetrics->getFontFamilies()); $domPdf->loadHTML($this->convertEntities($html)); $domPdf->render(); return (string) $domPdf->output(); } + /** + * @return array> + */ + protected function getUserDomPdfFontFamilies(): array + { + $fontStore = storage_path('fonts/dompdf'); + if (!is_dir($fontStore)) { + return []; + } + + $fontFamilies = []; + $fontFiles = glob($fontStore . DIRECTORY_SEPARATOR . '*.ttf'); + foreach ($fontFiles as $fontFile) { + $fontFileName = basename($fontFile, '.ttf'); + $expectedUfm = $fontStore . DIRECTORY_SEPARATOR . $fontFileName . '.ufm'; + if (!file_exists($expectedUfm)) { + $font = Font::load($fontFile); + $font->parse(); + $font->saveAdobeFontMetrics($expectedUfm); + } + + $nameParts = explode('-', $fontFileName); + if (count($nameParts) === 1 || $nameParts[1] === 'Regular') { + $nameParts[1] = 'Normal'; + } + + $family = trim(strtolower(preg_replace('/([A-Z])/', ' $1', $nameParts[0]))); + $variation = Str::snake($nameParts[1]); + if (!isset($fontFamilies[$family])) { + $fontFamilies[$family] = []; + } + + $fontFamilies[$family][$variation] = $fontStore . DIRECTORY_SEPARATOR . $fontFileName; + } + + return $fontFamilies; + } + /** * @throws PdfExportException */ diff --git a/storage/fonts/.gitignore b/storage/fonts/.gitignore index c96a04f00..cb0b47dac 100755 --- a/storage/fonts/.gitignore +++ b/storage/fonts/.gitignore @@ -1,2 +1,6 @@ +# Font cache files have once been stored directly in this folder +# therefore its important the contents non-ignored by git +# are chosen selectively * -!.gitignore \ No newline at end of file +!.gitignore +!dompdf/ \ No newline at end of file diff --git a/storage/fonts/dompdf/.gitignore b/storage/fonts/dompdf/.gitignore new file mode 100644 index 000000000..23ef65311 --- /dev/null +++ b/storage/fonts/dompdf/.gitignore @@ -0,0 +1,3 @@ +* +!.gitignore +!cache/ \ No newline at end of file diff --git a/storage/fonts/dompdf/cache/.gitignore b/storage/fonts/dompdf/cache/.gitignore new file mode 100644 index 000000000..c96a04f00 --- /dev/null +++ b/storage/fonts/dompdf/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From 241563e8fc272e9e12553462a72238d751ffe179 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 22 Apr 2026 13:12:34 +0100 Subject: [PATCH 28/74] Exports: Added testing coverage for DOMPDF font usage --- app/Exports/PdfGenerator.php | 1 - tests/Exports/PdfExportTest.php | 33 +++++++++++++++++++++++++ tests/test-data/fonts/Cardiff-Bold.ttf | Bin 0 -> 66940 bytes tests/test-data/fonts/Cardiff.ttf | Bin 0 -> 67984 bytes tests/test-data/fonts/attribution.txt | 2 ++ 5 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 tests/test-data/fonts/Cardiff-Bold.ttf create mode 100644 tests/test-data/fonts/Cardiff.ttf create mode 100644 tests/test-data/fonts/attribution.txt diff --git a/app/Exports/PdfGenerator.php b/app/Exports/PdfGenerator.php index df40bf44f..10f0624cf 100644 --- a/app/Exports/PdfGenerator.php +++ b/app/Exports/PdfGenerator.php @@ -68,7 +68,6 @@ class PdfGenerator $fontMetrics->setFontFamily($fontFamily, $fonts); } -// dd($userFontfamilies, $fontMetrics->getFontFamilies()); $domPdf->loadHTML($this->convertEntities($html)); $domPdf->render(); diff --git a/tests/Exports/PdfExportTest.php b/tests/Exports/PdfExportTest.php index f311f8457..78da3b0c2 100644 --- a/tests/Exports/PdfExportTest.php +++ b/tests/Exports/PdfExportTest.php @@ -79,6 +79,39 @@ class PdfExportTest extends TestCase $this->assertStringContainsString('
    entities->page()->forceFill([ + 'html' => '

    Boldtext

    ', + ]); + $page->save(); + $this->setSettings([ + 'app-custom-head' => '' + ]); + $normalFont = $this->files->testFilePath('fonts/Cardiff.ttf'); + $normalFontTarget = storage_path('fonts/dompdf/MeowWords.ttf'); + $boldFont = $this->files->testFilePath('fonts/Cardiff-Bold.ttf'); + $boldFontTarget = storage_path('fonts/dompdf/MeowWords-Bold.ttf'); + copy($normalFont, $normalFontTarget); + copy($boldFont, $boldFontTarget); + + $resp = $this->asEditor()->get($page->getUrl('/export/pdf')); + $resp->assertStatus(200); + + // Existance of UFM files indicates the metrics have been generated + $this->assertFileExists(storage_path('fonts/dompdf/MeowWords.ufm')); + $this->assertFileExists(storage_path('fonts/dompdf/MeowWords-Bold.ufm')); + // Existence of cache json files indicates the fonts have been used + $this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords.ufm.json')); + $this->assertFileExists(storage_path('fonts/dompdf/cache/MeowWords-Bold.ufm.json')); + + $filesToCleanUp = [...glob(storage_path('fonts/dompdf/Meow*')), ...glob(storage_path('fonts/dompdf/cache/Meow*'))]; + foreach ($filesToCleanUp as $file) { + unlink($file); + } + } + public function test_wkhtmltopdf_only_used_when_allow_untrusted_is_true() { $page = $this->entities->page(); diff --git a/tests/test-data/fonts/Cardiff-Bold.ttf b/tests/test-data/fonts/Cardiff-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..efaae4d9eb03a58d47a8f096ee53c1fb8c918664 GIT binary patch literal 66940 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 T)!tky7+*wA1H^|--Q)iNX_HIK literal 0 HcmV?d00001 diff --git a/tests/test-data/fonts/Cardiff.ttf b/tests/test-data/fonts/Cardiff.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4e8fea609c6b15e512542d46f8d7f53dcbb32c99 GIT binary patch literal 67984 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 h`ue*swe Date: Wed, 22 Apr 2026 13:22:20 +0100 Subject: [PATCH 29/74] Exports: Improved dompdf font loading permission errors --- app/Exports/PdfGenerator.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/Exports/PdfGenerator.php b/app/Exports/PdfGenerator.php index 10f0624cf..5506fe740 100644 --- a/app/Exports/PdfGenerator.php +++ b/app/Exports/PdfGenerator.php @@ -65,7 +65,12 @@ class PdfGenerator $fontMetrics = $domPdf->getFontMetrics(); $userFontfamilies = $this->getUserDomPdfFontFamilies(); foreach ($userFontfamilies as $fontFamily => $fonts) { - $fontMetrics->setFontFamily($fontFamily, $fonts); + try { + $fontMetrics->setFontFamily($fontFamily, $fonts); + } catch (\Exception $exception) { + $expectedPath = storage_path('fonts/dompdf'); + throw new PdfExportException("Failed to create required font data in {$expectedPath}, Ensure all content in this location is writable by the web server"); + } } $domPdf->loadHTML($this->convertEntities($html)); @@ -92,7 +97,11 @@ class PdfGenerator if (!file_exists($expectedUfm)) { $font = Font::load($fontFile); $font->parse(); - $font->saveAdobeFontMetrics($expectedUfm); + try { + $font->saveAdobeFontMetrics($expectedUfm); + } catch (\Exception $exception) { + throw new PdfExportException("Failed to create required font data at $expectedUfm, Ensure this location is writable by the web server"); + } } $nameParts = explode('-', $fontFileName); From 74aa897626e649cfa17b0c07c5fc1b985624e20a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 24 Apr 2026 23:16:44 +0100 Subject: [PATCH 30/74] Readme: Updated netways sponsor link --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d3a408ad0..340545dcc 100644 --- a/readme.md +++ b/readme.md @@ -72,7 +72,7 @@ Big thanks to these companies for supporting the project. Stellar Hosted - + NETWAYS Web Services From a37f903dc7e48da434947480917829af571721ab Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 12:07:43 +0100 Subject: [PATCH 31/74] CI: Migrated workflows to forgejo --- .forgejo/FUNDING.yml | 4 ++ .../workflows/analyse-php.yml | 17 ++++++--- {.github => .forgejo}/workflows/lint-js.yml | 7 +++- {.github => .forgejo}/workflows/lint-php.yml | 11 ++++-- {.github => .forgejo}/workflows/test-js.yml | 7 +++- .../workflows/test-migrations.yml | 38 +++++++++++-------- {.github => .forgejo}/workflows/test-php.yml | 36 ++++++++++-------- 7 files changed, 76 insertions(+), 44 deletions(-) create mode 100644 .forgejo/FUNDING.yml rename {.github => .forgejo}/workflows/analyse-php.yml (64%) rename {.github => .forgejo}/workflows/lint-js.yml (69%) rename {.github => .forgejo}/workflows/lint-php.yml (57%) rename {.github => .forgejo}/workflows/test-js.yml (74%) rename {.github => .forgejo}/workflows/test-migrations.yml (65%) rename {.github => .forgejo}/workflows/test-php.yml (64%) diff --git a/.forgejo/FUNDING.yml b/.forgejo/FUNDING.yml new file mode 100644 index 000000000..5c50c3f69 --- /dev/null +++ b/.forgejo/FUNDING.yml @@ -0,0 +1,4 @@ +# These are supported funding model platforms + +github: [ssddanbrown] +ko_fi: ssddanbrown diff --git a/.github/workflows/analyse-php.yml b/.forgejo/workflows/analyse-php.yml similarity index 64% rename from .github/workflows/analyse-php.yml rename to .forgejo/workflows/analyse-php.yml index 647835aeb..1214c39fb 100644 --- a/.github/workflows/analyse-php.yml +++ b/.forgejo/workflows/analyse-php.yml @@ -1,6 +1,7 @@ name: analyse-php on: + workflow_dispatch: push: paths: - '**.php' @@ -11,14 +12,16 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye steps: - - uses: actions/checkout@v4 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: https://github.com/shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.5 extensions: gd, mbstring, json, curl, xml, mysql, ldap - name: Get Composer Cache Directory @@ -27,14 +30,16 @@ jobs: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer packages - uses: actions/cache@v4 + uses: https://code.forgejo.org/actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-composer-8.3 + key: ${{ runner.os }}-composer-8.5 restore-keys: ${{ runner.os }}-composer- - name: Install composer dependencies run: composer install --prefer-dist --no-interaction --ansi + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}' - name: Run static analysis check run: composer check-static diff --git a/.github/workflows/lint-js.yml b/.forgejo/workflows/lint-js.yml similarity index 69% rename from .github/workflows/lint-js.yml rename to .forgejo/workflows/lint-js.yml index 9aceea2a2..cef1d0540 100644 --- a/.github/workflows/lint-js.yml +++ b/.forgejo/workflows/lint-js.yml @@ -1,6 +1,7 @@ name: lint-js on: + workflow_dispatch: push: paths: - '**.js' @@ -13,9 +14,11 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye steps: - - uses: actions/checkout@v4 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Install NPM deps run: npm ci diff --git a/.github/workflows/lint-php.yml b/.forgejo/workflows/lint-php.yml similarity index 57% rename from .github/workflows/lint-php.yml rename to .forgejo/workflows/lint-php.yml index cb9dedcb2..abebcc5ec 100644 --- a/.github/workflows/lint-php.yml +++ b/.forgejo/workflows/lint-php.yml @@ -1,6 +1,7 @@ name: lint-php on: + workflow_dispatch: push: paths: - '**.php' @@ -11,14 +12,16 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye steps: - - uses: actions/checkout@v4 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: https://github.com/shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.5 tools: phpcs - name: Run formatting check diff --git a/.github/workflows/test-js.yml b/.forgejo/workflows/test-js.yml similarity index 74% rename from .github/workflows/test-js.yml rename to .forgejo/workflows/test-js.yml index 379f1ebfa..6fa21ee27 100644 --- a/.github/workflows/test-js.yml +++ b/.forgejo/workflows/test-js.yml @@ -1,6 +1,7 @@ name: test-js on: + workflow_dispatch: push: paths: - '**.js' @@ -15,9 +16,11 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye steps: - - uses: actions/checkout@v6 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Install NPM deps run: npm ci diff --git a/.github/workflows/test-migrations.yml b/.forgejo/workflows/test-migrations.yml similarity index 65% rename from .github/workflows/test-migrations.yml rename to .forgejo/workflows/test-migrations.yml index 80075c3f7..7348ff2b3 100644 --- a/.github/workflows/test-migrations.yml +++ b/.forgejo/workflows/test-migrations.yml @@ -1,6 +1,7 @@ name: test-migrations on: + workflow_dispatch: push: paths: - '**.php' @@ -13,15 +14,25 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye strategy: matrix: php: ['8.2', '8.3', '8.4', '8.5'] + services: + mysql: + image: docker.io/library/mariadb:12.2.2-noble + env: + MARIADB_USER: bookstack-test + MARIADB_PASSWORD: bookstack-test + MARIADB_DATABASE: bookstack-test + MARIADB_ROOT_PASSWORD: password steps: - - uses: actions/checkout@v4 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: https://github.com/shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: gd, mbstring, json, curl, xml, mysql, ldap @@ -32,34 +43,31 @@ jobs: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer packages - uses: actions/cache@v4 + uses: https://code.forgejo.org/actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.php }} restore-keys: ${{ runner.os }}-composer- - - name: Start MySQL - run: | - sudo systemctl start mysql - - - name: Create database & user - run: | - mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;' - mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';" - mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';" - mysql -uroot -proot -e 'FLUSH PRIVILEGES;' - - name: Install composer dependencies run: composer install --prefer-dist --no-interaction --ansi + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}' - name: Start migration test + env: + DB_HOST: mysql run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing - name: Start migration:rollback test + env: + DB_HOST: mysql run: | php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing - name: Start migration rerun test + env: + DB_HOST: mysql run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing diff --git a/.github/workflows/test-php.yml b/.forgejo/workflows/test-php.yml similarity index 64% rename from .github/workflows/test-php.yml rename to .forgejo/workflows/test-php.yml index 5f4c16caf..0fc39d9fa 100644 --- a/.github/workflows/test-php.yml +++ b/.forgejo/workflows/test-php.yml @@ -1,6 +1,7 @@ name: test-php on: + workflow_dispatch: push: paths: - '**.php' @@ -13,15 +14,25 @@ on: jobs: build: if: ${{ github.ref != 'refs/heads/l10n_development' }} - runs-on: ubuntu-24.04 + runs-on: docker + container: + image: node:24-bullseye strategy: matrix: php: ['8.2', '8.3', '8.4', '8.5'] + services: + mysql: + image: docker.io/library/mariadb:12.2.2-noble + env: + MARIADB_USER: bookstack-test + MARIADB_PASSWORD: bookstack-test + MARIADB_DATABASE: bookstack-test + MARIADB_ROOT_PASSWORD: password steps: - - uses: actions/checkout@v4 + - uses: https://code.forgejo.org/actions/checkout@v6 - name: Setup PHP - uses: shivammathur/setup-php@v2 + uses: https://github.com/shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp @@ -32,30 +43,25 @@ jobs: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache composer packages - uses: actions/cache@v4 + uses: https://code.forgejo.org/actions/cache@v5 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ matrix.php }} restore-keys: ${{ runner.os }}-composer- - - name: Start Database - run: | - sudo systemctl start mysql - - - name: Setup Database - run: | - mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;' - mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';" - mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';" - mysql -uroot -proot -e 'FLUSH PRIVILEGES;' - - name: Install composer dependencies run: composer install --prefer-dist --no-interaction --ansi + env: + COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}' - name: Migrate and seed the database + env: + DB_HOST: mysql run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing - name: Run PHP tests + env: + DB_HOST: mysql run: php${{ matrix.php }} ./vendor/bin/phpunit From 0f59981932ea927d52c490537e0eb55f4e351d0a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 12:52:05 +0100 Subject: [PATCH 32/74] CI: Updated tests using DB to set test DB URL --- .forgejo/workflows/test-migrations.yml | 6 +++--- .forgejo/workflows/test-php.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/test-migrations.yml b/.forgejo/workflows/test-migrations.yml index 7348ff2b3..089dbd67a 100644 --- a/.forgejo/workflows/test-migrations.yml +++ b/.forgejo/workflows/test-migrations.yml @@ -56,18 +56,18 @@ jobs: - name: Start migration test env: - DB_HOST: mysql + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing - name: Start migration:rollback test env: - DB_HOST: mysql + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' run: | php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing - name: Start migration rerun test env: - DB_HOST: mysql + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing diff --git a/.forgejo/workflows/test-php.yml b/.forgejo/workflows/test-php.yml index 0fc39d9fa..a06cdfa66 100644 --- a/.forgejo/workflows/test-php.yml +++ b/.forgejo/workflows/test-php.yml @@ -56,12 +56,12 @@ jobs: - name: Migrate and seed the database env: - DB_HOST: mysql + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing - name: Run PHP tests env: - DB_HOST: mysql + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' run: php${{ matrix.php }} ./vendor/bin/phpunit From cc6e9e0546f2663d40a51039483d94d9814a57c7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 13:17:58 +0100 Subject: [PATCH 33/74] CI: Attempt a more robust avif support check --- tests/Uploads/ImageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 1088e657e..1bccee2c3 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -75,7 +75,7 @@ class ImageTest extends TestCase public function test_image_display_thumbnail_generation_for_animated_avif_images_uses_original_file() { - if (! function_exists('imageavif')) { + if ((gd_info()['AVIF Support'] ?? false) !== true) { $this->markTestSkipped('imageavif() is not available'); } From 2e2f59fa0f63d40be748ec905c29e76c16738ebd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 13:36:47 +0100 Subject: [PATCH 34/74] CI: Updated images to debian trixie --- .forgejo/workflows/analyse-php.yml | 2 +- .forgejo/workflows/lint-js.yml | 2 +- .forgejo/workflows/lint-php.yml | 2 +- .forgejo/workflows/test-js.yml | 2 +- .forgejo/workflows/test-migrations.yml | 2 +- .forgejo/workflows/test-php.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.forgejo/workflows/analyse-php.yml b/.forgejo/workflows/analyse-php.yml index 1214c39fb..8975d6e52 100644 --- a/.forgejo/workflows/analyse-php.yml +++ b/.forgejo/workflows/analyse-php.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie steps: - uses: https://code.forgejo.org/actions/checkout@v6 diff --git a/.forgejo/workflows/lint-js.yml b/.forgejo/workflows/lint-js.yml index cef1d0540..5cacec67a 100644 --- a/.forgejo/workflows/lint-js.yml +++ b/.forgejo/workflows/lint-js.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie steps: - uses: https://code.forgejo.org/actions/checkout@v6 diff --git a/.forgejo/workflows/lint-php.yml b/.forgejo/workflows/lint-php.yml index abebcc5ec..b409c62e2 100644 --- a/.forgejo/workflows/lint-php.yml +++ b/.forgejo/workflows/lint-php.yml @@ -14,7 +14,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie steps: - uses: https://code.forgejo.org/actions/checkout@v6 diff --git a/.forgejo/workflows/test-js.yml b/.forgejo/workflows/test-js.yml index 6fa21ee27..180e6d545 100644 --- a/.forgejo/workflows/test-js.yml +++ b/.forgejo/workflows/test-js.yml @@ -18,7 +18,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie steps: - uses: https://code.forgejo.org/actions/checkout@v6 diff --git a/.forgejo/workflows/test-migrations.yml b/.forgejo/workflows/test-migrations.yml index 089dbd67a..e969d3e47 100644 --- a/.forgejo/workflows/test-migrations.yml +++ b/.forgejo/workflows/test-migrations.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie strategy: matrix: php: ['8.2', '8.3', '8.4', '8.5'] diff --git a/.forgejo/workflows/test-php.yml b/.forgejo/workflows/test-php.yml index a06cdfa66..5ff2d14a5 100644 --- a/.forgejo/workflows/test-php.yml +++ b/.forgejo/workflows/test-php.yml @@ -16,7 +16,7 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: node:24-bullseye + image: docker.io/library/node:24-trixie strategy: matrix: php: ['8.2', '8.3', '8.4', '8.5'] From c1610c453298770db4c1100863f0aefb55a78eb3 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 17:48:27 +0100 Subject: [PATCH 35/74] Meta: Migrated repo content to forgejo Kept some GitHub templates with warnings about the migration. Made some initial updates to readme for the migration. --- .forgejo/CODE_OF_CONDUCT.md | 2 ++ .../ISSUE_TEMPLATE/api_request.yml | 0 {.github => .forgejo}/ISSUE_TEMPLATE/bug_report.yml | 0 .forgejo/ISSUE_TEMPLATE/config.yml | 13 +++++++++++++ .../ISSUE_TEMPLATE/feature_request.yml | 2 +- .../ISSUE_TEMPLATE/language_request.yml | 0 .../ISSUE_TEMPLATE/support_request.yml | 6 +++--- .../ISSUE_TEMPLATE/z_blank_request.yml | 0 {.github => .forgejo}/SECURITY.md | 0 .forgejo/pull_request_template.md | 11 +++++++++++ .github/ISSUE_TEMPLATE/config.yml | 6 +++--- .github/pull_request_template.md | 13 ++++++------- readme.md | 9 ++++----- 13 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 .forgejo/CODE_OF_CONDUCT.md rename {.github => .forgejo}/ISSUE_TEMPLATE/api_request.yml (100%) rename {.github => .forgejo}/ISSUE_TEMPLATE/bug_report.yml (100%) create mode 100644 .forgejo/ISSUE_TEMPLATE/config.yml rename {.github => .forgejo}/ISSUE_TEMPLATE/feature_request.yml (93%) rename {.github => .forgejo}/ISSUE_TEMPLATE/language_request.yml (100%) rename {.github => .forgejo}/ISSUE_TEMPLATE/support_request.yml (91%) rename {.github => .forgejo}/ISSUE_TEMPLATE/z_blank_request.yml (100%) rename {.github => .forgejo}/SECURITY.md (100%) create mode 100644 .forgejo/pull_request_template.md diff --git a/.forgejo/CODE_OF_CONDUCT.md b/.forgejo/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..7a0265672 --- /dev/null +++ b/.forgejo/CODE_OF_CONDUCT.md @@ -0,0 +1,2 @@ +Please find our community rules on our website here: +https://www.bookstackapp.com/about/community-rules/ \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/api_request.yml b/.forgejo/ISSUE_TEMPLATE/api_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/api_request.yml rename to .forgejo/ISSUE_TEMPLATE/api_request.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.forgejo/ISSUE_TEMPLATE/bug_report.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.yml rename to .forgejo/ISSUE_TEMPLATE/bug_report.yml diff --git a/.forgejo/ISSUE_TEMPLATE/config.yml b/.forgejo/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..a72fb1ef4 --- /dev/null +++ b/.forgejo/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,13 @@ +blank_issues_enabled: false +contact_links: + - name: Community Forum Support + url: https://community.bookstackapp.com + about: Get support by talking with the BookStack team & community. + + - name: Debugging & Common Issues + url: https://www.bookstackapp.com/docs/admin/debugging/ + about: Find details on how to debug issues and view common issues with their resolutions. + + - name: Official Support Plans + url: https://www.bookstackapp.com/support/ + about: View our official support plans that offer assured support for business. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.forgejo/ISSUE_TEMPLATE/feature_request.yml similarity index 93% rename from .github/ISSUE_TEMPLATE/feature_request.yml rename to .forgejo/ISSUE_TEMPLATE/feature_request.yml index ca1f2b830..c1420cb19 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/feature_request.yml @@ -33,7 +33,7 @@ body: attributes: label: Have you searched for an existing open/closed issue? description: | - To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundamental benefit/goal of your request. + To help us keep these issues under control, please ensure you have first [searched our issue list](https://codeberg.org/bookstack/bookstack/issues) for any existing issues that cover the fundamental benefit/goal of your request. options: - label: I have searched for existing issues and none cover my fundamental request required: true diff --git a/.github/ISSUE_TEMPLATE/language_request.yml b/.forgejo/ISSUE_TEMPLATE/language_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/language_request.yml rename to .forgejo/ISSUE_TEMPLATE/language_request.yml diff --git a/.github/ISSUE_TEMPLATE/support_request.yml b/.forgejo/ISSUE_TEMPLATE/support_request.yml similarity index 91% rename from .github/ISSUE_TEMPLATE/support_request.yml rename to .forgejo/ISSUE_TEMPLATE/support_request.yml index ae808a0b6..d60eab711 100644 --- a/.github/ISSUE_TEMPLATE/support_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/support_request.yml @@ -15,11 +15,11 @@ body: - type: checkboxes id: searchissue attributes: - label: Searched GitHub Issues + label: Searched Existing Issues description: | - I have searched for the issue and potential resolutions within the [project's GitHub issue list](https://github.com/BookStackApp/BookStack/issues) + I have searched for the issue and potential resolutions within the [project's issue list](https://codeberg.org/bookstack/bookstack/issues) options: - - label: I have searched GitHub for the issue. + - label: I have searched for the issue. required: true - type: textarea id: scenario diff --git a/.github/ISSUE_TEMPLATE/z_blank_request.yml b/.forgejo/ISSUE_TEMPLATE/z_blank_request.yml similarity index 100% rename from .github/ISSUE_TEMPLATE/z_blank_request.yml rename to .forgejo/ISSUE_TEMPLATE/z_blank_request.yml diff --git a/.github/SECURITY.md b/.forgejo/SECURITY.md similarity index 100% rename from .github/SECURITY.md rename to .forgejo/SECURITY.md diff --git a/.forgejo/pull_request_template.md b/.forgejo/pull_request_template.md new file mode 100644 index 000000000..70f105874 --- /dev/null +++ b/.forgejo/pull_request_template.md @@ -0,0 +1,11 @@ +## Details + + + + +## Checklist + + + +- [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/). +- [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 019667388..0cd657d7a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Discord Chat Support - url: https://discord.gg/ztkBqR2 - about: Realtime support & chat with the BookStack community and the team. + - name: Open Issues Here Instead + url: https://codeberg.org/bookstack/bookstack/issues + about: This project has migrated to Codeberg, please open issues there instead. - name: Debugging & Common Issues url: https://www.bookstackapp.com/docs/admin/debugging/ diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 70f105874..c185f3280 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,11 +1,10 @@ -## Details +**Warning:** - - +This project has migrated to Codeberg: +https://codeberg.org/bookstack/bookstack -## Checklist +Please open pull requests here instead. - +ANY PULL REQUESTS OPENED HERE WILL BE CLOSED WITHOUT COMMENT OR MERGE. -- [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/). -- [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above. +--- \ No newline at end of file diff --git a/readme.md b/readme.md index 340545dcc..2cc24c52e 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # BookStack -[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) +[![Codeberg release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) [![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/development/LICENSE) [![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack) [![Build Status](https://github.com/BookStackApp/BookStack/workflows/test-php/badge.svg)](https://github.com/BookStackApp/BookStack/actions) @@ -11,7 +11,6 @@ [![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) [![Community Discussions](https://img.shields.io/static/v1?label=Community&message=Discussions&color=4d36c4&logo=zulip)](https://community.bookstackapp.com/) [![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://www.bookstackapp.com/links/mastodon) -[![Discord](https://img.shields.io/static/v1?label=Discord&message=chat&color=738adb&logo=discord)](https://www.bookstackapp.com/links/discord)
    [![PeerTube](https://img.shields.io/static/v1?label=PeerTube&message=bookstack@foss.video&color=f2690d&logo=peertube)](https://foss.video/c/bookstack) [![YouTube](https://img.shields.io/static/v1?label=YouTube&message=bookstackapp&color=ff0000&logo=youtube)](https://www.youtube.com/bookstackapp) @@ -23,7 +22,7 @@ A platform for storing and organising information and documentation. Details for * [Demo Instance](https://demo.bookstackapp.com) * [Screenshots](https://www.bookstackapp.com/#screenshots) * [BookStack Blog](https://www.bookstackapp.com/blog) -* [Issue List](https://github.com/BookStackApp/BookStack/issues) +* [Issue List](https://codeberg.org/bookstack/bookstack/issues) * [Community Discussions](https://community.bookstackapp.com/) * [Support Options](https://www.bookstackapp.com/support/) @@ -134,7 +133,7 @@ Security information for administering a BookStack instance can be found on the If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates). -If you would like to report a security concern, details of doing so [can be found here](https://github.com/BookStackApp/BookStack/blob/development/.github/SECURITY.md). +If you would like to report a security concern, details of doing so [can be found here](/.forgejo/SECURITY.md). ## ♿ Accessibility @@ -142,7 +141,7 @@ We want BookStack to remain accessible to as many people as possible. We aim for ## 🖥️ Website, Docs & Blog -The website which contains the project docs & blog can be found in the [BookStackApp/website](https://codeberg.org/bookstack/website) repo. +The website which contains the project docs & blog can be found in the [bookstack/website](https://codeberg.org/bookstack/website) repo. ## ⚖️ License From 7c1d30bc8fc1464b7bcbeae3e70fd91d672759f7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 20:56:05 +0100 Subject: [PATCH 36/74] Translations: Added crowdin workflow action --- .forgejo/workflows/sync-translations.yml | 36 ++++++++++++++++++++++++ crowdin.yml | 5 +++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 .forgejo/workflows/sync-translations.yml diff --git a/.forgejo/workflows/sync-translations.yml b/.forgejo/workflows/sync-translations.yml new file mode 100644 index 000000000..5ff760220 --- /dev/null +++ b/.forgejo/workflows/sync-translations.yml @@ -0,0 +1,36 @@ +name: Crowdin Action + +on: + push: + branches: [ development ] + paths: + - 'lang/**.php' + schedule: + - cron: '30 4 * * *' + workflow_dispatch: + +jobs: + synchronize-with-crowdin: + runs-on: docker + container: + image: docker.io/library/node:24-trixie + + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v6 + + - name: crowdin action + uses: https://github.com/crowdin/github-action@v2 + with: + upload_sources: true + upload_translations: false + download_translations: true + localization_branch_name: l10n_development + create_pull_request: false + github_base_url: https://codeberg.org + env: + # A numeric ID, found at https://crowdin.com/project//tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + + # Visit https://crowdin.com/settings#api-key to create this token + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file diff --git a/crowdin.yml b/crowdin.yml index b803b07ee..53869eabd 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -1,10 +1,13 @@ project_id: "377219" project_identifier: bookstack +api_token_env: CROWDIN_PERSONAL_TOKEN + base_path: . preserve_hierarchy: false pull_request_title: Updated translations with latest Crowdin changes pull_request_labels: - - ":earth_africa: Translations" + - "Translations" + files: - source: /lang/en/*.php translation: /lang/%two_letters_code%/%original_file_name% From 24e6087ef8b54174e851dfd356632abad37ff551 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 27 Apr 2026 21:13:05 +0100 Subject: [PATCH 37/74] Meta: Updated readme shields and fixed workflow value --- .forgejo/workflows/sync-translations.yml | 5 +---- readme.md | 10 +++++----- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/sync-translations.yml b/.forgejo/workflows/sync-translations.yml index 5ff760220..a0c09ba8f 100644 --- a/.forgejo/workflows/sync-translations.yml +++ b/.forgejo/workflows/sync-translations.yml @@ -27,10 +27,7 @@ jobs: download_translations: true localization_branch_name: l10n_development create_pull_request: false - github_base_url: https://codeberg.org + github_base_url: codeberg.org env: - # A numeric ID, found at https://crowdin.com/project//tools/api CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} - - # Visit https://crowdin.com/settings#api-key to create this token CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file diff --git a/readme.md b/readme.md index 2cc24c52e..7bb71949d 100644 --- a/readme.md +++ b/readme.md @@ -1,14 +1,14 @@ # BookStack -[![Codeberg release](https://img.shields.io/github/release/BookStackApp/BookStack.svg)](https://github.com/BookStackApp/BookStack/releases/latest) -[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/BookStackApp/BookStack/blob/development/LICENSE) +[![Codeberg release](https://img.shields.io/gitea/v/release/bookstack/bookstack.svg?gitea_url=https://codeberg.org)](https://codeberg.org/bookstack/bookstack/releases/latest) +[![license](https://img.shields.io/badge/License-MIT-yellow.svg)](https://codeberg.org/bookstack/bookstack/src/branch/development/LICENSE) [![Crowdin](https://badges.crowdin.net/bookstack/localized.svg)](https://crowdin.com/project/bookstack) -[![Build Status](https://github.com/BookStackApp/BookStack/workflows/test-php/badge.svg)](https://github.com/BookStackApp/BookStack/actions) -[![Lint Status](https://github.com/BookStackApp/BookStack/workflows/lint-php/badge.svg)](https://github.com/BookStackApp/BookStack/actions) +[![Build Status](https://codeberg.org/bookstack/bookstack/badges/workflows/test-php.yml/badge.svg)](https://codeberg.org/bookstack/bookstack/actions?workflow=test-php.yml) +[![Lint Status](https://codeberg.org/bookstack/bookstack/badges/workflows/lint-php.yml/badge.svg)](https://codeberg.org/bookstack/bookstack/actions?workflow=lint-php.yml) [![php-metrics](https://img.shields.io/static/v1?label=Metrics&message=php&color=4F5B93)](https://source.bookstackapp.com/php-stats/index.html)
    [![Alternate Source](https://img.shields.io/static/v1?label=Alt+Source&message=Git&color=ef391a&logo=git)](https://source.bookstackapp.com/) -[![Repo Stats](https://img.shields.io/static/v1?label=GitHub+project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) +[![Repo Stats](https://img.shields.io/static/v1?label=Code+Project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) [![Community Discussions](https://img.shields.io/static/v1?label=Community&message=Discussions&color=4d36c4&logo=zulip)](https://community.bookstackapp.com/) [![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://www.bookstackapp.com/links/mastodon)
    From 55317039acd8c94f0d4bf0a275c865ed618a8f30 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 28 Apr 2026 09:30:48 +0100 Subject: [PATCH 38/74] Meta: Converted GitHub references in codebase to Codeberg --- .forgejo/SECURITY.md | 2 +- app/App/PwaManifestBuilder.php | 2 +- dev/docker/db-testing/Dockerfile | 2 +- dev/docs/logical-theme-system.md | 2 +- dev/docs/release-process.md | 6 +++--- dev/docs/wysiwyg-js-api.md | 2 +- readme.md | 11 +++++------ .../views/api-docs/parts/getting-started.blade.php | 4 ++-- resources/views/errors/debug.blade.php | 6 +++--- resources/views/settings/layout.blade.php | 2 +- tests/Entity/PageContentTest.php | 2 +- 11 files changed, 20 insertions(+), 21 deletions(-) diff --git a/.forgejo/SECURITY.md b/.forgejo/SECURITY.md index 12e5fe04c..f426079e8 100644 --- a/.forgejo/SECURITY.md +++ b/.forgejo/SECURITY.md @@ -2,7 +2,7 @@ ## Supported Versions -Only the [latest version](https://github.com/BookStackApp/BookStack/releases) of BookStack is supported. +Only the [latest version](https://codeberg.org/bookstack/bookstack/releases) of BookStack is supported. We generally don't support older versions of BookStack due to maintenance effort and since we aim to provide a fairly stable upgrade path for new versions. diff --git a/app/App/PwaManifestBuilder.php b/app/App/PwaManifestBuilder.php index 81ab2fcd7..2dbaead13 100644 --- a/app/App/PwaManifestBuilder.php +++ b/app/App/PwaManifestBuilder.php @@ -10,7 +10,7 @@ class PwaManifestBuilder // does not start a session, so we won't have current user context. // This was attempted but removed since manifest calls could affect user session // history tracking and back redirection. - // Context: https://github.com/BookStackApp/BookStack/issues/4649 + // Context: https://codeberg.org/bookstack/bookstack/issues/4649 $darkMode = (bool) setting()->getForCurrentUser('dark-mode-enabled'); $appName = setting('app-name'); diff --git a/dev/docker/db-testing/Dockerfile b/dev/docker/db-testing/Dockerfile index 618f5bb82..411e2197b 100644 --- a/dev/docker/db-testing/Dockerfile +++ b/dev/docker/db-testing/Dockerfile @@ -18,7 +18,7 @@ ARG BRANCH=development # Download BookStack & install PHP deps RUN mkdir -p /var/www && \ - git clone https://github.com/bookstackapp/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \ + git clone https://codeberg.org/bookstack/bookstack.git --branch "$BRANCH" --single-branch /var/www/bookstack && \ cd /var/www/bookstack && \ wget https://raw.githubusercontent.com/composer/getcomposer.org/f3108f64b4e1c1ce6eb462b159956461592b3e3e/web/installer -O - -q | php -- --quiet --filename=composer && \ php composer install diff --git a/dev/docs/logical-theme-system.md b/dev/docs/logical-theme-system.md index 9457ca78b..b6342ce54 100644 --- a/dev/docs/logical-theme-system.md +++ b/dev/docs/logical-theme-system.md @@ -74,7 +74,7 @@ Theme::registerCommand(new SayHelloCommand()); ## Available Events -All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on GitHub here](https://github.com/BookStackApp/BookStack/blob/release/app/Theming/ThemeEvents.php). +All available events dispatched by BookStack are exposed as static properties on the `\BookStack\Theming\ThemeEvents` class, which can be found within the file `app/Theming/ThemeEvents.php` relative to your root BookStack folder. Alternatively, the events for the latest release can be [seen on Codeberg here](https://codeberg.org/bookstack/bookstack/src/branch/release/app/Theming/ThemeEvents.php). The comments above each constant with the `ThemeEvents.php` file describe the dispatch conditions of the event, in addition to the arguments the action will receive. The comments may also describe any ways the return value of the action may be used. diff --git a/dev/docs/release-process.md b/dev/docs/release-process.md index 758d6db4c..75e9a5ae6 100644 --- a/dev/docs/release-process.md +++ b/dev/docs/release-process.md @@ -12,13 +12,13 @@ Feature releases are generally larger, bringing new features in addition to fixe ### Release Planning Process -Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. +Each BookStack release will have a [milestone](https://codeberg.org/bookstack/bookstack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed. ### Release Announcements -Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates). +Feature releases, and some patch releases, will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [Codeberg release page](https://codeberg.org/bookstack/bookstack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blog posts (once per week maximum) [at this link](https://updates.bookstackapp.com/signup/bookstack-news-and-updates). ### Release Technical Process Deploying a release, at a high level, simply involves merging the development branch into the release branch before then building & committing any release-only assets. -A helper script [can be found in our](https://github.com/BookStackApp/devops/blob/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release. \ No newline at end of file +A helper script [can be found in our](https://codeberg.org/bookstack/devops/src/branch/main/meta-scripts/bookstack-release-steps) devops repo which provides the steps and commands for deploying a new release. \ No newline at end of file diff --git a/dev/docs/wysiwyg-js-api.md b/dev/docs/wysiwyg-js-api.md index 4b4fafe56..869bf8c49 100644 --- a/dev/docs/wysiwyg-js-api.md +++ b/dev/docs/wysiwyg-js-api.md @@ -2,7 +2,7 @@ **Warning: This API is currently in development and may change without notice.** -Feedback is very much welcomed via this issue: https://github.com/BookStackApp/BookStack/issues/5937 +Feedback is very much welcomed via this issue: https://codeberg.org/bookstack/bookstack/issues/5937 This document covers the JavaScript API for the (newer Lexical-based) WYSIWYG editor. This API is built and designed to abstract the internals of the editor away diff --git a/readme.md b/readme.md index 7bb71949d..8b81abaee 100644 --- a/readme.md +++ b/readme.md @@ -8,7 +8,6 @@ [![php-metrics](https://img.shields.io/static/v1?label=Metrics&message=php&color=4F5B93)](https://source.bookstackapp.com/php-stats/index.html)
    [![Alternate Source](https://img.shields.io/static/v1?label=Alt+Source&message=Git&color=ef391a&logo=git)](https://source.bookstackapp.com/) -[![Repo Stats](https://img.shields.io/static/v1?label=Code+Project&message=stats&color=f27e3f)](https://gh-stats.bookstackapp.com/) [![Community Discussions](https://img.shields.io/static/v1?label=Community&message=Discussions&color=4d36c4&logo=zulip)](https://community.bookstackapp.com/) [![Mastodon](https://img.shields.io/static/v1?label=Mastodon&message=@bookstack&color=595aff&logo=mastodon)](https://www.bookstackapp.com/links/mastodon)
    @@ -112,13 +111,13 @@ Translations for text within BookStack are managed through the [BookStack projec Please use [Crowdin](https://crowdin.com/project/bookstack) to contribute translations instead of opening a pull request. The translations within the working codebase can be out-of-date, and merging via code can cause conflicts & sync issues. If for some reason you can't use Crowdin feel free to open an issue to discuss alternative options. -If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://github.com/BookStackApp/BookStack/issues/new?template=language_request.yml). +If you'd like a new language to be added to Crowdin, for you to be able to provide translations for, please [open a new issue here](https://codeberg.org/bookstack/bookstack/issues/new?template=.forgejo%2fISSUE_TEMPLATE%2flanguage_request.yml). -Please note, translations in BookStack are provided to the "Crowdin Global Translation Memory" which helps BookStack and other projects with finding translations. If you are not happy with contributing to this then providing translations to BookStack, even manually via GitHub, is not advised. +Please note, translations in BookStack are provided to the "Crowdin Global Translation Memory" which helps BookStack and other projects with finding translations. If you are not happy with contributing to this then providing translations to BookStack, even manually via code, is not advised. ## 🎁 Contributing, Issues & Pull Requests -Feel free to [create issues](https://github.com/BookStackApp/BookStack/issues/new/choose) to request new features or to report bugs & problems. Just please follow the template given when creating the issue. +Feel free to [create issues](https://codeberg.org/bookstack/bookstack/issues/new/choose) to request new features or to report bugs & problems. Just please follow the template given when creating the issue. Pull requests are welcome but, unless it's a small tweak, it may be best to open the pull request early or create an issue for your intended change to discuss how it will fit into the project and plan out the merge. Just because a feature request exists, or is tagged, does not mean that feature would be accepted into the core project. @@ -145,14 +144,14 @@ The website which contains the project docs & blog can be found in the [bookstac ## ⚖️ License -The BookStack source is provided under the [MIT License](https://github.com/BookStackApp/BookStack/blob/development/LICENSE). +The BookStack source is provided under the [MIT License](https://codeberg.org/bookstack/bookstack/src/branch/development/LICENSE). The libraries used by, and included with, BookStack are provided under their own licenses and copyright. The licenses for many of our core dependencies can be found in the attribution list below, but this is not an exhaustive list of all projects used within BookStack. ## 👪 Attribution -The great people that have worked to build and improve BookStack can [be seen here](https://github.com/BookStackApp/BookStack/graphs/contributors). The wonderful people that have provided translations, either through GitHub or via Crowdin [can be seen here](https://github.com/BookStackApp/BookStack/blob/development/.github/translators.txt). +The great people that have worked to build and improve BookStack can [be seen here](https://codeberg.org/bookstack/bookstack/activity/contributors). The wonderful people that have provided translations, either through GitHub, Codeberg or via Crowdin [can be seen here](https://codeberg.org/bookstack/bookstack/src/branch/development/.github/translators.txt). Below are the great open-source projects used to help build BookStack. Note: This is not an exhaustive list of all libraries and projects that would be used in an active BookStack instance. diff --git a/resources/views/api-docs/parts/getting-started.blade.php b/resources/views/api-docs/parts/getting-started.blade.php index ebe3838ef..8a14befa5 100644 --- a/resources/views/api-docs/parts/getting-started.blade.php +++ b/resources/views/api-docs/parts/getting-started.blade.php @@ -14,11 +14,11 @@ HTTP POST calls upon events occurring in BookStack.
  • - Visual Theme System - + Visual Theme System - Methods to override views, translations and icons within BookStack.
  • - Logical Theme System - + Logical Theme System - Methods to extend back-end functionality within BookStack.
  • diff --git a/resources/views/errors/debug.blade.php b/resources/views/errors/debug.blade.php index e7155431c..969c49595 100644 --- a/resources/views/errors/debug.blade.php +++ b/resources/views/errors/debug.blade.php @@ -113,13 +113,13 @@ Review BookStack debugging documentation »
  • - Ensure your instance is up-to-date » + Ensure your instance is up-to-date »
  • - Search for the issue on GitHub » + Search for the issue on GitHub »
  • - Ask for help via Discord » + Ask for help in our community forums »
  • Search the error message » diff --git a/resources/views/settings/layout.blade.php b/resources/views/settings/layout.blade.php index 930d407a5..2ca06b86a 100644 --- a/resources/views/settings/layout.blade.php +++ b/resources/views/settings/layout.blade.php @@ -18,7 +18,7 @@
    {{ trans('settings.system_version') }}
    - + BookStack @if(!str_starts_with($version, 'v')) version @endif {{ $version }}
    diff --git a/tests/Entity/PageContentTest.php b/tests/Entity/PageContentTest.php index deae153e1..4d97e6b59 100644 --- a/tests/Entity/PageContentTest.php +++ b/tests/Entity/PageContentTest.php @@ -370,7 +370,7 @@ class PageContentTest extends TestCase public function test_base64_images_within_html_blanked_if_not_supported_extension_for_extract() { - // Relevant to https://github.com/BookStackApp/BookStack/issues/3010 and other cases + // Relevant to https://codeberg.org/bookstack/bookstack/issues/3010 and other cases $extensions = [ 'jiff', 'pngr', 'png ', ' png', '.png', 'png.', 'p.ng', ',png', 'data:image/png', ',data:image/png', From 3ddfa9b94838a19840bee477e8cb017ff9804b34 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 30 Apr 2026 00:32:27 +0100 Subject: [PATCH 39/74] Meta: Updated security info and fixed some tests/links --- .forgejo/SECURITY.md | 10 ++++------ readme.md | 2 +- tests/DebugViewTest.php | 2 +- version | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/.forgejo/SECURITY.md b/.forgejo/SECURITY.md index f426079e8..5c044c946 100644 --- a/.forgejo/SECURITY.md +++ b/.forgejo/SECURITY.md @@ -12,16 +12,14 @@ If you'd like to be notified of new potential security concerns you can [sign-up ## Reporting a Vulnerability -If you've found an issue that likely has no impact to existing users (For example, in a development-only branch) -feel free to raise it via a standard GitHub bug report issue. +If you've found an issue that likely has no impact to existing users (For example, an issue only in the development branch) +feel free to raise it via a standard Codeberg bug report issue. If the issue could have a security impact to BookStack instances, -please directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown). -You will need to log in to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). -Alternatively you can send a DM via Mastodon to [@danb@fosstodon.org](https://fosstodon.org/@danb). +please directly contact the lead maintainer via email Dan Brown using the [details found here](https://www.bookstackapp.com/links/contact/). Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability can often take a little time due to the amount of preparation required, to ensure the vulnerability has been covered, and to create the content required to adequately notify the user-base. -Thank you for keeping BookStack instances safe! +Thank you for keeping BookStack instances safe! \ No newline at end of file diff --git a/readme.md b/readme.md index 8b81abaee..30d4d1789 100644 --- a/readme.md +++ b/readme.md @@ -132,7 +132,7 @@ Security information for administering a BookStack instance can be found on the If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](https://updates.bookstackapp.com/signup/bookstack-security-updates). -If you would like to report a security concern, details of doing so [can be found here](/.forgejo/SECURITY.md). +If you would like to report a security concern, details of doing so [can be found here](.forgejo/SECURITY.md). ## ♿ Accessibility diff --git a/tests/DebugViewTest.php b/tests/DebugViewTest.php index 34de6b802..b9a99ec72 100644 --- a/tests/DebugViewTest.php +++ b/tests/DebugViewTest.php @@ -27,7 +27,7 @@ class DebugViewTest extends TestCase $resp->assertSeeText('BookStack Version: ' . trim(file_get_contents(base_path('version')))); // Dynamic help links $this->withHtml($resp)->assertElementExists('a[href*="q=' . urlencode('BookStack An error occurred during testing') . '"]'); - $this->withHtml($resp)->assertElementExists('a[href*="?q=is%3Aissue+' . urlencode('An error occurred during testing') . '"]'); + $this->withHtml($resp)->assertElementExists('a[href*="?q=' . urlencode('An error occurred during testing') . '"]'); } public function test_debug_view_only_shows_when_debug_mode_is_enabled() diff --git a/version b/version index 14f310dc3..9eadc470b 100644 --- a/version +++ b/version @@ -1 +1 @@ -v26.01-dev +v26.05-dev From ccbeefe674b1db6a7c882f3868f66108a05f7d01 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 5 May 2026 04:30:37 +0000 Subject: [PATCH 40/74] New Crowdin translations by GitHub Action --- lang/ar/entities.php | 1 + lang/ar/settings.php | 1 + lang/bg/entities.php | 1 + lang/bg/settings.php | 1 + lang/bn/entities.php | 1 + lang/bn/settings.php | 1 + lang/bs/entities.php | 1 + lang/bs/settings.php | 1 + lang/ca/entities.php | 1 + lang/ca/settings.php | 1 + lang/cs/entities.php | 1 + lang/cs/settings.php | 1 + lang/cy/entities.php | 1 + lang/cy/settings.php | 1 + lang/da/entities.php | 1 + lang/da/settings.php | 1 + lang/de/entities.php | 1 + lang/de/settings.php | 1 + lang/de/validation.php | 2 +- lang/de_informal/entities.php | 5 +- lang/de_informal/notifications.php | 4 +- lang/de_informal/settings.php | 3 +- lang/de_informal/validation.php | 2 +- lang/el/entities.php | 1 + lang/el/settings.php | 1 + lang/es/entities.php | 1 + lang/es/settings.php | 1 + lang/es_AR/entities.php | 1 + lang/es_AR/settings.php | 1 + lang/et/entities.php | 1 + lang/et/settings.php | 1 + lang/eu/entities.php | 1 + lang/eu/settings.php | 1 + lang/fa/entities.php | 1 + lang/fa/settings.php | 1 + lang/fi/entities.php | 1 + lang/fi/settings.php | 1 + lang/fr/auth.php | 4 +- lang/fr/entities.php | 1 + lang/fr/settings.php | 1 + lang/he/entities.php | 1 + lang/he/settings.php | 1 + lang/hr/entities.php | 1 + lang/hr/settings.php | 1 + lang/hu/entities.php | 1 + lang/hu/settings.php | 1 + lang/id/entities.php | 1 + lang/id/settings.php | 1 + lang/is/entities.php | 1 + lang/is/settings.php | 1 + lang/it/entities.php | 1 + lang/it/settings.php | 1 + lang/ja/entities.php | 1 + lang/ja/settings.php | 1 + lang/ka/entities.php | 1 + lang/ka/settings.php | 1 + lang/ko/entities.php | 1 + lang/ko/settings.php | 1 + lang/ku/entities.php | 1 + lang/ku/settings.php | 1 + lang/lt/entities.php | 1 + lang/lt/settings.php | 1 + lang/lv/entities.php | 1 + lang/lv/settings.php | 1 + lang/nb/entities.php | 1 + lang/nb/settings.php | 1 + lang/ne/entities.php | 1 + lang/ne/settings.php | 1 + lang/nl/entities.php | 1 + lang/nl/settings.php | 1 + lang/nn/entities.php | 1 + lang/nn/settings.php | 1 + lang/pl/entities.php | 1 + lang/pl/settings.php | 1 + lang/pt/entities.php | 1 + lang/pt/settings.php | 1 + lang/pt_BR/auth.php | 2 +- lang/pt_BR/entities.php | 1 + lang/pt_BR/settings.php | 1 + lang/ro/entities.php | 1 + lang/ro/settings.php | 1 + lang/ru/activities.php | 10 +- lang/ru/common.php | 4 +- lang/ru/editor.php | 6 +- lang/ru/entities.php | 23 +- lang/ru/notifications.php | 2 +- lang/ru/preferences.php | 2 +- lang/ru/settings.php | 25 +- lang/ru/validation.php | 10 +- lang/sk/entities.php | 1 + lang/sk/settings.php | 1 + lang/sl/entities.php | 1 + lang/sl/settings.php | 1 + lang/sq/entities.php | 1 + lang/sq/settings.php | 1 + lang/sr/entities.php | 1 + lang/sr/settings.php | 1 + lang/sv/entities.php | 1 + lang/sv/settings.php | 1 + lang/th/activities.php | 140 +++++++++ lang/th/auth.php | 117 +++++++ lang/th/common.php | 115 +++++++ lang/th/components.php | 46 +++ lang/th/editor.php | 182 +++++++++++ lang/th/entities.php | 477 +++++++++++++++++++++++++++++ lang/th/errors.php | 135 ++++++++ lang/th/notifications.php | 29 ++ lang/th/pagination.php | 12 + lang/th/passwords.php | 15 + lang/th/preferences.php | 52 ++++ lang/th/settings.php | 375 +++++++++++++++++++++++ lang/th/validation.php | 123 ++++++++ lang/tk/entities.php | 1 + lang/tk/settings.php | 1 + lang/tr/entities.php | 1 + lang/tr/settings.php | 1 + lang/uk/entities.php | 1 + lang/uk/settings.php | 1 + lang/uz/entities.php | 1 + lang/uz/settings.php | 1 + lang/vi/entities.php | 1 + lang/vi/settings.php | 1 + lang/zh_CN/entities.php | 1 + lang/zh_CN/settings.php | 1 + lang/zh_TW/entities.php | 1 + lang/zh_TW/settings.php | 1 + 126 files changed, 1970 insertions(+), 50 deletions(-) create mode 100644 lang/th/activities.php create mode 100644 lang/th/auth.php create mode 100644 lang/th/common.php create mode 100644 lang/th/components.php create mode 100644 lang/th/editor.php create mode 100644 lang/th/entities.php create mode 100644 lang/th/errors.php create mode 100644 lang/th/notifications.php create mode 100644 lang/th/pagination.php create mode 100644 lang/th/passwords.php create mode 100644 lang/th/preferences.php create mode 100644 lang/th/settings.php create mode 100644 lang/th/validation.php diff --git a/lang/ar/entities.php b/lang/ar/entities.php index 8b2882c60..64192a021 100644 --- a/lang/ar/entities.php +++ b/lang/ar/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.', 'books_sort_auto_sort' => 'خِيار الفرز التلقائي', 'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'فرز كتاب :bookName', 'books_sort_name' => 'ترتيب حسب الإسم', 'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء', diff --git a/lang/ar/settings.php b/lang/ar/settings.php index 3191bbe3a..a1fef8364 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'الكل', 'role_own' => 'ما يخص', 'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'حفظ الدور', 'role_users' => 'مستخدمون داخل هذا الدور', 'role_users_none' => 'لم يتم تعيين أي مستخدمين لهذا الدور', diff --git a/lang/bg/entities.php b/lang/bg/entities.php index 42cdad801..19e4f5188 100644 --- a/lang/bg/entities.php +++ b/lang/bg/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Сортирай книга :bookName', 'books_sort_name' => 'Сортиране по име', 'books_sort_created' => 'Сортирай по дата на създаване', diff --git a/lang/bg/settings.php b/lang/bg/settings.php index a1297e446..0554100fd 100644 --- a/lang/bg/settings.php +++ b/lang/bg/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Всички', 'role_own' => 'Собствени', 'role_controlled_by_asset' => 'Контролирани от актива, към който са качени', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Запази ролята', 'role_users' => 'Потребители в тази роля', 'role_users_none' => 'В момента няма потребители, назначени за тази роля', diff --git a/lang/bn/entities.php b/lang/bn/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/bn/entities.php +++ b/lang/bn/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/bn/settings.php b/lang/bn/settings.php index 94ad059d4..1bc5d1551 100644 --- a/lang/bn/settings.php +++ b/lang/bn/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/bs/entities.php b/lang/bs/entities.php index f671e1b45..b490c8c94 100644 --- a/lang/bs/entities.php +++ b/lang/bs/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sortiraj knjigu :bookName', 'books_sort_name' => 'Sortiraj po imenu', 'books_sort_created' => 'Sortiraj po datumu kreiranja', diff --git a/lang/bs/settings.php b/lang/bs/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/bs/settings.php +++ b/lang/bs/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/ca/entities.php b/lang/ca/entities.php index 108a38c2c..edbfdbc44 100644 --- a/lang/ca/entities.php +++ b/lang/ca/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Mou capítols i pàgines dins d\'un llibre per reorganitzar el seu contingut. Es poden afegir altres llibres que permetin moure fàcilment capítols i pàgines entre llibres. De manera opcional, es poden establir regles d\'ordenació automàtica per ordenar automàticament el contingut d\'aquest llibre quan hi hagi canvis.', 'books_sort_auto_sort' => 'Opció d\'ordenació automàtica', 'books_sort_auto_sort_active' => 'Opció d\'ordenació activa :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Ordena el llibre «:bookName»', 'books_sort_name' => 'Ordena pel nom', 'books_sort_created' => 'Ordena per la data de creació', diff --git a/lang/ca/settings.php b/lang/ca/settings.php index a890b9809..2a2106eb3 100644 --- a/lang/ca/settings.php +++ b/lang/ca/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Tot', 'role_own' => 'Propi', 'role_controlled_by_asset' => 'Controlat pel recurs a què estan pujats', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Desa el rol', 'role_users' => 'Usuaris assignats en aquest rol', 'role_users_none' => 'No hi ha cap usuari assignat en aquest rol', diff --git a/lang/cs/entities.php b/lang/cs/entities.php index d65d85ccb..7db62f7bb 100644 --- a/lang/cs/entities.php +++ b/lang/cs/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Pro přeuspořádání obsahu přesuňte kapitoly a stránky v knize. Mohou být přidány další knihy, které umožní snadný přesun kapitol a stránek mezi knihami. Volitelně lze nastavit pravidlo automatického řazení, aby se při změnách automaticky seřadil obsah této knihy.', 'books_sort_auto_sort' => 'Možnost automatického řazení', 'books_sort_auto_sort_active' => 'Aktivní automatické řazení: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Seřadit knihu :bookName', 'books_sort_name' => 'Seřadit podle názvu', 'books_sort_created' => 'Seřadit podle data vytvoření', diff --git a/lang/cs/settings.php b/lang/cs/settings.php index a8c4036e8..a7ab9927d 100644 --- a/lang/cs/settings.php +++ b/lang/cs/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Vše', 'role_own' => 'Vlastní', 'role_controlled_by_asset' => 'Řídí se obsahem, do kterého jsou nahrávány', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Uložit roli', 'role_users' => 'Uživatelé mající tuto roli', 'role_users_none' => 'Žádný uživatel nemá tuto roli', diff --git a/lang/cy/entities.php b/lang/cy/entities.php index e6df4317b..af1f6c43c 100644 --- a/lang/cy/entities.php +++ b/lang/cy/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Trefnu Llyfr :bookName', 'books_sort_name' => 'Trefnu yn ôl Enw', 'books_sort_created' => 'Trefnu yn ôl Dyddiad Creu', diff --git a/lang/cy/settings.php b/lang/cy/settings.php index f4fbf0bba..a0519cccf 100644 --- a/lang/cy/settings.php +++ b/lang/cy/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Popeth', 'role_own' => 'Meddu', 'role_controlled_by_asset' => 'Wedi\'u rheoli gan yr ased y maent yn cael eu huwchlwytho iddo', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Cadw Rôl', 'role_users' => 'Defnyddwyr yn y rôl hon', 'role_users_none' => 'Nid oes unrhyw ddefnyddwyr wedi’u neilltuo i\'r rôl hon ar hyn o bryd', diff --git a/lang/da/entities.php b/lang/da/entities.php index ecda8a8cf..0aa35cc5d 100644 --- a/lang/da/entities.php +++ b/lang/da/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Flyt kapitler og sider i en bog for at omorganisere dens indhold. Der kan tilføjes andre bøger, som gør det nemt at flytte kapitler og sider mellem bøgerne. Man kan indstille en automatisk sorteringsregel, så bogens indhold automatisk sorteres efter ændringer.', 'books_sort_auto_sort' => 'Mulighed for automatisk sortering', 'books_sort_auto_sort_active' => 'Automatisk sortering Aktiv: :sortName', + 'books_sort_auto_sort_creation_hint' => 'En bruger med de nødvendige rettigheder kan oprette regler for automatisk sortering i indstillingsområdet »Lister og sortering«.', 'books_sort_named' => 'Sorter bog :bookName', 'books_sort_name' => 'Sortér efter navn', 'books_sort_created' => 'Sortér efter oprettelsesdato', diff --git a/lang/da/settings.php b/lang/da/settings.php index 1edf10d0e..fb5a1c958 100644 --- a/lang/da/settings.php +++ b/lang/da/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alle', 'role_own' => 'Eget', 'role_controlled_by_asset' => 'Styres af det medie/"asset", de uploades til', + 'role_controlled_by_page_delete' => 'Styres af tilladelser til sletning af sider', 'role_save' => 'Gem rolle', 'role_users' => 'Brugere med denne rolle', 'role_users_none' => 'Ingen brugere er i øjeblikket tildelt denne rolle', diff --git a/lang/de/entities.php b/lang/de/entities.php index 52f9b7aca..db5d070f4 100644 --- a/lang/de/entities.php +++ b/lang/de/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Verschieben Sie Kapitel und Seiten innerhalb eines Buches, um dessen Inhalt neu zu ordnen. Es können weitere Bücher hinzugefügt werden, wodurch Kapitel und Seiten problemlos zwischen den Büchern verschoben werden können. Optional kann eine automatische Sortierregel festgelegt werden, um den Inhalt dieses Buches bei Änderungen automatisch zu sortieren.', 'books_sort_auto_sort' => 'Automatische Sortierfunktionsoption', 'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Buch ":bookName" sortieren', 'books_sort_name' => 'Sortieren nach Namen', 'books_sort_created' => 'Sortieren nach Erstellungsdatum', diff --git a/lang/de/settings.php b/lang/de/settings.php index 64af973a4..691ee2ee6 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alle', 'role_own' => 'Eigene', 'role_controlled_by_asset' => 'Abhängig von dem Asset, in das sie hochgeladen werden', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Rolle speichern', 'role_users' => 'Dieser Rolle zugeordnete Benutzer', 'role_users_none' => 'Derzeit sind diesem Rollentyp keine Benutzer zugewiesen', diff --git a/lang/de/validation.php b/lang/de/validation.php index 21e850bbf..40f6e76a1 100644 --- a/lang/de/validation.php +++ b/lang/de/validation.php @@ -106,7 +106,7 @@ return [ 'uploaded' => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.', 'zip_file' => ':attribute muss eine Datei innerhalb des ZIP referenzieren.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', + 'zip_file_size' => 'Die Datei :attribute darf :size MB nicht überschreiten.', 'zip_file_mime' => ':attribute muss eine Datei des Typs :validType referenzieren, gefunden :foundType.', 'zip_model_expected' => 'Datenobjekt erwartet, aber ":type" gefunden.', 'zip_unique' => ':attribute muss für den Objekttyp innerhalb des ZIP eindeutig sein.', diff --git a/lang/de_informal/entities.php b/lang/de_informal/entities.php index 16502051c..708397e7e 100644 --- a/lang/de_informal/entities.php +++ b/lang/de_informal/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Kapitel und Seiten innerhalb eines Buches verschieben, um dessen Inhalt zu reorganisieren. Andere Bücher können hinzugefügt werden, was das Verschieben von Kapiteln und Seiten zwischen Büchern erleichtert. Optional kann eine automatische Sortierregel erstellt werden, um den Inhalt dieses Buches nach Änderungen automatisch zu sortieren.', 'books_sort_auto_sort' => 'Auto-Sortieroption', 'books_sort_auto_sort_active' => 'Automatische Sortierung aktiv: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Buch ":bookName" sortieren', 'books_sort_name' => 'Sortieren nach Namen', 'books_sort_created' => 'Sortieren nach Erstellungsdatum', @@ -410,10 +411,10 @@ return [ 'comment_deleted_success' => 'Kommentar gelöscht', 'comment_created_success' => 'Kommentar hinzugefügt', 'comment_updated_success' => 'Kommentar aktualisiert', - 'comment_archive_success' => 'Kommentar archiviert', + 'comment_archive_success' => 'Kommentar wurde archiviert', 'comment_unarchive_success' => 'Kommentar nicht mehr archiviert', 'comment_view' => 'Kommentar ansehen', - 'comment_jump_to_thread' => 'Zum Thema springen', + 'comment_jump_to_thread' => 'Zu diesem Thema springen', 'comment_delete_confirm' => 'Möchtst du diesen Kommentar wirklich löschen?', 'comment_in_reply_to' => 'Antwort auf :commentId', 'comment_reference' => 'Referenz', diff --git a/lang/de_informal/notifications.php b/lang/de_informal/notifications.php index 4df170783..dcbe16c93 100644 --- a/lang/de_informal/notifications.php +++ b/lang/de_informal/notifications.php @@ -11,8 +11,8 @@ return [ 'updated_page_subject' => 'Aktualisierte Seite: :pageName', 'updated_page_intro' => 'Eine Seite wurde in :appName aktualisiert:', 'updated_page_debounce' => 'Um eine Flut von Benachrichtigungen zu vermeiden, wirst du für eine gewisse Zeit keine Benachrichtigungen für weitere Bearbeitungen dieser Seite durch denselben Bearbeiter erhalten.', - 'comment_mention_subject' => 'Sie wurden in einem Kommentar auf der Seite :pageName erwähnt', - 'comment_mention_intro' => 'Sie wurden in einem Kommentar zu :appName: erwähnt', + 'comment_mention_subject' => 'Du wurdest in einem Kommentar auf der Seite :pageName erwähnt', + 'comment_mention_intro' => 'Du wurdest in einem Kommentar zu :appName erwähnt:', 'detail_page_name' => 'Seitenname:', 'detail_page_path' => 'Seitenpfad:', diff --git a/lang/de_informal/settings.php b/lang/de_informal/settings.php index 97ad607d6..93ed1902f 100644 --- a/lang/de_informal/settings.php +++ b/lang/de_informal/settings.php @@ -76,7 +76,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', // Sorting Settings - 'sorting' => 'Listen & Sortieren', + 'sorting' => 'Listen und Sortierung', 'sorting_book_default' => 'Standardregel für die Sortierung von Büchern', 'sorting_book_default_desc' => 'Wähle die Standard-Sortierregel aus, die auf neue Bücher angewendet werden soll. Dies wirkt sich nicht auf bestehende Bücher aus und kann pro Buch überschrieben werden.', 'sorting_rules' => 'Sortierregeln', @@ -208,6 +208,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'role_all' => 'Alle', 'role_own' => 'Eigene', 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Rolle speichern', 'role_users' => 'Dieser Rolle zugeordnete Benutzer', 'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet', diff --git a/lang/de_informal/validation.php b/lang/de_informal/validation.php index d693adecd..5dcbd5102 100644 --- a/lang/de_informal/validation.php +++ b/lang/de_informal/validation.php @@ -106,7 +106,7 @@ return [ 'uploaded' => 'Die Datei konnte nicht hochgeladen werden. Der Server akzeptiert möglicherweise keine Dateien dieser Größe.', 'zip_file' => ':attribute muss auf eine Datei innerhalb des ZIP verweisen.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', + 'zip_file_size' => 'Die Datei :attribute darf :size MB nicht überschreiten.', 'zip_file_mime' => ':attribute muss eine Datei des Typs :validType referenzieren, gefunden :foundType.', 'zip_model_expected' => 'Datenobjekt erwartet, aber ":type" gefunden.', 'zip_unique' => ':attribute muss für den Objekttyp innerhalb des ZIP eindeutig sein.', diff --git a/lang/el/entities.php b/lang/el/entities.php index 3779cd18a..b551d1a86 100644 --- a/lang/el/entities.php +++ b/lang/el/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Ταξινόμηση Βιβλίου :bookname', 'books_sort_name' => 'Ταξινόμηση κατά όνομα', 'books_sort_created' => 'Ταξινόμηση κατά ημερομηνία δημιουργίας', diff --git a/lang/el/settings.php b/lang/el/settings.php index 6ec5c4fdd..605b8b40e 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Ολα', 'role_own' => 'Τα δικά του', 'role_controlled_by_asset' => 'Ελέγχονται από το στοιχείο στο οποίο ανεβαίνουν (Ράφια, Βιβλία)', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Αποθήκευση Ρόλου', 'role_users' => 'Χρήστες σε αυτόν τον Ρόλο', 'role_users_none' => 'Σε κανένα χρήστη δεν έχει ανατεθεί αυτήν τη στιγμή αυτός ο ρόλος.', diff --git a/lang/es/entities.php b/lang/es/entities.php index e652d4dc0..a8ec3b10a 100644 --- a/lang/es/entities.php +++ b/lang/es/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Mueve capítulos y páginas dentro de un libro para reorganizar su contenido. Se pueden añadir otros libros que permiten mover fácilmente capítulos y páginas entre libros. Opcionalmente, se puede establecer una regla de ordenación automática para ordenar automáticamente el contenido de este libro cuando haya cambios.', 'books_sort_auto_sort' => 'Opción de ordenación automática', 'books_sort_auto_sort_active' => 'Opción de ordenación activa: sortName', + 'books_sort_auto_sort_creation_hint' => 'Las reglas para los ajustes de ordenación automática pueden ser creadas en el área de configuración "Listas y ordenación" por un usuario con los permisos pertinentes.', 'books_sort_named' => 'Organizar libro :bookName', 'books_sort_name' => 'Organizar por Nombre', 'books_sort_created' => 'Organizar por Fecha de creación', diff --git a/lang/es/settings.php b/lang/es/settings.php index bfd3ce1cf..516480b64 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Todo', 'role_own' => 'Propio', 'role_controlled_by_asset' => 'Controlado por el contenido al que ha sido subido', + 'role_controlled_by_page_delete' => 'Controlado por página de eliminación de permisos', 'role_save' => 'Guardar rol', 'role_users' => 'Usuarios en este rol', 'role_users_none' => 'No hay usuarios asignados a este rol', diff --git a/lang/es_AR/entities.php b/lang/es_AR/entities.php index 1ae092de1..2dc1bd648 100644 --- a/lang/es_AR/entities.php +++ b/lang/es_AR/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Mueve capítulos y páginas dentro de un libro para reorganizar su contenido. Se pueden añadir otros libros que permiten mover fácilmente capítulos y páginas entre libros. Opcionalmente, se puede establecer una regla de ordenación automática para el contenido de este libro cuando haya cambios.', 'books_sort_auto_sort' => 'Opción de ordenación automática', 'books_sort_auto_sort_active' => 'Opción de ordenación activa: sortName', + 'books_sort_auto_sort_creation_hint' => 'Las reglas para los ajustes de ordenación automática pueden ser creadas en el área de configuración "Listas y ordenación" por un usuario con los permisos pertinentes.', 'books_sort_named' => 'Organizar libro :bookName', 'books_sort_name' => 'Organizar por nombre', 'books_sort_created' => 'Organizar por fecha de creación', diff --git a/lang/es_AR/settings.php b/lang/es_AR/settings.php index 90f43a6f2..3b82d0fb3 100644 --- a/lang/es_AR/settings.php +++ b/lang/es_AR/settings.php @@ -208,6 +208,7 @@ return [ 'role_all' => 'Todo', 'role_own' => 'Propio', 'role_controlled_by_asset' => 'Controlado por el activo al que ha sido subido', + 'role_controlled_by_page_delete' => 'Controlado por página de eliminación de permisos', 'role_save' => 'Guardar rol', 'role_users' => 'Usuarios en este rol', 'role_users_none' => 'No hay usuarios asignados a este rol', diff --git a/lang/et/entities.php b/lang/et/entities.php index 323985b2d..4b7be605a 100644 --- a/lang/et/entities.php +++ b/lang/et/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Liiguta raamatu sees peatükke ja lehti, et selle sisu ümber organiseerida. Saad lisada teisi raamatuid, mis võimaldab peatükke ja lehti lihtsasti raamatute vahel liigutada. Lisaks saad määrata automaatse sorteerimise reegli, et selle raamatu sisu muudatuste puhul automaatselt järjestada.', 'books_sort_auto_sort' => 'Automaatne sorteerimine', 'books_sort_auto_sort_active' => 'Automaatne sorteerimine aktiivne: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sorteeri raamat :bookName', 'books_sort_name' => 'Sorteeri nime järgi', 'books_sort_created' => 'Sorteeri loomisaja järgi', diff --git a/lang/et/settings.php b/lang/et/settings.php index bc5a7794e..fd00da9e3 100644 --- a/lang/et/settings.php +++ b/lang/et/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Kõik', 'role_own' => 'Enda omad', 'role_controlled_by_asset' => 'Õigused määratud seotud objekti kaudu', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Salvesta roll', 'role_users' => 'Selle rolliga kasutajad', 'role_users_none' => 'Seda rolli ei ole hetkel ühelgi kasutajal', diff --git a/lang/eu/entities.php b/lang/eu/entities.php index cc51012d5..4d9e7cb06 100644 --- a/lang/eu/entities.php +++ b/lang/eu/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Ordenatu :bookName liburua', 'books_sort_name' => 'Ordenatu izenaren arabera', 'books_sort_created' => 'Ordenatu argitaratze-dataren arabera', diff --git a/lang/eu/settings.php b/lang/eu/settings.php index 0f764dccb..dd346d1c9 100644 --- a/lang/eu/settings.php +++ b/lang/eu/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Guztiak', 'role_own' => 'Norberarenak', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Gorde rol-a', 'role_users' => 'Rol honetako erabiltzaileak', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/fa/entities.php b/lang/fa/entities.php index d7c0cc1c0..2abcff576 100644 --- a/lang/fa/entities.php +++ b/lang/fa/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'برای سامان‌دهی محتوای یک کتاب، می‌توانید فصل‌ها و صفحات آن را جابه‌جا کنید. همچنین می‌توانید کتاب‌های دیگری بیفزایید تا جابه‌جایی فصل‌ها و صفحات میان کتاب‌ها آسان شود. در صورت تمایل، می‌توانید قاعده‌ای برای مرتب‌سازی خودکار تعیین کنید تا محتوای کتاب در صورت ایجاد تغییرات، به طور خودکار مرتب شود.', 'books_sort_auto_sort' => 'گزینه مرتب‌سازی خودکار', 'books_sort_auto_sort_active' => 'مرتب‌سازی خودکار با قاعده: :sortName فعال است', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'مرتب‌سازی کتاب:bookName', 'books_sort_name' => 'مرتب‌سازی بر اساس نام', 'books_sort_created' => 'مرتب‌سازی بر اساس تاریخ ایجاد', diff --git a/lang/fa/settings.php b/lang/fa/settings.php index 2fa115118..21ad96240 100644 --- a/lang/fa/settings.php +++ b/lang/fa/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'همه', 'role_own' => 'صاحب', 'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'ذخیره نقش', 'role_users' => 'کاربران در این نقش', 'role_users_none' => 'در حال حاضر هیچ کاربری به این نقش اختصاص داده نشده است', diff --git a/lang/fi/entities.php b/lang/fi/entities.php index c64621f0d..dd2ad5e69 100644 --- a/lang/fi/entities.php +++ b/lang/fi/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Järjestä kirja :bookName', 'books_sort_name' => 'Järjestä nimen mukaan', 'books_sort_created' => 'Järjestä luontipäiväyksen mukaan', diff --git a/lang/fi/settings.php b/lang/fi/settings.php index adc47fe2d..3aec01886 100644 --- a/lang/fi/settings.php +++ b/lang/fi/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Kaikki', 'role_own' => 'Omat', 'role_controlled_by_asset' => 'Määräytyy sen sisällön mukaan, johon ne on ladattu', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Tallenna rooli', 'role_users' => 'Käyttäjät tässä roolissa', 'role_users_none' => 'Yhtään käyttäjää ei ole osoitettuna tähän rooliin', diff --git a/lang/fr/auth.php b/lang/fr/auth.php index fab11a83b..61ca0ee3e 100644 --- a/lang/fr/auth.php +++ b/lang/fr/auth.php @@ -12,9 +12,9 @@ return [ // Login & Register 'sign_up' => 'S\'inscrire', 'log_in' => 'Se connecter', - 'log_in_with' => 'Se connecter avec :socialDriver', + 'log_in_with' => 'Connexion avec :socialDriver', 'sign_up_with' => 'S\'inscrire avec :socialDriver', - 'logout' => 'Se déconnecter', + 'logout' => 'Déconnexion', 'name' => 'Nom', 'username' => 'Nom d\'utilisateur', diff --git a/lang/fr/entities.php b/lang/fr/entities.php index d650fc830..fae7480b1 100644 --- a/lang/fr/entities.php +++ b/lang/fr/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Déplacer les pages et chapitres au sein d’un livre pour en réorganiser le contenu. D’autres livres peuvent être ajoutés pour faciliter le déplacement des pages et chapitres entre les livres. Facultativement, une règle de tri automatique peut être mise en place afin de trier le livre lorsqu’il est édité.', 'books_sort_auto_sort' => 'Option de tri automatique', 'books_sort_auto_sort_active' => 'Tri automatique actif : :sortName', + 'books_sort_auto_sort_creation_hint' => 'Les règles de tri automatiques peuvent être créées dans la section « Listes et tri » des préférences par un utilisateur disposant des autorisations appropriées.', 'books_sort_named' => 'Trier le livre :bookName', 'books_sort_name' => 'Trier par le nom', 'books_sort_created' => 'Trier par la date de création', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 8c6c57f33..664184a7d 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Tous', 'role_own' => 'Propres', 'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Enregistrer le rôle', 'role_users' => 'Utilisateurs ayant ce rôle', 'role_users_none' => 'Aucun utilisateur avec ce rôle actuellement', diff --git a/lang/he/entities.php b/lang/he/entities.php index d8b264d8b..38ffc95cf 100644 --- a/lang/he/entities.php +++ b/lang/he/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'מיין את הספר :bookName', 'books_sort_name' => 'מיין לפי שם', 'books_sort_created' => 'מיין לפי תאריך יצירה', diff --git a/lang/he/settings.php b/lang/he/settings.php index 46150081a..b8f204813 100644 --- a/lang/he/settings.php +++ b/lang/he/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'הכל', 'role_own' => 'שלי', 'role_controlled_by_asset' => 'נשלטים על ידי המשאב אליו הועלו', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'שמור תפקיד', 'role_users' => 'משתמשים משוייכים לתפקיד זה', 'role_users_none' => 'אין משתמשים המשוייכים לתפקיד זה', diff --git a/lang/hr/entities.php b/lang/hr/entities.php index 99df50ea3..ff51c1b2f 100644 --- a/lang/hr/entities.php +++ b/lang/hr/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Razvrstaj knjigu :bookName', 'books_sort_name' => 'Razvrstaj po imenu', 'books_sort_created' => 'Razvrstaj po datumu nastanka', diff --git a/lang/hr/settings.php b/lang/hr/settings.php index 0692c8d7a..60b2485ef 100644 --- a/lang/hr/settings.php +++ b/lang/hr/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Sve', 'role_own' => 'Vlastito', 'role_controlled_by_asset' => 'Kontrolirano od strane vlasnika', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Spremi ulogu', 'role_users' => 'Korisnici u ovoj ulozi', 'role_users_none' => 'Trenutno nijedan korisnik nije u ovoj ulozi', diff --git a/lang/hu/entities.php b/lang/hu/entities.php index 570120dfa..707de62d8 100644 --- a/lang/hu/entities.php +++ b/lang/hu/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => ':bookName könyv rendezése', 'books_sort_name' => 'Rendezés név szerint', 'books_sort_created' => 'Rendezés létrehozás dátuma szerint', diff --git a/lang/hu/settings.php b/lang/hu/settings.php index 53b1cdcc4..aaccbd35f 100644 --- a/lang/hu/settings.php +++ b/lang/hu/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Összes', 'role_own' => 'Saját', 'role_controlled_by_asset' => 'Az általuk feltöltött eszköz által ellenőrzött', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Szerepkör mentése', 'role_users' => 'Felhasználók ebben a szerepkörben', 'role_users_none' => 'Jelenleg nincsenek felhasználók hozzárendelve ehhez a szerepkörhöz', diff --git a/lang/id/entities.php b/lang/id/entities.php index 2f9329925..d978a3d19 100644 --- a/lang/id/entities.php +++ b/lang/id/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sortir Buku :bookName', 'books_sort_name' => 'Diurutkan berdasarkan nama', 'books_sort_created' => 'Urutkan berdasarkan Tanggal Dibuat', diff --git a/lang/id/settings.php b/lang/id/settings.php index 8bdd99e68..fe3289ffa 100644 --- a/lang/id/settings.php +++ b/lang/id/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Semua', 'role_own' => 'Sendiri', 'role_controlled_by_asset' => 'Dikendalikan oleh aset tempat mereka diunggah', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Simpan Peran', 'role_users' => 'Peran berhasil diperbarui', 'role_users_none' => 'Saat ini tidak ada pengguna yang ditugaskan untuk peran ini', diff --git a/lang/is/entities.php b/lang/is/entities.php index 4308835c7..0d9a6fc1e 100644 --- a/lang/is/entities.php +++ b/lang/is/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Raða bók :bookName', 'books_sort_name' => 'Raða eftir nafni', 'books_sort_created' => 'Raða eftir skráningar dagsetningu', diff --git a/lang/is/settings.php b/lang/is/settings.php index b1f21ac10..877e0dfa8 100644 --- a/lang/is/settings.php +++ b/lang/is/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Allt', 'role_own' => 'Eigin', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Vista hlutverk', 'role_users' => 'Notendur í þessu hlutverki', 'role_users_none' => 'Engir notendur eru eins og er í þessu hlutverki', diff --git a/lang/it/entities.php b/lang/it/entities.php index eb9868973..1cce5fcd5 100644 --- a/lang/it/entities.php +++ b/lang/it/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Spostare i capitoli e le pagine di un libro per riorganizzarne il contenuto. Possono essere aggiunti altri libri che permettono di spostare facilmente capitoli e pagine tra i libri. Opzionalmente una regola di ordinamento automatico può essere impostata per ordinare automaticamente i contenuti di questo libro in caso di modifiche.', 'books_sort_auto_sort' => 'Opzione Ordinamento Automatico', 'books_sort_auto_sort_active' => 'Ordinamento Automatico Attivo: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Ordina il libro :bookName', 'books_sort_name' => 'Ordina per Nome', 'books_sort_created' => 'Ordina per Data di creazione', diff --git a/lang/it/settings.php b/lang/it/settings.php index 2b5819b2a..9a272db33 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Tutti', 'role_own' => 'Propri', 'role_controlled_by_asset' => 'Controllato dall\'entità in cui sono caricati', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Salva ruolo', 'role_users' => 'Utenti in questo ruolo', 'role_users_none' => 'Nessun utente assegnato a questo ruolo', diff --git a/lang/ja/entities.php b/lang/ja/entities.php index 551a2e496..57703997f 100644 --- a/lang/ja/entities.php +++ b/lang/ja/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'ブック内のチャプタおよびページを移動して内容を再編成できます。他のブックを並べて、ブック間でチャプタやページを簡単に移動することもできます。オプションで自動ソートルールを設定すると、変更時にブックの内容を自動的にソートすることができます。', 'books_sort_auto_sort' => '自動ソートオプション', 'books_sort_auto_sort_active' => '自動ソート有効: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'ブック「:bookName」を並べ替え', 'books_sort_name' => '名前で並べ替え', 'books_sort_created' => '作成日で並べ替え', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 378e3a774..a3cbf696a 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => '全て', 'role_own' => '自身', 'role_controlled_by_asset' => 'このアセットに対し、右記の操作を許可:', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => '役割を保存', 'role_users' => 'この役割を持つユーザー', 'role_users_none' => 'この役割が付与されたユーザーはいません', diff --git a/lang/ka/entities.php b/lang/ka/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/ka/entities.php +++ b/lang/ka/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/ka/settings.php b/lang/ka/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/ka/settings.php +++ b/lang/ka/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/ko/entities.php b/lang/ko/entities.php index 44809d99f..c54d6c7dc 100644 --- a/lang/ko/entities.php +++ b/lang/ko/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => '책 내의 챕터와 페이지를 이동하여 콘텐츠를 재구성할 수 있습니다. 다른 책들을 추가하여 책 간의 챕터와 페이지를 쉽게 이동할 수 있습니다. 선택적으로 자동 정렬 규칙을 설정하여 변경 시 이 책의 콘텐츠를 자동으로 정렬할 수 있습니다.', 'books_sort_auto_sort' => '자동 정렬 옵션', 'books_sort_auto_sort_active' => '현재 설정된 자동 정렬: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => ':bookName 정렬', 'books_sort_name' => '제목', 'books_sort_created' => '만든 날짜', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 97af673c6..9aabe5c2a 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => '모든 항목', 'role_own' => '직접 만든 항목', 'role_controlled_by_asset' => '저마다 다름', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => '저장', 'role_users' => '이 역할을 가진 사용자들', 'role_users_none' => '역할이 부여된 사용자가 없습니다.', diff --git a/lang/ku/entities.php b/lang/ku/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/ku/entities.php +++ b/lang/ku/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/ku/settings.php b/lang/ku/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/ku/settings.php +++ b/lang/ku/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/lt/entities.php b/lang/lt/entities.php index 6c4472d85..f6610a22a 100644 --- a/lang/lt/entities.php +++ b/lang/lt/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Rūšiuoti knygą :bookName', 'books_sort_name' => 'Rūšiuoti pagal vardą', 'books_sort_created' => 'Rūšiuoti pagal sukūrimo datą', diff --git a/lang/lt/settings.php b/lang/lt/settings.php index f797e567e..23dd38d50 100644 --- a/lang/lt/settings.php +++ b/lang/lt/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Visi', 'role_own' => 'Nuosavi', 'role_controlled_by_asset' => 'Kontroliuojami nuosavybės, į kurią yra įkelti', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Išsaugoti vaidmenį', 'role_users' => 'Naudotojai šiame vaidmenyje', 'role_users_none' => 'Šiuo metu prie šio vaidmens nėra priskirta naudotojų', diff --git a/lang/lv/entities.php b/lang/lv/entities.php index 5f921048e..f0f32ab21 100644 --- a/lang/lv/entities.php +++ b/lang/lv/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Kārtot grāmatu :bookName', 'books_sort_name' => 'Kārtot pēc nosaukuma', 'books_sort_created' => 'Kārtot pēc izveidošanas datuma', diff --git a/lang/lv/settings.php b/lang/lv/settings.php index 9dc6bf402..0e11ebd65 100644 --- a/lang/lv/settings.php +++ b/lang/lv/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Visi', 'role_own' => 'Savi', 'role_controlled_by_asset' => 'Kontrolē resurss, uz ko tie ir augšupielādēti', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Saglabāt grupu', 'role_users' => 'Lietotāji šajā grupā', 'role_users_none' => 'Pagaidām neviens lietotājs nav pievienots šai grupai', diff --git a/lang/nb/entities.php b/lang/nb/entities.php index cf0fdffde..a67c35e17 100644 --- a/lang/nb/entities.php +++ b/lang/nb/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Flytt kapitler og sider innen en bok for å reorganisere innholdet. Andre bøker kan legges til, noe som gjør det enkelt å flytte kapitler og sider mellom bøkene. Valgfritt kan en automatisk sorteringsregel settes for å automatisk sortere innholdet i denne boken ved endringer.', 'books_sort_auto_sort' => 'Automatisk sorteringsalternativ', 'books_sort_auto_sort_active' => 'Automatisk sortering aktiv: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Omorganisér :bookName (bok)', 'books_sort_name' => 'Sorter på navn', 'books_sort_created' => 'Sorter på opprettet dato', diff --git a/lang/nb/settings.php b/lang/nb/settings.php index 5fcaaaca6..5e779beea 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alle', 'role_own' => 'Egne', 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Lagre rolle', 'role_users' => 'Kontoholdere med denne rollen', 'role_users_none' => 'Ingen kontoholdere er gitt denne rollen', diff --git a/lang/ne/entities.php b/lang/ne/entities.php index 88b202dee..4d9f78ea5 100644 --- a/lang/ne/entities.php +++ b/lang/ne/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'पुस्तकमा अध्यायहरू र पृष्ठहरूलाई पुनः व्यवस्थित गर्नका लागि सार्नुहोस्। अन्य पुस्तकहरू थप्न सकिन्छ जसले अध्याय र पृष्ठहरूलाई पुस्तकहरू बीच सजिलै सर्न मद्दत गर्दछ। वैकल्पिक रूपमा एक स्वचालित वर्गीकरण नियम सेट गर्न सकिन्छ जसले पुस्तकको सामग्रीहरू परिवर्तन भएपछि स्वत: वर्गीकृत गर्छ।', 'books_sort_auto_sort' => 'स्वचालित वर्गीकरण विकल्प', 'books_sort_auto_sort_active' => 'स्वचालित वर्गीकरण सक्रिय: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'पुस्तक :bookName को वर्गीकरण गर्नुहोस्', 'books_sort_name' => 'नाम अनुसार वर्गीकृत गर्नुहोस्', 'books_sort_created' => 'सिर्जना मितिअनुसार वर्गीकृत गर्नुहोस्', diff --git a/lang/ne/settings.php b/lang/ne/settings.php index dbc7d8e9f..f52bcd420 100644 --- a/lang/ne/settings.php +++ b/lang/ne/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'सबै', 'role_own' => 'आफ्नो', 'role_controlled_by_asset' => 'अपलोड गरिएको सम्पत्तिले नियन्त्रण गरेको', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'भूमिका सुरक्षित गर्नुहोस्', 'role_users' => 'यस भूमिकाका प्रयोगकर्ताहरू', 'role_users_none' => 'यो भूमिकामा हाल कुनै प्रयोगकर्ता छैन', diff --git a/lang/nl/entities.php b/lang/nl/entities.php index b9be1f42b..c39beef2d 100644 --- a/lang/nl/entities.php +++ b/lang/nl/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Verplaats hoofdstukken en pagina\'s door het boek om ze te organiseren. Andere boeken kunnen worden toegevoegd zodat hoofdstukken en pagina\'s gemakkelijk tussen boeken kunnen worden verplaatst. Het is mogelijk om een automatische sorteerregel in te stellen die de inhoud zal sorteren bij wijzigingen.', 'books_sort_auto_sort' => 'Automatisch Sorteren', 'books_sort_auto_sort_active' => 'Automatisch Sorteren Actief: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sorteer boek :bookName', 'books_sort_name' => 'Sorteren op naam', 'books_sort_created' => 'Sorteren op datum van aanmaken', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index c8d071119..b82c52deb 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alles', 'role_own' => 'Eigen', 'role_controlled_by_asset' => 'Gecontroleerd door de asset waar deze is geüpload', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Rol Opslaan', 'role_users' => 'Gebruikers in deze rol', 'role_users_none' => 'Geen enkele gebruiker heeft deze rol', diff --git a/lang/nn/entities.php b/lang/nn/entities.php index 4c75d5019..4f79f3d75 100644 --- a/lang/nn/entities.php +++ b/lang/nn/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Omorganiser :bookName', 'books_sort_name' => 'Sorter på namn', 'books_sort_created' => 'Sorter på oppretta dato', diff --git a/lang/nn/settings.php b/lang/nn/settings.php index e4b6e6af9..b6c322c9c 100644 --- a/lang/nn/settings.php +++ b/lang/nn/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alle', 'role_own' => 'Egne', 'role_controlled_by_asset' => 'Kontrollert av eiendelen de er lastet opp til', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Lagre rolle', 'role_users' => 'Kontoholdere med denne rollen', 'role_users_none' => 'Ingen kontoholdere er gitt denne rollen', diff --git a/lang/pl/entities.php b/lang/pl/entities.php index f3ad807af..88e4344de 100644 --- a/lang/pl/entities.php +++ b/lang/pl/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Opcja automatycznego sortowania', 'books_sort_auto_sort_active' => 'Automatyczne sortowanie aktywne: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sortuj książkę :bookName', 'books_sort_name' => 'Sortuj według nazwy', 'books_sort_created' => 'Sortuj według daty utworzenia', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index 775d4f25d..98201406e 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Wszyscy', 'role_own' => 'Własne', 'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Zapisz rolę', 'role_users' => 'Użytkownicy w tej roli', 'role_users_none' => 'Brak użytkowników zapisanych do tej roli', diff --git a/lang/pt/entities.php b/lang/pt/entities.php index e882c79d0..709b8466a 100644 --- a/lang/pt/entities.php +++ b/lang/pt/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Mova capítulos e páginas de um livro para reorganizar o seu conteúdo. É possível acrescentar outros livros, o que permite uma movimentação fácil de capítulos e páginas entre livros. Opcionalmente, uma regra de organização automática pode ser definida para classificar automaticamente o conteúdo deste livro após alterações.', 'books_sort_auto_sort' => '', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Ordenar Livro :bookName', 'books_sort_name' => 'Ordenar por Nome', 'books_sort_created' => 'Ordenar por Data de Criação', diff --git a/lang/pt/settings.php b/lang/pt/settings.php index a59335b7c..47fecbcbf 100644 --- a/lang/pt/settings.php +++ b/lang/pt/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Todos', 'role_own' => 'Próprio', 'role_controlled_by_asset' => 'Controlado pelo ativo para o qual eles são enviados', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Guardar Cargo', 'role_users' => 'Utilizadores com este cargo', 'role_users_none' => 'Nenhum utilizador está atualmente vinculado a este cargo', diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index b5fd974aa..0580542b4 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -89,7 +89,7 @@ return [ 'mfa_setup_action' => 'Configurações', 'mfa_backup_codes_usage_limit_warning' => 'Você tem menos de 5 códigos de backup restantes, Por favor, gere e armazene um novo conjunto antes de esgotar suas opções de códigos de backup para evitar estar bloqueado para fora da sua conta.', 'mfa_option_totp_title' => 'Aplicativo Móvel', - 'mfa_option_totp_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Google Authenticator, Authy ou o Microsoft Authenticator.', + 'mfa_option_totp_desc' => 'Para usar a autenticação multi-fator, você precisará de um aplicativo móvel que suporte TOTP como o Google Authenticator, Authy, Microsoft Authenticator ou Proton Authenticator.', 'mfa_option_backup_codes_title' => 'Códigos de backup', 'mfa_option_backup_codes_desc' => 'Gera um conjunto de códigos de backup de uso único que você inserirá no login para verificar sua identidade. Certifique-se de armazená-los em um local seguro e protegido.', 'mfa_gen_confirm_and_enable' => 'Confirmar e habilitar', diff --git a/lang/pt_BR/entities.php b/lang/pt_BR/entities.php index 5a948c8dc..0211f45fa 100644 --- a/lang/pt_BR/entities.php +++ b/lang/pt_BR/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Mova capítulos e páginas de um livro para reorganizar seu conteúdo. É possível acrescentar outros livros, o que permite uma movimentação fácil de capítulos e páginas entre livros. Opcionalmente, uma regra de ordenação automática pode ser definida para ordenar automaticamente o conteúdo deste livro após alterações.', 'books_sort_auto_sort' => 'Opção de ordenação automática', 'books_sort_auto_sort_active' => 'Ordenação automática ativa: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Regras de ordenação automática podem ser criadas na área de configurações "Lista e Classificação" por um usuário com as permissões relevantes.', 'books_sort_named' => 'Ordenar Livro :bookName', 'books_sort_name' => 'Ordernar por Nome', 'books_sort_created' => 'Ordenar por Data de Criação', diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php index 97b434727..9b7c6a7d4 100644 --- a/lang/pt_BR/settings.php +++ b/lang/pt_BR/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Todos', 'role_own' => 'Próprio', 'role_controlled_by_asset' => 'Controlado pelos ativos nos quais o upload foi realizado', + 'role_controlled_by_page_delete' => 'Controlado pelas permissões de exclusão de página', 'role_save' => 'Salvar Perfil', 'role_users' => 'Usuários com este perfil', 'role_users_none' => 'Nenhum usuário está atualmente vinculado a este perfil', diff --git a/lang/ro/entities.php b/lang/ro/entities.php index ac8c9d326..aa18d09fb 100644 --- a/lang/ro/entities.php +++ b/lang/ro/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sortează cartea :bookName', 'books_sort_name' => 'Sortează după nume', 'books_sort_created' => 'Sortează după data creării', diff --git a/lang/ro/settings.php b/lang/ro/settings.php index d65a8e071..117c4ca61 100644 --- a/lang/ro/settings.php +++ b/lang/ro/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Tot', 'role_own' => 'Propriu', 'role_controlled_by_asset' => 'Controlat de activele pe care sunt încărcate', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Salvare rol', 'role_users' => 'Utilizatori cu acest rol', 'role_users_none' => 'Nici un utilizator nu este asociat acestui rol', diff --git a/lang/ru/activities.php b/lang/ru/activities.php index d353b7a76..d9ac1685b 100644 --- a/lang/ru/activities.php +++ b/lang/ru/activities.php @@ -85,12 +85,12 @@ return [ 'webhook_delete_notification' => 'Вебхук успешно удален', // Imports - 'import_create' => 'created import', - 'import_create_notification' => 'Import successfully uploaded', + 'import_create' => 'создал импорт', + 'import_create_notification' => 'Импорт успешно добавлен', 'import_run' => 'обновлен импорт', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_run_notification' => 'Контент успешно импортирован', + 'import_delete' => 'удалил импорт', + 'import_delete_notification' => 'Импорт успешно удален', // Users 'user_create' => 'создал пользователя', diff --git a/lang/ru/common.php b/lang/ru/common.php index fa9969c32..bf77a999d 100644 --- a/lang/ru/common.php +++ b/lang/ru/common.php @@ -30,8 +30,8 @@ return [ 'create' => 'Создание', 'update' => 'Обновление', 'edit' => 'Редактировать', - 'archive' => 'Archive', - 'unarchive' => 'Un-Archive', + 'archive' => 'Архивировать', + 'unarchive' => 'Вернуть из архива', 'sort' => 'Сортировать', 'move' => 'Переместить', 'copy' => 'Скопировать', diff --git a/lang/ru/editor.php b/lang/ru/editor.php index d26f7edfe..da661559b 100644 --- a/lang/ru/editor.php +++ b/lang/ru/editor.php @@ -149,7 +149,7 @@ return [ 'url' => 'URL-адрес', 'text_to_display' => 'Текст для отображения', 'title' => 'Заголовок', - 'browse_links' => 'Browse links', + 'browse_links' => 'Просмотр ссылки', 'open_link' => 'Открыть ссылку', 'open_link_in' => 'Открыть ссылку в...', 'open_link_current' => 'В текущем окне', @@ -166,8 +166,8 @@ return [ 'about' => 'О редакторе', 'about_title' => 'О редакторе WYSIWYG', 'editor_license' => 'Лицензия редактора и авторские права', - 'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.', - 'editor_lexical_license_link' => 'Full license details can be found here.', + 'editor_lexical_license' => 'Этот редактор создан с помощью :lexicalLink, распространяемый под лицензией MIT.', + 'editor_lexical_license_link' => 'Здесь вы можете найти полную информацию о лицензии.', 'editor_tiny_license' => 'Этот редактор собран с помощью :tinyLink, который предоставляется под MIT лицензией.', 'editor_tiny_license_link' => 'Авторские права и подробности лицензии TinyMCE вы можете найти здесь.', 'save_continue' => 'Сохранить страницу и продолжить', diff --git a/lang/ru/entities.php b/lang/ru/entities.php index bf224c10a..28d096fe8 100644 --- a/lang/ru/entities.php +++ b/lang/ru/entities.php @@ -51,18 +51,18 @@ return [ 'import_pending' => 'Ожидается импорт', 'import_pending_none' => 'Импорт не был запущен.', 'import_continue' => 'Продолжить импорт', - 'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.', - 'import_details' => 'Import Details', + 'import_continue_desc' => 'Проверьте содержимое, которое должно быть импортировано из загруженного ZIP-файла. Если все готово, выполните импорт, чтобы добавить его содержимое в эту систему. Загруженный ZIP файл будет автоматически удален при успешном импорте.', + 'import_details' => 'Детали загрузки', 'import_run' => 'Запустить импорт', 'import_size' => ':size Import ZIP Size', - 'import_uploaded_at' => 'Uploaded :relativeTime', - 'import_uploaded_by' => 'Uploaded by', + 'import_uploaded_at' => 'Загружено :relativeTime', + 'import_uploaded_by' => 'Загружено пользователем', 'import_location' => 'Import Location', - 'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.', + 'import_location_desc' => 'Выберите целевое местоположение для импортированного содержимого. Для создания в выбранном месте необходимы соответствующие разрешения.', 'import_delete_confirm' => 'Are you sure you want to delete this import?', - 'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.', + 'import_delete_desc' => 'Это приведет к удалению загруженного ZIP файла и не может быть отменено.', 'import_errors' => 'Ошибки импорта', - 'import_errors_desc' => 'The follow errors occurred during the import attempt:', + 'import_errors_desc' => 'Во время попытки импорта произошла следующая ошибка:', 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', @@ -170,9 +170,10 @@ return [ 'books_search_this' => 'Поиск в этой книге', 'books_navigation' => 'Навигация по книге', 'books_sort' => 'Сортировка содержимого книги', - 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', - 'books_sort_auto_sort' => 'Auto Sort Option', - 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_desc' => 'Переместите разделы и страницы в книге, чтобы изменить содержание книги. Могут быть добавлены другие книги, что позволяет легко перемещать разделы и страницы между книгами. При желании правило автоматической сортировки может быть установлено для автоматической сортировки содержимого этой книги после изменений.', + 'books_sort_auto_sort' => 'Автосортировка', + 'books_sort_auto_sort_active' => 'Автосортировка активна: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Правила автоматической сортировки могут быть созданы в области настроек «Список и Сортировка» пользователем с соответствующими разрешениями.', 'books_sort_named' => 'Сортировка книги :bookName', 'books_sort_name' => 'По имени', 'books_sort_created' => 'По дате создания', @@ -251,7 +252,7 @@ return [ 'pages_edit_switch_to_markdown_clean' => 'Только Markdown (с возможными потерями форматирования)', 'pages_edit_switch_to_markdown_stable' => 'Полное сохранение форматирования (HTML)', 'pages_edit_switch_to_wysiwyg' => 'Переключиться в WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg' => 'Переключиться на новый WYSIWYG', 'pages_edit_switch_to_new_wysiwyg_desc' => '(В бета-тестировании)', 'pages_edit_set_changelog' => 'Задать список изменений', 'pages_edit_enter_changelog_desc' => 'Введите краткое описание внесенных изменений', diff --git a/lang/ru/notifications.php b/lang/ru/notifications.php index 96e853723..ea885d910 100644 --- a/lang/ru/notifications.php +++ b/lang/ru/notifications.php @@ -12,7 +12,7 @@ return [ 'updated_page_intro' => 'Страница была обновлена в :appName:', 'updated_page_debounce' => 'Чтобы предотвратить массовые уведомления, в течение некоторого времени вы не будете получать уведомления о дальнейших правках этой страницы этим же редактором.', 'comment_mention_subject' => 'Вы были упомянуты в комментарии на странице: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'comment_mention_intro' => 'Вы были упомянуты в комментариях к :appName:', 'detail_page_name' => 'Имя страницы:', 'detail_page_path' => 'Путь страницы:', diff --git a/lang/ru/preferences.php b/lang/ru/preferences.php index b61b252c8..2dcd20b11 100644 --- a/lang/ru/preferences.php +++ b/lang/ru/preferences.php @@ -23,7 +23,7 @@ return [ 'notifications_desc' => 'Управляйте полученными по электронной почте уведомлениями при выполнении определенных действий в системе.', 'notifications_opt_own_page_changes' => 'Уведомлять об изменениях в собственных страницах', 'notifications_opt_own_page_comments' => 'Уведомлять о комментариях на собственных страницах', - 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', + 'notifications_opt_comment_mentions' => 'Уведомлять, когда меня упоминали в комментарии', 'notifications_opt_comment_replies' => 'Уведомлять об ответах на мои комментарии', 'notifications_save' => 'Сохранить настройки', 'notifications_update_success' => 'Настройки уведомлений были обновлены!', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 76a2eebbf..7bf5832a3 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -82,28 +82,28 @@ return [ 'sorting_rules_desc' => 'Выберите правило сортировки по умолчанию для новых книг. Это не повлияет на существующие книги и может быть изменено для каждой книги отдельно.', 'sort_rule_assigned_to_x_books' => 'Используется в :count книгах', 'sort_rule_create' => 'Создать правило сортировки', - 'sort_rule_edit' => 'Edit Sort Rule', + 'sort_rule_edit' => 'Изменить правило сортировки', 'sort_rule_delete' => 'Удалить правило сортировки', 'sort_rule_delete_desc' => 'Удалить это правило сортировки из системы. Книги, использующие эту сортировку, вернутся к ручной сортировке.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', + 'sort_rule_delete_warn_books' => 'Это правило сортировки в настоящее время используется в :count book(s). Вы уверены, что хотите удалить его?', 'sort_rule_delete_warn_default' => 'Это правило сортировки используется по умолчанию для книг. Вы уверены, что хотите удалить его?', 'sort_rule_details' => 'Детали правила сортировки', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', + 'sort_rule_details_desc' => 'Задайте имя для этого правила сортировки, оно будет отображаться в списках, когда пользователи выбирают сортировку.', 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', + 'sort_rule_operations_desc' => 'Настройка сортировки выполняемых действий путем перемещения их из списка доступных операций. После использования операции будут выполняться по порядку сверху вниз. Любые изменения, внесенные здесь, будут применены ко всем назначенным книгам после сохранения.', 'sort_rule_available_operations' => 'Доступные операции', - 'sort_rule_available_operations_empty' => 'No operations remaining', + 'sort_rule_available_operations_empty' => 'Операций не осталось', 'sort_rule_configured_operations' => 'Configured Operations', 'sort_rule_configured_operations_empty' => 'Перетащите/добавьте операции из списка "Доступные операции"', 'sort_rule_op_asc' => '(Возрастание)', 'sort_rule_op_desc' => '(Убывание)', - 'sort_rule_op_name' => 'Name - Alphabetical', + 'sort_rule_op_name' => 'Имя - по алфавиту', 'sort_rule_op_name_numeric' => 'По нумерации', - 'sort_rule_op_created_date' => 'Created Date', - 'sort_rule_op_updated_date' => 'Updated Date', + 'sort_rule_op_created_date' => 'По дате создания', + 'sort_rule_op_updated_date' => 'По дате обновления', 'sort_rule_op_chapters_first' => 'Главы в начале', 'sort_rule_op_chapters_last' => 'Главы в конце', - 'sorting_page_limits' => 'Per-Page Display Limits', + 'sorting_page_limits' => 'Ограничения показа на странице', 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', // Maintenance settings @@ -194,19 +194,20 @@ return [ 'role_access_api' => 'Доступ к системному API', 'role_manage_settings' => 'Управление настройками приложения', 'role_export_content' => 'Экспорт контента', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Импортировать содержимое', 'role_editor_change' => 'Изменение редактора страниц', 'role_notifications' => 'Получение и управление уведомлениями', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', + 'role_permission_note_users_and_roles' => 'Эти разрешения также обеспечивают видимость и поиск пользователей и ролей в системе.', 'role_asset' => 'Права доступа к материалам', 'roles_system_warning' => 'Имейте в виду, что доступ к любому из указанных выше трех разрешений может позволить пользователю изменить свои собственные привилегии или привилегии других пользователей системы. Назначать роли с этими правами можно только доверенным пользователям.', 'role_asset_desc' => 'Эти разрешения контролируют доступ по умолчанию к параметрам внутри системы. Разрешения на книги, главы и страницы перезапишут эти разрешения.', 'role_asset_admins' => 'Администраторы автоматически получают доступ ко всему контенту, но эти опции могут отображать или скрывать параметры пользовательского интерфейса.', 'role_asset_image_view_note' => 'Это относится к видимости в менеджере изображений. Фактический доступ к загруженным файлам изображений будет зависеть от опции хранения системных изображений.', - 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', + 'role_asset_users_note' => 'Эти разрешения также обеспечивают видимость и поиск пользователей в системе.', 'role_all' => 'Все', 'role_own' => 'Владелец', 'role_controlled_by_asset' => 'Контролируется активом, в который они загружены', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Сохранить роль', 'role_users' => 'Пользователи с данной ролью', 'role_users_none' => 'Нет пользователей с данной ролью', diff --git a/lang/ru/validation.php b/lang/ru/validation.php index ce94faa3b..6d011cff1 100644 --- a/lang/ru/validation.php +++ b/lang/ru/validation.php @@ -105,11 +105,11 @@ return [ 'url' => 'Формат :attribute некорректен.', 'uploaded' => 'Не удалось загрузить файл. Сервер не может принимать файлы такого размера.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => ':attribute должен ссылаться на файл внутри ZIP.', + 'zip_file_size' => 'Файл :attribute не должен превышать :size МБ.', + 'zip_file_mime' => ':attribute должен ссылаться на файл типа :validTypes, найден :foundType.', + 'zip_model_expected' => 'Ожидался объект данных, но найдено ":type".', + 'zip_unique' => 'Значение :attribute должно быть уникальным для типа объекта внутри ZIP.', // Custom validation lines 'custom' => [ diff --git a/lang/sk/entities.php b/lang/sk/entities.php index eb5254535..48b662bf3 100644 --- a/lang/sk/entities.php +++ b/lang/sk/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Zoradiť knihu :bookName', 'books_sort_name' => 'Zoradiť podľa mena', 'books_sort_created' => 'Zoradiť podľa dátumu vytvorenia', diff --git a/lang/sk/settings.php b/lang/sk/settings.php index 67671f6f8..e18801ff4 100644 --- a/lang/sk/settings.php +++ b/lang/sk/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Všetko', 'role_own' => 'Vlastné', 'role_controlled_by_asset' => 'Regulované zdrojom, do ktorého sú nahrané', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Uložiť rolu', 'role_users' => 'Používatelia s touto rolou', 'role_users_none' => 'Žiadni používatelia nemajú priradenú túto rolu', diff --git a/lang/sl/entities.php b/lang/sl/entities.php index 00ac99181..86a43132e 100644 --- a/lang/sl/entities.php +++ b/lang/sl/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Razvrsti knjigo :bookName', 'books_sort_name' => 'Razvrsti po imenu', 'books_sort_created' => 'Razvrsti po datumu nastanka', diff --git a/lang/sl/settings.php b/lang/sl/settings.php index 947621389..87c1e8e6d 100644 --- a/lang/sl/settings.php +++ b/lang/sl/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Vse', 'role_own' => 'Lasten', 'role_controlled_by_asset' => 'Nadzira ga sredstvo, v katerega so naloženi', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Shrani vlogo', 'role_users' => 'Uporabniki v tej vlogi', 'role_users_none' => 'Tej vlogi trenutno ni dodeljen noben uporabnik', diff --git a/lang/sq/entities.php b/lang/sq/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/sq/entities.php +++ b/lang/sq/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/sq/settings.php b/lang/sq/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/sq/settings.php +++ b/lang/sq/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/sr/entities.php b/lang/sr/entities.php index 151edee40..8f9c40e91 100644 --- a/lang/sr/entities.php +++ b/lang/sr/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/sr/settings.php b/lang/sr/settings.php index f6c86827e..3453bc344 100644 --- a/lang/sr/settings.php +++ b/lang/sr/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/sv/entities.php b/lang/sv/entities.php index 680e0908a..94fc23f50 100644 --- a/lang/sv/entities.php +++ b/lang/sv/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Flytta kapitel och sidor inom en bok för att omorganisera dess innehåll. Andra böcker kan läggas till, vilket gör det enkelt att flytta kapitel och sidor mellan böcker. Du kan även ställa in en regel som automatiskt sorterar bokens innehåll vid ändringar.', 'books_sort_auto_sort' => 'Automatiskt sorteringsalternativ', 'books_sort_auto_sort_active' => 'Aktiv automatisk sorteringsregel: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sortera boken :bookName', 'books_sort_name' => 'Sortera utifrån namn', 'books_sort_created' => 'Sortera utifrån skapelse', diff --git a/lang/sv/settings.php b/lang/sv/settings.php index 773c4bff3..47f602b8d 100644 --- a/lang/sv/settings.php +++ b/lang/sv/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Alla', 'role_own' => 'Egna', 'role_controlled_by_asset' => 'Kontrolleras av den sida de laddas upp till', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Spara roll', 'role_users' => 'Användare med denna roll', 'role_users_none' => 'Inga användare tillhör den här rollen', diff --git a/lang/th/activities.php b/lang/th/activities.php new file mode 100644 index 000000000..6d84813d6 --- /dev/null +++ b/lang/th/activities.php @@ -0,0 +1,140 @@ + 'สร้างหน้า', + 'page_create_notification' => 'สร้างหน้าสำเร็จแล้ว', + 'page_update' => 'แก้ไขหน้า', + 'page_update_notification' => 'แก้ไขหน้าสำเร็จแล้ว', + 'page_delete' => 'ลบหน้า', + 'page_delete_notification' => 'ลบหน้าสำเร็จแล้ว', + 'page_restore' => 'กู้คืนหน้า', + 'page_restore_notification' => 'กู้คืนหน้าสำเร็จแล้ว', + 'page_move' => 'ย้ายหน้า', + 'page_move_notification' => 'ย้ายหน้าสำเร็จแล้ว', + + // Chapters + 'chapter_create' => 'สร้างบท', + 'chapter_create_notification' => 'สร้างบทสำเร็จแล้ว', + 'chapter_update' => 'แก้ไขบท', + 'chapter_update_notification' => 'แก้ไขบทสำเร็จแล้ว', + 'chapter_delete' => 'ลบบท', + 'chapter_delete_notification' => 'ลบบทสำเร็จแล้ว', + 'chapter_move' => 'ย้ายบท', + 'chapter_move_notification' => 'ย้ายบทสำเร็จแล้ว', + + // Books + 'book_create' => 'สร้างหนังสือ', + 'book_create_notification' => 'สร้างหนังสือสำเร็จแล้ว', + 'book_create_from_chapter' => 'แปลงบทเป็นหนังสือ', + 'book_create_from_chapter_notification' => 'แปลงบทเป็นหนังสือสำเร็จแล้ว', + 'book_update' => 'แก้ไขหนังสือ', + 'book_update_notification' => 'แก้ไขหนังสือสำเร็จแล้ว', + 'book_delete' => 'ลบหนังสือ', + 'book_delete_notification' => 'ลบหนังสือสำเร็จแล้ว', + 'book_sort' => 'จัดเรียงหนังสือ', + 'book_sort_notification' => 'จัดเรียงหนังสือสำเร็จแล้ว', + + // Bookshelves + 'bookshelf_create' => 'สร้างชั้นวาง', + 'bookshelf_create_notification' => 'สร้างชั้นวางสำเร็จแล้ว', + 'bookshelf_create_from_book' => 'แปลงหนังสือเป็นชั้นวาง', + 'bookshelf_create_from_book_notification' => 'แปลงหนังสือเป็นชั้นวางสำเร็จแล้ว', + 'bookshelf_update' => 'แก้ไขชั้นวาง', + 'bookshelf_update_notification' => 'แก้ไขชั้นวางสำเร็จแล้ว', + 'bookshelf_delete' => 'ลบชั้นวาง', + 'bookshelf_delete_notification' => 'ลบชั้นวางสำเร็จแล้ว', + + // Revisions + 'revision_restore' => 'กู้คืนการแก้ไข', + 'revision_delete' => 'ลบการแก้ไข', + 'revision_delete_notification' => 'ลบการแก้ไขสำเร็จแล้ว', + + // Favourites + 'favourite_add_notification' => 'เพิ่ม ":name" ในรายการโปรดแล้ว', + 'favourite_remove_notification' => 'นำ ":name" ออกจากรายการโปรดแล้ว', + + // Watching + 'watch_update_level_notification' => 'อัปเดตการตั้งค่าการติดตามสำเร็จแล้ว', + + // Auth + 'auth_login' => 'เข้าสู่ระบบ', + 'auth_register' => 'ลงทะเบียนเป็นผู้ใช้ใหม่', + 'auth_password_reset_request' => 'ขอรีเซ็ตรหัสผ่าน', + 'auth_password_reset_update' => 'รีเซ็ตรหัสผ่านแล้ว', + 'mfa_setup_method' => 'ตั้งค่าวิธียืนยันตัวตน MFA', + 'mfa_setup_method_notification' => 'ตั้งค่าการยืนยันตัวตนแบบหลายขั้นตอนสำเร็จแล้ว', + 'mfa_remove_method' => 'ลบวิธียืนยันตัวตน MFA', + 'mfa_remove_method_notification' => 'ลบการยืนยันตัวตนแบบหลายขั้นตอนสำเร็จแล้ว', + + // Settings + 'settings_update' => 'แก้ไขการตั้งค่า', + 'settings_update_notification' => 'แก้ไขการตั้งค่าสำเร็จแล้ว', + 'maintenance_action_run' => 'ดำเนินการบำรุงรักษาระบบ', + + // Webhooks + 'webhook_create' => 'สร้าง Webhook', + 'webhook_create_notification' => 'สร้าง Webhook สำเร็จแล้ว', + 'webhook_update' => 'แก้ไข Webhook', + 'webhook_update_notification' => 'แก้ไข Webhook สำเร็จแล้ว', + 'webhook_delete' => 'ลบ Webhook', + 'webhook_delete_notification' => 'ลบ Webhook สำเร็จแล้ว', + + // Imports + 'import_create' => 'สร้างการนำเข้า', + 'import_create_notification' => 'อัปโหลดไฟล์นำเข้าสำเร็จแล้ว', + 'import_run' => 'ดำเนินการนำเข้า', + 'import_run_notification' => 'นำเข้าเนื้อหาสำเร็จแล้ว', + 'import_delete' => 'ลบการนำเข้า', + 'import_delete_notification' => 'ลบการนำเข้าสำเร็จแล้ว', + + // Users + 'user_create' => 'สร้างผู้ใช้', + 'user_create_notification' => 'สร้างผู้ใช้สำเร็จแล้ว', + 'user_update' => 'แก้ไขผู้ใช้', + 'user_update_notification' => 'แก้ไขผู้ใช้สำเร็จแล้ว', + 'user_delete' => 'ลบผู้ใช้', + 'user_delete_notification' => 'ลบผู้ใช้สำเร็จแล้ว', + + // API Tokens + 'api_token_create' => 'สร้าง API Token', + 'api_token_create_notification' => 'สร้าง API Token สำเร็จแล้ว', + 'api_token_update' => 'แก้ไข API Token', + 'api_token_update_notification' => 'แก้ไข API Token สำเร็จแล้ว', + 'api_token_delete' => 'ลบ API Token', + 'api_token_delete_notification' => 'ลบ API Token สำเร็จแล้ว', + + // Roles + 'role_create' => 'สร้างบทบาท', + 'role_create_notification' => 'สร้างบทบาทสำเร็จแล้ว', + 'role_update' => 'แก้ไขบทบาท', + 'role_update_notification' => 'แก้ไขบทบาทสำเร็จแล้ว', + 'role_delete' => 'ลบบทบาท', + 'role_delete_notification' => 'ลบบทบาทสำเร็จแล้ว', + + // Recycle Bin + 'recycle_bin_empty' => 'ล้างถังรีไซเคิล', + 'recycle_bin_restore' => 'กู้คืนจากถังรีไซเคิล', + 'recycle_bin_destroy' => 'ลบถาวรจากถังรีไซเคิล', + + // Comments + 'commented_on' => 'แสดงความคิดเห็นใน', + 'comment_create' => 'เพิ่มความคิดเห็น', + 'comment_update' => 'แก้ไขความคิดเห็น', + 'comment_delete' => 'ลบความคิดเห็น', + + // Sort Rules + 'sort_rule_create' => 'สร้างกฎการจัดเรียง', + 'sort_rule_create_notification' => 'สร้างกฎการจัดเรียงสำเร็จแล้ว', + 'sort_rule_update' => 'แก้ไขกฎการจัดเรียง', + 'sort_rule_update_notification' => 'แก้ไขกฎการจัดเรียงสำเร็จแล้ว', + 'sort_rule_delete' => 'ลบกฎการจัดเรียง', + 'sort_rule_delete_notification' => 'ลบกฎการจัดเรียงสำเร็จแล้ว', + + // Other + 'permissions_update' => 'แก้ไขสิทธิ์', +]; diff --git a/lang/th/auth.php b/lang/th/auth.php new file mode 100644 index 000000000..c00b29bcd --- /dev/null +++ b/lang/th/auth.php @@ -0,0 +1,117 @@ + 'ข้อมูลประจำตัวไม่ตรงกับที่มีในระบบ', + 'throttle' => 'เข้าสู่ระบบล้มเหลวหลายครั้งเกินไป กรุณาลองใหม่ในอีก :seconds วินาที', + + // Login & Register + 'sign_up' => 'สมัครสมาชิก', + 'log_in' => 'เข้าสู่ระบบ', + 'log_in_with' => 'เข้าสู่ระบบด้วย :socialDriver', + 'sign_up_with' => 'สมัครสมาชิกด้วย :socialDriver', + 'logout' => 'ออกจากระบบ', + + 'name' => 'ชื่อ', + 'username' => 'ชื่อผู้ใช้', + 'email' => 'อีเมล', + 'password' => 'รหัสผ่าน', + 'password_confirm' => 'ยืนยันรหัสผ่าน', + 'password_hint' => 'ต้องมีอย่างน้อย 8 ตัวอักษร', + 'forgot_password' => 'ลืมรหัสผ่าน?', + 'remember_me' => 'จดจำฉัน', + 'ldap_email_hint' => 'กรุณากรอกอีเมลที่จะใช้กับบัญชีนี้', + 'create_account' => 'สร้างบัญชี', + 'already_have_account' => 'มีบัญชีอยู่แล้ว?', + 'dont_have_account' => 'ยังไม่มีบัญชี?', + 'social_login' => 'เข้าสู่ระบบด้วย Social', + 'social_registration' => 'ลงทะเบียนด้วย Social', + 'social_registration_text' => 'ลงทะเบียนและเข้าสู่ระบบด้วยบริการอื่น', + + 'register_thanks' => 'ขอบคุณที่ลงทะเบียน กรุณายืนยันอีเมลเพื่อเข้าสู่ระบบ', + 'register_confirm' => 'ยืนยันและลงทะเบียน', + 'registrations_disabled' => 'ขณะนี้ปิดรับการลงทะเบียน', + 'registration_email_domain_invalid' => 'โดเมนอีเมลนี้ไม่มีสิทธิ์เข้าถึงระบบ', + 'register_success' => 'ขอบคุณที่สมัครสมาชิก คุณได้ลงทะเบียนและเข้าสู่ระบบแล้ว', + + // Login auto-initiation + 'auto_init_starting' => 'กำลังเข้าสู่ระบบ', + 'auto_init_starting_desc' => 'กำลังติดต่อระบบยืนยันตัวตนเพื่อเริ่มกระบวนการเข้าสู่ระบบ หากไม่มีความคืบหน้าภายใน 5 วินาที กรุณาคลิกลิงก์ด้านล่าง', + 'auto_init_start_link' => 'ดำเนินการยืนยันตัวตน', + + // Password Reset + 'reset_password' => 'รีเซ็ตรหัสผ่าน', + 'reset_password_send_instructions' => 'กรอกอีเมลด้านล่าง ระบบจะส่งลิงก์รีเซ็ตรหัสผ่านให้คุณ', + 'reset_password_send_button' => 'ส่งลิงก์รีเซ็ตรหัสผ่าน', + 'reset_password_sent' => 'หากพบอีเมล :email ในระบบ จะมีลิงก์รีเซ็ตรหัสผ่านส่งไปให้', + 'reset_password_success' => 'รีเซ็ตรหัสผ่านสำเร็จแล้ว', + 'email_reset_subject' => 'รีเซ็ตรหัสผ่าน :appName ของคุณ', + 'email_reset_text' => 'คุณได้รับอีเมลนี้เพราะมีการขอรีเซ็ตรหัสผ่านสำหรับบัญชีของคุณ', + 'email_reset_not_requested' => 'หากคุณไม่ได้ขอรีเซ็ตรหัสผ่าน ไม่ต้องดำเนินการใดๆ เพิ่มเติม', + + // Email Confirmation + 'email_confirm_subject' => 'ยืนยันอีเมลของคุณบน :appName', + 'email_confirm_greeting' => 'ขอบคุณที่เข้าร่วม :appName!', + 'email_confirm_text' => 'กรุณายืนยันอีเมลของคุณโดยคลิกปุ่มด้านล่าง:', + 'email_confirm_action' => 'ยืนยันอีเมล', + 'email_confirm_send_error' => 'จำเป็นต้องยืนยันอีเมล แต่ระบบไม่สามารถส่งอีเมลได้ กรุณาติดต่อผู้ดูแลระบบเพื่อตรวจสอบการตั้งค่าอีเมล', + 'email_confirm_success' => 'ยืนยันอีเมลสำเร็จแล้ว สามารถเข้าสู่ระบบได้', + 'email_confirm_resent' => 'ส่งอีเมลยืนยันใหม่แล้ว กรุณาตรวจสอบกล่องจดหมาย', + 'email_confirm_thanks' => 'ขอบคุณที่ยืนยัน!', + 'email_confirm_thanks_desc' => 'กรุณารอสักครู่ขณะที่ระบบดำเนินการยืนยัน หากไม่ถูกเปลี่ยนหน้าภายใน 3 วินาที กรุณาคลิกลิงก์ "ดำเนินการต่อ" ด้านล่าง', + + 'email_not_confirmed' => 'ยังไม่ได้ยืนยันอีเมล', + 'email_not_confirmed_text' => 'อีเมลของคุณยังไม่ได้รับการยืนยัน', + 'email_not_confirmed_click_link' => 'กรุณาคลิกลิงก์ในอีเมลที่ส่งให้คุณหลังจากลงทะเบียน', + 'email_not_confirmed_resend' => 'หากไม่พบอีเมล คุณสามารถส่งอีเมลยืนยันอีกครั้งโดยกรอกแบบฟอร์มด้านล่าง', + 'email_not_confirmed_resend_button' => 'ส่งอีเมลยืนยันอีกครั้ง', + + // User Invite + 'user_invite_email_subject' => 'คุณได้รับเชิญให้เข้าร่วม :appName!', + 'user_invite_email_greeting' => 'มีการสร้างบัญชีให้คุณบน :appName', + 'user_invite_email_text' => 'คลิกปุ่มด้านล่างเพื่อตั้งรหัสผ่านและเข้าใช้งาน:', + 'user_invite_email_action' => 'ตั้งรหัสผ่านบัญชี', + 'user_invite_page_welcome' => 'ยินดีต้อนรับสู่ :appName!', + 'user_invite_page_text' => 'เพื่อเสร็จสิ้นการสร้างบัญชีและเข้าใช้งาน คุณต้องตั้งรหัสผ่านสำหรับเข้าสู่ :appName ในครั้งถัดไป', + 'user_invite_page_confirm_button' => 'ยืนยันรหัสผ่าน', + 'user_invite_success_login' => 'ตั้งรหัสผ่านแล้ว คุณสามารถเข้าสู่ระบบ :appName ด้วยรหัสผ่านที่ตั้งไว้ได้แล้ว!', + + // Multi-factor Authentication + 'mfa_setup' => 'ตั้งค่าการยืนยันตัวตนแบบหลายขั้นตอน', + 'mfa_setup_desc' => 'ตั้งค่า MFA เพื่อเพิ่มความปลอดภัยให้บัญชีของคุณ', + 'mfa_setup_configured' => 'ตั้งค่าแล้ว', + 'mfa_setup_reconfigure' => 'ตั้งค่าใหม่', + 'mfa_setup_remove_confirmation' => 'คุณแน่ใจหรือไม่ว่าต้องการลบวิธียืนยันตัวตนแบบหลายขั้นตอนนี้?', + 'mfa_setup_action' => 'ตั้งค่า', + 'mfa_backup_codes_usage_limit_warning' => 'คุณมีรหัสสำรองเหลือน้อยกว่า 5 รหัส กรุณาสร้างและบันทึกชุดใหม่ก่อนหมด เพื่อป้องกันการถูกล็อกออกจากบัญชี', + 'mfa_option_totp_title' => 'แอป Authenticator', + 'mfa_option_totp_desc' => 'ในการใช้การยืนยันตัวตนแบบหลายขั้นตอน คุณต้องมีแอปพลิเคชันมือถือที่รองรับ TOTP เช่น Google Authenticator, Authy หรือ Microsoft Authenticator', + 'mfa_option_backup_codes_title' => 'รหัสสำรอง', + 'mfa_option_backup_codes_desc' => 'สร้างชุดรหัสสำรองแบบใช้ครั้งเดียว ซึ่งจะใช้กรอกเมื่อเข้าสู่ระบบเพื่อยืนยันตัวตน กรุณาเก็บรักษาไว้ในที่ปลอดภัย', + 'mfa_gen_confirm_and_enable' => 'ยืนยันและเปิดใช้งาน', + 'mfa_gen_backup_codes_title' => 'ตั้งค่ารหัสสำรอง', + 'mfa_gen_backup_codes_desc' => 'บันทึกรายการรหัสด้านล่างไว้ในที่ปลอดภัย เมื่อเข้าระบบคุณสามารถใช้รหัสเหล่านี้เป็นการยืนยันตัวตนขั้นที่สองได้', + 'mfa_gen_backup_codes_download' => 'ดาวน์โหลดรหัส', + 'mfa_gen_backup_codes_usage_warning' => 'รหัสแต่ละรหัสใช้ได้เพียงครั้งเดียว', + 'mfa_gen_totp_title' => 'ตั้งค่าแอปมือถือ', + 'mfa_gen_totp_desc' => 'ในการใช้การยืนยันตัวตนแบบหลายขั้นตอน คุณต้องมีแอปพลิเคชันมือถือที่รองรับ TOTP เช่น Google Authenticator, Authy หรือ Microsoft Authenticator', + 'mfa_gen_totp_scan' => 'สแกน QR code ด้านล่างด้วยแอป Authenticator ที่คุณต้องการใช้', + 'mfa_gen_totp_verify_setup' => 'ยืนยันการตั้งค่า', + 'mfa_gen_totp_verify_setup_desc' => 'ยืนยันว่าทุกอย่างทำงานได้โดยกรอกรหัสที่สร้างจากแอป Authenticator ในช่องด้านล่าง:', + 'mfa_gen_totp_provide_code_here' => 'กรอกรหัสที่สร้างจากแอปของคุณที่นี่', + 'mfa_verify_access' => 'ยืนยันการเข้าถึง', + 'mfa_verify_access_desc' => 'บัญชีของคุณต้องยืนยันตัวตนผ่านการตรวจสอบเพิ่มเติมก่อนเข้าใช้งาน กรุณายืนยันด้วยวิธีที่ตั้งค่าไว้เพื่อดำเนินการต่อ', + 'mfa_verify_no_methods' => 'ยังไม่ได้ตั้งค่าวิธียืนยันตัวตน', + 'mfa_verify_no_methods_desc' => 'ไม่พบวิธียืนยันตัวตนแบบหลายขั้นตอนสำหรับบัญชีของคุณ กรุณาตั้งค่าอย่างน้อยหนึ่งวิธีก่อนเข้าใช้งาน', + 'mfa_verify_use_totp' => 'ยืนยันด้วยแอปมือถือ', + 'mfa_verify_use_backup_codes' => 'ยืนยันด้วยรหัสสำรอง', + 'mfa_verify_backup_code' => 'รหัสสำรอง', + 'mfa_verify_backup_code_desc' => 'กรอกรหัสสำรองที่เหลืออยู่ของคุณด้านล่าง:', + 'mfa_verify_backup_code_enter_here' => 'กรอกรหัสสำรองที่นี่', + 'mfa_verify_totp_desc' => 'กรอกรหัสที่สร้างจากแอปมือถือของคุณด้านล่าง:', + 'mfa_setup_login_notification' => 'ตั้งค่าวิธียืนยันตัวตนแล้ว กรุณาเข้าสู่ระบบอีกครั้งด้วยวิธีที่ตั้งค่าไว้', +]; diff --git a/lang/th/common.php b/lang/th/common.php new file mode 100644 index 000000000..9a8b4fcc5 --- /dev/null +++ b/lang/th/common.php @@ -0,0 +1,115 @@ + 'ยกเลิก', + 'close' => 'ปิด', + 'confirm' => 'ยืนยัน', + 'back' => 'ย้อนกลับ', + 'save' => 'บันทึก', + 'continue' => 'ดำเนินการต่อ', + 'select' => 'เลือก', + 'toggle_all' => 'สลับทั้งหมด', + 'more' => 'เพิ่มเติม', + + // Form Labels + 'name' => 'ชื่อ', + 'description' => 'คำอธิบาย', + 'role' => 'บทบาท', + 'cover_image' => 'ภาพปก', + 'cover_image_description' => 'รูปภาพนี้ควรมีขนาดประมาณ 440x250px แต่จะถูกปรับขนาดและตัดให้เหมาะกับการแสดงผลในสถานการณ์ต่างๆ ดังนั้นขนาดที่แสดงจริงอาจแตกต่างกัน', + + // Actions + 'actions' => 'การดำเนินการ', + 'view' => 'ดู', + 'view_all' => 'ดูทั้งหมด', + 'new' => 'ใหม่', + 'create' => 'สร้าง', + 'update' => 'อัปเดต', + 'edit' => 'แก้ไข', + 'archive' => 'เก็บถาวร', + 'unarchive' => 'ยกเลิกการเก็บถาวร', + 'sort' => 'จัดเรียง', + 'move' => 'ย้าย', + 'copy' => 'คัดลอก', + 'reply' => 'ตอบกลับ', + 'delete' => 'ลบ', + 'delete_confirm' => 'ยืนยันการลบ', + 'search' => 'ค้นหา', + 'search_clear' => 'ล้างการค้นหา', + 'reset' => 'รีเซ็ต', + 'remove' => 'ลบออก', + 'add' => 'เพิ่ม', + 'configure' => 'กำหนดค่า', + 'manage' => 'จัดการ', + 'fullscreen' => 'เต็มหน้าจอ', + 'favourite' => 'เพิ่มในรายการโปรด', + 'unfavourite' => 'นำออกจากรายการโปรด', + 'next' => 'ถัดไป', + 'previous' => 'ก่อนหน้า', + 'filter_active' => 'ตัวกรองที่ใช้งานอยู่:', + 'filter_clear' => 'ล้างตัวกรอง', + 'download' => 'ดาวน์โหลด', + 'open_in_tab' => 'เปิดในแท็บใหม่', + 'open' => 'เปิด', + + // Sort Options + 'sort_options' => 'ตัวเลือกการจัดเรียง', + 'sort_direction_toggle' => 'สลับทิศทางการจัดเรียง', + 'sort_ascending' => 'จัดเรียงจากน้อยไปมาก', + 'sort_descending' => 'จัดเรียงจากมากไปน้อย', + 'sort_name' => 'ชื่อ', + 'sort_default' => 'ค่าเริ่มต้น', + 'sort_created_at' => 'วันที่สร้าง', + 'sort_updated_at' => 'วันที่แก้ไขล่าสุด', + + // Misc + 'deleted_user' => 'ผู้ใช้ที่ถูกลบ', + 'no_activity' => 'ไม่มีกิจกรรมที่จะแสดง', + 'no_items' => 'ไม่มีรายการ', + 'back_to_top' => 'กลับไปด้านบน', + 'skip_to_main_content' => 'ข้ามไปยังเนื้อหาหลัก', + 'toggle_details' => 'แสดง/ซ่อนรายละเอียด', + 'toggle_thumbnails' => 'แสดง/ซ่อนภาพย่อ', + 'details' => 'รายละเอียด', + 'grid_view' => 'มุมมองตาราง', + 'list_view' => 'มุมมองรายการ', + 'default' => 'ค่าเริ่มต้น', + 'breadcrumb' => 'เส้นทางนำทาง', + 'status' => 'สถานะ', + 'status_active' => 'ใช้งาน', + 'status_inactive' => 'ไม่ใช้งาน', + 'never' => 'ไม่เคย', + 'none' => 'ไม่มี', + + // Header + 'homepage' => 'หน้าแรก', + 'header_menu_expand' => 'ขยายเมนูส่วนหัว', + 'profile_menu' => 'เมนูโปรไฟล์', + 'view_profile' => 'ดูโปรไฟล์', + 'edit_profile' => 'แก้ไขโปรไฟล์', + 'dark_mode' => 'โหมดมืด', + 'light_mode' => 'โหมดสว่าง', + 'global_search' => 'ค้นหาทั้งระบบ', + + // Layout tabs + 'tab_info' => 'ข้อมูล', + 'tab_info_label' => 'แท็บ: แสดงข้อมูลเพิ่มเติม', + 'tab_content' => 'เนื้อหา', + 'tab_content_label' => 'แท็บ: แสดงเนื้อหาหลัก', + + // Email Content + 'email_action_help' => 'หากไม่สามารถคลิกปุ่ม ":actionText" ได้ กรุณาคัดลอก URL ด้านล่างและวางในเบราว์เซอร์:', + 'email_rights' => 'สงวนลิขสิทธิ์', + + // Footer Link Options + // Not directly used but available for convenience to users. + 'privacy_policy' => 'นโยบายความเป็นส่วนตัว', + 'terms_of_service' => 'ข้อกำหนดการใช้งาน', + + // OpenSearch + 'opensearch_description' => 'ค้นหา :appName', +]; diff --git a/lang/th/components.php b/lang/th/components.php new file mode 100644 index 000000000..2931d2bc7 --- /dev/null +++ b/lang/th/components.php @@ -0,0 +1,46 @@ + 'เลือกรูปภาพ', + 'image_list' => 'รายการรูปภาพ', + 'image_details' => 'รายละเอียดรูปภาพ', + 'image_upload' => 'อัปโหลดรูปภาพ', + 'image_intro' => 'คุณสามารถเลือกและจัดการรูปภาพที่เคยอัปโหลดไว้ในระบบได้ที่นี่', + 'image_intro_upload' => 'อัปโหลดรูปภาพใหม่โดยลากไฟล์รูปภาพมาวางในหน้าต่างนี้ หรือใช้ปุ่ม "อัปโหลดรูปภาพ" ด้านบน', + 'image_all' => 'ทั้งหมด', + 'image_all_title' => 'ดูรูปภาพทั้งหมด', + 'image_book_title' => 'ดูรูปภาพที่อัปโหลดในหนังสือนี้', + 'image_page_title' => 'ดูรูปภาพที่อัปโหลดในหน้านี้', + 'image_search_hint' => 'ค้นหาตามชื่อรูปภาพ', + 'image_uploaded' => 'อัปโหลดเมื่อ :uploadedDate', + 'image_uploaded_by' => 'อัปโหลดโดย :userName', + 'image_uploaded_to' => 'อัปโหลดไปยัง :pageLink', + 'image_updated' => 'อัปเดตเมื่อ :updateDate', + 'image_load_more' => 'โหลดเพิ่มเติม', + 'image_image_name' => 'ชื่อรูปภาพ', + 'image_delete_used' => 'รูปภาพนี้ถูกใช้งานในหน้าด้านล่าง', + 'image_delete_confirm_text' => 'คุณแน่ใจหรือไม่ว่าต้องการลบรูปภาพนี้?', + 'image_select_image' => 'เลือกรูปภาพ', + 'image_dropzone' => 'วางรูปภาพหรือคลิกที่นี่เพื่ออัปโหลด', + 'image_dropzone_drop' => 'วางรูปภาพที่นี่เพื่ออัปโหลด', + 'images_deleted' => 'ลบรูปภาพแล้ว', + 'image_preview' => 'ดูตัวอย่างรูปภาพ', + 'image_upload_success' => 'อัปโหลดรูปภาพสำเร็จแล้ว', + 'image_update_success' => 'อัปเดตรายละเอียดรูปภาพสำเร็จแล้ว', + 'image_delete_success' => 'ลบรูปภาพสำเร็จแล้ว', + 'image_replace' => 'แทนที่รูปภาพ', + 'image_replace_success' => 'อัปเดตไฟล์รูปภาพสำเร็จแล้ว', + 'image_rebuild_thumbs' => 'สร้างภาพย่อขนาดต่างๆ ใหม่', + 'image_rebuild_thumbs_success' => 'สร้างภาพย่อขนาดต่างๆ ใหม่สำเร็จแล้ว!', + + // Code Editor + 'code_editor' => 'แก้ไขโค้ด', + 'code_language' => 'ภาษาโค้ด', + 'code_content' => 'เนื้อหาโค้ด', + 'code_session_history' => 'ประวัติเซสชัน', + 'code_save' => 'บันทึกโค้ด', +]; diff --git a/lang/th/editor.php b/lang/th/editor.php new file mode 100644 index 000000000..4af059bf1 --- /dev/null +++ b/lang/th/editor.php @@ -0,0 +1,182 @@ + 'ทั่วไป', + 'advanced' => 'ขั้นสูง', + 'none' => 'ไม่มี', + 'cancel' => 'ยกเลิก', + 'save' => 'บันทึก', + 'close' => 'ปิด', + 'apply' => 'ใช้งาน', + 'undo' => 'เลิกทำ', + 'redo' => 'ทำซ้ำ', + 'left' => 'ซ้าย', + 'center' => 'กลาง', + 'right' => 'ขวา', + 'top' => 'บน', + 'middle' => 'กลาง', + 'bottom' => 'ล่าง', + 'width' => 'ความกว้าง', + 'height' => 'ความสูง', + 'More' => 'เพิ่มเติม', + 'select' => 'เลือก...', + + // Toolbar + 'formats' => 'รูปแบบ', + 'header_large' => 'หัวข้อใหญ่', + 'header_medium' => 'หัวข้อกลาง', + 'header_small' => 'หัวข้อเล็ก', + 'header_tiny' => 'หัวข้อเล็กมาก', + 'paragraph' => 'ย่อหน้า', + 'blockquote' => 'คำพูดอ้างอิง', + 'inline_code' => 'โค้ดแบบอินไลน์', + 'callouts' => 'กล่องข้อความเน้น', + 'callout_information' => 'ข้อมูล', + 'callout_success' => 'สำเร็จ', + 'callout_warning' => 'คำเตือน', + 'callout_danger' => 'อันตราย', + 'bold' => 'ตัวหนา', + 'italic' => 'ตัวเอียง', + 'underline' => 'ขีดเส้นใต้', + 'strikethrough' => 'ขีดทับ', + 'superscript' => 'ตัวยก', + 'subscript' => 'ตัวห้อย', + 'text_color' => 'สีตัวอักษร', + 'highlight_color' => 'สีไฮไลต์', + 'custom_color' => 'สีกำหนดเอง', + 'remove_color' => 'ลบสี', + 'background_color' => 'สีพื้นหลัง', + 'align_left' => 'จัดชิดซ้าย', + 'align_center' => 'จัดกึ่งกลาง', + 'align_right' => 'จัดชิดขวา', + 'align_justify' => 'จัดเต็มบรรทัด', + 'list_bullet' => 'รายการแบบจุด', + 'list_numbered' => 'รายการแบบตัวเลข', + 'list_task' => 'รายการงาน', + 'indent_increase' => 'เพิ่มการย่อหน้า', + 'indent_decrease' => 'ลดการย่อหน้า', + 'table' => 'ตาราง', + 'insert_image' => 'แทรกรูปภาพ', + 'insert_image_title' => 'แทรก/แก้ไขรูปภาพ', + 'insert_link' => 'แทรก/แก้ไขลิงก์', + 'insert_link_title' => 'แทรก/แก้ไขลิงก์', + 'insert_horizontal_line' => 'แทรกเส้นแนวนอน', + 'insert_code_block' => 'แทรกบล็อกโค้ด', + 'edit_code_block' => 'แก้ไขบล็อกโค้ด', + 'insert_drawing' => 'แทรก/แก้ไขภาพวาด', + 'drawing_manager' => 'ตัวจัดการภาพวาด', + 'insert_media' => 'แทรก/แก้ไขสื่อ', + 'insert_media_title' => 'แทรก/แก้ไขสื่อ', + 'clear_formatting' => 'ล้างการจัดรูปแบบ', + 'source_code' => 'ซอร์สโค้ด', + 'source_code_title' => 'ซอร์สโค้ด', + 'fullscreen' => 'เต็มหน้าจอ', + 'image_options' => 'ตัวเลือกรูปภาพ', + + // Tables + 'table_properties' => 'คุณสมบัติตาราง', + 'table_properties_title' => 'คุณสมบัติตาราง', + 'delete_table' => 'ลบตาราง', + 'table_clear_formatting' => 'ล้างการจัดรูปแบบตาราง', + 'resize_to_contents' => 'ปรับขนาดตามเนื้อหา', + 'row_header' => 'แถวหัวตาราง', + 'insert_row_before' => 'แทรกแถวก่อนหน้า', + 'insert_row_after' => 'แทรกแถวถัดไป', + 'delete_row' => 'ลบแถว', + 'insert_column_before' => 'แทรกคอลัมน์ก่อนหน้า', + 'insert_column_after' => 'แทรกคอลัมน์ถัดไป', + 'delete_column' => 'ลบคอลัมน์', + 'table_cell' => 'เซลล์', + 'table_row' => 'แถว', + 'table_column' => 'คอลัมน์', + 'cell_properties' => 'คุณสมบัติเซลล์', + 'cell_properties_title' => 'คุณสมบัติเซลล์', + 'cell_type' => 'ประเภทเซลล์', + 'cell_type_cell' => 'เซลล์', + 'cell_scope' => 'ขอบเขต', + 'cell_type_header' => 'เซลล์หัวตาราง', + 'merge_cells' => 'รวมเซลล์', + 'split_cell' => 'แยกเซลล์', + 'table_row_group' => 'กลุ่มแถว', + 'table_column_group' => 'กลุ่มคอลัมน์', + 'horizontal_align' => 'การจัดแนวนอน', + 'vertical_align' => 'การจัดแนวตั้ง', + 'border_width' => 'ความกว้างเส้นขอบ', + 'border_style' => 'รูปแบบเส้นขอบ', + 'border_color' => 'สีเส้นขอบ', + 'row_properties' => 'คุณสมบัติแถว', + 'row_properties_title' => 'คุณสมบัติแถว', + 'cut_row' => 'ตัดแถว', + 'copy_row' => 'คัดลอกแถว', + 'paste_row_before' => 'วางแถวก่อนหน้า', + 'paste_row_after' => 'วางแถวถัดไป', + 'row_type' => 'ประเภทแถว', + 'row_type_header' => 'ส่วนหัว', + 'row_type_body' => 'ส่วนเนื้อหา', + 'row_type_footer' => 'ส่วนท้าย', + 'alignment' => 'การจัดตำแหน่ง', + 'cut_column' => 'ตัดคอลัมน์', + 'copy_column' => 'คัดลอกคอลัมน์', + 'paste_column_before' => 'วางคอลัมน์ก่อนหน้า', + 'paste_column_after' => 'วางคอลัมน์ถัดไป', + 'cell_padding' => 'ระยะห่างภายในเซลล์', + 'cell_spacing' => 'ระยะห่างระหว่างเซลล์', + 'caption' => 'คำบรรยาย', + 'show_caption' => 'แสดงคำบรรยาย', + 'constrain' => 'รักษาสัดส่วน', + 'cell_border_solid' => 'เส้นทึบ', + 'cell_border_dotted' => 'เส้นจุด', + 'cell_border_dashed' => 'เส้นประ', + 'cell_border_double' => 'เส้นคู่', + 'cell_border_groove' => 'เส้นร่อง', + 'cell_border_ridge' => 'เส้นนูน', + 'cell_border_inset' => 'เส้นฝัง', + 'cell_border_outset' => 'เส้นนูนออก', + 'cell_border_none' => 'ไม่มี', + 'cell_border_hidden' => 'ซ่อน', + + // Images, links, details/summary & embed + 'source' => 'แหล่งที่มา', + 'alt_desc' => 'คำอธิบายทดแทน', + 'embed' => 'ฝังเนื้อหา', + 'paste_embed' => 'วางโค้ดฝังเนื้อหาด้านล่าง:', + 'url' => 'URL', + 'text_to_display' => 'ข้อความที่แสดง', + 'title' => 'ชื่อเรื่อง', + 'browse_links' => 'เรียกดูลิงก์', + 'open_link' => 'เปิดลิงก์', + 'open_link_in' => 'เปิดลิงก์ใน...', + 'open_link_current' => 'หน้าต่างปัจจุบัน', + 'open_link_new' => 'หน้าต่างใหม่', + 'remove_link' => 'ลบลิงก์', + 'insert_collapsible' => 'แทรกบล็อกที่ย่อได้', + 'collapsible_unwrap' => 'ยกเลิกการห่อ', + 'edit_label' => 'แก้ไขป้ายกำกับ', + 'toggle_open_closed' => 'สลับเปิด/ปิด', + 'collapsible_edit' => 'แก้ไขบล็อกที่ย่อได้', + 'toggle_label' => 'ป้ายกำกับสลับ', + + // About view + 'about' => 'เกี่ยวกับตัวแก้ไข', + 'about_title' => 'เกี่ยวกับตัวแก้ไข WYSIWYG', + 'editor_license' => 'สัญญาอนุญาตและลิขสิทธิ์ตัวแก้ไข', + 'editor_lexical_license' => 'ตัวแก้ไขนี้สร้างจาก :lexicalLink ซึ่งเผยแพร่ภายใต้สัญญาอนุญาต MIT', + 'editor_lexical_license_link' => 'ดูรายละเอียดสัญญาอนุญาตฉบับเต็มได้ที่นี่', + 'editor_tiny_license' => 'ตัวแก้ไขนี้สร้างด้วย :tinyLink ซึ่งเผยแพร่ภายใต้สัญญาอนุญาต MIT', + 'editor_tiny_license_link' => 'ดูรายละเอียดลิขสิทธิ์และสัญญาอนุญาตของ TinyMCE ได้ที่นี่', + 'save_continue' => 'บันทึกหน้าและดำเนินการต่อ', + 'callouts_cycle' => '(กดต่อเนื่องเพื่อสลับประเภท)', + 'link_selector' => 'ลิงก์ไปยังเนื้อหา', + 'shortcuts' => 'แป้นพิมพ์ลัด', + 'shortcut' => 'แป้นพิมพ์ลัด', + 'shortcuts_intro' => 'แป้นพิมพ์ลัดต่อไปนี้ใช้งานได้ในตัวแก้ไข:', + 'windows_linux' => '(Windows/Linux)', + 'mac' => '(Mac)', + 'description' => 'คำอธิบาย', +]; diff --git a/lang/th/entities.php b/lang/th/entities.php new file mode 100644 index 000000000..1f0f30ec6 --- /dev/null +++ b/lang/th/entities.php @@ -0,0 +1,477 @@ + 'สร้างล่าสุด', + 'recently_created_pages' => 'หน้าที่สร้างล่าสุด', + 'recently_updated_pages' => 'หน้าที่แก้ไขล่าสุด', + 'recently_created_chapters' => 'บทที่สร้างล่าสุด', + 'recently_created_books' => 'หนังสือที่สร้างล่าสุด', + 'recently_created_shelves' => 'ชั้นวางที่สร้างล่าสุด', + 'recently_update' => 'แก้ไขล่าสุด', + 'recently_viewed' => 'ดูล่าสุด', + 'recent_activity' => 'กิจกรรมล่าสุด', + 'create_now' => 'สร้างตอนนี้', + 'revisions' => 'การแก้ไข', + 'meta_revision' => 'การแก้ไข #:revisionCount', + 'meta_created' => 'สร้างเมื่อ :timeLength', + 'meta_created_name' => 'สร้างเมื่อ :timeLength โดย :user', + 'meta_updated' => 'แก้ไขเมื่อ :timeLength', + 'meta_updated_name' => 'แก้ไขเมื่อ :timeLength โดย :user', + 'meta_owned_name' => 'เป็นของ :user', + 'meta_reference_count' => 'อ้างอิงโดย :count รายการ', + 'entity_select' => 'เลือกรายการ', + 'entity_select_lack_permission' => 'คุณไม่มีสิทธิ์เลือกรายการนี้', + 'images' => 'รูปภาพ', + 'my_recent_drafts' => 'ร่างล่าสุดของฉัน', + 'my_recently_viewed' => 'ที่ฉันดูล่าสุด', + 'my_most_viewed_favourites' => 'รายการโปรดที่ดูบ่อยที่สุด', + 'my_favourites' => 'รายการโปรดของฉัน', + 'no_pages_viewed' => 'คุณยังไม่ได้ดูหน้าใด', + 'no_pages_recently_created' => 'ยังไม่มีหน้าที่สร้างล่าสุด', + 'no_pages_recently_updated' => 'ยังไม่มีหน้าที่แก้ไขล่าสุด', + 'export' => 'ส่งออก', + 'export_html' => 'ไฟล์เว็บ (HTML)', + 'export_pdf' => 'ไฟล์ PDF', + 'export_text' => 'ไฟล์ข้อความธรรมดา', + 'export_md' => 'ไฟล์ Markdown', + 'export_zip' => 'ZIP แบบพกพา', + 'default_template' => 'แม่แบบหน้าเริ่มต้น', + 'default_template_explain' => 'กำหนดแม่แบบหน้าที่จะใช้เป็นเนื้อหาเริ่มต้นสำหรับหน้าที่สร้างในรายการนี้ โปรดทราบว่าจะใช้งานได้เฉพาะเมื่อผู้สร้างหน้ามีสิทธิ์ดูแม่แบบที่เลือก', + 'default_template_select' => 'เลือกแม่แบบหน้า', + 'import' => 'นำเข้า', + 'import_validate' => 'ตรวจสอบการนำเข้า', + 'import_desc' => 'นำเข้าหนังสือ บท และหน้าจากไฟล์ ZIP แบบพกพาจากระบบเดียวกันหรือต่างระบบ เลือกไฟล์ ZIP เพื่อดำเนินการต่อ หลังจากอัปโหลดและตรวจสอบแล้ว คุณจะสามารถกำหนดค่าและยืนยันการนำเข้าในขั้นตอนถัดไป', + 'import_zip_select' => 'เลือกไฟล์ ZIP ที่จะอัปโหลด', + 'import_zip_validation_errors' => 'พบข้อผิดพลาดขณะตรวจสอบไฟล์ ZIP:', + 'import_pending' => 'การนำเข้าที่รอดำเนินการ', + 'import_pending_none' => 'ยังไม่มีการนำเข้าที่เริ่มไว้', + 'import_continue' => 'ดำเนินการนำเข้าต่อ', + 'import_continue_desc' => 'ตรวจสอบเนื้อหาที่จะนำเข้าจากไฟล์ ZIP เมื่อพร้อมแล้ว ให้รันการนำเข้าเพื่อเพิ่มเนื้อหาเข้าสู่ระบบ ไฟล์ ZIP จะถูกลบโดยอัตโนมัติเมื่อนำเข้าสำเร็จ', + 'import_details' => 'รายละเอียดการนำเข้า', + 'import_run' => 'รันการนำเข้า', + 'import_size' => 'ขนาดไฟล์ ZIP :size', + 'import_uploaded_at' => 'อัปโหลดเมื่อ :relativeTime', + 'import_uploaded_by' => 'อัปโหลดโดย', + 'import_location' => 'ตำแหน่งนำเข้า', + 'import_location_desc' => 'เลือกตำแหน่งปลายทางสำหรับเนื้อหาที่นำเข้า คุณต้องมีสิทธิ์สร้างเนื้อหาในตำแหน่งที่เลือก', + 'import_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบการนำเข้านี้?', + 'import_delete_desc' => 'การดำเนินการนี้จะลบไฟล์ ZIP ที่อัปโหลดไว้และไม่สามารถยกเลิกได้', + 'import_errors' => 'ข้อผิดพลาดในการนำเข้า', + 'import_errors_desc' => 'เกิดข้อผิดพลาดต่อไปนี้ระหว่างการนำเข้า:', + 'breadcrumb_siblings_for_page' => 'นำทางไปยังหน้าที่อยู่ระดับเดียวกัน', + 'breadcrumb_siblings_for_chapter' => 'นำทางไปยังบทที่อยู่ระดับเดียวกัน', + 'breadcrumb_siblings_for_book' => 'นำทางไปยังหนังสือที่อยู่ระดับเดียวกัน', + 'breadcrumb_siblings_for_bookshelf' => 'นำทางไปยังชั้นวางที่อยู่ระดับเดียวกัน', + + // Permissions and restrictions + 'permissions' => 'สิทธิ์', + 'permissions_desc' => 'ตั้งค่าสิทธิ์ที่นี่เพื่อแทนที่สิทธิ์เริ่มต้นที่กำหนดโดยบทบาทผู้ใช้', + 'permissions_book_cascade' => 'สิทธิ์ที่กำหนดบนหนังสือจะส่งต่อไปยังบทและหน้าลูกโดยอัตโนมัติ เว้นแต่มีการกำหนดสิทธิ์ของตัวเองไว้', + 'permissions_chapter_cascade' => 'สิทธิ์ที่กำหนดบนบทจะส่งต่อไปยังหน้าลูกโดยอัตโนมัติ เว้นแต่มีการกำหนดสิทธิ์ของตัวเองไว้', + 'permissions_save' => 'บันทึกสิทธิ์', + 'permissions_owner' => 'เจ้าของ', + 'permissions_role_everyone_else' => 'ทุกคนที่เหลือ', + 'permissions_role_everyone_else_desc' => 'ตั้งค่าสิทธิ์สำหรับบทบาทที่ไม่ได้กำหนดไว้โดยเฉพาะ', + 'permissions_role_override' => 'แทนที่สิทธิ์สำหรับบทบาท', + 'permissions_inherit_defaults' => 'รับสิทธิ์เริ่มต้น', + + // Search + 'search_results' => 'ผลการค้นหา', + 'search_total_results_found' => 'พบ :count ผลลัพธ์', + 'search_clear' => 'ล้างการค้นหา', + 'search_no_pages' => 'ไม่มีหน้าที่ตรงกับการค้นหานี้', + 'search_for_term' => 'ค้นหา :term', + 'search_more' => 'ผลลัพธ์เพิ่มเติม', + 'search_advanced' => 'ค้นหาขั้นสูง', + 'search_terms' => 'คำค้นหา', + 'search_content_type' => 'ประเภทเนื้อหา', + 'search_exact_matches' => 'ตรงทั้งหมด', + 'search_tags' => 'ค้นหาด้วยแท็ก', + 'search_options' => 'ตัวเลือก', + 'search_viewed_by_me' => 'ที่ฉันดูแล้ว', + 'search_not_viewed_by_me' => 'ที่ฉันยังไม่ได้ดู', + 'search_permissions_set' => 'มีการกำหนดสิทธิ์', + 'search_created_by_me' => 'ที่ฉันสร้าง', + 'search_updated_by_me' => 'ที่ฉันแก้ไข', + 'search_owned_by_me' => 'ที่ฉันเป็นเจ้าของ', + 'search_date_options' => 'ตัวเลือกวันที่', + 'search_updated_before' => 'แก้ไขก่อน', + 'search_updated_after' => 'แก้ไขหลัง', + 'search_created_before' => 'สร้างก่อน', + 'search_created_after' => 'สร้างหลัง', + 'search_set_date' => 'กำหนดวันที่', + 'search_update' => 'อัปเดตการค้นหา', + + // Shelves + 'shelf' => 'ชั้นวาง', + 'shelves' => 'ชั้นวาง', + 'x_shelves' => ':count ชั้นวาง', + 'shelves_empty' => 'ยังไม่มีชั้นวาง', + 'shelves_create' => 'สร้างชั้นวางใหม่', + 'shelves_popular' => 'ชั้นวางยอดนิยม', + 'shelves_new' => 'ชั้นวางใหม่', + 'shelves_new_action' => 'ชั้นวางใหม่', + 'shelves_popular_empty' => 'ชั้นวางที่ได้รับความนิยมมากที่สุดจะแสดงที่นี่', + 'shelves_new_empty' => 'ชั้นวางที่สร้างล่าสุดจะแสดงที่นี่', + 'shelves_save' => 'บันทึกชั้นวาง', + 'shelves_books' => 'หนังสือในชั้นวางนี้', + 'shelves_add_books' => 'เพิ่มหนังสือในชั้นวางนี้', + 'shelves_drag_books' => 'ลากหนังสือด้านล่างเพื่อเพิ่มในชั้นวางนี้', + 'shelves_empty_contents' => 'ชั้นวางนี้ยังไม่มีหนังสือ', + 'shelves_edit_and_assign' => 'แก้ไขชั้นวางเพื่อกำหนดหนังสือ', + 'shelves_edit_named' => 'แก้ไขชั้นวาง :name', + 'shelves_edit' => 'แก้ไขชั้นวาง', + 'shelves_delete' => 'ลบชั้นวาง', + 'shelves_delete_named' => 'ลบชั้นวาง :name', + 'shelves_delete_explain' => "การดำเนินการนี้จะลบชั้นวางชื่อ ':name' หนังสือภายในจะไม่ถูกลบ", + 'shelves_delete_confirmation' => 'คุณแน่ใจหรือไม่ว่าต้องการลบชั้นวางนี้?', + 'shelves_permissions' => 'สิทธิ์ชั้นวาง', + 'shelves_permissions_updated' => 'อัปเดตสิทธิ์ชั้นวางแล้ว', + 'shelves_permissions_active' => 'สิทธิ์ชั้นวางเปิดใช้งานอยู่', + 'shelves_permissions_cascade_warning' => 'สิทธิ์บนชั้นวางไม่ส่งต่อไปยังหนังสือภายในโดยอัตโนมัติ เนื่องจากหนังสือสามารถอยู่ในหลายชั้นวางได้ อย่างไรก็ตาม สามารถคัดลอกสิทธิ์ไปยังหนังสือลูกได้โดยใช้ตัวเลือกด้านล่าง', + 'shelves_permissions_create' => 'สิทธิ์สร้างชั้นวางจะใช้สำหรับการคัดลอกสิทธิ์ไปยังหนังสือลูกเท่านั้น ไม่ได้ควบคุมความสามารถในการสร้างหนังสือ', + 'shelves_copy_permissions_to_books' => 'คัดลอกสิทธิ์ไปยังหนังสือ', + 'shelves_copy_permissions' => 'คัดลอกสิทธิ์', + 'shelves_copy_permissions_explain' => 'การดำเนินการนี้จะนำการตั้งค่าสิทธิ์ปัจจุบันของชั้นวางนี้ไปใช้กับหนังสือทั้งหมดภายใน ก่อนเริ่มต้น ให้ตรวจสอบว่าบันทึกการเปลี่ยนแปลงสิทธิ์แล้ว', + 'shelves_copy_permission_success' => 'คัดลอกสิทธิ์ชั้นวางไปยัง :count หนังสือแล้ว', + + // Books + 'book' => 'หนังสือ', + 'books' => 'หนังสือ', + 'x_books' => ':count หนังสือ', + 'books_empty' => 'ยังไม่มีหนังสือ', + 'books_popular' => 'หนังสือยอดนิยม', + 'books_recent' => 'หนังสือล่าสุด', + 'books_new' => 'หนังสือใหม่', + 'books_new_action' => 'หนังสือใหม่', + 'books_popular_empty' => 'หนังสือที่ได้รับความนิยมมากที่สุดจะแสดงที่นี่', + 'books_new_empty' => 'หนังสือที่สร้างล่าสุดจะแสดงที่นี่', + 'books_create' => 'สร้างหนังสือใหม่', + 'books_delete' => 'ลบหนังสือ', + 'books_delete_named' => 'ลบหนังสือ :bookName', + 'books_delete_explain' => 'การดำเนินการนี้จะลบหนังสือชื่อ \':bookName\' หน้าและบทภายในทั้งหมดจะถูกลบด้วย', + 'books_delete_confirmation' => 'คุณแน่ใจหรือไม่ว่าต้องการลบหนังสือนี้?', + 'books_edit' => 'แก้ไขหนังสือ', + 'books_edit_named' => 'แก้ไขหนังสือ :bookName', + 'books_form_book_name' => 'ชื่อหนังสือ', + 'books_save' => 'บันทึกหนังสือ', + 'books_permissions' => 'สิทธิ์หนังสือ', + 'books_permissions_updated' => 'อัปเดตสิทธิ์หนังสือแล้ว', + 'books_empty_contents' => 'ยังไม่มีหน้าหรือบทในหนังสือนี้', + 'books_empty_create_page' => 'สร้างหน้าใหม่', + 'books_empty_sort_current_book' => 'จัดเรียงหนังสือนี้', + 'books_empty_add_chapter' => 'เพิ่มบท', + 'books_permissions_active' => 'สิทธิ์หนังสือเปิดใช้งานอยู่', + 'books_search_this' => 'ค้นหาในหนังสือนี้', + 'books_navigation' => 'การนำทางหนังสือ', + 'books_sort' => 'จัดเรียงเนื้อหาหนังสือ', + 'books_sort_desc' => 'ย้ายบทและหน้าภายในหนังสือเพื่อจัดเรียงใหม่ สามารถเพิ่มหนังสือเล่มอื่นเพื่อย้ายบทและหน้าระหว่างหนังสือได้ อาจตั้งกฎการจัดเรียงอัตโนมัติเพื่อจัดเรียงเนื้อหาโดยอัตโนมัติเมื่อมีการเปลี่ยนแปลง', + 'books_sort_auto_sort' => 'ตัวเลือกการจัดเรียงอัตโนมัติ', + 'books_sort_auto_sort_active' => 'การจัดเรียงอัตโนมัติเปิดใช้งาน: :sortName', + 'books_sort_auto_sort_creation_hint' => 'กฎการจัดเรียงอัตโนมัติสามารถสร้างได้ในหน้าตั้งค่า "รายการและการจัดเรียง" โดยผู้ใช้ที่มีสิทธิ์ที่เกี่ยวข้อง', + 'books_sort_named' => 'จัดเรียงหนังสือ :bookName', + 'books_sort_name' => 'จัดเรียงตามชื่อ', + 'books_sort_created' => 'จัดเรียงตามวันที่สร้าง', + 'books_sort_updated' => 'จัดเรียงตามวันที่แก้ไข', + 'books_sort_chapters_first' => 'บทอยู่ก่อน', + 'books_sort_chapters_last' => 'บทอยู่หลัง', + 'books_sort_show_other' => 'แสดงหนังสือเล่มอื่น', + 'books_sort_save' => 'บันทึกลำดับใหม่', + 'books_sort_show_other_desc' => 'เพิ่มหนังสือเล่มอื่นที่นี่เพื่อรวมในการจัดเรียง และอนุญาตให้จัดระเบียบข้ามหนังสือได้ง่าย', + 'books_sort_move_up' => 'เลื่อนขึ้น', + 'books_sort_move_down' => 'เลื่อนลง', + 'books_sort_move_prev_book' => 'ย้ายไปหนังสือก่อนหน้า', + 'books_sort_move_next_book' => 'ย้ายไปหนังสือถัดไป', + 'books_sort_move_prev_chapter' => 'ย้ายเข้าบทก่อนหน้า', + 'books_sort_move_next_chapter' => 'ย้ายเข้าบทถัดไป', + 'books_sort_move_book_start' => 'ย้ายไปต้นหนังสือ', + 'books_sort_move_book_end' => 'ย้ายไปท้ายหนังสือ', + 'books_sort_move_before_chapter' => 'ย้ายไปก่อนบท', + 'books_sort_move_after_chapter' => 'ย้ายไปหลังบท', + 'books_copy' => 'คัดลอกหนังสือ', + 'books_copy_success' => 'คัดลอกหนังสือสำเร็จแล้ว', + + // Chapters + 'chapter' => 'บท', + 'chapters' => 'บท', + 'x_chapters' => ':count บท', + 'chapters_popular' => 'บทยอดนิยม', + 'chapters_new' => 'บทใหม่', + 'chapters_create' => 'สร้างบทใหม่', + 'chapters_delete' => 'ลบบท', + 'chapters_delete_named' => 'ลบบท :chapterName', + 'chapters_delete_explain' => 'การดำเนินการนี้จะลบบทชื่อ \':chapterName\' หน้าทั้งหมดในบทนี้จะถูกลบด้วย', + 'chapters_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบบทนี้?', + 'chapters_edit' => 'แก้ไขบท', + 'chapters_edit_named' => 'แก้ไขบท :chapterName', + 'chapters_save' => 'บันทึกบท', + 'chapters_move' => 'ย้ายบท', + 'chapters_move_named' => 'ย้ายบท :chapterName', + 'chapters_copy' => 'คัดลอกบท', + 'chapters_copy_success' => 'คัดลอกบทสำเร็จแล้ว', + 'chapters_permissions' => 'สิทธิ์บท', + 'chapters_empty' => 'ยังไม่มีหน้าในบทนี้', + 'chapters_permissions_active' => 'สิทธิ์บทเปิดใช้งานอยู่', + 'chapters_permissions_success' => 'อัปเดตสิทธิ์บทแล้ว', + 'chapters_search_this' => 'ค้นหาในบทนี้', + 'chapter_sort_book' => 'จัดเรียงหนังสือ', + + // Pages + 'page' => 'หน้า', + 'pages' => 'หน้า', + 'x_pages' => ':count หน้า', + 'pages_popular' => 'หน้ายอดนิยม', + 'pages_new' => 'หน้าใหม่', + 'pages_attachments' => 'ไฟล์แนบ', + 'pages_navigation' => 'การนำทางหน้า', + 'pages_delete' => 'ลบหน้า', + 'pages_delete_named' => 'ลบหน้า :pageName', + 'pages_delete_draft_named' => 'ลบร่างหน้า :pageName', + 'pages_delete_draft' => 'ลบร่างหน้า', + 'pages_delete_success' => 'ลบหน้าแล้ว', + 'pages_delete_draft_success' => 'ลบร่างหน้าแล้ว', + 'pages_delete_warning_template' => 'หน้านี้ถูกใช้งานเป็นแม่แบบหน้าเริ่มต้นของหนังสือหรือบท หลังจากลบหน้านี้ หนังสือหรือบทดังกล่าวจะไม่มีแม่แบบหน้าเริ่มต้น', + 'pages_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบหน้านี้?', + 'pages_delete_draft_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบร่างหน้านี้?', + 'pages_editing_named' => 'กำลังแก้ไขหน้า :pageName', + 'pages_edit_draft_options' => 'ตัวเลือกร่าง', + 'pages_edit_save_draft' => 'บันทึกร่าง', + 'pages_edit_draft' => 'แก้ไขร่างหน้า', + 'pages_editing_draft' => 'กำลังแก้ไขร่าง', + 'pages_editing_page' => 'กำลังแก้ไขหน้า', + 'pages_edit_draft_save_at' => 'บันทึกร่างเมื่อ ', + 'pages_edit_delete_draft' => 'ลบร่าง', + 'pages_edit_delete_draft_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบร่างหน้านี้? การเปลี่ยนแปลงทั้งหมดตั้งแต่บันทึกครั้งล่าสุดจะสูญหาย และตัวแก้ไขจะโหลดเนื้อหาที่บันทึกล่าสุด', + 'pages_edit_discard_draft' => 'ยกเลิกร่าง', + 'pages_edit_switch_to_markdown' => 'เปลี่ยนไปใช้ตัวแก้ไข Markdown', + 'pages_edit_switch_to_markdown_clean' => '(เนื้อหาสะอาด)', + 'pages_edit_switch_to_markdown_stable' => '(เนื้อหาเสถียร)', + 'pages_edit_switch_to_wysiwyg' => 'เปลี่ยนไปใช้ตัวแก้ไข WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg' => 'เปลี่ยนไปใช้ WYSIWYG ใหม่', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(กำลังทดสอบ Beta)', + 'pages_edit_set_changelog' => 'ตั้งค่า Changelog', + 'pages_edit_enter_changelog_desc' => 'กรอกคำอธิบายสั้นๆ เกี่ยวกับการเปลี่ยนแปลงที่ทำ', + 'pages_edit_enter_changelog' => 'กรอก Changelog', + 'pages_editor_switch_title' => 'เปลี่ยนตัวแก้ไข', + 'pages_editor_switch_are_you_sure' => 'คุณแน่ใจหรือไม่ว่าต้องการเปลี่ยนตัวแก้ไขสำหรับหน้านี้?', + 'pages_editor_switch_consider_following' => 'โปรดพิจารณาสิ่งต่อไปนี้เมื่อเปลี่ยนตัวแก้ไข:', + 'pages_editor_switch_consideration_a' => 'เมื่อบันทึกแล้ว ตัวแก้ไขใหม่จะถูกใช้โดยผู้แก้ไขในอนาคต รวมถึงผู้ที่อาจไม่สามารถเปลี่ยนประเภทตัวแก้ไขได้เอง', + 'pages_editor_switch_consideration_b' => 'อาจทำให้รายละเอียดและไวยากรณ์บางอย่างสูญหายในบางสถานการณ์', + 'pages_editor_switch_consideration_c' => 'การเปลี่ยนแปลงแท็กหรือ changelog ที่ทำตั้งแต่บันทึกครั้งล่าสุดจะไม่ถูกบันทึกหลังจากเปลี่ยนนี้', + 'pages_save' => 'บันทึกหน้า', + 'pages_title' => 'ชื่อหน้า', + 'pages_name' => 'ชื่อหน้า', + 'pages_md_editor' => 'ตัวแก้ไข', + 'pages_md_preview' => 'ดูตัวอย่าง', + 'pages_md_insert_image' => 'แทรกรูปภาพ', + 'pages_md_insert_link' => 'แทรกลิงก์รายการ', + 'pages_md_insert_drawing' => 'แทรกภาพวาด', + 'pages_md_show_preview' => 'แสดงตัวอย่าง', + 'pages_md_sync_scroll' => 'ซิงค์การเลื่อนหน้าตัวอย่าง', + 'pages_md_plain_editor' => 'ตัวแก้ไขข้อความธรรมดา', + 'pages_drawing_unsaved' => 'พบภาพวาดที่ยังไม่บันทึก', + 'pages_drawing_unsaved_confirm' => 'พบข้อมูลภาพวาดที่ยังไม่บันทึกจากการบันทึกที่ล้มเหลวก่อนหน้า ต้องการกู้คืนและแก้ไขภาพวาดที่ยังไม่บันทึกนี้ต่อหรือไม่?', + 'pages_not_in_chapter' => 'หน้านี้ไม่ได้อยู่ในบท', + 'pages_move' => 'ย้ายหน้า', + 'pages_copy' => 'คัดลอกหน้า', + 'pages_copy_desination' => 'ปลายทางการคัดลอก', + 'pages_copy_success' => 'คัดลอกหน้าสำเร็จแล้ว', + 'pages_permissions' => 'สิทธิ์หน้า', + 'pages_permissions_success' => 'อัปเดตสิทธิ์หน้าแล้ว', + 'pages_revision' => 'การแก้ไข', + 'pages_revisions' => 'การแก้ไขหน้า', + 'pages_revisions_desc' => 'แสดงรายการการแก้ไขทั้งหมดของหน้านี้ คุณสามารถดูย้อนหลัง เปรียบเทียบ และกู้คืนเวอร์ชันเก่าได้หากมีสิทธิ์ ประวัติทั้งหมดอาจไม่แสดงครบเนื่องจากการตั้งค่าระบบอาจลบการแก้ไขเก่าโดยอัตโนมัติ', + 'pages_revisions_named' => 'การแก้ไขหน้าสำหรับ :pageName', + 'pages_revision_named' => 'การแก้ไขหน้าสำหรับ :pageName', + 'pages_revision_restored_from' => 'กู้คืนจาก #:id; :summary', + 'pages_revisions_created_by' => 'สร้างโดย', + 'pages_revisions_date' => 'วันที่แก้ไข', + 'pages_revisions_number' => '#', + 'pages_revisions_sort_number' => 'หมายเลขการแก้ไข', + 'pages_revisions_numbered' => 'การแก้ไข #:id', + 'pages_revisions_numbered_changes' => 'การเปลี่ยนแปลงในการแก้ไข #:id', + 'pages_revisions_editor' => 'ประเภทตัวแก้ไข', + 'pages_revisions_changelog' => 'Changelog', + 'pages_revisions_changes' => 'การเปลี่ยนแปลง', + 'pages_revisions_current' => 'เวอร์ชันปัจจุบัน', + 'pages_revisions_preview' => 'ดูตัวอย่าง', + 'pages_revisions_restore' => 'กู้คืน', + 'pages_revisions_none' => 'หน้านี้ยังไม่มีการแก้ไข', + 'pages_copy_link' => 'คัดลอกลิงก์', + 'pages_edit_content_link' => 'ข้ามไปยังส่วนในตัวแก้ไข', + 'pages_pointer_enter_mode' => 'เข้าสู่โหมดเลือกส่วน', + 'pages_pointer_label' => 'ตัวเลือกส่วนหน้า', + 'pages_pointer_permalink' => 'ลิงก์ถาวรส่วนหน้า', + 'pages_pointer_include_tag' => 'แท็กรวมส่วนหน้า', + 'pages_pointer_toggle_link' => 'โหมดลิงก์ถาวร กดเพื่อแสดงแท็กรวม', + 'pages_pointer_toggle_include' => 'โหมดแท็กรวม กดเพื่อแสดงลิงก์ถาวร', + 'pages_permissions_active' => 'สิทธิ์หน้าเปิดใช้งานอยู่', + 'pages_initial_revision' => 'เผยแพร่ครั้งแรก', + 'pages_references_update_revision' => 'ระบบอัปเดตลิงก์ภายในอัตโนมัติ', + 'pages_initial_name' => 'หน้าใหม่', + 'pages_editing_draft_notification' => 'คุณกำลังแก้ไขร่างที่บันทึกล่าสุดเมื่อ :timeDiff', + 'pages_draft_edited_notification' => 'หน้านี้ได้รับการอัปเดตตั้งแต่นั้นมา แนะนำให้ยกเลิกร่างนี้', + 'pages_draft_page_changed_since_creation' => 'หน้านี้ได้รับการอัปเดตตั้งแต่สร้างร่างนี้ แนะนำให้ยกเลิกร่างหรือระมัดระวังไม่ให้เขียนทับการเปลี่ยนแปลงของหน้า', + 'pages_draft_edit_active' => [ + 'start_a' => 'มีผู้ใช้ :count คนกำลังแก้ไขหน้านี้', + 'start_b' => ':userName กำลังแก้ไขหน้านี้', + 'time_a' => 'ตั้งแต่อัปเดตหน้าล่าสุด', + 'time_b' => 'ใน :minCount นาทีที่ผ่านมา', + 'message' => ':start :time โปรดระวังอย่าเขียนทับการแก้ไขของกันและกัน!', + ], + 'pages_draft_discarded' => 'ยกเลิกร่างแล้ว! ตัวแก้ไขได้รับการอัปเดตด้วยเนื้อหาปัจจุบันของหน้า', + 'pages_draft_deleted' => 'ลบร่างแล้ว! ตัวแก้ไขได้รับการอัปเดตด้วยเนื้อหาปัจจุบันของหน้า', + 'pages_specific' => 'หน้าเฉพาะ', + 'pages_is_template' => 'แม่แบบหน้า', + + // Editor Sidebar + 'toggle_sidebar' => 'แสดง/ซ่อนแถบด้านข้าง', + 'page_tags' => 'แท็กหน้า', + 'chapter_tags' => 'แท็กบท', + 'book_tags' => 'แท็กหนังสือ', + 'shelf_tags' => 'แท็กชั้นวาง', + 'tag' => 'แท็ก', + 'tags' => 'แท็ก', + 'tags_index_desc' => 'แท็กสามารถนำไปใช้กับเนื้อหาในระบบเพื่อจัดหมวดหมู่แบบยืดหยุ่น แท็กสามารถมีทั้งคีย์และค่า โดยค่าไม่บังคับ เมื่อกำหนดแล้ว สามารถค้นหาเนื้อหาด้วยชื่อและค่าของแท็กได้', + 'tag_name' => 'ชื่อแท็ก', + 'tag_value' => 'ค่าแท็ก (ไม่บังคับ)', + 'tags_explain' => "เพิ่มแท็กเพื่อจัดหมวดหมู่เนื้อหาให้ดีขึ้น \n คุณสามารถกำหนดค่าให้แท็กเพื่อการจัดระเบียบที่ละเอียดขึ้น", + 'tags_add' => 'เพิ่มแท็ก', + 'tags_remove' => 'ลบแท็กนี้', + 'tags_usages' => 'การใช้งานแท็กทั้งหมด', + 'tags_assigned_pages' => 'กำหนดให้หน้า', + 'tags_assigned_chapters' => 'กำหนดให้บท', + 'tags_assigned_books' => 'กำหนดให้หนังสือ', + 'tags_assigned_shelves' => 'กำหนดให้ชั้นวาง', + 'tags_x_unique_values' => ':count ค่าที่ไม่ซ้ำ', + 'tags_all_values' => 'ค่าทั้งหมด', + 'tags_view_tags' => 'ดูแท็ก', + 'tags_view_existing_tags' => 'ดูแท็กที่มีอยู่', + 'tags_list_empty_hint' => 'แท็กสามารถกำหนดได้ผ่านแถบด้านข้างของตัวแก้ไขหน้า หรือขณะแก้ไขรายละเอียดของหนังสือ บท หรือชั้นวาง', + 'attachments' => 'ไฟล์แนบ', + 'attachments_explain' => 'อัปโหลดไฟล์หรือแนบลิงก์เพื่อแสดงในหน้านี้ จะมองเห็นได้ในแถบด้านข้างของหน้า', + 'attachments_explain_instant_save' => 'การเปลี่ยนแปลงที่นี่จะบันทึกทันที', + 'attachments_upload' => 'อัปโหลดไฟล์', + 'attachments_link' => 'แนบลิงก์', + 'attachments_upload_drop' => 'หรือคุณสามารถลากและวางไฟล์ที่นี่เพื่ออัปโหลดเป็นไฟล์แนบ', + 'attachments_set_link' => 'ตั้งค่าลิงก์', + 'attachments_delete' => 'คุณแน่ใจหรือไม่ว่าต้องการลบไฟล์แนบนี้?', + 'attachments_dropzone' => 'วางไฟล์ที่นี่เพื่ออัปโหลด', + 'attachments_no_files' => 'ยังไม่มีไฟล์ที่อัปโหลด', + 'attachments_explain_link' => 'คุณสามารถแนบลิงก์แทนการอัปโหลดไฟล์ได้ ลิงก์อาจเป็นลิงก์ไปยังหน้าอื่นหรือไฟล์ในคลาวด์', + 'attachments_link_name' => 'ชื่อลิงก์', + 'attachment_link' => 'ลิงก์ไฟล์แนบ', + 'attachments_link_url' => 'ลิงก์ไปยังไฟล์', + 'attachments_link_url_hint' => 'URL ของเว็บไซต์หรือไฟล์', + 'attach' => 'แนบ', + 'attachments_insert_link' => 'เพิ่มลิงก์ไฟล์แนบในหน้า', + 'attachments_edit_file' => 'แก้ไขไฟล์', + 'attachments_edit_file_name' => 'ชื่อไฟล์', + 'attachments_edit_drop_upload' => 'วางไฟล์หรือคลิกที่นี่เพื่ออัปโหลดและแทนที่', + 'attachments_order_updated' => 'อัปเดตลำดับไฟล์แนบแล้ว', + 'attachments_updated_success' => 'อัปเดตรายละเอียดไฟล์แนบแล้ว', + 'attachments_deleted' => 'ลบไฟล์แนบแล้ว', + 'attachments_file_uploaded' => 'อัปโหลดไฟล์สำเร็จแล้ว', + 'attachments_file_updated' => 'อัปเดตไฟล์สำเร็จแล้ว', + 'attachments_link_attached' => 'แนบลิงก์ไปยังหน้าสำเร็จแล้ว', + 'templates' => 'แม่แบบ', + 'templates_set_as_template' => 'หน้านี้เป็นแม่แบบ', + 'templates_explain_set_as_template' => 'คุณสามารถตั้งหน้านี้เป็นแม่แบบเพื่อให้นำเนื้อหาไปใช้เมื่อสร้างหน้าอื่น ผู้ใช้อื่นจะสามารถใช้แม่แบบนี้ได้หากมีสิทธิ์ดูหน้านี้', + 'templates_replace_content' => 'แทนที่เนื้อหาหน้า', + 'templates_append_content' => 'ต่อท้ายเนื้อหาหน้า', + 'templates_prepend_content' => 'เพิ่มก่อนเนื้อหาหน้า', + + // Profile View + 'profile_user_for_x' => 'ผู้ใช้มาแล้ว :time', + 'profile_created_content' => 'เนื้อหาที่สร้าง', + 'profile_not_created_pages' => ':userName ยังไม่ได้สร้างหน้าใด', + 'profile_not_created_chapters' => ':userName ยังไม่ได้สร้างบทใด', + 'profile_not_created_books' => ':userName ยังไม่ได้สร้างหนังสือใด', + 'profile_not_created_shelves' => ':userName ยังไม่ได้สร้างชั้นวางใด', + + // Comments + 'comment' => 'ความคิดเห็น', + 'comments' => 'ความคิดเห็น', + 'comment_add' => 'เพิ่มความคิดเห็น', + 'comment_none' => 'ไม่มีความคิดเห็น', + 'comment_placeholder' => 'เขียนความคิดเห็นที่นี่', + 'comment_thread_count' => ':count เธรดความคิดเห็น', + 'comment_archived_count' => ':count ที่เก็บถาวร', + 'comment_archived_threads' => 'เธรดที่เก็บถาวร', + 'comment_save' => 'บันทึกความคิดเห็น', + 'comment_new' => 'ความคิดเห็นใหม่', + 'comment_created' => 'แสดงความคิดเห็นเมื่อ :createDiff', + 'comment_updated' => 'แก้ไขเมื่อ :updateDiff โดย :username', + 'comment_updated_indicator' => 'แก้ไขแล้ว', + 'comment_deleted_success' => 'ลบความคิดเห็นแล้ว', + 'comment_created_success' => 'เพิ่มความคิดเห็นแล้ว', + 'comment_updated_success' => 'แก้ไขความคิดเห็นแล้ว', + 'comment_archive_success' => 'เก็บถาวรความคิดเห็นแล้ว', + 'comment_unarchive_success' => 'ยกเลิกการเก็บถาวรความคิดเห็นแล้ว', + 'comment_view' => 'ดูความคิดเห็น', + 'comment_jump_to_thread' => 'ไปยังเธรด', + 'comment_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบความคิดเห็นนี้?', + 'comment_in_reply_to' => 'ตอบกลับ :commentId', + 'comment_reference' => 'อ้างอิง', + 'comment_reference_outdated' => '(ล้าสมัย)', + 'comment_editor_explain' => 'แสดงความคิดเห็นที่มีในหน้านี้ สามารถเพิ่มและจัดการความคิดเห็นได้เมื่อดูหน้าที่บันทึกแล้ว', + + // Revision + 'revision_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบการแก้ไขนี้?', + 'revision_restore_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการกู้คืนการแก้ไขนี้? เนื้อหาปัจจุบันของหน้าจะถูกแทนที่', + 'revision_cannot_delete_latest' => 'ไม่สามารถลบการแก้ไขล่าสุดได้', + + // Copy view + 'copy_consider' => 'โปรดพิจารณาสิ่งต่อไปนี้เมื่อคัดลอกเนื้อหา', + 'copy_consider_permissions' => 'การตั้งค่าสิทธิ์แบบกำหนดเองจะไม่ถูกคัดลอก', + 'copy_consider_owner' => 'คุณจะเป็นเจ้าของเนื้อหาที่คัดลอกทั้งหมด', + 'copy_consider_images' => 'ไฟล์รูปภาพในหน้าจะไม่ถูกทำสำเนา และรูปภาพต้นฉบับจะยังคงเชื่อมกับหน้าที่อัปโหลดไว้เดิม', + 'copy_consider_attachments' => 'ไฟล์แนบในหน้าจะไม่ถูกคัดลอก', + 'copy_consider_access' => 'การเปลี่ยนตำแหน่ง เจ้าของ หรือสิทธิ์อาจทำให้เนื้อหานี้เข้าถึงได้โดยผู้ที่ไม่เคยมีสิทธิ์มาก่อน', + + // Conversions + 'convert_to_shelf' => 'แปลงเป็นชั้นวาง', + 'convert_to_shelf_contents_desc' => 'คุณสามารถแปลงหนังสือนี้เป็นชั้นวางใหม่ที่มีเนื้อหาเดิม บทในหนังสือนี้จะถูกแปลงเป็นหนังสือใหม่ หากหนังสือนี้มีหน้าที่ไม่อยู่ในบท หนังสือนี้จะถูกเปลี่ยนชื่อและเก็บหน้าเหล่านั้น และจะกลายเป็นส่วนหนึ่งของชั้นวางใหม่', + 'convert_to_shelf_permissions_desc' => 'สิทธิ์ที่กำหนดบนหนังสือนี้จะถูกคัดลอกไปยังชั้นวางใหม่และหนังสือลูกทั้งหมดที่ไม่มีสิทธิ์ของตัวเอง โปรดทราบว่าสิทธิ์บนชั้นวางไม่ส่งต่อไปยังเนื้อหาภายในโดยอัตโนมัติเหมือนกับหนังสือ', + 'convert_book' => 'แปลงหนังสือ', + 'convert_book_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการแปลงหนังสือนี้?', + 'convert_undo_warning' => 'การดำเนินการนี้ยากที่จะยกเลิก', + 'convert_to_book' => 'แปลงเป็นหนังสือ', + 'convert_to_book_desc' => 'คุณสามารถแปลงบทนี้เป็นหนังสือใหม่ที่มีเนื้อหาเดิม สิทธิ์ที่กำหนดบนบทนี้จะถูกคัดลอกไปยังหนังสือใหม่ แต่สิทธิ์ที่รับมาจากหนังสือแม่จะไม่ถูกคัดลอก ซึ่งอาจทำให้การควบคุมการเข้าถึงเปลี่ยนแปลงได้', + 'convert_chapter' => 'แปลงบท', + 'convert_chapter_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการแปลงบทนี้?', + + // References + 'references' => 'การอ้างอิง', + 'references_none' => 'ยังไม่มีการอ้างอิงที่ติดตามไปยังรายการนี้', + 'references_to_desc' => 'แสดงรายการเนื้อหาทั้งหมดในระบบที่ลิงก์ไปยังรายการนี้', + + // Watch Options + 'watch' => 'ติดตาม', + 'watch_title_default' => 'การตั้งค่าเริ่มต้น', + 'watch_desc_default' => 'กลับไปใช้การตั้งค่าการแจ้งเตือนเริ่มต้นของคุณ', + 'watch_title_ignore' => 'ไม่สนใจ', + 'watch_desc_ignore' => 'ไม่รับการแจ้งเตือนใดๆ รวมถึงจากการตั้งค่าระดับผู้ใช้', + 'watch_title_new' => 'หน้าใหม่', + 'watch_desc_new' => 'แจ้งเตือนเมื่อมีการสร้างหน้าใหม่ในรายการนี้', + 'watch_title_updates' => 'การอัปเดตหน้าทั้งหมด', + 'watch_desc_updates' => 'แจ้งเตือนเมื่อมีหน้าใหม่และการแก้ไขหน้า', + 'watch_desc_updates_page' => 'แจ้งเตือนเมื่อมีการแก้ไขหน้า', + 'watch_title_comments' => 'การอัปเดตและความคิดเห็นทั้งหมด', + 'watch_desc_comments' => 'แจ้งเตือนเมื่อมีหน้าใหม่ การแก้ไขหน้า และความคิดเห็นใหม่', + 'watch_desc_comments_page' => 'แจ้งเตือนเมื่อมีการแก้ไขหน้าและความคิดเห็นใหม่', + 'watch_change_default' => 'เปลี่ยนการตั้งค่าการแจ้งเตือนเริ่มต้น', + 'watch_detail_ignore' => 'ไม่รับการแจ้งเตือน', + 'watch_detail_new' => 'ติดตามหน้าใหม่', + 'watch_detail_updates' => 'ติดตามหน้าใหม่และการอัปเดต', + 'watch_detail_comments' => 'ติดตามหน้าใหม่ การอัปเดต และความคิดเห็น', + 'watch_detail_parent_book' => 'ติดตามผ่านหนังสือแม่', + 'watch_detail_parent_book_ignore' => 'ไม่รับการแจ้งเตือนผ่านหนังสือแม่', + 'watch_detail_parent_chapter' => 'ติดตามผ่านบทแม่', + 'watch_detail_parent_chapter_ignore' => 'ไม่รับการแจ้งเตือนผ่านบทแม่', +]; diff --git a/lang/th/errors.php b/lang/th/errors.php new file mode 100644 index 000000000..61f1ed82c --- /dev/null +++ b/lang/th/errors.php @@ -0,0 +1,135 @@ + 'คุณไม่มีสิทธิ์เข้าถึงหน้าที่ร้องขอ', + 'permissionJson' => 'คุณไม่มีสิทธิ์ดำเนินการที่ร้องขอ', + + // Auth + 'error_user_exists_different_creds' => 'มีผู้ใช้ที่ใช้อีเมล :email อยู่แล้วแต่ใช้ข้อมูลประจำตัวต่างกัน', + 'auth_pre_register_theme_prevention' => 'ไม่สามารถลงทะเบียนบัญชีผู้ใช้สำหรับข้อมูลที่ให้มาได้', + 'email_already_confirmed' => 'ยืนยันอีเมลแล้ว กรุณาลองเข้าสู่ระบบ', + 'email_confirmation_invalid' => 'โทเค็นยืนยันนี้ไม่ถูกต้องหรือถูกใช้ไปแล้ว กรุณาลองลงทะเบียนใหม่', + 'email_confirmation_expired' => 'โทเค็นยืนยันหมดอายุแล้ว ส่งอีเมลยืนยันใหม่ให้แล้ว', + 'email_confirmation_awaiting' => 'ที่อยู่อีเมลของบัญชีที่ใช้งานอยู่ต้องได้รับการยืนยัน', + 'ldap_fail_anonymous' => 'การเข้าถึง LDAP ล้มเหลวโดยใช้การเชื่อมต่อแบบไม่ระบุตัวตน', + 'ldap_fail_authed' => 'การเข้าถึง LDAP ล้มเหลวโดยใช้ข้อมูล dn และรหัสผ่านที่กำหนด', + 'ldap_extension_not_installed' => 'ไม่ได้ติดตั้ง PHP extension สำหรับ LDAP', + 'ldap_cannot_connect' => 'ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ LDAP ได้ การเชื่อมต่อเริ่มต้นล้มเหลว', + 'saml_already_logged_in' => 'เข้าสู่ระบบแล้ว', + 'saml_no_email_address' => 'ไม่พบที่อยู่อีเมลสำหรับผู้ใช้นี้ในข้อมูลที่ระบบยืนยันตัวตนภายนอกส่งมา', + 'saml_invalid_response_id' => 'คำขอจากระบบยืนยันตัวตนภายนอกไม่ได้รับการยอมรับจากกระบวนการที่เริ่มต้นโดยแอปพลิเคชันนี้ การนำทางย้อนกลับหลังเข้าสู่ระบบอาจทำให้เกิดปัญหานี้', + 'saml_fail_authed' => 'การเข้าสู่ระบบด้วย :system ล้มเหลว ระบบไม่ได้ให้การอนุญาตที่สำเร็จ', + 'oidc_already_logged_in' => 'เข้าสู่ระบบแล้ว', + 'oidc_no_email_address' => 'ไม่พบที่อยู่อีเมลสำหรับผู้ใช้นี้ในข้อมูลที่ระบบยืนยันตัวตนภายนอกส่งมา', + 'oidc_fail_authed' => 'การเข้าสู่ระบบด้วย :system ล้มเหลว ระบบไม่ได้ให้การอนุญาตที่สำเร็จ', + 'social_no_action_defined' => 'ไม่ได้กำหนดการดำเนินการ', + 'social_login_bad_response' => "เกิดข้อผิดพลาดระหว่างเข้าสู่ระบบด้วย :socialAccount: \n:error", + 'social_account_in_use' => 'บัญชี :socialAccount นี้ถูกใช้งานแล้ว ลองเข้าสู่ระบบผ่านตัวเลือก :socialAccount', + 'social_account_email_in_use' => 'อีเมล :email ถูกใช้งานแล้ว หากมีบัญชีอยู่แล้ว คุณสามารถเชื่อมต่อบัญชี :socialAccount จากการตั้งค่าโปรไฟล์ได้', + 'social_account_existing' => ':socialAccount นี้เชื่อมต่อกับโปรไฟล์ของคุณแล้ว', + 'social_account_already_used_existing' => 'บัญชี :socialAccount นี้ถูกใช้งานโดยผู้ใช้อื่นแล้ว', + 'social_account_not_used' => 'บัญชี :socialAccount นี้ไม่ได้เชื่อมต่อกับผู้ใช้ใด กรุณาแนบในการตั้งค่าโปรไฟล์', + 'social_account_register_instructions' => 'หากยังไม่มีบัญชี คุณสามารถลงทะเบียนโดยใช้ตัวเลือก :socialAccount', + 'social_driver_not_found' => 'ไม่พบ Social driver', + 'social_driver_not_configured' => 'การตั้งค่า Social ของ :socialAccount ไม่ถูกต้อง', + 'invite_token_expired' => 'ลิงก์เชิญนี้หมดอายุแล้ว คุณสามารถลองรีเซ็ตรหัสผ่านบัญชีแทนได้', + 'login_user_not_found' => 'ไม่พบผู้ใช้สำหรับการดำเนินการนี้', + + // System + 'path_not_writable' => 'ไม่สามารถอัปโหลดไปยังพาธ :filePath ได้ โปรดตรวจสอบว่าเซิร์ฟเวอร์มีสิทธิ์เขียนได้', + 'cannot_get_image_from_url' => 'ไม่สามารถดึงรูปภาพจาก :url ได้', + 'cannot_create_thumbs' => 'เซิร์ฟเวอร์ไม่สามารถสร้างภาพย่อได้ โปรดตรวจสอบว่าติดตั้ง PHP extension GD แล้ว', + 'server_upload_limit' => 'เซิร์ฟเวอร์ไม่อนุญาตให้อัปโหลดไฟล์ขนาดนี้ กรุณาลองใช้ไฟล์ขนาดเล็กกว่า', + 'server_post_limit' => 'เซิร์ฟเวอร์ไม่สามารถรับข้อมูลในปริมาณที่กำหนดได้ ลองใหม่ด้วยข้อมูลน้อยลงหรือไฟล์ขนาดเล็กกว่า', + 'uploaded' => 'เซิร์ฟเวอร์ไม่อนุญาตให้อัปโหลดไฟล์ขนาดนี้ กรุณาลองใช้ไฟล์ขนาดเล็กกว่า', + + // Drawing & Images + 'image_upload_error' => 'เกิดข้อผิดพลาดขณะอัปโหลดรูปภาพ', + 'image_upload_type_error' => 'ประเภทรูปภาพที่อัปโหลดไม่ถูกต้อง', + 'image_upload_replace_type' => 'การแทนที่ไฟล์รูปภาพต้องใช้ประเภทเดียวกัน', + 'image_upload_memory_limit' => 'ไม่สามารถจัดการการอัปโหลดรูปภาพและ/หรือสร้างภาพย่อได้เนื่องจากทรัพยากรระบบไม่เพียงพอ', + 'image_thumbnail_memory_limit' => 'ไม่สามารถสร้างขนาดรูปภาพต่างๆ ได้เนื่องจากทรัพยากรระบบไม่เพียงพอ', + 'image_gallery_thumbnail_memory_limit' => 'ไม่สามารถสร้างภาพย่อแกลเลอรีได้เนื่องจากทรัพยากรระบบไม่เพียงพอ', + 'drawing_data_not_found' => 'ไม่สามารถโหลดข้อมูลภาพวาดได้ ไฟล์ภาพวาดอาจไม่มีอยู่แล้วหรือคุณไม่มีสิทธิ์เข้าถึง', + + // Attachments + 'attachment_not_found' => 'ไม่พบไฟล์แนบ', + 'attachment_upload_error' => 'เกิดข้อผิดพลาดขณะอัปโหลดไฟล์แนบ', + + // Pages + 'page_draft_autosave_fail' => 'บันทึกร่างล้มเหลว โปรดตรวจสอบการเชื่อมต่ออินเทอร์เน็ตก่อนบันทึกหน้านี้', + 'page_draft_delete_fail' => 'ลบร่างหน้าและดึงเนื้อหาที่บันทึกปัจจุบันล้มเหลว', + 'page_custom_home_deletion' => 'ไม่สามารถลบหน้าได้ในขณะที่ตั้งเป็นหน้าแรก', + + // Entities + 'entity_not_found' => 'ไม่พบรายการ', + 'bookshelf_not_found' => 'ไม่พบชั้นวาง', + 'book_not_found' => 'ไม่พบหนังสือ', + 'page_not_found' => 'ไม่พบหน้า', + 'chapter_not_found' => 'ไม่พบบท', + 'selected_book_not_found' => 'ไม่พบหนังสือที่เลือก', + 'selected_book_chapter_not_found' => 'ไม่พบหนังสือหรือบทที่เลือก', + 'guests_cannot_save_drafts' => 'ผู้เยี่ยมชมไม่สามารถบันทึกร่างได้', + + // Users + 'users_cannot_delete_only_admin' => 'ไม่สามารถลบผู้ดูแลระบบคนเดียวได้', + 'users_cannot_delete_guest' => 'ไม่สามารถลบผู้ใช้แบบผู้เยี่ยมชมได้', + 'users_could_not_send_invite' => 'ไม่สามารถสร้างผู้ใช้ได้เนื่องจากส่งอีเมลเชิญล้มเหลว', + + // Roles + 'role_cannot_be_edited' => 'บทบาทนี้ไม่สามารถแก้ไขได้', + 'role_system_cannot_be_deleted' => 'บทบาทนี้เป็นบทบาทระบบและไม่สามารถลบได้', + 'role_registration_default_cannot_delete' => 'บทบาทนี้ไม่สามารถลบได้ในขณะที่ตั้งเป็นบทบาทลงทะเบียนเริ่มต้น', + 'role_cannot_remove_only_admin' => 'ผู้ใช้นี้เป็นผู้ใช้คนเดียวที่ได้รับบทบาทผู้ดูแลระบบ กรุณากำหนดบทบาทผู้ดูแลระบบให้ผู้ใช้อื่นก่อนที่จะลบออกที่นี่', + + // Comments + 'comment_list' => 'เกิดข้อผิดพลาดขณะดึงความคิดเห็น', + 'cannot_add_comment_to_draft' => 'ไม่สามารถเพิ่มความคิดเห็นในร่างได้', + 'comment_add' => 'เกิดข้อผิดพลาดขณะเพิ่ม/อัปเดตความคิดเห็น', + 'comment_delete' => 'เกิดข้อผิดพลาดขณะลบความคิดเห็น', + 'empty_comment' => 'ไม่สามารถเพิ่มความคิดเห็นที่ว่างเปล่าได้', + + // Error pages + '404_page_not_found' => 'ไม่พบหน้า', + 'sorry_page_not_found' => 'ขออภัย ไม่พบหน้าที่คุณกำลังมองหา', + 'sorry_page_not_found_permission_warning' => 'หากคุณคาดว่าหน้านี้มีอยู่ คุณอาจไม่มีสิทธิ์ดูหน้านี้', + 'image_not_found' => 'ไม่พบรูปภาพ', + 'image_not_found_subtitle' => 'ขออภัย ไม่พบไฟล์รูปภาพที่คุณกำลังมองหา', + 'image_not_found_details' => 'หากคุณคาดว่ารูปภาพนี้มีอยู่ อาจถูกลบไปแล้ว', + 'return_home' => 'กลับไปหน้าแรก', + 'error_occurred' => 'เกิดข้อผิดพลาด', + 'app_down' => ':appName ไม่พร้อมใช้งานในขณะนี้', + 'back_soon' => 'จะกลับมาให้บริการเร็วๆ นี้', + + // Import + 'import_zip_cant_read' => 'ไม่สามารถอ่านไฟล์ ZIP ได้', + 'import_zip_cant_decode_data' => 'ไม่สามารถค้นหาและถอดรหัสเนื้อหา data.json ใน ZIP ได้', + 'import_zip_no_data' => 'ข้อมูลในไฟล์ ZIP ไม่มีเนื้อหาหนังสือ บท หรือหน้าที่คาดไว้', + 'import_zip_data_too_large' => 'เนื้อหา data.json ใน ZIP เกินขนาดอัปโหลดสูงสุดที่กำหนดในแอปพลิเคชัน', + 'import_validation_failed' => 'ตรวจสอบ ZIP นำเข้าล้มเหลวพร้อมข้อผิดพลาด:', + 'import_zip_failed_notification' => 'นำเข้าไฟล์ ZIP ล้มเหลว', + 'import_perms_books' => 'คุณขาดสิทธิ์ที่จำเป็นในการสร้างหนังสือ', + 'import_perms_chapters' => 'คุณขาดสิทธิ์ที่จำเป็นในการสร้างบท', + 'import_perms_pages' => 'คุณขาดสิทธิ์ที่จำเป็นในการสร้างหน้า', + 'import_perms_images' => 'คุณขาดสิทธิ์ที่จำเป็นในการสร้างรูปภาพ', + 'import_perms_attachments' => 'คุณขาดสิทธิ์ที่จำเป็นในการสร้างไฟล์แนบ', + + // API errors + 'api_no_authorization_found' => 'ไม่พบโทเค็นการอนุญาตในคำขอ', + 'api_bad_authorization_format' => 'พบโทเค็นการอนุญาตในคำขอแต่รูปแบบดูเหมือนไม่ถูกต้อง', + 'api_user_token_not_found' => 'ไม่พบ API token ที่ตรงกับโทเค็นการอนุญาตที่ให้มา', + 'api_incorrect_token_secret' => 'รหัสลับที่ให้มาสำหรับ API token ที่ใช้ไม่ถูกต้อง', + 'api_user_no_api_permission' => 'เจ้าของ API token ที่ใช้ไม่มีสิทธิ์เรียกใช้ API', + 'api_user_token_expired' => 'โทเค็นการอนุญาตที่ใช้หมดอายุแล้ว', + 'api_cookie_auth_only_get' => 'อนุญาตเฉพาะคำขอ GET เมื่อใช้ API ด้วยการยืนยันตัวตนแบบ cookie', + + // Settings & Maintenance + 'maintenance_test_email_failure' => 'เกิดข้อผิดพลาดขณะส่งอีเมลทดสอบ:', + + // HTTP errors + 'http_ssr_url_no_match' => 'URL ไม่ตรงกับโฮสต์ SSR ที่อนุญาตที่กำหนดค่าไว้', +]; diff --git a/lang/th/notifications.php b/lang/th/notifications.php new file mode 100644 index 000000000..661c83fdb --- /dev/null +++ b/lang/th/notifications.php @@ -0,0 +1,29 @@ + 'ความคิดเห็นใหม่ในหน้า: :pageName', + 'new_comment_intro' => 'มีผู้ใช้แสดงความคิดเห็นในหน้าใน :appName:', + 'new_page_subject' => 'หน้าใหม่: :pageName', + 'new_page_intro' => 'มีการสร้างหน้าใหม่ใน :appName:', + 'updated_page_subject' => 'แก้ไขหน้า: :pageName', + 'updated_page_intro' => 'มีการแก้ไขหน้าใน :appName:', + 'updated_page_debounce' => 'เพื่อป้องกันการแจ้งเตือนจำนวนมาก คุณจะไม่ได้รับการแจ้งเตือนสำหรับการแก้ไขเพิ่มเติมในหน้านี้โดยผู้แก้ไขคนเดิมในช่วงเวลาหนึ่ง', + 'comment_mention_subject' => 'คุณถูกกล่าวถึงในความคิดเห็นในหน้า: :pageName', + 'comment_mention_intro' => 'คุณถูกกล่าวถึงในความคิดเห็นใน :appName:', + + 'detail_page_name' => 'ชื่อหน้า:', + 'detail_page_path' => 'พาธหน้า:', + 'detail_commenter' => 'ผู้แสดงความคิดเห็น:', + 'detail_comment' => 'ความคิดเห็น:', + 'detail_created_by' => 'สร้างโดย:', + 'detail_updated_by' => 'แก้ไขโดย:', + + 'action_view_comment' => 'ดูความคิดเห็น', + 'action_view_page' => 'ดูหน้า', + + 'footer_reason' => 'คุณได้รับการแจ้งเตือนนี้เพราะ :link ครอบคลุมกิจกรรมประเภทนี้สำหรับรายการนี้', + 'footer_reason_link' => 'การตั้งค่าการแจ้งเตือนของคุณ', +]; diff --git a/lang/th/pagination.php b/lang/th/pagination.php new file mode 100644 index 000000000..c29e7d0d9 --- /dev/null +++ b/lang/th/pagination.php @@ -0,0 +1,12 @@ + '« ก่อนหน้า', + 'next' => 'ถัดไป »', + +]; diff --git a/lang/th/passwords.php b/lang/th/passwords.php new file mode 100644 index 000000000..3429c7763 --- /dev/null +++ b/lang/th/passwords.php @@ -0,0 +1,15 @@ + 'รหัสผ่านต้องมีอย่างน้อยแปดตัวอักษรและต้องตรงกับการยืนยัน', + 'user' => "ไม่พบผู้ใช้ที่ใช้ที่อยู่อีเมลนี้", + 'token' => 'โทเค็นรีเซ็ตรหัสผ่านไม่ถูกต้องสำหรับที่อยู่อีเมลนี้', + 'sent' => 'ส่งลิงก์รีเซ็ตรหัสผ่านไปยังอีเมลของคุณแล้ว!', + 'reset' => 'รีเซ็ตรหัสผ่านของคุณแล้ว!', + +]; diff --git a/lang/th/preferences.php b/lang/th/preferences.php new file mode 100644 index 000000000..56d57fc26 --- /dev/null +++ b/lang/th/preferences.php @@ -0,0 +1,52 @@ + 'บัญชีของฉัน', + + 'shortcuts' => 'แป้นพิมพ์ลัด', + 'shortcuts_interface' => 'การตั้งค่าแป้นพิมพ์ลัด UI', + 'shortcuts_toggle_desc' => 'คุณสามารถเปิดหรือปิดแป้นพิมพ์ลัดของระบบที่ใช้สำหรับการนำทางและการดำเนินการได้ที่นี่', + 'shortcuts_customize_desc' => 'คุณสามารถปรับแต่งแป้นพิมพ์ลัดแต่ละรายการด้านล่างได้ เพียงกดคีย์ผสมที่ต้องการหลังจากเลือกช่องป้อนข้อมูลสำหรับแป้นลัดนั้น', + 'shortcuts_toggle_label' => 'เปิดใช้งานแป้นพิมพ์ลัด', + 'shortcuts_section_navigation' => 'การนำทาง', + 'shortcuts_section_actions' => 'การดำเนินการทั่วไป', + 'shortcuts_save' => 'บันทึกแป้นพิมพ์ลัด', + 'shortcuts_overlay_desc' => 'หมายเหตุ: เมื่อเปิดใช้งานแป้นพิมพ์ลัด จะมีแผงช่วยเหลือที่เข้าถึงได้โดยกด "?" ซึ่งจะแสดงแป้นพิมพ์ลัดที่ใช้ได้สำหรับการดำเนินการที่แสดงอยู่บนหน้าจอ', + 'shortcuts_update_success' => 'อัปเดตการตั้งค่าแป้นพิมพ์ลัดแล้ว!', + 'shortcuts_overview_desc' => 'จัดการแป้นพิมพ์ลัดที่ใช้นำทางในส่วนติดต่อผู้ใช้ของระบบ', + + 'notifications' => 'การตั้งค่าการแจ้งเตือน', + 'notifications_desc' => 'ควบคุมการแจ้งเตือนทางอีเมลที่คุณได้รับเมื่อมีกิจกรรมบางอย่างในระบบ', + 'notifications_opt_own_page_changes' => 'แจ้งเตือนเมื่อมีการเปลี่ยนแปลงหน้าที่ฉันเป็นเจ้าของ', + 'notifications_opt_own_page_comments' => 'แจ้งเตือนเมื่อมีความคิดเห็นในหน้าที่ฉันเป็นเจ้าของ', + 'notifications_opt_comment_mentions' => 'แจ้งเตือนเมื่อฉันถูกกล่าวถึงในความคิดเห็น', + 'notifications_opt_comment_replies' => 'แจ้งเตือนเมื่อมีการตอบกลับความคิดเห็นของฉัน', + 'notifications_save' => 'บันทึกการตั้งค่า', + 'notifications_update_success' => 'อัปเดตการตั้งค่าการแจ้งเตือนแล้ว!', + 'notifications_watched' => 'รายการที่ติดตามและไม่สนใจ', + 'notifications_watched_desc' => 'รายการด้านล่างมีการตั้งค่าการติดตามแบบกำหนดเอง หากต้องการอัปเดตการตั้งค่า ให้ดูรายการนั้นแล้วค้นหาตัวเลือกการติดตามในแถบด้านข้าง', + + 'auth' => 'การเข้าถึงและความปลอดภัย', + 'auth_change_password' => 'เปลี่ยนรหัสผ่าน', + 'auth_change_password_desc' => 'เปลี่ยนรหัสผ่านที่ใช้เข้าสู่ระบบ ต้องมีความยาวอย่างน้อย 8 ตัวอักษร', + 'auth_change_password_success' => 'อัปเดตรหัสผ่านแล้ว!', + + 'profile' => 'รายละเอียดโปรไฟล์', + 'profile_desc' => 'จัดการรายละเอียดบัญชีที่แสดงตัวตนต่อผู้ใช้อื่น รวมถึงรายละเอียดที่ใช้สำหรับการสื่อสารและการปรับแต่งระบบ', + 'profile_view_public' => 'ดูโปรไฟล์สาธารณะ', + 'profile_name_desc' => 'กำหนดชื่อที่แสดงซึ่งจะมองเห็นได้โดยผู้ใช้อื่นในระบบผ่านกิจกรรมที่คุณดำเนินการและเนื้อหาที่คุณเป็นเจ้าของ', + 'profile_email_desc' => 'อีเมลนี้จะใช้สำหรับการแจ้งเตือนและการเข้าถึงระบบขึ้นอยู่กับการยืนยันตัวตนที่ใช้งานอยู่', + 'profile_email_no_permission' => 'ขออภัย คุณไม่มีสิทธิ์เปลี่ยนที่อยู่อีเมล หากต้องการเปลี่ยน กรุณาติดต่อผู้ดูแลระบบ', + 'profile_avatar_desc' => 'เลือกรูปภาพที่จะใช้แสดงตัวตนต่อผู้อื่นในระบบ รูปภาพควรเป็นรูปสี่เหลี่ยมจัตุรัสขนาดประมาณ 256px', + 'profile_admin_options' => 'ตัวเลือกสำหรับผู้ดูแลระบบ', + 'profile_admin_options_desc' => 'ตัวเลือกระดับผู้ดูแลระบบเพิ่มเติม เช่น การจัดการการกำหนดบทบาท สามารถพบได้ในพื้นที่ "การตั้งค่า > ผู้ใช้" ของแอปพลิเคชัน', + + 'delete_account' => 'ลบบัญชี', + 'delete_my_account' => 'ลบบัญชีของฉัน', + 'delete_my_account_desc' => 'การดำเนินการนี้จะลบบัญชีผู้ใช้ของคุณออกจากระบบทั้งหมด คุณจะไม่สามารถกู้คืนบัญชีหรือยกเลิกการดำเนินการนี้ได้ เนื้อหาที่คุณสร้าง เช่น หน้าที่สร้างและรูปภาพที่อัปโหลด จะยังคงอยู่', + 'delete_my_account_warning' => 'คุณแน่ใจหรือไม่ว่าต้องการลบบัญชีของคุณ?', +]; diff --git a/lang/th/settings.php b/lang/th/settings.php new file mode 100644 index 000000000..1b90d2a71 --- /dev/null +++ b/lang/th/settings.php @@ -0,0 +1,375 @@ + 'การตั้งค่า', + 'settings_save' => 'บันทึกการตั้งค่า', + 'system_version' => 'เวอร์ชันระบบ', + 'categories' => 'หมวดหมู่', + + // App Settings + 'app_customization' => 'การปรับแต่ง', + 'app_features_security' => 'คุณสมบัติและความปลอดภัย', + 'app_name' => 'ชื่อแอปพลิเคชัน', + 'app_name_desc' => 'ชื่อนี้จะแสดงในส่วนหัวและอีเมลที่ส่งโดยระบบ', + 'app_name_header' => 'แสดงชื่อในส่วนหัว', + 'app_public_access' => 'การเข้าถึงสาธารณะ', + 'app_public_access_desc' => 'การเปิดใช้งานตัวเลือกนี้จะอนุญาตให้ผู้เยี่ยมชมที่ไม่ได้เข้าสู่ระบบสามารถเข้าถึงเนื้อหาใน BookStack ของคุณได้', + 'app_public_access_desc_guest' => 'การเข้าถึงของผู้เยี่ยมชมสาธารณะสามารถควบคุมได้ผ่านผู้ใช้ "Guest"', + 'app_public_access_toggle' => 'อนุญาตการเข้าถึงสาธารณะ', + 'app_public_viewing' => 'อนุญาตให้ดูแบบสาธารณะ?', + 'app_secure_images' => 'การอัปโหลดรูปภาพแบบความปลอดภัยสูง', + 'app_secure_images_toggle' => 'เปิดใช้งานการอัปโหลดรูปภาพแบบความปลอดภัยสูง', + 'app_secure_images_desc' => 'เพื่อประสิทธิภาพ รูปภาพทั้งหมดเป็นสาธารณะ ตัวเลือกนี้จะเพิ่มสตริงสุ่มที่คาดเดาได้ยากหน้า URL รูปภาพ ตรวจสอบให้แน่ใจว่าไม่ได้เปิดใช้งานการแสดงรายการไดเรกทอรีเพื่อป้องกันการเข้าถึงที่ง่าย', + 'app_default_editor' => 'ตัวแก้ไขหน้าเริ่มต้น', + 'app_default_editor_desc' => 'เลือกตัวแก้ไขที่จะใช้โดยค่าเริ่มต้นเมื่อแก้ไขหน้าใหม่ ซึ่งสามารถแทนที่ได้ในระดับหน้าเมื่อมีสิทธิ์อนุญาต', + 'app_custom_html' => 'เนื้อหา HTML Head แบบกำหนดเอง', + 'app_custom_html_desc' => 'เนื้อหาที่เพิ่มที่นี่จะถูกแทรกที่ด้านล่างของส่วน ของทุกหน้า มีประโยชน์สำหรับการแทนที่สไตล์หรือเพิ่มโค้ด Analytics', + 'app_custom_html_disabled_notice' => 'เนื้อหา HTML head แบบกำหนดเองถูกปิดใช้งานในหน้าการตั้งค่านี้เพื่อให้สามารถเปลี่ยนแปลงที่ทำให้ใช้งานไม่ได้กลับคืนได้', + 'app_logo' => 'โลโก้แอปพลิเคชัน', + 'app_logo_desc' => 'ใช้ในแถบส่วนหัวของแอปพลิเคชัน รูปภาพควรมีความสูง 86px รูปภาพขนาดใหญ่จะถูกย่อขนาด', + 'app_icon' => 'ไอคอนแอปพลิเคชัน', + 'app_icon_desc' => 'ไอคอนนี้ใช้สำหรับแท็บเบราว์เซอร์และไอคอนทางลัด ควรเป็นรูปภาพ PNG สี่เหลี่ยมขนาด 256px', + 'app_homepage' => 'หน้าแรกของแอปพลิเคชัน', + 'app_homepage_desc' => 'เลือกมุมมองที่จะแสดงบนหน้าแรกแทนมุมมองเริ่มต้น สิทธิ์หน้าจะถูกละเว้นสำหรับหน้าที่เลือก', + 'app_homepage_select' => 'เลือกหน้า', + 'app_footer_links' => 'ลิงก์ส่วนท้าย', + 'app_footer_links_desc' => 'เพิ่มลิงก์เพื่อแสดงในส่วนท้ายของเว็บไซต์ จะแสดงที่ด้านล่างของส่วนใหญ่ของหน้า รวมถึงหน้าที่ไม่ต้องเข้าสู่ระบบ คุณสามารถใช้ป้ายชื่อ "trans::" เพื่อใช้การแปลที่ระบบกำหนดไว้ เช่น "trans::common.privacy_policy" จะให้ข้อความ "นโยบายความเป็นส่วนตัว"', + 'app_footer_links_label' => 'ป้ายชื่อลิงก์', + 'app_footer_links_url' => 'URL ลิงก์', + 'app_footer_links_add' => 'เพิ่มลิงก์ส่วนท้าย', + 'app_disable_comments' => 'ปิดใช้งานความคิดเห็น', + 'app_disable_comments_toggle' => 'ปิดใช้งานความคิดเห็น', + 'app_disable_comments_desc' => 'ปิดใช้งานความคิดเห็นในทุกหน้าในแอปพลิเคชัน
    ความคิดเห็นที่มีอยู่จะไม่ถูกแสดง', + + // Color settings + 'color_scheme' => 'โครงสีของแอปพลิเคชัน', + 'color_scheme_desc' => 'ตั้งค่าสีที่จะใช้ในส่วนติดต่อผู้ใช้ของแอปพลิเคชัน สามารถกำหนดค่าสีแยกกันสำหรับโหมดมืดและโหมดสว่างเพื่อให้เหมาะกับธีมและรับประกันการอ่านง่าย', + 'ui_colors_desc' => 'ตั้งค่าสีหลักและสีลิงก์เริ่มต้นของแอปพลิเคชัน สีหลักใช้สำหรับแบนเนอร์ส่วนหัว ปุ่ม และการตกแต่งส่วนติดต่อ สีลิงก์เริ่มต้นใช้สำหรับลิงก์และการดำเนินการที่เป็นข้อความ', + 'app_color' => 'สีหลัก', + 'link_color' => 'สีลิงก์เริ่มต้น', + 'content_colors_desc' => 'ตั้งค่าสีสำหรับองค์ประกอบทั้งหมดในลำดับชั้นการจัดระเบียบหน้า แนะนำให้เลือกสีที่มีความสว่างใกล้เคียงกับสีเริ่มต้นเพื่อการอ่านง่าย', + 'bookshelf_color' => 'สีชั้นวาง', + 'book_color' => 'สีหนังสือ', + 'chapter_color' => 'สีบท', + 'page_color' => 'สีหน้า', + 'page_draft_color' => 'สีร่างหน้า', + + // Registration Settings + 'reg_settings' => 'การลงทะเบียน', + 'reg_enable' => 'เปิดใช้งานการลงทะเบียน', + 'reg_enable_toggle' => 'เปิดใช้งานการลงทะเบียน', + 'reg_enable_desc' => 'เมื่อเปิดใช้งานการลงทะเบียน ผู้ใช้จะสามารถสมัครเป็นผู้ใช้แอปพลิเคชันได้ด้วยตนเอง เมื่อลงทะเบียนแล้วจะได้รับบทบาทผู้ใช้เริ่มต้นหนึ่งบทบาท', + 'reg_default_role' => 'บทบาทผู้ใช้เริ่มต้นหลังการลงทะเบียน', + 'reg_enable_external_warning' => 'ตัวเลือกด้านบนจะถูกละเว้นเมื่อการยืนยันตัวตน LDAP หรือ SAML ภายนอกเปิดใช้งานอยู่ บัญชีผู้ใช้สำหรับสมาชิกที่ไม่มีอยู่จะถูกสร้างอัตโนมัติหากการยืนยันตัวตนกับระบบภายนอกที่ใช้งานอยู่สำเร็จ', + 'reg_email_confirmation' => 'การยืนยันอีเมล', + 'reg_email_confirmation_toggle' => 'ต้องยืนยันอีเมล', + 'reg_confirm_email_desc' => 'หากใช้การจำกัดโดเมน จะต้องยืนยันอีเมลและตัวเลือกนี้จะถูกละเว้น', + 'reg_confirm_restrict_domain' => 'การจำกัดโดเมน', + 'reg_confirm_restrict_domain_desc' => 'กรอกรายการโดเมนอีเมลที่คั่นด้วยเครื่องหมายจุลภาคที่ต้องการจำกัดการลงทะเบียน ผู้ใช้จะได้รับอีเมลเพื่อยืนยันที่อยู่ก่อนที่จะสามารถโต้ตอบกับแอปพลิเคชัน
    ผู้ใช้จะสามารถเปลี่ยนที่อยู่อีเมลได้หลังจากลงทะเบียนสำเร็จ', + 'reg_confirm_restrict_domain_placeholder' => 'ไม่มีการจำกัด', + + // Sorting Settings + 'sorting' => 'รายการและการจัดเรียง', + 'sorting_book_default' => 'กฎการจัดเรียงหนังสือเริ่มต้น', + 'sorting_book_default_desc' => 'เลือกกฎการจัดเรียงเริ่มต้นที่จะใช้กับหนังสือใหม่ ไม่มีผลกับหนังสือที่มีอยู่แล้ว และสามารถแทนที่ได้ต่อหนังสือ', + 'sorting_rules' => 'กฎการจัดเรียง', + 'sorting_rules_desc' => 'นี่คือการดำเนินการจัดเรียงที่กำหนดไว้ล่วงหน้าซึ่งสามารถนำไปใช้กับเนื้อหาในระบบ', + 'sort_rule_assigned_to_x_books' => 'กำหนดให้ :count หนังสือ', + 'sort_rule_create' => 'สร้างกฎการจัดเรียง', + 'sort_rule_edit' => 'แก้ไขกฎการจัดเรียง', + 'sort_rule_delete' => 'ลบกฎการจัดเรียง', + 'sort_rule_delete_desc' => 'ลบกฎการจัดเรียงนี้ออกจากระบบ หนังสือที่ใช้กฎนี้จะกลับไปใช้การจัดเรียงแบบกำหนดเอง', + 'sort_rule_delete_warn_books' => 'กฎการจัดเรียงนี้ใช้งานอยู่กับ :count เล่ม คุณแน่ใจหรือไม่ว่าต้องการลบ?', + 'sort_rule_delete_warn_default' => 'กฎการจัดเรียงนี้ใช้งานอยู่เป็นค่าเริ่มต้นสำหรับหนังสือ คุณแน่ใจหรือไม่ว่าต้องการลบ?', + 'sort_rule_details' => 'รายละเอียดกฎการจัดเรียง', + 'sort_rule_details_desc' => 'ตั้งชื่อสำหรับกฎการจัดเรียงนี้ ซึ่งจะปรากฏในรายการเมื่อผู้ใช้เลือกการจัดเรียง', + 'sort_rule_operations' => 'การดำเนินการจัดเรียง', + 'sort_rule_operations_desc' => 'กำหนดการดำเนินการจัดเรียงโดยย้ายจากรายการที่มีให้ เมื่อใช้งาน การดำเนินการจะถูกนำไปใช้ตามลำดับจากบนลงล่าง การเปลี่ยนแปลงจะถูกนำไปใช้กับหนังสือที่กำหนดทั้งหมดเมื่อบันทึก', + 'sort_rule_available_operations' => 'การดำเนินการที่มีให้', + 'sort_rule_available_operations_empty' => 'ไม่มีการดำเนินการเหลือ', + 'sort_rule_configured_operations' => 'การดำเนินการที่กำหนดค่า', + 'sort_rule_configured_operations_empty' => 'ลาก/เพิ่มการดำเนินการจากรายการ "การดำเนินการที่มีให้"', + 'sort_rule_op_asc' => '(น้อยไปมาก)', + 'sort_rule_op_desc' => '(มากไปน้อย)', + 'sort_rule_op_name' => 'ชื่อ - ตามตัวอักษร', + 'sort_rule_op_name_numeric' => 'ชื่อ - ตามตัวเลข', + 'sort_rule_op_created_date' => 'วันที่สร้าง', + 'sort_rule_op_updated_date' => 'วันที่แก้ไข', + 'sort_rule_op_chapters_first' => 'บทอยู่ก่อน', + 'sort_rule_op_chapters_last' => 'บทอยู่หลัง', + 'sorting_page_limits' => 'ขีดจำกัดการแสดงผลต่อหน้า', + 'sorting_page_limits_desc' => 'ตั้งค่าจำนวนรายการที่จะแสดงต่อหน้าในรายการต่างๆ ในระบบ โดยทั่วไปจำนวนน้อยจะมีประสิทธิภาพดีกว่า ขณะที่จำนวนมากจะลดความจำเป็นในการคลิกผ่านหลายหน้า แนะนำให้ใช้ทวีคูณของ 6', + + // Maintenance settings + 'maint' => 'การบำรุงรักษา', + 'maint_image_cleanup' => 'ล้างรูปภาพ', + 'maint_image_cleanup_desc' => 'สแกนเนื้อหาหน้าและการแก้ไขเพื่อตรวจสอบว่ารูปภาพและภาพวาดใดกำลังใช้งานอยู่และรูปภาพใดซ้ำซ้อน ตรวจสอบให้แน่ใจว่าสร้างข้อมูลสำรองฐานข้อมูลและรูปภาพฉบับสมบูรณ์ก่อนรัน', + 'maint_delete_images_only_in_revisions' => 'ลบรูปภาพที่มีเฉพาะในการแก้ไขหน้าเก่าด้วย', + 'maint_image_cleanup_run' => 'รันการล้างข้อมูล', + 'maint_image_cleanup_warning' => 'พบรูปภาพที่อาจไม่ได้ใช้งาน :count รายการ คุณแน่ใจหรือไม่ว่าต้องการลบรูปภาพเหล่านี้?', + 'maint_image_cleanup_success' => 'พบและลบรูปภาพที่อาจไม่ได้ใช้งาน :count รายการ!', + 'maint_image_cleanup_nothing_found' => 'ไม่พบรูปภาพที่ไม่ได้ใช้งาน ไม่มีการลบ!', + 'maint_send_test_email' => 'ส่งอีเมลทดสอบ', + 'maint_send_test_email_desc' => 'ส่งอีเมลทดสอบไปยังที่อยู่อีเมลที่ระบุในโปรไฟล์ของคุณ', + 'maint_send_test_email_run' => 'ส่งอีเมลทดสอบ', + 'maint_send_test_email_success' => 'ส่งอีเมลไปยัง :address แล้ว', + 'maint_send_test_email_mail_subject' => 'อีเมลทดสอบ', + 'maint_send_test_email_mail_greeting' => 'การส่งอีเมลดูเหมือนจะทำงานได้!', + 'maint_send_test_email_mail_text' => 'ยินดีด้วย! เนื่องจากคุณได้รับการแจ้งเตือนทางอีเมลนี้ การตั้งค่าอีเมลของคุณดูเหมือนจะได้รับการกำหนดค่าอย่างถูกต้อง', + 'maint_recycle_bin_desc' => 'ชั้นวาง หนังสือ บท และหน้าที่ถูกลบจะถูกส่งไปยังถังรีไซเคิลเพื่อให้สามารถกู้คืนหรือลบถาวรได้ รายการเก่าในถังรีไซเคิลอาจถูกลบโดยอัตโนมัติหลังจากสักพักขึ้นอยู่กับการกำหนดค่าระบบ', + 'maint_recycle_bin_open' => 'เปิดถังรีไซเคิล', + 'maint_regen_references' => 'สร้างการอ้างอิงใหม่', + 'maint_regen_references_desc' => 'การดำเนินการนี้จะสร้างดัชนีการอ้างอิงข้ามรายการในฐานข้อมูลใหม่ โดยปกติจะจัดการโดยอัตโนมัติ แต่การดำเนินการนี้มีประโยชน์สำหรับการสร้างดัชนีเนื้อหาเก่าหรือเนื้อหาที่เพิ่มผ่านวิธีที่ไม่เป็นทางการ', + 'maint_regen_references_success' => 'สร้างดัชนีการอ้างอิงใหม่แล้ว!', + 'maint_timeout_command_note' => 'หมายเหตุ: การดำเนินการนี้อาจใช้เวลานาน ซึ่งอาจทำให้เกิดปัญหา timeout ในบางสภาพแวดล้อมเว็บ ทางเลือกหนึ่งคือดำเนินการผ่านคำสั่ง terminal', + + // Recycle Bin + 'recycle_bin' => 'ถังรีไซเคิล', + 'recycle_bin_desc' => 'คุณสามารถกู้คืนรายการที่ถูกลบหรือเลือกลบออกจากระบบถาวรได้ที่นี่ รายการนี้ไม่ได้กรองต่างจากรายการกิจกรรมที่คล้ายกันในระบบซึ่งมีการกรองสิทธิ์', + 'recycle_bin_deleted_item' => 'รายการที่ถูกลบ', + 'recycle_bin_deleted_parent' => 'รายการแม่', + 'recycle_bin_deleted_by' => 'ลบโดย', + 'recycle_bin_deleted_at' => 'เวลาที่ลบ', + 'recycle_bin_permanently_delete' => 'ลบถาวร', + 'recycle_bin_restore' => 'กู้คืน', + 'recycle_bin_contents_empty' => 'ถังรีไซเคิลว่างเปล่า', + 'recycle_bin_empty' => 'ล้างถังรีไซเคิล', + 'recycle_bin_empty_confirm' => 'การดำเนินการนี้จะทำลายรายการทั้งหมดในถังรีไซเคิลอย่างถาวร รวมถึงเนื้อหาที่อยู่ในแต่ละรายการ คุณแน่ใจหรือไม่ว่าต้องการล้างถังรีไซเคิล?', + 'recycle_bin_destroy_confirm' => 'การดำเนินการนี้จะลบรายการนี้ออกจากระบบอย่างถาวร พร้อมกับองค์ประกอบลูกที่แสดงด้านล่าง และคุณจะไม่สามารถกู้คืนเนื้อหานี้ได้ คุณแน่ใจหรือไม่?', + 'recycle_bin_destroy_list' => 'รายการที่จะถูกทำลาย', + 'recycle_bin_restore_list' => 'รายการที่จะถูกกู้คืน', + 'recycle_bin_restore_confirm' => 'การดำเนินการนี้จะกู้คืนรายการที่ถูกลบ รวมถึงองค์ประกอบลูก ไปยังตำแหน่งเดิม หากตำแหน่งเดิมถูกลบไปแล้วและอยู่ในถังรีไซเคิล จะต้องกู้คืนรายการแม่ด้วย', + 'recycle_bin_restore_deleted_parent' => 'รายการแม่ของรายการนี้ถูกลบเช่นกัน จะยังคงถูกลบจนกว่าจะกู้คืนรายการแม่', + 'recycle_bin_restore_parent' => 'กู้คืนรายการแม่', + 'recycle_bin_destroy_notification' => 'ลบรายการทั้งหมด :count รายการจากถังรีไซเคิล', + 'recycle_bin_restore_notification' => 'กู้คืนรายการทั้งหมด :count รายการจากถังรีไซเคิล', + + // Audit Log + 'audit' => 'บันทึกการตรวจสอบ', + 'audit_desc' => 'บันทึกการตรวจสอบนี้แสดงรายการกิจกรรมที่ติดตามในระบบ รายการนี้ไม่ได้กรองต่างจากรายการกิจกรรมที่คล้ายกันในระบบซึ่งมีการกรองสิทธิ์', + 'audit_event_filter' => 'ตัวกรองกิจกรรม', + 'audit_event_filter_no_filter' => 'ไม่มีตัวกรอง', + 'audit_deleted_item' => 'รายการที่ถูกลบ', + 'audit_deleted_item_name' => 'ชื่อ: :name', + 'audit_table_user' => 'ผู้ใช้', + 'audit_table_event' => 'กิจกรรม', + 'audit_table_related' => 'รายการหรือรายละเอียดที่เกี่ยวข้อง', + 'audit_table_ip' => 'IP Address', + 'audit_table_date' => 'วันที่กิจกรรม', + 'audit_date_from' => 'ช่วงวันที่ตั้งแต่', + 'audit_date_to' => 'ช่วงวันที่ถึง', + + // Role Settings + 'roles' => 'บทบาท', + 'role_user_roles' => 'บทบาทผู้ใช้', + 'roles_index_desc' => 'บทบาทใช้สำหรับจัดกลุ่มผู้ใช้และให้สิทธิ์ระบบแก่สมาชิก เมื่อผู้ใช้เป็นสมาชิกของหลายบทบาท สิทธิ์ที่ได้รับจะสะสมกัน และผู้ใช้จะได้รับความสามารถทั้งหมด', + 'roles_x_users_assigned' => 'กำหนด :count ผู้ใช้', + 'roles_x_permissions_provided' => ':count สิทธิ์', + 'roles_assigned_users' => 'ผู้ใช้ที่กำหนด', + 'roles_permissions_provided' => 'สิทธิ์ที่ให้', + 'role_create' => 'สร้างบทบาทใหม่', + 'role_delete' => 'ลบบทบาท', + 'role_delete_confirm' => 'การดำเนินการนี้จะลบบทบาทชื่อ \':roleName\'', + 'role_delete_users_assigned' => 'บทบาทนี้มีผู้ใช้ :userCount คนที่กำหนดไว้ หากต้องการย้ายผู้ใช้จากบทบาทนี้ให้เลือกบทบาทใหม่ด้านล่าง', + 'role_delete_no_migration' => "ไม่ต้องย้ายผู้ใช้", + 'role_delete_sure' => 'คุณแน่ใจหรือไม่ว่าต้องการลบบทบาทนี้?', + 'role_edit' => 'แก้ไขบทบาท', + 'role_details' => 'รายละเอียดบทบาท', + 'role_name' => 'ชื่อบทบาท', + 'role_desc' => 'คำอธิบายสั้นของบทบาท', + 'role_mfa_enforced' => 'ต้องใช้การยืนยันตัวตนแบบหลายขั้นตอน', + 'role_external_auth_id' => 'ID การยืนยันตัวตนภายนอก', + 'role_system' => 'สิทธิ์ระบบ', + 'role_manage_users' => 'จัดการผู้ใช้', + 'role_manage_roles' => 'จัดการบทบาทและสิทธิ์บทบาท', + 'role_manage_entity_permissions' => 'จัดการสิทธิ์หนังสือ บท และหน้าทั้งหมด', + 'role_manage_own_entity_permissions' => 'จัดการสิทธิ์บนหนังสือ บท และหน้าของตัวเอง', + 'role_manage_page_templates' => 'จัดการแม่แบบหน้า', + 'role_access_api' => 'เข้าถึง API ระบบ', + 'role_manage_settings' => 'จัดการการตั้งค่าแอปพลิเคชัน', + 'role_export_content' => 'ส่งออกเนื้อหา', + 'role_import_content' => 'นำเข้าเนื้อหา', + 'role_editor_change' => 'เปลี่ยนตัวแก้ไขหน้า', + 'role_notifications' => 'รับและจัดการการแจ้งเตือน', + 'role_permission_note_users_and_roles' => 'สิทธิ์เหล่านี้จะให้การมองเห็นและการค้นหาผู้ใช้และบทบาทในระบบด้วยในทางเทคนิค', + 'role_asset' => 'สิทธิ์ Asset', + 'roles_system_warning' => 'โปรดทราบว่าการเข้าถึงสิทธิ์ใดๆ ในสามสิทธิ์ข้างต้นอาจอนุญาตให้ผู้ใช้เปลี่ยนแปลงสิทธิ์ของตัวเองหรือของผู้อื่นในระบบ กรุณากำหนดบทบาทที่มีสิทธิ์เหล่านี้เฉพาะกับผู้ใช้ที่เชื่อถือได้เท่านั้น', + 'role_asset_desc' => 'สิทธิ์เหล่านี้ควบคุมการเข้าถึงเริ่มต้นสำหรับ asset ในระบบ สิทธิ์บนหนังสือ บท และหน้าจะแทนที่สิทธิ์เหล่านี้', + 'role_asset_admins' => 'ผู้ดูแลระบบได้รับสิทธิ์เข้าถึงเนื้อหาทั้งหมดโดยอัตโนมัติ แต่ตัวเลือกเหล่านี้อาจแสดงหรือซ่อนตัวเลือก UI', + 'role_asset_image_view_note' => 'เกี่ยวข้องกับการมองเห็นภายในตัวจัดการรูปภาพ การเข้าถึงไฟล์รูปภาพที่อัปโหลดจริงจะขึ้นอยู่กับตัวเลือกการจัดเก็บรูปภาพของระบบ', + 'role_asset_users_note' => 'สิทธิ์เหล่านี้จะให้การมองเห็นและการค้นหาผู้ใช้ในระบบด้วยในทางเทคนิค', + 'role_all' => 'ทั้งหมด', + 'role_own' => 'ของตัวเอง', + 'role_controlled_by_asset' => 'ควบคุมโดย asset ที่อัปโหลดไปยัง', + 'role_controlled_by_page_delete' => 'ควบคุมโดยสิทธิ์ลบหน้า', + 'role_save' => 'บันทึกบทบาท', + 'role_users' => 'ผู้ใช้ในบทบาทนี้', + 'role_users_none' => 'ยังไม่มีผู้ใช้ที่กำหนดให้บทบาทนี้', + + // Users + 'users' => 'ผู้ใช้', + 'users_index_desc' => 'สร้างและจัดการบัญชีผู้ใช้แต่ละรายในระบบ บัญชีผู้ใช้ใช้สำหรับเข้าสู่ระบบและระบุที่มาของเนื้อหาและกิจกรรม สิทธิ์การเข้าถึงหลักอิงตามบทบาท แต่ความเป็นเจ้าของเนื้อหาและปัจจัยอื่นๆ อาจส่งผลต่อสิทธิ์และการเข้าถึงด้วย', + 'user_profile' => 'โปรไฟล์ผู้ใช้', + 'users_add_new' => 'เพิ่มผู้ใช้ใหม่', + 'users_search' => 'ค้นหาผู้ใช้', + 'users_latest_activity' => 'กิจกรรมล่าสุด', + 'users_details' => 'รายละเอียดผู้ใช้', + 'users_details_desc' => 'ตั้งชื่อที่แสดงและที่อยู่อีเมลสำหรับผู้ใช้นี้ ที่อยู่อีเมลจะใช้สำหรับเข้าสู่ระบบแอปพลิเคชัน', + 'users_details_desc_no_email' => 'ตั้งชื่อที่แสดงสำหรับผู้ใช้นี้เพื่อให้ผู้อื่นรู้จัก', + 'users_role' => 'บทบาทผู้ใช้', + 'users_role_desc' => 'เลือกบทบาทที่จะกำหนดให้ผู้ใช้นี้ หากผู้ใช้ถูกกำหนดให้หลายบทบาท สิทธิ์จากบทบาทเหล่านั้นจะสะสมกัน', + 'users_password' => 'รหัสผ่านผู้ใช้', + 'users_password_desc' => 'ตั้งรหัสผ่านสำหรับเข้าสู่ระบบ ต้องมีความยาวอย่างน้อย 8 ตัวอักษร', + 'users_send_invite_text' => 'คุณสามารถเลือกส่งอีเมลเชิญให้ผู้ใช้นี้เพื่อให้ตั้งรหัสผ่านของตัวเอง หรือตั้งรหัสผ่านให้เองก็ได้', + 'users_send_invite_option' => 'ส่งอีเมลเชิญผู้ใช้', + 'users_external_auth_id' => 'ID การยืนยันตัวตนภายนอก', + 'users_external_auth_id_desc' => 'เมื่อใช้ระบบยืนยันตัวตนภายนอก (เช่น SAML2, OIDC หรือ LDAP) นี่คือ ID ที่เชื่อมผู้ใช้ BookStack นี้กับบัญชีระบบยืนยันตัวตน สามารถละเว้นช่องนี้หากใช้การยืนยันตัวตนแบบอีเมลเริ่มต้น', + 'users_password_warning' => 'กรอกด้านล่างเฉพาะเมื่อต้องการเปลี่ยนรหัสผ่านสำหรับผู้ใช้นี้', + 'users_system_public' => 'ผู้ใช้นี้แทนผู้เยี่ยมชมที่เข้าถึง instance ของคุณ ไม่สามารถใช้เข้าสู่ระบบได้ แต่จะถูกกำหนดโดยอัตโนมัติ', + 'users_delete' => 'ลบผู้ใช้', + 'users_delete_named' => 'ลบผู้ใช้ :userName', + 'users_delete_warning' => 'การดำเนินการนี้จะลบผู้ใช้ชื่อ \':userName\' ออกจากระบบทั้งหมด', + 'users_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบผู้ใช้นี้?', + 'users_migrate_ownership' => 'ย้ายความเป็นเจ้าของ', + 'users_migrate_ownership_desc' => 'เลือกผู้ใช้ที่นี่หากต้องการให้ผู้ใช้อื่นเป็นเจ้าของรายการทั้งหมดที่เป็นของผู้ใช้นี้ในปัจจุบัน', + 'users_none_selected' => 'ไม่มีผู้ใช้ที่เลือก', + 'users_edit' => 'แก้ไขผู้ใช้', + 'users_edit_profile' => 'แก้ไขโปรไฟล์', + 'users_avatar' => 'รูปโปรไฟล์ผู้ใช้', + 'users_avatar_desc' => 'เลือกรูปภาพเพื่อแสดงตัวผู้ใช้นี้ ควรเป็นรูปสี่เหลี่ยมขนาดประมาณ 256px', + 'users_preferred_language' => 'ภาษาที่ต้องการ', + 'users_preferred_language_desc' => 'ตัวเลือกนี้จะเปลี่ยนภาษาที่ใช้สำหรับส่วนติดต่อผู้ใช้ของแอปพลิเคชัน ไม่มีผลต่อเนื้อหาที่ผู้ใช้สร้าง', + 'users_social_accounts' => 'บัญชี Social', + 'users_social_accounts_desc' => 'ดูสถานะของบัญชี Social ที่เชื่อมต่อสำหรับผู้ใช้นี้ บัญชี Social สามารถใช้นอกเหนือจากระบบยืนยันตัวตนหลักสำหรับการเข้าถึงระบบ', + 'users_social_accounts_info' => 'คุณสามารถเชื่อมต่อบัญชีอื่นที่นี่เพื่อเข้าสู่ระบบที่เร็วและง่ายขึ้น การตัดการเชื่อมต่อบัญชีที่นี่ไม่ได้เพิกถอนสิทธิ์ที่ให้ไว้ก่อนหน้า กรุณาเพิกถอนสิทธิ์จากการตั้งค่าโปรไฟล์บนบัญชี Social ที่เชื่อมต่อ', + 'users_social_connect' => 'เชื่อมต่อบัญชี', + 'users_social_disconnect' => 'ตัดการเชื่อมต่อบัญชี', + 'users_social_status_connected' => 'เชื่อมต่อแล้ว', + 'users_social_status_disconnected' => 'ตัดการเชื่อมต่อแล้ว', + 'users_social_connected' => 'เชื่อมต่อบัญชี :socialAccount กับโปรไฟล์ของคุณสำเร็จแล้ว', + 'users_social_disconnected' => 'ตัดการเชื่อมต่อบัญชี :socialAccount จากโปรไฟล์ของคุณสำเร็จแล้ว', + 'users_api_tokens' => 'API Tokens', + 'users_api_tokens_desc' => 'สร้างและจัดการโทเค็นการเข้าถึงที่ใช้ยืนยันตัวตนกับ BookStack REST API สิทธิ์สำหรับ API จัดการผ่านผู้ใช้ที่โทเค็นนั้นสังกัด', + 'users_api_tokens_none' => 'ยังไม่มี API token ที่สร้างสำหรับผู้ใช้นี้', + 'users_api_tokens_create' => 'สร้าง Token', + 'users_api_tokens_expires' => 'หมดอายุ', + 'users_api_tokens_docs' => 'เอกสาร API', + 'users_mfa' => 'การยืนยันตัวตนแบบหลายขั้นตอน', + 'users_mfa_desc' => 'ตั้งค่าการยืนยันตัวตนแบบหลายขั้นตอนเป็นชั้นความปลอดภัยเพิ่มเติมสำหรับบัญชีผู้ใช้ของคุณ', + 'users_mfa_x_methods' => 'กำหนดค่า :count วิธี', + 'users_mfa_configure' => 'กำหนดค่าวิธี', + + // API Tokens + 'user_api_token_create' => 'สร้าง API Token', + 'user_api_token_name' => 'ชื่อ', + 'user_api_token_name_desc' => 'ตั้งชื่อที่อ่านง่ายสำหรับ token นี้เพื่อเตือนความจำเกี่ยวกับวัตถุประสงค์ในอนาคต', + 'user_api_token_expiry' => 'วันหมดอายุ', + 'user_api_token_expiry_desc' => 'ตั้งวันที่ token นี้หมดอายุ หลังจากวันนี้ คำขอที่ใช้ token นี้จะไม่ทำงานอีกต่อไป การเว้นช่องนี้ว่างจะตั้งวันหมดอายุเป็น 100 ปีในอนาคต', + 'user_api_token_create_secret_message' => 'ทันทีหลังจากสร้าง token นี้ "Token ID" และ "Token Secret" จะถูกสร้างและแสดง Secret จะแสดงเพียงครั้งเดียว ดังนั้นให้คัดลอกค่าไปยังที่ปลอดภัยก่อนดำเนินการต่อ', + 'user_api_token' => 'API Token', + 'user_api_token_id' => 'Token ID', + 'user_api_token_id_desc' => 'นี่คือตัวระบุที่สร้างโดยระบบซึ่งไม่สามารถแก้ไขได้สำหรับ token นี้ ซึ่งจะต้องระบุในคำขอ API', + 'user_api_token_secret' => 'Token Secret', + 'user_api_token_secret_desc' => 'นี่คือ secret ที่สร้างโดยระบบสำหรับ token นี้ซึ่งจะต้องระบุในคำขอ API จะแสดงเพียงครั้งเดียวนี้เท่านั้น ดังนั้นให้คัดลอกค่าไปยังที่ปลอดภัย', + 'user_api_token_created' => 'สร้าง Token เมื่อ :timeAgo', + 'user_api_token_updated' => 'อัปเดต Token เมื่อ :timeAgo', + 'user_api_token_delete' => 'ลบ Token', + 'user_api_token_delete_warning' => 'การดำเนินการนี้จะลบ API token ชื่อ \':tokenName\' ออกจากระบบทั้งหมด', + 'user_api_token_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบ API token นี้?', + + // Webhooks + 'webhooks' => 'Webhooks', + 'webhooks_index_desc' => 'Webhooks เป็นวิธีส่งข้อมูลไปยัง URL ภายนอกเมื่อมีการดำเนินการและกิจกรรมบางอย่างในระบบ ซึ่งช่วยให้สามารถรวมกับแพลตฟอร์มภายนอก เช่น ระบบส่งข้อความหรือการแจ้งเตือน', + 'webhooks_x_trigger_events' => ':count กิจกรรมทริกเกอร์', + 'webhooks_create' => 'สร้าง Webhook ใหม่', + 'webhooks_none_created' => 'ยังไม่มี Webhook ที่สร้าง', + 'webhooks_edit' => 'แก้ไข Webhook', + 'webhooks_save' => 'บันทึก Webhook', + 'webhooks_details' => 'รายละเอียด Webhook', + 'webhooks_details_desc' => 'ระบุชื่อที่เป็นมิตรกับผู้ใช้และ POST endpoint เป็นตำแหน่งสำหรับส่งข้อมูล Webhook', + 'webhooks_events' => 'กิจกรรม Webhook', + 'webhooks_events_desc' => 'เลือกกิจกรรมทั้งหมดที่ควรทริกเกอร์ให้เรียก Webhook นี้', + 'webhooks_events_warning' => 'โปรดทราบว่ากิจกรรมเหล่านี้จะถูกทริกเกอร์สำหรับกิจกรรมที่เลือกทั้งหมด แม้จะมีสิทธิ์แบบกำหนดเองก็ตาม ตรวจสอบให้แน่ใจว่าการใช้ Webhook นี้จะไม่เปิดเผยเนื้อหาที่เป็นความลับ', + 'webhooks_events_all' => 'กิจกรรมทั้งหมดในระบบ', + 'webhooks_name' => 'ชื่อ Webhook', + 'webhooks_timeout' => 'หมดเวลาคำขอ Webhook (วินาที)', + 'webhooks_endpoint' => 'Endpoint ของ Webhook', + 'webhooks_active' => 'Webhook เปิดใช้งาน', + 'webhook_events_table_header' => 'กิจกรรม', + 'webhooks_delete' => 'ลบ Webhook', + 'webhooks_delete_warning' => 'การดำเนินการนี้จะลบ Webhook ชื่อ \':webhookName\' ออกจากระบบทั้งหมด', + 'webhooks_delete_confirm' => 'คุณแน่ใจหรือไม่ว่าต้องการลบ Webhook นี้?', + 'webhooks_format_example' => 'ตัวอย่างรูปแบบ Webhook', + 'webhooks_format_example_desc' => 'ข้อมูล Webhook ถูกส่งเป็นคำขอ POST ไปยัง endpoint ที่กำหนดค่าเป็น JSON ตามรูปแบบด้านล่าง คุณสมบัติ "related_item" และ "url" เป็นทางเลือกและขึ้นอยู่กับประเภทกิจกรรมที่ทริกเกอร์', + 'webhooks_status' => 'สถานะ Webhook', + 'webhooks_last_called' => 'เรียกล่าสุด:', + 'webhooks_last_errored' => 'เกิดข้อผิดพลาดล่าสุด:', + 'webhooks_last_error_message' => 'ข้อความข้อผิดพลาดล่าสุด:', + + // Licensing + 'licenses' => 'สัญญาอนุญาต', + 'licenses_desc' => 'หน้านี้แสดงข้อมูลสัญญาอนุญาตสำหรับ BookStack รวมถึงโปรเจกต์และไลบรารีที่ใช้ใน BookStack โปรเจกต์หลายรายการอาจใช้เฉพาะในบริบทการพัฒนา', + 'licenses_bookstack' => 'สัญญาอนุญาต BookStack', + 'licenses_php' => 'สัญญาอนุญาตไลบรารี PHP', + 'licenses_js' => 'สัญญาอนุญาตไลบรารี JavaScript', + 'licenses_other' => 'สัญญาอนุญาตอื่นๆ', + 'license_details' => 'รายละเอียดสัญญาอนุญาต', + + //! If editing translations files directly please ignore this in all + //! languages apart from en. Content will be auto-copied from en. + //!//////////////////////////////// + 'language_select' => [ + 'en' => 'English', + 'ar' => 'العربية', + 'bg' => 'Bǎlgarski', + 'bs' => 'Bosanski', + 'ca' => 'Català', + 'cs' => 'Česky', + 'cy' => 'Cymraeg', + 'da' => 'Dansk', + 'de' => 'Deutsch (Sie)', + 'de_informal' => 'Deutsch (Du)', + 'el' => 'ελληνικά', + 'es' => 'Español', + 'es_AR' => 'Español Argentina', + 'et' => 'Eesti keel', + 'eu' => 'Euskara', + 'fa' => 'فارسی', + 'fi' => 'Suomi', + 'fr' => 'Français', + 'he' => 'עברית', + 'hr' => 'Hrvatski', + 'hu' => 'Magyar', + 'id' => 'Bahasa Indonesia', + 'it' => 'Italian', + 'ja' => '日本語', + 'ko' => '한국어', + 'lt' => 'Lietuvių Kalba', + 'lv' => 'Latviešu Valoda', + 'nb' => 'Norsk (Bokmål)', + 'ne' => 'नेपाली', + 'nn' => 'Nynorsk', + 'nl' => 'Nederlands', + 'pl' => 'Polski', + 'pt' => 'Português', + 'pt_BR' => 'Português do Brasil', + 'ro' => 'Română', + 'ru' => 'Русский', + 'sk' => 'Slovensky', + 'sl' => 'Slovenščina', + 'sv' => 'Svenska', + 'tr' => 'Türkçe', + 'uk' => 'Українська', + 'uz' => 'O‘zbekcha', + 'vi' => 'Tiếng Việt', + 'zh_CN' => '简体中文', + 'zh_TW' => '繁體中文', + ], + //!//////////////////////////////// +]; diff --git a/lang/th/validation.php b/lang/th/validation.php new file mode 100644 index 000000000..347eeb36c --- /dev/null +++ b/lang/th/validation.php @@ -0,0 +1,123 @@ + 'ต้องยอมรับ :attribute', + 'active_url' => ':attribute ไม่ใช่ URL ที่ถูกต้อง', + 'after' => ':attribute ต้องเป็นวันที่หลังจาก :date', + 'alpha' => ':attribute ต้องมีเฉพาะตัวอักษรเท่านั้น', + 'alpha_dash' => ':attribute ต้องมีเฉพาะตัวอักษร ตัวเลข ขีดกลาง และขีดล่างเท่านั้น', + 'alpha_num' => ':attribute ต้องมีเฉพาะตัวอักษรและตัวเลขเท่านั้น', + 'array' => ':attribute ต้องเป็น array', + 'backup_codes' => 'รหัสที่ให้มาไม่ถูกต้องหรือถูกใช้ไปแล้ว', + 'before' => ':attribute ต้องเป็นวันที่ก่อน :date', + 'between' => [ + 'numeric' => ':attribute ต้องอยู่ระหว่าง :min ถึง :max', + 'file' => ':attribute ต้องมีขนาดระหว่าง :min ถึง :max กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวระหว่าง :min ถึง :max ตัวอักษร', + 'array' => ':attribute ต้องมีระหว่าง :min ถึง :max รายการ', + ], + 'boolean' => 'ฟิลด์ :attribute ต้องเป็น true หรือ false', + 'confirmed' => 'การยืนยัน :attribute ไม่ตรงกัน', + 'date' => ':attribute ไม่ใช่วันที่ที่ถูกต้อง', + 'date_format' => ':attribute ไม่ตรงกับรูปแบบ :format', + 'different' => ':attribute และ :other ต้องแตกต่างกัน', + 'digits' => ':attribute ต้องมี :digits หลัก', + 'digits_between' => ':attribute ต้องมีระหว่าง :min ถึง :max หลัก', + 'email' => ':attribute ต้องเป็นที่อยู่อีเมลที่ถูกต้อง', + 'ends_with' => ':attribute ต้องลงท้ายด้วยหนึ่งในนี้: :values', + 'file' => ':attribute ต้องเป็นไฟล์ที่ถูกต้อง', + 'filled' => 'ฟิลด์ :attribute จำเป็นต้องกรอก', + 'gt' => [ + 'numeric' => ':attribute ต้องมากกว่า :value', + 'file' => ':attribute ต้องมากกว่า :value กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวมากกว่า :value ตัวอักษร', + 'array' => ':attribute ต้องมีมากกว่า :value รายการ', + ], + 'gte' => [ + 'numeric' => ':attribute ต้องมากกว่าหรือเท่ากับ :value', + 'file' => ':attribute ต้องมากกว่าหรือเท่ากับ :value กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวมากกว่าหรือเท่ากับ :value ตัวอักษร', + 'array' => ':attribute ต้องมี :value รายการขึ้นไป', + ], + 'exists' => ':attribute ที่เลือกไม่ถูกต้อง', + 'image' => ':attribute ต้องเป็นรูปภาพ', + 'image_extension' => ':attribute ต้องมีนามสกุลรูปภาพที่ถูกต้องและรองรับ', + 'in' => ':attribute ที่เลือกไม่ถูกต้อง', + 'integer' => ':attribute ต้องเป็นจำนวนเต็ม', + 'ip' => ':attribute ต้องเป็นที่อยู่ IP ที่ถูกต้อง', + 'ipv4' => ':attribute ต้องเป็นที่อยู่ IPv4 ที่ถูกต้อง', + 'ipv6' => ':attribute ต้องเป็นที่อยู่ IPv6 ที่ถูกต้อง', + 'json' => ':attribute ต้องเป็น JSON string ที่ถูกต้อง', + 'lt' => [ + 'numeric' => ':attribute ต้องน้อยกว่า :value', + 'file' => ':attribute ต้องน้อยกว่า :value กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวน้อยกว่า :value ตัวอักษร', + 'array' => ':attribute ต้องมีน้อยกว่า :value รายการ', + ], + 'lte' => [ + 'numeric' => ':attribute ต้องน้อยกว่าหรือเท่ากับ :value', + 'file' => ':attribute ต้องน้อยกว่าหรือเท่ากับ :value กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวน้อยกว่าหรือเท่ากับ :value ตัวอักษร', + 'array' => ':attribute ต้องไม่มีมากกว่า :value รายการ', + ], + 'max' => [ + 'numeric' => ':attribute ต้องไม่มากกว่า :max', + 'file' => ':attribute ต้องไม่มากกว่า :max กิโลไบต์', + 'string' => ':attribute ต้องไม่มากกว่า :max ตัวอักษร', + 'array' => ':attribute ต้องไม่มีมากกว่า :max รายการ', + ], + 'mimes' => ':attribute ต้องเป็นไฟล์ประเภท: :values', + 'min' => [ + 'numeric' => ':attribute ต้องอย่างน้อย :min', + 'file' => ':attribute ต้องอย่างน้อย :min กิโลไบต์', + 'string' => ':attribute ต้องมีความยาวอย่างน้อย :min ตัวอักษร', + 'array' => ':attribute ต้องมีอย่างน้อย :min รายการ', + ], + 'not_in' => ':attribute ที่เลือกไม่ถูกต้อง', + 'not_regex' => 'รูปแบบ :attribute ไม่ถูกต้อง', + 'numeric' => ':attribute ต้องเป็นตัวเลข', + 'regex' => 'รูปแบบ :attribute ไม่ถูกต้อง', + 'required' => 'ฟิลด์ :attribute จำเป็นต้องกรอก', + 'required_if' => 'ฟิลด์ :attribute จำเป็นต้องกรอกเมื่อ :other เป็น :value', + 'required_with' => 'ฟิลด์ :attribute จำเป็นต้องกรอกเมื่อมี :values', + 'required_with_all' => 'ฟิลด์ :attribute จำเป็นต้องกรอกเมื่อมี :values', + 'required_without' => 'ฟิลด์ :attribute จำเป็นต้องกรอกเมื่อไม่มี :values', + 'required_without_all' => 'ฟิลด์ :attribute จำเป็นต้องกรอกเมื่อไม่มีสิ่งใดใน :values', + 'same' => ':attribute และ :other ต้องตรงกัน', + 'safe_url' => 'ลิงก์ที่ให้มาอาจไม่ปลอดภัย', + 'size' => [ + 'numeric' => ':attribute ต้องเป็น :size', + 'file' => ':attribute ต้องมีขนาด :size กิโลไบต์', + 'string' => ':attribute ต้องมีความยาว :size ตัวอักษร', + 'array' => ':attribute ต้องมี :size รายการ', + ], + 'string' => ':attribute ต้องเป็น string', + 'timezone' => ':attribute ต้องเป็นเขตเวลาที่ถูกต้อง', + 'totp' => 'รหัสที่ให้มาไม่ถูกต้องหรือหมดอายุแล้ว', + 'unique' => ':attribute ถูกใช้งานแล้ว', + 'url' => 'รูปแบบ :attribute ไม่ถูกต้อง', + 'uploaded' => 'ไม่สามารถอัปโหลดไฟล์ได้ เซิร์ฟเวอร์อาจไม่รับไฟล์ขนาดนี้', + + 'zip_file' => ':attribute ต้องอ้างอิงถึงไฟล์ภายใน ZIP', + 'zip_file_size' => 'ไฟล์ :attribute ต้องไม่เกิน :size MB', + 'zip_file_mime' => ':attribute ต้องอ้างอิงถึงไฟล์ประเภท :validTypes แต่พบ :foundType', + 'zip_model_expected' => 'คาดหวัง data object แต่พบ ":type"', + 'zip_unique' => ':attribute ต้องไม่ซ้ำกันสำหรับประเภท object ภายใน ZIP', + + // Custom validation lines + 'custom' => [ + 'password-confirm' => [ + 'required_with' => 'จำเป็นต้องยืนยันรหัสผ่าน', + ], + ], + + // Custom validation attributes + 'attributes' => [], +]; diff --git a/lang/tk/entities.php b/lang/tk/entities.php index 74c50be3b..5501d2bc2 100644 --- a/lang/tk/entities.php +++ b/lang/tk/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sort Book :bookName', 'books_sort_name' => 'Sort by Name', 'books_sort_created' => 'Sort by Created Date', diff --git a/lang/tk/settings.php b/lang/tk/settings.php index c4d1eb136..3937c650f 100644 --- a/lang/tk/settings.php +++ b/lang/tk/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'All', 'role_own' => 'Own', 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Save Role', 'role_users' => 'Users in this role', 'role_users_none' => 'No users are currently assigned to this role', diff --git a/lang/tr/entities.php b/lang/tr/entities.php index 39feb7e39..3d271ecb5 100644 --- a/lang/tr/entities.php +++ b/lang/tr/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => ':bookName Kitabını Sırala', 'books_sort_name' => 'İsme Göre Sırala', 'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index a33d3e0ac..af8d2c649 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Hepsi', 'role_own' => 'Kendine Ait', 'role_controlled_by_asset' => 'Yüklendikleri varlık tarafından kontrol ediliyor', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Rolü Kaydet', 'role_users' => 'Bu roldeki kullanıcılar', 'role_users_none' => 'Bu role henüz bir kullanıcı atanmadı', diff --git a/lang/uk/entities.php b/lang/uk/entities.php index cf79b530c..6bf7b241e 100644 --- a/lang/uk/entities.php +++ b/lang/uk/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Перекладіть розділи та сторінки в межах книги, щоб реорганізувати вміст. Інші книги можна додати, що дозволяє легко переміщати глави та сторінки між книгами. При необхідності правило автоматичного сортування може бути встановлено для автоматичного сортування вмісту цієї книги при змінах.', 'books_sort_auto_sort' => 'Опція автоматичного сортування', 'books_sort_auto_sort_active' => 'Автосортування : :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Сортувати книгу :bookName', 'books_sort_name' => 'Сортувати за назвою', 'books_sort_created' => 'Сортувати за датою створення', diff --git a/lang/uk/settings.php b/lang/uk/settings.php index afeb2c489..9706b328b 100644 --- a/lang/uk/settings.php +++ b/lang/uk/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Все', 'role_own' => 'Власне', 'role_controlled_by_asset' => 'Контролюється за об\'єктом, до якого вони завантажуються', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Зберегти роль', 'role_users' => 'Користувачі в цій ролі', 'role_users_none' => 'Наразі жоден користувач не призначений для цієї ролі', diff --git a/lang/uz/entities.php b/lang/uz/entities.php index e35a485b6..d3fdb5944 100644 --- a/lang/uz/entities.php +++ b/lang/uz/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', 'books_sort_auto_sort' => 'Auto Sort Option', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Kitobni tartiblash: kitob nomi', 'books_sort_name' => 'Nomi bo\'yicha saralash', 'books_sort_created' => 'Yaratilgan sana bo\'yicha saralash', diff --git a/lang/uz/settings.php b/lang/uz/settings.php index 259aee71a..0dbf8351e 100644 --- a/lang/uz/settings.php +++ b/lang/uz/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Hammasi', 'role_own' => 'Shaxsiy', 'role_controlled_by_asset' => 'Ular yuklangan obyekt tomonidan nazorat qilinadi', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Rolni saqlash', 'role_users' => 'Ushbu roldagi foydalanuvchilar', 'role_users_none' => 'Hozirda bu rolga hech qanday foydalanuvchi tayinlanmagan', diff --git a/lang/vi/entities.php b/lang/vi/entities.php index f538f993c..fc1ca6d56 100644 --- a/lang/vi/entities.php +++ b/lang/vi/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => 'Di chuyển các chương và trang trong một cuốn sách để sắp xếp lại nội dung của nó. Các sách khác có thể được thêm vào để dễ dàng di chuyển các chương và trang giữa các sách. Tùy chọn, một quy tắc sắp xếp tự động có thể được đặt để tự động sắp xếp nội dung cuốn sách này khi có thay đổi.', 'books_sort_auto_sort' => 'Tùy chọn sắp xếp tự động', 'books_sort_auto_sort_active' => 'Sắp xếp tự động đang hoạt động: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => 'Sắp xếp sách :bookName', 'books_sort_name' => 'Sắp xếp theo tên', 'books_sort_created' => 'Sắp xếp theo ngày tạo', diff --git a/lang/vi/settings.php b/lang/vi/settings.php index f5f2377c8..c0bca5691 100644 --- a/lang/vi/settings.php +++ b/lang/vi/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => 'Tất cả', 'role_own' => 'Sở hữu', 'role_controlled_by_asset' => 'Kiểm soát các tài sản (asset) người dùng tải lên', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => 'Lưu Quyền', 'role_users' => 'Người dùng được gán quyền này', 'role_users_none' => 'Không có người dùng nào hiện được gán quyền này', diff --git a/lang/zh_CN/entities.php b/lang/zh_CN/entities.php index c4ec0414d..9d57eeb57 100644 --- a/lang/zh_CN/entities.php +++ b/lang/zh_CN/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => '在书籍内部移动章节与页面以重组内容;支持添加其他书籍,实现跨书籍便捷移动章节与页面;还可设置自动排序规则,在内容发生变更时自动对本书内容进行排序。', 'books_sort_auto_sort' => '自动排序选项', 'books_sort_auto_sort_active' => '自动排序已激活:::sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => '排序书籍「:bookName」', 'books_sort_name' => '按名称排序', 'books_sort_created' => '创建时间排序', diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index e53e67aba..86dd680b0 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -207,6 +207,7 @@ return [ 'role_all' => '全部的', 'role_own' => '拥有的', 'role_controlled_by_asset' => '由其所在的资源来控制', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => '保存角色', 'role_users' => '此角色的用户', 'role_users_none' => '目前没有用户被分配到这个角色', diff --git a/lang/zh_TW/entities.php b/lang/zh_TW/entities.php index 46bbe62f9..08886013c 100644 --- a/lang/zh_TW/entities.php +++ b/lang/zh_TW/entities.php @@ -173,6 +173,7 @@ return [ 'books_sort_desc' => '在書籍中移動章節和頁面,重新安排其內容。可加入其他書籍,方便在書籍之間移動章節與頁面。可選擇設定自動排序規則,以便在變更時自動排序此書籍的內容。', 'books_sort_auto_sort' => '自動排序選項', 'books_sort_auto_sort_active' => '自動排序啟動::sortName', + 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', 'books_sort_named' => '排序書本 :bookName', 'books_sort_name' => '按名稱排序', 'books_sort_created' => '按建立時間排序', diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php index 65778f77c..fd5b088f5 100644 --- a/lang/zh_TW/settings.php +++ b/lang/zh_TW/settings.php @@ -208,6 +208,7 @@ return [ 'role_all' => '全部', 'role_own' => '擁有', 'role_controlled_by_asset' => '依據隸屬的資源來決定', + 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', 'role_save' => '儲存角色', 'role_users' => '屬於此角色的使用者', 'role_users_none' => '目前沒有使用者被分配到此角色', From 1532a99d4e0cf7e17c17d8e0f4b26c2e6a08a702 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 5 May 2026 20:35:45 +0100 Subject: [PATCH 41/74] Meta: Updated issue template labels, fixed minor issues --- .forgejo/ISSUE_TEMPLATE/api_request.yml | 2 +- .forgejo/ISSUE_TEMPLATE/bug_report.yml | 2 +- .forgejo/ISSUE_TEMPLATE/feature_request.yml | 2 +- .forgejo/ISSUE_TEMPLATE/language_request.yml | 2 +- .forgejo/ISSUE_TEMPLATE/support_request.yml | 2 +- readme.md | 2 +- tests/Api/TagsApiTest.php | 3 +-- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.forgejo/ISSUE_TEMPLATE/api_request.yml b/.forgejo/ISSUE_TEMPLATE/api_request.yml index def952c52..c68d26258 100644 --- a/.forgejo/ISSUE_TEMPLATE/api_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/api_request.yml @@ -1,6 +1,6 @@ name: New API Endpoint or API Ability description: Request a new endpoint or API feature be added -labels: [":nut_and_bolt: API Request"] +labels: ["Type/API Request"] body: - type: textarea id: feature diff --git a/.forgejo/ISSUE_TEMPLATE/bug_report.yml b/.forgejo/ISSUE_TEMPLATE/bug_report.yml index d301826c9..9e4173cc7 100644 --- a/.forgejo/ISSUE_TEMPLATE/bug_report.yml +++ b/.forgejo/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: Bug Report description: Create a report to help us fix bugs & issues in existing supported functionality -labels: [":bug: Bug"] +labels: ["Type/Bug Report"] body: - type: markdown attributes: diff --git a/.forgejo/ISSUE_TEMPLATE/feature_request.yml b/.forgejo/ISSUE_TEMPLATE/feature_request.yml index c1420cb19..0d799d0a7 100644 --- a/.forgejo/ISSUE_TEMPLATE/feature_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ name: Feature Request description: Request a new feature or idea to be added to BookStack -labels: [":hammer: Feature Request"] +labels: ["Type/Feature Request"] body: - type: textarea id: description diff --git a/.forgejo/ISSUE_TEMPLATE/language_request.yml b/.forgejo/ISSUE_TEMPLATE/language_request.yml index fad9ef1e8..b86fb08e8 100644 --- a/.forgejo/ISSUE_TEMPLATE/language_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/language_request.yml @@ -1,6 +1,6 @@ name: Language Request description: Request a new language to be added to Crowdin for you to translate -labels: [":earth_africa: Translations"] +labels: ["Focus: Translations"] assignees: - ssddanbrown body: diff --git a/.forgejo/ISSUE_TEMPLATE/support_request.yml b/.forgejo/ISSUE_TEMPLATE/support_request.yml index d60eab711..fde4aad71 100644 --- a/.forgejo/ISSUE_TEMPLATE/support_request.yml +++ b/.forgejo/ISSUE_TEMPLATE/support_request.yml @@ -1,6 +1,6 @@ name: Support Request description: Request support for a specific problem you have not been able to solve yourself -labels: [":dog2: Support"] +labels: ["Type/Support"] body: - type: checkboxes id: useddocs diff --git a/readme.md b/readme.md index 30d4d1789..e9e5d197b 100644 --- a/readme.md +++ b/readme.md @@ -29,7 +29,7 @@ A platform for storing and organising information and documentation. Details for BookStack is an opinionated documentation platform that provides a pleasant and simple out-of-the-box experience. New users to an instance should find the experience intuitive and only basic word-processing skills should be required to get involved in creating content on BookStack. The platform should provide advanced power features to those that desire it, but they should not interfere with the core simple user experience. -BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above. +BookStack is not designed as an extensible platform to be used for purposes that differ from the statement above. In regard to development philosophy, BookStack has a relaxed, open & positive approach. We aim to slowly yet continuously evolve the platform while providing a stable & easy upgrade path. diff --git a/tests/Api/TagsApiTest.php b/tests/Api/TagsApiTest.php index a079fa639..e39d72ce1 100644 --- a/tests/Api/TagsApiTest.php +++ b/tests/Api/TagsApiTest.php @@ -1,12 +1,11 @@ Date: Tue, 5 May 2026 20:44:04 +0100 Subject: [PATCH 42/74] Languages: Enabled Thai as a language option --- app/Translation/LocaleManager.php | 1 + lang/en/settings.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/Translation/LocaleManager.php b/app/Translation/LocaleManager.php index d23c23610..ea32976a8 100644 --- a/app/Translation/LocaleManager.php +++ b/app/Translation/LocaleManager.php @@ -64,6 +64,7 @@ class LocaleManager 'sq' => 'sq_AL', 'sr' => 'sr_RS', 'sv' => 'sv_SE', + 'th' => 'th_TH', 'tk' => 'tk_TM', 'tr' => 'tr_TR', 'uk' => 'uk_UA', diff --git a/lang/en/settings.php b/lang/en/settings.php index 3937c650f..3ccf15e72 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -364,6 +364,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', From 50d3be4c95fb9ed17699a86cafc5634c22972250 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 8 May 2026 17:03:27 +0200 Subject: [PATCH 43/74] CI: Made actions more efficient (#6124) Updates our CI process to be more efficient by: - Uses setupphp/node image for more direct access to desired PHP versions. - Adds php extension caching via https://github.com/shivammathur/cache-extensions - Reverted to using MySQL in-test-container to reduce syscalls across the container stack which seemed to be slowing things down. - Update JS testing to only use one worker, to avoid exhausting all CPUs. I think it was attempting to use all threads on the host system before, causing the machine to lock up, since only a subset of cores were available to the environment. Reviewed-on: https://codeberg.org/bookstack/bookstack/pulls/6124 --- .forgejo/workflows/test-js.yml | 2 +- .forgejo/workflows/test-php.yml | 45 ++++++++++++++++++++++++--------- package.json | 3 ++- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.forgejo/workflows/test-js.yml b/.forgejo/workflows/test-js.yml index 180e6d545..d3e8467fe 100644 --- a/.forgejo/workflows/test-js.yml +++ b/.forgejo/workflows/test-js.yml @@ -29,4 +29,4 @@ jobs: run: npm run ts:lint - name: Run JavaScript tests - run: npm run test \ No newline at end of file + run: npm run test:ci \ No newline at end of file diff --git a/.forgejo/workflows/test-php.yml b/.forgejo/workflows/test-php.yml index 5ff2d14a5..06a6de276 100644 --- a/.forgejo/workflows/test-php.yml +++ b/.forgejo/workflows/test-php.yml @@ -16,26 +16,36 @@ jobs: if: ${{ github.ref != 'refs/heads/l10n_development' }} runs-on: docker container: - image: docker.io/library/node:24-trixie + image: docker.io/setupphp/node:noble strategy: matrix: php: ['8.2', '8.3', '8.4', '8.5'] - services: - mysql: - image: docker.io/library/mariadb:12.2.2-noble - env: - MARIADB_USER: bookstack-test - MARIADB_PASSWORD: bookstack-test - MARIADB_DATABASE: bookstack-test - MARIADB_ROOT_PASSWORD: password + env: + phpextensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp + phpextensioncachekey: cache-v1 steps: - uses: https://code.forgejo.org/actions/checkout@v6 + - name: Setup cache environment + id: extcache + uses: https://github.com/shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php }} + extensions: ${{ env.phpextensions }} + key: ${{ env.phpextensioncachekey }} + + - name: Cache extensions + uses: https://code.forgejo.org/actions/cache@v5 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + - name: Setup PHP uses: https://github.com/shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp + extensions: ${{ env.phpextensions }} - name: Get Composer Cache Directory id: composer-cache @@ -54,14 +64,25 @@ jobs: env: COMPOSER_AUTH: '{"github-oauth": {"github.com": "${{ secrets.GH_TOKEN }}"}}' + - name: Start MySQL + run: | + sudo systemctl start mysql + + - name: Create database & user + run: | + mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;' + mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';" + mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';" + mysql -uroot -proot -e 'FLUSH PRIVILEGES;' + - name: Migrate and seed the database env: - TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@localhost/bookstack-test' run: | php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing php${{ matrix.php }} artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing - name: Run PHP tests env: - TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@mysql/bookstack-test' + TEST_DATABASE_URL: 'mysql://bookstack-test:bookstack-test@localhost/bookstack-test' run: php${{ matrix.php }} ./vendor/bin/phpunit diff --git a/package.json b/package.json index 80810fff1..2ffebc36a 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "lint": "eslint \"resources/**/*.js\" \"resources/**/*.mjs\"", "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\"", "ts:lint": "tsc --noEmit", - "test": "jest" + "test": "jest", + "test:ci": "jest --maxWorkers=1" }, "devDependencies": { "@eslint/js": "^10.0.1", From 6917eaf7bd35c125304acdc20bd4c5ceff3931d4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 26 Apr 2026 11:48:05 +0100 Subject: [PATCH 44/74] Lexical: Added support for keyCode-based fallback shortcut use Helps in cases where languages like cyrillic may have the relevant key to use but the actual text/.key value is the cyrillic key value instead of the shorcut key we expect. --- resources/js/wysiwyg/services/shortcuts.ts | 70 ++++++++++++++++++---- 1 file changed, 59 insertions(+), 11 deletions(-) diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts index 00abe0c6d..2702b5486 100644 --- a/resources/js/wysiwyg/services/shortcuts.ts +++ b/resources/js/wysiwyg/services/shortcuts.ts @@ -94,30 +94,78 @@ const extendedActionsByKeys: Record = { }; function createKeyDownListener(context: EditorUiContext, useExtended: boolean): (e: KeyboardEvent) => void { - const keySetToUse = useExtended ? extendedActionsByKeys : baseActionsByKeys; + const baseKeySetToUse = useExtended ? extendedActionsByKeys : baseActionsByKeys; + const keySetToUse = extendKeySetWithKeyCodes(baseKeySetToUse); return (event: KeyboardEvent) => { - const combo = keyboardEventToKeyComboString(event); - // console.log(`pressed: ${combo}`); - if (keySetToUse[combo]) { - const handled = keySetToUse[combo](context.editor, context); - if (handled) { - event.stopPropagation(); - event.preventDefault(); + const comboStrings = keyboardEventToKeyComboStrings(event); + // console.log(comboStrings, event, keySetToUse); + for (const combo of comboStrings) { + if (keySetToUse[combo]) { + const handled = keySetToUse[combo](context.editor, context); + if (handled) { + event.stopPropagation(); + event.preventDefault(); + } + break; } } }; } -function keyboardEventToKeyComboString(event: KeyboardEvent): string { +/** + * Takes a shortcut key set and returns a new set with added variations of shortcts where + * they can be sensibly represented as their key code instead of just key, which we can use + * for matching in scenarios where the physical key may be represented of the letter used + * in the shortcut, but produces a different 'key' value. + * Useful for Cyrillic scenarios where the keyboard key would show a latin character + * as an option, and therefore be expected for use for the relevant latin shortcut, but the main + * key output is a Cyrillic character. + */ +function extendKeySetWithKeyCodes(keySet: Record): Record { + const newKeys: Record = {}; + + const setKeys = Object.keys(keySet); + for (const keyCombo of setKeys) { + const action = keySet[keyCombo]; + newKeys[keyCombo] = action; + + const comboParts = keyCombo.split('+'); + const lastComboPart = comboParts.pop() || ''; + if (lastComboPart.match(/^[a-zA-Z]$/)) { + const keyCode = lastComboPart.toUpperCase().charCodeAt(0); + comboParts.push(String(keyCode)); + const newCombo = comboParts.join('+'); + newKeys[newCombo] = action; + } + } + + return newKeys; +} + +function keyboardEventToKeyComboStrings(event: KeyboardEvent): string[] { const metaKeyPressed = isMac() ? event.metaKey : event.ctrlKey; - const parts = [ + const mainParts = [ metaKeyPressed ? 'meta' : '', event.shiftKey ? 'shift' : '', event.key, ]; - return parts.filter(Boolean).join('+').toLowerCase(); + const toReturn = [ + mainParts.filter(Boolean).join('+').toLowerCase(), + ]; + + // If ending with a standard latin character, provide an alternative + // keyCode based option for scenarios of dual-language keyboard use. + const keyCode = event.keyCode || 0; + if (keyCode >= 65 && keyCode <= 90) { + const keyCodeParts = [...mainParts]; + keyCodeParts.pop(); + keyCodeParts.push(String(keyCode)); + toReturn.push(keyCodeParts.filter(Boolean).join('+').toLowerCase()); + } + + return toReturn; } function isMac(): boolean { From f1452ebe2a70829a16a85cb50c15ec3d9c9b959e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 4 May 2026 20:52:29 +0100 Subject: [PATCH 45/74] Lexical: Improved content insert on drop handling - Adds specific support for inline content handling. - Adds attempting to use caret position at drop location for accurate placement. --- .../wysiwyg/services/drop-paste-handling.ts | 34 +++++++--------- resources/js/wysiwyg/utils/nodes.ts | 22 +++++++++++ resources/js/wysiwyg/utils/selection.ts | 39 +++++++++++++++++++ 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/resources/js/wysiwyg/services/drop-paste-handling.ts b/resources/js/wysiwyg/services/drop-paste-handling.ts index 57f9a80ae..b63c57ada 100644 --- a/resources/js/wysiwyg/services/drop-paste-handling.ts +++ b/resources/js/wysiwyg/services/drop-paste-handling.ts @@ -1,40 +1,35 @@ import { $createParagraphNode, $insertNodes, - $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND, + $isDecoratorNode, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, DROP_COMMAND, LexicalEditor, LexicalNode, PASTE_COMMAND } from "lexical"; -import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection"; -import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes"; +import {$insertNewNodesAtSelection, $selectSingleNode} from "../utils/selection"; +import {$getNodePositionFromMouseEvent, $htmlToBlockNodes} from "../utils/nodes"; import {Clipboard} from "../../services/clipboard"; import {$createImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$createLinkNode} from "@lexical/link"; import {EditorImageData, uploadImageFile} from "../utils/images"; import {EditorUiContext} from "../ui/framework/core"; -function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null { - const x = event.clientX; - const y = event.clientY; - const dom = document.elementFromPoint(x, y); - if (!dom) { - return null; - } - - return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY); -} function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) { - const positionNode = $getNodeFromMouseEvent(event, editor); + const position = $getNodePositionFromMouseEvent(event, editor); - if (positionNode) { - $selectSingleNode(positionNode); + if (position && $isTextNode(position.node)) { + const selection = position.node.select(position.offset, position.offset); + $setSelection(selection); + } else if (position) { + $selectSingleNode(position.node); } - $insertNewBlockNodesAtSelection(nodes, true); + $insertNewNodesAtSelection(nodes); - if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) { - positionNode?.remove(); + if (position) { + if (!$isDecoratorNode(position.node) && !position.node?.getTextContent()) { + position.node.remove(); + } } } @@ -113,6 +108,7 @@ function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): bo function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean { const editor = context.editor; return (event: DragEvent): boolean => { + // Template handling const templateId = event.dataTransfer?.getData('bookstack/template') || ''; if (templateId) { diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index ed70bf699..eab8203a0 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -1,5 +1,6 @@ import { $createParagraphNode, + $getNearestNodeFromDOMNode, $getRoot, $isDecoratorNode, $isElementNode, $isRootNode, @@ -64,6 +65,27 @@ export function $getAllNodesOfType(matcher: LexicalNodeMatcher, root?: ElementNo return matches; } +/** + * Get the node based on the given mouse event. + */ +export function $getNodePositionFromMouseEvent(event: MouseEvent, editor: LexicalEditor): {node: LexicalNode, offset: number}|null { + const x = event.clientX; + const y = event.clientY; + const caretPosition = window.document.caretPositionFromPoint(event.x, event.y); + if (!caretPosition) { + const backup = $getNearestBlockNodeForCoords(editor, x, y); + return backup ? {node: backup, offset: 0} : null; + } + + const node = $getNearestNodeFromDOMNode(caretPosition.offsetNode); + if (!node) { + const backup = $getNearestBlockNodeForCoords(editor, x, y); + return backup ? {node: backup, offset: 0} : null; + } + + return {node, offset: caretPosition.offset}; +} + /** * Get the nearest root/block level node for the given position. */ diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index e4b5bf2dc..f5d3699fc 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -109,6 +109,45 @@ export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfte } } +export function $insertNewNodesAtSelection(nodes: LexicalNode[]) { + const selection = $getSelection(); + const selectionPoints = selection?.getStartEndPoints(); + let target: LexicalNode|null = null; + let targetBlock: LexicalNode|null = null; + if (selectionPoints) { + const selectionEnd = selectionPoints[1]; + target = selectionEnd.getNode(); + targetBlock = target ? $getNearestNodeBlockParent(target) : null; + } + + for (const node of nodes) { + const isBlock = $isBlockElementNode(node); + + if (isBlock && !targetBlock) { + // Append to the root if its a block and we can't determine position + $getRoot().append(node); + } else if (isBlock && targetBlock) { + // Insert after the target block if we have a block + targetBlock.insertAfter(node); + } else if (!isBlock && selection) { + // Insert at selection if likely inline + selection.insertNodes(nodes); + } else if (!isBlock && $isElementNode(targetBlock)) { + // Append inside the target block if inline but we don't have + // a selection (typically used by the case below) + targetBlock.append(node); + } else { + // Otherwise (where inline) create a new root level paragraph + // and insert content into that. Update the target block + // for re-use by other inline elements. + const paragraph = $createParagraphNode(); + paragraph.append(node); + $getRoot().append(paragraph); + targetBlock = paragraph; + } + } +} + export function $selectSingleNode(node: LexicalNode) { const nodeSelection = $createNodeSelection(); nodeSelection.add(node.getKey()); From 0eed869735eec0f26bb911cd742ad74ccb63e5e4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 4 May 2026 22:32:19 +0100 Subject: [PATCH 46/74] Lexical: Fixed in-editor content drag and drop Now more reliable and aligned to expectations, instead of loosing information and/or deleting elements, or inserting above/below blocks. --- .../wysiwyg/services/drop-paste-handling.ts | 61 ++++++++++++++++--- resources/js/wysiwyg/utils/selection.ts | 36 ++++------- 2 files changed, 66 insertions(+), 31 deletions(-) diff --git a/resources/js/wysiwyg/services/drop-paste-handling.ts b/resources/js/wysiwyg/services/drop-paste-handling.ts index b63c57ada..e11d6bd5e 100644 --- a/resources/js/wysiwyg/services/drop-paste-handling.ts +++ b/resources/js/wysiwyg/services/drop-paste-handling.ts @@ -1,18 +1,21 @@ import { - $createParagraphNode, + $createParagraphNode, $getSelection, $insertNodes, - $isDecoratorNode, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, DROP_COMMAND, + $isDecoratorNode, + $isRangeSelection, $isTextNode, $setSelection, COMMAND_PRIORITY_HIGH, DRAGSTART_COMMAND, DROP_COMMAND, LexicalEditor, LexicalNode, PASTE_COMMAND } from "lexical"; -import {$insertNewNodesAtSelection, $selectSingleNode} from "../utils/selection"; -import {$getNodePositionFromMouseEvent, $htmlToBlockNodes} from "../utils/nodes"; +import {$getBlockElementNodesInSelection, $insertNewNodesAtSelection, $selectSingleNode} from "../utils/selection"; +import {$getNodePositionFromMouseEvent, $htmlToBlockNodes, $htmlToNodes} from "../utils/nodes"; import {Clipboard} from "../../services/clipboard"; import {$createImageNode} from "@lexical/rich-text/LexicalImageNode"; import {$createLinkNode} from "@lexical/link"; import {EditorImageData, uploadImageFile} from "../utils/images"; import {EditorUiContext} from "../ui/framework/core"; +import {$getHtmlContent} from "@lexical/clipboard"; +const internalActiveDragTracker: WeakMap = new WeakMap(); function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) { const position = $getNodePositionFromMouseEvent(event, editor); @@ -44,6 +47,26 @@ async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, }); } +function insertHtmlToEditor(editor: LexicalEditor, html: string, isFromInternal: boolean, event: DragEvent) { + editor.update(() => { + if (isFromInternal) { + const selected = $getSelection(); + if ($isRangeSelection(selected)) { + selected.removeText(); + const selectionBlocks = $getBlockElementNodesInSelection(selected); + for (const block of selectionBlocks) { + if (block.isEmpty()) { + block.remove(); + } + } + } + } + + const newNodes = $htmlToNodes(editor, html); + $insertNodesAtEvent(newNodes, event, editor); + }); +} + function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolean { const clipboard = new Clipboard(data); let handled = false; @@ -109,6 +132,9 @@ function createDropListener(context: EditorUiContext): (event: DragEvent) => boo const editor = context.editor; return (event: DragEvent): boolean => { + const hadInternalActiveDrag = internalActiveDragTracker.has(editor); + internalActiveDragTracker.delete(editor); + // Template handling const templateId = event.dataTransfer?.getData('bookstack/template') || ''; if (templateId) { @@ -121,10 +147,7 @@ function createDropListener(context: EditorUiContext): (event: DragEvent) => boo // HTML contents drop const html = event.dataTransfer?.getData('text/html') || ''; if (html) { - editor.update(() => { - const newNodes = $htmlToBlockNodes(editor, html); - $insertNodesAtEvent(newNodes, event, editor); - }); + insertHtmlToEditor(editor, html, hadInternalActiveDrag, event); event.preventDefault(); event.stopPropagation(); return true; @@ -161,17 +184,39 @@ function createPasteListener(context: EditorUiContext): (event: ClipboardEvent) }; } +function createDragStartListener(context: EditorUiContext): (event: DragEvent) => boolean { + return (event: DragEvent) => { + // Track when drag events are started internally from the editor + internalActiveDragTracker.set(context.editor, event); + + // If an internal range selection, serialize the range contents + // fully as output HTML, instead of editor HTML + context.editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.extract(); + const html = $getHtmlContent(context.editor, selection); + event.dataTransfer?.setData('text/html', html); + } + }); + return false; + }; +} + export function registerDropPasteHandling(context: EditorUiContext): () => void { const dropListener = createDropListener(context); const pasteListener = createPasteListener(context); + const dragstartListener = createDragStartListener(context); const unregisterDrop = context.editor.registerCommand(DROP_COMMAND, dropListener, COMMAND_PRIORITY_HIGH); const unregisterPaste = context.editor.registerCommand(PASTE_COMMAND, pasteListener, COMMAND_PRIORITY_HIGH); + const unregisterDragStart = context.editor.registerCommand(DRAGSTART_COMMAND, dragstartListener, COMMAND_PRIORITY_HIGH); context.scrollDOM.addEventListener('drop', dropListener); return () => { unregisterDrop(); unregisterPaste(); + unregisterDragStart(); context.scrollDOM.removeEventListener('drop', dropListener); }; } \ No newline at end of file diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index f5d3699fc..28050571e 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -111,38 +111,28 @@ export function $insertNewBlockNodesAtSelection(nodes: LexicalNode[], insertAfte export function $insertNewNodesAtSelection(nodes: LexicalNode[]) { const selection = $getSelection(); - const selectionPoints = selection?.getStartEndPoints(); - let target: LexicalNode|null = null; - let targetBlock: LexicalNode|null = null; - if (selectionPoints) { - const selectionEnd = selectionPoints[1]; - target = selectionEnd.getNode(); - targetBlock = target ? $getNearestNodeBlockParent(target) : null; + if (selection) { + selection.insertNodes(nodes); + return; } + // Do something relatively sensible if we don't have a selection within view + const root = $getRoot(); + let targetBlock = root.getLastChild(); for (const node of nodes) { const isBlock = $isBlockElementNode(node); - if (isBlock && !targetBlock) { - // Append to the root if its a block and we can't determine position - $getRoot().append(node); - } else if (isBlock && targetBlock) { - // Insert after the target block if we have a block - targetBlock.insertAfter(node); - } else if (!isBlock && selection) { - // Insert at selection if likely inline - selection.insertNodes(nodes); - } else if (!isBlock && $isElementNode(targetBlock)) { - // Append inside the target block if inline but we don't have - // a selection (typically used by the case below) + root.append(node); + targetBlock = node; + } else if (isBlock) { + targetBlock?.insertAfter(node); + targetBlock = node; + } else if ($isElementNode(targetBlock)) { targetBlock.append(node); } else { - // Otherwise (where inline) create a new root level paragraph - // and insert content into that. Update the target block - // for re-use by other inline elements. const paragraph = $createParagraphNode(); paragraph.append(node); - $getRoot().append(paragraph); + root.append(paragraph); targetBlock = paragraph; } } From df831a0564bee28294e093f8a9e509c1528e8ddf Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 5 May 2026 16:37:24 +0100 Subject: [PATCH 47/74] Lexical: Added RTL support for UI dropdown menus They now show in the correct direction and do not overlap. Added new helper for RTL bounding box handling. --- .../ui/framework/blocks/table-creator.ts | 2 +- .../wysiwyg/ui/framework/helpers/dropdowns.ts | 44 ++++++++++------- resources/js/wysiwyg/ui/framework/manager.ts | 1 + resources/js/wysiwyg/utils/rtl.ts | 49 +++++++++++++++++++ 4 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 resources/js/wysiwyg/utils/rtl.ts diff --git a/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts b/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts index 6f026ca18..03e55ce64 100644 --- a/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts +++ b/resources/js/wysiwyg/ui/framework/blocks/table-creator.ts @@ -27,7 +27,7 @@ export class EditorTableCreator extends EditorUiElement { }, rowCells)); } - const display = el('div', {class: 'editor-table-creator-display'}, ['0 x 0']); + const display = el('div', {class: 'editor-table-creator-display', dir: 'ltr'}, ['0 x 0']); const grid = el('div', {class: 'editor-table-creator-grid'}, rows); grid.addEventListener('mousemove', event => { const cell = (event.target as HTMLElement).closest('.editor-table-creator-cell') as HTMLElement|null; diff --git a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts index 890d5b325..f86482fdc 100644 --- a/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts +++ b/resources/js/wysiwyg/ui/framework/helpers/dropdowns.ts @@ -1,3 +1,5 @@ +import {getViewportRect, RTLRect} from "../../../utils/rtl"; + interface HandleDropdownParams { toggle: HTMLElement; menu: HTMLElement; @@ -7,30 +9,31 @@ interface HandleDropdownParams { showAside?: boolean; } -function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean) { - const toggleRect = toggle.getBoundingClientRect(); - const menuBounds = menu.getBoundingClientRect(); +function positionMenu(menu: HTMLElement, toggle: HTMLElement, showAside: boolean, isRTL: boolean) { + const toggleRect = new RTLRect(toggle, isRTL); + const menuBounds = new RTLRect(menu, isRTL); + const viewport = getViewportRect(); menu.style.position = 'fixed'; if (showAside) { - let targetLeft = toggleRect.right; - const isRightOOB = toggleRect.right + menuBounds.width > window.innerWidth; - if (isRightOOB) { - targetLeft = Math.max(toggleRect.left - menuBounds.width, 0); + let targetLeft = toggleRect.inlineEnd; + const isEndOOB = toggleRect.inlineEnd + menuBounds.width > viewport.width; + if (isEndOOB) { + targetLeft = Math.max(toggleRect.inlineStart - menuBounds.width, 0); } - menu.style.top = toggleRect.top + 'px'; - menu.style.left = targetLeft + 'px'; + menu.style.top = toggleRect.blockStart + 'px'; + menu.style.insetInlineStart = targetLeft + 'px'; } else { - const isRightOOB = toggleRect.left + menuBounds.width > window.innerWidth; - let targetLeft = toggleRect.left; - if (isRightOOB) { - targetLeft = Math.max(toggleRect.right - menuBounds.width, 0); + const isEndOOB = toggleRect.inlineStart + menuBounds.width > viewport.width; + let targetLeft = toggleRect.inlineStart; + if (isEndOOB) { + targetLeft = Math.max(toggleRect.inlineEnd - menuBounds.width, 0); } - menu.style.top = toggleRect.bottom + 'px'; - menu.style.left = targetLeft + 'px'; + menu.style.top = toggleRect.blockEnd + 'px'; + menu.style.insetInlineStart = targetLeft + 'px'; } } @@ -38,6 +41,7 @@ export class DropDownManager { protected dropdownOptions: WeakMap = new WeakMap(); protected openDropdowns: Set = new Set(); + protected isRTL: boolean = false; constructor() { this.onMenuMouseOver = this.onMenuMouseOver.bind(this); @@ -46,6 +50,10 @@ export class DropDownManager { window.addEventListener('click', this.onWindowClick); } + setIsRTL(isRTL: boolean): void { + this.isRTL = isRTL; + } + teardown(): void { window.removeEventListener('click', this.onWindowClick); } @@ -80,7 +88,7 @@ export class DropDownManager { protected closeDropdown(menu: HTMLElement): void { menu.hidden = true; menu.style.removeProperty('position'); - menu.style.removeProperty('left'); + menu.style.removeProperty('inset-inline-start'); menu.style.removeProperty('top'); this.openDropdowns.delete(menu); @@ -94,8 +102,8 @@ export class DropDownManager { protected openDropdown(menu: HTMLElement): void { const {toggle, showAside, onOpen} = this.getOptions(menu); - menu.hidden = false - positionMenu(menu, toggle, Boolean(showAside)); + menu.hidden = false; + positionMenu(menu, toggle, Boolean(showAside), this.isRTL); this.openDropdowns.add(menu); menu.addEventListener('mouseover', this.onMenuMouseOver); diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index 3b4d5b495..f8525bc7c 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -30,6 +30,7 @@ export class EditorUIManager { this.context = context; this.setupEventListeners(); this.setupEditor(context.editor, context); + this.dropdowns.setIsRTL(this.context.manager.getDefaultDirection() === 'rtl'); } getContext(): EditorUiContext { diff --git a/resources/js/wysiwyg/utils/rtl.ts b/resources/js/wysiwyg/utils/rtl.ts new file mode 100644 index 000000000..e04554f34 --- /dev/null +++ b/resources/js/wysiwyg/utils/rtl.ts @@ -0,0 +1,49 @@ +/** + * Create a viewport relative rect for an element which provides + * logical property support. + */ +export class RTLRect { + protected rect: DOMRect; + protected isRTL: boolean; + + constructor(element: HTMLElement, isRTL = false) { + this.rect = element.getBoundingClientRect(); + this.isRTL = isRTL; + } + + get blockStart(): number { + return this.rect.top; + } + + get inlineStart(): number { + if (!this.isRTL) { + return this.rect.left; + } + + return window.innerWidth - this.rect.right; + } + + get blockEnd(): number { + return this.rect.bottom; + } + + get inlineEnd(): number { + if (!this.isRTL) { + return this.rect.right; + } + + return window.innerWidth - this.rect.left; + } + + get width(): number { + return this.rect.width; + } + + get height(): number { + return this.rect.height; + } +} + +export function getViewportRect(): RTLRect { + return new RTLRect(document.documentElement, false); +} From b794f749ddbfb9d84ed53602f18b92c63d3e6e6f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 5 May 2026 20:03:53 +0100 Subject: [PATCH 48/74] Lexical: Updated core inline formats to instead custom built handler Aligns logic used for shortcut handling, so enables these to work for cyrillic equivilent keyboard keys. --- .../js/wysiwyg/lexical/core/LexicalEvents.ts | 13 -------- .../js/wysiwyg/lexical/core/LexicalUtils.ts | 33 ------------------- resources/js/wysiwyg/services/shortcuts.ts | 20 ++++++++--- 3 files changed, 15 insertions(+), 51 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalEvents.ts b/resources/js/wysiwyg/lexical/core/LexicalEvents.ts index 2d197ccc2..7d87d9055 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalEvents.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalEvents.ts @@ -99,7 +99,6 @@ import { getNearestEditorFromDOMNode, getWindow, isAt, isBackspace, - isBold, isCopy, isCut, isDelete, @@ -111,7 +110,6 @@ import { isDeleteWordForward, isEscape, isFirefoxClipboardEvents, - isItalic, isLexicalEditor, isLineBreak, isModifier, @@ -128,7 +126,6 @@ import { isSelectionWithinEditor, isSpace, isTab, - isUnderline, isUndo, } from './LexicalUtils'; @@ -479,7 +476,6 @@ function onClick(event: PointerEvent, editor: LexicalEditor): void { } function onPointerDown(event: PointerEvent, editor: LexicalEditor) { - // TODO implement text drag & drop const target = event.target; const pointerType = event.pointerType; if (target instanceof Node && pointerType !== 'touch') { @@ -1064,15 +1060,6 @@ function onKeyDown(event: KeyboardEvent, editor: LexicalEditor): void { dispatchCommand(editor, DELETE_LINE_COMMAND, false); } else if (isAt(key)) { dispatchCommand(editor, KEY_AT_COMMAND, event); - } else if (isBold(key, altKey, metaKey, ctrlKey)) { - event.preventDefault(); - dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'bold'); - } else if (isUnderline(key, altKey, metaKey, ctrlKey)) { - event.preventDefault(); - dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'underline'); - } else if (isItalic(key, altKey, metaKey, ctrlKey)) { - event.preventDefault(); - dispatchCommand(editor, FORMAT_TEXT_COMMAND, 'italic'); } else if (isTab(key, altKey, ctrlKey, metaKey)) { dispatchCommand(editor, KEY_TAB_COMMAND, event); } else if (isUndo(key, shiftKey, metaKey, ctrlKey)) { diff --git a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts index b0bf2f180..cff86a74a 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts @@ -783,39 +783,6 @@ export function isTab( return key === 'Tab' && !altKey && !ctrlKey && !metaKey; } -export function isBold( - key: string, - altKey: boolean, - metaKey: boolean, - ctrlKey: boolean, -): boolean { - return ( - key.toLowerCase() === 'b' && !altKey && controlOrMeta(metaKey, ctrlKey) - ); -} - -export function isItalic( - key: string, - altKey: boolean, - metaKey: boolean, - ctrlKey: boolean, -): boolean { - return ( - key.toLowerCase() === 'i' && !altKey && controlOrMeta(metaKey, ctrlKey) - ); -} - -export function isUnderline( - key: string, - altKey: boolean, - metaKey: boolean, - ctrlKey: boolean, -): boolean { - return ( - key.toLowerCase() === 'u' && !altKey && controlOrMeta(metaKey, ctrlKey) - ); -} - export function isParagraph(key: string, shiftKey: boolean): boolean { return isReturn(key) && !shiftKey; } diff --git a/resources/js/wysiwyg/services/shortcuts.ts b/resources/js/wysiwyg/services/shortcuts.ts index 2702b5486..f66665703 100644 --- a/resources/js/wysiwyg/services/shortcuts.ts +++ b/resources/js/wysiwyg/services/shortcuts.ts @@ -1,4 +1,11 @@ -import {$getSelection, COMMAND_PRIORITY_HIGH, FORMAT_TEXT_COMMAND, KEY_ENTER_COMMAND, LexicalEditor} from "lexical"; +import { + $getSelection, + COMMAND_PRIORITY_HIGH, + FORMAT_TEXT_COMMAND, + KEY_ENTER_COMMAND, + LexicalEditor, + TextFormatType +} from "lexical"; import { cycleSelectionCalloutFormats, formatCodeBlock, insertOrUpdateLink, @@ -27,8 +34,8 @@ function wrapFormatAction(formatAction: (editor: LexicalEditor) => any): Shortcu }; } -function toggleInlineCode(editor: LexicalEditor): boolean { - editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code'); +function toggleInlineFormat(editor: LexicalEditor, format: TextFormatType): boolean { + editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); return true; } @@ -39,8 +46,11 @@ type ShortcutAction = (editor: LexicalEditor, context: EditorUiContext) => boole * We use "meta" as an abstraction for ctrl/cmd depending on platform. */ const baseActionsByKeys: Record = { - 'meta+8': toggleInlineCode, - 'meta+shift+e': toggleInlineCode, + 'meta+8': (e) => toggleInlineFormat(e, 'code'), + 'meta+shift+e': (e) => toggleInlineFormat(e, 'code'), + 'meta+b': (e) => toggleInlineFormat(e, 'bold'), + 'meta+i': (e) => toggleInlineFormat(e, 'italic'), + 'meta+u': (e) => toggleInlineFormat(e, 'underline'), 'meta+o': wrapFormatAction((e) => toggleSelectionAsList(e, 'number')), 'meta+p': wrapFormatAction((e) => toggleSelectionAsList(e, 'bullet')), 'meta+k': (editor, context) => { From d6b114de7489ca4df591a79b312671555b9e4e14 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 6 May 2026 00:25:43 +0100 Subject: [PATCH 49/74] Lexical: Added some test coverage for shortcut handling Updates existing keydown test helper to accept other event options. --- .../lexical/core/__tests__/utils/index.ts | 15 ++- .../services/__tests__/shortcuts.test.ts | 91 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 resources/js/wysiwyg/services/__tests__/shortcuts.test.ts diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index ab54bdb31..a30c86fcc 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -837,22 +837,31 @@ function formatHtml(s: string): string { return s.replace(/>\s+<').replace(/\s*\n\s*/g, ' ').trim(); } -export function dispatchKeydownEventForNode(node: LexicalNode, editor: LexicalEditor, key: string) { +interface TestKeyboardEventOptions { + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; + metaKey?: boolean; + keyCode?: number; +} + +export function dispatchKeydownEventForNode(node: LexicalNode, editor: LexicalEditor, key: string, options: TestKeyboardEventOptions = {}) { const nodeDomEl = editor.getElementByKey(node.getKey()); const event = new KeyboardEvent('keydown', { bubbles: true, cancelable: true, key, + ...options, }); nodeDomEl?.dispatchEvent(event); editor.commitUpdates(); } -export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key: string) { +export function dispatchKeydownEventForSelectedNode(editor: LexicalEditor, key: string, options: TestKeyboardEventOptions = {}) { editor.getEditorState().read((): void => { const node = $getSelection()?.getNodes()[0] || null; if (node) { - dispatchKeydownEventForNode(node, editor, key); + dispatchKeydownEventForNode(node, editor, key, options); } }); } diff --git a/resources/js/wysiwyg/services/__tests__/shortcuts.test.ts b/resources/js/wysiwyg/services/__tests__/shortcuts.test.ts new file mode 100644 index 000000000..f7e7298e8 --- /dev/null +++ b/resources/js/wysiwyg/services/__tests__/shortcuts.test.ts @@ -0,0 +1,91 @@ +import { + createTestContext, destroyFromContext, + dispatchKeydownEventForSelectedNode, expectEditorStateJSONPropToEqual, +} from "lexical/__tests__/utils"; +import { + $createParagraphNode, $createTextNode, + $getRoot, IS_BOLD, LexicalEditor, +} from "lexical"; +import {registerRichText} from "@lexical/rich-text"; +import {EditorUiContext} from "../../ui/framework/core"; +import {registerShortcuts} from "../shortcuts"; + +describe('Keyboard-handling service tests', () => { + + let context!: EditorUiContext; + let editor!: LexicalEditor; + + beforeEach(() => { + context = createTestContext(); + editor = context.editor; + registerRichText(editor); + registerShortcuts(context, true); + }); + + afterEach(() => { + destroyFromContext(context); + }); + + test('Basic block format shortcuts works', () => { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + p.append($createTextNode('Hello World')) + $getRoot().append(p); + p.select(); + }); + + dispatchKeydownEventForSelectedNode(editor, '1', {ctrlKey: true}); + + expectEditorStateJSONPropToEqual(editor, '0.type', 'heading'); + expectEditorStateJSONPropToEqual(editor, '0.tag', 'h2'); + + dispatchKeydownEventForSelectedNode(editor, '2', {ctrlKey: true}); + + expectEditorStateJSONPropToEqual(editor, '0.type', 'heading'); + expectEditorStateJSONPropToEqual(editor, '0.tag', 'h3'); + + dispatchKeydownEventForSelectedNode(editor, 'd', {ctrlKey: true}); + + expectEditorStateJSONPropToEqual(editor, '0.type', 'paragraph'); + expectEditorStateJSONPropToEqual(editor, '0.0.text', 'Hello World'); + }); + + test('Basic bold format shortcut works', () => { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + const text = $createTextNode('Hello World'); + p.append(text) + $getRoot().append(p); + text.select(0, 5); + }); + + // Toggle bold for selection + dispatchKeydownEventForSelectedNode(editor, 'b', {ctrlKey: true}); + expectEditorStateJSONPropToEqual(editor, '0.0.format', IS_BOLD); + expectEditorStateJSONPropToEqual(editor, '0.1.format', 0); + + // Untoggle bold for selection + dispatchKeydownEventForSelectedNode(editor, 'b', {ctrlKey: true}); + expectEditorStateJSONPropToEqual(editor, '0.0.format', 0); + }); + + test('Basic bold format shortcut works when using cyrillic equivalent keys', () => { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + const text = $createTextNode('Hello World'); + p.append(text) + $getRoot().append(p); + text.select(0, 5); + }); + + // Toggle bold for selection + dispatchKeydownEventForSelectedNode(editor, 'и', {ctrlKey: true, keyCode: 66}); + expectEditorStateJSONPropToEqual(editor, '0.0.format', IS_BOLD); + expectEditorStateJSONPropToEqual(editor, '0.1.format', 0); + + // Untoggle bold for selection + dispatchKeydownEventForSelectedNode(editor, 'и', {ctrlKey: true, keyCode: 66}); + expectEditorStateJSONPropToEqual(editor, '0.0.format', 0); + }); + +}); From dc8f80365cf6e540faccda26079e64c31fc76c6f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 12:25:49 +0100 Subject: [PATCH 50/74] Lexical: Added missing table header row toggle button Also updated DOM converstion, and CSS, to make lexical format (th inside tbody) somewhat compatible/convertible with the tinymce format (td inside thead). --- resources/icons/editor/table-header.svg | 1 + .../lexical/table/LexicalTableCellNode.ts | 9 ++++--- .../js/wysiwyg/ui/defaults/buttons/tables.ts | 27 +++++++++++++++++-- resources/js/wysiwyg/ui/defaults/toolbars.ts | 15 ++++++++++- resources/js/wysiwyg/utils/tables.ts | 20 +++++++++++--- resources/sass/_tables.scss | 13 ++++++--- 6 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 resources/icons/editor/table-header.svg diff --git a/resources/icons/editor/table-header.svg b/resources/icons/editor/table-header.svg new file mode 100644 index 000000000..e3cfcbb63 --- /dev/null +++ b/resources/icons/editor/table-header.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts index 1c9d7ecf6..0e714e70f 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableCellNode.ts @@ -334,10 +334,13 @@ export function $convertTableCellNodeElement( width = parseFloat(domNode_.style.width); } + let isHeader = nodeName === 'th'; + if (domNode instanceof HTMLElement && domNode.closest('thead')) { + isHeader = true; + } + const tableCellNode = $createTableCellNode( - nodeName === 'th' - ? TableCellHeaderStates.ROW - : TableCellHeaderStates.NO_STATUS, + isHeader ? TableCellHeaderStates.ROW : TableCellHeaderStates.NO_STATUS, domNode_.colSpan, width, ); diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts index 2e4883d88..0e01b301c 100644 --- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts +++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts @@ -7,6 +7,7 @@ import insertColumnAfterIcon from "@icons/editor/table-insert-column-after.svg"; import insertColumnBeforeIcon from "@icons/editor/table-insert-column-before.svg"; import insertRowAboveIcon from "@icons/editor/table-insert-row-above.svg"; import insertRowBelowIcon from "@icons/editor/table-insert-row-below.svg"; +import tableHeaderIcon from "@icons/editor/table-header.svg"; import {EditorUiContext} from "../../framework/core"; import {$getSelection, BaseSelection} from "lexical"; import { @@ -14,7 +15,7 @@ import { $deleteTableRow__EXPERIMENTAL, $insertTableColumn__EXPERIMENTAL, $insertTableRow__EXPERIMENTAL, $isTableCellNode, - $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellNode, + $isTableNode, $isTableRowNode, $isTableSelection, $unmergeCell, TableCellHeaderStates, TableCellNode, } from "@lexical/table"; import {$getNodeFromSelection, $selectionContainsNodeType} from "../../../utils/selection"; import {$getParentOfType} from "../../../utils/nodes"; @@ -23,7 +24,7 @@ import { $clearTableFormatting, $clearTableSizes, $getTableFromSelection, $getTableRowsFromSelection, - $mergeTableCellsInSelection + $mergeTableCellsInSelection, $toggleRowCellHeaderState } from "../../../utils/tables"; import { $copySelectedColumnsToClipboard, @@ -239,6 +240,28 @@ export const pasteRowAfter: EditorButtonDefinition = { isDisabled: (selection) => cellNotSelected(selection) || isRowClipboardEmpty(), }; +export const toggleRowHeaders: EditorButtonDefinition = { + label: 'Row header', + format: 'small', + icon: tableHeaderIcon, + action(context: EditorUiContext, button) { + context.editor.update(() => { + const row = $getNodeFromSelection($getSelection(), $isTableCellNode)?.getParent(); + if (!$isTableRowNode(row)) { + return; + } + + const isNowHeader = $toggleRowCellHeaderState(row); + button.setActiveState(isNowHeader); + }); + }, + isActive: (selection) => { + return $selectionContainsNodeType(selection, (node) => { + return $isTableCellNode(node) && node.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS; + }); + } +}; + export const cutColumn: EditorButtonDefinition = { label: 'Cut column', format: 'long', diff --git a/resources/js/wysiwyg/ui/defaults/toolbars.ts b/resources/js/wysiwyg/ui/defaults/toolbars.ts index a3ada5c89..99d3b96d4 100644 --- a/resources/js/wysiwyg/ui/defaults/toolbars.ts +++ b/resources/js/wysiwyg/ui/defaults/toolbars.ts @@ -28,7 +28,7 @@ import { pasteRowBefore, resizeTableToContents, rowProperties, splitCell, - table, tableProperties + table, tableProperties, toggleRowHeaders } from "./buttons/tables"; import {about, fullscreen, redo, source, undo} from "./buttons/controls"; import { @@ -284,6 +284,19 @@ export const contextToolbars: Record = { return originalTarget.closest('table') as HTMLTableElement; } }, + table_header: { + selector: 'table tr:first-of-type td, table tr:first-of-type th', + content() { + return [ + new EditorOverflowContainer('table_headers', 1, [ + new EditorButton(toggleRowHeaders), + ]), + ]; + }, + displayTargetLocator(originalTarget: HTMLElement): HTMLElement { + return originalTarget.closest('table') as HTMLTableElement; + } + }, details: { selector: 'details', content() { diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts index 15cc3cbbe..b835614ab 100644 --- a/resources/js/wysiwyg/utils/tables.ts +++ b/resources/js/wysiwyg/utils/tables.ts @@ -3,7 +3,7 @@ import { $isTableCellNode, $isTableNode, $isTableRowNode, - $isTableSelection, TableCellNode, TableNode, + $isTableSelection, TableCellHeaderStates, TableCellNode, TableNode, TableRowNode, TableSelection, } from "@lexical/table"; @@ -328,9 +328,21 @@ export function $getCellPaddingForTable(table: TableNode): string { return padding || ''; } - - - +/** + * Toggle the header state of the cells in the provided row. + * Returns a boolean to indicate if the new state of the cells is as headers. + */ +export function $toggleRowCellHeaderState(row: TableRowNode): boolean { + const firstCell = row.getFirstChild(); + const isHeader = $isTableCellNode(firstCell) ? firstCell.getHeaderStyles() !== TableCellHeaderStates.NO_STATUS : false; + const cells = row.getChildren(); + for (const cell of cells) { + if ($isTableCellNode(cell)) { + cell.setHeaderStyles(isHeader ? TableCellHeaderStates.NO_STATUS : TableCellHeaderStates.ROW); + } + } + return !isHeader; +} diff --git a/resources/sass/_tables.scss b/resources/sass/_tables.scss index 16be32fb3..5ea520d73 100644 --- a/resources/sass/_tables.scss +++ b/resources/sass/_tables.scss @@ -4,10 +4,6 @@ table { min-width: 100px; max-width: 100%; - thead { - @include mixins.lightDark(background-color, #f8f8f8, #333); - font-weight: 500; - } td, th { min-width: 10px; padding: 6px 8px; @@ -22,6 +18,15 @@ table { } } +// Table Header styles. +// Initial selector for in-body th cells, intended to (somewhat) not conflict with +// previous approach (second selector) to table headers where +// we would target the thead specifically. +table:not(:has(thead)) th, table thead { + @include mixins.lightDark(background-color, #f8f8f8, #333); + font-weight: 500; +} + table.table { width: 100%; tr td, tr th { From 7254dc3ab54f6005a5075d4ce10d2d09fb597727 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 15:38:44 +0100 Subject: [PATCH 51/74] Lexical: Fixed diagrams not updating on edit Caused by lack of proper key use on clone --- resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts | 2 +- resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts index e69f97848..6be19c293 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalDiagramNode.ts @@ -27,7 +27,7 @@ export class DiagramNode extends DecoratorNode { } static clone(node: DiagramNode): DiagramNode { - const newNode = new DiagramNode(node.__drawingId, node.__drawingUrl); + const newNode = new DiagramNode(node.__drawingId, node.__drawingUrl, node.__key); newNode.__id = node.__id; return newNode; } diff --git a/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts b/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts index e46dcc312..58a4de1dc 100644 --- a/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts +++ b/resources/js/wysiwyg/ui/decorators/DiagramDecorator.ts @@ -1,5 +1,4 @@ import {EditorDecorator} from "../framework/decorator"; -import {EditorUiContext} from "../framework/core"; import {BaseSelection, CLICK_COMMAND, COMMAND_PRIORITY_NORMAL} from "lexical"; import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode"; import {$selectionContainsNode, $selectSingleNode} from "../../utils/selection"; From 1ef9b7d48fb9180dea11d3770e1e1e2ccdc1ab5f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 16:05:21 +0100 Subject: [PATCH 52/74] Lexical: Added a little testing coverage for DiagramNode --- .../js/wysiwyg/lexical/core/LexicalNode.ts | 6 ++-- .../lexical/core/__tests__/utils/index.ts | 4 +++ .../__tests__/unit/LexicalDiagramNode.test.ts | 33 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDiagramNode.test.ts diff --git a/resources/js/wysiwyg/lexical/core/LexicalNode.ts b/resources/js/wysiwyg/lexical/core/LexicalNode.ts index 95a723469..4aba2a22f 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalNode.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalNode.ts @@ -208,9 +208,9 @@ export class LexicalNode { } /** - * Clones this node, creating a new node with a different key - * and adding it to the EditorState (but not attaching it anywhere!). All nodes must - * implement this method. + * Clones this node, creating a new matching instance. + * Should be created with the existing node key if it exists. + * All nodes must implement this method. * */ static clone(_data: unknown): LexicalNode { diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index a30c86fcc..c18c4b4ca 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -39,6 +39,8 @@ import {EditorUiContext} from "../../../../ui/framework/core"; import {EditorUIManager} from "../../../../ui/framework/manager"; import {ImageNode} from "@lexical/rich-text/LexicalImageNode"; import {MediaNode} from "@lexical/rich-text/LexicalMediaNode"; +import {DiagramNode} from "@lexical/rich-text/LexicalDiagramNode"; +import {DiagramDecorator} from "../../../../ui/decorators/DiagramDecorator"; type TestEnv = { readonly container: HTMLDivElement; @@ -489,6 +491,7 @@ export function createTestContext(): EditorUiContext { nodes: [ ImageNode, MediaNode, + DiagramNode, ] }); @@ -509,6 +512,7 @@ export function createTestContext(): EditorUiContext { }; context.manager.setContext(context); + context.manager.registerDecoratorType('diagram', DiagramDecorator); return context; } diff --git a/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDiagramNode.test.ts b/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDiagramNode.test.ts new file mode 100644 index 000000000..e6bb244f4 --- /dev/null +++ b/resources/js/wysiwyg/lexical/rich-text/__tests__/unit/LexicalDiagramNode.test.ts @@ -0,0 +1,33 @@ +import {createTestContext} from "lexical/__tests__/utils"; +import {$createDiagramNode, DiagramNode} from "@lexical/rich-text/LexicalDiagramNode"; +import {$getHtmlContent} from "@lexical/clipboard"; +import {getEditorContentAsHtml} from "../../../../utils/actions"; +import {$getRoot} from "lexical"; + + +describe('LexicalDiagramNode', () => { + + test('clone creates new instance with same key', () => { + const {editor} = createTestContext(); + editor.updateAndCommit(() => { + const node = $createDiagramNode('10', 'https://example.com/barry.png'); + const clone = DiagramNode.clone(node); + + expect(node).not.toBe(clone); + expect(node.getKey()).toBe(clone.getKey()); + }); + }); + + test('output HTML format', async () => { + const {editor} = createTestContext(); + editor.updateAndCommit(() => { + const node = $createDiagramNode('10', 'https://example.com/barry.png'); + node.setId('cat-123'); + $getRoot().append(node); + }); + + const html = await getEditorContentAsHtml(editor); + expect(html).toBe(`
    `); + }); + +}); \ No newline at end of file From b53499932bdcfa29b1a6c20aaf9e52f3e49fc03c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 17:54:24 +0100 Subject: [PATCH 53/74] Lexical: Fixed actions not applying on empty state Updated editor to always attempt to start from at least a paragraph isntead of empty state. Improved focus on HTML change. --- .../js/wysiwyg/ui/defaults/forms/controls.ts | 6 +- .../wysiwyg/utils/__tests__/actions.test.ts | 107 ++++++++++++++++++ resources/js/wysiwyg/utils/actions.ts | 17 ++- .../pages/parts/wysiwyg-editor.blade.php | 2 +- 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 resources/js/wysiwyg/utils/__tests__/actions.test.ts diff --git a/resources/js/wysiwyg/ui/defaults/forms/controls.ts b/resources/js/wysiwyg/ui/defaults/forms/controls.ts index 8e7219d67..c92a16834 100644 --- a/resources/js/wysiwyg/ui/defaults/forms/controls.ts +++ b/resources/js/wysiwyg/ui/defaults/forms/controls.ts @@ -1,12 +1,16 @@ import {EditorFormDefinition} from "../../framework/forms"; import {EditorUiContext, EditorUiElement} from "../../framework/core"; -import {setEditorContentFromHtml} from "../../../utils/actions"; +import {focusEditor, setEditorContentFromHtml} from "../../../utils/actions"; import {ExternalContent} from "../../framework/blocks/external-content"; export const source: EditorFormDefinition = { submitText: 'Save', async action(formData, context: EditorUiContext) { setEditorContentFromHtml(context.editor, formData.get('source')?.toString() || ''); + context.editor.commitUpdates(); + window.requestAnimationFrame(() => { + focusEditor(context.editor); + }); return true; }, fields: [ diff --git a/resources/js/wysiwyg/utils/__tests__/actions.test.ts b/resources/js/wysiwyg/utils/__tests__/actions.test.ts new file mode 100644 index 000000000..9450638f5 --- /dev/null +++ b/resources/js/wysiwyg/utils/__tests__/actions.test.ts @@ -0,0 +1,107 @@ +import { + createTestContext, + destroyFromContext, + expectNodeShapeToMatch, +} from "lexical/__tests__/utils"; +import { $createParagraphNode, $createTextNode, $getRoot, LexicalEditor } from "lexical"; +import { EditorUiContext } from "../../ui/framework/core"; +import { setEditorContentFromHtml } from "../actions"; + +describe('Actions', () => { + + let context!: EditorUiContext; + let editor!: LexicalEditor; + + beforeEach(() => { + context = createTestContext(); + editor = context.editor; + }); + + afterEach(() => { + destroyFromContext(context); + }); + + describe('setEditorContentFromHtml', () => { + it('parses HTML and sets content in editor', async () => { + setEditorContentFromHtml(editor, '

    Hello World

    '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'paragraph', + children: [{ text: 'Hello World' }], + }, + ]); + }); + + it('parses multiple block elements', async () => { + setEditorContentFromHtml(editor, '

    First

    Second

    '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph', children: [{ text: 'First' }] }, + { type: 'paragraph', children: [{ text: 'Second' }] }, + ]); + }); + + it('wraps plain text in a paragraph', async () => { + setEditorContentFromHtml(editor, 'Plain text'); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'paragraph', + children: [{ text: 'Plain text' }], + }, + ]); + }); + + it('ensures at least a paragraph when HTML is empty', async () => { + setEditorContentFromHtml(editor, ''); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph' }, + ]); + }); + + it('ensures at least a paragraph when HTML contains only whitespace', async () => { + setEditorContentFromHtml(editor, ' '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph' }, + ]); + }); + + it('clears existing content before setting new content', async () => { + editor.updateAndCommit(() => { + const p = $createParagraphNode(); + p.append($createTextNode('Existing')); + $getRoot().append(p); + }); + + setEditorContentFromHtml(editor, '

    New

    '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { type: 'paragraph', children: [{ text: 'New' }] }, + ]); + }); + + it('handles nested HTML structures', async () => { + setEditorContentFromHtml(editor, '
    • Item A
    • Item B
    '); + await Promise.resolve().then(); + + expectNodeShapeToMatch(editor, [ + { + type: 'list', + children: [ + { type: 'listitem', children: [{ text: 'Item A' }] }, + { type: 'listitem', children: [{ text: 'Item B' }] }, + ], + }, + ]); + }); + }); +}); diff --git a/resources/js/wysiwyg/utils/actions.ts b/resources/js/wysiwyg/utils/actions.ts index e18ac515f..465d44d31 100644 --- a/resources/js/wysiwyg/utils/actions.ts +++ b/resources/js/wysiwyg/utils/actions.ts @@ -1,4 +1,4 @@ -import {$getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; +import {$createParagraphNode, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; import {$generateHtmlFromNodes} from "@lexical/html"; import {$getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from "./nodes"; @@ -12,6 +12,12 @@ export function setEditorContentFromHtml(editor: LexicalEditor, html: string) { const nodes = $htmlToBlockNodes(editor, html); root.append(...nodes); + + // Always ensure we at least have a paragraph in the root + // as a target for the cursor/focus/actions. + if (root.isEmpty()) { + root.append($createParagraphNode()); + } }); } @@ -85,5 +91,14 @@ export function getEditorContentAsHtml(editor: LexicalEditor): Promise { } export function focusEditor(editor: LexicalEditor): void { + editor.update(() => { + const root = $getRoot(); + const selection = $getSelection(); + const firstChild = root.getFirstChild(); + if (firstChild && !selection) { + firstChild.selectStart(); + } + }); + editor.commitUpdates(); editor.focus(() => {}, {defaultSelection: "rootStart"}); } \ No newline at end of file diff --git a/resources/views/pages/parts/wysiwyg-editor.blade.php b/resources/views/pages/parts/wysiwyg-editor.blade.php index 73e655752..4344b234d 100644 --- a/resources/views/pages/parts/wysiwyg-editor.blade.php +++ b/resources/views/pages/parts/wysiwyg-editor.blade.php @@ -11,7 +11,7 @@ {{--
    --}} - +
    @if($errors->has('html')) From 5f306801d7a192021a01021aec07a7976514d94e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 19:17:37 +0100 Subject: [PATCH 54/74] Lexical: Used non-breaking spaces instead of text spans for whitespace Leading/trailing text whitespace would use spans with css whitespace rules so the spaces would be represented, but this would lead to a messier output. Instead, this attempts to smartly use non-breaking spaces, along with normal spaces, to represent white space in a way that still allows breaking. It attempts to reduce non-breaking spaces where not needed (next to other inline content). Also fixes duplicate usage on italic content. --- .../js/wysiwyg/lexical/core/LexicalUtils.ts | 4 +++ .../lexical/core/nodes/LexicalTextNode.ts | 30 ++++++++++++---- .../__tests__/unit/LexicalTextNode.test.ts | 35 ++++++++++++++++--- 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts index cff86a74a..840c5e8ab 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalUtils.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalUtils.ts @@ -1338,6 +1338,10 @@ export function $isInlineElementOrDecoratorNode(node: LexicalNode): boolean { ); } +export function $isInlineElementOrTextNode(node: LexicalNode|null): boolean { + return node !== null && ($isTextNode(node) || ($isElementNode(node) && node.isInline())); +} + export function $getNearestRootOrShadowRoot( node: LexicalNode, ): RootNode | ElementNode { diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts index 35cc073a0..6588b7e10 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts @@ -58,7 +58,7 @@ import { import {errorOnReadOnly} from '../LexicalUpdates'; import { $applyNodeReplacement, - $getCompositionKey, + $getCompositionKey, $isInlineElementOrTextNode, $setCompositionKey, getCachedClassNameArray, internalMarkSiblingsAsDirty, @@ -275,6 +275,16 @@ function wrapElementWith( return el; } +function alternatingWhitespaceReplacer(inlineAdjacent: boolean): (match: string) => string { + return (match: string): string => { + let offset = inlineAdjacent ? 1 : 0; + return match + .split('') + .map((char, i) => (i % 2 === offset ? '\u00A0' : char)) + .join(''); + }; +} + // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging export interface TextNode { getTopLevelElement(): ElementNode | null; @@ -626,10 +636,15 @@ export class TextNode extends LexicalNode { 'Expected TextNode createDOM to always return a HTMLElement', ); - // Wrap up to retain space if head/tail whitespace exists - const text = this.getTextContent(); - if (/^\s|\s$/.test(text)) { - element.style.whiteSpace = 'pre-wrap'; + // Handle head/tail whitespace if it exists + let text = this.getTextContent(); + const prevIsInline = $isInlineElementOrTextNode(this.getPreviousSibling()); + const nextIsInline = $isInlineElementOrTextNode(this.getNextSibling()); + if (/^\s/.test(text)) { + text = text.replace(/^(\s+)/, alternatingWhitespaceReplacer(prevIsInline)); + } + if (/\s$/.test(text)) { + text = text.replace(/(\s+)$/, alternatingWhitespaceReplacer(nextIsInline)); } // Strip editor theme classes @@ -642,6 +657,9 @@ export class TextNode extends LexicalNode { element.removeAttribute('class'); } + // Apply whitespace replacement to the element + element.textContent = text; + // Remove placeholder tag if redundant if (element.nodeName === 'SPAN' && !element.getAttribute('style')) { element = document.createTextNode(text); @@ -653,7 +671,7 @@ export class TextNode extends LexicalNode { if (this.hasFormat('bold') && originalElementName !== 'strong') { element = wrapElementWith(element, 'strong'); } - if (this.hasFormat('italic')) { + if (this.hasFormat('italic') && originalElementName !== 'em') { element = wrapElementWith(element, 'em'); } if (this.hasFormat('strikethrough')) { diff --git a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts index 0dbf9b94e..8c81d13ac 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/__tests__/unit/LexicalTextNode.test.ts @@ -42,6 +42,7 @@ import { getEditorStateTextContent, } from '../../../LexicalUtils'; import {$generateHtmlFromNodes} from "@lexical/html"; +import {setEditorContentFromHtml} from "../../../../../utils/actions"; const editorConfig = Object.freeze({ namespace: '', @@ -806,12 +807,13 @@ describe('LexicalTextNode tests', () => { }); }); - test('simple text wrapped in span if leading or ending spacing', async () => { + test('non-breaking-spaces used if leading or ending spacing', async () => { const textByExpectedHtml = { - 'hello ': '

    hello

    ', - ' hello': '

    hello

    ', - ' hello ': '

    hello

    ', + 'hello ': '

    hello 

    ', + ' hello': '

     hello

    ', + ' hello ': '

     hello 

    ', + 'hello ': '

    hello   

    ', } await update(() => { @@ -827,6 +829,31 @@ describe('LexicalTextNode tests', () => { }); }); + test('normal spaces used when text is adjacent to other inline text', async () => { + await update(() => { + setEditorContentFromHtml($getEditor(), '

     Hello there is text here 

    '); + }); + + await update(() => { + const html = $generateHtmlFromNodes($getEditor(), null); + expect(html).toBe('

     Hello there is text here 

    '); + }); + }); + + test('normal and non-breaking spaces used when text with multiple spaces is adjacent to inline text', async () => { + await update(() => { + const paragraph = $getRoot().getFirstChild()!; + $getRoot().append(paragraph); + paragraph.append($createTextNode('hello ')); + const bold = $createTextNode('world '); + bold.setFormat("bold"); + paragraph.append(bold); + + const html = $generateHtmlFromNodes($getEditor(), null); + expect(html).toBe('

    hello   world   

    '); + }); + }); + test('text with formats exports using format elements instead of classes', async () => { await update(() => { const paragraph = $getRoot().getFirstChild()!; From 16a50b0ca9af301482b5d1ad32a5e16d99ec3e7e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 9 May 2026 19:36:43 +0100 Subject: [PATCH 55/74] Lexical: Fixed updating of TextNode text on export --- .../js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts index 6588b7e10..860f03925 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalTextNode.ts @@ -657,12 +657,17 @@ export class TextNode extends LexicalNode { element.removeAttribute('class'); } - // Apply whitespace replacement to the element - element.textContent = text; - // Remove placeholder tag if redundant if (element.nodeName === 'SPAN' && !element.getAttribute('style')) { element = document.createTextNode(text); + } else { + // Apply whitespace replaced text to the element + // Search down the child chain in the event this element is already wrapped + let child: Element = element; + while (child.childElementCount > 0) { + child = child.children[0]; + } + child.textContent = text; } // This is the only way to properly add support for most clients, From e98244398824fa622db86b72ec8e76f2c3461ccc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 May 2026 12:58:17 +0100 Subject: [PATCH 56/74] Lexical: Made toolbar placement smarter - Set z-index so toolbars for deeper (more specific) content is bought forward above more general toolbars. - Added some basic overlap checking as an indicator to whether the toolbar should sit above the target. --- resources/js/wysiwyg/ui/framework/manager.ts | 8 +- resources/js/wysiwyg/ui/framework/toolbars.ts | 75 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/resources/js/wysiwyg/ui/framework/manager.ts b/resources/js/wysiwyg/ui/framework/manager.ts index f8525bc7c..4dab455fe 100644 --- a/resources/js/wysiwyg/ui/framework/manager.ts +++ b/resources/js/wysiwyg/ui/framework/manager.ts @@ -167,8 +167,10 @@ export class EditorUIManager { triggerLayoutUpdate(): void { window.requestAnimationFrame(() => { + const toolbarBounds: (DOMRect|null)[] = []; for (const toolbar of this.activeContextToolbars) { - toolbar.updatePosition(); + const bounds = toolbar.updatePosition(toolbarBounds); + toolbarBounds.push(bounds); } }); } @@ -247,13 +249,15 @@ export class EditorUIManager { } } + const toolbarBounds: (DOMRect|null)[] = []; for (const [target, contents] of contentByTarget) { const toolbar = new EditorContextToolbar(target, contents); toolbar.setContext(this.getContext()); this.activeContextToolbars.push(toolbar); this.getContext().containerDOM.append(toolbar.getDOMElement()); - toolbar.updatePosition(); + const bounds = toolbar.updatePosition(toolbarBounds); + toolbarBounds.push(bounds); } } diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts index cf5ec4ad1..bd86eca7e 100644 --- a/resources/js/wysiwyg/ui/framework/toolbars.ts +++ b/resources/js/wysiwyg/ui/framework/toolbars.ts @@ -23,8 +23,14 @@ export class EditorContextToolbar extends EditorContainerUiElement { }, this.getChildren().map(child => child.getDOMElement())); } - updatePosition() { - const editorBounds = this.getContext().scrollDOM.getBoundingClientRect(); + /** + * Update the position of the toolbar based on the target element. + * Takes the bounds of other toolbars processed so far so that they can be considered + * when positioning the toolbar to help prevent overlaps. + */ + updatePosition(otherBounds: (DOMRect|null)[] = []): DOMRect|null { + const context = this.getContext(); + const editorBounds = context.scrollDOM.getBoundingClientRect(); const targetBounds = this.target.getBoundingClientRect(); const dom = this.getDOMElement(); const domBounds = dom.getBoundingClientRect(); @@ -36,23 +42,43 @@ export class EditorContextToolbar extends EditorContainerUiElement { if (!this.target.isConnected) { // If our target is no longer in the DOM, tell the manager an update is needed. - this.getContext().manager.triggerFutureStateRefresh(); - return; + context.manager.triggerFutureStateRefresh(); + return null; } else if (!showing) { - return; + return null; } - const showAbove: boolean = targetBounds.bottom + 6 + domBounds.height > editorBounds.bottom; - dom.classList.toggle('is-above', showAbove); - const targetMid = targetBounds.left + (targetBounds.width / 2); - const targetLeft = targetMid - (domBounds.width / 2); + const intendedBounds: DOMRectInit = { + x: targetMid -(domBounds.width / 2), + y: targetBounds.bottom + 6, + width: domBounds.width, + height: domBounds.height, + }; + + let showAbove: boolean = ( + targetBounds.bottom + 6 + domBounds.height > editorBounds.bottom + || this.willOverlapWithOthersIfBelow(intendedBounds, otherBounds) + ); + dom.classList.toggle('is-above', showAbove); if (showAbove) { - dom.style.top = (targetBounds.top - 6 - domBounds.height) + 'px'; - } else { - dom.style.top = (targetBounds.bottom + 6) + 'px'; + intendedBounds.y = targetBounds.top - 6 - domBounds.height; } - dom.style.left = targetLeft + 'px'; + + dom.style.top = intendedBounds.y + 'px'; + dom.style.left = intendedBounds.x + 'px'; + + // Set z-index based on depth, so that the most specific toolbar + // is bought forward. + let depth = 1; + let parent = this.target.parentElement; + while (parent && parent !== context.editorDOM) { + parent = parent.parentElement; + depth++; + } + dom.style.zIndex = `${depth}`; + + return dom.getBoundingClientRect(); } insert(children: EditorUiElement[]) { @@ -60,4 +86,27 @@ export class EditorContextToolbar extends EditorContainerUiElement { const dom = this.getDOMElement(); dom.append(...children.map(child => child.getDOMElement())); } + + protected willOverlapWithOthersIfBelow(intendedBounds: DOMRectInit, otherBounds: (DOMRect|null)[]): boolean { + for (const bounds of otherBounds) { + if (bounds === null) continue; + + const iLeft = intendedBounds.x ?? 0; + const iTop = intendedBounds.y ?? 0; + const iRight = iLeft + (intendedBounds.width ?? 0); + const iBottom = iTop + (intendedBounds.height ?? 0); + + const overlaps = ( + iLeft < bounds.right && + iRight > bounds.left && + iTop < bounds.bottom && + iBottom > bounds.top + ); + + if (overlaps) { + return true; + } + } + return false; + } } \ No newline at end of file From 6367f007c18b7e7bfac61fd077c9b777aa03a7ee Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 May 2026 13:04:39 +0100 Subject: [PATCH 57/74] Lexical: Added fade to table resizers --- resources/js/wysiwyg/ui/framework/toolbars.ts | 4 ++-- resources/sass/_editor.scss | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts index bd86eca7e..95f0af11c 100644 --- a/resources/js/wysiwyg/ui/framework/toolbars.ts +++ b/resources/js/wysiwyg/ui/framework/toolbars.ts @@ -50,7 +50,7 @@ export class EditorContextToolbar extends EditorContainerUiElement { const targetMid = targetBounds.left + (targetBounds.width / 2); const intendedBounds: DOMRectInit = { - x: targetMid -(domBounds.width / 2), + x: targetMid - (domBounds.width / 2), y: targetBounds.bottom + 6, width: domBounds.width, height: domBounds.height, @@ -109,4 +109,4 @@ export class EditorContextToolbar extends EditorContainerUiElement { } return false; } -} \ No newline at end of file +} diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 0580d7377..4c05de0a8 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -461,6 +461,7 @@ body.editor-is-fullscreen { z-index: 3; user-select: none; opacity: 0; + transition: opacity 120ms ease-in-out; &:hover, &.active { opacity: 0.4; } From 2aba39b176a9350750c0cfced812f479339961bd Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 10 May 2026 17:28:21 +0100 Subject: [PATCH 58/74] Lexical: Updated toolbars to re-focus on editor on escape press --- resources/js/wysiwyg/ui/framework/core.ts | 2 +- resources/js/wysiwyg/ui/framework/toolbars.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts index 9c524dff0..68a80d2e1 100644 --- a/resources/js/wysiwyg/ui/framework/core.ts +++ b/resources/js/wysiwyg/ui/framework/core.ts @@ -30,7 +30,7 @@ export function isUiBuilderDefinition(object: any): object is EditorUiBuilderDef export abstract class EditorUiElement { protected dom: HTMLElement|null = null; private context: EditorUiContext|null = null; - private abortController: AbortController = new AbortController(); + protected abortController: AbortController = new AbortController(); protected abstract buildDOM(): HTMLElement; diff --git a/resources/js/wysiwyg/ui/framework/toolbars.ts b/resources/js/wysiwyg/ui/framework/toolbars.ts index 95f0af11c..ec8e9772b 100644 --- a/resources/js/wysiwyg/ui/framework/toolbars.ts +++ b/resources/js/wysiwyg/ui/framework/toolbars.ts @@ -18,9 +18,20 @@ export class EditorContextToolbar extends EditorContainerUiElement { } protected buildDOM(): HTMLElement { - return el('div', { + const toolbar = el('div', { class: 'editor-context-toolbar', }, this.getChildren().map(child => child.getDOMElement())); + + // Focus back on the editor on escape press + toolbar.addEventListener('keydown', (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + event.stopPropagation(); + this.getContext().editor.focus(); + } + }, {signal: this.abortController.signal}); + + return toolbar; } /** From 5d429ea9bb5d48b4a4b3bab4b09fe21dea00b752 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 11 May 2026 17:07:50 +0100 Subject: [PATCH 59/74] Lexical: Added better support for block content in list items Improved the ability to set/use block formats in lists. Setting a format, will now attempt to create that, if just plain text to start with, when in a list. Added handling so that blocks split into new list elements instead of new blocks within the list element. Also fixed some issues with cyclic import references, and updated editor event types to always include their type for easier debug. --- resources/js/wysiwyg/index.ts | 3 ++ .../wysiwyg/lexical/core/LexicalCommands.ts | 2 +- .../wysiwyg/lexical/core/LexicalSelection.ts | 7 ++- .../lexical/core/nodes/LexicalElementNode.ts | 4 +- .../lexical/list/LexicalListItemNode.ts | 4 +- .../wysiwyg/lexical/list/LexicalListNode.ts | 2 +- .../js/wysiwyg/lexical/list/formatList.ts | 8 ++-- resources/js/wysiwyg/lexical/list/index.ts | 38 +++++++++++++++- resources/js/wysiwyg/lexical/list/utils.ts | 6 ++- resources/js/wysiwyg/utils/formats.ts | 45 +++++++++++-------- resources/js/wysiwyg/utils/selection.ts | 25 +++++++++++ 11 files changed, 112 insertions(+), 32 deletions(-) diff --git a/resources/js/wysiwyg/index.ts b/resources/js/wysiwyg/index.ts index dc0ea211f..59fb2acde 100644 --- a/resources/js/wysiwyg/index.ts +++ b/resources/js/wysiwyg/index.ts @@ -29,6 +29,7 @@ import {registerSelectionHandling} from "./services/selection-handling"; import {EditorApi} from "./api/api"; import {registerMentions} from "./services/mentions"; import {MentionDecorator} from "./ui/decorators/MentionDecorator"; +import {registerLists} from "@lexical/list"; const theme = { text: { @@ -58,6 +59,7 @@ export function createPageEditorInstance(container: HTMLElement, htmlContent: st mergeRegister( registerRichText(editor), + registerLists(editor), registerHistory(editor, createEmptyHistoryState(), 300), registerShortcuts(context, true), registerKeyboardHandling(context), @@ -122,6 +124,7 @@ export function createBasicEditorInstance(container: HTMLElement, htmlContent: s const editorTeardown = mergeRegister( registerRichText(editor), + registerLists(editor), registerHistory(editor, createEmptyHistoryState(), 300), registerShortcuts(context, false), registerAutoLinks(editor), diff --git a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts index 1b378b4a0..36badba97 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalCommands.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalCommands.ts @@ -16,7 +16,7 @@ import type { export type PasteCommandType = ClipboardEvent | InputEvent | KeyboardEvent; export function createCommand(type?: string): LexicalCommand { - return __DEV__ ? {type} : {}; + return {type}; } export const SELECTION_CHANGE_COMMAND: LexicalCommand = createCommand( diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index 36e2db547..2904867f0 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -63,7 +63,6 @@ import { toggleTextFormatType, } from './LexicalUtils'; import {$createTabNode, $isTabNode} from './nodes/LexicalTabNode'; -import {$selectSingleNode} from "../../utils/selection"; export type TextPointType = { _selection: BaseSelection; @@ -2224,6 +2223,12 @@ export function $createNodeSelection(): NodeSelection { return new NodeSelection(new Set()); } +function $selectSingleNode(node: LexicalNode): void { + const nodeSelection = $createNodeSelection(); + nodeSelection.add(node.getKey()); + $setSelection(nodeSelection); +} + export function $internalCreateSelection( editor: LexicalEditor, ): null | BaseSelection { diff --git a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts index 0329aed6f..0304fe4ad 100644 --- a/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts +++ b/resources/js/wysiwyg/lexical/core/nodes/LexicalElementNode.ts @@ -16,11 +16,9 @@ import type {KlassConstructor, Spread} from 'lexical'; import invariant from 'lexical/shared/invariant'; -import {$isTextNode, TextNode} from '../index'; +import {$isTextNode, TextNode} from './LexicalTextNode'; import { DOUBLE_LINE_BREAK, - - } from '../LexicalConstants'; import {LexicalNode} from '../LexicalNode'; import { diff --git a/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts b/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts index 239c49a8c..a7b04d476 100644 --- a/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts +++ b/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts @@ -6,7 +6,7 @@ * */ -import type {ListNode, ListType} from './'; +import type {ListNode, ListType} from './LexicalListNode'; import type { BaseSelection, DOMConversionMap, @@ -32,7 +32,7 @@ import { } from 'lexical'; import invariant from 'lexical/shared/invariant'; -import {$createListNode, $isListNode} from './'; +import {$createListNode, $isListNode} from './LexicalListNode'; import {mergeLists} from './formatList'; import {isNestedListNode} from './utils'; import {el} from "../../utils/dom"; diff --git a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts index b5c83addd..0311e8b31 100644 --- a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts +++ b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts @@ -30,7 +30,7 @@ import { import invariant from 'lexical/shared/invariant'; import normalizeClassNames from 'lexical/shared/normalizeClassNames'; -import {$createListItemNode, $isListItemNode, ListItemNode} from '.'; +import {$createListItemNode, $isListItemNode, ListItemNode} from './LexicalListItemNode'; import { mergeNextSiblingListIfSameType, updateChildrenListItemValue, diff --git a/resources/js/wysiwyg/lexical/list/formatList.ts b/resources/js/wysiwyg/lexical/list/formatList.ts index aa0d5d611..5c7b81b7a 100644 --- a/resources/js/wysiwyg/lexical/list/formatList.ts +++ b/resources/js/wysiwyg/lexical/list/formatList.ts @@ -25,12 +25,14 @@ import invariant from 'lexical/shared/invariant'; import { $createListItemNode, - $createListNode, $isListItemNode, - $isListNode, ListItemNode, +} from './LexicalListItemNode'; +import { + $createListNode, + $isListNode, ListNode, -} from './'; +} from './LexicalListNode'; import {ListType} from './LexicalListNode'; import { $getAllListItems, diff --git a/resources/js/wysiwyg/lexical/list/index.ts b/resources/js/wysiwyg/lexical/list/index.ts index 157fe79de..00e33c115 100644 --- a/resources/js/wysiwyg/lexical/list/index.ts +++ b/resources/js/wysiwyg/lexical/list/index.ts @@ -8,7 +8,10 @@ import type {SerializedListItemNode} from './LexicalListItemNode'; import type {ListType, SerializedListNode} from './LexicalListNode'; -import type {LexicalCommand} from 'lexical'; +import { + $getSelection, + $isRangeSelection, COMMAND_PRIORITY_NORMAL, INSERT_PARAGRAPH_COMMAND, LexicalCommand, LexicalEditor +} from 'lexical'; import {createCommand} from 'lexical'; @@ -20,6 +23,8 @@ import { } from './LexicalListItemNode'; import {$createListNode, $isListNode, ListNode} from './LexicalListNode'; import {$getListDepth} from './utils'; +import {mergeRegister} from "@lexical/utils"; +import {$getAncestor, INTERNAL_$isBlock} from "lexical/LexicalUtils"; export { $createListItemNode, @@ -48,3 +53,34 @@ export const INSERT_CHECK_LIST_COMMAND: LexicalCommand = createCommand( export const REMOVE_LIST_COMMAND: LexicalCommand = createCommand( 'REMOVE_LIST_COMMAND', ); + +export function registerLists(editor: LexicalEditor): () => void { + return mergeRegister( + + // Override the default insert paragraph command when within a list item + // so that new blocks are inserted as their own list items. + editor.registerCommand(INSERT_PARAGRAPH_COMMAND, () => { + const selection = $getSelection(); + if (!$isRangeSelection(selection)) { + return false; + } + + const anchorNode = selection.anchor.getNode(); + const block = $getAncestor(anchorNode, INTERNAL_$isBlock)!; + const blockParent = block.getParent(); + if ($isListItemNode(blockParent)) { + const newBlock = selection.insertParagraph(); + if (newBlock) { + const newListItem = $createListItemNode(); + const newBlockSiblings = newBlock.getNextSiblings(); + newListItem.append(newBlock, ...newBlockSiblings); + blockParent.insertAfter(newListItem, false); + newListItem.selectStart(); + } + return true; + } + + return false; + }, COMMAND_PRIORITY_NORMAL), + ); +} diff --git a/resources/js/wysiwyg/lexical/list/utils.ts b/resources/js/wysiwyg/lexical/list/utils.ts index c451a4508..984e77016 100644 --- a/resources/js/wysiwyg/lexical/list/utils.ts +++ b/resources/js/wysiwyg/lexical/list/utils.ts @@ -14,10 +14,12 @@ import invariant from 'lexical/shared/invariant'; import { $createListItemNode, $isListItemNode, - $isListNode, ListItemNode, +} from './LexicalListItemNode'; +import { + $isListNode, ListNode, -} from './'; +} from './LexicalListNode'; /** * Checks the depth of listNode from the root node. diff --git a/resources/js/wysiwyg/utils/formats.ts b/resources/js/wysiwyg/utils/formats.ts index a5f06f147..fbc11af49 100644 --- a/resources/js/wysiwyg/utils/formats.ts +++ b/resources/js/wysiwyg/utils/formats.ts @@ -3,7 +3,7 @@ import { $createTextNode, $getSelection, $insertNodes, - $isParagraphNode, + $isParagraphNode, $isRangeSelection, LexicalEditor, LexicalNode } from "lexical"; @@ -16,10 +16,12 @@ import { } from "./selection"; import {$createCodeBlockNode, $isCodeBlockNode, $openCodeEditorForNode, CodeBlockNode} from "@lexical/rich-text/LexicalCodeBlockNode"; import {$createCalloutNode, $isCalloutNode, CalloutCategory} from "@lexical/rich-text/LexicalCalloutNode"; -import {$isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list"; +import {$isListItemNode, $isListNode, insertList, ListNode, ListType, removeList} from "@lexical/list"; import {$createLinkNode, $isLinkNode} from "@lexical/link"; import {$createHeadingNode, $isHeadingNode, HeadingTagType} from "@lexical/rich-text/LexicalHeadingNode"; import {$createQuoteNode, $isQuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; +import {$setBlocksType} from "@lexical/selection"; + const $isHeaderNodeOfTag = (node: LexicalNode | null | undefined, tag: HeadingTagType) => { return $isHeadingNode(node) && node.getTag() === tag; @@ -62,28 +64,35 @@ export function toggleSelectionAsList(editor: LexicalEditor, type: ListType) { } export function formatCodeBlock(editor: LexicalEditor) { - editor.getEditorState().read(() => { + editor.update(() => { const selection = $getSelection(); const lastSelection = getLastSelection(editor); const codeBlock = $getNodeFromSelection(lastSelection, $isCodeBlockNode) as (CodeBlockNode | null); if (codeBlock === null) { - editor.update(() => { - const codeBlock = $createCodeBlockNode(); - codeBlock.setCode(selection?.getTextContent() || ''); + const codeBlock = $createCodeBlockNode(); - const selectionNodes = $getBlockElementNodesInSelection(selection); - const firstSelectionNode = selectionNodes[0]; - const extraNodes = selectionNodes.slice(1); - if (firstSelectionNode) { - firstSelectionNode.replace(codeBlock); - extraNodes.forEach(n => n.remove()); - } else { - $insertNewBlockNodeAtSelection(codeBlock, true); - } + const codeLines = []; + const selectionNodes = $getBlockElementNodesInSelection(selection); + for (const node of selectionNodes) { + codeLines.push(node.getTextContent()); + } + codeBlock.setCode(codeLines.join('\n')); - $openCodeEditorForNode(editor, codeBlock); - $selectSingleNode(codeBlock); - }); + const firstSelectionNode = selectionNodes[0]; + const extraNodes = selectionNodes.slice(1); + if ($isListItemNode(firstSelectionNode)) { + firstSelectionNode.getChildren().forEach(c => c.remove()); + firstSelectionNode.append(codeBlock); + extraNodes.forEach(n => n.remove()); + } else if (firstSelectionNode) { + firstSelectionNode.replace(codeBlock); + extraNodes.forEach(n => n.remove()); + } else { + $insertNewBlockNodeAtSelection(codeBlock, true); + } + + $openCodeEditorForNode(editor, codeBlock); + $selectSingleNode(codeBlock); } else { $openCodeEditorForNode(editor, codeBlock); } diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index 28050571e..e6815e250 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -17,6 +17,7 @@ import {$setBlocksType} from "@lexical/selection"; import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes"; import {CommonBlockAlignment} from "lexical/nodes/common"; +import {$isListItemNode} from "@lexical/list"; const lastSelectionByEditor = new WeakMap; @@ -76,9 +77,33 @@ export function $selectionContainsTextFormat(selection: BaseSelection | null, fo return false; } +function createNewBlockIfSelectionIsSingleListItemText(selection: BaseSelection): void { + const startEnd = selection.getStartEndPoints(); + if (!startEnd) { + return; + } + + const startBlock = $getNearestNodeBlockParent(startEnd[0].getNode()); + const endBlock = $getNearestNodeBlockParent(startEnd[1].getNode()); + const isSingleListItemTextSelection = $isListItemNode(startBlock) && startBlock.getKey() === endBlock?.getKey(); + + if (isSingleListItemTextSelection) { + const wrapper = $createParagraphNode(); + const startNode = startEnd[0].getNode(); + startNode.insertBefore(wrapper); + wrapper.append(...selection.getNodes()); + } +} + export function $toggleSelectionBlockNodeType(matcher: LexicalNodeMatcher, creator: LexicalElementNodeCreator) { const selection = $getSelection(); const blockElement = selection ? $getNearestBlockElementAncestorOrThrow(selection.getNodes()[0]) : null; + + const inListItem = $isListItemNode(blockElement); + if (inListItem && selection) { + createNewBlockIfSelectionIsSingleListItemText(selection); + } + if (selection && matcher(blockElement)) { $setBlocksType(selection, $createParagraphNode); } else { From 9f4afac7bc44f9692551e832b1065c606b09f795 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 12 May 2026 18:42:06 +0100 Subject: [PATCH 60/74] Lexical: Improved ability to break out of lists Updates list handling so that you can break out of a list (or move down a level) via enter on an existing empty list item at any point in the list, not just the end. Added test to cover. --- .../lexical/list/LexicalListItemNode.ts | 23 +++---- .../unit/LexicalListItemNode.test.ts | 68 ++++++++++++++++++- resources/js/wysiwyg/utils/lists.ts | 33 ++++++++- 3 files changed, 104 insertions(+), 20 deletions(-) diff --git a/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts b/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts index a7b04d476..2a1b698b1 100644 --- a/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts +++ b/resources/js/wysiwyg/lexical/list/LexicalListItemNode.ts @@ -36,6 +36,7 @@ import {$createListNode, $isListNode} from './LexicalListNode'; import {mergeLists} from './formatList'; import {isNestedListNode} from './utils'; import {el} from "../../utils/dom"; +import {$escapeListAtItem} from "../../utils/lists"; export type SerializedListItemNode = Spread< { @@ -273,21 +274,13 @@ export class ListItemNode extends ElementNode { restoreSelection = true, ): ListItemNode | ParagraphNode | null { - if (this.getTextContent().trim() === '' && this.isLastChild()) { - const list = this.getParentOrThrow(); - const parentListItem = list.getParent(); - if ($isListItemNode(parentListItem)) { - // Un-nest list item if empty nested item - parentListItem.insertAfter(this); - this.selectStart(); - return null; - } else { - // Insert empty paragraph after list if adding after last empty child - const paragraph = $createParagraphNode(); - list.insertAfter(paragraph, restoreSelection); - this.remove(); - return paragraph; - } + // If we're adding a new empty item, and coming from an empty item, + // take that as a desire to break from the current list level. + // Intended to be a bit more lenient on the last list item hence ignores whitespace, + // which allows empty items to be listed with just a space (except for last). + const textContent = this.getTextContent(); + if (textContent === '' || (textContent.trim() === '' && this.isLastChild())) { + return $escapeListAtItem(this); } const newElement = $createListItemNode( diff --git a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts index 10ff0fc66..48fa2faf8 100644 --- a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts +++ b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListItemNode.test.ts @@ -9,12 +9,12 @@ import { $createParagraphNode, $createRangeSelection, - $getRoot, LexicalEditor, + $getRoot, LexicalEditor, LexicalNode, ParagraphNode, TextNode, } from 'lexical'; import { createTestContext, destroyFromContext, - expectHtmlToBeEqual, + expectHtmlToBeEqual, expectNodeShapeToMatch, html, } from 'lexical/__tests__/utils'; @@ -1200,6 +1200,70 @@ describe('LexicalListItemNode tests', () => { `, ); }); + + test('new items after empty top-level items splits the list in two, and inserts a paragraph inbetween', () => { + let newItem: LexicalNode|null = null; + const input = `
      +
    • Item A
    • +
    • +
    • Item C
    • +
    `; + + editor.updateAndCommit(() => { + const root = $getRoot(); + root.append(...$htmlToBlockNodes(editor, input)); + const list = root.getFirstChild() as ListNode; + const itemB = list.getChildAtIndex(1) as ListItemNode; + + newItem = itemB.insertNewAfter($createRangeSelection()); + }); + + expect(newItem).toBeInstanceOf(ParagraphNode); + + expectNodeShapeToMatch(editor, [ + { + type: 'list', + children: [{type: 'listitem', children: [{text: 'Item A'}]}], + }, + { + type: 'paragraph', + }, + { + type: 'list', + children: [{type: 'listitem', children: [{text: 'Item C'}]}], + } + ]) + }); + + test('new items after last empty top-level inserts a new paragraph below, and removes the list item', () => { + let newItem: LexicalNode|null = null; + const input = `
      +
    • Item A
    • +
    • Item B
    • +
    • +
    `; + + editor.updateAndCommit(() => { + const root = $getRoot(); + root.append(...$htmlToBlockNodes(editor, input)); + const list = root.getFirstChild() as ListNode; + const itemC = list.getChildAtIndex(2) as ListItemNode; + + newItem = itemC.insertNewAfter($createRangeSelection()); + }); + + expect(newItem).toBeInstanceOf(ParagraphNode); + + expectNodeShapeToMatch(editor, [ + { + type: 'list', + children: [{type: 'listitem', children: [{text: 'Item A'}]}, {type: 'listitem', children: [{text: 'Item B'}]}], + }, + { + type: 'paragraph', + }, + ]) + }); }); test('$createListItemNode()', async () => { diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index 3deb9dfb6..cdbaba6fb 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -1,7 +1,7 @@ -import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; +import {$createParagraphNode, $createTextNode, $getSelection, BaseSelection, LexicalEditor, ParagraphNode, TextNode} from "lexical"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; import {$sortNodes, nodeHasInset} from "./nodes"; -import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; +import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode, ListNode} from "@lexical/list"; export function $nestListItem(node: ListItemNode): ListItemNode { @@ -186,4 +186,31 @@ export function $setInsetForSelection(editor: LexicalEditor, change: number): vo } $toggleSelection(editor); -} \ No newline at end of file +} + +export function $escapeListAtItem(item: ListItemNode): ParagraphNode|null { + const list = item.getParentOrThrow(); + const parentListItem = list.getParent(); + + // Un-nest list item if it's an empty nested item + if ($isListItemNode(parentListItem)) { + parentListItem.insertAfter(item); + item.selectStart(); + return null; + } + + // If we're anywhere but at the end, split the list, with following items + // moved into their own new list. + if (!item.isLastChild()) { + const afterSiblings = item.getNextSiblings(); + const newList = $createListNode(list.getListType()); + newList.append(...afterSiblings); + list.insertAfter(newList); + } + + // Insert a new empty paragraph to land on after our list + const paragraph = $createParagraphNode(); + list.insertAfter(paragraph, true); + item.remove(); + return paragraph; +} From ddb0a22504bedb121a3cbe0b7d3233704f0f2903 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 13 May 2026 16:54:45 +0100 Subject: [PATCH 61/74] Lexical: Made table cell up/down arrow nav smarter Added logic to attempt to retain x position when navigating cells up/down via arrow keys. --- .../wysiwyg/lexical/core/LexicalSelection.ts | 7 ++++ .../lexical/table/LexicalCaptionNode.ts | 1 - .../table/LexicalTableSelectionHelpers.ts | 15 ++++++- resources/js/wysiwyg/utils/selection.ts | 42 +++++++++++++++++-- 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts index 2904867f0..73ce0edbb 100644 --- a/resources/js/wysiwyg/lexical/core/LexicalSelection.ts +++ b/resources/js/wysiwyg/lexical/core/LexicalSelection.ts @@ -2219,6 +2219,13 @@ export function $createRangeSelection(): RangeSelection { return new RangeSelection(anchor, focus, 0, ''); } +export function $createCollapsedRangeSelectionForNode(node: LexicalNode, offset: number = 0): RangeSelection { + const type = $isTextNode(node) ? 'text' : 'element'; + const anchor = $createPoint(node.getKey(), offset, type); + const focus = $createPoint(node.getKey(), offset, type); + return new RangeSelection(anchor, focus, 0, ''); +} + export function $createNodeSelection(): NodeSelection { return new NodeSelection(new Set()); } diff --git a/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts b/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts index d9d83562c..5f2ef9a25 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalCaptionNode.ts @@ -1,7 +1,6 @@ import { $createTextNode, DOMConversionMap, - DOMExportOutput, EditorConfig, ElementNode, LexicalEditor, diff --git a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts index 6e5e5416f..7e2d93ad9 100644 --- a/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts +++ b/resources/js/wysiwyg/lexical/table/LexicalTableSelectionHelpers.ts @@ -72,6 +72,7 @@ import {$isTableRowNode} from './LexicalTableRowNode'; import {$isTableSelection} from './LexicalTableSelection'; import {$computeTableMap, $getNodeTriplet} from './LexicalTableUtils'; import {$selectOrCreateAdjacent} from "../../utils/nodes"; +import {$selectNodeAtXPixelOffset} from "../../utils/selection"; const LEXICAL_ELEMENT_KEY = '__lexicalTableSelection'; @@ -1073,6 +1074,7 @@ const selectTableNodeInDirection = ( x: number, y: number, direction: Direction, + selectionOffset: number = -1 ): boolean => { const isForward = direction === 'forward'; @@ -1112,6 +1114,7 @@ const selectTableNodeInDirection = ( selectTableCellNode( tableNode.getCellNodeFromCordsOrThrow(x, y - 1, tableObserver.table), false, + selectionOffset, ); } else { $selectOrCreateAdjacent(tableNode, false); @@ -1124,6 +1127,7 @@ const selectTableNodeInDirection = ( selectTableCellNode( tableNode.getCellNodeFromCordsOrThrow(x, y + 1, tableObserver.table), true, + selectionOffset, ); } else { $selectOrCreateAdjacent(tableNode, true); @@ -1197,7 +1201,14 @@ function $isSelectionInTable( return false; } -function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean) { +function selectTableCellNode(tableCell: TableCellNode, fromStart: boolean, selectionOffsetPixels : number = -1) { + if (selectionOffsetPixels !== -1) { + const selection = $selectNodeAtXPixelOffset(tableCell, selectionOffsetPixels, fromStart); + if (selection) { + return; + } + } + if (fromStart) { tableCell.selectStart(); } else { @@ -1491,12 +1502,14 @@ function $handleArrowKey( tableObserver.setAnchorCellForSelection(cell); tableObserver.setFocusCellForSelection(cell, true); } else { + const selectionOffset = edgeSelectionRect.x - edgeRect.x; return selectTableNodeInDirection( tableObserver, tableNode, cords.x, cords.y, direction, + selectionOffset ); } diff --git a/resources/js/wysiwyg/utils/selection.ts b/resources/js/wysiwyg/utils/selection.ts index e6815e250..77aa902f0 100644 --- a/resources/js/wysiwyg/utils/selection.ts +++ b/resources/js/wysiwyg/utils/selection.ts @@ -1,6 +1,6 @@ import { $createNodeSelection, - $createParagraphNode, $createRangeSelection, + $createParagraphNode, $createRangeSelection, $getEditor, $getNearestNodeFromDOMNode, $getRoot, $getSelection, $isBlockElementNode, $isDecoratorNode, $isElementNode, $isParagraphNode, @@ -8,7 +8,7 @@ import { $setSelection, BaseSelection, DecoratorNode, ElementNode, LexicalEditor, - LexicalNode, + LexicalNode, RangeSelection, TextFormatType, TextNode } from "lexical"; import {$getNearestBlockElementAncestorOrThrow} from "@lexical/utils"; @@ -18,6 +18,7 @@ import {$setBlocksType} from "@lexical/selection"; import {$getNearestNodeBlockParent, $getParentOfType, nodeHasAlignment} from "./nodes"; import {CommonBlockAlignment} from "lexical/nodes/common"; import {$isListItemNode} from "@lexical/list"; +import {$createCollapsedRangeSelectionForNode} from "lexical/LexicalSelection"; const lastSelectionByEditor = new WeakMap; @@ -300,4 +301,39 @@ export function $getDecoratorNodesInSelection(selection: BaseSelection | null): } return selection.getNodes().filter(node => $isDecoratorNode(node)); -} \ No newline at end of file +} + +/** + * Attempt to select the given node at roughly the pixel offset, relative to the left of the node. + * Returns the range selection if a selection could be made. + * Returns null if no selection can be made. + */ +export function $selectNodeAtXPixelOffset(node: LexicalNode, pixelOffset: number, targetStart: boolean = true): RangeSelection|null { + const targetDOM = $getEditor().getElementByKey(node.getKey()); + if (!targetDOM) { + return null; + } + + const targetChild = targetDOM.children[targetStart ? 0 : targetDOM.children.length - 1] || targetDOM; + const targetBounds = targetChild.getBoundingClientRect(); + const targetY = targetBounds[targetStart ? 'top' : 'bottom'] + (targetStart ? 1 : -1); + const targetX = targetBounds.x + pixelOffset; + // Temporary caretRangeFromPoint usage due to caretPositionFromPoint being only + // very recently supported in Safari + // To remove post 2026 + const caretRange = document.caretRangeFromPoint?.(targetX, targetY); + const caret = document.caretPositionFromPoint?.(targetX, targetY) + ?? (caretRange ? { offsetNode: caretRange.startContainer, offset: caretRange.startOffset } : undefined); + if (!caret) { + return null; + } + + const targetNode = $getNearestNodeFromDOMNode(caret.offsetNode); + if (!targetNode) { + return null; + } + + const rangeSelection = $createCollapsedRangeSelectionForNode(targetNode, caret.offset); + $setSelection(rangeSelection); + return rangeSelection; +} From c87abbd23f2dca8c733a8e010b42a089041fa2ae Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 14 May 2026 18:10:46 +0100 Subject: [PATCH 62/74] Images: Increased validation against related page on upload Adds validation that the related page exists, is visible, and that the user has page edit permissions for the page. Aligns with attachment permission model, and aligns across different image endpoints. Added tests to cover. Closes #6126 --- app/Config/database.php | 1 - .../Controllers/DrawioImageController.php | 14 ++-- .../Controllers/GalleryImageController.php | 18 +++-- .../Controllers/ImageGalleryApiController.php | 1 + tests/Activity/CommentMentionTest.php | 2 +- tests/Api/ContentPermissionsApiTest.php | 4 +- tests/Api/ImageGalleryApiTest.php | 28 ++++++++ .../RegeneratePermissionsCommandTest.php | 2 +- tests/ErrorTest.php | 3 +- tests/Helpers/PermissionsProvider.php | 3 +- .../Scenarios/EntityRolePermissionsTest.php | 66 +++++++++---------- tests/PublicActionTest.php | 3 +- tests/Uploads/DrawioTest.php | 24 +++++++ tests/Uploads/ImageTest.php | 39 +++++++++++ 14 files changed, 155 insertions(+), 53 deletions(-) diff --git a/app/Config/database.php b/app/Config/database.php index 6fc861312..86bae5f5b 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -81,7 +81,6 @@ return [ 'strict' => false, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ - // @phpstan-ignore class.notFound (PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'), ]) : [], ], diff --git a/app/Uploads/Controllers/DrawioImageController.php b/app/Uploads/Controllers/DrawioImageController.php index 8295febc1..53ae79004 100644 --- a/app/Uploads/Controllers/DrawioImageController.php +++ b/app/Uploads/Controllers/DrawioImageController.php @@ -2,6 +2,7 @@ namespace BookStack\Uploads\Controllers; +use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\ImageUploadException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; @@ -14,7 +15,8 @@ use Illuminate\Http\Request; class DrawioImageController extends Controller { public function __construct( - protected ImageRepo $imageRepo + protected ImageRepo $imageRepo, + protected PageQueries $pageQueries, ) { } @@ -53,16 +55,18 @@ class DrawioImageController extends Controller */ public function create(Request $request) { - $this->validate($request, [ + $this->checkPermission(Permission::ImageCreateAll); + $validated = $this->validate($request, [ 'image' => ['required', 'string'], 'uploaded_to' => ['required', 'integer'], ]); - $this->checkPermission(Permission::ImageCreateAll); - $imageBase64Data = $request->input('image'); + $imageBase64Data = $validated['image']; + $uploadedTo = $validated['uploaded_to']; + $targetPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo); + $this->checkOwnablePermission(Permission::PageUpdate, $targetPage); try { - $uploadedTo = $request->input('uploaded_to', 0); $image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); diff --git a/app/Uploads/Controllers/GalleryImageController.php b/app/Uploads/Controllers/GalleryImageController.php index 908322be0..ca60f9f8e 100644 --- a/app/Uploads/Controllers/GalleryImageController.php +++ b/app/Uploads/Controllers/GalleryImageController.php @@ -2,6 +2,7 @@ namespace BookStack\Uploads\Controllers; +use BookStack\Entities\Queries\PageQueries; use BookStack\Exceptions\ImageUploadException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; @@ -14,7 +15,8 @@ use Illuminate\Validation\ValidationException; class GalleryImageController extends Controller { public function __construct( - protected ImageRepo $imageRepo + protected ImageRepo $imageRepo, + protected PageQueries $pageQueries, ) { } @@ -56,20 +58,26 @@ class GalleryImageController extends Controller $this->checkPermission(Permission::ImageCreateAll); try { - $this->validate($request, [ + $validated = $this->validate($request, [ 'file' => $this->getImageValidationRules(), + 'uploaded_to' => ['required', 'integer'], ]); } catch (ValidationException $exception) { - return $this->jsonError(implode("\n", $exception->errors()['file'])); + $errors = $exception->errors(); + $messages = array_merge($errors['file'] ?? [], $errors['uploaded_to'] ?? []); + return $this->jsonError(implode("\n", $messages)); } + $uploadedTo = intval($validated['uploaded_to']); + $targetPage = $this->pageQueries->findVisibleByIdOrFail($uploadedTo); + $this->checkOwnablePermission(Permission::PageUpdate, $targetPage); + new OutOfMemoryHandler(function () { return $this->jsonError(trans('errors.image_upload_memory_limit')); }); try { - $imageUpload = $request->file('file'); - $uploadedTo = $request->input('uploaded_to', 0); + $imageUpload = $validated['file']; $image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo); } catch (ImageUploadException $e) { return response($e->getMessage(), 500); diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index c4168a77e..6a72e4c30 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -75,6 +75,7 @@ class ImageGalleryApiController extends ApiController $this->checkPermission(Permission::ImageCreateAll); $data = $this->validate($request, $this->rules()['create']); $page = $this->pageQueries->findVisibleByIdOrFail($data['uploaded_to']); + $this->checkOwnablePermission(Permission::PageUpdate, $page); $image = $this->imageRepo->saveNew($data['image'], $data['type'], $page->id); diff --git a/tests/Activity/CommentMentionTest.php b/tests/Activity/CommentMentionTest.php index 7f26f8689..b9701b115 100644 --- a/tests/Activity/CommentMentionTest.php +++ b/tests/Activity/CommentMentionTest.php @@ -117,7 +117,7 @@ class CommentMentionTest extends TestCase $page = $this->entities->page(); $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, ['view'], $userA->roles()->first()); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $userA->roles()->first()); $this->asAdmin()->post("/comment/{$page->id}", [ 'html' => '

    Hello and

    ' diff --git a/tests/Api/ContentPermissionsApiTest.php b/tests/Api/ContentPermissionsApiTest.php index 464d62683..6a26b4032 100644 --- a/tests/Api/ContentPermissionsApiTest.php +++ b/tests/Api/ContentPermissionsApiTest.php @@ -39,7 +39,7 @@ class ContentPermissionsApiTest extends TestCase $page = $this->entities->page(); $owner = $this->users->newUser(); $role = $this->users->createRole(); - $this->permissions->addEntityPermission($page, ['view', 'delete'], $role); + $this->permissions->setEntityPermissionsForRole($page, ['view', 'delete'], $role); $this->permissions->changeEntityOwner($page, $owner); $this->permissions->setFallbackPermissions($page, ['update', 'create']); @@ -209,7 +209,7 @@ class ContentPermissionsApiTest extends TestCase public function test_update_can_clear_roles_permissions() { $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, ['view'], $this->users->createRole()); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $this->users->createRole()); $page->owned_by = null; $page->save(); diff --git a/tests/Api/ImageGalleryApiTest.php b/tests/Api/ImageGalleryApiTest.php index 07c20c834..09dba84f5 100644 --- a/tests/Api/ImageGalleryApiTest.php +++ b/tests/Api/ImageGalleryApiTest.php @@ -154,6 +154,34 @@ class ImageGalleryApiTest extends TestCase $resp->assertStatus(404); } + public function test_create_requires_update_permission_for_the_target_page() + { + $editor = $this->users->editor(); + $this->actingAsForApi($editor); + + $makeRequest = function (int $uploadedTo) { + return $this->call('POST', $this->baseEndpoint, [ + 'type' => 'gallery', + 'uploaded_to' => $uploadedTo, + 'name' => 'My awesome image!', + ], [], [ + 'image' => $this->files->uploadedImage('my-cool-image.png'), + ]); + }; + + $page = $this->entities->page(); + $this->permissions->disableEntityInheritedPermissions($page); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first()); + + $resp = $makeRequest($page->id); + $resp->assertStatus(403); + + $this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first()); + + $resp = $makeRequest($page->id); + $resp->assertStatus(200); + } + public function test_create_has_restricted_types() { $this->actingAsApiEditor(); diff --git a/tests/Commands/RegeneratePermissionsCommandTest.php b/tests/Commands/RegeneratePermissionsCommandTest.php index 75c6c1b38..27a339fbe 100644 --- a/tests/Commands/RegeneratePermissionsCommandTest.php +++ b/tests/Commands/RegeneratePermissionsCommandTest.php @@ -16,7 +16,7 @@ class RegeneratePermissionsCommandTest extends TestCase $page = $this->entities->page(); $editor = $this->users->editor(); $role = $editor->roles()->first(); - $this->permissions->addEntityPermission($page, ['view'], $role); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $role); JointPermission::query()->truncate(); $this->assertDatabaseMissing('joint_permissions', ['entity_id' => $page->id]); diff --git a/tests/ErrorTest.php b/tests/ErrorTest.php index 642945d43..27c83e212 100644 --- a/tests/ErrorTest.php +++ b/tests/ErrorTest.php @@ -2,7 +2,6 @@ namespace Tests; -use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Support\Facades\Log; class ErrorTest extends TestCase @@ -44,7 +43,7 @@ class ErrorTest extends TestCase $this->actingAs($editor)->get($page->getUrl())->assertOk(); $this->permissions->disableEntityInheritedPermissions($book); - $this->permissions->addEntityPermission($page, ['view'], $editor->roles()->first()); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first()); $resp = $this->actingAs($editor)->get($book->getUrl()); $resp->assertNotFound(); diff --git a/tests/Helpers/PermissionsProvider.php b/tests/Helpers/PermissionsProvider.php index c9ae30919..9b12ee985 100644 --- a/tests/Helpers/PermissionsProvider.php +++ b/tests/Helpers/PermissionsProvider.php @@ -101,8 +101,9 @@ class PermissionsProvider $this->addEntityPermissionEntries($entity, $permissions); } - public function addEntityPermission(Entity $entity, array $actionList, Role $role) + public function setEntityPermissionsForRole(Entity $entity, array $actionList, Role $role) { + $entity->permissions()->where('role_id', '=', $role->id)->delete(); $permissionData = $this->actionListToEntityPermissionData($actionList, $role->id); $this->addEntityPermissionEntries($entity, [$permissionData]); } diff --git a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php index 55761e08c..fa89d365c 100644 --- a/tests/Permissions/Scenarios/EntityRolePermissionsTest.php +++ b/tests/Permissions/Scenarios/EntityRolePermissionsTest.php @@ -29,8 +29,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, [], $roleA); - $this->permissions->addEntityPermission($page, ['view'], $roleB); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleB); $this->assertVisibleToUser($page, $user); } @@ -42,7 +42,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -54,7 +54,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -67,8 +67,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], $roleA); - $this->permissions->addEntityPermission($chapter, ['view'], $roleB); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleB); $this->assertVisibleToUser($page, $user); } @@ -80,8 +80,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, [], $roleA); - $this->permissions->addEntityPermission($page, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -93,8 +93,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); - $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -107,8 +107,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($page, [], $roleA); - $this->permissions->addEntityPermission($chapter, ['view'], $roleB); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleB); $this->assertVisibleToUser($page, $user); } @@ -121,8 +121,8 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->disableEntityInheritedPermissions($chapter); - $this->permissions->addEntityPermission($page, ['view'], $roleA); - $this->permissions->addEntityPermission($chapter, [], $roleB); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleB); $this->assertVisibleToUser($page, $user); } @@ -131,7 +131,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase { [$user, $roleA] = $this->users->newUserWithRole(); $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -140,7 +140,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase { [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -150,7 +150,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase [$user, $roleA] = $this->users->newUserWithRole([], []); $page = $this->entities->pageWithinChapter(); $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -160,7 +160,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-all']); $page = $this->entities->pageWithinChapter(); $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -170,7 +170,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase [$user, $roleA] = $this->users->newUserWithRole([], ['page-view-own']); $page = $this->entities->pageWithinChapter(); $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); $this->permissions->changeEntityOwner($page, $user); $this->assertNotVisibleToUser($page, $user); @@ -182,7 +182,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $roleB = $this->users->attachNewRole($user); $page = $this->entities->page(); - $this->permissions->addEntityPermission($page, [], $roleB); + $this->permissions->setEntityPermissionsForRole($page, [], $roleB); $this->assertNotVisibleToUser($page, $user); } @@ -194,7 +194,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->changeEntityOwner($page, $user); - $this->permissions->addEntityPermission($page, [], $roleB); + $this->permissions->setEntityPermissionsForRole($page, [], $roleB); $this->assertNotVisibleToUser($page, $user); } @@ -207,7 +207,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->pageWithinChapter(); $chapter = $page->chapter; - $this->permissions->addEntityPermission($chapter, [], $roleB); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleB); $this->assertNotVisibleToUser($page, $user); } @@ -220,7 +220,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->changeEntityOwner($page, $user); - $this->permissions->addEntityPermission($chapter, [], $roleB); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleB); $this->assertNotVisibleToUser($page, $user); } @@ -231,7 +231,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->setFallbackPermissions($page, []); - $this->permissions->addEntityPermission($page, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -241,7 +241,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->setFallbackPermissions($page, ['view']); - $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -253,7 +253,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->setFallbackPermissions($page, []); - $this->permissions->addEntityPermission($page, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($page, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -265,7 +265,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $page = $this->entities->page(); $this->permissions->setFallbackPermissions($page, ['view']); - $this->permissions->addEntityPermission($page, [], $roleA); + $this->permissions->setEntityPermissionsForRole($page, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -277,7 +277,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->setFallbackPermissions($chapter, []); - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -289,7 +289,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->setFallbackPermissions($chapter, ['view']); - $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -302,7 +302,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->setFallbackPermissions($chapter, []); - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); $this->assertVisibleToUser($page, $user); } @@ -315,7 +315,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $chapter = $page->chapter; $this->permissions->setFallbackPermissions($chapter, ['view']); - $this->permissions->addEntityPermission($chapter, [], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, [], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -328,7 +328,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $this->permissions->setFallbackPermissions($chapter, []); $this->permissions->setFallbackPermissions($page, []); - $this->permissions->addEntityPermission($chapter, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($chapter, ['view'], $roleA); $this->assertNotVisibleToUser($page, $user); } @@ -342,7 +342,7 @@ class EntityRolePermissionsTest extends PermissionScenarioTestCase $this->permissions->setFallbackPermissions($book, []); $this->permissions->setFallbackPermissions($chapter, []); - $this->permissions->addEntityPermission($book, ['view'], $roleA); + $this->permissions->setEntityPermissionsForRole($book, ['view'], $roleA); $this->assertNotVisibleToUser($page, $user); } diff --git a/tests/PublicActionTest.php b/tests/PublicActionTest.php index e6fc7a6a3..5b0f5353a 100644 --- a/tests/PublicActionTest.php +++ b/tests/PublicActionTest.php @@ -6,7 +6,6 @@ use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; use BookStack\Permissions\Models\RolePermission; use BookStack\Users\Models\Role; -use BookStack\Users\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; @@ -173,7 +172,7 @@ class PublicActionTest extends TestCase $newRole = $this->users->attachNewRole($this->users->guest(), []); $page = $this->entities->page(); $this->permissions->disableEntityInheritedPermissions($page); - $this->permissions->addEntityPermission($page, ['view', 'update'], $newRole); + $this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $newRole); $resp = $this->get($page->getUrl()); $resp->assertOk(); diff --git a/tests/Uploads/DrawioTest.php b/tests/Uploads/DrawioTest.php index d5b3f6088..faa2a0bfb 100644 --- a/tests/Uploads/DrawioTest.php +++ b/tests/Uploads/DrawioTest.php @@ -72,6 +72,30 @@ class DrawioTest extends TestCase $this->assertTrue($testImageData === $uploadedImageData, 'Uploaded image file data does not match our test image as expected'); } + public function test_base64_upload_requires_edit_permission_to_page() + { + $page = $this->entities->page(); + $editor = $this->users->editor(); + $this->actingAs($editor); + + $this->permissions->disableEntityInheritedPermissions($page); + + $upload = function () use ($page) { + return $this->postJson('images/drawio', [ + 'uploaded_to' => $page->id, + 'image' => 'image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4gEcDCo5iYNs+gAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAFElEQVQI12O0jN/KgASYGFABqXwAZtoBV6Sl3hIAAAAASUVORK5CYII=', + ]); + }; + + $upload()->assertStatus(404); + + $this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first()); + $upload()->assertStatus(403); + + $this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first()); + $upload()->assertStatus(200); + } + public function test_drawio_url_can_be_configured() { config()->set('services.drawio', 'http://cats.com?dog=tree'); diff --git a/tests/Uploads/ImageTest.php b/tests/Uploads/ImageTest.php index 1bccee2c3..ac7bf31e4 100644 --- a/tests/Uploads/ImageTest.php +++ b/tests/Uploads/ImageTest.php @@ -36,6 +36,45 @@ class ImageTest extends TestCase ]); } + public function test_image_upload_with_page_reference_requires_visibility_and_update_permissions_of_target_page() + { + $page = $this->entities->page(); + $editor = $this->users->editor(); + + $this->permissions->disableEntityInheritedPermissions($page); + $this->actingAs($editor); + + $resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id); + $resp->assertStatus(404); + + $this->permissions->setEntityPermissionsForRole($page, ['view'], $editor->roles()->first()); + + $resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id); + $this->assertPermissionError($resp); + + $this->permissions->setEntityPermissionsForRole($page, ['view', 'update'], $editor->roles()->first()); + + $resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id); + $resp->assertStatus(200); + + $this->files->deleteAtRelativePath($resp->json('path')); + } + + public function test_image_upload_with_page_reference_requires_page_to_exist() + { + $page = $this->entities->page(); + $this->entities->destroy($page); + + $editor = $this->users->editor(); + $this->actingAs($editor); + + $resp = $this->files->uploadGalleryImage($this, 'test-image.png', $page->id); + $resp->assertStatus(404); + + $resp = $this->files->uploadGalleryImage($this, 'test-image.png', 0); + $resp->assertStatus(404); + } + public function test_image_display_thumbnail_generation_does_not_increase_image_size() { $page = $this->entities->page(); From 39a14cff8f64f51fa3b4be2f4c2a7b85d7c2b600 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 May 2026 13:00:38 +0100 Subject: [PATCH 63/74] User MFA: Reviewed addition of reset, added tests Review of #6056 Added test coverage. --- app/Activity/ActivityType.php | 1 + app/Users/Controllers/UserController.php | 10 ++-- lang/en/activities.php | 2 + lang/en/settings.php | 8 ++-- resources/views/users/edit.blade.php | 44 ++++++++++++------ routes/web.php | 2 +- tests/User/UserManagementMfaTest.php | 59 ++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 24 deletions(-) create mode 100644 tests/User/UserManagementMfaTest.php diff --git a/app/Activity/ActivityType.php b/app/Activity/ActivityType.php index a7f129f71..64532de17 100644 --- a/app/Activity/ActivityType.php +++ b/app/Activity/ActivityType.php @@ -46,6 +46,7 @@ class ActivityType const USER_CREATE = 'user_create'; const USER_UPDATE = 'user_update'; const USER_DELETE = 'user_delete'; + const USER_MFA_RESET = 'user_mfa_reset'; const API_TOKEN_CREATE = 'api_token_create'; const API_TOKEN_UPDATE = 'api_token_update'; diff --git a/app/Users/Controllers/UserController.php b/app/Users/Controllers/UserController.php index ba85f99fb..f73104381 100644 --- a/app/Users/Controllers/UserController.php +++ b/app/Users/Controllers/UserController.php @@ -4,6 +4,7 @@ namespace BookStack\Users\Controllers; use BookStack\Access\SocialDriverManager; use BookStack\Access\UserInviteException; +use BookStack\Activity\ActivityType; use BookStack\Exceptions\ImageUploadException; use BookStack\Exceptions\UserUpdateException; use BookStack\Http\Controller; @@ -214,11 +215,14 @@ class UserController extends Controller */ public function resetMfa(Request $request, int $id) { + $this->preventAccessInDemoMode(); $this->checkPermission(Permission::UsersManage); + $user = $this->userRepo->getById($id); - // Resetear el 2FA del usuario $user->mfaValues()->delete(); - session()->flash('success', trans('settings.users_mfa_reset_success', ['userName' => $user->name])); - return redirect()->back(); + + $this->logActivity(ActivityType::USER_MFA_RESET, $user); + + return redirect("/settings/users/{$user->id}"); } } diff --git a/lang/en/activities.php b/lang/en/activities.php index 4362fc029..e9344a3d4 100644 --- a/lang/en/activities.php +++ b/lang/en/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/en/settings.php b/lang/en/settings.php index 920c3c1bc..d03024a89 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -264,11 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', - 'users_mfa_reset' => 'Reset 2FA', - 'users_mfa_reset_desc' => 'Reset and clear all configured MFA methods for :userName. They will be prompted to reconfigure on next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset 2FA for :userName?', - 'users_mfa_reset_success' => '2FA has been reset for :userName', - 'users_mfa_reset_error' => 'Failed to reset 2FA for :userName', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php index 64d45f503..8a4a8bcce 100644 --- a/resources/views/users/edit.blade.php +++ b/resources/views/users/edit.blade.php @@ -71,21 +71,35 @@
  • - @if(user()->hasSystemRole('admin')) -
    -
    -
    -
    - {{ trans('settings.users_mfa_reset') }} -

    {{ trans('settings.users_mfa_reset_desc', ['userName' => $user->name]) }}

    -
    -
    -
    id}/reset-mfa") }}" method="POST" style="display: inline;"> - @csrf - + @if($mfaMethods->count() > 0) +
    + +
    +
    +

    {{ trans('settings.users_mfa_reset_desc') }}

    + id}/mfa") }}" method="POST" style="display: inline;"> + {{ csrf_field() }} + {{ method_field('DELETE') }} +
    diff --git a/routes/web.php b/routes/web.php index 2571da2f3..3e3b5aef3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -251,7 +251,7 @@ Route::middleware('auth')->group(function () { Route::get('/settings/users/{id}', [UserControllers\UserController::class, 'edit']); Route::put('/settings/users/{id}', [UserControllers\UserController::class, 'update']); Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']); - Route::post('/settings/users/{id}/reset-mfa', [UserControllers\UserController::class, 'resetMfa']); + Route::delete('/settings/users/{id}/mfa', [UserControllers\UserController::class, 'resetMfa']); // User Account Route::get('/my-account', [UserControllers\UserAccountController::class, 'redirect']); diff --git a/tests/User/UserManagementMfaTest.php b/tests/User/UserManagementMfaTest.php new file mode 100644 index 000000000..ea05869cb --- /dev/null +++ b/tests/User/UserManagementMfaTest.php @@ -0,0 +1,59 @@ +users->editor(); + + $resp = $this->asAdmin()->get($editor->getEditUrl()); + $resp->assertSeeText('0 methods configured'); + + MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_BACKUP_CODES]); + + $resp = $this->get($editor->getEditUrl()); + $resp->assertSeeText('1 method configured'); + $resp->assertDontSeeText('0 methods configured'); + } + + public function test_reset_mfa_flow() + { + $editor = $this->users->editor(); + MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_BACKUP_CODES]); + MfaValue::factory()->create(['user_id' => $editor->id, 'method' => MfaValue::METHOD_TOTP]); + + $this->assertEquals(2, $editor->mfaValues()->count()); + + $resp = $this->asAdmin()->get($editor->getEditUrl()); + $this->withHtml($resp)->assertElementContains('form[action$="/mfa"] button[type="submit"]', 'Reset'); + + $resp = $this->delete($editor->getEditUrl('/mfa')); + $resp->assertRedirect($editor->getEditUrl()); + $this->assertActivityExists(ActivityType::USER_MFA_RESET); + + $resp = $this->followRedirects($resp); + $resp->assertSee('Multi-factor authentication methods reset'); + + $this->assertEquals(0, $editor->mfaValues()->count()); + } + + public function test_users_manage_permission_required_for_mfa_reset() + { + $editor = $this->users->editor(); + $resp = $this->actingAs($editor)->delete($editor->getEditUrl('/mfa')); + $this->assertPermissionError($resp); + + $this->permissions->grantUserRolePermissions($editor, [Permission::UsersManage]); + + $resp = $this->delete($editor->getEditUrl('/mfa')); + $this->assertNotPermissionError($resp); + $resp->assertRedirect($editor->getEditUrl()); + } +} From 0cd773a5d3d07b1299660c22a3f65456b6aa28d0 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 17 May 2026 18:33:23 +0100 Subject: [PATCH 64/74] CSP Headers: Review of #6071 - Removed extra non-needed docs in repo - Tweaked some wording. - Added extra test scenarios. - Added options to phpunit default env. - Added auto-quote-handling for unsafe-inline CSS rule. For #6033 --- .env.example.complete | 5 +++-- app/Config/app.php | 3 ++- app/Util/CspService.php | 10 ++++++++- dev/docs/development.md | 42 ------------------------------------ phpunit.xml | 2 ++ readme.md | 1 - tests/SecurityHeaderTest.php | 35 ++++++++++++++++++++++++++++-- 7 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.env.example.complete b/.env.example.complete index 6a7f9db65..ed0cac5f4 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -402,9 +402,10 @@ ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtub ALLOWED_CSS_SOURCES=null # A list of sources/hostnames that can be loaded as image content within BookStack. -# Space separated if multiple. BookStack host domain is auto-inferred. +# Space separated if multiple. BookStack host domain is auto-inferred, in addition to +# data and blob images, due to their use for various functionality. # Defaults to a permissive set if not provided. -# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com data:" +# Example: ALLOWED_IMAGE_SOURCES="https://images.example.com" ALLOWED_IMAGE_SOURCES=null # A list of the sources/hostnames that can be reached by application SSR calls. diff --git a/app/Config/app.php b/app/Config/app.php index 5536e9abd..12f285ea9 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -78,7 +78,8 @@ return [ 'css_sources' => env('ALLOWED_CSS_SOURCES', null), // A list of sources/hostnames that can be loaded as image content within BookStack. - // Space separated if multiple. BookStack host domain is auto-inferred. + // Space separated if multiple. BookStack host domain is auto-inferred, in addition to + // data and blob images, due to their use for various functionality. // If not set, a permissive default set is used to reduce potential breakage. 'image_sources' => env('ALLOWED_IMAGE_SOURCES', null), diff --git a/app/Util/CspService.php b/app/Util/CspService.php index a0e1faadf..68c544ff0 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -175,6 +175,14 @@ class CspService $sources = array_filter(explode(' ', $configured)); array_unshift($sources, "'self'"); + // Ensure 'unsafe-inline' is quoted if present + // This is done as attempting to pass this in env values with quotes can either + // be awkward or cause issues. + $unsafeInlineIndex = array_search('unsafe-inline', $sources, true); + if ($unsafeInlineIndex !== false) { + $sources[$unsafeInlineIndex] = "'unsafe-inline'"; + } + return array_values(array_unique($sources)); } @@ -195,7 +203,7 @@ class CspService if (is_string($configured)) { $sources = array_filter(explode(' ', $configured)); - array_unshift($sources, "'self'"); + array_unshift($sources, "'self'", 'blob:', 'data:'); return array_values(array_unique($sources)); } diff --git a/dev/docs/development.md b/dev/docs/development.md index 16f168f9c..2c73a0256 100644 --- a/dev/docs/development.md +++ b/dev/docs/development.md @@ -31,48 +31,6 @@ BookStack has a large suite of PHP tests to cover application functionality. We For details about setting-up, running and writing tests please see the [php-testing.md document](php-testing.md). -## Content Security Policy Controls - -BookStack enforces a Content Security Policy (CSP) response header to reduce risk from injected content and untrusted embeds. - -For backward compatibility, image and CSS controls are intentionally permissive by default, but can be tightened via environment options. - -### Related Environment Options - -These values are defined in `.env.example.complete`: - -- `ALLOWED_CSS_SOURCES` - - Controls allowed `style-src` sources. - - Defaults to a permissive fallback if unset. -- `ALLOWED_IMAGE_SOURCES` - - Controls allowed `img-src` sources. - - Defaults to a permissive fallback if unset. - -Values should be space-separated source expressions. - -### Example Configurations - -Allow Google Fonts CSS and local styles only: - -```bash -ALLOWED_CSS_SOURCES="https://fonts.googleapis.com" -``` - -Allow local images, embedded data images, and a dedicated image CDN: - -```bash -ALLOWED_IMAGE_SOURCES="data: https://images.example.com" -``` - -### Tightening Guidance - -When hardening a deployment: - -1. Start with defaults to avoid unexpected breakage. -2. Set explicit `ALLOWED_CSS_SOURCES` and `ALLOWED_IMAGE_SOURCES` values for the domains you actually use. -3. Test key workflows (editor, page display, theme assets, external embeds) and browser console CSP warnings. -4. Remove unnecessary protocols and hosts over time. - ## Code Standards We use tools to manage code standards and formatting within the project. If submitting a PR, formatting as per our project standards would help for clarity but don't worry too much about using/understanding these tools as we can always address issues at a later stage when they're picked up by our automated tools. diff --git a/phpunit.xml b/phpunit.xml index 94fc002b7..52c5e7de8 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,6 +18,8 @@ + + diff --git a/readme.md b/readme.md index e231ad9f0..e9e5d197b 100644 --- a/readme.md +++ b/readme.md @@ -100,7 +100,6 @@ Big thanks to these companies for supporting the project. ## 🛠️ Development & Testing Please see our [development docs](dev/docs/development.md) for full details regarding work on the BookStack source code. -For details on Content Security Policy controls (including image and CSS source options), see the **Content Security Policy Controls** section in the [development docs](dev/docs/development.md). If you're just looking to customize or extend your own BookStack instance, take a look at our [Hacking BookStack documentation page](https://www.bookstackapp.com/docs/admin/hacking-bookstack/) for details on various options to achieve this without altering the BookStack source code. diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index 126a85f23..8badc39b9 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -153,6 +153,7 @@ class SecurityHeaderTest extends TestCase public function test_style_src_csp_header_set_to_permissive_defaults_when_not_configured() { + config()->set('app.css_sources', null); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'style-src'); @@ -169,8 +170,29 @@ class SecurityHeaderTest extends TestCase $this->assertEquals("style-src 'self' https://fonts.example.com", $header); } + public function test_style_src_csp_header_unsafe_inline_value_will_be_auto_quoted() + { + config()->set('app.css_sources', 'unsafe-inline https://css.example.com'); + + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'style-src'); + + $this->assertEquals("style-src 'self' 'unsafe-inline' https://css.example.com", $header); + } + + public function test_style_src_can_be_blank_to_set_no_additions() + { + config()->set('app.css_sources', ''); + + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'style-src'); + + $this->assertEquals("style-src 'self'", $header); + } + public function test_img_src_csp_header_set_to_permissive_defaults_when_not_configured() { + config()->set('app.image_sources', null); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'img-src'); @@ -179,12 +201,21 @@ class SecurityHeaderTest extends TestCase public function test_img_src_csp_header_can_be_overridden_by_config() { - config()->set('app.image_sources', 'https://images.example.com data:'); + config()->set('app.image_sources', 'https://images.example.com'); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'img-src'); - $this->assertEquals("img-src 'self' https://images.example.com data:", $header); + $this->assertEquals("img-src 'self' blob: data: https://images.example.com", $header); + } + + public function test_img_src_can_be_blank_to_set_no_additions() + { + config()->set('app.image_sources', ''); + $resp = $this->get('/'); + $header = $this->getCspHeader($resp, 'img-src'); + + $this->assertEquals("img-src 'self' blob: data:", $header); } public function test_cache_control_headers_are_set_on_responses() From b5d3ba2726dc2f3792f580a8d00cf8d6bbe01126 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 18 May 2026 17:39:00 +0100 Subject: [PATCH 65/74] Testing: Added extra page edit test Added during investigation for #6062 Might as well leave in even though it does not trigger the cause for that particuluar issue. --- tests/Entity/PageTest.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Entity/PageTest.php b/tests/Entity/PageTest.php index 1b2f3c9fe..0da76b8e9 100644 --- a/tests/Entity/PageTest.php +++ b/tests/Entity/PageTest.php @@ -273,4 +273,29 @@ class PageTest extends TestCase $resp->assertSessionHas('error', 'You do not have permission to access the requested page.'); } + + public function test_page_html_content_remains_stable_through_re_edit_and_does_not_create_revision() + { + $page = $this->entities->page(); + + $this->asEditor()->put($page->getUrl(), [ + 'name' => 'Stability Test', + 'html' => '
    a
    ', + ]); + $initialRevisionCount = $page->revisions()->count(); + $page->refresh(); + + // Get the page content from the edit view to ensure we're using the + // exact same content which would be loaded into the editor. + $editView = $this->get($page->getUrl('/edit')); + $htmlContentEncoded = $this->withHtml($editView)->getInnerHtml('textarea#html-editor'); + $htmlContent = html_entity_decode($htmlContentEncoded); + + $this->asEditor()->put($page->getUrl(), [ + 'name' => 'Stability Test', + 'html' => $htmlContent, + ]); + + $this->assertEquals($initialRevisionCount, $page->revisions()->count()); + } } From 5ebfa65a46a0ee0d1ef9c54140ac2bbd3dbd59e6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 19 May 2026 13:29:26 +0100 Subject: [PATCH 66/74] Testing: Changed ordering in tests to help prevent flaky test Think it would primariy use the created_at ordering based in the relation which could cause trouble in CI test environment. This better forces Id based ordering --- tests/Activity/WatchTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Activity/WatchTest.php b/tests/Activity/WatchTest.php index 8be09f890..dc78e8942 100644 --- a/tests/Activity/WatchTest.php +++ b/tests/Activity/WatchTest.php @@ -218,10 +218,10 @@ class WatchTest extends TestCase $notifications = Notification::fake(); - $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [ + $resp = $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [ 'html' => '

    My new comment

    ' ]); - $comment = $entities['page']->comments()->orderBy('id', 'desc')->first(); + $comment = $entities['page']->comments()->reorder('id', 'desc')->first(); $this->asAdmin()->post("/comment/{$entities['page']->id}", [ 'html' => '

    My new comment response

    ', From 49aa0250121f177c1828cc8c984c4e04cfeeba35 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 19 May 2026 17:56:48 +0100 Subject: [PATCH 67/74] Page Editor: Started contents view in toolbox Added visual system, not yet added on-click logic. Related to #4218 --- lang/en/entities.php | 3 + resources/icons/contents.svg | 1 + resources/js/components/index.ts | 1 + resources/js/components/toolbox-contents.ts | 132 ++++++++++++++++++ resources/js/services/vdom.ts | 4 + resources/sass/_lists.scss | 10 ++ .../pages/parts/editor-toolbox.blade.php | 3 + .../show-sidebar-section-page-nav.blade.php | 4 +- .../pages/parts/toolbox-contents.blade.php | 9 ++ 9 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 resources/icons/contents.svg create mode 100644 resources/js/components/toolbox-contents.ts create mode 100644 resources/views/pages/parts/toolbox-contents.blade.php diff --git a/lang/en/entities.php b/lang/en/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/resources/icons/contents.svg b/resources/icons/contents.svg new file mode 100644 index 000000000..3a315b431 --- /dev/null +++ b/resources/icons/contents.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/index.ts b/resources/js/components/index.ts index 736d93f05..688877227 100644 --- a/resources/js/components/index.ts +++ b/resources/js/components/index.ts @@ -58,6 +58,7 @@ export {Tabs} from './tabs'; export {TagManager} from './tag-manager'; export {TemplateManager} from './template-manager'; export {ToggleSwitch} from './toggle-switch'; +export {ToolboxContents} from './toolbox-contents'; export {TriLayout} from './tri-layout'; export {UserSelect} from './user-select'; export {WebhookEvents} from './webhook-events'; diff --git a/resources/js/components/toolbox-contents.ts b/resources/js/components/toolbox-contents.ts new file mode 100644 index 000000000..e09f2b5ac --- /dev/null +++ b/resources/js/components/toolbox-contents.ts @@ -0,0 +1,132 @@ +import {Component} from "./component"; +import {debounce} from "../services/util"; +import {EditorToolboxChangeEventData} from "./editor-toolbox"; +import {PageEditor} from "./page-editor"; +import {MarkdownEditor} from "./markdown-editor"; +import {WysiwygEditorTinymce} from "./wysiwyg-editor-tinymce"; +import {WysiwygEditor} from "./wysiwyg-editor"; +import {elem} from "../services/dom"; +import {patchDomFromDom} from "../services/vdom"; + +interface ToolboxContentHeader { + // The text shown for the header + text: string; + // The level/depth of the header + level: number; + // The index of the header relative to all other headers in the content + index: number; + // The id set for the header (if at all) + id: string; +} + +export class ToolboxContents extends Component { + protected container!: HTMLElement; + protected noneEl!: HTMLElement; + protected display!: HTMLElement; + protected isActive: boolean = false; + + setup() { + this.container = this.$el as HTMLLinkElement; + this.noneEl = this.$refs.none; + this.display = this.$refs.display; + + // Listen to when visible in the editor toolbox so we only update when visible + window.addEventListener('editor-toolbox-change', ((event: CustomEvent) => { + const tabName: string = event.detail.tab; + const isOpen = event.detail.open; + if (tabName === 'contents' && isOpen) { + if (this.isActive !== true) { + this.render(); + } + this.isActive = true; + } else { + this.isActive = false; + } + }) as EventListener); + + // Listen to content changes from the editor + const onContentChangeDebounced = debounce(this.onContentChange.bind(this), 500, false); + window.$events.listen('editor-html-change', onContentChangeDebounced); + window.$events.listen('editor-markdown-change', onContentChangeDebounced); + } + + protected onContentChange(): void { + if (!this.isActive) { + return; + } + this.render(); + } + + protected async render(): Promise { + const editorHtml = await this.getEditorHtml(); + const headers = this.parseHeadersFromHtml(editorHtml); + this.rebaseHeaders(headers); + + const headerDom = this.headersToDom(headers); + let displayChild = this.display.firstElementChild; + if (!displayChild) { + displayChild = document.createElement("ul"); + this.display.appendChild(displayChild); + } + patchDomFromDom(displayChild, headerDom); + + this.noneEl.hidden = headers.length > 0; + } + + protected async getEditorHtml(): Promise { + const pageEditorComponent = window.$components.first('page-editor') as PageEditor; + const editor = pageEditorComponent.getEditorComponent() as (MarkdownEditor|WysiwygEditorTinymce|WysiwygEditor); + return (await editor.getContent()).html; + } + + protected parseHeadersFromHtml(html: string): ToolboxContentHeader[] { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const headerNodes = doc.querySelectorAll('h1, h2, h3, h4, h5, h6'); + const headers: ToolboxContentHeader[] = []; + + for (let i = 0; i < headerNodes.length; i++) { + const headerNode = headerNodes[i]; + const level = Number(headerNode.tagName.replace('H', '')); + headers.push({ + text: headerNode.textContent, + level, + id: headerNode.id || '', + index: i, + }); + } + + return headers; + } + + protected rebaseHeaders(headers: ToolboxContentHeader[]): void { + if (headers.length === 0) { + return; + } + + do { + var minLevel = Math.min(...headers.map(h => h.level)); + if (minLevel > 1) { + for (const header of headers) { + header.level--; + } + } + } while (minLevel > 1); + } + + protected headersToDom(headers: ToolboxContentHeader[]): HTMLElement { + const headerItems = headers.map(header => { + return elem('li', { + 'data-level': String(header.level), + id: header.id, + class: `page-nav-item h${header.level}`, + }, [ + elem('a', {class: 'text-limit-lines-1 block', href: `#${header.id}`}, [header.text]), + elem('div', {class: 'link-background sidebar-page-nav-bullet'}), + ]); + }); + return elem('ul', { + class: 'sidebar-page-nav menu' + }, headerItems); + } +} \ No newline at end of file diff --git a/resources/js/services/vdom.ts b/resources/js/services/vdom.ts index c32f328f5..051b1a42a 100644 --- a/resources/js/services/vdom.ts +++ b/resources/js/services/vdom.ts @@ -24,3 +24,7 @@ export function patchDomFromHtmlString(domTarget: Element, html: string): void { contentDom.innerHTML = html; getPatcher()(toVNode(domTarget), toVNode(contentDom)); } + +export function patchDomFromDom(domTarget: Element, domSource: Element): void { + getPatcher()(toVNode(domTarget), toVNode(domSource)); +} diff --git a/resources/sass/_lists.scss b/resources/sass/_lists.scss index d9a1195aa..249dca0d5 100644 --- a/resources/sass/_lists.scss +++ b/resources/sass/_lists.scss @@ -93,6 +93,8 @@ list-style: none; @include mixins.margin(vars.$s, 0, vars.$m, vars.$xs); position: relative; + padding-inline: 0; + display: block; &:after { content: ''; display: block; @@ -153,6 +155,14 @@ } } } +.toolbox-tab-content .sidebar-page-nav { + .sidebar-page-nav-bullet { + box-shadow: 0 0 0 6px #FFFFFF; + } + li { + font-size: 1em; + } +} // Sidebar list .book-tree .sidebar-page-list { diff --git a/resources/views/pages/parts/editor-toolbox.blade.php b/resources/views/pages/parts/editor-toolbox.blade.php index 8b7a68a9a..8ef259b91 100644 --- a/resources/views/pages/parts/editor-toolbox.blade.php +++ b/resources/views/pages/parts/editor-toolbox.blade.php @@ -11,6 +11,7 @@ @if($comments->enabled()) @endif +
    @@ -37,4 +38,6 @@ @include('pages.parts.toolbox-comments') @endif + @include('pages.parts.toolbox-contents') +
    diff --git a/resources/views/pages/parts/show-sidebar-section-page-nav.blade.php b/resources/views/pages/parts/show-sidebar-section-page-nav.blade.php index 88db87e64..d046a8ca1 100644 --- a/resources/views/pages/parts/show-sidebar-section-page-nav.blade.php +++ b/resources/views/pages/parts/show-sidebar-section-page-nav.blade.php @@ -2,14 +2,14 @@ @endif \ No newline at end of file diff --git a/resources/views/pages/parts/toolbox-contents.blade.php b/resources/views/pages/parts/toolbox-contents.blade.php new file mode 100644 index 000000000..6234cdf43 --- /dev/null +++ b/resources/views/pages/parts/toolbox-contents.blade.php @@ -0,0 +1,9 @@ +
    +

    {{ trans('entities.page_contents') }}

    + +
    +

    {{ trans('entities.page_contents_info') }}

    + +
    +
    +
    \ No newline at end of file From c58eb918939c9ef24998e3da9379c29969d75f0b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 20 May 2026 12:12:38 +0100 Subject: [PATCH 68/74] Page Editor: Added contents view click logic Added jump-to-header logic for lexical WYSIWYG, and both codemirror & plaintext markdown editor windows. --- resources/js/components/toolbox-contents.ts | 22 ++++++++++++- resources/js/markdown/actions.ts | 31 +++++++++++++++++-- resources/js/markdown/common-events.ts | 5 +++ resources/js/markdown/inputs/codemirror.ts | 13 ++++++++ resources/js/markdown/inputs/interface.ts | 6 ++++ resources/js/markdown/inputs/textarea.ts | 14 +++++++++ resources/js/services/vdom.ts | 2 ++ .../js/wysiwyg/services/common-events.ts | 6 +++- resources/js/wysiwyg/utils/actions.ts | 13 +++++++- 9 files changed, 107 insertions(+), 5 deletions(-) diff --git a/resources/js/components/toolbox-contents.ts b/resources/js/components/toolbox-contents.ts index e09f2b5ac..569b574ee 100644 --- a/resources/js/components/toolbox-contents.ts +++ b/resources/js/components/toolbox-contents.ts @@ -48,6 +48,24 @@ export class ToolboxContents extends Component { const onContentChangeDebounced = debounce(this.onContentChange.bind(this), 500, false); window.$events.listen('editor-html-change', onContentChangeDebounced); window.$events.listen('editor-markdown-change', onContentChangeDebounced); + + // Listen to header click + this.container.addEventListener('click', (event) => { + const header = (event.target as HTMLElement).closest('li[data-index]'); + if (header instanceof HTMLElement) { + event.preventDefault(); + this.onHeaderSelect(header); + } + }); + } + + protected onHeaderSelect(headerElement: HTMLElement): void { + const id = headerElement.getAttribute('data-id') || ''; + const index = Number(headerElement.getAttribute('data-index') || ''); + window.$events.emit('editor::focus-heading', { + index, + id, + }); } protected onContentChange(): void { @@ -118,7 +136,9 @@ export class ToolboxContents extends Component { const headerItems = headers.map(header => { return elem('li', { 'data-level': String(header.level), - id: header.id, + 'data-index': String(header.index), + 'data-id': header.id, + id: `page-contents-${header.id}`, class: `page-nav-item h${header.level}`, }, [ elem('a', {class: 'text-limit-lines-1 block', href: `#${header.id}`}, [header.text]), diff --git a/resources/js/markdown/actions.ts b/resources/js/markdown/actions.ts index 36d21ab1d..ff1f0bc94 100644 --- a/resources/js/markdown/actions.ts +++ b/resources/js/markdown/actions.ts @@ -184,14 +184,41 @@ export class Actions { } } - focus() { + /** + * Set user focus on the editor edit area. + */ + focus(): void { this.editor.input.focus(); } + /** + * Focus on the text for a specific header in the content. + */ + focusOnHeader(index: number): void { + const headerPattern = /^\s{0,3}(#+| { + const isHeader = headerPattern.test(content); + if (isHeader && !inCodeBoundary) { + currentIndex++; + if (currentIndex === index) { + this.editor.input.setSelection({from: range.to, to: range.to}, true); + return false; + } + } else if (codeBoundary.test(content)) { + inCodeBoundary = !inCodeBoundary; + } + }); + + this.focus(); + } + /** * Insert content into the editor. */ - insertContent(content: string) { + insertContent(content: string): void { this.#replaceSelection(content, content.length); } diff --git a/resources/js/markdown/common-events.ts b/resources/js/markdown/common-events.ts index 4bfc4bb46..e8cbe561c 100644 --- a/resources/js/markdown/common-events.ts +++ b/resources/js/markdown/common-events.ts @@ -1,4 +1,5 @@ import {MarkdownEditor} from "./index.mjs"; +import {focusOnHeader} from "../wysiwyg/utils/actions"; export interface HtmlOrMarkdown { html: string; @@ -33,4 +34,8 @@ export function listenToCommonEvents(editor: MarkdownEditor): void { window.$events.listen('editor::focus', () => { editor.actions.focus(); }); + + window.$events.listen<{id: string, index: number}>('editor::focus-heading', ({index}) => { + editor.actions.focusOnHeader(index); + }); } diff --git a/resources/js/markdown/inputs/codemirror.ts b/resources/js/markdown/inputs/codemirror.ts index 827068238..8cc7f409c 100644 --- a/resources/js/markdown/inputs/codemirror.ts +++ b/resources/js/markdown/inputs/codemirror.ts @@ -102,6 +102,19 @@ export class CodemirrorInput implements MarkdownEditorInput { return {from: line.from, to: line.to}; } + forEachLine(callback: (lineNum: number, content: string, range: MarkdownEditorInputSelection) => false | void): void { + const docText = this.cm.state.doc; + let lineCount = 0; + for (const line of docText.iterLines()) { + lineCount++; + const lineInfo = docText.line(lineCount); + const result = callback(lineCount, line, {from: lineInfo.from, to: lineInfo.to}); + if (result === false) { + break; + } + } + } + /** * Dispatch changes to the editor. */ diff --git a/resources/js/markdown/inputs/interface.ts b/resources/js/markdown/inputs/interface.ts index 1f7474a50..d39f7dc14 100644 --- a/resources/js/markdown/inputs/interface.ts +++ b/resources/js/markdown/inputs/interface.ts @@ -74,6 +74,12 @@ export interface MarkdownEditorInput { */ searchForLineContaining(text: string): MarkdownEditorInputSelection|null; + /** + * Run the provided callback against each line of content within the input. + * Callback can return false to stop further processing. + */ + forEachLine(callback: (lineNum: number, content: string, range: MarkdownEditorInputSelection) => false|void): void; + /** * Tear down the input. */ diff --git a/resources/js/markdown/inputs/textarea.ts b/resources/js/markdown/inputs/textarea.ts index e0c3ac37c..c232a24bf 100644 --- a/resources/js/markdown/inputs/textarea.ts +++ b/resources/js/markdown/inputs/textarea.ts @@ -214,6 +214,20 @@ export class TextareaInput implements MarkdownEditorInput { return null; } + forEachLine(callback: (lineNum: number, content: string, range: MarkdownEditorInputSelection) => false | void): void { + const lines = this.getText().split('\n'); + let lineStart = 0; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const lineEnd = lineStart + line.length; + const result = callback(i + 1, line, {from: lineStart, to: lineEnd}); + if (result === false) { + break; + } + lineStart = lineEnd + 1; + } + } + setSelection(selection: MarkdownEditorInputSelection, scrollIntoView: boolean): void { this.input.selectionStart = selection.from; this.input.selectionEnd = selection.to; diff --git a/resources/js/services/vdom.ts b/resources/js/services/vdom.ts index 051b1a42a..a83686778 100644 --- a/resources/js/services/vdom.ts +++ b/resources/js/services/vdom.ts @@ -1,6 +1,7 @@ import { init, attributesModule, + datasetModule, toVNode, } from 'snabbdom'; import {VNode} from "snabbdom/build/vnode"; @@ -14,6 +15,7 @@ function getPatcher(): vDomPatcher { patcher = init([ attributesModule, + datasetModule, ]); return patcher; diff --git a/resources/js/wysiwyg/services/common-events.ts b/resources/js/wysiwyg/services/common-events.ts index f7fc81cb3..9c6a91787 100644 --- a/resources/js/wysiwyg/services/common-events.ts +++ b/resources/js/wysiwyg/services/common-events.ts @@ -1,7 +1,7 @@ import {LexicalEditor} from "lexical"; import { appendHtmlToEditor, - focusEditor, + focusEditor, focusOnHeader, insertHtmlIntoEditor, prependHtmlToEditor, setEditorContentFromHtml @@ -41,6 +41,10 @@ export function listen(editor: LexicalEditor): void { focusEditor(editor); }); + window.$events.listen<{id: string, index: number}>('editor::focus-heading', ({index}) => { + focusOnHeader(editor, index); + }); + let changeFromLoading = true; editor.registerUpdateListener(({dirtyElements, dirtyLeaves, editorState, prevEditorState}) => { // Emit change event to component system (for draft detection) on actual user content change diff --git a/resources/js/wysiwyg/utils/actions.ts b/resources/js/wysiwyg/utils/actions.ts index 465d44d31..f5b2db6aa 100644 --- a/resources/js/wysiwyg/utils/actions.ts +++ b/resources/js/wysiwyg/utils/actions.ts @@ -1,6 +1,7 @@ import {$createParagraphNode, $getRoot, $getSelection, $insertNodes, $isBlockElementNode, LexicalEditor} from "lexical"; import {$generateHtmlFromNodes} from "@lexical/html"; -import {$getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from "./nodes"; +import {$getAllNodesOfType, $getNearestNodeBlockParent, $htmlToBlockNodes, $htmlToNodes} from "./nodes"; +import {$isHeadingNode} from "@lexical/rich-text/LexicalHeadingNode"; export function setEditorContentFromHtml(editor: LexicalEditor, html: string) { editor.update(() => { @@ -101,4 +102,14 @@ export function focusEditor(editor: LexicalEditor): void { }); editor.commitUpdates(); editor.focus(() => {}, {defaultSelection: "rootStart"}); +} + +export function focusOnHeader(editor: LexicalEditor, headerIndex: number): void { + editor.update(() => { + const headers = $getAllNodesOfType($isHeadingNode); + const target = headers[headerIndex]; + if (target) { + target.selectStart(); + } + }); } \ No newline at end of file From d7ba0dc43082e09bd253bc92998c47293b0b1502 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 26 May 2026 12:50:19 +0100 Subject: [PATCH 69/74] CSP: Renamed CSS CSP option --- .env.example.complete | 4 ++-- app/Config/app.php | 4 ++-- app/Util/CspService.php | 2 +- phpunit.xml | 2 +- tests/SecurityHeaderTest.php | 8 ++++---- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.env.example.complete b/.env.example.complete index ed0cac5f4..6c773f601 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -398,8 +398,8 @@ ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtub # A list of sources/hostnames that can be loaded as CSS styles within BookStack. # Space separated if multiple. BookStack host domain is auto-inferred. # Defaults to a permissive set if not provided. -# Example: ALLOWED_CSS_SOURCES="https://fonts.googleapis.com" -ALLOWED_CSS_SOURCES=null +# Example: ALLOWED_STYLE_SOURCES="https://fonts.googleapis.com" +ALLOWED_STYLE_SOURCES=null # A list of sources/hostnames that can be loaded as image content within BookStack. # Space separated if multiple. BookStack host domain is auto-inferred, in addition to diff --git a/app/Config/app.php b/app/Config/app.php index 12f285ea9..c38cd0e1f 100644 --- a/app/Config/app.php +++ b/app/Config/app.php @@ -72,10 +72,10 @@ return [ // Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured. 'iframe_sources' => env('ALLOWED_IFRAME_SOURCES', 'https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com'), - // A list of sources/hostnames that can be loaded as CSS styles within BookStack. + // A list of style sources/hostnames that can be loaded styles within BookStack. // Space separated if multiple. BookStack host domain is auto-inferred. // If not set, a permissive default set is used to reduce potential breakage. - 'css_sources' => env('ALLOWED_CSS_SOURCES', null), + 'style_sources' => env('ALLOWED_STYLE_SOURCES', null), // A list of sources/hostnames that can be loaded as image content within BookStack. // Space separated if multiple. BookStack host domain is auto-inferred, in addition to diff --git a/app/Util/CspService.php b/app/Util/CspService.php index 68c544ff0..9b871e7ff 100644 --- a/app/Util/CspService.php +++ b/app/Util/CspService.php @@ -169,7 +169,7 @@ class CspService */ protected function getAllowedStyleSources(): array { - $configured = config('app.css_sources'); + $configured = config('app.style_sources'); if (is_string($configured)) { $sources = array_filter(explode(' ', $configured)); diff --git a/phpunit.xml b/phpunit.xml index 52c5e7de8..55d84d129 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -18,7 +18,7 @@ - + diff --git a/tests/SecurityHeaderTest.php b/tests/SecurityHeaderTest.php index 8badc39b9..5664084ff 100644 --- a/tests/SecurityHeaderTest.php +++ b/tests/SecurityHeaderTest.php @@ -153,7 +153,7 @@ class SecurityHeaderTest extends TestCase public function test_style_src_csp_header_set_to_permissive_defaults_when_not_configured() { - config()->set('app.css_sources', null); + config()->set('app.style_sources', null); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'style-src'); @@ -162,7 +162,7 @@ class SecurityHeaderTest extends TestCase public function test_style_src_csp_header_can_be_overridden_by_config() { - config()->set('app.css_sources', 'https://fonts.example.com'); + config()->set('app.style_sources', 'https://fonts.example.com'); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'style-src'); @@ -172,7 +172,7 @@ class SecurityHeaderTest extends TestCase public function test_style_src_csp_header_unsafe_inline_value_will_be_auto_quoted() { - config()->set('app.css_sources', 'unsafe-inline https://css.example.com'); + config()->set('app.style_sources', 'unsafe-inline https://css.example.com'); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'style-src'); @@ -182,7 +182,7 @@ class SecurityHeaderTest extends TestCase public function test_style_src_can_be_blank_to_set_no_additions() { - config()->set('app.css_sources', ''); + config()->set('app.style_sources', ''); $resp = $this->get('/'); $header = $this->getCspHeader($resp, 'style-src'); From 99e405f80f4be5c3b6cc89afe4a1fd5c4c0cd385 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 26 May 2026 12:58:48 +0100 Subject: [PATCH 70/74] Page Editor: Added contents click handling for TinyMCE editor --- resources/js/wysiwyg-tinymce/common-events.js | 7 +++++++ resources/js/wysiwyg-tinymce/scrolling.js | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/resources/js/wysiwyg-tinymce/common-events.js b/resources/js/wysiwyg-tinymce/common-events.js index d0a5acdc2..6f3d34f1f 100644 --- a/resources/js/wysiwyg-tinymce/common-events.js +++ b/resources/js/wysiwyg-tinymce/common-events.js @@ -1,3 +1,5 @@ +import {scrollToHeader} from "./scrolling"; + /** * @param {Editor} editor */ @@ -30,4 +32,9 @@ export function listen(editor) { editor.focus(); } }); + + // Focus on a specific heading + window.$events.listen('editor::focus-heading', ({index}) => { + scrollToHeader(editor, index); + }); } diff --git a/resources/js/wysiwyg-tinymce/scrolling.js b/resources/js/wysiwyg-tinymce/scrolling.js index 92f8f1583..b4f07984e 100644 --- a/resources/js/wysiwyg-tinymce/scrolling.js +++ b/resources/js/wysiwyg-tinymce/scrolling.js @@ -15,6 +15,23 @@ function scrollToText(editor, scrollId) { editor.focus(); } +/** + * Scroll to a specific header of the given index, relative to all headers in the content. + * @param {Editor} editor + * @param {Number} index + */ +export function scrollToHeader(editor, index) { + const headers = editor.dom.select('h1, h2, h3, h4, h5, h6'); + const targetHeader = headers[index]; + + if (targetHeader) { + targetHeader.scrollIntoView(); + editor.selection.select(targetHeader, true); + editor.selection.collapse(false); + editor.focus(); + } +} + /** * Scroll to a section dictated by the current URL query string, if present. * Used when directly editing a specific section of the page. From 0de1196b62efd626066dd893099edab35604a158 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 26 May 2026 13:27:48 +0100 Subject: [PATCH 71/74] Page Editor: Minor fixes - Removed unused import - Added some trailing newlines to code files - Prevented
    s confusing logic in MD editor - Aligned logic to select end of header across editors --- resources/js/components/toolbox-contents.ts | 2 +- resources/js/markdown/actions.ts | 2 +- resources/js/markdown/common-events.ts | 1 - resources/js/wysiwyg/utils/actions.ts | 4 ++-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/resources/js/components/toolbox-contents.ts b/resources/js/components/toolbox-contents.ts index 569b574ee..1cbaeda5d 100644 --- a/resources/js/components/toolbox-contents.ts +++ b/resources/js/components/toolbox-contents.ts @@ -149,4 +149,4 @@ export class ToolboxContents extends Component { class: 'sidebar-page-nav menu' }, headerItems); } -} \ No newline at end of file +} diff --git a/resources/js/markdown/actions.ts b/resources/js/markdown/actions.ts index ff1f0bc94..2fd02c8c8 100644 --- a/resources/js/markdown/actions.ts +++ b/resources/js/markdown/actions.ts @@ -195,7 +195,7 @@ export class Actions { * Focus on the text for a specific header in the content. */ focusOnHeader(index: number): void { - const headerPattern = /^\s{0,3}(#+|])/i; const codeBoundary = /^\s{0,3}```/i; let currentIndex = -1; let inCodeBoundary = false; diff --git a/resources/js/markdown/common-events.ts b/resources/js/markdown/common-events.ts index e8cbe561c..e24f39dab 100644 --- a/resources/js/markdown/common-events.ts +++ b/resources/js/markdown/common-events.ts @@ -1,5 +1,4 @@ import {MarkdownEditor} from "./index.mjs"; -import {focusOnHeader} from "../wysiwyg/utils/actions"; export interface HtmlOrMarkdown { html: string; diff --git a/resources/js/wysiwyg/utils/actions.ts b/resources/js/wysiwyg/utils/actions.ts index f5b2db6aa..c1d819f22 100644 --- a/resources/js/wysiwyg/utils/actions.ts +++ b/resources/js/wysiwyg/utils/actions.ts @@ -109,7 +109,7 @@ export function focusOnHeader(editor: LexicalEditor, headerIndex: number): void const headers = $getAllNodesOfType($isHeadingNode); const target = headers[headerIndex]; if (target) { - target.selectStart(); + target.selectEnd(); } }); -} \ No newline at end of file +} From c26b66887fc70e878c3aad74a9ab8db6217a9d89 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 May 2026 11:29:39 +0200 Subject: [PATCH 72/74] Updated translations with changes from Crowdin (#6139) ## Details ## Checklist - [ ] I have read the [BookStack community rules](https://www.bookstackapp.com/about/community-rules/). - [ ] This PR does not feature significant use of LLM/AI generation as per the community rules above. Co-authored-by: Crowdin Bot Reviewed-on: https://codeberg.org/bookstack/bookstack/pulls/6139 --- lang/ar/activities.php | 2 + lang/ar/auth.php | 1 + lang/ar/entities.php | 3 + lang/ar/settings.php | 4 + lang/bg/activities.php | 2 + lang/bg/auth.php | 1 + lang/bg/entities.php | 3 + lang/bg/settings.php | 4 + lang/bn/activities.php | 2 + lang/bn/auth.php | 1 + lang/bn/entities.php | 3 + lang/bn/settings.php | 4 + lang/bs/activities.php | 2 + lang/bs/auth.php | 1 + lang/bs/entities.php | 3 + lang/bs/settings.php | 4 + lang/ca/activities.php | 2 + lang/ca/auth.php | 1 + lang/ca/entities.php | 3 + lang/ca/settings.php | 4 + lang/cs/activities.php | 2 + lang/cs/auth.php | 1 + lang/cs/entities.php | 3 + lang/cs/settings.php | 4 + lang/cy/activities.php | 2 + lang/cy/auth.php | 1 + lang/cy/entities.php | 3 + lang/cy/settings.php | 4 + lang/da/activities.php | 2 + lang/da/auth.php | 1 + lang/da/entities.php | 3 + lang/da/settings.php | 4 + lang/de/activities.php | 2 + lang/de/auth.php | 3 +- lang/de/entities.php | 3 + lang/de/settings.php | 6 +- lang/de_informal/activities.php | 2 + lang/de_informal/auth.php | 1 + lang/de_informal/entities.php | 3 + lang/de_informal/settings.php | 4 + lang/el/activities.php | 2 + lang/el/auth.php | 1 + lang/el/entities.php | 3 + lang/el/settings.php | 4 + lang/es/activities.php | 2 + lang/es/auth.php | 1 + lang/es/entities.php | 3 + lang/es/settings.php | 4 + lang/es_AR/activities.php | 2 + lang/es_AR/auth.php | 1 + lang/es_AR/entities.php | 3 + lang/es_AR/settings.php | 4 + lang/et/activities.php | 2 + lang/et/auth.php | 1 + lang/et/entities.php | 5 +- lang/et/settings.php | 4 + lang/eu/activities.php | 2 + lang/eu/auth.php | 1 + lang/eu/entities.php | 3 + lang/eu/settings.php | 4 + lang/fa/activities.php | 2 + lang/fa/auth.php | 1 + lang/fa/entities.php | 3 + lang/fa/settings.php | 4 + lang/fi/activities.php | 2 + lang/fi/auth.php | 1 + lang/fi/entities.php | 3 + lang/fi/settings.php | 4 + lang/fr/activities.php | 2 + lang/fr/auth.php | 1 + lang/fr/entities.php | 3 + lang/fr/settings.php | 4 + lang/he/activities.php | 2 + lang/he/auth.php | 1 + lang/he/entities.php | 3 + lang/he/settings.php | 4 + lang/hr/activities.php | 2 + lang/hr/auth.php | 1 + lang/hr/entities.php | 3 + lang/hr/settings.php | 4 + lang/hu/activities.php | 36 +++-- lang/hu/auth.php | 63 ++++---- lang/hu/common.php | 12 +- lang/hu/components.php | 10 +- lang/hu/editor.php | 52 +++---- lang/hu/entities.php | 267 ++++++++++++++++---------------- lang/hu/errors.php | 110 ++++++------- lang/hu/notifications.php | 18 +-- lang/hu/passwords.php | 6 +- lang/hu/preferences.php | 34 ++-- lang/hu/settings.php | 232 +++++++++++++-------------- lang/hu/validation.php | 144 ++++++++--------- lang/id/activities.php | 2 + lang/id/auth.php | 1 + lang/id/entities.php | 3 + lang/id/settings.php | 4 + lang/is/activities.php | 2 + lang/is/auth.php | 1 + lang/is/entities.php | 3 + lang/is/settings.php | 4 + lang/it/activities.php | 2 + lang/it/auth.php | 1 + lang/it/entities.php | 3 + lang/it/settings.php | 6 +- lang/ja/activities.php | 2 + lang/ja/auth.php | 1 + lang/ja/entities.php | 3 + lang/ja/settings.php | 4 + lang/ka/activities.php | 2 + lang/ka/auth.php | 1 + lang/ka/entities.php | 3 + lang/ka/settings.php | 4 + lang/ko/activities.php | 2 + lang/ko/auth.php | 1 + lang/ko/entities.php | 3 + lang/ko/settings.php | 4 + lang/ku/activities.php | 2 + lang/ku/auth.php | 1 + lang/ku/entities.php | 3 + lang/ku/settings.php | 4 + lang/lt/activities.php | 2 + lang/lt/auth.php | 1 + lang/lt/entities.php | 3 + lang/lt/settings.php | 4 + lang/lv/activities.php | 2 + lang/lv/auth.php | 1 + lang/lv/entities.php | 3 + lang/lv/settings.php | 4 + lang/nb/activities.php | 2 + lang/nb/auth.php | 1 + lang/nb/entities.php | 3 + lang/nb/settings.php | 4 + lang/ne/activities.php | 2 + lang/ne/auth.php | 1 + lang/ne/entities.php | 3 + lang/ne/settings.php | 4 + lang/nl/activities.php | 2 + lang/nl/auth.php | 1 + lang/nl/entities.php | 3 + lang/nl/settings.php | 4 + lang/nn/activities.php | 2 + lang/nn/auth.php | 1 + lang/nn/entities.php | 3 + lang/nn/settings.php | 4 + lang/pl/activities.php | 2 + lang/pl/auth.php | 1 + lang/pl/entities.php | 3 + lang/pl/settings.php | 4 + lang/pt/activities.php | 2 + lang/pt/auth.php | 1 + lang/pt/entities.php | 3 + lang/pt/settings.php | 4 + lang/pt_BR/activities.php | 2 + lang/pt_BR/auth.php | 1 + lang/pt_BR/entities.php | 3 + lang/pt_BR/settings.php | 4 + lang/ro/activities.php | 2 + lang/ro/auth.php | 1 + lang/ro/entities.php | 3 + lang/ro/settings.php | 4 + lang/ru/activities.php | 2 + lang/ru/auth.php | 1 + lang/ru/entities.php | 3 + lang/ru/settings.php | 4 + lang/sk/activities.php | 2 + lang/sk/auth.php | 1 + lang/sk/entities.php | 3 + lang/sk/settings.php | 4 + lang/sl/activities.php | 2 + lang/sl/auth.php | 1 + lang/sl/entities.php | 3 + lang/sl/settings.php | 4 + lang/sq/activities.php | 2 + lang/sq/auth.php | 1 + lang/sq/entities.php | 3 + lang/sq/settings.php | 4 + lang/sr/activities.php | 2 + lang/sr/auth.php | 1 + lang/sr/entities.php | 3 + lang/sr/settings.php | 4 + lang/sv/activities.php | 2 + lang/sv/auth.php | 1 + lang/sv/entities.php | 3 + lang/sv/settings.php | 4 + lang/th/activities.php | 2 + lang/th/auth.php | 1 + lang/th/entities.php | 3 + lang/th/settings.php | 4 + lang/tk/activities.php | 2 + lang/tk/auth.php | 1 + lang/tk/entities.php | 3 + lang/tk/settings.php | 4 + lang/tr/activities.php | 2 + lang/tr/auth.php | 1 + lang/tr/entities.php | 3 + lang/tr/settings.php | 4 + lang/uk/activities.php | 2 + lang/uk/auth.php | 1 + lang/uk/entities.php | 3 + lang/uk/settings.php | 4 + lang/uz/activities.php | 2 + lang/uz/auth.php | 1 + lang/uz/entities.php | 3 + lang/uz/settings.php | 4 + lang/vi/activities.php | 2 + lang/vi/auth.php | 1 + lang/vi/entities.php | 3 + lang/vi/settings.php | 4 + lang/zh_CN/activities.php | 2 + lang/zh_CN/auth.php | 1 + lang/zh_CN/entities.php | 3 + lang/zh_CN/settings.php | 4 + lang/zh_TW/activities.php | 2 + lang/zh_TW/auth.php | 1 + lang/zh_TW/entities.php | 3 + lang/zh_TW/settings.php | 4 + 216 files changed, 1011 insertions(+), 491 deletions(-) diff --git a/lang/ar/activities.php b/lang/ar/activities.php index 69da12138..efef9af92 100644 --- a/lang/ar/activities.php +++ b/lang/ar/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'تم تحديث المستخدم بنجاح', 'user_delete' => 'المستخدم المحذوف', 'user_delete_notification' => 'تم إزالة المستخدم بنجاح', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-', diff --git a/lang/ar/auth.php b/lang/ar/auth.php index fb6f3d1cf..6551df4dc 100644 --- a/lang/ar/auth.php +++ b/lang/ar/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'البيانات المعطاة لا توافق سجلاتنا.', 'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'إنشاء حساب', diff --git a/lang/ar/entities.php b/lang/ar/entities.php index 64192a021..59b0d52e0 100644 --- a/lang/ar/entities.php +++ b/lang/ar/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'تبديل الشريط الجانبي', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'وسوم الصفحة', 'chapter_tags' => 'وسوم الفصل', 'book_tags' => 'وسوم الكتاب', diff --git a/lang/ar/settings.php b/lang/ar/settings.php index a1fef8364..aa361e04b 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.', 'users_mfa_x_methods' => ':count طريقة مُهيأة | :count طرق مُهيأة', 'users_mfa_configure' => 'إعداد الطرق', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'قم بإنشاء رمز API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/bg/activities.php b/lang/bg/activities.php index f5ed71a4c..344423e0d 100644 --- a/lang/bg/activities.php +++ b/lang/bg/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Потребителят е обновен успешно', 'user_delete' => 'deleted user', 'user_delete_notification' => 'Потребителят е премахнат успешно', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/bg/auth.php b/lang/bg/auth.php index 4a7e56f22..91c7495a1 100644 --- a/lang/bg/auth.php +++ b/lang/bg/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Въведените данни не съвпадат с информацията в системата.', 'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Регистриране', diff --git a/lang/bg/entities.php b/lang/bg/entities.php index 19e4f5188..c501483a3 100644 --- a/lang/bg/entities.php +++ b/lang/bg/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Тагове на страницата', 'chapter_tags' => 'Тагове на главата', 'book_tags' => 'Тагове на книгата', diff --git a/lang/bg/settings.php b/lang/bg/settings.php index 0554100fd..0af974140 100644 --- a/lang/bg/settings.php +++ b/lang/bg/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Настрой многофакторно удостверяване като втори слой сигурност на твоя профил.', 'users_mfa_x_methods' => ':count метод е настроен|:count методи са настроени', 'users_mfa_configure' => 'Конфигурирай методи', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Създай API маркер', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/bn/activities.php b/lang/bn/activities.php index c9268f8ab..a9e04399d 100644 --- a/lang/bn/activities.php +++ b/lang/bn/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'ব্যবহারকারীটি সার্থকভাবে হালনাগাদ করা হয়েছে', 'user_delete' => 'ব্যবহারকারীটি মুছে ফেলেছেন', 'user_delete_notification' => 'ব্যবহারকারীটি সার্থকভাবে মুছে ফেলা হয়েছে', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'এপিআই টোকেনটি তৈরী করেছেন', diff --git a/lang/bn/auth.php b/lang/bn/auth.php index 30879fcb6..9e3c07b2c 100644 --- a/lang/bn/auth.php +++ b/lang/bn/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'প্রদত্ত তথ্যনিরূপিত কোন রেকর্ড পাওয়া যায়নি।', 'throttle' => 'লগইন প্রচেষ্টার সীমা অতিক্রান্ত। দয়া করে :seconds সেকেন্ড পর আবার চেষ্টা করুন।', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'নিবন্ধিত হোন', diff --git a/lang/bn/entities.php b/lang/bn/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/bn/entities.php +++ b/lang/bn/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/bn/settings.php b/lang/bn/settings.php index 1bc5d1551..ab7fe9512 100644 --- a/lang/bn/settings.php +++ b/lang/bn/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/bs/activities.php b/lang/bs/activities.php index e7308770a..bf07d4a56 100644 --- a/lang/bs/activities.php +++ b/lang/bs/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/bs/auth.php b/lang/bs/auth.php index a8d0da78e..73848fbe1 100644 --- a/lang/bs/auth.php +++ b/lang/bs/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Ovi pristupni podaci se ne slažu sa našom evidencijom.', 'throttle' => 'Preveliki broj pokušaja prijave. Molimo vas da pokušate ponovo za :seconds sekundi.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registruj se', diff --git a/lang/bs/entities.php b/lang/bs/entities.php index b490c8c94..879e2b3ea 100644 --- a/lang/bs/entities.php +++ b/lang/bs/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Oznake stranice', 'chapter_tags' => 'Oznake poglavlja', 'book_tags' => 'Oznake knjige', diff --git a/lang/bs/settings.php b/lang/bs/settings.php index 3937c650f..d03024a89 100644 --- a/lang/bs/settings.php +++ b/lang/bs/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ca/activities.php b/lang/ca/activities.php index 4894c279a..d797286cb 100644 --- a/lang/ca/activities.php +++ b/lang/ca/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'S’ha actualitzat l’usuari', 'user_delete' => 'ha suprimit l’usuari', 'user_delete_notification' => 'S’ha suprimit l’usuari', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'ha creat el testimoni API', diff --git a/lang/ca/auth.php b/lang/ca/auth.php index ba6ede42f..c503c6b6b 100644 --- a/lang/ca/auth.php +++ b/lang/ca/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Aquestes credencials no existeixen al nostre registre.', 'throttle' => 'Massa intents d’inici de sessió. Torneu-ho a provar d’aquí :seconds segons.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registreu-vos', diff --git a/lang/ca/entities.php b/lang/ca/entities.php index edbfdbc44..3a37d3037 100644 --- a/lang/ca/entities.php +++ b/lang/ca/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Commuta la barra lateral', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Etiquetes de la pàgina', 'chapter_tags' => 'Etiquetes del capítol', 'book_tags' => 'Etiquetes del llibre', diff --git a/lang/ca/settings.php b/lang/ca/settings.php index 2a2106eb3..ac60ce8e2 100644 --- a/lang/ca/settings.php +++ b/lang/ca/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Configureu l’autenticació multifactorial per a afegir una capa de seguretat extra al vostre compte d’usuari.', 'users_mfa_x_methods' => 'Hi ha :count mètode configurat|Hi ha :count mètodes configurats', 'users_mfa_configure' => 'Configura un mètode', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Crea un testimoni API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/cs/activities.php b/lang/cs/activities.php index f7e9337d3..3e497eced 100644 --- a/lang/cs/activities.php +++ b/lang/cs/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Uživatel byl úspěšně aktualizován', 'user_delete' => 'odstranil uživatele', 'user_delete_notification' => 'Uživatel byl úspěšně odstraněn', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API token byl vytvořen', diff --git a/lang/cs/auth.php b/lang/cs/auth.php index ad225a2fe..4ad62ff1a 100644 --- a/lang/cs/auth.php +++ b/lang/cs/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Neplatné přihlašovací údaje.', 'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds sekund.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrace', diff --git a/lang/cs/entities.php b/lang/cs/entities.php index 7db62f7bb..e44dcf84e 100644 --- a/lang/cs/entities.php +++ b/lang/cs/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Skrýt/Zobrazit postranní panel', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Štítky stránky', 'chapter_tags' => 'Štítky kapitoly', 'book_tags' => 'Štítky knihy', diff --git a/lang/cs/settings.php b/lang/cs/settings.php index a7ab9927d..c11b7ee58 100644 --- a/lang/cs/settings.php +++ b/lang/cs/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Nastavit vícefaktorové ověřování jako další vrstvu zabezpečení vašeho uživatelského účtu.', 'users_mfa_x_methods' => ':count nastavená metoda|:count nastavených metod', 'users_mfa_configure' => 'Konfigurovat metody', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Vytvořit API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/cy/activities.php b/lang/cy/activities.php index 2459f2357..da2ac372c 100644 --- a/lang/cy/activities.php +++ b/lang/cy/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Diweddarwyd y defnyddiwr yn llwyddiannus', 'user_delete' => 'dileodd ddefnyddiwr', 'user_delete_notification' => 'Tynnwyd y defnyddiwr yn llwyddiannus', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'creodd docyn API', diff --git a/lang/cy/auth.php b/lang/cy/auth.php index 5c4034fcd..44781a9bb 100644 --- a/lang/cy/auth.php +++ b/lang/cy/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Nid yw\'r manylion hyn yn cyfateb i\'n cofnodion.', 'throttle' => 'Gormod o ymdrechion mewngofnodi. Rhowch gynnig arall arni o gwmpas :seconds eiliadau.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Cofrestru', diff --git a/lang/cy/entities.php b/lang/cy/entities.php index af1f6c43c..0a042e715 100644 --- a/lang/cy/entities.php +++ b/lang/cy/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toglo Bar ochr', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Tagiau Tudalennau', 'chapter_tags' => 'Tagiau Penodau', 'book_tags' => 'Tagiau Llyfrau', diff --git a/lang/cy/settings.php b/lang/cy/settings.php index a0519cccf..816a4b89f 100644 --- a/lang/cy/settings.php +++ b/lang/cy/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Gosod dilysu aml-ffactor fel haen ychwanegol o ddiogelwch ar gyfer eich cyfrif defnyddiwr.', 'users_mfa_x_methods' => ':count dull wedi\'i ffurfweddu|:count dull wedi\'u ffurfweddu', 'users_mfa_configure' => 'Ffurfweddu Dulliau', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Creu Tocyn API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/da/activities.php b/lang/da/activities.php index fa11e8e3a..00cb76b5f 100644 --- a/lang/da/activities.php +++ b/lang/da/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Brugeren blev opdateret', 'user_delete' => 'slettet bruger', 'user_delete_notification' => 'Brugeren blev fjernet', + 'user_mfa_reset' => 'nulstil MFA for brugeren', + 'user_mfa_reset_notification' => 'Nulstilling af metoder til multifaktor-godkendelse', // API Tokens 'api_token_create' => 'oprettet API token', diff --git a/lang/da/auth.php b/lang/da/auth.php index dcc531125..95ebd6230 100644 --- a/lang/da/auth.php +++ b/lang/da/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'De indtastede brugeroplysninger stemmer ikke overens med vores registreringer.', 'throttle' => 'For mange mislykkede loginforsøg. Prøv igen om :seconds sekunder.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrer', diff --git a/lang/da/entities.php b/lang/da/entities.php index 0aa35cc5d..f1c5735ed 100644 --- a/lang/da/entities.php +++ b/lang/da/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Sidebjælke til/fra', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sidetags', 'chapter_tags' => 'Kapiteltags', 'book_tags' => 'Bogtags', diff --git a/lang/da/settings.php b/lang/da/settings.php index fb5a1c958..448b00c4c 100644 --- a/lang/da/settings.php +++ b/lang/da/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Opsæt multi-faktor godkendelse som et ekstra lag af sikkerhed for din brugerkonto.', 'users_mfa_x_methods' => ':count metode konfigureret|:count metoder konfigureret', 'users_mfa_configure' => 'Konfigurer metoder', + 'users_mfa_reset' => 'Nulstil metoder til multifaktor-godkendelse', + 'users_mfa_reset_desc' => 'Dette vil nulstille og slette alle konfigurerede metoder til multifaktor-godkendelse for denne bruger. Hvis multifaktor-godkendelse er påkrævet for en af brugerens roller, vil vedkommende blive bedt om at konfigurere nye metoder ved næste login.', + 'users_mfa_reset_confirm' => 'Er du sikker på, at du vil nulstille multifaktorautentificering for denne bruger?', // API Tokens 'user_api_token_create' => 'Opret API-token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/de/activities.php b/lang/de/activities.php index 025cbaeff..a680a72c5 100644 --- a/lang/de/activities.php +++ b/lang/de/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Benutzer erfolgreich aktualisiert', 'user_delete' => 'hat Benutzer gelöscht: ', 'user_delete_notification' => 'Benutzer erfolgreich entfernt', + 'user_mfa_reset' => 'Setze MFA für Nutzer zurück', + 'user_mfa_reset_notification' => 'Multifaktor-Authenifizierungsmethoden zurücksetzen', // API Tokens 'api_token_create' => 'API-Token erstellt', diff --git a/lang/de/auth.php b/lang/de/auth.php index 92789d182..0ae2dcd97 100644 --- a/lang/de/auth.php +++ b/lang/de/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Diese Anmeldedaten stimmen nicht mit unseren Aufzeichnungen überein.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen Sie es in :seconds Sekunden erneut.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrieren', @@ -39,7 +40,7 @@ return [ 'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.', // Login auto-initiation - 'auto_init_starting' => 'Anmeldeversuche', + 'auto_init_starting' => 'Anmeldeversuch', 'auto_init_starting_desc' => 'Wir verbinden uns mit Ihrem Authentifizierungssystem, um den Anmeldeprozess zu starten. Sollte es nach 5 Sekunden nicht weitergehen, klicken Sie bitte auf den unten stehenden Link.', 'auto_init_start_link' => 'Mit Authentifizierung fortfahren', diff --git a/lang/de/entities.php b/lang/de/entities.php index db5d070f4..a35a5f4c4 100644 --- a/lang/de/entities.php +++ b/lang/de/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Seitenleiste umschalten', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Seiten-Schlagwörter', 'chapter_tags' => 'Kapitel-Schlagwörter', 'book_tags' => 'Buch-Schlagwörter', diff --git a/lang/de/settings.php b/lang/de/settings.php index 691ee2ee6..983de75ba 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -147,7 +147,7 @@ return [ 'recycle_bin_restore_confirm' => 'Durch diese Aktion wird das gelöschte Element einschließlich aller untergeordneten Elemente an seinem ursprünglichen Speicherort wiederhergestellt. Sollte der ursprüngliche Speicherort inzwischen gelöscht worden sein und sich nun im Papierkorb befinden, muss auch das übergeordnete Element wiederhergestellt werden.', 'recycle_bin_restore_deleted_parent' => 'Das übergeordnete Element dieses Eintrags wurde ebenfalls gelöscht. Diese Einträge bleiben gelöscht, bis auch das übergeordnete Element wiederhergestellt wird.', 'recycle_bin_restore_parent' => 'Übergeordneter Eintrag wiederherstellen', - 'recycle_bin_destroy_notification' => 'Löscht :count Elemente aus dem Papierkorb.', + 'recycle_bin_destroy_notification' => ':count Elemente aus dem Papierkorb gelöscht.', 'recycle_bin_restore_notification' => 'Es wurden :count der Elemente aus dem Papierkorb wiederhergestellt.', // Audit Log @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Richten Sie die Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für Ihr Benutzerkonto ein.', 'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert', 'users_mfa_configure' => 'Methoden konfigurieren', + 'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück', + 'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, werden sie aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.', + 'users_mfa_reset_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?', // API Tokens 'user_api_token_create' => 'Neuen API-Token erstellen', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slowenisch', 'sl' => 'Slowenisch', 'sv' => 'Schwedisch', + 'th' => 'ภาษาไทย', 'tr' => 'Türkisch', 'uk' => 'Ukrainisch', 'uz' => 'O‘zbekcha', diff --git a/lang/de_informal/activities.php b/lang/de_informal/activities.php index 990bfa0bc..85a572c5c 100644 --- a/lang/de_informal/activities.php +++ b/lang/de_informal/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Benutzer erfolgreich aktualisiert', 'user_delete' => 'hat Benutzer gelöscht: ', 'user_delete_notification' => 'Benutzer erfolgreich entfernt', + 'user_mfa_reset' => 'Setze MFA für Nutzer zurück', + 'user_mfa_reset_notification' => 'Multifaktor-Authenifizierungsmethoden zurücksetzen', // API Tokens 'api_token_create' => 'API Token wurde erstellt', diff --git a/lang/de_informal/auth.php b/lang/de_informal/auth.php index 17dcdbcb2..b91db2551 100644 --- a/lang/de_informal/auth.php +++ b/lang/de_informal/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Die eingegebenen Anmeldedaten sind ungültig.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrieren', diff --git a/lang/de_informal/entities.php b/lang/de_informal/entities.php index 708397e7e..4b4c7e4ef 100644 --- a/lang/de_informal/entities.php +++ b/lang/de_informal/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Seitenleiste umschalten', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Seiten-Schlagwörter', 'chapter_tags' => 'Kapitel-Schlagwörter', 'book_tags' => 'Buch-Schlagwörter', diff --git a/lang/de_informal/settings.php b/lang/de_informal/settings.php index 93ed1902f..84e0c8b10 100644 --- a/lang/de_informal/settings.php +++ b/lang/de_informal/settings.php @@ -265,6 +265,9 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'users_mfa_desc' => 'Richte Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', 'users_mfa_x_methods' => ':count Methode konfiguriert|:count Methoden konfiguriert', 'users_mfa_configure' => 'Methoden konfigurieren', + 'users_mfa_reset' => 'Setze Multifaktor-Authentifizierung zurück', + 'users_mfa_reset_desc' => 'Dies wird alle konfigurierten Multifaktor-Authentifizierungsmethoden für diesen Nutzer zurücksetzen. Falls Multifaktor-Authentifizierung für eine seiner Rollen erforderlich ist, werden sie aufgefordert, neue Methoden beim nächsten Login zu konfigurieren.', + 'users_mfa_reset_confirm' => 'Sind Sie sicher, dass Sie diese Multi-Faktor-Authentifizierungsmethode für diesen Nutzer zurücksetzen möchten?', // API Tokens 'user_api_token_create' => 'Neuen API-Token erstellen', @@ -365,6 +368,7 @@ Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung 'sk' => 'Slowenisch', 'sl' => 'Slowenisch', 'sv' => 'Schwedisch', + 'th' => 'ภาษาไทย', 'tr' => 'Türkisch', 'uk' => 'Ukrainisch', 'uz' => 'O‘zbekcha', diff --git a/lang/el/activities.php b/lang/el/activities.php index 226ef2540..bd82628f4 100644 --- a/lang/el/activities.php +++ b/lang/el/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Ο Χρήστης ενημερώθηκε με επιτυχία', 'user_delete' => 'διαγραμμένος χρήστης', 'user_delete_notification' => 'Ο Χρήστης αφαιρέθηκε επιτυχώς', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/el/auth.php b/lang/el/auth.php index 0b94ef859..b51208fbd 100644 --- a/lang/el/auth.php +++ b/lang/el/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Αυτά τα διαπιστευτήρια δεν ταιριάζουν με τα αρχεία μας.', 'throttle' => 'Πάρα πολλές προσπάθειες σύνδεσης. Δοκιμάστε ξανά σε :δευτερόλεπτα.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Εγγραφείτε', diff --git a/lang/el/entities.php b/lang/el/entities.php index b551d1a86..06b263361 100644 --- a/lang/el/entities.php +++ b/lang/el/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Ετικέτες Σελίδας', 'chapter_tags' => 'Ετικέτες Κεφαλαίου', 'book_tags' => 'Ετικέτες Βιβλίου', diff --git a/lang/el/settings.php b/lang/el/settings.php index 605b8b40e..42422a8e7 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Ρυθμίστε τον έλεγχο ταυτότητας πολλαπλών παραγόντων ως ένα επιπλέον επίπεδο ασφάλειας για τον λογαριασμό χρήστη σας.', 'users_mfa_x_methods' => 'Έχει ρυθμιστεί :count μέθοδος|Έχουν ρυθμιστεί :count μέθοδοι', 'users_mfa_configure' => 'Ρύθμιση Μεθόδων', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Δημιουργία διακριτικού (API Token)', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/es/activities.php b/lang/es/activities.php index 89a11c63a..ecfffeb51 100644 --- a/lang/es/activities.php +++ b/lang/es/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Usuario actualizado correctamente', 'user_delete' => 'usuario eliminado', 'user_delete_notification' => 'Usuario eliminado correctamente', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'token de API creado', diff --git a/lang/es/auth.php b/lang/es/auth.php index 45d3eb329..467508001 100644 --- a/lang/es/auth.php +++ b/lang/es/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Estas credenciales no coinciden con nuestros registros.', 'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, inténtalo de nuevo en :seconds segundos.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrarse', diff --git a/lang/es/entities.php b/lang/es/entities.php index a8ec3b10a..781416122 100644 --- a/lang/es/entities.php +++ b/lang/es/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Mostrar/ocultar barra lateral', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Etiquetas de Página', 'chapter_tags' => 'Etiquetas de Capítulo', 'book_tags' => 'Etiquetas de Libro', diff --git a/lang/es/settings.php b/lang/es/settings.php index 516480b64..2fe672f83 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar métodos', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Eslovaco', 'sl' => 'Esloveno', 'sv' => 'Sueco', + 'th' => 'ภาษาไทย', 'tr' => 'Turco', 'uk' => 'Ucraniano', 'uz' => 'O‘zbekcha', diff --git a/lang/es_AR/activities.php b/lang/es_AR/activities.php index e01fe4e64..b4ba5690e 100644 --- a/lang/es_AR/activities.php +++ b/lang/es_AR/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Usuario actualizado con éxito', 'user_delete' => 'usuario eliminado', 'user_delete_notification' => 'El usuario fue eliminado correctamente', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'token de API creado', diff --git a/lang/es_AR/auth.php b/lang/es_AR/auth.php index 7687f709e..ad69e651e 100644 --- a/lang/es_AR/auth.php +++ b/lang/es_AR/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Estas credenciales no concuerdan con nuestros registros.', 'throttle' => 'Demasiados intentos fallidos de inicio de sesión. Por favor intente nuevamente en :seconds segundos.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrarse', diff --git a/lang/es_AR/entities.php b/lang/es_AR/entities.php index 2dc1bd648..aac46b447 100644 --- a/lang/es_AR/entities.php +++ b/lang/es_AR/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Mostrar/ocultar barra lateral', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Etiquetas de página', 'chapter_tags' => 'Etiquetas de capítulo', 'book_tags' => 'Etiquetas de libro', diff --git a/lang/es_AR/settings.php b/lang/es_AR/settings.php index 3b82d0fb3..5545b91f3 100644 --- a/lang/es_AR/settings.php +++ b/lang/es_AR/settings.php @@ -265,6 +265,9 @@ return [ 'users_mfa_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -365,6 +368,7 @@ return [ 'sk' => 'Eslovaco', 'sl' => 'Esloveno', 'sv' => 'Sueco', + 'th' => 'ภาษาไทย', 'tr' => 'Turco', 'uk' => 'Ucraniano', 'uz' => 'O‘zbekcha', diff --git a/lang/et/activities.php b/lang/et/activities.php index 3a86dcd86..e42dd7fda 100644 --- a/lang/et/activities.php +++ b/lang/et/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Kasutaja on muudetud', 'user_delete' => 'kustutas kasutaja', 'user_delete_notification' => 'Kasutaja on kustutatud', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'lisas API tunnuse', diff --git a/lang/et/auth.php b/lang/et/auth.php index 014a9f858..afbd2870b 100644 --- a/lang/et/auth.php +++ b/lang/et/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Kasutajanimi ja parool ei klapi.', 'throttle' => 'Liiga palju sisselogimiskatseid. Proovi uuesti :seconds sekundi pärast.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registreeru', diff --git a/lang/et/entities.php b/lang/et/entities.php index 4b7be605a..11c2f77ae 100644 --- a/lang/et/entities.php +++ b/lang/et/entities.php @@ -173,7 +173,7 @@ return [ 'books_sort_desc' => 'Liiguta raamatu sees peatükke ja lehti, et selle sisu ümber organiseerida. Saad lisada teisi raamatuid, mis võimaldab peatükke ja lehti lihtsasti raamatute vahel liigutada. Lisaks saad määrata automaatse sorteerimise reegli, et selle raamatu sisu muudatuste puhul automaatselt järjestada.', 'books_sort_auto_sort' => 'Automaatne sorteerimine', 'books_sort_auto_sort_active' => 'Automaatne sorteerimine aktiivne: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'Automaatse sorteerimise reegleid saab lisada vajalike õigustega kasutaja "Loendid ja järjestamine" seadetes.', 'books_sort_named' => 'Sorteeri raamat :bookName', 'books_sort_name' => 'Sorteeri nime järgi', 'books_sort_created' => 'Sorteeri loomisaja järgi', @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Kuva/peida külgriba', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Lehe sildid', 'chapter_tags' => 'Peatüki sildid', 'book_tags' => 'Raamatu sildid', diff --git a/lang/et/settings.php b/lang/et/settings.php index fd00da9e3..3e28eae95 100644 --- a/lang/et/settings.php +++ b/lang/et/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.', 'users_mfa_x_methods' => ':count meetod seadistatud|:count meetodit seadistatud', 'users_mfa_configure' => 'Seadista meetodid', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Lisa API tunnus', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Sloveenia', 'sv' => 'Rootsi', + 'th' => 'ภาษาไทย', 'tr' => 'Türgi', 'uk' => 'Ukraina', 'uz' => 'O‘zbekcha', diff --git a/lang/eu/activities.php b/lang/eu/activities.php index 847014efe..dadee1315 100644 --- a/lang/eu/activities.php +++ b/lang/eu/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Erabiltzailea egoki eguneratua', 'user_delete' => 'deleted user', 'user_delete_notification' => 'Erabiltzailea egoki ezabatua', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/eu/auth.php b/lang/eu/auth.php index 35cbafd72..6033035bd 100644 --- a/lang/eu/auth.php +++ b/lang/eu/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Kredentzial hauek ez dira egokiak.', 'throttle' => 'Login saiakera kopurua pasa duzu. Mesedez, saiatu berriz :seconds segundu barru.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Izena eman', diff --git a/lang/eu/entities.php b/lang/eu/entities.php index 4d9e7cb06..43277cc55 100644 --- a/lang/eu/entities.php +++ b/lang/eu/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Orrialde etiketak', 'chapter_tags' => 'Kapitulu etiketak', 'book_tags' => 'Liburu etiketak', diff --git a/lang/eu/settings.php b/lang/eu/settings.php index dd346d1c9..ffa2c182e 100644 --- a/lang/eu/settings.php +++ b/lang/eu/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Sortu Tokena', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/fa/activities.php b/lang/fa/activities.php index 07eb78f37..876c545a6 100644 --- a/lang/fa/activities.php +++ b/lang/fa/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'کاربر با موفقیت به روز شد', 'user_delete' => 'کاربر حذف شده', 'user_delete_notification' => 'کاربر با موفقیت حذف شد', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'ایجاد توکن API', diff --git a/lang/fa/auth.php b/lang/fa/auth.php index c92f5f22b..189666399 100644 --- a/lang/fa/auth.php +++ b/lang/fa/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'مشخصات وارد شده با اطلاعات ما سازگار نیست.', 'throttle' => 'دفعات تلاش شما برای ورود بیش از حد مجاز است. لطفا پس از :seconds ثانیه مجددا تلاش فرمایید.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'ثبت نام', diff --git a/lang/fa/entities.php b/lang/fa/entities.php index 2abcff576..afeeca0e0 100644 --- a/lang/fa/entities.php +++ b/lang/fa/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'نمایش/پنهان‌سازی نوار کناری', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'برچسب‌های صفحه', 'chapter_tags' => 'برچسب‌های فصل', 'book_tags' => 'برچسب های کتاب', diff --git a/lang/fa/settings.php b/lang/fa/settings.php index 21ad96240..bb1b1ca7e 100644 --- a/lang/fa/settings.php +++ b/lang/fa/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.', 'users_mfa_x_methods' => ':count روش پیکربندی شده است|:count روش های پیکربندی شده', 'users_mfa_configure' => 'روش پیکربندی', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'ایجاد توکن API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/fi/activities.php b/lang/fi/activities.php index a5ee14906..95c192e22 100644 --- a/lang/fi/activities.php +++ b/lang/fi/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Käyttäjä päivitettiin onnistuneesti', 'user_delete' => 'poisti käyttäjän', 'user_delete_notification' => 'Käyttäjä poistettiin onnistuneesti', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'loi API-tunnisteen', diff --git a/lang/fi/auth.php b/lang/fi/auth.php index 580c86797..16d6de346 100644 --- a/lang/fi/auth.php +++ b/lang/fi/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Annettuja käyttäjätietoja ei löydy.', 'throttle' => 'Liikaa kirjautumisyrityksiä. Yritä uudelleen :seconds sekunnin päästä.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Rekisteröidy', diff --git a/lang/fi/entities.php b/lang/fi/entities.php index dd2ad5e69..db53669bb 100644 --- a/lang/fi/entities.php +++ b/lang/fi/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Näytä/piilota sivupalkki', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sivun tunnisteet', 'chapter_tags' => 'Lukujen tunnisteet', 'book_tags' => 'Kirjojen tunnisteet', diff --git a/lang/fi/settings.php b/lang/fi/settings.php index 3aec01886..aa8ac3e59 100644 --- a/lang/fi/settings.php +++ b/lang/fi/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Paranna käyttäjätilisi turvallisuutta ja ota käyttöön monivaiheinen tunnistautuminen.', 'users_mfa_x_methods' => ':count menetelmä määritetty|:count menetelmää määritetty', 'users_mfa_configure' => 'Määritä menetelmiä', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Luo uusi API-tunniste', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/fr/activities.php b/lang/fr/activities.php index 0e70917da..fcacc9089 100644 --- a/lang/fr/activities.php +++ b/lang/fr/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Utilisateur mis à jour avec succès', 'user_delete' => 'utilisateur supprimé', 'user_delete_notification' => 'Utilisateur supprimé avec succès', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'a créé un jeton API', diff --git a/lang/fr/auth.php b/lang/fr/auth.php index 61ca0ee3e..a7fe59d0c 100644 --- a/lang/fr/auth.php +++ b/lang/fr/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Ces informations ne correspondent à aucun compte.', 'throttle' => 'Trop d\'essais, veuillez réessayer dans :seconds secondes.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'S\'inscrire', diff --git a/lang/fr/entities.php b/lang/fr/entities.php index fae7480b1..fa0808912 100644 --- a/lang/fr/entities.php +++ b/lang/fr/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Afficher/masquer la barre latérale', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Étiquettes de la page', 'chapter_tags' => 'Étiquettes du chapitre', 'book_tags' => 'Étiquettes du livre', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 664184a7d..8ff81ba6b 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', 'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées', 'users_mfa_configure' => 'Méthode de configuration', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Créer un nouveau jeton API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovaque', 'sl' => 'Slovène', 'sv' => 'Suédois', + 'th' => 'ภาษาไทย', 'tr' => 'Turc', 'uk' => 'Ukrainien', 'uz' => 'O‘zbekcha', diff --git a/lang/he/activities.php b/lang/he/activities.php index e94d53e0d..52bf134d0 100644 --- a/lang/he/activities.php +++ b/lang/he/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'משתמש עודכן בהצלחה', 'user_delete' => 'משתמש נמחק', 'user_delete_notification' => 'משתמש הוסר בהצלחה', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API Token נוצר', diff --git a/lang/he/auth.php b/lang/he/auth.php index 6f4f77223..6d3f04a61 100644 --- a/lang/he/auth.php +++ b/lang/he/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'פרטי ההתחברות אינם תואמים את הנתונים שלנו.', 'throttle' => 'נסיונות התחברות מהירים מדי, יש להמתין :seconds שניות ולנסות שנית.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'הרשמה למערכת', diff --git a/lang/he/entities.php b/lang/he/entities.php index 38ffc95cf..12f92e1f0 100644 --- a/lang/he/entities.php +++ b/lang/he/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'תגיות דף', 'chapter_tags' => 'תגיות פרק', 'book_tags' => 'תגיות ספר', diff --git a/lang/he/settings.php b/lang/he/settings.php index b8f204813..e81676681 100644 --- a/lang/he/settings.php +++ b/lang/he/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'צור אסימון API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/hr/activities.php b/lang/hr/activities.php index bf79f9ab1..06fec08d2 100644 --- a/lang/hr/activities.php +++ b/lang/hr/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Korisnik je uspješno ažuriran', 'user_delete' => 'izbrisani korisnik', 'user_delete_notification' => 'Korisnik je uspješno uklonjen', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/hr/auth.php b/lang/hr/auth.php index bef07edcd..fcc81d5b0 100644 --- a/lang/hr/auth.php +++ b/lang/hr/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Ove vjerodajnice ne podudaraju se s našim zapisima.', 'throttle' => 'Previše pokušaja prijave. Molimo vas da pokušate za :seconds sekundi.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrirajte se', diff --git a/lang/hr/entities.php b/lang/hr/entities.php index ff51c1b2f..320f85ecc 100644 --- a/lang/hr/entities.php +++ b/lang/hr/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Oznake stranice', 'chapter_tags' => 'Oznake poglavlja', 'book_tags' => 'Oznake knjiga', diff --git a/lang/hr/settings.php b/lang/hr/settings.php index 60b2485ef..a595b5b16 100644 --- a/lang/hr/settings.php +++ b/lang/hr/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Postavite višestruku provjeru autentičnosti kao dodatni sloj sigurnosti za svoj korisnički račun.', 'users_mfa_x_methods' => ':count metoda konfigurirano|:count metode konfigurirane', 'users_mfa_configure' => 'Konfiguriraj Metode', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Stvori API token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/hu/activities.php b/lang/hu/activities.php index 68184e597..a64af5a18 100644 --- a/lang/hu/activities.php +++ b/lang/hu/activities.php @@ -67,9 +67,9 @@ return [ 'auth_password_reset_request' => 'jelszó visszaállítást kért', 'auth_password_reset_update' => 'felhasználói jelszó visszaállítás', 'mfa_setup_method' => 'MFA módszert állított be', - 'mfa_setup_method_notification' => 'Többfaktoros azonosítás sikeresen beállítva', + 'mfa_setup_method_notification' => 'Többlépcsős azonosítás sikeresen beállítva', 'mfa_remove_method' => 'MFA módszert törölt', - 'mfa_remove_method_notification' => 'Többfaktoros azonosítás sikeresen törölve', + 'mfa_remove_method_notification' => 'Többlépcsős azonosítás sikeresen törölve', // Settings 'settings_update' => 'frissítette a beállításokat', @@ -86,9 +86,9 @@ return [ // Imports 'import_create' => 'import elkészült', - 'import_create_notification' => 'Az import sikeresen feltöltötve', + 'import_create_notification' => 'Az import sikeresen feltöltve', 'import_run' => 'import frissítve', - 'import_run_notification' => 'A tartalmat sikeresen importáltam.', + 'import_run_notification' => 'A tartalom sikeresen importálva', 'import_delete' => 'import törölve', 'import_delete_notification' => 'Az import sikeresen törölve', @@ -99,14 +99,16 @@ return [ 'user_update_notification' => 'Felhasználó sikeresen frissítve', 'user_delete' => 'felhasználót törölt', 'user_delete_notification' => 'Felhasználó sikeresen eltávolítva', + 'user_mfa_reset' => 'MFA alaphelyzetbe állítva felhasználónak', + 'user_mfa_reset_notification' => 'Többlépcsős azonosítási módok alaphelyzetbe állítva', // API Tokens - 'api_token_create' => 'létrehozta az API tokent', - 'api_token_create_notification' => 'API token sikeresen létrehozva', - 'api_token_update' => 'frissítette az API tokent', - 'api_token_update_notification' => 'API token sikeresen frissítve', - 'api_token_delete' => 'törölte az API tokent', - 'api_token_delete_notification' => 'API token sikeresen törölve', + 'api_token_create' => 'létrehozta az API kulcsot', + 'api_token_create_notification' => 'API kulcs sikeresen létrehozva', + 'api_token_update' => 'frissítette az API kulcsot', + 'api_token_update_notification' => 'API kulcs sikeresen frissítve', + 'api_token_delete' => 'törölte az API kulcsot', + 'api_token_delete_notification' => 'API kulcs sikeresen törölve', // Roles 'role_create' => 'szerepkört hozott létre', @@ -118,14 +120,14 @@ return [ // Recycle Bin 'recycle_bin_empty' => 'kiürítette a lomtárat', - 'recycle_bin_restore' => 'lomtárból visszaállítva', - 'recycle_bin_destroy' => 'lomtárból törölve', + 'recycle_bin_restore' => 'visszaállított a lomtárból', + 'recycle_bin_destroy' => 'törölte a lomtárból', // Comments - 'commented_on' => 'megjegyzést fűzött hozzá:', - 'comment_create' => 'hozzáadott hozzászólás', - 'comment_update' => 'frissített hozzászólás', - 'comment_delete' => 'megjegyzés törlése', + 'commented_on' => 'megjegyzést fűzött hozzá', + 'comment_create' => 'hozzáadott egy hozzászólást', + 'comment_update' => 'frissített egy hozzászólást', + 'comment_delete' => 'törölt egy hozzászólást', // Sort Rules 'sort_rule_create' => 'létrehozta a rendezési szabályt', @@ -136,5 +138,5 @@ return [ 'sort_rule_delete_notification' => 'Rendezési szabály sikeresen törölve', // Other - 'permissions_update' => 'engedélyek frissítve', + 'permissions_update' => 'frissítette ez engedélyeket', ]; diff --git a/lang/hu/auth.php b/lang/hu/auth.php index 90669e2fb..4e3fef6b0 100644 --- a/lang/hu/auth.php +++ b/lang/hu/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Ezek a hitelesítő adatok nem egyeznek a rögzítettekkel.', 'throttle' => 'Túl sok bejelentkezési próbálkozás. :seconds múlva lehet újra megpróbálni.', + 'mfa_throttle' => 'Túl sok többlépcsős azonosítási próbálkozás. :seconds múlva lehet újra megpróbálni.', // Login & Register 'sign_up' => 'Regisztráció', @@ -33,85 +34,85 @@ return [ 'social_registration_text' => 'Regisztráció és bejelentkezés másik szolgáltatással.', 'register_thanks' => 'Köszönjük a regisztrációt!', - 'register_confirm' => 'Ellenőrizze a megadott e-mail címet, és kattintson a megerősítő gombra :appName eléréséhez.', + 'register_confirm' => 'Ellenőrizze a megadott email címet, és kattintson a megerősítő gombra :appName eléréséhez.', 'registrations_disabled' => 'A regisztráció jelenleg le van tiltva', 'registration_email_domain_invalid' => 'Ebből az email tartományról nem lehet hozzáférni ehhez az alkalmazáshoz', 'register_success' => 'Köszönjük a regisztrációt! A regisztráció és a bejelentkezés megtörtént.', // Login auto-initiation 'auto_init_starting' => 'Bejelentkezési kísérlet', - 'auto_init_starting_desc' => 'Kapcsolatba lépünk az azonosítási rendszereddel, hogy elkezdjük a bejelentkezési folyamatot. Ha 5 másodperc után sem történik előrelépés, próbálkozhatsz az alábbi linkre kattintva.', + 'auto_init_starting_desc' => 'Kapcsolatba lépünk az azonosítási rendszerrel, hogy elkezdjük a bejelentkezési folyamatot. Ha 5 másodperc után sem történik előrelépés, próbálkozhat az alábbi linkre kattintva.', 'auto_init_start_link' => 'Folytatás azonosítással', // Password Reset 'reset_password' => 'Jelszó visszaállítása', - 'reset_password_send_instructions' => 'Adja meg az e-mail címet, amire a jelszó-visszaállító linket küldjük.', + 'reset_password_send_instructions' => 'Adja meg az email címet, amire a jelszó-visszaállító linket küldjük.', 'reset_password_send_button' => 'Visszaállító hivatkozás elküldése', - 'reset_password_sent' => 'A jelszó-visszaállító linket e-mailben fogjuk elküldeni a(z) :email címre, ha beállításra került a rendszerben.', + 'reset_password_sent' => 'A jelszó-visszaállító linket emailben fogjuk elküldeni a(z) :email címre, ha található felhasználó ezzel a címmel a rendszerben.', 'reset_password_success' => 'A jelszó sikeresen visszaállítva.', 'email_reset_subject' => ':appName jelszó visszaállítása', - 'email_reset_text' => 'Ezt az e-mailt azért küldtük, mert egy jelszó-visszaállításra vonatkozó kérést kaptunk ebből a fiókból.', + 'email_reset_text' => 'Ezt az emailt azért küldtük, mert egy jelszó-visszaállításra vonatkozó kérést kaptunk ebből a fiókból.', 'email_reset_not_requested' => 'Ha nem Ön kérte a jelszó visszaállítását, akkor nincs szükség további intézkedésre.', // Email Confirmation 'email_confirm_subject' => ':appName alkalmazásban beállított email címet meg kell erősíteni', 'email_confirm_greeting' => ':appName köszöni a csatlakozást!', - 'email_confirm_text' => 'Az email címet a lenti gombra kattintva lehet megerősíteni:', + 'email_confirm_text' => 'Kérjük erősítse meg email címét a lenti gombra kattintva:', 'email_confirm_action' => 'Email megerősítése', - 'email_confirm_send_error' => 'Az e-mail megerősítés kötelező, de a rendszer nem tudta elküldeni az e-mailt. Keresse fel az adminisztrátort, és gondoskodjon róla, hogy az e-mail helyesen van beállítva.', - 'email_confirm_success' => 'Az Ön e-mail címe sikeresen meg lett erősítve, most már be tud jelentkezni az e-mail címe használatával.', - 'email_confirm_resent' => 'Megerősítő e-mail újraküldve. Ellenőrizze a bejövő üzeneteit!', + 'email_confirm_send_error' => 'Az email megerősítés kötelező, de a rendszer nem tudta elküldeni az emailt. Keresse fel az adminisztrátort, és gondoskodjon róla, hogy az email helyesen van beállítva.', + 'email_confirm_success' => 'Az Ön email címe sikeresen meg lett erősítve, most már be tud jelentkezni az email címe használatával.', + 'email_confirm_resent' => 'Megerősítő email újraküldve. Ellenőrizze a bejövő üzeneteit!', 'email_confirm_thanks' => 'Köszönjük a megerősítést!', - 'email_confirm_thanks_desc' => 'Kérjük, várjon egy pillanatot, amíg a megerősítést kezeljük. Ha nem kerül átirányításra 3 másodperc után, kattintson a lenti "Folytatás" linkre a továbbhaladáshoz.', + 'email_confirm_thanks_desc' => 'Kérjük, várjon egy pillanatot, amíg a megerősítést kezeljük. Ha nem kerül átirányításra 3 másodpercen belül, kattintson a lenti "Folytatás" linkre a továbbhaladáshoz.', 'email_not_confirmed' => 'Az email cím nincs megerősítve', - 'email_not_confirmed_text' => 'Az email cím még nincs megerősítve.', - 'email_not_confirmed_click_link' => 'Kattintson a regisztráció után nem sokkal elküldött e-mailben található hivatkozásra.', + 'email_not_confirmed_text' => 'Az Ön email címe még nincs megerősítve.', + 'email_not_confirmed_click_link' => 'Kattintson a regisztráció után nem sokkal elküldött emailben található hivatkozásra.', 'email_not_confirmed_resend' => 'Ha nem érkezik meg a megerősítő email, a lenti űrlap beküldésével újra lehet küldeni.', 'email_not_confirmed_resend_button' => 'Megerősítő email újraküldése', // User Invite - 'user_invite_email_subject' => 'Ez egy meghívó :appName weboldalhoz!', - 'user_invite_email_greeting' => 'Létre lett hozva egy fiók az :appName weboldalon.', - 'user_invite_email_text' => 'Jelszó beállításához és hozzáféréshez a lenti gombra kell kattintani:', + 'user_invite_email_subject' => 'Meghívták a(z) :appName weboldalra!', + 'user_invite_email_greeting' => 'Létre lett hozva egy fiók a(z) :appName weboldalon.', + 'user_invite_email_text' => 'Jelszó beállításához és hozzáféréshez kattintson a lenti gombra:', 'user_invite_email_action' => 'Fiók jelszó beállítása', 'user_invite_page_welcome' => ':appName üdvözöl!', - 'user_invite_page_text' => 'A fiók véglegesítéséhez és a hozzáféréshez be kell állítani egy jelszót ami :appName weboldalon lesz használva a bejelentkezéshez.', + 'user_invite_page_text' => 'A fiók véglegesítéséhez és a hozzáféréshez be kell állítania egy jelszót ami a(z) :appName weboldalon lesz használva a bejelentkezéshez.', 'user_invite_page_confirm_button' => 'Jelszó megerősítése', - 'user_invite_success_login' => 'Jelszó beállítva. Most már be tudsz jelentkezni a beállított jelszóval a következő rendszerbe: :appName!', + 'user_invite_success_login' => 'Jelszó beállítva. Most már be tud jelentkezni a beállított jelszóval a(z) :appName rendszerbe!', // Multi-factor Authentication 'mfa_setup' => 'Többlépcsős azonosítás beállítása', 'mfa_setup_desc' => 'Állítsa be a többlépcsős azonosítást egy extra biztonsági rétegként a felhasználói fiókjához.', 'mfa_setup_configured' => 'Már beállítva', 'mfa_setup_reconfigure' => 'Újrakonfigurálás', - 'mfa_setup_remove_confirmation' => 'Biztosan ki szeretné kapcsolni a többlépcsős azonosítást?', + 'mfa_setup_remove_confirmation' => 'Biztosan el szeretné távolítani ezt a többlépcsős azonosítási módot?', 'mfa_setup_action' => 'Beállítások', 'mfa_backup_codes_usage_limit_warning' => 'Kevesebb, mint 5 visszaállítási kódja maradt. Kérem, hogy generáljon új kódokat, hogy csökkentse a rendszerből való kizárásának esélyét.', 'mfa_option_totp_title' => 'Mobilalkalmazás', - 'mfa_option_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szükséged, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.', - 'mfa_option_backup_codes_title' => 'Visszaállítási kulcsok', + 'mfa_option_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szüksége, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.', + 'mfa_option_backup_codes_title' => 'Visszaállítási kódok', 'mfa_option_backup_codes_desc' => 'Egyszer használatos biztonsági kódokat hoz létre, amelyeket bejelentkezéskor kell megadnia személyazonosságának igazolására. Ügyeljen arra, hogy ezeket biztonságos helyen tárolja.', 'mfa_gen_confirm_and_enable' => 'Jóváhagyás és engedélyezés', 'mfa_gen_backup_codes_title' => 'Visszaállítási kódok beállítása', - 'mfa_gen_backup_codes_desc' => 'Tárolja el egy biztonságos helyen az alábbi kódokat. Bejelentkezés során fel tudja használni őket másodlagos bejelentkezési kódként.', + 'mfa_gen_backup_codes_desc' => 'Tárolja el egy biztonságos helyen az alábbi kódokat! Bejelentkezés során fel tudja használni őket másodlagos bejelentkezési kódként.', 'mfa_gen_backup_codes_download' => 'Kódok letöltése', - 'mfa_gen_backup_codes_usage_warning' => 'A kódok egyszerhasználatosak', + 'mfa_gen_backup_codes_usage_warning' => 'A kódok egyszer használatosak', 'mfa_gen_totp_title' => 'Mobilalkalmazás beállítása', - 'mfa_gen_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szükséged, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Szkenneld be az alábbi QR-kódot az általad használt azonosító alkalmazásoddal, hogy használhasd az alkalmazást.', + 'mfa_gen_totp_desc' => 'A többlépcsős azonosításhoz olyan mobilalkalmazásra lesz szüksége, amely támogatja a TOTP-t, például a Google Authenticator, az Authy vagy a Microsoft Authenticator.', + 'mfa_gen_totp_scan' => 'Olvassa be az alábbi QR-kódot az Ön által használt azonosító alkalmazással, hogy használhassa az alkalmazást.', 'mfa_gen_totp_verify_setup' => 'Beállítások ellenőrzése', - 'mfa_gen_totp_verify_setup_desc' => 'Ellenőrizd, hogy minden működik, azzal hogy beírod a kapott kódot amit az authentikátor alkalmazás generált az alábbi beviteli mezőbe:', - 'mfa_gen_totp_provide_code_here' => 'Add meg az alkalmazás által generált kódot ide', + 'mfa_gen_totp_verify_setup_desc' => 'Ellenőrizze, hogy minden működik azzal, hogy beírja az azonosító alkalmazás által generált kódot az alábbi mezőbe:', + 'mfa_gen_totp_provide_code_here' => 'Adja meg az alkalmazás által generált kódot ide', 'mfa_verify_access' => 'Hozzáférés ellenőrzése', - 'mfa_verify_access_desc' => 'Felhasználói fiókja megköveteli, hogy erősítse meg személyazonosságát egy további ellenőrzési szinttel, mielőtt hozzáférést kapna. A folytatáshoz használja az egyik konfigurált módszert.', - 'mfa_verify_no_methods' => 'Nincs konfigurálva MFA', - 'mfa_verify_no_methods_desc' => 'Nem található többlépcsős hitelesítési módszer a fiókjához. A hozzáféréshez legalább egy módszert be kell állítania.', + 'mfa_verify_access_desc' => 'Felhasználói fiókja megköveteli, hogy erősítse meg személyazonosságát egy további ellenőrzési szinttel, mielőtt hozzáférést kapna. A folytatáshoz használja az egyik beállított módot.', + 'mfa_verify_no_methods' => 'Nincsen beállítva mód', + 'mfa_verify_no_methods_desc' => 'Nem található többlépcsős hitelesítési mód a fiókjához. A hozzáféréshez legalább egy módot be kell állítania.', 'mfa_verify_use_totp' => 'Ellenőrzés mobil alkalmazás használatával', 'mfa_verify_use_backup_codes' => 'Ellenőrzés visszaállítási kóddal', 'mfa_verify_backup_code' => 'Visszaállítási kód', 'mfa_verify_backup_code_desc' => 'Adjon meg egy még fel nem használt visszaállítási kódot:', - 'mfa_verify_backup_code_enter_here' => 'Írd be a tartalék kódot', + 'mfa_verify_backup_code_enter_here' => 'Írja be a visszaállítási kódot', 'mfa_verify_totp_desc' => 'Írja be alább a mobilalkalmazásával generált kódot:', - 'mfa_setup_login_notification' => 'Többfaktoros hitelesítés konfigurálva. Kérjük, most jelentkezzen be újra a konfigurált módszerrel.', + 'mfa_setup_login_notification' => 'Többlépcsős hitelesítés beállítva. Kérjük, most jelentkezzen be újra a beállított módszerrel.', ]; diff --git a/lang/hu/common.php b/lang/hu/common.php index d25a76528..080446892 100644 --- a/lang/hu/common.php +++ b/lang/hu/common.php @@ -20,7 +20,7 @@ return [ 'description' => 'Leírás', 'role' => 'Szerepkör', 'cover_image' => 'Borítókép', - 'cover_image_description' => 'Ennek a képnek körülbelül 440 x 250 képpont méretűnek kell lennie, bár rugalmasan méretezhető és levágható, hogy a felhasználói felülethez illeszkedjen a különböző lehetőségek esetén, így a megjelenítés tényleges méretei eltérőek lesznek.', + 'cover_image_description' => 'Ennek a képnek körülbelül 440 x 250 pixel méretűnek kell lennie, bár rugalmasan méretezhető és levágható, hogy a felhasználói felülethez illeszkedjen a különböző alkalmazások esetén, így a megjelenítés tényleges méretei eltérőek lesznek.', // Actions 'actions' => 'Műveletek', @@ -53,7 +53,7 @@ return [ 'filter_active' => 'Aktív szűrő:', 'filter_clear' => 'Szűrő törlése', 'download' => 'Letöltés', - 'open_in_tab' => 'Megnyitás új tab-on', + 'open_in_tab' => 'Megnyitás új fülön', 'open' => 'Megnyitás', // Sort Options @@ -70,7 +70,7 @@ return [ 'deleted_user' => 'Törölt felhasználó', 'no_activity' => 'Nincs megjeleníthető aktivitás', 'no_items' => 'Nincsenek elérhető elemek', - 'back_to_top' => 'Oldal eleje', + 'back_to_top' => 'Oldal tetejére', 'skip_to_main_content' => 'Ugrás a fő tartalomra', 'toggle_details' => 'Részletek átkapcsolása', 'toggle_thumbnails' => 'Bélyegképek átkapcsolása', @@ -97,12 +97,12 @@ return [ // Layout tabs 'tab_info' => 'Információ', - 'tab_info_label' => 'Tab: Másodlagos információk megjelenítése', + 'tab_info_label' => 'Fül: Másodlagos információk megjelenítése', 'tab_content' => 'Tartalom', - 'tab_content_label' => 'Tab: Elsődleges információk megjelenítése', + 'tab_content_label' => 'Fül: Elsődleges információk megjelenítése', // Email Content - 'email_action_help' => 'Probléma esetén a lenti ":actionText" gombra kell kattintani, majd ki kell másolni a lenti webcímet és be kell illeszteni egy böngészőbe:', + 'email_action_help' => 'Ha problémája van a(z) ":actionText" gombra kattintással, akkor másolja ki az URL-t, és illessze be a böngészőbe:', 'email_rights' => 'Minden jog fenntartva', // Footer Link Options diff --git a/lang/hu/components.php b/lang/hu/components.php index 6a54dd1b0..0b111df38 100644 --- a/lang/hu/components.php +++ b/lang/hu/components.php @@ -9,8 +9,8 @@ return [ 'image_list' => 'Képek listája', 'image_details' => 'A kép részletei', 'image_upload' => 'Kép feltöltése', - 'image_intro' => 'Itt választhatsz ki és kezelhetsz olyan képeket, amelyeket korábban feltöltöttek a rendszerbe.', - 'image_intro_upload' => 'Húzz ide egy új képfájlt az új kép feltöltéséhez, vagy használd a fenti "Kép feltöltése" gombot.', + 'image_intro' => 'Itt kiválaszthatja és kezelheti a rendszerbe korábban feltöltött képeket.', + 'image_intro_upload' => 'Húzzon ide egy új képfájlt az új kép feltöltéséhez, vagy használja a fenti "Kép feltöltése" gombot.', 'image_all' => 'Összes', 'image_all_title' => 'Összes kép megtekintése', 'image_book_title' => 'A könyvhöz feltöltött képek megtekintése', @@ -22,8 +22,8 @@ return [ 'image_updated' => 'Frissítve ekkor: :updateDate', 'image_load_more' => 'Több betöltése', 'image_image_name' => 'Kép neve', - 'image_delete_used' => 'Ez a kép a lenti oldalakon van használatban.', - 'image_delete_confirm_text' => 'Biztosan törölhető ez a kép?', + 'image_delete_used' => 'Ez a kép az alábbi oldalakon van használatban.', + 'image_delete_confirm_text' => 'Biztosan törli ezt a képet?', 'image_select_image' => 'Kép kiválasztása', 'image_dropzone' => 'Képek feltöltése ejtéssel vagy kattintással', 'image_dropzone_drop' => 'Húzza a képeket ide a feltöltéshez', @@ -34,7 +34,7 @@ return [ 'image_delete_success' => 'Kép sikeresen törölve', 'image_replace' => 'Kép cseréje', 'image_replace_success' => 'Képfájl sikeresen frissítve', - 'image_rebuild_thumbs' => 'Méret variációk újragenerálása', + 'image_rebuild_thumbs' => 'Méret változatok újragenerálása', 'image_rebuild_thumbs_success' => 'Kép méret változatok sikeresen újra lettek generálva!', // Code Editor diff --git a/lang/hu/editor.php b/lang/hu/editor.php index 705bc6d54..885e6951f 100644 --- a/lang/hu/editor.php +++ b/lang/hu/editor.php @@ -13,7 +13,7 @@ return [ 'cancel' => 'Mégsem', 'save' => 'Mentés', 'close' => 'Bezárás', - 'apply' => 'Apply', + 'apply' => 'Alkalmaz', 'undo' => 'Visszavonás', 'redo' => 'Újra', 'left' => 'Balra', @@ -48,7 +48,7 @@ return [ 'superscript' => 'Felső index', 'subscript' => 'Alsó index', 'text_color' => 'Szöveg szín', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Kiemelő szín', 'custom_color' => 'Egyéni szín', 'remove_color' => 'Szín eltávolítása', 'background_color' => 'Háttérszín', @@ -65,14 +65,14 @@ return [ 'insert_image' => 'Kép beszúrása', 'insert_image_title' => 'Kép beszúrása/szerkesztése', 'insert_link' => 'Hivatkozás beszúrása/szerkesztése', - 'insert_link_title' => 'Hivatkozás Beszúrása/Szerkesztése', + 'insert_link_title' => 'Hivatkozás beszúrása/szerkesztése', 'insert_horizontal_line' => 'Vízszintes vonal beszúrása', 'insert_code_block' => 'Kódrészlet beszúrása', - 'edit_code_block' => 'Kódrészlet beszúrása', + 'edit_code_block' => 'Kódrészlet szerkesztése', 'insert_drawing' => 'Rajz beszúrása/szerkesztése', 'drawing_manager' => 'Rajzkezelő', - 'insert_media' => 'Media beszúrása/szerkesztése', - 'insert_media_title' => 'Media Beszúrása/Szerkesztése', + 'insert_media' => 'Média beszúrása/szerkesztése', + 'insert_media_title' => 'Média beszúrása/szerkesztése', 'clear_formatting' => 'Formázás törlése', 'source_code' => 'Forráskód', 'source_code_title' => 'Forráskód', @@ -81,11 +81,11 @@ return [ // Tables 'table_properties' => 'Táblázat tulajdonságai', - 'table_properties_title' => 'Táblázat Tulajdonságai', + 'table_properties_title' => 'Táblázat tulajdonságai', 'delete_table' => 'Táblázat törlése', - 'table_clear_formatting' => 'Tábla formázás törlése', + 'table_clear_formatting' => 'Táblázat formázás törlése', 'resize_to_contents' => 'Átméretezés a tartalomhoz', - 'row_header' => 'Sorfejléc', + 'row_header' => 'Fejléc sor', 'insert_row_before' => 'Sor beszúrása elé', 'insert_row_after' => 'Sor beszúrása mögé', 'delete_row' => 'Sor törlése', @@ -96,7 +96,7 @@ return [ 'table_row' => 'Sor', 'table_column' => 'Oszlop', 'cell_properties' => 'Cella tulajdonságai', - 'cell_properties_title' => 'Cella Tulajdonságai', + 'cell_properties_title' => 'Cella tulajdonságai', 'cell_type' => 'Cella típusa', 'cell_type_cell' => 'Cella', 'cell_scope' => 'Hatáskör', @@ -111,7 +111,7 @@ return [ 'border_style' => 'Szegély stílusa', 'border_color' => 'Szegély színe', 'row_properties' => 'Sor tulajdonságai', - 'row_properties_title' => 'Sor Tulajdonságai', + 'row_properties_title' => 'Sor tulajdonságai', 'cut_row' => 'Sor kivágása', 'copy_row' => 'Sor másolása', 'paste_row_before' => 'Sor beillesztése elé', @@ -125,10 +125,10 @@ return [ 'copy_column' => 'Oszlop másolása', 'paste_column_before' => 'Oszlop beszúrása elé', 'paste_column_after' => 'Oszlop beszúrása utána', - 'cell_padding' => 'Cellatávolság', + 'cell_padding' => 'Cellamargó', 'cell_spacing' => 'Cellatávolság', 'caption' => 'Felirat', - 'show_caption' => 'Képaláírás mutatása', + 'show_caption' => 'Felirat megjelenítése', 'constrain' => 'Arányok megőrzése', 'cell_border_solid' => 'Folyamatos', 'cell_border_dotted' => 'Pontozott', @@ -145,33 +145,33 @@ return [ 'source' => 'Forrás', 'alt_desc' => 'Alternatív leírás', 'embed' => 'Beágyazás', - 'paste_embed' => 'Illeszd be a beágyazási kódot ide:', + 'paste_embed' => 'Illessze be a beágyazási kódot ide:', 'url' => 'URL', - 'text_to_display' => 'Megjelenő szöveg', + 'text_to_display' => 'Megjelenítendő szöveg', 'title' => 'Cím', - 'browse_links' => 'Browse links', + 'browse_links' => 'Hivatkozások tallózása', 'open_link' => 'Hivatkozás megnyitása', 'open_link_in' => 'Hivatkozás megnyitása...', 'open_link_current' => 'Aktuális ablak', 'open_link_new' => 'Új ablak', 'remove_link' => 'Hivatkozás eltávolítása', - 'insert_collapsible' => 'Illeszd be az összecsukható blokkot', - 'collapsible_unwrap' => 'Kicsomagol', + 'insert_collapsible' => 'Összecsukható blokk beszúrása', + 'collapsible_unwrap' => 'Eltávolítás', 'edit_label' => 'Címke szerkesztése', - 'toggle_open_closed' => 'Nyitott/zárt váltása', + 'toggle_open_closed' => 'Nyitott/zárt állapot váltása', 'collapsible_edit' => 'Összecsukható blokk szerkesztése', - 'toggle_label' => 'Címke ki-be kapcsolása', + 'toggle_label' => 'Címke ki/be kapcsolása', // About view - 'about' => 'A szerkesztőről', + 'about' => 'A szerkesztő névjegye', 'about_title' => 'A WYSIWYG szerkesztőről', - 'editor_license' => 'Szerkesztő Licensz és Copyright információi', - 'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.', - 'editor_lexical_license_link' => 'Full license details can be found here.', + 'editor_license' => 'Szerkesztő licenc és jogi információi', + 'editor_lexical_license' => 'Ez a szerkesztő az MIT licenc alatt szolgáltatott :lexicalLink segítségével készült.', + 'editor_lexical_license_link' => 'A teljes licenc az itt található.', 'editor_tiny_license' => 'Ez a szerkesztő az MIT licenc alatt szolgáltatott :tinyLink segítségével készült.', 'editor_tiny_license_link' => 'A TinyMCE szerzői jogi és licencinformációi itt találhatók.', - 'save_continue' => 'Mentés és Folytatás', - 'callouts_cycle' => '(Folyamatos lenyomva tartással válassza ki a típusok közötti váltást)', + 'save_continue' => 'Oldal mentése és folytatás', + 'callouts_cycle' => '(Ismételt lenyomással váltogathat a típusok között)', 'link_selector' => 'Tartalom hivatkozása', 'shortcuts' => 'Gyorsbillentyűk', 'shortcut' => 'Gyorsbillentyű', diff --git a/lang/hu/entities.php b/lang/hu/entities.php index 707de62d8..b499a9b6b 100644 --- a/lang/hu/entities.php +++ b/lang/hu/entities.php @@ -44,45 +44,45 @@ return [ 'default_template_explain' => 'Rendeljen hozzá egy oldalsablont, amely alapértelmezett tartalomként lesz használva az ezen az elemen belül létrehozott összes oldalon. Ne feledje, hogy ezt csak akkor használja, ha az oldal készítője megtekintési hozzáféréssel rendelkezik a kiválasztott sablonoldalhoz.', 'default_template_select' => 'Válasszon ki egy oldalsablont', 'import' => 'Import', - 'import_validate' => 'Validate Import', - 'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.', - 'import_zip_select' => 'Select ZIP file to upload', - 'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:', - 'import_pending' => 'Pending Imports', - 'import_pending_none' => 'No imports have been started.', - 'import_continue' => 'Continue Import', - 'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.', - 'import_details' => 'Import Details', - 'import_run' => 'Run Import', - 'import_size' => ':size Import ZIP Size', - 'import_uploaded_at' => 'Uploaded :relativeTime', - 'import_uploaded_by' => 'Uploaded by', - 'import_location' => 'Import Location', - 'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.', - 'import_delete_confirm' => 'Are you sure you want to delete this import?', - 'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.', - 'import_errors' => 'Import Errors', - 'import_errors_desc' => 'The follow errors occurred during the import attempt:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'import_validate' => 'Importált adatok ellenőrzése', + 'import_desc' => 'Importáljon könyveket, fejezeteket és oldalakat egy azonos, vagy egy másik alkalmazásból származó ZIP állományból. A fájl feltöltése és ellenőrzése után beállíthatja és elindíthatja az importálást a következő oldalon.', + 'import_zip_select' => 'Feltöltendő ZIP fájl kiválasztása', + 'import_zip_validation_errors' => 'A megadott ZIP fájlban hibák találhatók:', + 'import_pending' => 'Függő importálások', + 'import_pending_none' => 'Nincs elindított importálás.', + 'import_continue' => 'Importálás folytatása', + 'import_continue_desc' => 'Importálás előtt nézze át a ZIP fájlból betöltendő tartalmakat. Amikor készen áll, futtassa le az importálást, hogy hozzáadja az állományokat a rendszerhez. A feltöltött ZIP fájl az importálás után automatikusan törlésre kerül.', + 'import_details' => 'Importálás részletei', + 'import_run' => 'Importálás', + 'import_size' => ':size méretű ZIP', + 'import_uploaded_at' => 'Feltöltve:', + 'import_uploaded_by' => 'Feltöltötte:', + 'import_location' => 'Importálás helye', + 'import_location_desc' => 'Válasszon ki egy helyet az importált tartalomnak. Létrehozási jogosultsággal kell rendelkezni a kiválasztott helyen.', + 'import_delete_confirm' => 'Biztosan törölni akarja ezt az importálást?', + 'import_delete_desc' => 'Ez véglegesen el fogja távolítani a feltöltött ZIP fájlt.', + 'import_errors' => 'Importálási hibák', + 'import_errors_desc' => 'A következő hibák jelentkeztek az importálás során:', + 'breadcrumb_siblings_for_page' => 'Navigáció az oldal szomszédaihoz', + 'breadcrumb_siblings_for_chapter' => 'Navigáció a fejezet szomszédaihoz', + 'breadcrumb_siblings_for_book' => 'Navigáció a könyv szomszédaihoz', + 'breadcrumb_siblings_for_bookshelf' => 'Navigáció a polc szomszédaihoz', // Permissions and restrictions 'permissions' => 'Jogosultságok', 'permissions_desc' => 'Itt állítsa be az engedélyeket a felhasználói szerepkörök által biztosított alapértelmezett engedélyek felülbírálásához.', - 'permissions_book_cascade' => 'A könyvekre beállított engedélyek automatikusan az alárendelt fejezetekhez és oldalakhoz kapcsolódnak, kivéve, ha saját engedélyekkel rendelkeznek.', - 'permissions_chapter_cascade' => 'A fejezetekre beállított engedélyek automatikusan az alárendelt oldalakra lépnek át, hacsak nem rendelkeznek saját engedélyekkel.', + 'permissions_book_cascade' => 'A könyvekre beállított engedélyek automatikusan az alárendelt fejezetekre és oldalakra is érvényesek, kivéve, ha azok saját engedély beállításokkal rendelkeznek.', + 'permissions_chapter_cascade' => 'A fejezetekre beállított engedélyek automatikusan az alárendelt oldalakra is érvényesek, kivéve, ha azok saját engedély beállításokkal rendelkeznek.', 'permissions_save' => 'Jogosultságok mentése', 'permissions_owner' => 'Tulajdonos', 'permissions_role_everyone_else' => 'Mindenki más', - 'permissions_role_everyone_else_desc' => 'Állítson be engedélyeket az összes, kifejezetten nem felülírt szerepkörhöz.', + 'permissions_role_everyone_else_desc' => 'Állítson be engedélyeket az összes nem felülírt szerepkörhöz.', 'permissions_role_override' => 'A szerepkör engedélyeinek felülbírálása', - 'permissions_inherit_defaults' => 'Alapértelmezett értékek öröklése', + 'permissions_inherit_defaults' => 'Alapértelmezett engedélyek öröklése', // Search 'search_results' => 'Keresési eredmények', - 'search_total_results_found' => ':count találat|összesen :count találat', + 'search_total_results_found' => ':count találat|Összesen :count találat', 'search_clear' => 'Keresés törlése', 'search_no_pages' => 'Nincsenek a keresésnek megfelelő oldalak', 'search_for_term' => ':term keresése', @@ -110,7 +110,7 @@ return [ // Shelves 'shelf' => 'Polc', 'shelves' => 'Polcok', - 'x_shelves' => ':count polc|:count polcok', + 'x_shelves' => ':count polc|:count polc', 'shelves_empty' => 'Nincsenek könyvespolcok létrehozva', 'shelves_create' => 'Új polc létrehozása', 'shelves_popular' => 'Népszerű polcok', @@ -121,24 +121,24 @@ return [ 'shelves_save' => 'Polc mentése', 'shelves_books' => 'Könyvek ezen a polcon', 'shelves_add_books' => 'Könyvek hozzáadása ehhez a polchoz', - 'shelves_drag_books' => 'Könyveket áthúzással lehet elhelyezni ezen a polcon', + 'shelves_drag_books' => 'Húzzon ide könyveket a polchoz hozzáadáshoz', 'shelves_empty_contents' => 'Ehhez a polchoz nincsenek könyvek rendelve', 'shelves_edit_and_assign' => 'Polc szerkesztése könyvek hozzárendeléséhez', 'shelves_edit_named' => ':name polc szerkesztése', 'shelves_edit' => 'Polc szerkesztése', 'shelves_delete' => 'Polc törlése', 'shelves_delete_named' => ':name polc törlése', - 'shelves_delete_explain' => "':name'. nevű polc ezzel le lesz törölve. A benne található könyvek nem lesznek törölve.", - 'shelves_delete_confirmation' => 'Biztosan törölhető ez a polc?', - 'shelves_permissions' => 'Polc jogosultság', + 'shelves_delete_explain' => "Ez törölni fogja a(z) ':name' nevű polcot. A benne található könyvek nem lesznek törölve.", + 'shelves_delete_confirmation' => 'Biztosan törli ezt a polcot?', + 'shelves_permissions' => 'Polc jogosultságok', 'shelves_permissions_updated' => 'Polc jogosultságok frissítve', 'shelves_permissions_active' => 'Polc jogosultságok aktívak', - 'shelves_permissions_cascade_warning' => 'A polcokhoz kapcsolódó jogosultságok nem kapcsolódnak automatikusan a tárolt könyvekhez. Ennek az az oka, hogy egy könyv több polcon is létezhet. Az engedélyek azonban lemásolhatók a gyermekkönyvekbe az alábbi lehetőség segítségével.', - 'shelves_permissions_create' => 'A polclétrehozási jogosultságok csak az alárendelt könyvekbe való másoláshoz használhatók az alábbi művelettel. Nem szabályozzák a könyvek létrehozásának lehetőségét.', - 'shelves_copy_permissions_to_books' => 'Jogosultság másolása könyvekre', - 'shelves_copy_permissions' => 'Jogosultság másolása', - 'shelves_copy_permissions_explain' => 'Ezzel a polc jelenlegi engedélybeállításait alkalmazza a benne található összes könyvre. Az aktiválás előtt győződjön meg arról, hogy a polc engedélyeinek módosításait elmentette.', - 'shelves_copy_permission_success' => 'Könyvespolc jogosultságok átmásolva :count könyvre', + 'shelves_permissions_cascade_warning' => 'A polcokhoz kapcsolódó jogosultságok nem vonatkoznak automatikusan a hozzájuk rendelt könyvekre. Ennek az az oka, hogy egy könyv több polchoz is tartozhat. Az engedélyek azonban lemásolhatók a könyvekre az alábbi opcióval.', + 'shelves_permissions_create' => 'A polc létrehozási jogosultságok csak a jogosultságok a hozzárendelt könyvekre másolásához vannak használva. Nem szabályozzák a könyvek létrehozásának lehetőségét.', + 'shelves_copy_permissions_to_books' => 'Jogosultságok másolása könyvekre', + 'shelves_copy_permissions' => 'Jogosultságok másolása', + 'shelves_copy_permissions_explain' => 'Ezzel alkalmazza a polc jelenlegi engedély beállításait a benne található összes könyvre. Az aktiválás előtt győződjön meg arról, hogy a polc engedélyeinek módosításait elmentette.', + 'shelves_copy_permission_success' => 'Polc jogosultságok átmásolva :count könyvre', // Books 'book' => 'Könyv', @@ -154,8 +154,8 @@ return [ 'books_create' => 'Új könyv létrehozása', 'books_delete' => 'Könyv törlése', 'books_delete_named' => ':bookName könyv törlése', - 'books_delete_explain' => '\':bookName\' nevű könyv törölve lesz. Minden oldal és fejezet el lesz távolítva.', - 'books_delete_confirmation' => 'Biztosan törölhető ez a könyv?', + 'books_delete_explain' => 'Ez törölni fogja a(z) \':bookName\' nevű könyvet. Minden oldal és fejezet el lesz távolítva.', + 'books_delete_confirmation' => 'Biztosan törli ezt a könyvet?', 'books_edit' => 'Könyv szerkesztése', 'books_edit_named' => ':bookName könyv szerkesztése', 'books_form_book_name' => 'Könyv neve', @@ -170,10 +170,10 @@ return [ 'books_search_this' => 'Keresés ebben a könyvben', 'books_navigation' => 'Könyv navigáció', 'books_sort' => 'Könyv tartalmak rendezése', - 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', - 'books_sort_auto_sort' => 'Auto Sort Option', - 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_desc' => 'Rendezze át egy könyv tartalmát a fejezetek és oldalak mozgatásával. Más könyvek is hozzáadhatók, így könnyű az átmozgatás könyvek között is. Opcionálisan egy rendezési szabály is megadható, hogy minden változtatáskor automatikusan rendezze a könyv tartalmát.', + 'books_sort_auto_sort' => 'Automatikus rendezés opció', + 'books_sort_auto_sort_active' => 'Aktív automatikus rendezés: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Automatikus rendezési szabályok a "Listák és rendezés" beállításoknál hozhatók létre egy megfelelő jogosultságokkal rendelkező felhasználó által.', 'books_sort_named' => ':bookName könyv rendezése', 'books_sort_name' => 'Rendezés név szerint', 'books_sort_created' => 'Rendezés létrehozás dátuma szerint', @@ -181,7 +181,7 @@ return [ 'books_sort_chapters_first' => 'Fejezetek elől', 'books_sort_chapters_last' => 'Fejezetek hátul', 'books_sort_show_other' => 'Egyéb könyvek mutatása', - 'books_sort_save' => 'Új elrendezés mentése', + 'books_sort_save' => 'Új sorrend mentése', 'books_sort_show_other_desc' => 'Adjon hozzá más könyveket, hogy bevonja őket a rendezési műveletbe, és lehetővé tegye a könyvek közötti egyszerű átszervezést.', 'books_sort_move_up' => 'Mozgatás fel', 'books_sort_move_down' => 'Mozgatás le', @@ -191,7 +191,7 @@ return [ 'books_sort_move_next_chapter' => 'Mozgatás a következő fejezetbe', 'books_sort_move_book_start' => 'Mozgatás a könyv elejére', 'books_sort_move_book_end' => 'Mozgatás a könyv végére', - 'books_sort_move_before_chapter' => 'Morgazás a fejezet elé', + 'books_sort_move_before_chapter' => 'Mozgatás a fejezet elé', 'books_sort_move_after_chapter' => 'Mozgatás a fejezet után', 'books_copy' => 'Könyv másolása', 'books_copy_success' => 'Könyv sikeresen lemásolva', @@ -199,14 +199,14 @@ return [ // Chapters 'chapter' => 'Fejezet', 'chapters' => 'Fejezetek', - 'x_chapters' => ':count fejezet|:count fejezetek', + 'x_chapters' => ':count fejezet|:count fejezet', 'chapters_popular' => 'Népszerű fejezetek', 'chapters_new' => 'Új fejezet', 'chapters_create' => 'Új fejezet létrehozása', 'chapters_delete' => 'Fejezet törlése', 'chapters_delete_named' => ':chapterName fejezet törlése', - 'chapters_delete_explain' => 'A(z) \':chapterName\' törlésére készül. A fejezethez tartozó minden oldal is törlésre fog kerülni.', - 'chapters_delete_confirm' => 'Biztosan törölhető ez a fejezet?', + 'chapters_delete_explain' => 'Ez törölni fogja a(z) \':chapterName\' fejezetet. A fejezethez tartozó minden oldal is törlésre fog kerülni.', + 'chapters_delete_confirm' => 'Biztosan törli ezt a fejezetet?', 'chapters_edit' => 'Fejezet szerkesztése', 'chapters_edit_named' => ':chapterName fejezet szerkesztése', 'chapters_save' => 'Fejezet mentése', @@ -235,32 +235,32 @@ return [ 'pages_delete_draft' => 'Vázlat oldal törlése', 'pages_delete_success' => 'Oldal törölve', 'pages_delete_draft_success' => 'Vázlat oldal törölve', - 'pages_delete_warning_template' => 'Ez az oldal aktívan használatban van könyv vagy fejezet alapértelmezett oldalsablonjaként. Ezekhez a könyvekhez vagy fejezetekhez a továbbiakban nem lesz alapértelmezett oldalsablon hozzárendelve az oldal törlése után.', - 'pages_delete_confirm' => 'Biztosan törölhető ez az oldal?', - 'pages_delete_draft_confirm' => 'Biztosan törölhető ez a vázlatoldal?', + 'pages_delete_warning_template' => 'Ez az oldal aktívan használatban van egy könyv vagy egy fejezet alapértelmezett oldalsablonjaként. Ezekhez a könyvekhez vagy fejezetekhez nem lesz alapértelmezett oldalsablon hozzárendelve az oldal törlése után.', + 'pages_delete_confirm' => 'Biztosan törli ezt az oldalt?', + 'pages_delete_draft_confirm' => 'Biztosan törli ezt az vázlat oldalt?', 'pages_editing_named' => ':pageName oldal szerkesztése', - 'pages_edit_draft_options' => 'Vázlatbeállítások', + 'pages_edit_draft_options' => 'Vázlat beállítások', 'pages_edit_save_draft' => 'Vázlat mentése', 'pages_edit_draft' => 'Oldal vázlat szerkesztése', 'pages_editing_draft' => 'Vázlat szerkesztése', 'pages_editing_page' => 'Oldal szerkesztése', - 'pages_edit_draft_save_at' => 'Vázlat elmentve:', + 'pages_edit_draft_save_at' => 'Vázlat elmentve: ', 'pages_edit_delete_draft' => 'Vázlat törlése', - 'pages_edit_delete_draft_confirm' => 'Biztos benne, hogy törölni kívánja az oldalmódosítások piszkozatát? Az utolsó teljes mentés óta végrehajtott összes módosítása elvész, és a szerkesztő frissül a legfrissebb, nem vázlatos mentési állapottal.', + 'pages_edit_delete_draft_confirm' => 'Biztos benne, hogy törli az oldalmódosítások vázlatát? Az utolsó teljes mentés óta végrehajtott összes módosítása elvész, és a szerkesztő frissül a legfrissebb, nem vázlatos mentési állapottal.', 'pages_edit_discard_draft' => 'Vázlat elvetése', 'pages_edit_switch_to_markdown' => 'Váltás Markdown szerkesztőre', - 'pages_edit_switch_to_markdown_clean' => '(Tisztított tartalom)', + 'pages_edit_switch_to_markdown_clean' => '(Tiszta tartalom)', 'pages_edit_switch_to_markdown_stable' => '(Stabil tartalom)', 'pages_edit_switch_to_wysiwyg' => 'Váltás a WYSIWYG szerkesztőre', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', + 'pages_edit_switch_to_new_wysiwyg' => 'Váltás az új WYSIWYG szerkeztőre', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(Béta tesztelés alatt)', 'pages_edit_set_changelog' => 'Változásnapló beállítása', 'pages_edit_enter_changelog_desc' => 'A végrehajtott módosítások rövid leírása', 'pages_edit_enter_changelog' => 'Változásnapló megadása', 'pages_editor_switch_title' => 'Szerkesztőváltás', 'pages_editor_switch_are_you_sure' => 'Biztosan módosítani szeretné ennek az oldalnak a szerkesztőjét?', - 'pages_editor_switch_consider_following' => 'A szerkesztők módosításakor vegye figyelembe a következőket:', - 'pages_editor_switch_consideration_a' => 'Mentés után az új szerkesztő opciót minden jövőbeli szerkesztő használni fogja, beleértve azokat is, amelyek esetleg nem tudják maguk módosítani a szerkesztő típusát.', + 'pages_editor_switch_consider_following' => 'A szerkesztők váltásakor vegye figyelembe a következőket:', + 'pages_editor_switch_consideration_a' => 'Mentés után az új szerkesztő opciót fogja minden jövőbeli szerkesztő használni, beleértve azokat is, akik esetleg nem tudják maguk módosítani a szerkesztő típusát.', 'pages_editor_switch_consideration_b' => 'Ez bizonyos körülmények között a részletek és a szintaxis elvesztéséhez vezethet.', 'pages_editor_switch_consideration_c' => 'A legutóbbi mentés óta végrehajtott címke- vagy változásnapló-módosítások nem maradnak fenn a módosítás során.', 'pages_save' => 'Oldal mentése', @@ -269,11 +269,11 @@ return [ 'pages_md_editor' => 'Szerkesztő', 'pages_md_preview' => 'Előnézet', 'pages_md_insert_image' => 'Kép beillesztése', - 'pages_md_insert_link' => 'Entitás hivatkozás beillesztése', + 'pages_md_insert_link' => 'Belső hivatkozás beillesztése', 'pages_md_insert_drawing' => 'Rajz beillesztése', 'pages_md_show_preview' => 'Előnézet megjelenítése', 'pages_md_sync_scroll' => 'Előnézet pozíció szinkronizálása', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Sima szöveg szerkesztő', 'pages_drawing_unsaved' => 'Nem mentett rajz található', 'pages_drawing_unsaved_confirm' => 'A rendszer nem mentett rajzadatokat talált egy korábbi sikertelen rajzmentési kísérletből. Szeretné visszaállítani és folytatni a nem mentett rajz szerkesztését?', 'pages_not_in_chapter' => 'Az oldal nincs fejezetben', @@ -285,7 +285,7 @@ return [ 'pages_permissions_success' => 'Oldal jogosultságok frissítve', 'pages_revision' => 'Változat', 'pages_revisions' => 'Oldal változatai', - 'pages_revisions_desc' => 'Az alábbiakban az oldal összes korábbi verziója látható. Visszatekinthet, összehasonlíthatja és visszaállíthatja a régi oldalverziókat, ha az engedélyek lehetővé teszik. Előfordulhat, hogy az oldal teljes előzménye itt nem jelenik meg teljes mértékben, mivel a rendszerkonfigurációtól függően a régi változatok automatikusan törlődnek.', + 'pages_revisions_desc' => 'Az alábbiakban az oldal összes korábbi verziója látható. Visszanézheti, összehasonlíthatja és visszaállíthatja a régi verziókat, ha az engedélyek lehetővé teszik. Előfordulhat, hogy az oldal teljes előzménye itt nem jelenik meg, mivel a beállításoktól függően a régi változatok automatikusan törlődhetnek.', 'pages_revisions_named' => ':pageName oldal változatai', 'pages_revision_named' => ':pageName oldal változata', 'pages_revision_restored_from' => 'Visszaállítva innen: #:id; :summary', @@ -293,8 +293,8 @@ return [ 'pages_revisions_date' => 'Változat dátuma', 'pages_revisions_number' => '#', 'pages_revisions_sort_number' => 'Változat száma', - 'pages_revisions_numbered' => 'Változat #:id', - 'pages_revisions_numbered_changes' => '#:id változat módosításai', + 'pages_revisions_numbered' => '#:id. változat', + 'pages_revisions_numbered_changes' => '#:id. változat módosításai', 'pages_revisions_editor' => 'Szerkesztő típusa', 'pages_revisions_changelog' => 'Változásnapló', 'pages_revisions_changes' => 'Módosítások', @@ -304,12 +304,12 @@ return [ 'pages_revisions_none' => 'Ennek az oldalnak nincsenek változatai', 'pages_copy_link' => 'Hivatkozás másolása', 'pages_edit_content_link' => 'Ugrás a szakaszhoz a szerkesztőben', - 'pages_pointer_enter_mode' => 'Lépjen be a szakaszválasztó módba', + 'pages_pointer_enter_mode' => 'Belépés szakaszválasztó módba', 'pages_pointer_label' => 'Oldalszakasz beállításai', 'pages_pointer_permalink' => 'Oldalszakasz állandó hivatkozás', 'pages_pointer_include_tag' => 'Oldalszakasz tartalmazza a címkét', - 'pages_pointer_toggle_link' => 'Permalink mód, Nyomja meg az include tag megjelenítéséhez', - 'pages_pointer_toggle_include' => 'Include tag mód, Nyomja meg az permalink megjelenítéséhez', + 'pages_pointer_toggle_link' => 'Állandó hivatkozás mód. Nyomja meg az beágyazó címke megjelenítéséhez!', + 'pages_pointer_toggle_include' => 'Beágyazó címke mód. Nyomja meg az állandó hivatkozás megjelenítéséhez!', 'pages_permissions_active' => 'Oldal jogosultságok aktívak', 'pages_initial_revision' => 'Kezdeti közzététel', 'pages_references_update_revision' => 'A belső hivatkozások automatikus frissítése', @@ -318,36 +318,39 @@ return [ 'pages_draft_edited_notification' => 'Ezt az oldalt azóta már frissítették. Javasolt ennek a vázlatnak az elvetése.', 'pages_draft_page_changed_since_creation' => 'Ez az oldal a vázlat létrehozása óta frissült. Javasoljuk, hogy dobja el ezt a piszkozatot, vagy ügyeljen arra, hogy ne írja felül az oldal módosításait.', 'pages_draft_edit_active' => [ - 'start_a' => ':count felhasználók kezdte el szerkeszteni ezt az oldalt', + 'start_a' => ':count felhasználó kezdte el szerkeszteni ezt az oldalt', 'start_b' => ':userName elkezdte szerkeszteni ezt az oldalt', 'time_a' => 'mióta az oldal utoljára frissítve volt', 'time_b' => 'az utolsó :minCount percben', - 'message' => ':start :time. Ügyeljen arra, hogy ne írjuk felül egymás frissítéseit!', + 'message' => ':start :time. Ügyeljen arra, hogy ne írja felül mások frissítéseit!', ], - 'pages_draft_discarded' => 'Vázlat elvetve! A szerkesztő frissítve lesz az oldal aktuális tartalmával', - 'pages_draft_deleted' => 'Vázlat elvetve! A szerkesztő frissítve lesz az oldal aktuális tartalmával', + 'pages_draft_discarded' => 'Vázlat elvetve! A szerkesztő frissítve lett az oldal aktuális tartalmával', + 'pages_draft_deleted' => 'Vázlat törölve! A szerkesztő frissítve lett az oldal aktuális tartalmával', 'pages_specific' => 'Egy bizonyos oldal', 'pages_is_template' => 'Oldalsablon', // Editor Sidebar 'toggle_sidebar' => 'Oldalsáv ki/be', + 'page_contents' => 'Oldal tartalma', + 'page_contents_none' => 'Nincsen egy címsor sem az oldalon.', + 'page_contents_info' => 'A tartalom menü automatikusan generálódik az oldalon használt címsorokból.', 'page_tags' => 'Oldal címkék', 'chapter_tags' => 'Fejezet címkék', 'book_tags' => 'Könyv címkék', 'shelf_tags' => 'Polc címkék', 'tag' => 'Címke', 'tags' => 'Címkék', - 'tags_index_desc' => 'A címkék a rendszeren belüli tartalomra alkalmazhatók a kategorizálás rugalmas formája alkalmazása érdekében. A címkéknek kulcsuk és értékük is lehetnek, de az érték nem kötelező. Alkalmazása után a tartalom lekérdezhető a címkenév és érték használatával.', + 'tags_index_desc' => 'A rendszeren belül a tartalomra címkék helyezhetők a rugalmasan rendezhetőség érdekében. A címkéknek neve és értéke is lehet, de az érték nem kötelező. Beállítás után a tartalom lekérdezhető a címkenév és érték használatával.', 'tag_name' => 'Címkenév', 'tag_value' => 'Címke érték (nem kötelező)', 'tags_explain' => "Címkék hozzáadása a tartalom jobb kategorizálásához.\nA mélyebb szervezettség megvalósításához hozzá lehet rendelni egy értéket a címkéhez.", 'tags_add' => 'Másik címke hozzáadása', 'tags_remove' => 'Címke eltávolítása', - 'tags_usages' => 'Összes címkehasználat', - 'tags_assigned_pages' => 'Oldalakhoz Rendelt', + 'tags_usages' => 'Összes címke használat', + 'tags_assigned_pages' => 'Oldalakhoz rendelt', 'tags_assigned_chapters' => 'Fejezetekhez rendelt', - 'tags_assigned_books' => 'Könyvekhez Rendelt', - 'tags_assigned_shelves' => 'Polcokhoz Rendelt', + 'tags_assigned_books' => 'Könyvekhez rendelt', + 'tags_assigned_shelves' => 'Polcokhoz rendelt', 'tags_x_unique_values' => ':count egyedi érték', 'tags_all_values' => 'Összes érték', 'tags_view_tags' => 'Címkék megtekintése', @@ -356,12 +359,12 @@ return [ 'attachments' => 'Csatolmányok', 'attachments_explain' => 'Az oldalon megjelenő fájlok feltöltése vagy hivatkozások csatolása. Az oldal oldalsávjában fognak megjelenni.', 'attachments_explain_instant_save' => 'Az itt történt módosítások azonnal el lesznek mentve.', - 'attachments_upload' => 'Fájlfeltöltés', + 'attachments_upload' => 'Fáj lfeltöltése', 'attachments_link' => 'Hivatkozás csatolása', 'attachments_upload_drop' => 'Alternatív megoldásként a fájlt ide húzva is fel lehet tölteni mellékletként.', 'attachments_set_link' => 'Hivatkozás beállítása', - 'attachments_delete' => 'Biztosan törölhető ez a melléklet?', - 'attachments_dropzone' => 'Húzza a file(oka)t ide a feltöltéshez', + 'attachments_delete' => 'Biztosan törli a mellékletet?', + 'attachments_dropzone' => 'Húzza ide a fájlokat a feltöltéshez', 'attachments_no_files' => 'Nincsenek fájlok feltöltve', 'attachments_explain_link' => 'Fájl feltöltése helyett hozzá lehet kapcsolni egy hivatkozást. Ez egy hivatkozás lesz egy másik oldalra vagy egy fájlra a felhőben.', 'attachments_link_name' => 'Hivatkozás neve', @@ -369,12 +372,12 @@ return [ 'attachments_link_url' => 'Hivatkozás fájlra', 'attachments_link_url_hint' => 'Weboldal vagy fájl webcíme', 'attach' => 'Csatolás', - 'attachments_insert_link' => 'Melléklet hivatkozás hozzáadása oldalhoz', + 'attachments_insert_link' => 'Melléklet hivatkozás hozzáadása az oldalhoz', 'attachments_edit_file' => 'Fájl szerkesztése', 'attachments_edit_file_name' => 'Fájl neve', 'attachments_edit_drop_upload' => 'Feltöltés és felülírás ejtéssel vagy kattintással', 'attachments_order_updated' => 'Csatolmány sorrend frissítve', - 'attachments_updated_success' => 'Csatolmány részletei frissítve', + 'attachments_updated_success' => 'Csatolmány részletek frissítve', 'attachments_deleted' => 'Csatolmány törölve', 'attachments_file_uploaded' => 'Fájl sikeresen feltöltve', 'attachments_file_updated' => 'Fájl sikeresen frissítve', @@ -387,7 +390,7 @@ return [ 'templates_prepend_content' => 'Hozzáadás az oldal tartalmának elejéhez', // Profile View - 'profile_user_for_x' => 'Felhasználó ez óta: :time', + 'profile_user_for_x' => 'Felhasználó ennyi ideje: :time', 'profile_created_content' => 'Létrehozott tartalom', 'profile_not_created_pages' => ':userName még nem hozott létre oldalt', 'profile_not_created_chapters' => ':userName még nem hozott létre fejezetet', @@ -395,61 +398,61 @@ return [ 'profile_not_created_shelves' => ':userName még nem hozott létre polcot', // Comments - 'comment' => 'Megjegyzés', - 'comments' => 'Megjegyzések', - 'comment_add' => 'Megjegyzés hozzáadása', - 'comment_none' => 'No comments to display', - 'comment_placeholder' => 'Megjegyzés írása', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', - 'comment_save' => 'Megjegyzés mentése', - 'comment_new' => 'Új megjegyzés', - 'comment_created' => 'megjegyzést fűzött hozzá :createDiff', + 'comment' => 'Hozzászólás', + 'comments' => 'Hozzászólások', + 'comment_add' => 'Hozzászólás hozzáadása', + 'comment_none' => 'Nincs megjeleníthető hozzászólás', + 'comment_placeholder' => 'Hozzászólás írása', + 'comment_thread_count' => ':count hozzászóláslánc|:count hozzászóláslánc', + 'comment_archived_count' => ':count archivált', + 'comment_archived_threads' => 'Archivált hozzászólásláncok', + 'comment_save' => 'Hozzászólás mentése', + 'comment_new' => 'Új hozzászólás', + 'comment_created' => 'hozzászólt :createDiff', 'comment_updated' => 'Frissítve :updateDiff :username által', 'comment_updated_indicator' => 'Frissített', - 'comment_deleted_success' => 'Megjegyzés törölve', - 'comment_created_success' => 'Megjegyzés hozzáadva', - 'comment_updated_success' => 'Megjegyzés frissítve', - 'comment_archive_success' => 'Comment archived', - 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', - 'comment_delete_confirm' => 'Biztosan törölhető ez a megjegyzés?', - 'comment_in_reply_to' => 'Válasz erre: :commentId', - 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', - 'comment_editor_explain' => 'Itt vannak az ezen az oldalon lévő megjegyzések. Megjegyzések hozzáadhatók és kezelhetők a mentett oldal megtekintésekor.', + 'comment_deleted_success' => 'Hozzászólás törölve', + 'comment_created_success' => 'Hozzászólás hozzáadva', + 'comment_updated_success' => 'Hozzászólás frissítve', + 'comment_archive_success' => 'Hozzászólás archiválva', + 'comment_unarchive_success' => 'Hozzászólás visszaállítva', + 'comment_view' => 'Hozzászólás megjelenítése', + 'comment_jump_to_thread' => 'Ugrás hozzászóláslánchoz', + 'comment_delete_confirm' => 'Biztosan törli a megjegyzést?', + 'comment_in_reply_to' => 'Válaszolva erre: :commentId', + 'comment_reference' => 'Hivatkozás', + 'comment_reference_outdated' => '(Elavult)', + 'comment_editor_explain' => 'Itt vannak az ezen az oldalon lévő hozzászólások. Hozzászólás hozzáadhatók és kezelhetők a mentett oldal megtekintésekor.', // Revision - 'revision_delete_confirm' => 'Biztosan törölhető ez a változat?', - 'revision_restore_confirm' => 'Biztosan visszaállítható ez a változat? A oldal jelenlegi tartalma le lesz cserélve.', - 'revision_cannot_delete_latest' => 'A legutolsó változat nem törölhető.', + 'revision_delete_confirm' => 'Biztosan törli ezt a változatot?', + 'revision_restore_confirm' => 'Biztosan visszaállítja ezt a változatot? A oldal jelenlegi tartalma le lesz cserélve.', + 'revision_cannot_delete_latest' => 'A legutóbbi változat nem törölhető.', // Copy view 'copy_consider' => 'Kérem, fontolja meg az alábbiakat, amikor tartalmat kíván másolni.', - 'copy_consider_permissions' => 'Az egyéni engedélybeállítások nem kerülnek másolásra.', + 'copy_consider_permissions' => 'Az egyedi engedély beállítások nem kerülnek másolásra.', 'copy_consider_owner' => 'Minden lemásolt tartalomnak Ön lesz a tulajdonosa.', - 'copy_consider_images' => 'Az oldalképfájlok nem duplikálódnak, és az eredeti képek megőrzik kapcsolatukat az eredetileg feltöltött oldallal.', - 'copy_consider_attachments' => 'Az oldal mellékletei nem kerülnek másolásra.', - 'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.', + 'copy_consider_images' => 'Az kép fájlok nem duplikálódnak, és az eredeti képek megőrzik kapcsolatukat az oldallal, ahova eredetileg fel lettek töltve.', + 'copy_consider_attachments' => 'Az oldal mellékletei nem lesznek lemásolva.', + 'copy_consider_access' => 'A hely, tulajdonos vagy jogosultságok megváltoztatása olyanoknak is hozzáférést adhat a tartalomhoz, akik korábban nem érhették el.', // Conversions 'convert_to_shelf' => 'Átalakítás polccá', - 'convert_to_shelf_contents_desc' => 'Ezt a könyvet új polccá alakíthatja, azonos tartalommal. A könyvben található fejezetek új könyvekké lesznek átalakítva. Ha ez a könyv tartalmaz olyan oldalakat, amelyek nem szerepelnek egy fejezetben, akkor a könyv átnevezzük és tartalmaz ilyen oldalakat, és ez a könyv az új polc részévé válik.', - 'convert_to_shelf_permissions_desc' => 'A könyvhöz beállított engedélyek át lesznek másolva az új polcra és az összes olyan új alárendelt könyvre, amelyek nem rendelkeznek saját engedélyekkel. Vegye figyelembe, hogy a polcokon lévő engedélyek nem kapcsolódnak automatikusan a tartalomhoz, ahogy a könyvek esetében.', + 'convert_to_shelf_contents_desc' => 'Ezt a könyvet új polccá alakíthatja, azonos tartalommal. A könyvben található fejezetek új könyvekké lesznek átalakítva. Ha ez a könyv tartalmaz olyan oldalakat, amelyek nem szerepelnek egy fejezetben, akkor a könyv át lesz nevezve, az oldalak hozzárendelve és ez a könyv az új polc részévé válik.', + 'convert_to_shelf_permissions_desc' => 'A könyvhöz beállított engedélyek át lesznek másolva az új polcra és az összes olyan új alárendelt könyvre, amelyek nem rendelkeznek saját engedélyekkel. Vegye figyelembe, hogy a polcokon lévő engedélyek nem vonatkoznak automatikusan az alárendelt tartalomra, ahogy a könyvek esetében.', 'convert_book' => 'Könyv átalakítása', - 'convert_book_confirm' => 'Biztosan konvertálni szeretné ezt a könyvet?', + 'convert_book_confirm' => 'Biztosan át szeretné alakítani ezt a könyvet?', 'convert_undo_warning' => 'Ezt nem lehet olyan könnyen visszavonni.', 'convert_to_book' => 'Átalakítás könyvvé', 'convert_to_book_desc' => 'Ezt a fejezetet új, azonos tartalmú könyvvé alakíthatja. Az ebben a fejezetben beállított engedélyek átmásolódnak az új könyvbe, de a szülőkönyvből származó örökölt engedélyek nem kerülnek másolásra, ami a hozzáférés-szabályozás megváltozásához vezethet.', 'convert_chapter' => 'Fejezet átalakítása', - 'convert_chapter_confirm' => 'Biztosan át szeretnéd alakítani ezt a fejezetet?', + 'convert_chapter_confirm' => 'Biztosan át szeretné alakítani ezt a fejezetet?', // References - 'references' => 'Értékelések', + 'references' => 'Hivatkozások', 'references_none' => 'Nincsenek nyomon követett hivatkozások erre az elemre.', - 'references_to_desc' => 'Az alábbiakban felsoroljuk az összes ismert tartalmat a rendszerben, amely erre az elemre hivatkozik.', + 'references_to_desc' => 'Alább fel van sorolva az összes ismert tartalom a rendszerben, amely erre az elemre hivatkozik.', // Watch Options 'watch' => 'Megfigyelés', @@ -459,19 +462,19 @@ return [ 'watch_desc_ignore' => 'Figyelmen kívül hagyja az összes értesítést, beleértve a felhasználói szintű beállításokból származó értesítéseket is.', 'watch_title_new' => 'Új oldalak', 'watch_desc_new' => 'Értesítés, ha új oldal jön létre ezen az elemen belül.', - 'watch_title_updates' => 'Minden oldal frissítése', - 'watch_desc_updates' => 'Értesítés minden új oldalról és oldalváltozásról.', - 'watch_desc_updates_page' => 'Értesítsen minden oldalváltozásról.', - 'watch_title_comments' => 'Az oldal összes frissítése és megjegyzése', - 'watch_desc_comments' => 'Értesítés minden új oldalról, oldalváltozásról és új megjegyzésről.', - 'watch_desc_comments_page' => 'Értesítés az oldal változásairól és az új megjegyzésekről.', + 'watch_title_updates' => 'Minden oldal változás', + 'watch_desc_updates' => 'Értesítés minden új oldalról és oldal változásról.', + 'watch_desc_updates_page' => 'Értesítsen minden oldal változásról.', + 'watch_title_comments' => 'Minden oldal változás és hozzászólás', + 'watch_desc_comments' => 'Értesítés minden új oldalról, oldal változásról és új hozzászólásról.', + 'watch_desc_comments_page' => 'Értesítés az oldal változásairól és az új hozzászólásokról.', 'watch_change_default' => 'Az alapértelmezett értesítési beállítások módosítása', 'watch_detail_ignore' => 'Az értesítések figyelmen kívül hagyása', 'watch_detail_new' => 'Új oldalak figyelése', - 'watch_detail_updates' => 'Új oldalak és frissítések figyelése', - 'watch_detail_comments' => 'Új oldalak, frissítések és megjegyzések figyelése', - 'watch_detail_parent_book' => 'Megfigyelés szülőkönyvből', - 'watch_detail_parent_book_ignore' => 'Figyelmen kívül hagyás a szülőkönyvön keresztül', - 'watch_detail_parent_chapter' => 'Megfigyelés szülő fejezetből', - 'watch_detail_parent_chapter_ignore' => 'Figyelmen kívül hagyás a szülő fejezeten keresztül', + 'watch_detail_updates' => 'Új oldalak és változások figyelése', + 'watch_detail_comments' => 'Új oldalak, változások és hozzászólások figyelése', + 'watch_detail_parent_book' => 'Megfigyelés tartalmazó könyvön keresztül', + 'watch_detail_parent_book_ignore' => 'Figyelmen kívül hagyás tartalmazó könyvön keresztül', + 'watch_detail_parent_chapter' => 'Megfigyelés tartalmazó fejezetten keresztül', + 'watch_detail_parent_chapter_ignore' => 'Figyelmen kívül hagyás tartalmazó fejezetten keresztül', ]; diff --git a/lang/hu/errors.php b/lang/hu/errors.php index 4264af8ab..c3b1a4d6d 100644 --- a/lang/hu/errors.php +++ b/lang/hu/errors.php @@ -6,46 +6,46 @@ return [ // Permissions 'permission' => 'Nincs jogosultság a kért oldal eléréséhez.', - 'permissionJson' => 'Nincs jogosultság a kért művelet végrehajtásához.', + 'permissionJson' => 'Nincs jogosultsága a kért művelet végrehajtásához.', // Auth 'error_user_exists_different_creds' => ':email címmel már létezik felhasználó, de más hitelesítő adatokkal.', 'auth_pre_register_theme_prevention' => 'A felhasználói fiók nem regisztrálható a megadott adatokkal', - 'email_already_confirmed' => 'Az email cím már meg van erősítve, meg lehet próbálni a bejelentkezést.', - 'email_confirmation_invalid' => 'A megerősítő vezérjel nem érvényes vagy használva volt. Meg kell próbálni újraregisztrálni.', - 'email_confirmation_expired' => 'A megerősítő vezérjel lejárt. Egy új megerősítő email lett elküldve.', + 'email_already_confirmed' => 'Az email cím már meg van erősítve. Próbáljon meg bejelentkezni!', + 'email_confirmation_invalid' => 'Ez a megerősítő kulcs nem érvényes vagy használva már volt. Próbáljon meg újra regisztrálni!', + 'email_confirmation_expired' => 'Ez a megerősítő kulcs már lejárt. Egy új megerősítő email lett küldve.', 'email_confirmation_awaiting' => 'A használatban lévő fiók email címét meg kell erősíteni', 'ldap_fail_anonymous' => 'Nem sikerült az LDAP elérése névtelen csatlakozással', - 'ldap_fail_authed' => 'Az LDAP hozzáférés nem sikerült a megadott DN és jelszó beállításokkal', - 'ldap_extension_not_installed' => 'LDAP PHP kiterjesztés nincs telepítve', - 'ldap_cannot_connect' => 'Nem lehet kapcsolódni az LDAP kiszolgálóhoz, a kezdeti kapcsolatfelvétel nem sikerült', - 'saml_already_logged_in' => 'Már bejelentkezett', + 'ldap_fail_authed' => 'Nem sikerült az LDAP elérése a megadott DN és jelszó adatokkal', + 'ldap_extension_not_installed' => 'Az LDAP PHP kiterjesztés nincsen telepítve', + 'ldap_cannot_connect' => 'Nem lehet kapcsolódni az LDAP szerverhez, a kezdeti kapcsolatfelvétel nem sikerült', + 'saml_already_logged_in' => 'Már be van jelentkezve', 'saml_no_email_address' => 'Ehhez a felhasználóhoz nem található email cím a külső hitelesítő rendszer által átadott adatokban', - 'saml_invalid_response_id' => 'A külső hitelesítő rendszerből érkező kérést nem ismerte fel az alkalmazás által indított folyamat. Bejelentkezés után az előző oldalra történő visszalépés okozhatja ezt a hibát.', - 'saml_fail_authed' => 'Bejelentkezés :system használatával sikertelen, a rendszer nem biztosított sikeres hitelesítést', - 'oidc_already_logged_in' => 'Már bejelentkezett', + 'saml_invalid_response_id' => 'A külső hitelesítő rendszerből érkező kérést nem ismerte fel egy az alkalmazás által indított folyamat sem. Bejelentkezés után az előző oldalra történő visszalépés okozhatja ezt a hibát.', + 'saml_fail_authed' => 'A bejelentkezés a(z) :system használatával sikertelen volt, a rendszer nem biztosított sikeres hitelesítést', + 'oidc_already_logged_in' => 'Már be van jelentkezve', 'oidc_no_email_address' => 'Ehhez a felhasználóhoz nem található email cím a külső hitelesítő rendszer által átadott adatokban', - 'oidc_fail_authed' => 'Bejelentkezés :system használatával sikertelen, a rendszer nem biztosított sikeres hitelesítést', - 'social_no_action_defined' => 'Nincs művelet meghatározva', - 'social_login_bad_response' => "Hiba történt :socialAccount bejelentkezés közben:\n:error", - 'social_account_in_use' => ':socialAccount fiók már használatban van. :socialAccount opción keresztül érdemes megpróbálni a bejelentkezést.', - 'social_account_email_in_use' => ':email email cím már használatban van. Ha már van fiók létrehozva, :egy socialAccount fiókot hozzá lehet csatolni a profil beállításainál.', - 'social_account_existing' => ':socialAccount már hozzá van kapcsolva a fiókhoz.', - 'social_account_already_used_existing' => ':socialAccount fiókot már egy másik felhasználó használja.', - 'social_account_not_used' => ':socialAccount fiók nincs felhasználóhoz kapcsolva. A hozzákapcsolást a profil oldalon lehet elvégezni. ', - 'social_account_register_instructions' => ':socialAccount beállítása használatával is lehet fiókot regisztrálni, ha még nem volt fiók létrehozva.', - 'social_driver_not_found' => 'Közösségi meghajtó nem található', - 'social_driver_not_configured' => ':socialAccount közösségi beállítások nem megfelelőek.', - 'invite_token_expired' => 'Ez a meghívó hivatkozás lejárt. Helyette meg lehet próbálni új jelszót megadni a fiókhoz.', + 'oidc_fail_authed' => 'A bejelentkezés a(z) :system használatával sikertelen volt, a rendszer nem biztosított sikeres hitelesítést', + 'social_no_action_defined' => 'Nincs meghatározott művelet', + 'social_login_bad_response' => "Hiba történt a(z) :socialAccount bejelentkezés közben:\n:error", + 'social_account_in_use' => 'Ez a(z) :socialAccount fiók már használatban van. Próbáljon meg bejelentkezni a(z) :socialAccount opción keresztül!', + 'social_account_email_in_use' => 'A(z) :email email cím már használatban van. Ha már van fiókja, hozzá tudja kapcsolni a(z) :socialAccount fiókját a profil beállításainál.', + 'social_account_existing' => 'Ez a(z) :socialAccount már hozzá van kapcsolva a fiókjához.', + 'social_account_already_used_existing' => 'Ezt a(z) :socialAccount fiókot egy másik felhasználó már használja.', + 'social_account_not_used' => 'Ez a(z) :socialAccount fiók nincs egy felhasználóhoz sem kapcsolva. Kapcsolja hozzá a profil beállításoknál. ', + 'social_account_register_instructions' => 'Ha még nincsen fiókja, regisztrálhat egyet a(z) :socialAccount opció használatával.', + 'social_driver_not_found' => 'Nem található közösségi média illesztőprogram', + 'social_driver_not_configured' => 'A(z) :socialAccount fiók összekapcsolás beállításai nem megfelelőek.', + 'invite_token_expired' => 'Ez a meghívó már lejárt. Helyette megpróbálhatja a fiók jelszavát visszaállítani.', 'login_user_not_found' => 'A művelethez nem található felhasználó.', // System - 'path_not_writable' => ':filePath elérési út nem tölthető fel. Ellenőrizni kell, hogy az útvonal a kiszolgáló számára írható.', - 'cannot_get_image_from_url' => 'Nem lehet lekérni a képet innen: :url', - 'cannot_create_thumbs' => 'A kiszolgáló nem tud létrehozni bélyegképeket. Ellenőrizni kell, hogy telepítve van-a a GD PHP kiterjesztés.', - 'server_upload_limit' => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.', - 'server_post_limit' => 'A szerver nem tudja fogadni a megadott adatmennyiséget. Próbálkozz újra kevesebb adattal vagy egy kisebb fájllal.', - 'uploaded' => 'A kiszolgáló nem engedélyez ilyen méretű feltöltéseket. Kisebb fájlmérettel kell próbálkozni.', + 'path_not_writable' => 'Nem sikerült feltölteni a :filePath elérési útra. Ellenőrizze, hogy az útvonal írható a szerveren!', + 'cannot_get_image_from_url' => 'Nem lehet lekérni a képet a(z) :url címről', + 'cannot_create_thumbs' => 'A szerver nem tud létrehozni bélyegképeket. Ellenőrizze, hogy telepítve van a GD PHP kiterjesztés!', + 'server_upload_limit' => 'A szerver nem engedélyez ekkora méretű feltöltéseket. Kérjük próbálkozzon kisebb fájl mérettel.', + 'server_post_limit' => 'A szerver nem tudja fogadni a megadott adatmennyiséget. Próbálkozzon újra kevesebb adattal vagy egy kisebb fájllal!', + 'uploaded' => 'A szerver nem engedélyez ekkora méretű feltöltéseket. Kérjük próbálkozzon kisebb fájl mérettel.', // Drawing & Images 'image_upload_error' => 'Hiba történt a kép feltöltése közben', @@ -78,7 +78,7 @@ return [ // Users 'users_cannot_delete_only_admin' => 'Nem lehet törölni az egyetlen adminisztrátort', 'users_cannot_delete_guest' => 'A vendég felhasználót nem lehet törölni', - 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', + 'users_could_not_send_invite' => 'Nem lehetett létrehozni a felhasználót, mivel a meghívó levelet nem sikerült elküldeni', // Roles 'role_cannot_be_edited' => 'Ezt a szerepkört nem lehet szerkeszteni', @@ -87,16 +87,16 @@ return [ 'role_cannot_remove_only_admin' => 'Ez a felhasználó az egyetlen, az adminisztrátor szerepkörhöz rendelt felhasználó. Eltávolítása előtt az adminisztrátor szerepkört át kell ruházni egy másik felhasználóra.', // Comments - 'comment_list' => 'Hiba történt a megjegyzések lekérése közben.', - 'cannot_add_comment_to_draft' => 'Vázlathoz nem lehet megjegyzéseket fűzni.', - 'comment_add' => 'Hiba történt a megjegyzés hozzáadása / frissítése közben.', - 'comment_delete' => 'Hiba történt a megjegyzés törlése közben.', - 'empty_comment' => 'Üres megjegyzést nem lehet hozzáadni.', + 'comment_list' => 'Hiba történt a hozzászólások lekérése közben.', + 'cannot_add_comment_to_draft' => 'Vázlathoz nem lehet hozzászólni.', + 'comment_add' => 'Hiba történt a hozzászólás hozzáadása/frissítése közben.', + 'comment_delete' => 'Hiba történt a hozzászólás törlése közben.', + 'empty_comment' => 'Üres hozzászólást nem lehet hozzáadni.', // Error pages - '404_page_not_found' => 'Oldal nem található', + '404_page_not_found' => 'Az oldal nem található', 'sorry_page_not_found' => 'Sajnáljuk, a keresett oldal nem található.', - 'sorry_page_not_found_permission_warning' => 'Ha arra számított, hogy ez az oldal létezik, előfordulhat, hogy nincs engedélye a megtekintésére.', + 'sorry_page_not_found_permission_warning' => 'Ha arra számított, hogy ez az oldal létezik, előfordulhat, hogy nincs jogosultsága a megtekintésére.', 'image_not_found' => 'A kép nem található', 'image_not_found_subtitle' => 'Sajnáljuk, a keresett kép nem található.', 'image_not_found_details' => 'Ha arra számított, hogy ez a kép létezik, akkor előfordulhat, hogy törölték.', @@ -106,26 +106,26 @@ return [ 'back_soon' => 'Hamarosan újra elérhető lesz.', // Import - 'import_zip_cant_read' => 'Could not read ZIP file.', - 'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.', - 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', - 'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.', - 'import_validation_failed' => 'Import ZIP failed to validate with errors:', - 'import_zip_failed_notification' => 'Failed to import ZIP file.', - 'import_perms_books' => 'You are lacking the required permissions to create books.', - 'import_perms_chapters' => 'You are lacking the required permissions to create chapters.', - 'import_perms_pages' => 'You are lacking the required permissions to create pages.', - 'import_perms_images' => 'You are lacking the required permissions to create images.', - 'import_perms_attachments' => 'You are lacking the required permission to create attachments.', + 'import_zip_cant_read' => 'Nem sikerült a ZIP fájlt olvasni.', + 'import_zip_cant_decode_data' => 'Nem sikerült megtalálni és dekódolni a data.json tartalmát a ZIP fájlban.', + 'import_zip_no_data' => 'A ZIP fájlban nincsen könyv, fejezet vagy oldal tartalom.', + 'import_zip_data_too_large' => 'A ZIP fájlban lévő data.json tartalma meghaladja a beállított feltöltési méret limitet.', + 'import_validation_failed' => 'A ZIP fájl importálásának ellenőrzése sikertelen volt a következő hibák miatt:', + 'import_zip_failed_notification' => 'Nem sikerült a ZIP fájlt importálni.', + 'import_perms_books' => 'Nem rendelkezik a könyvek létrehozásához szükséges jogosultságokkal.', + 'import_perms_chapters' => 'Nem rendelkezik a fejezetek létrehozásához szükséges jogosultságokkal.', + 'import_perms_pages' => 'Nem rendelkezik az oldalak létrehozásához szükséges jogosultságokkal.', + 'import_perms_images' => 'Nem rendelkezik a képek létrehozásához szükséges jogosultságokkal.', + 'import_perms_attachments' => 'Nem rendelkezik a csatolmányok létrehozásához szükséges jogosultságokkal.', // API errors - 'api_no_authorization_found' => 'A kérésben nem található hitelesítési vezérjel', - 'api_bad_authorization_format' => 'A kérésben hitelesítési vezérjel található de a formátuma érvénytelennek tűnik', - 'api_user_token_not_found' => 'A megadott hitelesítési vezérjelhez nem található egyező API vezérjel', - 'api_incorrect_token_secret' => 'Az API tokenhez használt secret helytelen', - 'api_user_no_api_permission' => 'A használt API vezérjel tulajdonosának nincs jogosultsága API hívások végrehajtásához', - 'api_user_token_expired' => 'A használt hitelesítési vezérjel lejárt', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_no_authorization_found' => 'A kérésben nem található hitelesítési kulcs', + 'api_bad_authorization_format' => 'A kérésben hitelesítési kulcs található de a formátuma érvénytelennek tűnik', + 'api_user_token_not_found' => 'A megadott hitelesítési kulcshoz nem található egyező API kulcs', + 'api_incorrect_token_secret' => 'Az API kulcshoz megadott jelkulcs helytelen', + 'api_user_no_api_permission' => 'A használt API kulcs tulajdonosának nincs jogosultsága API hívások végrehajtásához', + 'api_user_token_expired' => 'A használt hitelesítési kulcs lejárt', + 'api_cookie_auth_only_get' => 'Kizárólag GET kérések engedélyezettek az API-n keresztül süti alapú authentikáció használatkor', // Settings & Maintenance 'maintenance_test_email_failure' => 'Hiba történt egy teszt email küldésekor:', diff --git a/lang/hu/notifications.php b/lang/hu/notifications.php index d8a29688a..e7073a496 100644 --- a/lang/hu/notifications.php +++ b/lang/hu/notifications.php @@ -4,26 +4,26 @@ */ return [ - 'new_comment_subject' => 'Új megjegyzés ezen az oldalon: :pageName', - 'new_comment_intro' => 'Egy felhasználó hozzászólt egy oldalon itt: :appName:', + 'new_comment_subject' => 'Új megjegyzés a(z) :pageName oldalon', + 'new_comment_intro' => 'Egy felhasználó hozzászólt egy oldalon a(z) :appName: alkalmazásban', 'new_page_subject' => 'Új oldal: :pageName', - 'new_page_intro' => 'Az új oldal létrehozása sikeres volt itt: :appName:', + 'new_page_intro' => 'Egy új oldal lett létrehozva a(z) :appName: alkalmazásban', 'updated_page_subject' => 'Frissített oldal: :pageName', 'updated_page_intro' => 'Az oldal frissítése sikeres volt itt: :appName:', - 'updated_page_debounce' => 'Az értesítések tömegének elkerülése érdekében egy ideig nem kap értesítést az oldal további szerkesztéseiről ugyanaz a szerkesztő.', - 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'updated_page_debounce' => 'Az értesítések tömegének elkerülése érdekében egy ideig nem kap értesítést az oldal további szerkesztéseiről ugyanattól a szerkesztőtől.', + 'comment_mention_subject' => 'Ön meg lett említve a(z) :pageName oldalon', + 'comment_mention_intro' => 'Ön meg lett említve egy hozzászólásban a(z) :appName: alkalmazásban', 'detail_page_name' => 'Oldal neve:', 'detail_page_path' => 'Oldal helye:', 'detail_commenter' => 'Hozzászóló:', - 'detail_comment' => 'Megjegyzés:', + 'detail_comment' => 'Hozzászólás:', 'detail_created_by' => 'Készítette:', 'detail_updated_by' => 'Frissítette:', 'action_view_comment' => 'Hozzászólás megtekintése', 'action_view_page' => 'Oldal megtekintése', - 'footer_reason' => 'Ezt az értesítést azért küldtük, mert a :link lefedi ezt a tevékenységtípust ehhez az elemhez.', - 'footer_reason_link' => 'értesítési beállításait', + 'footer_reason' => 'Ezt az értesítést azért küldtük, mert a :link lefedi ezt a tevékenység típust ehhez az elemhez.', + 'footer_reason_link' => 'az Ön értesítési beállításai', ]; diff --git a/lang/hu/passwords.php b/lang/hu/passwords.php index 03a37684e..90b13c30f 100644 --- a/lang/hu/passwords.php +++ b/lang/hu/passwords.php @@ -7,9 +7,9 @@ return [ 'password' => 'A jelszónak legalább hat karakterből kell állnia, és egyeznie kell a megerősítéssel.', - 'user' => "Nem található felhasználó ezzel az e-mail címmel.", - 'token' => 'A jelszó visszaállító biztonsági kód nem érvényes ehhez az e-mail címhez.', - 'sent' => 'E-mailben elküldtük a jelszó visszaállító hivatkozást!', + 'user' => "Nem található felhasználó ezzel az email címmel.", + 'token' => 'A jelszó visszaállító kulcs nem érvényes ehhez az email címhez.', + 'sent' => 'Emailben elküldtük a jelszó visszaállító hivatkozást!', 'reset' => 'A jelszó visszaállítva!', ]; diff --git a/lang/hu/preferences.php b/lang/hu/preferences.php index fb3c335e8..735e1fab7 100644 --- a/lang/hu/preferences.php +++ b/lang/hu/preferences.php @@ -15,38 +15,38 @@ return [ 'shortcuts_section_navigation' => 'Navigáció', 'shortcuts_section_actions' => 'Gyakori műveletek', 'shortcuts_save' => 'Billentyűparancsok mentése', - 'shortcuts_overlay_desc' => 'Megjegyzés: Amikor a gyorsbillentyűk engedélyezve vannak, egy segítő átfedés érhető el azzal, hogy a "?" billentyűt megnyomva kiemeli az aktuálisan látható képernyőn elérhető gyorsbillentyűket a műveletekhez.', - 'shortcuts_update_success' => 'A gyorsbillentyű-beállítások frissítve lettek!', - 'shortcuts_overview_desc' => 'A rendszerfelhasználói felületen történő navigálásához használható billentyűparancsok kezelése.', + 'shortcuts_overlay_desc' => 'Megjegyzés: Amikor a gyorsbillentyűk engedélyezve vannak, a súgó ablak elérhető azzal, hogy a "?" billentyűt megnyomva kiemeli az aktuálisan látható képernyőn elérhető gyorsbillentyűket a műveletekhez.', + 'shortcuts_update_success' => 'A gyorsbillentyű beállítások frissítve lettek!', + 'shortcuts_overview_desc' => 'A felhasználói felületen történő navigálásához használható billentyűparancsok kezelése.', 'notifications' => 'Értesítési beállítások', - 'notifications_desc' => 'Állítsd be az e-mail értesítéseket, amelyeket akkor kapsz, ha bizonyos tevékenység történik a rendszeren belül.', - 'notifications_opt_own_page_changes' => 'Értesítsen változásokról az általam tulajdonolt oldalakon', - 'notifications_opt_own_page_comments' => 'Értesítés a hozzászólásokról az általam tulajdonolt oldalakon', - 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', + 'notifications_desc' => 'A rendszerben folytatott tevékenységekről kapott értesítések beállítása.', + 'notifications_opt_own_page_changes' => 'Értesítsen változásokról az általam birtokolt oldalakon', + 'notifications_opt_own_page_comments' => 'Értesítés a hozzászólásokról az általam birtokolt oldalakon', + 'notifications_opt_comment_mentions' => 'Értesítsen, amikor megemlítenek egy hozzászólásban', 'notifications_opt_comment_replies' => 'Értesítsen válaszokról a hozzászólásaimra', 'notifications_save' => 'Beállítások mentése', 'notifications_update_success' => 'Az értesítési beállítások frissítve lettek!', 'notifications_watched' => 'Megfigyelt és figyelmen kívül hagyott elemek', 'notifications_watched_desc' => 'Az alábbi elemekre egyedi figyelési beállítások vannak alkalmazva. A beállítások frissítéséhez tekintsd meg az elemet, majd keresd a figyelési lehetőségeket az oldalsávban.', - 'auth' => 'Hozzáférés és Biztonság', + 'auth' => 'Hozzáférés és biztonság', 'auth_change_password' => 'Jelszó módosítása', - 'auth_change_password_desc' => 'Változtasd meg az alkalmazásba történő bejelentkezéshez használt jelszavadat. Ennek legalább 8 karakter hosszúnak kell lennie.', + 'auth_change_password_desc' => 'A bejelentkezéshez használt jelszó módosítása. Legalább 8 karakter hosszúnak kell lennie.', 'auth_change_password_success' => 'A jelszó frissítve lett!', 'profile' => 'Felhasználó részletei', 'profile_desc' => 'A kommunikációhoz és a rendszer személyre szabásához használt adatokon kívül kezelheti fiókja adatait, amelyek más felhasználók számára jelennek meg.', 'profile_view_public' => 'Nyilvános profil megtekintése', - 'profile_name_desc' => 'Állítsd be a megjelenített nevedet, amely látható lesz a rendszer többi felhasználója számára az általad végzett tevékenység és a saját tartalom révén.', - 'profile_email_desc' => 'Ezt az e-mail címet értesítésekre fogjuk használni, valamint az érvényben lévő beállítások függvényében hitelesítéshez is.', - 'profile_email_no_permission' => 'Sajnos nincs jogosultságod az e-mail cím megváltoztatására. Ha szeretnéd ezt megváltoztatni, kérj meg egy adminisztrátort, hogy ezt megtegye helyetted.', - 'profile_avatar_desc' => 'Válassz egy képet, amelyet a rendszerben használnál a neved mellett. A kép lehetőleg négyzet alakú és körülbelül 256px szélességű és magasságú legyen.', + 'profile_name_desc' => 'Állítsa be a megjelenített nevét amely látható lesz a rendszer többi felhasználója számára az Ön által végzett tevékenység és a saját tartalom révén.', + 'profile_email_desc' => 'Ezt az email címet értesítésekre fogjuk használni, valamint az érvényben lévő beállítások függvényében hitelesítéshez is.', + 'profile_email_no_permission' => 'Sajnos nincs jogosultsága az email cím megváltoztatására. Ha szeretné ezt megváltoztatni, kérjen meg egy adminisztrátort, hogy ezt megtegye helyette.', + 'profile_avatar_desc' => 'Válasszon egy képet, amelyet a rendszerben használni szeretne a neve mellett. A kép lehetőleg négyzet alakú és körülbelül 256 pixel szélességű és magasságú legyen.', 'profile_admin_options' => 'Adminisztrátori beállítások', - 'profile_admin_options_desc' => 'További adminisztrátori szintű lehetőségek, például a szerepkörök hozzárendelésének kezelése, megtalálhatóak a felhasználói fiókod beállításai között az "Beállítások > Felhasználók" területen az alkalmazásban.', + 'profile_admin_options_desc' => 'További adminisztrátori szintű lehetőségek, például a szerepkörök hozzárendelésének kezelése, megtalálhatók a felhasználói fiókod beállításai között az "Beállítások > Felhasználók" területen az alkalmazásban.', 'delete_account' => 'Felhasználói fiók törlése', - 'delete_my_account' => 'Törlöm a felhasználói fiókomat', - 'delete_my_account_desc' => 'Ez véglegesen törölni fogja a felhasználói fiókodat a rendszerből. Nem lesz lehetőséged visszaállítani ezt a fiókot, vagy visszavonni ezt a műveletet. A létrehozott tartalmak, például az oldalak és feltöltött képek megmaradnak.', - 'delete_my_account_warning' => 'Biztosan törölni szeretnéd a fiókodat?', + 'delete_my_account' => 'Saját felhasználói fiók törlése', + 'delete_my_account_desc' => 'Ez véglegesen törölni fogja a felhasználói fiókját a rendszerből. Nem lesz lehetősége visszaállítani ezt a fiókot, vagy visszavonni ezt a műveletet. A létrehozott tartalmak, például az oldalak és feltöltött képek megmaradnak.', + 'delete_my_account_warning' => 'Biztosan törölni szeretné a fiókját?', ]; diff --git a/lang/hu/settings.php b/lang/hu/settings.php index aaccbd35f..6dd7de33b 100644 --- a/lang/hu/settings.php +++ b/lang/hu/settings.php @@ -14,46 +14,46 @@ return [ // App Settings 'app_customization' => 'Személyre szabás', - 'app_features_security' => 'Jellemzők és biztonság', + 'app_features_security' => 'Funkciók és biztonság', 'app_name' => 'Alkalmazás neve', 'app_name_desc' => 'Ez a név meg fog jelenni a fejlécben és minden a rendszer által küldött emailben.', 'app_name_header' => 'Név mutatása a fejlécben', 'app_public_access' => 'Nyilvános hozzáférés', - 'app_public_access_desc' => 'Ha engedélyezett, a nem bejelentkezett felhasználók is hozzá tudnak férni a BookStack példány tartalmaihoz.', + 'app_public_access_desc' => 'Az opció engedélyezése lehetővé teszi látogatóknak, hogy bejelentkezés nélkül hozzáférjenek az alkalmazásban tárolt bizonyos tartalmakhoz.', 'app_public_access_desc_guest' => 'A nyilvános látogatók hozzáférése a "Guest" felhasználón keresztül irányítható.', 'app_public_access_toggle' => 'Nyilvános hozzáférés engedélyezése', - 'app_public_viewing' => 'Nyilvános megtekintés engedélyezve?', + 'app_public_viewing' => 'Engedélyezi a nyilvános elérést?', 'app_secure_images' => 'Magasabb biztonságú képfeltöltés', 'app_secure_images_toggle' => 'Magasabb biztonságú képfeltöltés engedélyezése', - 'app_secure_images_desc' => 'Teljesítmény optimalizálási okokból minden kép nyilvános. Ez a beállítás egy véletlenszerű, nehezen kitalálható karakterláncot illeszt a képek útvonalának elejére. Meg kell győződni róla, hogy a könnyű hozzáférés megakadályozása érdekében a könyvtár indexek nincsenek engedélyezve.', + 'app_secure_images_desc' => 'Teljesítmény optimalizálási okokból minden kép nyilvános. Ez a beállítás egy véletlenszerű, nehezen kitalálható karakterláncot illeszt a kép URL-ek elejére. Győződjön meg róla, hogy a könyvtárak indexelése nincsen engedélyezve!', 'app_default_editor' => 'Alapértelmezett oldal szerkesztő', - 'app_default_editor_desc' => 'Válassza ki, hogy alapértelmezés szerint melyik szerkesztőt szeretné használni az új oldalak szerkesztésekor. Ezt felülírhatja oldalszintű szinten, amennyiben az engedélyek lehetővé teszik.', + 'app_default_editor_desc' => 'Válassza ki, hogy alapértelmezés szerint melyik szerkesztőt szeretné használni az új oldalak szerkesztésekor. Ezt felülírhatja az egyes oldalak szintjén, amennyiben az engedélyek lehetővé teszik.', 'app_custom_html' => 'Egyéni HTML fejléc tartalom', 'app_custom_html_desc' => 'Az itt hozzáadott bármilyen tartalom be lesz illesztve minden oldal szekciójának aljára. Ez hasznos a stílusok felülírásához van analitikai kódok hozzáadásához.', - 'app_custom_html_disabled_notice' => 'Az egyéni HTML fejléc tartalom le van tiltva ezen a beállítási oldalon, hogy az esetleg hibásan megadott módosításokat vissza lehessen állítani.', + 'app_custom_html_disabled_notice' => 'Az egyéni HTML fejléc tartalom le van tiltva ezen a beállítási oldalon, hogy az esetleg hibásan megadott módosításokat vissza lehessen vonni.', 'app_logo' => 'Alkalmazás logó', - 'app_logo_desc' => 'Ez az alkalmazás fejléc sávjában van használva többek között. Ennek a képnek 86 képpont magasnak kell lennie. A nagy képek át lesznek méretezve.', + 'app_logo_desc' => 'Ez többek között az alkalmazás fejléc sávjában van használva. A képnek 86 pixel magasnak kell lennie. A nagy képek átméretezésre kerülnek.', 'app_icon' => 'Alkalmazás ikon', - 'app_icon_desc' => 'Ez az ikon a böngésző fülekhez és a gyorsikonokhoz használatos. Ez egy 256 képpont négyzet alakú PNG képnek kell lennie.', + 'app_icon_desc' => 'Ez az ikon a böngésző fülekhez és parancsikonokhoz használatos. Az ikonnak egy 256 pixeles, négyzetes PNG képnek kell lennie.', 'app_homepage' => 'Alkalmazás kezdőlapja', - 'app_homepage_desc' => 'A kezdőlapon az alapértelmezés szerinti nézet helyett megjelenő nézet kiválasztása. A kiválasztott oldalakon figyelmen kívül lesznek hagyva az oldal engedélyek.', + 'app_homepage_desc' => 'Válassza ki a kezdőlapon az alapértelmezett nézet helyett megjelenő oldalt!. A kiválasztott oldalon figyelmen kívül lesznek hagyva a hozzáférési jogosultságok.', 'app_homepage_select' => 'Egy oldal kiválasztása', 'app_footer_links' => 'Lábléc linkek', - 'app_footer_links_desc' => 'Adj hozzá linkeket a weboldal láblécéhez. Ezek a legtöbb oldalon megjelennek, beleértve azokat is, amelyekhez nincs szükség bejelentkezésre. Használhatsz egy "trans::" címkét a rendszer által definiált fordítások használatához. Például: A "trans::common.privacy_policy" használata a lefordított szöveget ("Adatvédelmi Irányelvek") adja vissza, és a "trans::common.terms_of_service" a "Szolgáltatási feltételek" lefordított szöveget adja eredményül.', + 'app_footer_links_desc' => 'Adjon hozzá linkeket a weboldal láblécéhez. Ezek a legtöbb oldalon megjelennek, beleértve azokat is, amelyekhez nincs szükség bejelentkezésre. Használhat egy "trans::" címkét a rendszer által definiált fordítások használatához. Például: A "trans::common.privacy_policy" használata a "Adatvédelmi Irányelvek" lefordított szöveget adja vissza, és a "trans::common.terms_of_service" a "Szolgáltatási feltételek" lefordított szöveget adja eredményül.', 'app_footer_links_label' => 'Link címke', 'app_footer_links_url' => 'Link URL', 'app_footer_links_add' => 'Lábléc hivatkozás hozzáadása', - 'app_disable_comments' => 'Megjegyzések letiltása', - 'app_disable_comments_toggle' => 'Megjegyzések letiltása', - 'app_disable_comments_desc' => 'Megjegyzések letiltása az alkalmazás összes oldalán.
    A már létező megjegyzések el lesznek rejtve.', + 'app_disable_comments' => 'Hozzászólások letiltása', + 'app_disable_comments_toggle' => 'Hozzászólások letiltása', + 'app_disable_comments_desc' => 'Hozzászólások letiltása az alkalmazás összes oldalán.
    A már létező hozzászólások el lesznek rejtve.', // Color settings 'color_scheme' => 'Alkalmazás színséma', - 'color_scheme_desc' => 'Állítsd be a színeket az alkalmazás felhasználói felületén. A színeket külön-külön lehet konfigurálni a sötét és a világos módokhoz, hogy a legjobban illeszkedjenek a témához, és biztosítsák az olvashatóságot.', - 'ui_colors_desc' => 'Állítsa be az alkalmazás elsődleges színét és alapértelmezett hivatkozási színét. Az elsődleges színt főként a fejléc szalaghirdetéséhez, a gombokhoz és a felület díszítéséhez használják. Az alapértelmezett hivatkozásszín a szöveges hivatkozásokhoz és műveletekhez használatos, mind az írott tartalomban, mind az alkalmazás felületén.', + 'color_scheme_desc' => 'Állítsa be az alkalmazás felhasználói felületének színeit. A színeket külön-külön lehet konfigurálni a sötét és a világos módokhoz, hogy a legjobban illeszkedjenek a témához, és biztosítsák az olvashatóságot.', + 'ui_colors_desc' => 'Állítsa be az alkalmazás elsődleges színét és az alapértelmezett hivatkozási színt. Az elsődleges színt főként a fejléc szalag, a gombok és egyéb díszítőelemek használják. Az alapértelmezett hivatkozás szín a szöveges hivatkozásokhoz és műveletekhez használatos, mind az írott tartalomban, mind az alkalmazás felhasználói felületén.', 'app_color' => 'Elsődleges szín', 'link_color' => 'Alapértelmezett link szín', - 'content_colors_desc' => 'Beállítja az elemek színét az oldalszervezési hierarchiában. Az olvashatóság szempontjából javasolt az alapértelmezés szerinti színhez hasonló fényerősséget választani.', + 'content_colors_desc' => 'Állítsa be az elemek színeit az oldal hierarchiában. Az olvashatóság érdekében javasolt az alapértelmezett színekhez hasonló világosságú színeket választani.', 'bookshelf_color' => 'Polc színe', 'book_color' => 'Könyv színe', 'chapter_color' => 'Fejezet színe', @@ -64,65 +64,65 @@ return [ 'reg_settings' => 'Regisztráció', 'reg_enable' => 'Regisztráció engedélyezése', 'reg_enable_toggle' => 'Regisztráció engedélyezése', - 'reg_enable_desc' => 'Ha a regisztráció engedélyezett, akkor a felhasználó képes lesz bejelentkezni mint az alkalmazás egy felhasználója. Regisztráció után egy egyszerű, alapértelmezés szerinti felhasználói szerepkör lesz hozzárendelve.', + 'reg_enable_desc' => 'Ha a regisztráció engedélyezve van, akkor látogatók regisztrálhatják saját magukat az alkalmazásba. Az új felhasználókhoz az alapértelmezett felhasználói szerepkör kerül hozzárendelésre.', 'reg_default_role' => 'Regisztráció utáni alapértelmezett felhasználói szerepkör', - 'reg_enable_external_warning' => 'A fenti beállítási lehetőség nincs használatban, ha külső LDAP vagy SAML hitelesítés aktív. A nem létező tagok felhasználói fiókjai automatikusan létrejönnek ha a használatban lévő külső rendszeren sikeres a hitelesítés.', - 'reg_email_confirmation' => 'Email megerősítés', - 'reg_email_confirmation_toggle' => 'Email megerősítés szükséges', - 'reg_confirm_email_desc' => 'Ha a tartomány korlátozás be van állítva, akkor email megerősítés szükséges és ez a beállítás figyelmen kívül lesz hagyva.', - 'reg_confirm_restrict_domain' => 'Tartomány korlátozás', - 'reg_confirm_restrict_domain_desc' => 'Azoknak az email tartományoknak a vesszővel elválasztott listája, melyekre a regisztráció korlátozva lesz. A felhasználók egy emailt fognak kapni, hogy megerősítsék az email címüket mielőtt használni kezdhetnék az alkalmazást.
    Fontos tudni, hogy a felhasználók a sikeres regisztráció után megváltoztathatják az email címüket.', + 'reg_enable_external_warning' => 'A fenti beállítási lehetőség figyelmen kívül van hagyva, ha külső LDAP vagy SAML hitelesítés aktív. A nem létező tagok felhasználói fiókjai automatikusan létrejönnek ha a használatban lévő külső rendszeren sikeres a hitelesítés.', + 'reg_email_confirmation' => 'Email megerősítése', + 'reg_email_confirmation_toggle' => 'Email megerősítése szükséges', + 'reg_confirm_email_desc' => 'Ha a domain korlátozás be van állítva, akkor az email megerősítése kötelező, és ez a beállítás figyelmen kívül van hagyva.', + 'reg_confirm_restrict_domain' => 'Domain korlátozás', + 'reg_confirm_restrict_domain_desc' => 'Adja meg azon email domain-eket vesszővel tagolt listaként, amikre a regisztrációt korlátozni szeretné. A felhasználók egy emailt fognak kapni, hogy megerősítsék az email címüket mielőtt használni kezdhetnék az alkalmazást.
    Fontos tudni, hogy a felhasználók a sikeres regisztráció után megváltoztathatják az email címüket.', 'reg_confirm_restrict_domain_placeholder' => 'Nincs beállítva korlátozás', // Sorting Settings - 'sorting' => 'Lists & Sorting', - 'sorting_book_default' => 'Default Book Sort Rule', - 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', - 'sorting_rules' => 'Sort Rules', - 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', - 'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books', - 'sort_rule_create' => 'Create Sort Rule', - 'sort_rule_edit' => 'Edit Sort Rule', - 'sort_rule_delete' => 'Delete Sort Rule', - 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', - 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?', - 'sort_rule_details' => 'Sort Rule Details', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', - 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', - 'sort_rule_available_operations' => 'Available Operations', - 'sort_rule_available_operations_empty' => 'No operations remaining', - 'sort_rule_configured_operations' => 'Configured Operations', - 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', - 'sort_rule_op_asc' => '(Asc)', - 'sort_rule_op_desc' => '(Desc)', - 'sort_rule_op_name' => 'Name - Alphabetical', - 'sort_rule_op_name_numeric' => 'Name - Numeric', - 'sort_rule_op_created_date' => 'Created Date', - 'sort_rule_op_updated_date' => 'Updated Date', - 'sort_rule_op_chapters_first' => 'Chapters First', - 'sort_rule_op_chapters_last' => 'Chapters Last', - 'sorting_page_limits' => 'Per-Page Display Limits', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting' => 'Listák és rendezés', + 'sorting_book_default' => 'Alapértelmezett könyv rendezési szabály', + 'sorting_book_default_desc' => 'Válassza ki az új könyvekre alkalmazandó alapértelmezett rendezési szabályt. Ez nem fogja befolyásolni a már létező könyveket, és könyvenként felülbírálható.', + 'sorting_rules' => 'Rendezési szabályok', + 'sorting_rules_desc' => 'Ezek előre meghatározott rendezési szabályok, amelyek alkalmazhatók a rendszerben található tartalomra.', + 'sort_rule_assigned_to_x_books' => 'Hozzárendelve :count könyvhöz|Hozzárendelve :count könyvhöz', + 'sort_rule_create' => 'Rendezési szabály létrehozása', + 'sort_rule_edit' => 'Rendezési szabály szerkesztése', + 'sort_rule_delete' => 'Rendezési szabály törlése', + 'sort_rule_delete_desc' => 'Eltávolítja ezt a rendezési szabályt a rendszerből. A jelenleg ezt használó könyvek vissza lesznek állítva kézi rendezésre.', + 'sort_rule_delete_warn_books' => 'Ez a rendezési szabály :count könyvön van használva. Biztosan törli?', + 'sort_rule_delete_warn_default' => 'Ez a rendezési szabály jelenleg az alapértelmezett az új könyvekhez. Biztosan törli?', + 'sort_rule_details' => 'Rendezési szabály részletei', + 'sort_rule_details_desc' => 'Állítson be egy nevet a rendezési szabálynak, amely megjelenik majd a rendezési listákban.', + 'sort_rule_operations' => 'Rendezési műveletek', + 'sort_rule_operations_desc' => 'Állítsa be rendezési műveleteket az elemek átmozgatásával az elérhető műveletek listájából. A műveletek mindig fentről lefelé haladva kerülnek végrehajtásra. Mentéskor minden itt végzett változtatás az összes hozzárendelt könyvre érvényesítve lesz.', + 'sort_rule_available_operations' => 'Elérhető műveletek', + 'sort_rule_available_operations_empty' => 'Nincs több művelet', + 'sort_rule_configured_operations' => 'Beállított műveletek', + 'sort_rule_configured_operations_empty' => 'Húzzon/adjon hozzá műveleteket az "Elérhető műveletek" listáról', + 'sort_rule_op_asc' => '(Növekvő)', + 'sort_rule_op_desc' => '(Csökkenő)', + 'sort_rule_op_name' => 'Név - betűrend', + 'sort_rule_op_name_numeric' => 'Név - számsorrend', + 'sort_rule_op_created_date' => 'Létrehozás dátuma', + 'sort_rule_op_updated_date' => 'Frissítés dátuma', + 'sort_rule_op_chapters_first' => 'Fejezetek elől', + 'sort_rule_op_chapters_last' => 'Fejezetek hátul', + 'sorting_page_limits' => 'Oldalankénti megjelenítési limitek', + 'sorting_page_limits_desc' => 'Állítsa be oldalanként mennyi elem kerüljön megjelenítésre a rendszer külöböző listáiban. Általában a kisebb mennyiség jobb teljesítményhez vezet, nagyobb mennyiségnél nem kell annyi oldalon végigmenni. Ajánlott a 6 egyik többszörösét használni.', // Maintenance settings 'maint' => 'Karbantartás', - 'maint_image_cleanup' => 'Képek tisztítása', + 'maint_image_cleanup' => 'Képek kitakarítása', 'maint_image_cleanup_desc' => 'Végigolvassa az oldalakat és a tartalmak változatait, hogy leellenőrizze jelenleg mely képek és rajzok vannak használatban, és mely képek szerepelnek többször. A futtatása előtt feltétlen készíteni kell egy teljes adatbázis és lemezkép mentést.', 'maint_delete_images_only_in_revisions' => 'Törölje azokat a képeket is, amelyek csak a régi oldalverziókban léteznek', - 'maint_image_cleanup_run' => 'Tisztítás futtatása', - 'maint_image_cleanup_warning' => ':count potenciálisan nem használt képet találtam. Biztosan törölhetőek ezek a képek?', + 'maint_image_cleanup_run' => 'Takarítás futtatása', + 'maint_image_cleanup_warning' => ':count potenciálisan nem használt kép van. Biztosan törölni szeretné ezeket a képeket?', 'maint_image_cleanup_success' => ':count potenciálisan nem használt kép megtalálva és törölve!', 'maint_image_cleanup_nothing_found' => 'Nincsenek nem használt képek, semmi sem lett törölve!', - 'maint_send_test_email' => 'Teszt e-mail küldése', + 'maint_send_test_email' => 'Teszt email küldése', 'maint_send_test_email_desc' => 'Ez elküld egy teszt emailt a profilban megadott email címre.', - 'maint_send_test_email_run' => 'Teszt e-mail küldése', - 'maint_send_test_email_success' => 'Email elküldve :address címre', - 'maint_send_test_email_mail_subject' => 'Teszt e-mail', + 'maint_send_test_email_run' => 'Teszt email küldése', + 'maint_send_test_email_success' => 'Email elküldve a(z) :address címre', + 'maint_send_test_email_mail_subject' => 'Teszt email', 'maint_send_test_email_mail_greeting' => 'Az email kézbesítés működőképesnek tűnik!', 'maint_send_test_email_mail_text' => 'Gratulálunk! Mivel ez az email figyelmeztetés megérkezett az email beállítások megfelelőek.', - 'maint_recycle_bin_desc' => 'A törölt polcok, könyvek, fejezetek és oldalak a lomtárba kerülnek, így visszaállíthatók vagy véglegesen törölhetők. A rendszer konfigurációtól függően egy idő után a lomtárban lévő régebbi elemek automatikusan eltávolíthatók.', + 'maint_recycle_bin_desc' => 'A törölt polcok, könyvek, fejezetek és oldalak a lomtárba kerülnek, így visszaállíthatók vagy véglegesen törölhetők. A rendszer konfigurációtól függően egy idő után a lomtárban lévő régebbi elemek automatikusan eltávolításra kerülhetnek.', 'maint_recycle_bin_open' => 'Lomtár megnyitása', 'maint_regen_references' => 'Referenciák újragenerálása', 'maint_regen_references_desc' => 'Ez a művelet újraépíti az adatbázison belüli elemek közötti hivatkozási indexet. Ez általában automatikusan történik, de ez a művelet hasznos lehet régi vagy nem hivatalos módszerekkel hozzáadott tartalom indexeléséhez.', @@ -144,7 +144,7 @@ return [ 'recycle_bin_destroy_confirm' => 'Ez a művelet véglegesen törli ezt az elemet a rendszerből az alább felsorolt összes alárendelt elemmel együtt, és nem fogja tudni visszaállítani ezt a tartalmat. Biztosan véglegesen törli ezt az elemet?', 'recycle_bin_destroy_list' => 'Megsemmisítendő elemek', 'recycle_bin_restore_list' => 'Visszaállítandó elemek', - 'recycle_bin_restore_confirm' => 'Ez a művelet visszaállítja a törölt elemet, beleértve az utódelemeket is, az eredeti helyükre. Ha az eredeti helyet azóta törölték, és most a lomtárban van, akkor a szülőelemet is vissza kell állítani.', + 'recycle_bin_restore_confirm' => 'Ez a művelet visszaállítja a törölt elemet, beleértve az alárendelt elemeket is, az eredeti helyükre. Ha az eredeti helyet azóta törölték, és most a lomtárban van, akkor a tartalmazó elemet is vissza kell állítani.', 'recycle_bin_restore_deleted_parent' => 'Ennek az elemnek a szülője is törölve lett. Ezek mindaddig törölve maradnak, amíg az adott szülőt is vissza nem állítják.', 'recycle_bin_restore_parent' => 'Szűlő visszaállítása', 'recycle_bin_destroy_notification' => 'Összesen :count elemet törölt a lomtárból.', @@ -160,7 +160,7 @@ return [ 'audit_table_user' => 'Felhasználó', 'audit_table_event' => 'Esemény', 'audit_table_related' => 'Kapcsolódó elem vagy részlet', - 'audit_table_ip' => 'IP Cím', + 'audit_table_ip' => 'IP cím', 'audit_table_date' => 'Tevékenység időpontja', 'audit_date_from' => 'Kezdő dátum', 'audit_date_to' => 'Végdátum', @@ -168,46 +168,46 @@ return [ // Role Settings 'roles' => 'Szerepkörök', 'role_user_roles' => 'Felhasználói szerepkörök', - 'roles_index_desc' => 'A szerepkörök a felhasználók csoportosítására és rendszerengedélyek biztosítására szolgálnak tagjaiknak. Ha egy felhasználó több szerepkör tagja, a megadott jogosultságok halmozódnak, és a felhasználó örökli az összes képességet.', + 'roles_index_desc' => 'A szerepkörök a felhasználók csoportosítására és rendszerengedélyek biztosítására szolgálnak. Ha egy felhasználó több szerepkör tagja, a megadott jogosultságok halmozódnak, és a felhasználó örökli az összes képességet.', 'roles_x_users_assigned' => ':count hozzárendelt felhasználó|:count hozzárendelt felhasználó', 'roles_x_permissions_provided' => ':count jogosultság|:count jogosultság', 'roles_assigned_users' => 'Hozzárendelt felhasználók', 'roles_permissions_provided' => 'Megadott jogosultságok', 'role_create' => 'Új szerepkör létrehozása', 'role_delete' => 'Szerepkör törlése', - 'role_delete_confirm' => 'Ez törölni fogja \':roleName\' szerepkört.', - 'role_delete_users_assigned' => 'Ehhez a szerepkörhöz :userCount felhasználó van hozzárendelve. Ha a felhasználókat át kell helyezni ebből a szerepkörből, akkor ki kell választani egy új szerepkört.', - 'role_delete_no_migration' => "Nincs felhasználó áthelyezés", - 'role_delete_sure' => 'Biztosan törölhető ez a szerepkör?', + 'role_delete_confirm' => 'Ez törölni fogja a(z) \':roleName\' szerepkört.', + 'role_delete_users_assigned' => 'Ehhez a szerepkörhöz :userCount felhasználó van hozzárendelve. Ha át szeretné helyezni a felhasználókat egy másik szerepkörbe, akkor válasszon egy új szerepkört az alábbi listából.', + 'role_delete_no_migration' => "Ne helyezze át a felhasználókat", + 'role_delete_sure' => 'Biztosan törli ezt a szerepkört?', 'role_edit' => 'Szerepkör szerkesztése', 'role_details' => 'Szerepkör részletei', 'role_name' => 'Szerepkör neve', 'role_desc' => 'Szerepkör rövid leírása', - 'role_mfa_enforced' => 'Kétlépcsős hitelesítés megkövetelése', + 'role_mfa_enforced' => 'Többlépcsős hitelesítés megkövetelése', 'role_external_auth_id' => 'Külső hitelesítés azonosítók', 'role_system' => 'Rendszer jogosultságok', 'role_manage_users' => 'Felhasználók kezelése', - 'role_manage_roles' => 'Szerepkörök és szerepkör engedélyek kezelése', + 'role_manage_roles' => 'Szerepkörök és jogosultságok kezelése', 'role_manage_entity_permissions' => 'Minden könyv, fejezet és oldalengedély kezelése', 'role_manage_own_entity_permissions' => 'Saját könyv, fejezet és oldalak engedélyeinek kezelése', 'role_manage_page_templates' => 'Oldalsablonok kezelése', 'role_access_api' => 'Hozzáférés a rendszer API-hoz', 'role_manage_settings' => 'Alkalmazás beállításainak kezelése', 'role_export_content' => 'Tartalom exportálása', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Tartalom importálása', 'role_editor_change' => 'Oldalszerkesztő módosítása', 'role_notifications' => 'Értesítések fogadása és kezelése', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', - 'role_asset' => 'Eszköz jogosultságok', + 'role_permission_note_users_and_roles' => 'Ezek a jogosultságok gyakorlatilag megtekintést és felhasználó/szerepkör keresést is lehetővé fognak tenni.', + 'role_asset' => 'Tartalom jogosultságok', 'roles_system_warning' => 'Ne feledje, hogy a fenti három engedély bármelyikéhez való hozzáférés lehetővé teszi a felhasználó számára, hogy módosítsa saját vagy a rendszerben mások jogosultságait. Csak megbízható felhasználókhoz rendeljen szerepeket ezekkel az engedélyekkel.', - 'role_asset_desc' => 'Ezek a jogosultságok vezérlik az alapértelmezés szerinti hozzáférést a rendszerben található eszközökhöz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.', - 'role_asset_admins' => 'Az adminisztrátorok automatikusan hozzáférést kapnak minden tartalomhoz, de ezek a beállítások megjeleníthetnek vagy elrejthetnek felhasználói felület beállításokat.', + 'role_asset_desc' => 'Ezek a jogosultságok vezérlik az alapértelmezés szerinti hozzáférést a rendszerben található tartalomhoz. A könyvek, fejezetek és oldalak jogosultságai felülírják ezeket a jogosultságokat.', + 'role_asset_admins' => 'Az adminisztrátorok automatikusan hozzáférést kapnak minden tartalomhoz, de ezek a beállítások megjeleníthetnek vagy elrejthetnek beállításokat a felhasználói felületen.', 'role_asset_image_view_note' => 'Ez a képkezelőn belüli láthatóságra vonatkozik. A feltöltött képfájlok tényleges elérése a rendszerkép tárolási beállításától függ.', - 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', + 'role_asset_users_note' => 'Ezek a jogosultságok gyakorlatilag megtekintést és felhasználó keresést is lehetővé fognak tenni.', 'role_all' => 'Összes', 'role_own' => 'Saját', - 'role_controlled_by_asset' => 'Az általuk feltöltött eszköz által ellenőrzött', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_asset' => 'A feltöltött tartalom beállításaitól függ', + 'role_controlled_by_page_delete' => 'Az oldal törlési jogosultságoktól függ', 'role_save' => 'Szerepkör mentése', 'role_users' => 'Felhasználók ebben a szerepkörben', 'role_users_none' => 'Jelenleg nincsenek felhasználók hozzárendelve ehhez a szerepkörhöz', @@ -220,72 +220,75 @@ return [ 'users_search' => 'Felhasználók keresése', 'users_latest_activity' => 'Legújabb tevékenység', 'users_details' => 'Felhasználó részletei', - 'users_details_desc' => 'Egy megjelenítendő név és email cím beállítása ennek a felhasználónak. Az email cím az alkalmazásba történő bejelentkezéshez lesz használva.', - 'users_details_desc_no_email' => 'Egy megjelenítendő név beállítása ennek a felhasználónak amiről mások felismerik.', + 'users_details_desc' => 'Megjelenítendő név és email cím beállítása ennek a felhasználónak. Az email cím az alkalmazásba történő bejelentkezéshez lesz használva.', + 'users_details_desc_no_email' => 'Megjelenítendő név beállítása ennek a felhasználónak, amiről mások felismerik.', 'users_role' => 'Felhasználói szerepkörök', - 'users_role_desc' => 'A felhasználó melyik szerepkörhöz lesz rendelve. Ha a felhasználó több szerepkörhöz van rendelve, akkor ezeknek a szerepköröknek a jogosultságai összeadódnak, és a a felhasználó a hozzárendelt szerepkörök minden képességét megkapja.', + 'users_role_desc' => 'Felhasználó szerepköreinek kiválasztása. Ha a felhasználó több szerepkörhöz van rendelve, akkor ezeknek a szerepköröknek a jogosultságai összeadódnak, és a a felhasználó a hozzárendelt szerepkörök minden képességét megkapja.', 'users_password' => 'Felhasználó jelszava', 'users_password_desc' => 'Az alkalmazásba bejelentkezéshez használható jelszó beállítása. Legalább 8 karakter hosszúnak kell lennie.', - 'users_send_invite_text' => 'Lehetséges egy meghívó emailt küldeni ennek a felhasználónak ami lehetővé teszi, hogy beállíthassa a saját jelszavát. Máskülönben a jelszót az erre jogosult felhasználónak kell beállítania.', + 'users_send_invite_text' => 'Dönthet úgy, hogy meghívó emailt küld ennek a felhasználónak, így az beállíthatja saját jelszavát. Ellenkező esetben Ön is beállíthat egy jelszót neki.', 'users_send_invite_option' => 'Felhasználó meghívó levél küldése', 'users_external_auth_id' => 'Külső hitelesítés azonosítója', - 'users_external_auth_id_desc' => 'Ha külső hitelesítési rendszer van használatban (például SAML2, OIDC vagy LDAP), ez az az azonosító, amely a BookStack felhasználót a hitelesítési rendszerfiókhoz kapcsolja. Ha az alapértelmezett e-mail alapú hitelesítést használja, figyelmen kívül hagyhatja ezt a mezőt.', + 'users_external_auth_id_desc' => 'Ha külső hitelesítési rendszer van használatban (például SAML2, OIDC vagy LDAP), ez az az azonosító, amely a BookStack felhasználót a hitelesítési rendszerfiókhoz kapcsolja. Ha az alapértelmezett email alapú hitelesítést használja, figyelmen kívül hagyhatja ezt a mezőt.', 'users_password_warning' => 'Csak akkor töltse ki az alábbi mezőt, ha módosítani szeretné ennek a felhasználónak a jelszavát.', - 'users_system_public' => 'Ez a felhasználó bármelyik, a példányt megtekintő felhasználót képviseli. Nem lehet vele bejelentkezni de automatikusan hozzá lesz rendelve.', + 'users_system_public' => 'Ez a felhasználó bármelyik, az alkalmazást megtekintő látogatót képviseli. Nem lehet vele bejelentkezni de automatikusan hozzá lesz rendelve.', 'users_delete' => 'Felhasználó törlése', 'users_delete_named' => ':userName felhasználó törlése', 'users_delete_warning' => '\':userName\' felhasználó teljesen törölve lesz a rendszerből.', - 'users_delete_confirm' => 'Biztosan törölhető ez a felhasználó?', + 'users_delete_confirm' => 'Biztosan törli ezt a felhasználót?', 'users_migrate_ownership' => 'Tulajdonjog átruházása', 'users_migrate_ownership_desc' => 'Válasszon itt egy felhasználót, ha azt szeretné, hogy egy másik felhasználó legyen a tulajdonosa az összes, jelenleg a felhasználó tulajdonában lévő elemnek.', 'users_none_selected' => 'Nincs felhasználó kiválasztva', 'users_edit' => 'Felhasználó szerkesztése', 'users_edit_profile' => 'Profil szerkesztése', - 'users_avatar' => 'Avatar használata', - 'users_avatar_desc' => 'A felhasználót ábrázoló kép kiválasztása. Kb. 256px méretű négyzetes képnek kell lennie.', + 'users_avatar' => 'Felhasználó profilképe', + 'users_avatar_desc' => 'Válasszon ki egy képet, ami ehhez a felhasználóhoz fog tartozni. Négyzetes, körülbelül 256 pixel széles képnek kell lennie.', 'users_preferred_language' => 'Előnyben részesített nyelv', 'users_preferred_language_desc' => 'Ez a beállítás megváltoztatja az alkalmazás felhasználói felületén használt nyelvet. Nincs hatása a felhasználók által létrehozott tartalomra.', - 'users_social_accounts' => 'Közösségi fiókok', + 'users_social_accounts' => 'Közösségi média fiókok', 'users_social_accounts_desc' => 'Tekintse meg a felhasználó csatlakoztatott közösségi fiókjainak állapotát. A közösségi fiókok az elsődleges hitelesítési rendszer mellett használhatók a rendszerhez való hozzáféréshez.', - 'users_social_accounts_info' => 'Itt lehet egyéb fiókokat hozzákapcsolni a gyorsabb és könnyebb bejelentkezés érdekében. Itt olyan fiókot lehet lecsatlakoztatni, melynek korábban nem volt engedélyezett hozzáférése. Visszavonja a hozzáférést a csatlakoztatott szociális fiók profilbeállításaiból.', + 'users_social_accounts_info' => 'Itt kapcsolhat össze külső fiókokat a gyorsabb és egyszerűbb bejelentkezés érdekében. Egy összekapcsolás megszüntetése itt, nem vonja vissza a már korábban engedélyezett hozzáférést. Vonja vissza a hozzáférést a külső fiók profil beállításaiban.', 'users_social_connect' => 'Fiók csatlakoztatása', 'users_social_disconnect' => 'Fiók lecsatlakoztatása', 'users_social_status_connected' => 'Csatlakozva', 'users_social_status_disconnected' => 'Lecsatlakozva', 'users_social_connected' => ':socialAccount fiók sikeresen csatlakoztatva a profilhoz.', 'users_social_disconnected' => ':socialAccount fiók sikeresen lecsatlakoztatva a profilról.', - 'users_api_tokens' => 'API vezérjelek', - 'users_api_tokens_desc' => 'A BookStack REST API-val történő hitelesítéshez használt hozzáférési token létrehozása és kezelése. Az API engedélyeit azon a felhasználón keresztül kezelik, akihez a token tartozik.', - 'users_api_tokens_none' => 'Ehhez a felhasználóhoz nincsenek létrehozva API vezérjelek', - 'users_api_tokens_create' => 'Vezérjel létrehozása', + 'users_api_tokens' => 'API kulcsok', + 'users_api_tokens_desc' => 'A BookStack REST API-val történő hitelesítéshez használt hozzáférési kulcs létrehozása és kezelése. Az API engedélyeit azon a felhasználón keresztül kezelik, akihez a token tartozik.', + 'users_api_tokens_none' => 'Ehhez a felhasználóhoz nincsenek létrehozva API kulcsok', + 'users_api_tokens_create' => 'Kulcs létrehozása', 'users_api_tokens_expires' => 'Lejárat', 'users_api_tokens_docs' => 'API dokumentáció', - 'users_mfa' => 'Többfaktoros hitelesítés', + 'users_mfa' => 'Többlépcsős hitelesítés', 'users_mfa_desc' => 'Állítsa be a többlépcsős azonosítást egy extra biztonsági rétegként a felhasználói fiókjához.', - 'users_mfa_x_methods' => ':count metódus konfigurálva|:count metódus konfigurálva', + 'users_mfa_x_methods' => ':count mód beállítva|:count mód beállítva', 'users_mfa_configure' => 'Módszer beállítása', + 'users_mfa_reset' => 'Többlépcsős azonosítási módok alaphelyzetbe állítása', + 'users_mfa_reset_desc' => 'Ez alaphelyzetbe fogja állítani a felhasználónak az összes beállított többlépcsős azonosítási módját. Ha valamelyik a szerepkörük előírja a többlépcsős azonosítást, akkor a következő bejelentkezésnél fel lesznek szólítva egy új beállítására.', + 'users_mfa_reset_confirm' => 'Biztos benne, hogy alaphelyzetbe szeretné állítani ennek a felhasználónak a többlépcsős azonosítási módjait?', // API Tokens - 'user_api_token_create' => 'API vezérjel létrehozása', + 'user_api_token_create' => 'API kulcs létrehozása', 'user_api_token_name' => 'Név', - 'user_api_token_name_desc' => 'Adjon a tokennek egy olvasható nevet, hogy a jövőben emlékeztessen a tervezett céljára.', + 'user_api_token_name_desc' => 'Adjon a kulcsnak egy olvasható nevet, hogy a jövőben emlékeztessen a tervezett céljára.', 'user_api_token_expiry' => 'Lejárati dátum', - 'user_api_token_expiry_desc' => 'Dátum megadása ameddig a vezérjel érvényes. Ez után a dátum után az ezzel a vezérjellel történő kérések nem fognak működni. Üresen hagyva a lejárati idő 100 évre lesz beállítva.', - 'user_api_token_create_secret_message' => 'Közvetlenül a token létrehozása után egy „Token ID” és „Token Secret” generálódik és jelenik meg. A Secret csak egyszer jelenik meg, ezért a folytatás előtt másolja át az értéket egy biztonságos helyre.', - 'user_api_token' => 'API vezérjel', - 'user_api_token_id' => 'Vezérjel azonosító', - 'user_api_token_id_desc' => 'Ez egy nem szerkeszthető, a rendszer által létrehozott azonosító ehhez a vezérjelhez amire API kérésekben lehet szükség.', - 'user_api_token_secret' => 'Vezérjel titkos kódja', - 'user_api_token_secret_desc' => 'Ez egy rendszer által generált "secret" ehhez a tokenhez, amelyet meg kell adni az API-kérésekben. Ez csak most jelenik meg, ezért másolja ezt az értéket egy biztonságos helyre.', - 'user_api_token_created' => 'Vezérjel létrehozva :timeAgo', - 'user_api_token_updated' => 'Vezérjel frissítve :timeAgo', - 'user_api_token_delete' => 'Vezérjel törlése', - 'user_api_token_delete_warning' => '\':tokenName\' nevű API vezérjel teljesen törölve lesz a rendszerből.', - 'user_api_token_delete_confirm' => 'Biztosan törölhető ez az API vezérjel?', + 'user_api_token_expiry_desc' => 'Dátum ameddig a kulcs érvényes. Ez után a dátum után az ezzel a kulccsal történő kérések nem fognak működni. Üresen hagyva a lejárati idő 100 évre lesz beállítva.', + 'user_api_token_create_secret_message' => 'Közvetlenül a kulcs létrehozása után egy „Kulcs azonosító” és „Titkos kód” generálódik és jelenik meg. A titkos kód csak egyszer jelenik meg, ezért a folytatás előtt másolja át az értéket egy biztonságos helyre.', + 'user_api_token' => 'API kulcs', + 'user_api_token_id' => 'Kulcs azonosító', + 'user_api_token_id_desc' => 'Ez egy nem szerkeszthető, a rendszer által létrehozott azonosító ehhez a kulcshoz, amire API kérésekben lehet szükség.', + 'user_api_token_secret' => 'Kulcs titkos kódja', + 'user_api_token_secret_desc' => 'Ez egy rendszer által generált "titok" ehhez a kulcshoz, amelyet meg kell adni az API-kérésekben. Ez csak most jelenik meg, ezért másolja ezt az értéket egy biztonságos helyre.', + 'user_api_token_created' => 'Kulcs létrehozva :timeAgo', + 'user_api_token_updated' => 'Kulcs frissítve :timeAgo', + 'user_api_token_delete' => 'Kulcs törlése', + 'user_api_token_delete_warning' => '\':tokenName\' nevű API kulcs teljesen törölve lesz a rendszerből.', + 'user_api_token_delete_confirm' => 'Biztosan törli ezt az API kulcsot?', // Webhooks 'webhooks' => 'Webhook-ok', - 'webhooks_index_desc' => 'A webhookok segítségével adatokat küldhetünk külső URL-ekre, amikor bizonyos műveletek és események történnek a rendszeren belül, ami lehetővé teszi az eseményalapú integrációt külső platformokkal, például üzenetküldő vagy értesítési rendszerekkel.', + 'webhooks_index_desc' => 'A webhookok segítségével adatokat küldhet külső URL-ekre, amikor bizonyos műveletek és események történnek a rendszeren belül. Ez lehetővé teszi az eseményalapú integrációt külső platformokkal, például üzenetküldő vagy értesítési rendszerekkel.', 'webhooks_x_trigger_events' => ':count kiváltó esemény|:count kiváltó esemény', 'webhooks_create' => 'Új webhook létrehozása', 'webhooks_none_created' => 'Még nincs létrehozva egy webhook sem.', @@ -303,19 +306,19 @@ return [ 'webhooks_active' => 'Webhook aktív', 'webhook_events_table_header' => 'Események', 'webhooks_delete' => 'Webhook törlése', - 'webhooks_delete_warning' => 'Ezzel a \':webhookName\' nevű webhookot teljesen törli a rendszerből.', + 'webhooks_delete_warning' => 'Ezzel a(z) \':webhookName\' nevű webhookot teljesen törli a rendszerből.', 'webhooks_delete_confirm' => 'Biztosan törli ezt a webhookot?', 'webhooks_format_example' => 'Webhook formátum példa', - 'webhooks_format_example_desc' => 'A Webhook-adatok POST-kérésként kerülnek elküldésre a konfigurált végponthoz JSON-ként az alábbi formátumban. A "related_item" és az "url" tulajdonság nem kötelező, és az aktivált esemény típusától függ.', + 'webhooks_format_example_desc' => 'A Webhook-adatok POST-kérésként kerülnek elküldésre a beállított végponthoz JSON-ként az alábbi formátumban. A "related_item" és az "url" tulajdonság nem kötelező, és az aktivált esemény típusától függ.', 'webhooks_status' => 'Webhook állapota', 'webhooks_last_called' => 'Utolsó hívás:', 'webhooks_last_errored' => 'Utolsó hiba:', 'webhooks_last_error_message' => 'Utolsó hibaüzenet:', // Licensing - 'licenses' => 'Licenszek', + 'licenses' => 'Licencek', 'licenses_desc' => 'Ez az oldal a BookStack licencinformációit részletezi, a BookStackben használt projekteken és könyvtárakon kívül. Sok felsorolt projekt csak fejlesztési környezetben használható.', - 'licenses_bookstack' => 'BookStack Licensz', + 'licenses_bookstack' => 'BookStack licenc', 'licenses_php' => 'PHP könyvtár licencek', 'licenses_js' => 'JavaScript könyvtár licencek', 'licenses_other' => 'Egyéb licencek', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/hu/validation.php b/lang/hu/validation.php index b740ca0c6..ec6f68fe2 100644 --- a/lang/hu/validation.php +++ b/lang/hu/validation.php @@ -8,56 +8,56 @@ return [ // Standard laravel validation lines - 'accepted' => ':attribute elfogadott kell legyen.', - 'active_url' => ':attribute nem érvényes webcím.', - 'after' => ':attribute dátumnak :date utáninak kell lennie.', - 'alpha' => ':attribute csak betűket tartalmazhat.', - 'alpha_dash' => ':attribute csak betűket, számokat és kötőjeleket tartalmazhat.', - 'alpha_num' => ':attribute csak betűket és számokat tartalmazhat.', - 'array' => ':attribute tömb kell legyen.', + 'accepted' => 'A(z) :attribute elfogadott kell legyen.', + 'active_url' => 'A(z) :attribute nem egy érvényes URL.', + 'after' => 'A(z) :attribute objektumnak egy :date utáni dátumnak kell lennie.', + 'alpha' => 'A(z) :attribute csak betűket tartalmazhat.', + 'alpha_dash' => 'A(z) :attribute csak betűket, számokat, kötőjeleket és alávonásokat tartalmazhat.', + 'alpha_num' => 'A(z) :attribute csak betűket és számokat tartalmazhat.', + 'array' => 'A(z) :attribute tömb kell legyen.', 'backup_codes' => 'A megadott kód érvénytelen, vagy már felhasználták.', - 'before' => ':attribute dátumnak :date előttinek kell lennie.', + 'before' => 'A(z) :attribute objektumnak egy :date előtti dátumnak kell lennie.', 'between' => [ - 'numeric' => ':attribute értékének :min és :max között kell lennie.', - 'file' => ':attribute értékének :min és :max kilobájt között kell lennie.', - 'string' => ':attribute hosszának :min és :max karakter között kell lennie.', - 'array' => ':attribute mennyiségének :min és :max elem között kell lennie.', + 'numeric' => 'A(z) :attribute értékének :min és :max között kell lennie.', + 'file' => 'A(z) :attribute értékének :min és :max kB között kell lennie.', + 'string' => 'A(z) :attribute hosszának :min és :max karakter között kell lennie.', + 'array' => 'A(z) :attribute tömbnek :min és :max közötti elemszámának kell lennie.', ], - 'boolean' => ':attribute mezőnek igaznak vagy hamisnak kell lennie.', - 'confirmed' => ':attribute megerősítés nem egyezik.', - 'date' => ':attribute nem érvényes dátum.', - 'date_format' => ':attribute nem egyezik :format formátummal.', - 'different' => ':attribute és :other értékének különböznie kell.', - 'digits' => ':attribute :digits számból kell álljon.', - 'digits_between' => ':attribute hosszának :min és :max számjegy között kell lennie.', - 'email' => ':attribute érvényes email cím kell legyen.', - 'ends_with' => ':attribute attribútumnak a következők egyikével kell végződnie: :values', - 'file' => 'A(z) :attribute érvényes fájlnak kell lennie.', - 'filled' => ':attribute mező kötelező.', + 'boolean' => 'A(z) :attribute mezőnek igaznak vagy hamisnak kell lennie.', + 'confirmed' => 'A(z) :attribute megerősítés nem egyezik.', + 'date' => 'A(z) :attribute nem egy érvényes dátum.', + 'date_format' => 'A(z) :attribute nem egyezik a(z) :format formátummal.', + 'different' => 'A(z) :attribute és :other értékének különböznie kell.', + 'digits' => 'A(z) :attribute :digits számjegyből kell álljon.', + 'digits_between' => 'A(z) :attribute hosszának :min és :max számjegy között kell lennie.', + 'email' => 'A(z) :attribute érvényes email cím kell legyen.', + 'ends_with' => 'A(z) :attribute értékének a következők egyikével kell végződnie: :values', + 'file' => 'A(z) :attribute érvényes fájl kell legyen.', + 'filled' => 'A(z) :attribute mező kötelező.', 'gt' => [ - 'numeric' => ':attribute nagyobb kell, hogy legyen, mint :value.', - 'file' => ':attribute nagyobb kell, hogy legyen, mint :value kilobájt.', - 'string' => ':attribute nagyobb kell legyen mint :value karakter.', - 'array' => ':attribute több, mint :value elemet kell, hogy tartalmazzon.', + 'numeric' => 'A(z) :attribute nagyobb kell, hogy legyen, mint :value.', + 'file' => 'A(z) :attribute nagyobb kell, hogy legyen, mint :value kB.', + 'string' => 'A(z) :attribute hosszabb kell legyen mint :value karakter.', + 'array' => 'A(z) :attribute több, mint :value elemet kell, hogy tartalmazzon.', ], 'gte' => [ - 'numeric' => ':attribute attribútumnak :value értéknél nagyobbnak vagy vele egyenlőnek kell lennie.', - 'file' => 'A(z) :attribute mérete nem lehet kevesebb, mint :value kilobájt.', - 'string' => 'A(z) :attribute nagyobbnak, vagy egyenlőnek kell lennie, mint a :value karakter.', - 'array' => 'A(z) :attribute rendelkezzen :value vagy több elemmel.', + 'numeric' => 'A(z) :attribute számnak :value értéknél nagyobbnak vagy vele egyenlőnek kell lennie.', + 'file' => 'A(z) :attribute mérete nem lehet kevesebb, mint :value kB.', + 'string' => 'A(z) :attribute szövegnek legalább :value karakter hosszúnak kell lennie.', + 'array' => 'A(z) :attribute tömbnek :value vagy több elemmel kell rendelkeznie.', ], 'exists' => 'A kiválasztott :attribute érvénytelen.', - 'image' => ':attribute kép kell legyen.', - 'image_extension' => 'A :attribute kép kiterjesztése érvényes és támogatott kell legyen.', + 'image' => 'A(z) :attribute kép kell legyen.', + 'image_extension' => 'A(z) :attribute kép kiterjesztése érvényes és támogatott kell legyen.', 'in' => 'A kiválasztott :attribute érvénytelen.', - 'integer' => ':attribute egész szám kell legyen.', - 'ip' => ':attribute érvényes IP cím kell legyen.', - 'ipv4' => 'A(z) :attribute érvényes IPv4 címnek kell lennie.', - 'ipv6' => 'A(z) :attribute érvényes IPv6 címnek kell lennie.', - 'json' => 'A(z) :attribute érvényes JSON stringnek kell lennie.', + 'integer' => 'A(z) :attribute egész szám kell legyen.', + 'ip' => 'A(z) :attribute érvényes IP cím kell legyen.', + 'ipv4' => 'A(z) :attribute érvényes IPv4 cím kell legyen.', + 'ipv6' => 'A(z) :attribute érvényes IPv6 cím kell legyen.', + 'json' => 'A(z) :attribute érvényes JSON szöveg kell legyen.', 'lt' => [ - 'numeric' => 'A(z) :attribute kisebb kell, hogy legyen, mint :value.', - 'file' => 'A(z) :attribute kevesebbnek kell lennie, mint :value kilobájt.', + 'numeric' => 'A(z) :attribute szám kisebb kell, hogy legyen, mint :value.', + 'file' => 'A(z) :attribute fájlnak kisebbnek kell lennie, mint :value kB.', 'string' => 'A(z) :attribute rövidebb kell, hogy legyen, mint :value karakter.', 'array' => 'A(z) :attribute kevesebb, mint :value elemet kell, hogy tartalmazzon.', ], @@ -69,47 +69,47 @@ return [ ], 'max' => [ 'numeric' => ':attribute nem lehet nagyobb mint :max.', - 'file' => ':attribute nem lehet nagyobb mint :max kilobájt.', - 'string' => ':attribute nem lehet nagyobb mint :max karakter.', - 'array' => ':attribute mennyisége nem lehet több mint :max elem.', + 'file' => 'A(z) :attribute nem lehet nagyobb, mint :max kB.', + 'string' => 'A(z) :attribute nem lehet hosszabb, mint :max karakter.', + 'array' => 'A(z) :attribute nem tartalmazhat több, mint :max elemet.', ], - 'mimes' => 'A :attribute típusa csak :values lehet.', + 'mimes' => 'A(z) :attribute típusa csak :values lehet.', 'min' => [ - 'numeric' => ':attribute legalább :min kell legyen.', - 'file' => ':attribute legalább :min kilobájt kell legyen.', - 'string' => ':attribute legalább :min karakter kell legyen.', - 'array' => ':attribute legalább :min elem kell legyen.', + 'numeric' => 'A(z) :attribute legalább :min kell legyen.', + 'file' => 'A(z) :attribute legalább :min kB kell legyen.', + 'string' => 'A(z) :attribute legalább :min karakter kell legyen.', + 'array' => 'A(z) :attribute legalább :min elemet kell tartalmazzon.', ], 'not_in' => 'A kiválasztott :attribute érvénytelen.', - 'not_regex' => ':attribute formátuma érvénytelen.', - 'numeric' => ':attribute szám kell legyen.', - 'regex' => ':attribute formátuma érvénytelen.', - 'required' => ':attribute mező kötelező.', - 'required_if' => ':attribute mező kötelező ha :other értéke :value.', - 'required_with' => ':attribute mező kötelező ha :values be van állítva.', - 'required_with_all' => ':attribute mező kötelező ha van :value.', - 'required_without' => ':attribute mező kötelező ha :values nincs beállítva.', - 'required_without_all' => ':attribute mező kötelező ha egyik :values sincs beállítva.', - 'same' => ':attribute és :other értékének egyeznie kell.', + 'not_regex' => 'A(z) :attribute formátuma érvénytelen.', + 'numeric' => 'A(z) :attribute szám kell legyen.', + 'regex' => 'A(z) :attribute formátuma érvénytelen.', + 'required' => 'A(z) :attribute mező kötelező.', + 'required_if' => 'A(z) :attribute mező kötelező ha :other értéke :value.', + 'required_with' => 'A(z) :attribute mező kötelező ha :values be van állítva.', + 'required_with_all' => 'A(z) :attribute mező kötelező ha :values be van állítva.', + 'required_without' => 'A(z) :attribute mező kötelező ha :values nincs beállítva.', + 'required_without_all' => 'A(z) :attribute mező kötelező ha egyik :values sincs beállítva.', + 'same' => 'A(z) :attribute és :other értékének egyeznie kell.', 'safe_url' => 'Előfordulhat, hogy a megadott link nem biztonságos.', 'size' => [ - 'numeric' => ':attribute :size méretű kell legyen.', - 'file' => ':attribute :size kilobájt méretű kell legyen.', - 'string' => ':attribute :size karakter kell legyen.', - 'array' => ':attribute : size elemet kell tartalmazzon.', + 'numeric' => 'A(z) :attribute :size méretű kell legyen.', + 'file' => 'A(z) :attribute :size kB méretű kell legyen.', + 'string' => 'A(z) :attribute :size karakter kell legyen.', + 'array' => 'A(z) :attribute :size elemet kell tartalmazzon.', ], - 'string' => ':attribute karaktersorozatnak kell legyen.', - 'timezone' => ':attribute érvényes zóna kell legyen.', + 'string' => 'A(z) :attribute szöveg kell legyen.', + 'timezone' => 'A(z) :attribute érvényes időzóna kell legyen.', 'totp' => 'A megadott kód érvénytelen vagy lejárt.', - 'unique' => ':attribute már elkészült.', - 'url' => ':attribute formátuma érvénytelen.', - 'uploaded' => 'A fájlt nem lehet feltölteni. A kiszolgáló nem fogad el ilyen méretű fájlokat.', + 'unique' => 'A(z) :attribute már foglalt.', + 'url' => 'A(z) :attribute formátuma érvénytelen.', + 'uploaded' => 'A fájlt nem lehet feltölteni. A szerver nem fogad el ilyen méretű fájlokat.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => 'A(z) :attribute egy a ZIP fájlban található fájlra kell, hogy hivatkozzon', + 'zip_file_size' => 'A(z) :attribute fájl nem haladhatja meg a :size MB méretet.', + 'zip_file_mime' => 'A(z) :attribute egy :validTypes típusú fájlra kell, hogy hivatkozzon, a :foundType helyett.', + 'zip_model_expected' => 'Adat objektum helyett ":type" lett találva.', + 'zip_unique' => 'A(z) :attribute egyedi kell hogy legyen a ZIP fájlban az adott objektum típushoz.', // Custom validation lines 'custom' => [ diff --git a/lang/id/activities.php b/lang/id/activities.php index 6e1583a94..db32fcf64 100644 --- a/lang/id/activities.php +++ b/lang/id/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Pengguna berhasil diperbarui', 'user_delete' => 'pengguna yang dihapus', 'user_delete_notification' => 'Pengguna berhasil dihapus', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API token yang dibuat', diff --git a/lang/id/auth.php b/lang/id/auth.php index 6af60ca80..a5653ede6 100644 --- a/lang/id/auth.php +++ b/lang/id/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Kredensial tidak cocok dengan catatan kami.', 'throttle' => 'Terlalu banyak upaya masuk. Silahkan mencoba lagi dalam :seconds detik.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Daftar', diff --git a/lang/id/entities.php b/lang/id/entities.php index d978a3d19..a423ef39c 100644 --- a/lang/id/entities.php +++ b/lang/id/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Halaman Tag', 'chapter_tags' => 'Bab Tag', 'book_tags' => 'Tag Buku', diff --git a/lang/id/settings.php b/lang/id/settings.php index fe3289ffa..5d7853147 100644 --- a/lang/id/settings.php +++ b/lang/id/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Buat Token API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/is/activities.php b/lang/is/activities.php index e70aa2567..7056e633c 100644 --- a/lang/is/activities.php +++ b/lang/is/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Tókst að uppfæra notanda', 'user_delete' => 'eyddur notandi', 'user_delete_notification' => 'Tókst að eyða notanda', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API token búið til', diff --git a/lang/is/auth.php b/lang/is/auth.php index c5e7ce4f1..9b779aec7 100644 --- a/lang/is/auth.php +++ b/lang/is/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Þeesi auðkenning er ekki á skrá.', 'throttle' => 'Of margar tilraunir til innskráningar. Reyndu aftur eftir :seconds sekúndur.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Nýskrá', diff --git a/lang/is/entities.php b/lang/is/entities.php index 0d9a6fc1e..8735a18ae 100644 --- a/lang/is/entities.php +++ b/lang/is/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/is/settings.php b/lang/is/settings.php index 877e0dfa8..cabe31917 100644 --- a/lang/is/settings.php +++ b/lang/is/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Búa til API tóka', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/it/activities.php b/lang/it/activities.php index d682ccca6..e0dba5403 100644 --- a/lang/it/activities.php +++ b/lang/it/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Utente aggiornato con successo', 'user_delete' => 'ha eliminato un utente', 'user_delete_notification' => 'Utente rimosso con successo', + 'user_mfa_reset' => 'resetta MFA per l\'utente', + 'user_mfa_reset_notification' => 'Metodi di autenticazione multi-fattore reimpostati', // API Tokens 'api_token_create' => 'ha creato un token API', diff --git a/lang/it/auth.php b/lang/it/auth.php index 9191ecba7..3dd5e1ec6 100644 --- a/lang/it/auth.php +++ b/lang/it/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Credenziali errate.', 'throttle' => 'Troppi tentativi di login. Riprova in :seconds secondi.', + 'mfa_throttle' => 'Troppi tentativi di verifica multi-fattore. Riprova tra :seconds secondi.', // Login & Register 'sign_up' => 'Registrati', diff --git a/lang/it/entities.php b/lang/it/entities.php index 1cce5fcd5..89e1cd988 100644 --- a/lang/it/entities.php +++ b/lang/it/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Attiva/disattiva barra laterale', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Tag pagina', 'chapter_tags' => 'Tag capitolo', 'book_tags' => 'Tag libro', diff --git a/lang/it/settings.php b/lang/it/settings.php index 9a272db33..3b8681840 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -207,7 +207,7 @@ return [ 'role_all' => 'Tutti', 'role_own' => 'Propri', 'role_controlled_by_asset' => 'Controllato dall\'entità in cui sono caricati', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Controllato dai permessi di cancellazione della pagina', 'role_save' => 'Salva ruolo', 'role_users' => 'Utenti in questo ruolo', 'role_users_none' => 'Nessun utente assegnato a questo ruolo', @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Imposta l\'autenticazione multi-fattore come misura di sicurezza aggiuntiva per il tuo account.', 'users_mfa_x_methods' => ':count metodo configurato|:count metodi configurati', 'users_mfa_configure' => 'Configura metodi', + 'users_mfa_reset' => 'Reimposta Metodi Di Autenticazione Multi-Fattore', + 'users_mfa_reset_desc' => 'Questo ripristinerà e cancellerà tutti i metodi di autenticazione multi-fattore configurati per questo utente. Se l\'autenticazione multi-fattore è richiesta da uno qualsiasi dei loro ruoli, sarà richiesto loro di configurare nuovi metodi al loro prossimo accesso.', + 'users_mfa_reset_confirm' => 'Sei sicuro di voler resettare l\'autenticazione multi-fattore per questo utente?', // API Tokens 'user_api_token_create' => 'Crea token API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Sloveno', 'sl' => 'Sloveno', 'sv' => 'Svedese', + 'th' => 'ภาษาไทย', 'tr' => 'Turco', 'uk' => 'Ucraino', 'uz' => 'O‘zbekcha', diff --git a/lang/ja/activities.php b/lang/ja/activities.php index 6f55098bb..25ded1d49 100644 --- a/lang/ja/activities.php +++ b/lang/ja/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'ユーザーを更新しました', 'user_delete' => 'がユーザを削除', 'user_delete_notification' => 'ユーザーを削除しました', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'がAPIトークンを作成', diff --git a/lang/ja/auth.php b/lang/ja/auth.php index 077c31ac8..b4ee5f4ac 100644 --- a/lang/ja/auth.php +++ b/lang/ja/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'この資格情報は登録されていません。', 'throttle' => 'ログイン試行回数が制限を超えました。:seconds秒後に再試行してください。', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => '新規登録', diff --git a/lang/ja/entities.php b/lang/ja/entities.php index 57703997f..eaaddf5b3 100644 --- a/lang/ja/entities.php +++ b/lang/ja/entities.php @@ -332,6 +332,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'サイドバーの切り替え', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'タグ', 'chapter_tags' => 'チャプターのタグ', 'book_tags' => 'ブックのタグ', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index a3cbf696a..95f33f340 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'アカウントのセキュリティを強化するために、多要素認証を設定してください。', 'users_mfa_x_methods' => ':count個の手段が設定されています|:count個の手段が設定されています', 'users_mfa_configure' => '手段を設定', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'APIトークンの作成', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ka/activities.php b/lang/ka/activities.php index 35730fd77..07304a562 100644 --- a/lang/ka/activities.php +++ b/lang/ka/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/ka/auth.php b/lang/ka/auth.php index 57f0cb5c6..47be4ea72 100644 --- a/lang/ka/auth.php +++ b/lang/ka/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'These credentials do not match our records.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Sign up', diff --git a/lang/ka/entities.php b/lang/ka/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/ka/entities.php +++ b/lang/ka/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/ka/settings.php b/lang/ka/settings.php index 3937c650f..d03024a89 100644 --- a/lang/ka/settings.php +++ b/lang/ka/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ko/activities.php b/lang/ko/activities.php index 061b7fb1e..e011523fa 100644 --- a/lang/ko/activities.php +++ b/lang/ko/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => '사용자가 업데이트되었습니다', 'user_delete' => '사용자 삭제', 'user_delete_notification' => '사용자가 삭제되었습니다', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => '생성된 API 토큰', diff --git a/lang/ko/auth.php b/lang/ko/auth.php index af9129f14..9e93c8846 100644 --- a/lang/ko/auth.php +++ b/lang/ko/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => '자격 증명이 기록과 일치하지 않습니다.', 'throttle' => '로그인 시도가 너무 많습니다. :seconds초 후에 다시 시도해주세요.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => '가입', diff --git a/lang/ko/entities.php b/lang/ko/entities.php index c54d6c7dc..838aebd7a 100644 --- a/lang/ko/entities.php +++ b/lang/ko/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => '사이드바 토글', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => '페이지 태그', 'chapter_tags' => '장 태그', 'book_tags' => '책 태그', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 9aabe5c2a..90d501a7c 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => '추가 보안 계층으로 다중 인증을 설정합니다.', 'users_mfa_x_methods' => ':count 설정함|:count 설정함', 'users_mfa_configure' => '설정', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'API 토큰 만들기', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ku/activities.php b/lang/ku/activities.php index 4362fc029..e9344a3d4 100644 --- a/lang/ku/activities.php +++ b/lang/ku/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/ku/auth.php b/lang/ku/auth.php index 369e06f93..5597683ae 100644 --- a/lang/ku/auth.php +++ b/lang/ku/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'ئەم بەکارهێنەرە نەدۆزرایەوە.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Sign up', diff --git a/lang/ku/entities.php b/lang/ku/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/ku/entities.php +++ b/lang/ku/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/ku/settings.php b/lang/ku/settings.php index 3937c650f..d03024a89 100644 --- a/lang/ku/settings.php +++ b/lang/ku/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/lt/activities.php b/lang/lt/activities.php index a808171bb..0d08d41ea 100644 --- a/lang/lt/activities.php +++ b/lang/lt/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/lt/auth.php b/lang/lt/auth.php index a2f7f4668..8a91f2104 100644 --- a/lang/lt/auth.php +++ b/lang/lt/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Šie įgaliojimai neatitinka mūsų įrašų.', 'throttle' => 'Per daug prisijungimo bandymų. Prašome pabandyti dar kartą po :seconds sekundžių.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Užsiregistruoti', diff --git a/lang/lt/entities.php b/lang/lt/entities.php index f6610a22a..6bf86635a 100644 --- a/lang/lt/entities.php +++ b/lang/lt/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Puslapio žymos', 'chapter_tags' => 'Skyriaus žymos', 'book_tags' => 'Knygos žymos', diff --git a/lang/lt/settings.php b/lang/lt/settings.php index 23dd38d50..96ee2beba 100644 --- a/lang/lt/settings.php +++ b/lang/lt/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Sukurti API sąsajos prieigos raktą', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/lv/activities.php b/lang/lv/activities.php index 38d9ae217..8f9025170 100644 --- a/lang/lv/activities.php +++ b/lang/lv/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Lietotājs veiksmīgi atjaunināts', 'user_delete' => 'dzēsa lietotāju', 'user_delete_notification' => 'Lietotājs veiksmīgi dzēsts', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'izveidoja API žetonu', diff --git a/lang/lv/auth.php b/lang/lv/auth.php index 71b92ac01..462ce110f 100644 --- a/lang/lv/auth.php +++ b/lang/lv/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Šie reģistrācijas dati neatbilst mūsu ierakstiem.', 'throttle' => 'Pārāk daudz pieteikšanās mēģinājumu. Lūdzu, mēģiniet vēlreiz pēc :seconds seconds.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Reģistrēties', diff --git a/lang/lv/entities.php b/lang/lv/entities.php index f0f32ab21..dccab4bd1 100644 --- a/lang/lv/entities.php +++ b/lang/lv/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Pārslēgt sānjoslu', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Lapas birkas', 'chapter_tags' => 'Nodaļas birkas', 'book_tags' => 'Grāmatas birkas', diff --git a/lang/lv/settings.php b/lang/lv/settings.php index 0e11ebd65..886e0ef0a 100644 --- a/lang/lv/settings.php +++ b/lang/lv/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Iestati vairākfaktoru autentifikāciju kā papildus drošības līmeni tavam lietotāja kontam.', 'users_mfa_x_methods' => ':count metode iestatīta|:count metodes iestatītas', 'users_mfa_configure' => 'Iestatīt metodes', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Izveidot API žetonu', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/nb/activities.php b/lang/nb/activities.php index fd4de5321..771f5438b 100644 --- a/lang/nb/activities.php +++ b/lang/nb/activities.php @@ -100,6 +100,8 @@ return [ 'user_update_notification' => 'Brukeren ble oppdatert', 'user_delete' => 'slettet bruker', 'user_delete_notification' => 'Brukeren ble fjernet', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'opprettet API-nøkkel', diff --git a/lang/nb/auth.php b/lang/nb/auth.php index 125e6daf5..f345f691a 100644 --- a/lang/nb/auth.php +++ b/lang/nb/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Disse detaljene samsvarer ikke med det vi har på bok.', 'throttle' => 'For mange forsøk, prøv igjen om :seconds sekunder.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrer deg', diff --git a/lang/nb/entities.php b/lang/nb/entities.php index a67c35e17..0e9070e53 100644 --- a/lang/nb/entities.php +++ b/lang/nb/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Bytt sidestolpe', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sidemerker', 'chapter_tags' => 'Kapittelmerker', 'book_tags' => 'Bokmerker', diff --git a/lang/nb/settings.php b/lang/nb/settings.php index 5e779beea..1cc5d8e02 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som et ekstra lag med sikkerhet for din konto.', 'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert', 'users_mfa_configure' => 'Konfigurer metoder', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Opprett API-nøkkel', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ne/activities.php b/lang/ne/activities.php index 4b4edf7be..24c2b948a 100644 --- a/lang/ne/activities.php +++ b/lang/ne/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'प्रयोगकर्ता सफलतापूर्वक अद्यावधिक गरियो', 'user_delete' => 'प्रयोगकर्ता हटाइयो', 'user_delete_notification' => 'प्रयोगकर्ता सफलतापूर्वक हटाइयो', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API टोकन सिर्जना गरियो', diff --git a/lang/ne/auth.php b/lang/ne/auth.php index cd875a226..378f2dea2 100644 --- a/lang/ne/auth.php +++ b/lang/ne/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'यी प्रमाणिकरण जानकारी हाम्रो अभिलेखसँग मेल खाँदैन।', 'throttle' => 'लगइन प्रयासहरूको संख्या धेरै भएको छ। कृपया :seconds सेकेन्ड पछि पुनः प्रयास गर्नुहोस्।', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'साइन अप गर्नुहोस्', diff --git a/lang/ne/entities.php b/lang/ne/entities.php index 4d9f78ea5..f51c265c5 100644 --- a/lang/ne/entities.php +++ b/lang/ne/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'साइडबार टगल गर्नुहोस्', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'पाना ट्यागहरू', 'chapter_tags' => 'अध्याय ट्यागहरू', 'book_tags' => 'पुस्तक ट्यागहरू', diff --git a/lang/ne/settings.php b/lang/ne/settings.php index f52bcd420..549a4dc8b 100644 --- a/lang/ne/settings.php +++ b/lang/ne/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'तपाईंको प्रयोगकर्ता खाताको लागि थप सुरक्षा तहको रूपमा बहु-फ्याक्टर प्रमाणीकरण सेटअप गर्नुहोस्।', 'users_mfa_x_methods' => ':count विधि सेटअप गरिएको|:count विधिहरू सेटअप गरिएको', 'users_mfa_configure' => 'विधिहरू सेटअप गर्नुहोस्', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'API टोकन सिर्जना गर्नुहोस्', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/nl/activities.php b/lang/nl/activities.php index 230c7a58b..55356966d 100644 --- a/lang/nl/activities.php +++ b/lang/nl/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Gebruiker succesvol bijgewerkt', 'user_delete' => 'verwijderde gebruiker', 'user_delete_notification' => 'Gebruiker succesvol verwijderd', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'API-token aangemaakt', diff --git a/lang/nl/auth.php b/lang/nl/auth.php index 12c6b6c7b..49d04dd4f 100644 --- a/lang/nl/auth.php +++ b/lang/nl/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Deze inloggegevens zijn niet bij ons bekend.', 'throttle' => 'Te veel inlogpogingen! Probeer het opnieuw na :seconds seconden.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registreer', diff --git a/lang/nl/entities.php b/lang/nl/entities.php index c39beef2d..e90aff575 100644 --- a/lang/nl/entities.php +++ b/lang/nl/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Zijbalk Tonen/Verbergen', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Pagina Labels', 'chapter_tags' => 'Hoofdstuk Labels', 'book_tags' => 'Boek Labels', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index b82c52deb..9b0fe5ad1 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Stel meervoudige verificatie in als extra beveiligingslaag voor je gebruikersaccount.', 'users_mfa_x_methods' => ':count methode geconfigureerd|:count methoden geconfigureerd', 'users_mfa_configure' => 'Configureer methoden', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'API-token aanmaken', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky (Slowaaks)', 'sl' => 'Slovenščina (Sloveens)', 'sv' => 'Svenska (Zweeds)', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe (Turks)', 'uk' => 'Українська (Oekraïens)', 'uz' => 'Oezbeeks', diff --git a/lang/nn/activities.php b/lang/nn/activities.php index f87aadc66..fa934bf45 100644 --- a/lang/nn/activities.php +++ b/lang/nn/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Brukaren vart oppdatert', 'user_delete' => 'sletta brukar', 'user_delete_notification' => 'Brukaren vart fjerna', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'opprett API-nøkkel', diff --git a/lang/nn/auth.php b/lang/nn/auth.php index 5da686eaa..7e53aaee0 100644 --- a/lang/nn/auth.php +++ b/lang/nn/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Desse detaljane samsvarar ikkje med det me har på bok.', 'throttle' => 'For mange forsøk, prøv på nytt om :seconds sekunder.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrer deg', diff --git a/lang/nn/entities.php b/lang/nn/entities.php index 4f79f3d75..878e2ef0f 100644 --- a/lang/nn/entities.php +++ b/lang/nn/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Vis/gøym sidepanelet', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sidemerker', 'chapter_tags' => 'Kapittelmerker', 'book_tags' => 'Bokmerker', diff --git a/lang/nn/settings.php b/lang/nn/settings.php index b6c322c9c..087098332 100644 --- a/lang/nn/settings.php +++ b/lang/nn/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Konfigurer flerfaktorautentisering som eit ekstra lag med tryggleik for din konto.', 'users_mfa_x_methods' => ':count metode konfigurert|:count metoder konfigurert', 'users_mfa_configure' => 'Konfigurer metoder', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Opprett API-nøkkel', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/pl/activities.php b/lang/pl/activities.php index 323b7035e..00687187d 100644 --- a/lang/pl/activities.php +++ b/lang/pl/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Użytkownik zaktualizowany pomyślnie', 'user_delete' => 'usunięto użytkownika', 'user_delete_notification' => 'Użytkownik pomyślnie usunięty', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'utworzono token API', diff --git a/lang/pl/auth.php b/lang/pl/auth.php index 445f8198a..7b4997a6c 100644 --- a/lang/pl/auth.php +++ b/lang/pl/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Wprowadzone poświadczenia są nieprawidłowe.', 'throttle' => 'Zbyt wiele prób logowania. Spróbuj ponownie za :seconds s.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Zarejestruj się', diff --git a/lang/pl/entities.php b/lang/pl/entities.php index 88e4344de..ea0ac7367 100644 --- a/lang/pl/entities.php +++ b/lang/pl/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Przełącz pasek boczny', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Tagi strony', 'chapter_tags' => 'Tagi rozdziału', 'book_tags' => 'Tagi książki', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index 98201406e..bfa6339d5 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Skonfiguruj uwierzytelnianie wieloskładnikowe jako dodatkową warstwę bezpieczeństwa dla swojego konta użytkownika.', 'users_mfa_x_methods' => ':count metoda skonfigurowana|:count metody skonfigurowane', 'users_mfa_configure' => 'Konfiguruj metody', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Utwórz klucz API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/pt/activities.php b/lang/pt/activities.php index 33d9e2ca6..e2ed42c71 100644 --- a/lang/pt/activities.php +++ b/lang/pt/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Utilizador atualizado com sucesso', 'user_delete' => 'utilizador eliminado', 'user_delete_notification' => 'Utilizador removido com sucesso', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'token API criado', diff --git a/lang/pt/auth.php b/lang/pt/auth.php index 8807e1a87..453b20168 100644 --- a/lang/pt/auth.php +++ b/lang/pt/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Estas credenciais não coincidem com os nossos registos.', 'throttle' => 'Demasiadas tentativas de acesso. Tente novamente em :seconds segundos.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registar', diff --git a/lang/pt/entities.php b/lang/pt/entities.php index 709b8466a..3bdaeb12c 100644 --- a/lang/pt/entities.php +++ b/lang/pt/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Alternar barra lateral', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Etiquetas de Página', 'chapter_tags' => 'Etiquetas do Capítulo', 'book_tags' => 'Etiquetas do Livro', diff --git a/lang/pt/settings.php b/lang/pt/settings.php index 47fecbcbf..1488db87a 100644 --- a/lang/pt/settings.php +++ b/lang/pt/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/pt_BR/activities.php b/lang/pt_BR/activities.php index 2a2d173c7..e9069564f 100644 --- a/lang/pt_BR/activities.php +++ b/lang/pt_BR/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Usuário atualizado com sucesso', 'user_delete' => 'usuário excluído', 'user_delete_notification' => 'Usuário removido com sucesso', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'token de API criado', diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index 0580542b4..dfcc47da6 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'As credenciais fornecidas não puderam ser validadas em nossos registros.', 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Criar Conta', diff --git a/lang/pt_BR/entities.php b/lang/pt_BR/entities.php index 0211f45fa..7fa0b50c1 100644 --- a/lang/pt_BR/entities.php +++ b/lang/pt_BR/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => '', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Marcadores de Página', 'chapter_tags' => 'Marcadores de Capítulo', 'book_tags' => 'Marcadores de Livro', diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php index 9b7c6a7d4..8129637c8 100644 --- a/lang/pt_BR/settings.php +++ b/lang/pt_BR/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'A autenticação multi-fator adiciona outra camada de segurança à sua conta.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ro/activities.php b/lang/ro/activities.php index eed5cc083..c00976cf7 100644 --- a/lang/ro/activities.php +++ b/lang/ro/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Utilizator actualizat cu succes', 'user_delete' => 'utilizator șters', 'user_delete_notification' => 'Utilizator eliminat cu succes', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/ro/auth.php b/lang/ro/auth.php index e02eeb4c1..73e165381 100644 --- a/lang/ro/auth.php +++ b/lang/ro/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Aceste credenţiale nu se potrivesc cu înregistrările noastre.', 'throttle' => 'Prea multe încercări de conectare. Vă rugăm să încercați din nou în :seconds secunde.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Inregistrează-te', diff --git a/lang/ro/entities.php b/lang/ro/entities.php index aa18d09fb..18ffd30c9 100644 --- a/lang/ro/entities.php +++ b/lang/ro/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Comutați bara laterală', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Etichete pagină', 'chapter_tags' => 'Etichete capitol', 'book_tags' => 'Etichete carte', diff --git a/lang/ro/settings.php b/lang/ro/settings.php index 117c4ca61..bbbaa34d9 100644 --- a/lang/ro/settings.php +++ b/lang/ro/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Configurare autentificarea multi-factor ca un nivel suplimentar de securitate pentru contul tău de utilizator.', 'users_mfa_x_methods' => ':count metodă configurată|:count metode configurate', 'users_mfa_configure' => 'Configurare metode', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Creare token API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/ru/activities.php b/lang/ru/activities.php index d9ac1685b..9d08d1d4a 100644 --- a/lang/ru/activities.php +++ b/lang/ru/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Пользователь успешно обновлен', 'user_delete' => 'удалил пользователя', 'user_delete_notification' => 'Пользователь успешно удален', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'создан API токен', diff --git a/lang/ru/auth.php b/lang/ru/auth.php index d39da119d..433bfbe5f 100644 --- a/lang/ru/auth.php +++ b/lang/ru/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Введенные вами данные не найдены в нашей базе.', 'throttle' => 'Слишком много попыток входа. Пожалуйста, повторите попытку через :seconds секунд.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Регистрация', diff --git a/lang/ru/entities.php b/lang/ru/entities.php index 28d096fe8..d3c4230ac 100644 --- a/lang/ru/entities.php +++ b/lang/ru/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Переключить боковую панель', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Теги страницы', 'chapter_tags' => 'Теги главы', 'book_tags' => 'Теги книги', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 7bf5832a3..4fdd5784a 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Многофакторная аутентификация повышает степень безопасности вашей учетной записи.', 'users_mfa_x_methods' => 'методов настроено :count|методов сконфигурировано :count', 'users_mfa_configure' => 'Настройка методов', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Создать токен', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sk/activities.php b/lang/sk/activities.php index 65542fbe2..b9a532e2e 100644 --- a/lang/sk/activities.php +++ b/lang/sk/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Používateľ úspešne upravený', 'user_delete' => 'odstránený používateľ', 'user_delete_notification' => 'Používateľ úspešne zmazaný', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/sk/auth.php b/lang/sk/auth.php index 11ccdc889..52f34f68f 100644 --- a/lang/sk/auth.php +++ b/lang/sk/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Tieto údaje sa nezhodujú s našimi záznamami.', 'throttle' => 'Priveľa pokusov o prihlásenie. Skúste znova o :seconds sekúnd.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registrácia', diff --git a/lang/sk/entities.php b/lang/sk/entities.php index 48b662bf3..697aef0b8 100644 --- a/lang/sk/entities.php +++ b/lang/sk/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Štítky stránok', 'chapter_tags' => 'Štítky kapitol', 'book_tags' => 'Štítky kníh', diff --git a/lang/sk/settings.php b/lang/sk/settings.php index e18801ff4..e76fefb98 100644 --- a/lang/sk/settings.php +++ b/lang/sk/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Pre vyššiu úroveň bezpečnosti si nastavte viacúrovňové prihlasovanie.', 'users_mfa_x_methods' => ':count nakonfigurované metódy|:count nakonfigurovaných metód', 'users_mfa_configure' => 'Konfigurovať metódy', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Vytvoriť API token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sl/activities.php b/lang/sl/activities.php index 117c73ca1..3b8977c2f 100644 --- a/lang/sl/activities.php +++ b/lang/sl/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Uporabnik uspešno posodobljen', 'user_delete' => 'uporabnik izbrisan', 'user_delete_notification' => 'Uporabnik uspešno izbrisan', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'ustvarjen žeton API', diff --git a/lang/sl/auth.php b/lang/sl/auth.php index e06d823e7..46c6a565d 100644 --- a/lang/sl/auth.php +++ b/lang/sl/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Poverilnice se ne ujemajo s podatki v naši bazi.', 'throttle' => 'Prekoračili ste število možnih prijav. Poskusite znova čez :seconds sekund.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Registracija', diff --git a/lang/sl/entities.php b/lang/sl/entities.php index 86a43132e..0efbc6d17 100644 --- a/lang/sl/entities.php +++ b/lang/sl/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Oznake strani', 'chapter_tags' => 'Oznake poglavja', 'book_tags' => 'Oznake knjige', diff --git a/lang/sl/settings.php b/lang/sl/settings.php index 87c1e8e6d..9bd63a0e8 100644 --- a/lang/sl/settings.php +++ b/lang/sl/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Ustvari žeton', @@ -365,6 +368,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sq/activities.php b/lang/sq/activities.php index 9296d1759..2d41a1121 100644 --- a/lang/sq/activities.php +++ b/lang/sq/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Përdoruesi u përditësua me sukses', 'user_delete' => 'fshi përdorues', 'user_delete_notification' => 'Përdoruesi u fshi me sukses', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'Krijoi token API', diff --git a/lang/sq/auth.php b/lang/sq/auth.php index 6fdc4ee4a..d5bf7acfb 100644 --- a/lang/sq/auth.php +++ b/lang/sq/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Këto kredenciale nuk përputhen me të dhënat tona.', 'throttle' => 'Shumë përpjekje për hyrje. Ju lutemi provoni përsëri në :seconds sekonda.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Regjistrohu', diff --git a/lang/sq/entities.php b/lang/sq/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/sq/entities.php +++ b/lang/sq/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/sq/settings.php b/lang/sq/settings.php index 3937c650f..d03024a89 100644 --- a/lang/sq/settings.php +++ b/lang/sq/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sr/activities.php b/lang/sr/activities.php index b4aa5e2a7..555d01adc 100644 --- a/lang/sr/activities.php +++ b/lang/sr/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Корисник је успешно ажуриран', 'user_delete' => 'избрисан корисника', 'user_delete_notification' => 'Корисник је успешно уклоњен', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'креирао апи токен', diff --git a/lang/sr/auth.php b/lang/sr/auth.php index 0100e666f..96169fe2b 100644 --- a/lang/sr/auth.php +++ b/lang/sr/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Ови акредитиви се не поклапају са нашом евиденцијом.', 'throttle' => 'Превише покушаја пријаве. Покушајте поново за :seconds секунди.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Региструј се', diff --git a/lang/sr/entities.php b/lang/sr/entities.php index 8f9c40e91..cd4e732f4 100644 --- a/lang/sr/entities.php +++ b/lang/sr/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/sr/settings.php b/lang/sr/settings.php index 3453bc344..143fdaef1 100644 --- a/lang/sr/settings.php +++ b/lang/sr/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sv/activities.php b/lang/sv/activities.php index 3acf6d4ff..c501c6752 100644 --- a/lang/sv/activities.php +++ b/lang/sv/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Användaren har uppdaterats', 'user_delete' => 'raderad användare', 'user_delete_notification' => 'Användaren har tagits bort', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'skapade API-token', diff --git a/lang/sv/auth.php b/lang/sv/auth.php index c9feb8312..6c94f21f9 100644 --- a/lang/sv/auth.php +++ b/lang/sv/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Uppgifterna stämmer inte överens med våra register.', 'throttle' => 'För många inloggningsförsök. Prova igen om :seconds sekunder.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Skapa konto', diff --git a/lang/sv/entities.php b/lang/sv/entities.php index 94fc23f50..1df816849 100644 --- a/lang/sv/entities.php +++ b/lang/sv/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Visa/Dölj sidopanel', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sidtaggar', 'chapter_tags' => 'Kapiteltaggar', 'book_tags' => 'Boktaggar', diff --git a/lang/sv/settings.php b/lang/sv/settings.php index 47f602b8d..e900a86a3 100644 --- a/lang/sv/settings.php +++ b/lang/sv/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Konfigurera multifaktorsautentisering som ett extra skydd för ditt konto.', 'users_mfa_x_methods' => ':count metod konfigurerad|:count metoder konfigurerade', 'users_mfa_configure' => 'Konfigurera metoder', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Skapa API-nyckel', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenska', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/th/activities.php b/lang/th/activities.php index 6d84813d6..0ea18ca0a 100644 --- a/lang/th/activities.php +++ b/lang/th/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'แก้ไขผู้ใช้สำเร็จแล้ว', 'user_delete' => 'ลบผู้ใช้', 'user_delete_notification' => 'ลบผู้ใช้สำเร็จแล้ว', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'สร้าง API Token', diff --git a/lang/th/auth.php b/lang/th/auth.php index c00b29bcd..ca24b593c 100644 --- a/lang/th/auth.php +++ b/lang/th/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'ข้อมูลประจำตัวไม่ตรงกับที่มีในระบบ', 'throttle' => 'เข้าสู่ระบบล้มเหลวหลายครั้งเกินไป กรุณาลองใหม่ในอีก :seconds วินาที', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'สมัครสมาชิก', diff --git a/lang/th/entities.php b/lang/th/entities.php index 1f0f30ec6..bda46b62c 100644 --- a/lang/th/entities.php +++ b/lang/th/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'แสดง/ซ่อนแถบด้านข้าง', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'แท็กหน้า', 'chapter_tags' => 'แท็กบท', 'book_tags' => 'แท็กหนังสือ', diff --git a/lang/th/settings.php b/lang/th/settings.php index 1b90d2a71..558d2da0f 100644 --- a/lang/th/settings.php +++ b/lang/th/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'ตั้งค่าการยืนยันตัวตนแบบหลายขั้นตอนเป็นชั้นความปลอดภัยเพิ่มเติมสำหรับบัญชีผู้ใช้ของคุณ', 'users_mfa_x_methods' => 'กำหนดค่า :count วิธี', 'users_mfa_configure' => 'กำหนดค่าวิธี', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'สร้าง API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/tk/activities.php b/lang/tk/activities.php index 4362fc029..e9344a3d4 100644 --- a/lang/tk/activities.php +++ b/lang/tk/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'User successfully updated', 'user_delete' => 'deleted user', 'user_delete_notification' => 'User successfully removed', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/tk/auth.php b/lang/tk/auth.php index 57f0cb5c6..47be4ea72 100644 --- a/lang/tk/auth.php +++ b/lang/tk/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'These credentials do not match our records.', 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Sign up', diff --git a/lang/tk/entities.php b/lang/tk/entities.php index 5501d2bc2..58c00ec4b 100644 --- a/lang/tk/entities.php +++ b/lang/tk/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Page Tags', 'chapter_tags' => 'Chapter Tags', 'book_tags' => 'Book Tags', diff --git a/lang/tk/settings.php b/lang/tk/settings.php index 3937c650f..d03024a89 100644 --- a/lang/tk/settings.php +++ b/lang/tk/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Configure Methods', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Create API Token', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/tr/activities.php b/lang/tr/activities.php index 5bed9ea30..71282e448 100644 --- a/lang/tr/activities.php +++ b/lang/tr/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Kullanıcı başarıyla güncellendi', 'user_delete' => 'kullanıcı silindi', 'user_delete_notification' => 'Kullanıcı başarıyla silindi', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/tr/auth.php b/lang/tr/auth.php index c9663f21e..f8942a4df 100644 --- a/lang/tr/auth.php +++ b/lang/tr/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Girdiğiniz bilgiler kayıtlarımızla uyuşmuyor.', 'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Kaydol', diff --git a/lang/tr/entities.php b/lang/tr/entities.php index 3d271ecb5..e5bdacae7 100644 --- a/lang/tr/entities.php +++ b/lang/tr/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sayfa Etiketleri', 'chapter_tags' => 'Bölüm Etiketleri', 'book_tags' => 'Kitap Etiketleri', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index af8d2c649..39ae23f28 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Setup multi-factor authentication as an extra layer of security for your user account.', 'users_mfa_x_methods' => ':count method configured|:count methods configured', 'users_mfa_configure' => 'Yöntemleri Yapılandır', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'API Anahtarı Oluştur', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovence', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/uk/activities.php b/lang/uk/activities.php index e8b77e0d4..140fec47c 100644 --- a/lang/uk/activities.php +++ b/lang/uk/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Користувача було успішно оновлено', 'user_delete' => 'вилучений користувач', 'user_delete_notification' => 'Користувача успішно видалено', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'створений APi токен', diff --git a/lang/uk/auth.php b/lang/uk/auth.php index 27aae3936..cb927cfd4 100644 --- a/lang/uk/auth.php +++ b/lang/uk/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Цей обліковий запис не знайдено.', 'throttle' => 'Забагато спроб входу в систему. Будь ласка, спробуйте ще раз через :seconds секунд.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Реєстрація', diff --git a/lang/uk/entities.php b/lang/uk/entities.php index 6bf7b241e..a77b8e77d 100644 --- a/lang/uk/entities.php +++ b/lang/uk/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Перемикач бічної панелі', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Теги сторінки', 'chapter_tags' => 'Теги розділів', 'book_tags' => 'Теги книг', diff --git a/lang/uk/settings.php b/lang/uk/settings.php index 9706b328b..02fd6a4f6 100644 --- a/lang/uk/settings.php +++ b/lang/uk/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Двофакторна аутентифікація додає ще один рівень безпеки для вашого облікового запису.', 'users_mfa_x_methods' => ':count метод налаштовано|:count методів налаштовано', 'users_mfa_configure' => 'Налаштувати Методи', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Створити токен API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/uz/activities.php b/lang/uz/activities.php index 84764192d..f87a52870 100644 --- a/lang/uz/activities.php +++ b/lang/uz/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Foydalanuvchi muvaffaqiyatli yangilandi', 'user_delete' => 'deleted user', 'user_delete_notification' => 'Foydalanuvchi muvaffaqiyatli olib tashlandi', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'created API token', diff --git a/lang/uz/auth.php b/lang/uz/auth.php index 59a620dae..1c31d8f87 100644 --- a/lang/uz/auth.php +++ b/lang/uz/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Uchbu ma‘lumotlar, bizdagi ma‘lumotlarga mos kelmadi.', 'throttle' => 'Kirishga urinishlar juda ko‘p. Iltimos :seconds soniyadan so‘ng urinib ko‘ring.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Ro‘yxatdan o‘tish', diff --git a/lang/uz/entities.php b/lang/uz/entities.php index d3fdb5944..f87028926 100644 --- a/lang/uz/entities.php +++ b/lang/uz/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Toggle Sidebar', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Sahifa teglari', 'chapter_tags' => 'Bo\'lim teglari', 'book_tags' => 'Kitob teglari', diff --git a/lang/uz/settings.php b/lang/uz/settings.php index 0dbf8351e..c1d3b3d46 100644 --- a/lang/uz/settings.php +++ b/lang/uz/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Ko\'p faktorli autentifikatsiyani foydalanuvchi hisobingiz uchun qo\'shimcha xavfsizlik qatlami sifatida o\'rnating.', 'users_mfa_x_methods' => ':count usuli tuzilgan|:count usullari sozlangan', 'users_mfa_configure' => 'Usullarni sozlash', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'API tokenini yarating', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/vi/activities.php b/lang/vi/activities.php index 00ec6ce04..0625bb529 100644 --- a/lang/vi/activities.php +++ b/lang/vi/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => 'Người dùng được cập nhật thành công', 'user_delete' => 'người dùng đã bị xóa', 'user_delete_notification' => 'Người dùng đã được xóa thành công', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => 'Đã tạo Token API ', diff --git a/lang/vi/auth.php b/lang/vi/auth.php index d53d2cf4c..9e0ce731b 100644 --- a/lang/vi/auth.php +++ b/lang/vi/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => 'Thông tin đăng nhập này không khớp với dữ liệu của chúng tôi.', 'throttle' => 'Quá nhiều lần đăng nhập sai. Vui lòng thử lại sau :seconds giây.', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => 'Đăng ký', diff --git a/lang/vi/entities.php b/lang/vi/entities.php index fc1ca6d56..a3f92e030 100644 --- a/lang/vi/entities.php +++ b/lang/vi/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => 'Chuyển đổi thanh bên', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => 'Các Thẻ Trang', 'chapter_tags' => 'Các Thẻ Chương', 'book_tags' => 'Các Thẻ Sách', diff --git a/lang/vi/settings.php b/lang/vi/settings.php index c0bca5691..d029fe7f3 100644 --- a/lang/vi/settings.php +++ b/lang/vi/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => 'Thiết lập xác thực đa yếu tố như một lớp bảo mật bổ sung cho tài khoản người dùng của bạn.', 'users_mfa_x_methods' => ':count phương thức đã cấu hình|:count phương thức đã cấu hình', 'users_mfa_configure' => 'Cấu hình phương thức', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => 'Tạo Token API', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/zh_CN/activities.php b/lang/zh_CN/activities.php index a2a2acd24..d30e61f0f 100644 --- a/lang/zh_CN/activities.php +++ b/lang/zh_CN/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => '用户更新成功', 'user_delete' => '用户已删除', 'user_delete_notification' => '成功移除用户', + 'user_mfa_reset' => '为用户重置MFA', + 'user_mfa_reset_notification' => '多因素认证方法已重置', // API Tokens 'api_token_create' => '已创建 API 令牌', diff --git a/lang/zh_CN/auth.php b/lang/zh_CN/auth.php index 8c4ba63f9..4c97b46ce 100644 --- a/lang/zh_CN/auth.php +++ b/lang/zh_CN/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => '用户名或密码错误。', 'throttle' => '您的登录次数过多,请在:seconds秒后重试。', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => '注册', diff --git a/lang/zh_CN/entities.php b/lang/zh_CN/entities.php index 9d57eeb57..9daf816da 100644 --- a/lang/zh_CN/entities.php +++ b/lang/zh_CN/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => '切换侧边栏', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => '页面标签', 'chapter_tags' => '章节标签', 'book_tags' => '书籍标签', diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index 86dd680b0..eef14b168 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -264,6 +264,9 @@ return [ 'users_mfa_desc' => '设置多重身份认证能增加您账户的安全性。', 'users_mfa_x_methods' => ':count 个措施已配置|:count 个措施已配置', 'users_mfa_configure' => '配置安全措施', + 'users_mfa_reset' => '重置多重身份验证方法', + 'users_mfa_reset_desc' => '此操作将重置并清除该用户所有已配置的多重身份验证方法。如果多重身份验证是任何角色所要求的,用户在下次登录时将被提示配置新的方法。', + 'users_mfa_reset_confirm' => '您确定要重置此用户的多重身份验证吗?', // API Tokens 'user_api_token_create' => '创建 API 令牌', @@ -364,6 +367,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/zh_TW/activities.php b/lang/zh_TW/activities.php index 65ba0a565..791fa26b7 100644 --- a/lang/zh_TW/activities.php +++ b/lang/zh_TW/activities.php @@ -99,6 +99,8 @@ return [ 'user_update_notification' => '使用者已成功更新。', 'user_delete' => '已刪除使用者', 'user_delete_notification' => '使用者移除成功', + 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens 'api_token_create' => '建立 API 權杖', diff --git a/lang/zh_TW/auth.php b/lang/zh_TW/auth.php index 1168b48b2..47e8ed950 100644 --- a/lang/zh_TW/auth.php +++ b/lang/zh_TW/auth.php @@ -8,6 +8,7 @@ return [ 'failed' => '使用者名稱或密碼錯誤。', 'throttle' => '您的登入次數過多,請在 :seconds 秒後重試。', + 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', // Login & Register 'sign_up' => '註冊', diff --git a/lang/zh_TW/entities.php b/lang/zh_TW/entities.php index 08886013c..ba53d885d 100644 --- a/lang/zh_TW/entities.php +++ b/lang/zh_TW/entities.php @@ -331,6 +331,9 @@ return [ // Editor Sidebar 'toggle_sidebar' => '切換側邊欄', + 'page_contents' => 'Page Contents', + 'page_contents_none' => 'No headings were found in the page content.', + 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', 'page_tags' => '頁面標籤', 'chapter_tags' => '章節標籤', 'book_tags' => '書本標籤', diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php index fd5b088f5..951085450 100644 --- a/lang/zh_TW/settings.php +++ b/lang/zh_TW/settings.php @@ -265,6 +265,9 @@ return [ 'users_mfa_desc' => '設定多重身份驗證為您的帳戶多增加了一道防線', 'users_mfa_x_methods' => ':count 個措施已配置|:count 個措施已配置', 'users_mfa_configure' => '方式設置', + 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', + 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', + 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', // API Tokens 'user_api_token_create' => '建立 API 權杖', @@ -365,6 +368,7 @@ return [ 'sk' => 'Slovensky', 'sl' => 'Slovenščina', 'sv' => 'Svenska', + 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', From 67529dd883cf9e70f8e362a9fcfc6edcc4d19a96 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 May 2026 10:34:04 +0100 Subject: [PATCH 73/74] Deps: Updated PHP packages, fixed some types for phpstan --- app/Search/SearchIndex.php | 10 +- composer.lock | 279 +++++++++++++++++++------------------ 2 files changed, 150 insertions(+), 139 deletions(-) diff --git a/app/Search/SearchIndex.php b/app/Search/SearchIndex.php index ce78831ee..e2a4a21a4 100644 --- a/app/Search/SearchIndex.php +++ b/app/Search/SearchIndex.php @@ -119,7 +119,7 @@ class SearchIndex * Create a scored term array from the given text, where the keys are the terms * and the values are their scores. * - * @return array + * @return array */ protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array { @@ -136,7 +136,7 @@ class SearchIndex * Create a scored term array from the given HTML, where the keys are the terms * and the values are their scores. * - * @return array + * @return array */ protected function generateTermScoreMapFromHtml(string $html): array { @@ -177,7 +177,7 @@ class SearchIndex * * @param Tag[] $tags * - * @return array + * @return array */ protected function generateTermScoreMapFromTags(array $tags): array { @@ -277,9 +277,9 @@ class SearchIndex * For the given term data arrays, Merge their contents by term * while combining any scores. * - * @param array[] ...$scoreMaps + * @param array[] ...$scoreMaps * - * @return array + * @return array */ protected function mergeTermScoreMaps(...$scoreMaps): array { diff --git a/composer.lock b/composer.lock index c0346a7b2..710067eb0 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.381.5", + "version": "3.382.2", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "409208d62af0ddafbcb0af1a0bf514f5ffcaba92" + "reference": "6844cc6421c47d6b96633ab8039045012acbeb27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/409208d62af0ddafbcb0af1a0bf514f5ffcaba92", - "reference": "409208d62af0ddafbcb0af1a0bf514f5ffcaba92", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6844cc6421c47d6b96633ab8039045012acbeb27", + "reference": "6844cc6421c47d6b96633ab8039045012acbeb27", "shasum": "" }, "require": { @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.381.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.382.2" }, - "time": "2026-05-20T18:16:01+00:00" + "time": "2026-05-27T18:11:41+00:00" }, { "name": "bacon/bacon-qr-code", @@ -1179,16 +1179,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.10.3", + "version": "7.10.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86" + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", - "reference": "47ba23c7a55247e2e1b7407aca90e9bbed0d9d86", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7c8d84b39e680315f687e8662a9d6fb0865c5148", + "reference": "7c8d84b39e680315f687e8662a9d6fb0865c5148", "shasum": "" }, "require": { @@ -1206,7 +1206,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.3.2", + "guzzlehttp/test-server": "^0.4", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1286,7 +1286,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.10.3" + "source": "https://github.com/guzzle/guzzle/tree/7.10.5" }, "funding": [ { @@ -1302,7 +1302,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T22:59:19+00:00" + "time": "2026-05-27T11:53:46+00:00" }, { "name": "guzzlehttp/promises", @@ -1389,16 +1389,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.10.1", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "73ab136360b5dfd858006eae9795e8fe43c80361" + "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/73ab136360b5dfd858006eae9795e8fe43c80361", - "reference": "73ab136360b5dfd858006eae9795e8fe43c80361", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7c1472269227dc6f18930bd903d7a88fe6c52130", + "reference": "7c1472269227dc6f18930bd903d7a88fe6c52130", "shasum": "" }, "require": { @@ -1486,7 +1486,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.10.1" + "source": "https://github.com/guzzle/psr7/tree/2.10.3" }, "funding": [ { @@ -1502,20 +1502,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T09:27:36+00:00" + "time": "2026-05-27T11:48:20+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.5", + "version": "v1.0.6", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", - "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946", + "reference": "eef7f87bab6f204eba3c39224d8075c70c637946", "shasum": "" }, "require": { @@ -1524,7 +1524,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", "uri-template/tests": "1.0.0" }, "type": "library", @@ -1572,7 +1572,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.6" }, "funding": [ { @@ -1588,7 +1588,7 @@ "type": "tidelift" } ], - "time": "2025-08-22T14:27:06+00:00" + "time": "2026-05-23T22:00:21+00:00" }, { "name": "intervention/gif", @@ -1803,16 +1803,16 @@ }, { "name": "laravel/framework", - "version": "v12.60.2", + "version": "v12.61.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "b8b55ce32175cc00f834a56eeb6316f18ed6ea39" + "reference": "1124062a1ca92d290c8bcb9b7f649920fa6816bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/b8b55ce32175cc00f834a56eeb6316f18ed6ea39", - "reference": "b8b55ce32175cc00f834a56eeb6316f18ed6ea39", + "url": "https://api.github.com/repos/laravel/framework/zipball/1124062a1ca92d290c8bcb9b7f649920fa6816bf", + "reference": "1124062a1ca92d290c8bcb9b7f649920fa6816bf", "shasum": "" }, "require": { @@ -2021,7 +2021,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-05-20T11:48:19+00:00" + "time": "2026-05-26T23:41:33+00:00" }, { "name": "laravel/prompts", @@ -4666,16 +4666,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.22", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f" + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/3be75d5b9244936dd4ac62ade2bfb004d13acf0f", - "reference": "3be75d5b9244936dd4ac62ade2bfb004d13acf0f", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", "shasum": "" }, "require": { @@ -4739,9 +4739,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.22" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" }, - "time": "2026-03-22T23:03:24+00:00" + "time": "2026-05-23T13:41:31+00:00" }, { "name": "ralouphie/getallheaders", @@ -5511,16 +5511,16 @@ }, { "name": "symfony/console", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", - "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -5585,7 +5585,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.11" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -5605,7 +5605,7 @@ "type": "tidelift" } ], - "time": "2026-05-13T12:04:42+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/css-selector", @@ -6134,16 +6134,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.4.8", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab" + "reference": "bc354f47c62301e990b7874fa662326368508e2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9381209597ec66c25be154cbf2289076e64d1eab", - "reference": "9381209597ec66c25be154cbf2289076e64d1eab", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", + "reference": "bc354f47c62301e990b7874fa662326368508e2c", "shasum": "" }, "require": { @@ -6192,7 +6192,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.8" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" }, "funding": [ { @@ -6212,20 +6212,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.12", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d" + "reference": "9df847980c436451f4f51d1284491bb4356dd989" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", - "reference": "7922b53e70d2ba2027af8bb6a59d91eb3541ea4d", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", + "reference": "9df847980c436451f4f51d1284491bb4356dd989", "shasum": "" }, "require": { @@ -6311,7 +6311,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.12" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" }, "funding": [ { @@ -6331,7 +6331,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T09:27:11+00:00" + "time": "2026-05-27T08:31:43+00:00" }, { "name": "symfony/mailer", @@ -6419,16 +6419,16 @@ }, { "name": "symfony/mime", - "version": "v7.4.12", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470" + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b198dd66c211c97119bcaaff7c13431dbbb5e470", - "reference": "b198dd66c211c97119bcaaff7c13431dbbb5e470", + "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", + "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", "shasum": "" }, "require": { @@ -6484,7 +6484,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.12" + "source": "https://github.com/symfony/mime/tree/v7.4.13" }, "funding": [ { @@ -6504,7 +6504,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-05-23T16:22:37+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6591,16 +6591,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", - "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -6649,7 +6649,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -6669,20 +6669,20 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:13:48+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + "reference": "dc21118016c039a66235cf93d96b435ffb282412" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", - "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", "shasum": "" }, "require": { @@ -6736,7 +6736,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" }, "funding": [ { @@ -6756,20 +6756,20 @@ "type": "tidelift" } ], - "time": "2024-09-10T14:38:51+00:00" + "time": "2026-05-25T15:22:23+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.37.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -6821,7 +6821,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -6841,20 +6841,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", - "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/14c5439eec4ccff081ac14eca2dc57feb2a66d92", + "reference": "14c5439eec4ccff081ac14eca2dc57feb2a66d92", "shasum": "" }, "require": { @@ -6906,7 +6906,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.1" }, "funding": [ { @@ -6926,7 +6926,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php80", @@ -7014,16 +7014,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + "reference": "8339098cae28673c15cce00d80734af0453054e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", - "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/8339098cae28673c15cce00d80734af0453054e2", + "reference": "8339098cae28673c15cce00d80734af0453054e2", "shasum": "" }, "require": { @@ -7070,7 +7070,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.1" }, "funding": [ { @@ -7090,20 +7090,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T17:25:58+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/88486db2c389b290bf87ff1de7ebc1e13e42bb06", - "reference": "88486db2c389b290bf87ff1de7ebc1e13e42bb06", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -7150,7 +7150,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -7170,20 +7170,20 @@ "type": "tidelift" } ], - "time": "2026-04-10T18:47:49+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.37.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", - "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", "shasum": "" }, "require": { @@ -7230,7 +7230,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" }, "funding": [ { @@ -7250,7 +7250,7 @@ "type": "tidelift" } ], - "time": "2026-04-26T13:10:57+00:00" + "time": "2026-05-26T02:25:22+00:00" }, { "name": "symfony/polyfill-uuid", @@ -7337,16 +7337,16 @@ }, { "name": "symfony/process", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" + "reference": "f5804be144caceb570f6747519999636b664f24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", - "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", + "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", + "reference": "f5804be144caceb570f6747519999636b664f24c", "shasum": "" }, "require": { @@ -7378,7 +7378,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.11" + "source": "https://github.com/symfony/process/tree/v7.4.13" }, "funding": [ { @@ -7398,20 +7398,20 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:55:21+00:00" + "time": "2026-05-23T16:05:06+00:00" }, { "name": "symfony/routing", - "version": "v7.4.12", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204" + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", - "reference": "3b04a5ec4887a8135a12ebf0f4cbc5b8fc8ee204", + "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", + "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", "shasum": "" }, "require": { @@ -7463,7 +7463,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.12" + "source": "https://github.com/symfony/routing/tree/v7.4.13" }, "funding": [ { @@ -7483,7 +7483,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-05-24T11:20:33+00:00" }, { "name": "symfony/service-contracts", @@ -7574,16 +7574,16 @@ }, { "name": "symfony/string", - "version": "v7.4.11", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15" + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", - "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", "shasum": "" }, "require": { @@ -7641,7 +7641,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.11" + "source": "https://github.com/symfony/string/tree/v7.4.13" }, "funding": [ { @@ -7661,7 +7661,7 @@ "type": "tidelift" } ], - "time": "2026-05-13T12:04:42+00:00" + "time": "2026-05-23T15:23:29+00:00" }, { "name": "symfony/translation", @@ -8732,16 +8732,16 @@ }, { "name": "larastan/larastan", - "version": "v3.9.6", + "version": "v3.10.0", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636" + "reference": "2970f83398154178a739609c244577267c7ee8eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", - "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", + "url": "https://api.github.com/repos/larastan/larastan/zipball/2970f83398154178a739609c244577267c7ee8eb", + "reference": "2970f83398154178a739609c244577267c7ee8eb", "shasum": "" }, "require": { @@ -8755,17 +8755,17 @@ "illuminate/pipeline": "^11.44.2 || ^12.4.1 || ^13", "illuminate/support": "^11.44.2 || ^12.4.1 || ^13", "php": "^8.2", - "phpstan/phpstan": "^2.1.44" + "phpstan/phpstan": "^2.2.0" }, "require-dev": { - "doctrine/coding-standard": "^13", + "doctrine/coding-standard": "^14", "laravel/framework": "^11.44.2 || ^12.7.2 || ^13", "mockery/mockery": "^1.6.12", "nikic/php-parser": "^5.4", "orchestra/canvas": "^v9.2.2 || ^10.0.1 || ^11", "orchestra/testbench-core": "^9.12.0 || ^10.1 || ^11", "phpstan/phpstan-deprecation-rules": "^2.0.1", - "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8" + "phpunit/phpunit": "^10.5.35 || ^11.5.15 || ^12.5.8 || ^13.1.8" }, "suggest": { "orchestra/testbench": "Using Larastan for analysing a package needs Testbench", @@ -8810,7 +8810,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.9.6" + "source": "https://github.com/larastan/larastan/tree/v3.10.0" }, "funding": [ { @@ -8818,7 +8818,7 @@ "type": "github" } ], - "time": "2026-04-16T10:02:43+00:00" + "time": "2026-05-28T08:00:58+00:00" }, { "name": "mockery/mockery", @@ -9179,11 +9179,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.55", + "version": "2.2.0", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9eaac3826ed5e9b8427350a43cac825eeca3f566", - "reference": "9eaac3826ed5e9b8427350a43cac825eeca3f566", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b4cd98348c809924f62bb9cc8c047f5e73bc9a58", + "reference": "b4cd98348c809924f62bb9cc8c047f5e73bc9a58", "shasum": "" }, "require": { @@ -9206,6 +9206,17 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Ondřej Mirtes" + }, + { + "name": "Markus Staab" + }, + { + "name": "Vincent Langlet" + } + ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", @@ -9228,7 +9239,7 @@ "type": "github" } ], - "time": "2026-05-18T11:57:34+00:00" + "time": "2026-05-28T08:22:43+00:00" }, { "name": "phpunit/php-code-coverage", From d421a191935e9689709f5a949c3d23dfd87d6ecc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 28 May 2026 12:32:17 +0100 Subject: [PATCH 74/74] Updated translator & dependency attribution before release v26.05 --- .github/translators.txt | 9 +- dev/licensing/js-library-licenses.txt | 241 +++++++------------------ dev/licensing/php-library-licenses.txt | 2 +- 3 files changed, 75 insertions(+), 177 deletions(-) diff --git a/.github/translators.txt b/.github/translators.txt index 037887bcc..cae26547b 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -533,7 +533,14 @@ JanDziaslo :: Polish Charllys Fernandes (CharllysFernandes) :: Portuguese, Brazilian Ilgiz Zigangirov (inov8) :: Russian Max Israelsson (Blezie) :: Swedish -Skiddybison5924 (chris-devel0per) :: German +Skiddybison5924 (chris-devel0per) :: German Informal; German Veyilla Nightwhisper (Veyilla) :: German João Barbosa (hypeedd) :: Portuguese Abcdefg Hijklmn (collatek) :: Korean +Suthep Yonphimai (tomztt) :: Thai +MrClock (MrClock8163) :: Hungarian +Elena0875 :: Russian +FelixFrizzy :: German +Pedro de Mattia (pdmtt) :: Portuguese, Brazilian +lonestan :: Russian +Paul Kernstock (kernstock) :: German diff --git a/dev/licensing/js-library-licenses.txt b/dev/licensing/js-library-licenses.txt index 1c23c0937..45d1816dd 100644 --- a/dev/licensing/js-library-licenses.txt +++ b/dev/licensing/js-library-licenses.txt @@ -69,8 +69,9 @@ Source: zeit/arg Link: zeit/arg ----------- argparse -License: Python-2.0 +License: MIT License File: node_modules/argparse/LICENSE +Copyright: Copyright (C) 2012 by Vitaly Puzrin Source: nodeca/argparse Link: nodeca/argparse ----------- @@ -81,34 +82,6 @@ Copyright: Copyright (c) 2023 Inspect JS Source: git+https://github.com/inspect-js/array-buffer-byte-length.git Link: https://github.com/inspect-js/array-buffer-byte-length#readme ----------- -array-includes -License: MIT -License File: node_modules/array-includes/LICENSE -Copyright: Copyright (C) 2015 Jordan Harband -Source: git://github.com/es-shims/array-includes.git -Link: git://github.com/es-shims/array-includes.git ------------ -array.prototype.findlastindex -License: MIT -License File: node_modules/array.prototype.findlastindex/LICENSE -Copyright: Copyright (c) 2021 ECMAScript Shims -Source: git+https://github.com/es-shims/Array.prototype.findLastIndex.git -Link: https://github.com/es-shims/Array.prototype.findLastIndex#readme ------------ -array.prototype.flat -License: MIT -License File: node_modules/array.prototype.flat/LICENSE -Copyright: Copyright (c) 2017 ECMAScript Shims -Source: git://github.com/es-shims/Array.prototype.flat.git -Link: git://github.com/es-shims/Array.prototype.flat.git ------------ -array.prototype.flatmap -License: MIT -License File: node_modules/array.prototype.flatmap/LICENSE -Copyright: Copyright (c) 2017 ECMAScript Shims -Source: git://github.com/es-shims/Array.prototype.flatMap.git -Link: git://github.com/es-shims/Array.prototype.flatMap.git ------------ arraybuffer.prototype.slice License: MIT License File: node_modules/arraybuffer.prototype.slice/LICENSE @@ -169,9 +142,14 @@ Link: https://github.com/jestjs/jest.git balanced-match License: MIT License File: node_modules/balanced-match/LICENSE.md -Copyright: Copyright (c) 2013 Julian Gruber <******@************.***> Source: git://github.com/juliangruber/balanced-match.git -Link: https://github.com/juliangruber/balanced-match +Link: git://github.com/juliangruber/balanced-match.git +----------- +baseline-browser-mapping +License: Apache-2.0 +License File: node_modules/baseline-browser-mapping/LICENSE.txt +Source: git+https://github.com/web-platform-dx/baseline-browser-mapping.git +Link: git+https://github.com/web-platform-dx/baseline-browser-mapping.git ----------- binary-extensions License: MIT @@ -184,9 +162,8 @@ Link: sindresorhus/binary-extensions brace-expansion License: MIT License File: node_modules/brace-expansion/LICENSE -Copyright: Copyright (c) 2013 Julian Gruber <******@************.***> -Source: git://github.com/juliangruber/brace-expansion.git -Link: https://github.com/juliangruber/brace-expansion +Source: git+ssh://git@github.com/juliangruber/brace-expansion.git +Link: git+ssh://git@github.com/juliangruber/brace-expansion.git ----------- braces License: MIT @@ -472,8 +449,8 @@ Link: git://github.com/ljharb/define-properties.git detect-libc License: Apache-2.0 License File: node_modules/detect-libc/LICENSE -Source: git://github.com/lovell/detect-libc -Link: git://github.com/lovell/detect-libc +Source: git://github.com/lovell/detect-libc.git +Link: git://github.com/lovell/detect-libc.git ----------- detect-newline License: MIT @@ -489,12 +466,6 @@ Copyright: Copyright (c) 2009-2015, Kevin Decker <********@*****.***> Source: git://github.com/kpdecker/jsdiff.git Link: git://github.com/kpdecker/jsdiff.git ----------- -doctrine -License: Apache-2.0 -License File: node_modules/doctrine/LICENSE -Source: eslint/doctrine -Link: https://github.com/eslint/doctrine ------------ dunder-proto License: MIT License File: node_modules/dunder-proto/LICENSE @@ -511,8 +482,8 @@ electron-to-chromium License: ISC License File: node_modules/electron-to-chromium/LICENSE Copyright: Copyright 2018 Kilian Valkhof -Source: https://github.com/kilian/electron-to-chromium/ -Link: https://github.com/kilian/electron-to-chromium/ +Source: git+https://github.com/kilian/electron-to-chromium.git +Link: git+https://github.com/kilian/electron-to-chromium.git ----------- emittery License: MIT @@ -577,13 +548,6 @@ Copyright: Copyright (c) 2022 ECMAScript Shims Source: git+https://github.com/es-shims/es-set-tostringtag.git Link: https://github.com/es-shims/es-set-tostringtag#readme ----------- -es-shim-unscopables -License: MIT -License File: node_modules/es-shim-unscopables/LICENSE -Copyright: Copyright (c) 2022 Jordan Harband -Source: git+https://github.com/ljharb/es-shim-unscopables.git -Link: https://github.com/ljharb/es-shim-unscopables#readme ------------ es-to-primitive License: MIT License File: node_modules/es-to-primitive/LICENSE @@ -612,27 +576,6 @@ Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindres Source: sindresorhus/escape-string-regexp Link: sindresorhus/escape-string-regexp ----------- -eslint-import-resolver-node -License: MIT -License File: node_modules/eslint-import-resolver-node/LICENSE -Copyright: Copyright (c) 2015 Ben Mosher -Source: https://github.com/import-js/eslint-plugin-import -Link: https://github.com/import-js/eslint-plugin-import ------------ -eslint-module-utils -License: MIT -License File: node_modules/eslint-module-utils/LICENSE -Copyright: Copyright (c) 2015 Ben Mosher -Source: git+https://github.com/import-js/eslint-plugin-import.git -Link: https://github.com/import-js/eslint-plugin-import#readme ------------ -eslint-plugin-import -License: MIT -License File: node_modules/eslint-plugin-import/LICENSE -Copyright: Copyright (c) 2015 Ben Mosher -Source: https://github.com/import-js/eslint-plugin-import -Link: https://github.com/import-js/eslint-plugin-import ------------ eslint-scope License: BSD-2-Clause License File: node_modules/eslint-scope/LICENSE @@ -814,6 +757,13 @@ Copyright: Copyright (c) 2019 Jordan Harband Source: git+https://github.com/inspect-js/functions-have-names.git Link: https://github.com/inspect-js/functions-have-names#readme ----------- +generator-function +License: MIT +License File: node_modules/generator-function/LICENSE.md +Copyright: Copyright (c) 2015 Tiancheng “Timothy” Gu +Source: git+https://github.com/TimothyGu/generator-function.git +Link: https://github.com/TimothyGu/generator-function#readme +----------- gensync License: MIT License File: node_modules/gensync/LICENSE @@ -1026,13 +976,6 @@ Copyright: Copyright (c) 2014-present, Lee Byron and other contributors. Source: git://github.com/immutable-js/immutable-js.git Link: https://immutable-js.com ----------- -import-fresh -License: MIT -License File: node_modules/import-fresh/license -Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com) -Source: sindresorhus/import-fresh -Link: sindresorhus/import-fresh ------------ import-local License: MIT License File: node_modules/import-local/license @@ -1529,7 +1472,7 @@ License: MIT License File: node_modules/js-yaml/LICENSE Copyright: Copyright (C) 2011-2015 by Vitaly Puzrin Source: nodeca/js-yaml -Link: nodeca/js-yaml +Link: https://github.com/nodeca/js-yaml ----------- jsdom License: MIT @@ -1645,12 +1588,6 @@ License File: node_modules/lodash.memoize/LICENSE Source: lodash/lodash Link: https://lodash.com/ ----------- -lodash.merge -License: MIT -License File: node_modules/lodash.merge/LICENSE -Source: lodash/lodash -Link: https://lodash.com/ ------------ lodash.throttle License: MIT License File: node_modules/lodash.throttle/LICENSE @@ -1741,11 +1678,10 @@ Source: sindresorhus/mimic-fn Link: sindresorhus/mimic-fn ----------- minimatch -License: ISC -License File: node_modules/minimatch/LICENSE -Copyright: Copyright (c) Isaac Z. Schlueter and Contributors -Source: git://github.com/isaacs/minimatch.git -Link: git://github.com/isaacs/minimatch.git +License: BlueOak-1.0.0 +License File: node_modules/minimatch/LICENSE.md +Source: git@github.com:isaacs/minimatch +Link: git@github.com:isaacs/minimatch ----------- minimist License: MIT @@ -1754,9 +1690,8 @@ Source: git://github.com/minimistjs/minimist.git Link: https://github.com/minimistjs/minimist ----------- minipass -License: ISC -License File: node_modules/minipass/LICENSE -Copyright: Copyright (c) 2017-2023 npm, Inc., Isaac Z. Schlueter, and Contributors +License: BlueOak-1.0.0 +License File: node_modules/minipass/LICENSE.md Source: https://github.com/isaacs/minipass Link: https://github.com/isaacs/minipass ----------- @@ -1872,27 +1807,6 @@ Copyright: Copyright (c) 2014 Jordan Harband Source: git://github.com/ljharb/object.assign.git Link: git://github.com/ljharb/object.assign.git ----------- -object.fromentries -License: MIT -License File: node_modules/object.fromentries/LICENSE -Copyright: Copyright (c) 2018 Jordan Harband -Source: git://github.com/es-shims/Object.fromEntries.git -Link: git://github.com/es-shims/Object.fromEntries.git ------------ -object.groupby -License: MIT -License File: node_modules/object.groupby/LICENSE -Copyright: Copyright (c) 2023 ECMAScript Shims -Source: git+https://github.com/es-shims/Object.groupBy.git -Link: https://github.com/es-shims/Object.groupBy#readme ------------ -object.values -License: MIT -License File: node_modules/object.values/LICENSE -Copyright: Copyright (c) 2015 Jordan Harband -Source: git://github.com/es-shims/Object.values.git -Link: git://github.com/es-shims/Object.values.git ------------ once License: ISC License File: node_modules/once/LICENSE @@ -1948,13 +1862,6 @@ License File: node_modules/package-json-from-dist/LICENSE.md Source: git+https://github.com/isaacs/package-json-from-dist.git Link: git+https://github.com/isaacs/package-json-from-dist.git ----------- -parent-module -License: MIT -License File: node_modules/parent-module/license -Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com) -Source: sindresorhus/parent-module -Link: sindresorhus/parent-module ------------ parse-json License: MIT License File: node_modules/parse-json/license @@ -2159,8 +2066,8 @@ resolve License: MIT License File: node_modules/resolve/LICENSE Copyright: Copyright (c) 2012 James Halliday -Source: git://github.com/browserify/resolve.git -Link: git://github.com/browserify/resolve.git +Source: ssh://github.com/browserify/resolve.git +Link: ssh://github.com/browserify/resolve.git ----------- rrweb-cssom License: MIT @@ -2405,7 +2312,7 @@ Link: sindresorhus/string-width string-width License: MIT License File: node_modules/string-width/license -Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com) +Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com) Source: sindresorhus/string-width Link: sindresorhus/string-width ----------- @@ -2570,13 +2477,6 @@ Copyright: Copyright (c) 2014 Blake Embrey (*****@***********.***) Source: git://github.com/TypeStrong/ts-node.git Link: https://typestrong.org/ts-node ----------- -tsconfig-paths -License: MIT -License File: node_modules/tsconfig-paths/LICENSE -Copyright: Copyright (c) 2016 Jonas Kello -Source: https://github.com/dividab/tsconfig-paths -Link: https://github.com/dividab/tsconfig-paths ------------ type-check License: MIT License File: node_modules/type-check/LICENSE @@ -2810,7 +2710,7 @@ Link: chalk/wrap-ansi wrap-ansi License: MIT License File: node_modules/wrap-ansi/license -Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (sindresorhus.com) +Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindresorhus.com) Source: chalk/wrap-ansi Link: chalk/wrap-ansi ----------- @@ -2892,12 +2792,6 @@ Copyright: Copyright (c) Sindre Sorhus <************@*****.***> (https://sindres Source: sindresorhus/yocto-queue Link: sindresorhus/yocto-queue ----------- -@ampproject/remapping -License: Apache-2.0 -License File: node_modules/@ampproject/remapping/LICENSE -Source: git+https://github.com/ampproject/remapping.git -Link: git+https://github.com/ampproject/remapping.git ------------ @asamuzakjp/css-color License: MIT License File: node_modules/@asamuzakjp/css-color/LICENSE @@ -3172,15 +3066,15 @@ Link: https://demurgos.github.io/v8-coverage License: MIT License File: node_modules/@codemirror/autocomplete/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/autocomplete.git -Link: https://github.com/codemirror/autocomplete.git +Source: git+https://github.com/codemirror/autocomplete.git +Link: git+https://github.com/codemirror/autocomplete.git ----------- @codemirror/commands License: MIT License File: node_modules/@codemirror/commands/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/commands.git -Link: https://github.com/codemirror/commands.git +Source: git+https://github.com/codemirror/commands.git +Link: git+https://github.com/codemirror/commands.git ----------- @codemirror/lang-css License: MIT @@ -3200,8 +3094,8 @@ Link: https://github.com/codemirror/lang-html.git License: MIT License File: node_modules/@codemirror/lang-javascript/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/lang-javascript.git -Link: https://github.com/codemirror/lang-javascript.git +Source: git+https://github.com/codemirror/lang-javascript.git +Link: git+https://github.com/codemirror/lang-javascript.git ----------- @codemirror/lang-json License: MIT @@ -3235,8 +3129,8 @@ Link: https://github.com/codemirror/lang-xml.git License: MIT License File: node_modules/@codemirror/language/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/language.git -Link: https://github.com/codemirror/language.git +Source: git+https://github.com/codemirror/language.git +Link: git+https://github.com/codemirror/language.git ----------- @codemirror/legacy-modes License: MIT @@ -3249,22 +3143,22 @@ Link: https://github.com/codemirror/legacy-modes.git License: MIT License File: node_modules/@codemirror/lint/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/lint.git -Link: https://github.com/codemirror/lint.git +Source: git+https://github.com/codemirror/lint.git +Link: git+https://github.com/codemirror/lint.git ----------- @codemirror/search License: MIT License File: node_modules/@codemirror/search/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/search.git -Link: https://github.com/codemirror/search.git +Source: git+https://github.com/codemirror/search.git +Link: git+https://github.com/codemirror/search.git ----------- @codemirror/state License: MIT License File: node_modules/@codemirror/state/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/state.git -Link: https://github.com/codemirror/state.git +Source: git+https://github.com/codemirror/state.git +Link: git+https://github.com/codemirror/state.git ----------- @codemirror/theme-one-dark License: MIT @@ -3277,8 +3171,8 @@ Link: https://github.com/codemirror/theme-one-dark.git License: MIT License File: node_modules/@codemirror/view/LICENSE Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others -Source: https://github.com/codemirror/view.git -Link: https://github.com/codemirror/view.git +Source: git+https://github.com/codemirror/view.git +Link: git+https://github.com/codemirror/view.git ----------- @cspotcode/source-map-support License: MIT @@ -3358,12 +3252,6 @@ License File: node_modules/@eslint/core/LICENSE Source: git+https://github.com/eslint/rewrite.git Link: https://github.com/eslint/rewrite/tree/main/packages/core#readme ----------- -@eslint/eslintrc -License: MIT -License File: node_modules/@eslint/eslintrc/LICENSE -Source: eslint/eslintrc -Link: https://github.com/eslint/eslintrc#readme ------------ @eslint/js License: MIT License File: node_modules/@eslint/js/LICENSE @@ -3567,6 +3455,13 @@ Copyright: Copyright 2024 Justin Ridgewell <******@*********.****> Source: git+https://github.com/jridgewell/sourcemaps.git Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping ----------- +@jridgewell/remapping +License: MIT +License File: node_modules/@jridgewell/remapping/LICENSE +Copyright: Copyright 2024 Justin Ridgewell <******@*********.****> +Source: git+https://github.com/jridgewell/sourcemaps.git +Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/remapping +----------- @jridgewell/resolve-uri License: MIT License File: node_modules/@jridgewell/resolve-uri/LICENSE @@ -3704,18 +3599,12 @@ License: MIT Source: git+https://github.com/un-ts/pkgr.git Link: https://github.com/un-ts/pkgr/blob/master/packages/core ----------- -@rtsao/scc -License: MIT -License File: node_modules/@rtsao/scc/LICENSE -Copyright: Copyright (c) 2019 Ryan Tsao -Source: rtsao/scc -Link: rtsao/scc ------------ @sinclair/typebox License: MIT License File: node_modules/@sinclair/typebox/license -Source: https://github.com/sinclairzx81/typebox -Link: https://github.com/sinclairzx81/typebox +Copyright: Copyright (c) 2017-2026 Haydn Paterson +Source: https://github.com/sinclairzx81/typebox-legacy +Link: https://github.com/sinclairzx81/typebox-legacy ----------- @sinonjs/commons License: BSD-3-Clause @@ -3798,6 +3687,13 @@ Copyright: Copyright (c) Microsoft Corporation. Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/babel__traverse ----------- +@types/esrecurse +License: MIT +License File: node_modules/@types/esrecurse/LICENSE +Copyright: Copyright (c) Microsoft Corporation. +Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git +Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/esrecurse +----------- @types/estree License: MIT License File: node_modules/@types/estree/LICENSE @@ -3847,11 +3743,6 @@ Copyright: Copyright (c) Microsoft Corporation. Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/json-schema ----------- -@types/json5 -License: MIT -Source: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git -Link: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git ------------ @types/linkify-it License: MIT License File: node_modules/@types/linkify-it/LICENSE diff --git a/dev/licensing/php-library-licenses.txt b/dev/licensing/php-library-licenses.txt index 348fce2ff..2259c44c3 100644 --- a/dev/licensing/php-library-licenses.txt +++ b/dev/licensing/php-library-licenses.txt @@ -109,7 +109,7 @@ firebase/php-jwt License: BSD-3-Clause License File: vendor/firebase/php-jwt/LICENSE Copyright: Copyright (c) 2011, Neuman Vong -Source: https://github.com/firebase/php-jwt.git +Source: https://github.com/googleapis/php-jwt.git Link: https://github.com/firebase/php-jwt ----------- fruitcake/php-cors