From da82e70ca3cdd075f7ae148cb2f58fddb0d93627 Mon Sep 17 00:00:00 2001 From: Talstra Ruben SRSNL Date: Mon, 20 Jan 2025 17:21:46 +0100 Subject: [PATCH 001/474] =?UTF-8?q?Add=20optional=20OIDC=20avatar=20fetchi?= =?UTF-8?q?ng=20from=20the=20=E2=80=9Cpicture=E2=80=9D=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Access/Oidc/OidcService.php | 8 ++++++- app/Access/Oidc/OidcUserDetails.php | 2 ++ app/Config/oidc.php | 3 +++ app/Uploads/UserAvatars.php | 37 ++++++++++++++++++++++++++--- 4 files changed, 46 insertions(+), 4 deletions(-) diff --git a/app/Access/Oidc/OidcService.php b/app/Access/Oidc/OidcService.php index 7c1760649b5..660885e8b2a 100644 --- a/app/Access/Oidc/OidcService.php +++ b/app/Access/Oidc/OidcService.php @@ -11,6 +11,7 @@ use BookStack\Facades\Theme; use BookStack\Http\HttpRequestService; use BookStack\Theming\ThemeEvents; +use BookStack\Uploads\UserAvatars; use BookStack\Users\Models\User; use Illuminate\Support\Facades\Cache; use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider; @@ -26,7 +27,8 @@ public function __construct( protected RegistrationService $registrationService, protected LoginService $loginService, protected HttpRequestService $http, - protected GroupSyncService $groupService + protected GroupSyncService $groupService, + protected UserAvatars $userAvatars ) { } @@ -227,6 +229,10 @@ protected function processAccessTokenCallback(OidcAccessToken $accessToken, Oidc $this->loginService->login($user, 'oidc'); + if ($this->config()['fetch_avatars'] && $userDetails->picture) { + $this->userAvatars->assignToUserFromUrl($user, $userDetails->picture, $accessToken->getToken()); + } + return $user; } diff --git a/app/Access/Oidc/OidcUserDetails.php b/app/Access/Oidc/OidcUserDetails.php index fae20de0b62..10595d1e0dd 100644 --- a/app/Access/Oidc/OidcUserDetails.php +++ b/app/Access/Oidc/OidcUserDetails.php @@ -11,6 +11,7 @@ public function __construct( public ?string $email = null, public ?string $name = null, public ?array $groups = null, + public ?string $picture = null, ) { } @@ -40,6 +41,7 @@ public function populate( $this->email = $claims->getClaim('email') ?? $this->email; $this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name; $this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups; + $this->picture = $claims->getClaim('picture') ?: $this->picture; } protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $token): string diff --git a/app/Config/oidc.php b/app/Config/oidc.php index 8b5470931d0..62f19a119c0 100644 --- a/app/Config/oidc.php +++ b/app/Config/oidc.php @@ -54,4 +54,7 @@ 'groups_claim' => env('OIDC_GROUPS_CLAIM', 'groups'), // When syncing groups, remove any groups that no longer match. Otherwise, sync only adds new groups. 'remove_from_groups' => env('OIDC_REMOVE_FROM_GROUPS', false), + + // When enabled, BookStack will fetch the user’s avatar from the 'picture' claim (SSRF risk if URLs are untrusted). + 'fetch_avatars' => env('OIDC_FETCH_AVATARS', false), ]; diff --git a/app/Uploads/UserAvatars.php b/app/Uploads/UserAvatars.php index c623247352b..af91dfe70f1 100644 --- a/app/Uploads/UserAvatars.php +++ b/app/Uploads/UserAvatars.php @@ -53,6 +53,31 @@ public function assignToUserFromExistingData(User $user, string $imageData, stri } } + /** + * Assign a new avatar image to the given user by fetching from a remote URL. + */ + public function assignToUserFromUrl(User $user, string $avatarUrl, ?string $accessToken = null): void + { + // Quickly skip invalid or non-HTTP URLs + if (!$avatarUrl || !str_starts_with($avatarUrl, 'http')) { + return; + } + + try { + $this->destroyAllForUser($user); + $imageData = $this->getAvatarImageData($avatarUrl, $accessToken); + $avatar = $this->createAvatarImageFromData($user, $imageData, 'png'); + $user->avatar()->associate($avatar); + $user->save(); + } catch (Exception $e) { + Log::error('Failed to save user avatar image from URL', [ + 'exception' => $e, + 'url' => $avatarUrl, + 'user_id' => $user->id, + ]); + } + } + /** * Destroy all user avatars uploaded to the given user. */ @@ -105,15 +130,21 @@ protected function createAvatarImageFromData(User $user, string $imageData, stri } /** - * Gets an image from url and returns it as a string of image data. + * Gets an image from a URL (public or private) and returns it as a string of image data. * * @throws HttpFetchException */ - protected function getAvatarImageData(string $url): string + protected function getAvatarImageData(string $url, ?string $accessToken = null): string { try { + $headers = []; + if (!empty($accessToken)) { + $headers['Authorization'] = 'Bearer ' . $accessToken; + } + $client = $this->http->buildClient(5); - $response = $client->sendRequest(new Request('GET', $url)); + $response = $client->sendRequest(new Request('GET', $url, $headers)); + if ($response->getStatusCode() !== 200) { throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url])); } From 94b1cffa2d57141185d42965c8fa9f2da07f30ae Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 11 Mar 2025 23:45:26 +0000 Subject: [PATCH 002/474] System CLI: Updated with new version As per https://codeberg.org/bookstack/system-cli/pulls/21 dev/checksums folder added to support this new system. Related to #161 --- .gitignore | 1 + bookstack-system-cli | Bin 386055 -> 388307 bytes dev/checksums/.gitignore | 1 + dev/checksums/vendor | 1 + 4 files changed, 3 insertions(+) create mode 100644 dev/checksums/.gitignore create mode 100644 dev/checksums/vendor diff --git a/.gitignore b/.gitignore index 3582c410221..b545d161f13 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ webpack-stats.json phpstan.neon esbuild-meta.json .phpactor.json +/*.zip diff --git a/bookstack-system-cli b/bookstack-system-cli index 6fbfd98745b8bac922648e0354b9d55c9b9245a2..6283aa0cb41b89164a5fb67a2f208c03475e0d37 100755 GIT binary patch delta 43963 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 t0Vv*?Umhpmb7<)6qVy<|Zv8{9-BsD-7PkW Date: Sat, 15 Mar 2025 20:32:57 +0000 Subject: [PATCH 003/474] Config: Updated DB host to handle ipv6 Can be set via the square bracket format. For #5464 --- .env.example.complete | 1 + app/Config/database.php | 22 +++++++++++++--------- tests/TestCase.php | 14 +++++++++----- tests/Unit/ConfigTest.php | 21 +++++++++++++++++++++ 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/.env.example.complete b/.env.example.complete index 6b4936648f0..25687aaac38 100644 --- a/.env.example.complete +++ b/.env.example.complete @@ -56,6 +56,7 @@ APP_PROXIES=null # Database details # Host can contain a port (localhost:3306) or a separate DB_PORT option can be used. +# An ipv6 address can be used via the square bracket format ([::1]). DB_HOST=localhost DB_PORT=3306 DB_DATABASE=database_database diff --git a/app/Config/database.php b/app/Config/database.php index 59ac0f31beb..8d38a86dfed 100644 --- a/app/Config/database.php +++ b/app/Config/database.php @@ -40,12 +40,16 @@ // MYSQL // Split out port from host if set -$mysql_host = env('DB_HOST', 'localhost'); -$mysql_host_exploded = explode(':', $mysql_host); -$mysql_port = env('DB_PORT', 3306); -if (count($mysql_host_exploded) > 1) { - $mysql_host = $mysql_host_exploded[0]; - $mysql_port = intval($mysql_host_exploded[1]); +$mysqlHost = env('DB_HOST', 'localhost'); +$mysqlHostExploded = explode(':', $mysqlHost); +$mysqlPort = env('DB_PORT', 3306); +$mysqlHostIpv6 = str_starts_with($mysqlHost, '['); +if ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) { + $mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1)); + $mysqlPort = intval(end($mysqlHostExploded)); +} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) { + $mysqlHost = $mysqlHostExploded[0]; + $mysqlPort = intval($mysqlHostExploded[1]); } return [ @@ -61,12 +65,12 @@ 'mysql' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), - 'host' => $mysql_host, + 'host' => $mysqlHost, 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), - 'port' => $mysql_port, + 'port' => $mysqlPort, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', // Prefixes are only semi-supported and may be unstable @@ -88,7 +92,7 @@ 'database' => 'bookstack-test', 'username' => env('MYSQL_USER', 'bookstack-test'), 'password' => env('MYSQL_PASSWORD', 'bookstack-test'), - 'port' => $mysql_port, + 'port' => $mysqlPort, 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', diff --git a/tests/TestCase.php b/tests/TestCase.php index b63de307624..0fb899ea99e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -118,7 +118,7 @@ protected function mockHttpClient(array $responses = []): HttpClientHistory * Database config is juggled so the value can be restored when * parallel testing are used, where multiple databases exist. */ - protected function runWithEnv(string $name, $value, callable $callback) + protected function runWithEnv(string $name, $value, callable $callback, bool $handleDatabase = true) { Env::disablePutenv(); $originalVal = $_SERVER[$name] ?? null; @@ -132,13 +132,17 @@ protected function runWithEnv(string $name, $value, callable $callback) $database = config('database.connections.mysql_testing.database'); $this->refreshApplication(); - DB::purge(); - config()->set('database.connections.mysql_testing.database', $database); - DB::beginTransaction(); + if ($handleDatabase) { + DB::purge(); + config()->set('database.connections.mysql_testing.database', $database); + DB::beginTransaction(); + } $callback(); - DB::rollBack(); + if ($handleDatabase) { + DB::rollBack(); + } if (is_null($originalVal)) { unset($_SERVER[$name]); diff --git a/tests/Unit/ConfigTest.php b/tests/Unit/ConfigTest.php index d5c74392ffc..2883fddb8fc 100644 --- a/tests/Unit/ConfigTest.php +++ b/tests/Unit/ConfigTest.php @@ -160,6 +160,27 @@ public function test_smtp_scheme_and_certain_port_forces_tls_usage() $this->assertTrue($isMailTlsRequired()); } + public function test_mysql_host_parsed_as_expected() + { + $cases = [ + '127.0.0.1' => ['127.0.0.1', 3306], + '127.0.0.1:3307' => ['127.0.0.1', 3307], + 'a.example.com' => ['a.example.com', 3306], + 'a.example.com:3307' => ['a.example.com', 3307], + '[::1]' => ['[::1]', 3306], + '[::1]:123' => ['[::1]', 123], + '[2001:db8:3c4d:0015:0000:0000:1a2f]' => ['[2001:db8:3c4d:0015:0000:0000:1a2f]', 3306], + '[2001:db8:3c4d:0015:0000:0000:1a2f]:4567' => ['[2001:db8:3c4d:0015:0000:0000:1a2f]', 4567], + ]; + + foreach ($cases as $host => [$expectedHost, $expectedPort]) { + $this->runWithEnv("DB_HOST", $host, function () use ($expectedHost, $expectedPort) { + $this->assertEquals($expectedHost, config("database.connections.mysql.host")); + $this->assertEquals($expectedPort, config("database.connections.mysql.port")); + }, false); + } + } + /** * Set an environment variable of the given name and value * then check the given config key to see if it matches the given result. From dfbc78947f276f8b6782dd1e238c7bf28a40c265 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Mar 2025 11:59:42 +0000 Subject: [PATCH 004/474] Revisions: Hid changes link for oldest revision Just as a UX improvement to help avoid confusion, as the whole content will be changes for this revision. For #5454 --- app/Entities/Controllers/PageRevisionController.php | 2 +- .../views/pages/parts/revisions-index-row.blade.php | 6 ++++-- resources/views/pages/revisions.blade.php | 6 +++++- tests/Entity/PageRevisionTest.php | 12 ++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index 232d40668f4..4985c39f351 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -43,7 +43,6 @@ public function index(Request $request, string $bookSlug, string $pageSlug) ->selectRaw("IF(markdown = '', false, true) as is_markdown") ->with(['page.book', 'createdBy']) ->reorder('id', $listOptions->getOrder()) - ->reorder('created_at', $listOptions->getOrder()) ->paginate(50); $this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()])); @@ -52,6 +51,7 @@ public function index(Request $request, string $bookSlug, string $pageSlug) 'revisions' => $revisions, 'page' => $page, 'listOptions' => $listOptions, + 'oldestRevisionId' => $page->revisions()->min('id'), ]); } diff --git a/resources/views/pages/parts/revisions-index-row.blade.php b/resources/views/pages/parts/revisions-index-row.blade.php index db89284bb47..48bea5b577c 100644 --- a/resources/views/pages/parts/revisions-index-row.blade.php +++ b/resources/views/pages/parts/revisions-index-row.blade.php @@ -27,8 +27,10 @@ {{ $revision->summary }}
- {{ trans('entities.pages_revisions_changes') }} -  |  + @if(!$oldest) + {{ trans('entities.pages_revisions_changes') }} +  |  + @endif @if ($current) diff --git a/resources/views/pages/revisions.blade.php b/resources/views/pages/revisions.blade.php index 9f462e930eb..e8eb8d68bf0 100644 --- a/resources/views/pages/revisions.blade.php +++ b/resources/views/pages/revisions.blade.php @@ -37,7 +37,11 @@
{{ trans('common.actions') }}
@foreach($revisions as $index => $revision) - @include('pages.parts.revisions-index-row', ['revision' => $revision, 'current' => $page->revision_count === $revision->revision_number]) + @include('pages.parts.revisions-index-row', [ + 'revision' => $revision, + 'current' => $page->revision_count === $revision->revision_number, + 'oldest' => $oldestRevisionId === $revision->id, + ]) @endforeach @else diff --git a/tests/Entity/PageRevisionTest.php b/tests/Entity/PageRevisionTest.php index a272dc38bdb..9040254f78f 100644 --- a/tests/Entity/PageRevisionTest.php +++ b/tests/Entity/PageRevisionTest.php @@ -203,6 +203,18 @@ public function test_revision_list_shows_editor_type() $this->withHtml($resp)->assertElementContains('.item-list-row > div:nth-child(2)', 'Markdown)'); } + public function test_revision_changes_link_not_shown_for_oldest_revision() + { + $page = $this->entities->page(); + $this->createRevisions($page, 3, ['html' => 'new page html']); + + $resp = $this->asAdmin()->get($page->refresh()->getUrl('/revisions')); + $html = $this->withHtml($resp); + + $html->assertElementNotExists('.item-list > .item-list-row:last-child a[href*="/changes"]'); + $html->assertElementContains('.item-list > .item-list-row:nth-child(2)', 'Changes'); + } + public function test_revision_restore_action_only_visible_with_permission() { $page = $this->entities->page(); From 304a1d8f917325d3fa3a0ea8894bd3f2c7c8c122 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Mar 2025 12:04:19 +0000 Subject: [PATCH 005/474] Dependancies: Updated PHP composer deps --- composer.lock | 469 ++++++++++++++++++++++---------------------------- 1 file changed, 204 insertions(+), 265 deletions(-) diff --git a/composer.lock b/composer.lock index 95bc3ae3716..4dfc8ec85a4 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.339.19", + "version": "3.342.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "18f05efe983860ad899082e04c13f06ec9fd6e41" + "reference": "2ad93ef447289da27892707efa683c1ae7ced85f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/18f05efe983860ad899082e04c13f06ec9fd6e41", - "reference": "18f05efe983860ad899082e04c13f06ec9fd6e41", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2ad93ef447289da27892707efa683c1ae7ced85f", + "reference": "2ad93ef447289da27892707efa683c1ae7ced85f", "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.339.19" + "source": "https://github.com/aws/aws-sdk-php/tree/3.342.6" }, - "time": "2025-02-21T19:13:15+00:00" + "time": "2025-03-14T18:01:18+00:00" }, { "name": "bacon/bacon-qr-code", @@ -213,16 +213,16 @@ }, { "name": "brick/math", - "version": "0.12.1", + "version": "0.12.3", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", - "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", "shasum": "" }, "require": { @@ -231,7 +231,7 @@ "require-dev": { "php-coveralls/php-coveralls": "^2.2", "phpunit/phpunit": "^10.1", - "vimeo/psalm": "5.16.0" + "vimeo/psalm": "6.8.8" }, "type": "library", "autoload": { @@ -261,7 +261,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.12.1" + "source": "https://github.com/brick/math/tree/0.12.3" }, "funding": [ { @@ -269,7 +269,7 @@ "type": "github" } ], - "time": "2023-11-29T23:19:16+00:00" + "time": "2025-02-28T13:11:00+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1597,16 +1597,16 @@ }, { "name": "intervention/image", - "version": "3.11.1", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/Intervention/image.git", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af" + "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af", - "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af", + "url": "https://api.github.com/repos/Intervention/image/zipball/ebbb711871fb261c064cf4c422f5f3c124fe1842", + "reference": "ebbb711871fb261c064cf4c422f5f3c124fe1842", "shasum": "" }, "require": { @@ -1617,7 +1617,7 @@ "require-dev": { "mockery/mockery": "^1.6", "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "slevomat/coding-standard": "~8.0", "squizlabs/php_codesniffer": "^3.8" }, @@ -1653,7 +1653,7 @@ ], "support": { "issues": "https://github.com/Intervention/image/issues", - "source": "https://github.com/Intervention/image/tree/3.11.1" + "source": "https://github.com/Intervention/image/tree/3.11.2" }, "funding": [ { @@ -1669,7 +1669,7 @@ "type": "ko_fi" } ], - "time": "2025-02-01T07:28:26+00:00" + "time": "2025-02-27T13:08:55+00:00" }, { "name": "knplabs/knp-snappy", @@ -1740,16 +1740,16 @@ }, { "name": "laravel/framework", - "version": "v11.43.2", + "version": "v11.44.2", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "99d1573698abc42222f04d25fcd5b213d0eedf21" + "reference": "f85216c82cbd38b66d67ebd20ea762cb3751a4b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/99d1573698abc42222f04d25fcd5b213d0eedf21", - "reference": "99d1573698abc42222f04d25fcd5b213d0eedf21", + "url": "https://api.github.com/repos/laravel/framework/zipball/f85216c82cbd38b66d67ebd20ea762cb3751a4b4", + "reference": "f85216c82cbd38b66d67ebd20ea762cb3751a4b4", "shasum": "" }, "require": { @@ -1857,7 +1857,7 @@ "league/flysystem-read-only": "^3.25.1", "league/flysystem-sftp-v3": "^3.25.1", "mockery/mockery": "^1.6.10", - "orchestra/testbench-core": "^9.9.4", + "orchestra/testbench-core": "^9.11.2", "pda/pheanstalk": "^5.0.6", "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", @@ -1951,7 +1951,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-02-19T21:53:48+00:00" + "time": "2025-03-12T14:34:30+00:00" }, { "name": "laravel/prompts", @@ -2810,16 +2810,16 @@ }, { "name": "league/oauth2-client", - "version": "2.8.0", + "version": "2.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-client.git", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13" + "reference": "9df2924ca644736c835fc60466a3a60390d334f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/3d5cf8d0543731dfb725ab30e4d7289891991e13", - "reference": "3d5cf8d0543731dfb725ab30e4d7289891991e13", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/9df2924ca644736c835fc60466a3a60390d334f9", + "reference": "9df2924ca644736c835fc60466a3a60390d334f9", "shasum": "" }, "require": { @@ -2869,9 +2869,9 @@ ], "support": { "issues": "https://github.com/thephpleague/oauth2-client/issues", - "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.0" + "source": "https://github.com/thephpleague/oauth2-client/tree/2.8.1" }, - "time": "2024-12-11T05:05:52+00:00" + "time": "2025-02-26T04:37:30+00:00" }, { "name": "league/uri", @@ -4575,16 +4575,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.7", + "version": "v0.12.8", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c" + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", - "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625", + "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625", "shasum": "" }, "require": { @@ -4648,9 +4648,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.7" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.8" }, - "time": "2024-12-10T01:58:33+00:00" + "time": "2025-03-16T03:05:19+00:00" }, { "name": "ralouphie/getallheaders", @@ -4698,16 +4698,16 @@ }, { "name": "ramsey/collection", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", - "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", "shasum": "" }, "require": { @@ -4715,25 +4715,22 @@ }, "require-dev": { "captainhook/plugin-composer": "^5.3", - "ergebnis/composer-normalize": "^2.28.3", - "fakerphp/faker": "^1.21", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", "hamcrest/hamcrest-php": "^2.0", - "jangregor/phpstan-prophecy": "^1.0", - "mockery/mockery": "^1.5", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.3", - "phpcsstandards/phpcsutils": "^1.0.0-rc1", - "phpspec/prophecy-phpunit": "^2.0", - "phpstan/extension-installer": "^1.2", - "phpstan/phpstan": "^1.9", - "phpstan/phpstan-mockery": "^1.1", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5", - "psalm/plugin-mockery": "^1.1", - "psalm/plugin-phpunit": "^0.18.4", - "ramsey/coding-standard": "^2.0.3", - "ramsey/conventional-commits": "^1.3", - "vimeo/psalm": "^5.4" + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" }, "type": "library", "extra": { @@ -4771,19 +4768,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.0.0" + "source": "https://github.com/ramsey/collection/tree/2.1.0" }, - "funding": [ - { - "url": "https://github.com/ramsey", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", - "type": "tidelift" - } - ], - "time": "2022-12-31T21:50:55+00:00" + "time": "2025-03-02T04:48:29+00:00" }, { "name": "ramsey/uuid", @@ -5077,20 +5064,20 @@ }, { "name": "socialiteproviders/manager", - "version": "v4.8.0", + "version": "v4.8.1", "source": { "type": "git", "url": "https://github.com/SocialiteProviders/Manager.git", - "reference": "e93acc38f8464cc775a2b8bf09df311d1fdfefcb" + "reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/e93acc38f8464cc775a2b8bf09df311d1fdfefcb", - "reference": "e93acc38f8464cc775a2b8bf09df311d1fdfefcb", + "url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/8180ec14bef230ec2351cff993d5d2d7ca470ef4", + "reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4", "shasum": "" }, "require": { - "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", "laravel/socialite": "^5.5", "php": "^8.1" }, @@ -5147,7 +5134,7 @@ "issues": "https://github.com/socialiteproviders/manager/issues", "source": "https://github.com/socialiteproviders/manager" }, - "time": "2025-01-03T09:40:37+00:00" + "time": "2025-02-24T19:33:30+00:00" }, { "name": "socialiteproviders/microsoft-azure", @@ -5726,16 +5713,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49" + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49", - "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", + "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", "shasum": "" }, "require": { @@ -5781,7 +5768,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.3" + "source": "https://github.com/symfony/error-handler/tree/v7.2.4" }, "funding": [ { @@ -5797,7 +5784,7 @@ "type": "tidelift" } ], - "time": "2025-01-07T09:39:55+00:00" + "time": "2025-02-02T20:27:07+00:00" }, { "name": "symfony/event-dispatcher", @@ -6099,16 +6086,16 @@ }, { "name": "symfony/http-kernel", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b" + "reference": "9f1103734c5789798fefb90e91de4586039003ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", - "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", + "reference": "9f1103734c5789798fefb90e91de4586039003ed", "shasum": "" }, "require": { @@ -6193,7 +6180,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.2.3" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.4" }, "funding": [ { @@ -6209,20 +6196,20 @@ "type": "tidelift" } ], - "time": "2025-01-29T07:40:13+00:00" + "time": "2025-02-26T11:01:22+00:00" }, { "name": "symfony/mime", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204" + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204", - "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204", + "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", + "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", "shasum": "" }, "require": { @@ -6277,7 +6264,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.3" + "source": "https://github.com/symfony/mime/tree/v7.2.4" }, "funding": [ { @@ -6293,7 +6280,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-02-19T08:51:20+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6933,16 +6920,16 @@ }, { "name": "symfony/process", - "version": "v7.2.0", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e" + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", - "reference": "d34b22ba9390ec19d2dd966c40aa9e8462f27a7e", + "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", "shasum": "" }, "require": { @@ -6974,7 +6961,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.0" + "source": "https://github.com/symfony/process/tree/v7.2.4" }, "funding": [ { @@ -6990,7 +6977,7 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:24:19+00:00" + "time": "2025-02-05T08:33:46+00:00" }, { "name": "symfony/routing", @@ -7245,16 +7232,16 @@ }, { "name": "symfony/translation", - "version": "v7.2.2", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923" + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/e2674a30132b7cc4d74540d6c2573aa363f05923", - "reference": "e2674a30132b7cc4d74540d6c2573aa363f05923", + "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", + "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", "shasum": "" }, "require": { @@ -7320,7 +7307,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.2" + "source": "https://github.com/symfony/translation/tree/v7.2.4" }, "funding": [ { @@ -7336,7 +7323,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T08:18:10+00:00" + "time": "2025-02-13T10:27:23+00:00" }, { "name": "symfony/translation-contracts", @@ -7911,16 +7898,16 @@ }, { "name": "filp/whoops", - "version": "2.17.0", + "version": "2.18.0", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e" + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/075bc0c26631110584175de6523ab3f1652eb28e", - "reference": "075bc0c26631110584175de6523ab3f1652eb28e", + "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", "shasum": "" }, "require": { @@ -7970,7 +7957,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.17.0" + "source": "https://github.com/filp/whoops/tree/2.18.0" }, "funding": [ { @@ -7978,7 +7965,7 @@ "type": "github" } ], - "time": "2025-01-25T12:00:00+00:00" + "time": "2025-03-15T12:00:00+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -8031,6 +8018,47 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "iamcal/sql-parser", + "version": "v0.5", + "source": { + "type": "git", + "url": "https://github.com/iamcal/SQLParser.git", + "reference": "644fd994de3b54e5d833aecf406150aa3b66ca88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/644fd994de3b54e5d833aecf406150aa3b66ca88", + "reference": "644fd994de3b54e5d833aecf406150aa3b66ca88", + "shasum": "" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^1.0", + "phpunit/phpunit": "^5|^6|^7|^8|^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "iamcal\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Cal Henderson", + "email": "cal@iamcal.com" + } + ], + "description": "MySQL schema parser", + "support": { + "issues": "https://github.com/iamcal/SQLParser/issues", + "source": "https://github.com/iamcal/SQLParser/tree/v0.5" + }, + "time": "2024-03-22T22:46:32+00:00" + }, { "name": "itsgoingd/clockwork", "version": "v5.3.4", @@ -8109,34 +8137,34 @@ }, { "name": "larastan/larastan", - "version": "v3.1.0", + "version": "v3.2.0", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "dbb2dc20e5c8e1ed3ff289054e1955f269187312" + "reference": "d84d5a3b6536a586899ad6855a3e098473703690" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/dbb2dc20e5c8e1ed3ff289054e1955f269187312", - "reference": "dbb2dc20e5c8e1ed3ff289054e1955f269187312", + "url": "https://api.github.com/repos/larastan/larastan/zipball/d84d5a3b6536a586899ad6855a3e098473703690", + "reference": "d84d5a3b6536a586899ad6855a3e098473703690", "shasum": "" }, "require": { "ext-json": "*", - "illuminate/console": "^11.15.0 || ^12.0", - "illuminate/container": "^11.15.0 || ^12.0", - "illuminate/contracts": "^11.15.0 || ^12.0", - "illuminate/database": "^11.15.0 || ^12.0", - "illuminate/http": "^11.15.0 || ^12.0", - "illuminate/pipeline": "^11.15.0 || ^12.0", - "illuminate/support": "^11.15.0 || ^12.0", + "iamcal/sql-parser": "^0.5.0", + "illuminate/console": "^11.41.3 || ^12.0", + "illuminate/container": "^11.41.3 || ^12.0", + "illuminate/contracts": "^11.41.3 || ^12.0", + "illuminate/database": "^11.41.3 || ^12.0", + "illuminate/http": "^11.41.3 || ^12.0", + "illuminate/pipeline": "^11.41.3 || ^12.0", + "illuminate/support": "^11.41.3 || ^12.0", "php": "^8.2", - "phpmyadmin/sql-parser": "^5.9.0", "phpstan/phpstan": "^2.1.3" }, "require-dev": { "doctrine/coding-standard": "^12.0", - "laravel/framework": "^11.15.0 || ^12.0", + "laravel/framework": "^11.41.3 || ^12.0", "mockery/mockery": "^1.6", "nikic/php-parser": "^5.3", "orchestra/canvas": "^v9.1.3 || ^10.0", @@ -8190,7 +8218,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.1.0" + "source": "https://github.com/larastan/larastan/tree/v3.2.0" }, "funding": [ { @@ -8198,7 +8226,7 @@ "type": "github" } ], - "time": "2025-02-20T15:25:15+00:00" + "time": "2025-03-14T21:54:26+00:00" }, { "name": "mockery/mockery", @@ -8345,20 +8373,20 @@ }, { "name": "nunomaduro/collision", - "version": "v8.6.1", + "version": "v8.7.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7" + "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/86f003c132143d5a2ab214e19933946409e0cae7", - "reference": "86f003c132143d5a2ab214e19933946409e0cae7", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/586cb8181a257a2152b6a855ca8d9598878a1a26", + "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26", "shasum": "" }, "require": { - "filp/whoops": "^2.16.0", + "filp/whoops": "^2.17.0", "nunomaduro/termwind": "^2.3.0", "php": "^8.2.0", "symfony/console": "^7.2.1" @@ -8368,14 +8396,14 @@ "phpunit/phpunit": "<11.5.3 || >=12.0.0" }, "require-dev": { - "larastan/larastan": "^2.9.12", - "laravel/framework": "^11.39.1", - "laravel/pint": "^1.20.0", - "laravel/sail": "^1.40.0", - "laravel/sanctum": "^4.0.7", - "laravel/tinker": "^2.10.0", - "orchestra/testbench-core": "^9.9.2", - "pestphp/pest": "^3.7.3", + "larastan/larastan": "^2.10.0", + "laravel/framework": "^11.44.2", + "laravel/pint": "^1.21.2", + "laravel/sail": "^1.41.0", + "laravel/sanctum": "^4.0.8", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0", + "pestphp/pest": "^3.7.4", "sebastian/environment": "^6.1.0 || ^7.2.0" }, "type": "library", @@ -8439,7 +8467,7 @@ "type": "patreon" } ], - "time": "2025-01-23T13:41:43+00:00" + "time": "2025-03-14T22:37:40+00:00" }, { "name": "phar-io/manifest", @@ -8559,107 +8587,18 @@ }, "time": "2022-02-21T01:04:05+00:00" }, - { - "name": "phpmyadmin/sql-parser", - "version": "5.11.0", - "source": { - "type": "git", - "url": "https://github.com/phpmyadmin/sql-parser.git", - "reference": "07044bc8c13abd542756c3fd34dc66a5d6dee8e4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/07044bc8c13abd542756c3fd34dc66a5d6dee8e4", - "reference": "07044bc8c13abd542756c3fd34dc66a5d6dee8e4", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0", - "symfony/polyfill-mbstring": "^1.3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "phpmyadmin/motranslator": "<3.0" - }, - "require-dev": { - "phpbench/phpbench": "^1.1", - "phpmyadmin/coding-standard": "^3.0", - "phpmyadmin/motranslator": "^4.0 || ^5.0", - "phpstan/extension-installer": "^1.4", - "phpstan/phpstan": "^1.12", - "phpstan/phpstan-deprecation-rules": "^1.2", - "phpstan/phpstan-phpunit": "^1.4", - "phpstan/phpstan-strict-rules": "^1.6", - "phpunit/phpunit": "^8.5 || ^9.6", - "psalm/plugin-phpunit": "^0.16.1", - "vimeo/psalm": "^4.11", - "zumba/json-serializer": "~3.0.2" - }, - "suggest": { - "ext-mbstring": "For best performance", - "phpmyadmin/motranslator": "Translate messages to your favorite locale" - }, - "bin": [ - "bin/highlight-query", - "bin/lint-query", - "bin/sql-parser", - "bin/tokenize-query" - ], - "type": "library", - "autoload": { - "psr-4": { - "PhpMyAdmin\\SqlParser\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ - { - "name": "The phpMyAdmin Team", - "email": "developers@phpmyadmin.net", - "homepage": "https://www.phpmyadmin.net/team/" - } - ], - "description": "A validating SQL lexer and parser with a focus on MySQL dialect.", - "homepage": "https://github.com/phpmyadmin/sql-parser", - "keywords": [ - "analysis", - "lexer", - "parser", - "query linter", - "sql", - "sql lexer", - "sql linter", - "sql parser", - "sql syntax highlighter", - "sql tokenizer" - ], - "support": { - "issues": "https://github.com/phpmyadmin/sql-parser/issues", - "source": "https://github.com/phpmyadmin/sql-parser" - }, - "funding": [ - { - "url": "https://www.phpmyadmin.net/donate/", - "type": "other" - } - ], - "time": "2025-02-22T20:00:59+00:00" - }, { "name": "phpstan/phpstan", - "version": "2.1.6", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c" + "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", - "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f", + "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f", "shasum": "" }, "require": { @@ -8704,27 +8643,27 @@ "type": "github" } ], - "time": "2025-02-19T15:46:42+00:00" + "time": "2025-03-09T09:30:48+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "11.0.8", + "version": "11.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118" + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/418c59fd080954f8c4aa5631d9502ecda2387118", - "reference": "418c59fd080954f8c4aa5631d9502ecda2387118", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^5.3.1", + "nikic/php-parser": "^5.4.0", "php": ">=8.2", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-text-template": "^4.0.1", @@ -8736,7 +8675,7 @@ "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^11.5.0" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -8774,7 +8713,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.8" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" }, "funding": [ { @@ -8782,7 +8721,7 @@ "type": "github" } ], - "time": "2024-12-11T12:34:27+00:00" + "time": "2025-02-25T13:26:39+00:00" }, { "name": "phpunit/php-file-iterator", @@ -9031,16 +8970,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.9", + "version": "11.5.12", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b" + "reference": "d42785840519401ed2113292263795eb4c0f95da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b", - "reference": "c91c830e7108a81e5845aeb6ba8fe3c1a4351c0b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d42785840519401ed2113292263795eb4c0f95da", + "reference": "d42785840519401ed2113292263795eb4c0f95da", "shasum": "" }, "require": { @@ -9054,14 +8993,14 @@ "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", - "phpunit/php-code-coverage": "^11.0.8", + "phpunit/php-code-coverage": "^11.0.9", "phpunit/php-file-iterator": "^5.1.0", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", "sebastian/code-unit": "^3.0.2", - "sebastian/comparator": "^6.3.0", + "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.3.0", @@ -9112,7 +9051,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.9" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.12" }, "funding": [ { @@ -9128,7 +9067,7 @@ "type": "tidelift" } ], - "time": "2025-02-21T06:08:50+00:00" + "time": "2025-03-07T07:31:03+00:00" }, { "name": "sebastian/cli-parser", @@ -9302,16 +9241,16 @@ }, { "name": "sebastian/comparator", - "version": "6.3.0", + "version": "6.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115" + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/d4e47a769525c4dd38cea90e5dcd435ddbbc7115", - "reference": "d4e47a769525c4dd38cea90e5dcd435ddbbc7115", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "shasum": "" }, "require": { @@ -9330,7 +9269,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.2-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -9370,7 +9309,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.0" + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" }, "funding": [ { @@ -9378,7 +9317,7 @@ "type": "github" } ], - "time": "2025-01-06T10:28:19+00:00" + "time": "2025-03-07T06:57:01+00:00" }, { "name": "sebastian/complexity", @@ -10238,16 +10177,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v7.2.3", + "version": "v7.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "700a880e5089280c7cf3ca1ccf9d9de6630f5d25" + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/700a880e5089280c7cf3ca1ccf9d9de6630f5d25", - "reference": "700a880e5089280c7cf3ca1ccf9d9de6630f5d25", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", + "reference": "19cc7b08efe9ad1ab1b56e0948e8d02e15ed3ef7", "shasum": "" }, "require": { @@ -10285,7 +10224,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.2.3" + "source": "https://github.com/symfony/dom-crawler/tree/v7.2.4" }, "funding": [ { @@ -10301,7 +10240,7 @@ "type": "tidelift" } ], - "time": "2025-01-27T11:08:17+00:00" + "time": "2025-02-17T15:53:07+00:00" }, { "name": "theseer/tokenizer", From 8a9215ecad16747788dd449f296f845fdbc75f4f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Mar 2025 12:25:53 +0000 Subject: [PATCH 006/474] Updated translations with latest Crowdin changes (#5505) --- lang/ar/activities.php | 30 ++-- lang/ar/auth.php | 48 +++--- lang/ar/common.php | 8 +- lang/ar/components.php | 26 ++-- lang/ar/entities.php | 318 +++++++++++++++++++------------------- lang/cs/settings.php | 2 +- lang/et/settings.php | 2 +- lang/fr/activities.php | 12 +- lang/he/settings.php | 6 +- lang/he/validation.php | 2 +- lang/hu/activities.php | 30 ++-- lang/hu/auth.php | 20 +-- lang/hu/components.php | 2 +- lang/hu/editor.php | 6 +- lang/hu/entities.php | 10 +- lang/hu/errors.php | 4 +- lang/hu/preferences.php | 14 +- lang/it/settings.php | 2 +- lang/ja/activities.php | 18 +-- lang/ja/editor.php | 4 +- lang/ja/entities.php | 6 +- lang/ja/settings.php | 56 +++---- lang/ko/activities.php | 2 +- lang/ko/editor.php | 4 +- lang/ko/settings.php | 4 +- lang/nb/activities.php | 12 +- lang/nb/editor.php | 8 +- lang/nb/entities.php | 6 +- lang/nb/settings.php | 40 ++--- lang/nl/settings.php | 24 +-- lang/nn/errors.php | 2 +- lang/pl/activities.php | 2 +- lang/sv/notifications.php | 32 ++-- lang/tr/activities.php | 26 ++-- 34 files changed, 394 insertions(+), 394 deletions(-) diff --git a/lang/ar/activities.php b/lang/ar/activities.php index 22386fd131f..db807b015e4 100644 --- a/lang/ar/activities.php +++ b/lang/ar/activities.php @@ -85,12 +85,12 @@ 'webhook_delete_notification' => 'تم حذف Webhook بنجاح', // Imports - 'import_create' => 'created import', - 'import_create_notification' => 'Import successfully uploaded', - 'import_run' => 'updated import', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_create' => 'تم إنشاء الاستيراد', + 'import_create_notification' => 'تم رفع الاستيراد بنجاح', + 'import_run' => 'تم تحديث الاستيراد', + 'import_run_notification' => 'تم استيراد المحتوى بنجاح', + 'import_delete' => 'تم حذف الاستيراد', + 'import_delete_notification' => 'تم الاستيراد بنجاح', // Users 'user_create' => 'إنشاء مستخدم', @@ -101,11 +101,11 @@ 'user_delete_notification' => 'تم إزالة المستخدم بنجاح', // API Tokens - 'api_token_create' => 'created API token', + 'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-', 'api_token_create_notification' => 'تم إنشاء رمز الـ API بنجاح', - 'api_token_update' => 'updated API token', + 'api_token_update' => 'رمز واجهة برمجة التطبيقات المحدث', 'api_token_update_notification' => 'تم تحديث رمز الـ API بنجاح', - 'api_token_delete' => 'deleted API token', + 'api_token_delete' => 'رمز واجهة برمجة التطبيقات المحذوف', 'api_token_delete_notification' => 'تم حذف رمز الـ API بنجاح', // Roles @@ -128,12 +128,12 @@ 'comment_delete' => 'تعليق محذوف', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/ar/auth.php b/lang/ar/auth.php index 0d3b12f8fc6..aa406bcece4 100644 --- a/lang/ar/auth.php +++ b/lang/ar/auth.php @@ -87,31 +87,31 @@ 'mfa_setup_reconfigure' => 'إعادة التكوين', 'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العناصر هذه؟', 'mfa_setup_action' => 'إعداد (تنصيب)', - 'mfa_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.', + 'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.', 'mfa_option_totp_title' => 'تطبيق الجوال', 'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق محمول يدعم TOTP مثل Google Authenticator أو Authy أو Microsoft Authenticer.', 'mfa_option_backup_codes_title' => 'رموز النسخ الاحتياطي', - 'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.', - 'mfa_gen_confirm_and_enable' => 'Confirm and Enable', - 'mfa_gen_backup_codes_title' => 'Backup Codes Setup', - 'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.', - 'mfa_gen_backup_codes_download' => 'Download Codes', - 'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once', - 'mfa_gen_totp_title' => 'Mobile App Setup', - 'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.', - 'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.', - 'mfa_gen_totp_verify_setup' => 'Verify Setup', - 'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:', - 'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here', - 'mfa_verify_access' => 'Verify Access', - 'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.', - 'mfa_verify_no_methods' => 'No Methods Configured', - 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', - 'mfa_verify_use_totp' => 'Verify using a mobile app', - 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', - 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', - 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', - 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', - 'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.', + '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- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.', + 'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد', + 'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:', + '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/ar/common.php b/lang/ar/common.php index 454b36a9448..07576a2505a 100644 --- a/lang/ar/common.php +++ b/lang/ar/common.php @@ -20,7 +20,7 @@ 'description' => 'الوصف', 'role' => 'الدور', 'cover_image' => 'صورة الغلاف', - 'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440x250 بكسل، على الرغم من أنه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.', + 'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440 في 250 بكسل، مع أنّه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.', // Actions 'actions' => 'إجراءات', @@ -48,8 +48,8 @@ 'unfavourite' => 'إزالة من المفضلة', 'next' => 'التالي', 'previous' => 'السابق', - 'filter_active' => 'الفلاتر المفعلة:', - 'filter_clear' => 'مسح الفلاتر', + 'filter_active' => 'التصفية المفعلة:', + 'filter_clear' => 'مسح التصفية', 'download' => 'تنزيل', 'open_in_tab' => 'فتح في علامة تبويب', 'open' => 'فتح', @@ -109,5 +109,5 @@ 'terms_of_service' => 'اتفاقية شروط الخدمة', // OpenSearch - 'opensearch_description' => 'Search :appName', + 'opensearch_description' => 'البحث عن :appName', ]; diff --git a/lang/ar/components.php b/lang/ar/components.php index 211b249eadd..12bc0e58ae9 100644 --- a/lang/ar/components.php +++ b/lang/ar/components.php @@ -6,36 +6,36 @@ // Image Manager 'image_select' => 'تحديد صورة', - 'image_list' => 'Image List', - 'image_details' => 'Image Details', - 'image_upload' => 'Upload Image', - 'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.', - 'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.', + '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' => 'Uploaded by :userName', - 'image_uploaded_to' => 'Uploaded to :pageLink', - 'image_updated' => 'Updated :updateDate', + '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' => 'Drop images here to upload', + 'image_dropzone_drop' => 'إسقاط صورة أو اضغط هنا للرفع', 'images_deleted' => 'تم حذف الصور', 'image_preview' => 'معاينة الصور', 'image_upload_success' => 'تم رفع الصورة بنجاح', 'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح', 'image_delete_success' => 'تم حذف الصورة بنجاح', - 'image_replace' => 'Replace Image', - 'image_replace_success' => 'Image file successfully updated', - 'image_rebuild_thumbs' => 'Regenerate Size Variations', - 'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!', + 'image_replace' => 'استبدال صورة', + 'image_replace_success' => 'تم تحديث الصورة بنجاح', + 'image_rebuild_thumbs' => 'تجديد تغيرات الحجم', + 'image_rebuild_thumbs_success' => 'تم إعادة بناء تغيرات حجم الصورة بنجاح!', // Code Editor 'code_editor' => 'تعديل الشفرة', diff --git a/lang/ar/entities.php b/lang/ar/entities.php index 005b1f714fc..d012ca45944 100644 --- a/lang/ar/entities.php +++ b/lang/ar/entities.php @@ -22,15 +22,15 @@ 'meta_created_name' => 'أنشئ :timeLength بواسطة :user', 'meta_updated' => 'مُحدث :timeLength', 'meta_updated_name' => 'مُحدث :timeLength بواسطة :user', - 'meta_owned_name' => 'Owned by :user', - 'meta_reference_count' => 'Referenced by :count item|Referenced by :count items', + 'meta_owned_name' => 'مملوكة لـ:user', + 'meta_reference_count' => 'مشار إليه :count مرة|مشار إليه :count مرة', 'entity_select' => 'اختيار الكيان', - 'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item', + 'entity_select_lack_permission' => 'ليس لديك الصلاحيات المطلوبة لتحديد هذا العنصر', 'images' => 'صور', 'my_recent_drafts' => 'مسوداتي الحديثة', 'my_recently_viewed' => 'ما عرضته مؤخراً', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'مفضلاتي الأكثر مشاهدة', + 'my_favourites' => 'مفضلاتي', 'no_pages_viewed' => 'لم تستعرض أي صفحات', 'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً', 'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً', @@ -38,43 +38,43 @@ 'export_html' => 'صفحة ويب', 'export_pdf' => 'ملف PDF', 'export_text' => 'ملف نص عادي', - 'export_md' => 'Markdown File', - 'export_zip' => 'Portable ZIP', - 'default_template' => 'Default Page Template', - 'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.', - 'default_template_select' => 'Select a template page', - '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:', + '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 الذي تم تحميله. عندما يكون جاهزًا، تشتغل عملية الاستيراد لإضافة محتوياته إلى هذا النظام. سيتم إزالة مِلَفّ الاستيراد الذي تم تحميله تلقائيًا عند الاستيراد الناجح.', + 'import_details' => 'تفاصيل الاستيراد', + 'import_run' => 'تشغيل الاستيراد', + 'import_size' => 'حجم الاستيراد :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' => 'حدثت الأخطاء التالية خلال محاولة الاستيراد:', // Permissions and restrictions 'permissions' => 'الأذونات', - 'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.', - 'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.', - 'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.', + 'permissions_desc' => 'تعيين الصلاحيات هنا لتجاوز الصلاحيات الافتراضية التي توفرها أدوار المستخدم.', + 'permissions_book_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها للكتب تلقائيًا إلى الفصول والصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.', + 'permissions_chapter_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها على الفصول تلقائيًا إلى الصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.', 'permissions_save' => 'حفظ الأذونات', - 'permissions_owner' => 'Owner', - 'permissions_role_everyone_else' => 'Everyone Else', - 'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.', - 'permissions_role_override' => 'Override permissions for role', - 'permissions_inherit_defaults' => 'Inherit defaults', + 'permissions_owner' => 'المالك', + 'permissions_role_everyone_else' => 'الآخرين', + 'permissions_role_everyone_else_desc' => 'تعيين الصلاحيات لجميع الأدوار التي لم يتم تجاوزها على وجه التحديد.', + 'permissions_role_override' => 'تجاوز الصلاحيات للدور', + 'permissions_inherit_defaults' => 'وراثة الإعدادات الافتراضية', // Search 'search_results' => 'نتائج البحث', @@ -94,7 +94,7 @@ 'search_permissions_set' => 'حزمة الأذونات', 'search_created_by_me' => 'أنشئت بواسطتي', 'search_updated_by_me' => 'حُدثت بواسطتي', - 'search_owned_by_me' => 'Owned by me', + 'search_owned_by_me' => 'مملوكة لي', 'search_date_options' => 'خيارات التاريخ', 'search_updated_before' => 'حدثت قبل', 'search_updated_after' => 'حدثت بعد', @@ -117,24 +117,24 @@ 'shelves_save' => 'حفظ الرف', 'shelves_books' => 'كتب على هذا الرف', 'shelves_add_books' => 'إضافة كتب لهذا الرف', - 'shelves_drag_books' => 'Drag books below to add them to this shelf', + 'shelves_drag_books' => 'اسحب الكتب الموجودة بالأسفل لإضافتها إلى هذا الرف', 'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف', 'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب', - 'shelves_edit_named' => 'Edit Shelf :name', - 'shelves_edit' => 'Edit Shelf', - 'shelves_delete' => 'Delete Shelf', - 'shelves_delete_named' => 'Delete Shelf :name', - 'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.", - 'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?', - 'shelves_permissions' => 'Shelf Permissions', - 'shelves_permissions_updated' => 'Shelf Permissions Updated', - 'shelves_permissions_active' => 'Shelf Permissions Active', - 'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', - 'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.', + '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' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.', - 'shelves_copy_permission_success' => 'Shelf permissions copied to :count books', + 'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الصلاحيات الحالية لهذا الرف على جميع الكتب الموجودة بداخله. قبل التنشيط، تأكد من حفظ أي تغييرات على صلاحيات هذا الرف.', + 'shelves_copy_permission_success' => 'تم نسخ صلاحيات الرف إلى :count كتاب/كتب', // Books 'book' => 'كتاب', @@ -166,9 +166,9 @@ '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_named' => 'فرز كتاب :bookName', 'books_sort_name' => 'ترتيب حسب الإسم', 'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء', @@ -177,19 +177,19 @@ 'books_sort_chapters_last' => 'الفصول الأخيرة', 'books_sort_show_other' => 'عرض كتب أخرى', 'books_sort_save' => 'حفظ الترتيب الجديد', - 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', - 'books_sort_move_up' => 'Move Up', - 'books_sort_move_down' => 'Move Down', - 'books_sort_move_prev_book' => 'Move to Previous Book', - 'books_sort_move_next_book' => 'Move to Next Book', - 'books_sort_move_prev_chapter' => 'Move Into Previous Chapter', - 'books_sort_move_next_chapter' => 'Move Into Next Chapter', - 'books_sort_move_book_start' => 'Move to Start of Book', - 'books_sort_move_book_end' => 'Move to End of Book', - 'books_sort_move_before_chapter' => 'Move to Before Chapter', - 'books_sort_move_after_chapter' => 'Move to After Chapter', - 'books_copy' => 'Copy Book', - 'books_copy_success' => 'Book successfully copied', + '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' => 'فصل', @@ -200,21 +200,21 @@ 'chapters_create' => 'إنشاء فصل جديد', 'chapters_delete' => 'حذف الفصل', 'chapters_delete_named' => 'حذف فصل :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', + 'chapters_delete_explain' => 'سيؤدي هذا إلى حذف الفصل الذي يحمل الاسم \':chapterName\'. كما سيتم حذف جميع الصفحات الموجودة داخل هذا الفصل.', 'chapters_delete_confirm' => 'تأكيد حذف الفصل؟', 'chapters_edit' => 'تعديل الفصل', 'chapters_edit_named' => 'تعديل فصل :chapterName', 'chapters_save' => 'حفظ الفصل', 'chapters_move' => 'نقل الفصل', 'chapters_move_named' => 'نقل فصل :chapterName', - 'chapters_copy' => 'Copy Chapter', - 'chapters_copy_success' => 'Chapter successfully copied', + 'chapters_copy' => 'نسخ الفصل', + 'chapters_copy_success' => 'تم نسخ الفصل بنجاح', 'chapters_permissions' => 'أذونات الفصل', 'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً', 'chapters_permissions_active' => 'أذونات الفصل مفعلة', 'chapters_permissions_success' => 'تم تحديث أذونات الفصل', 'chapters_search_this' => 'البحث في هذا الفصل', - 'chapter_sort_book' => 'Sort Book', + 'chapter_sort_book' => 'فرز الكتاب', // Pages 'page' => 'صفحة', @@ -230,7 +230,7 @@ 'pages_delete_draft' => 'حذف المسودة', 'pages_delete_success' => 'تم حذف الصفحة', 'pages_delete_draft_success' => 'تم حذف المسودة', - 'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.', + 'pages_delete_warning_template' => 'هذه الصفحة قيد الاستخدام كقالب افتراضي لصفحات الكتب أو الفصول. لن يكون لهذه الكتب أو الفصول قالب افتراضي بعد حذفها.', 'pages_delete_confirm' => 'تأكيد حذف الصفحة؟', 'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟', 'pages_editing_named' => ':pageName قيد التعديل', @@ -241,23 +241,23 @@ 'pages_editing_page' => 'الصفحة قيد التعديل', 'pages_edit_draft_save_at' => 'تم خفظ المسودة في ', 'pages_edit_delete_draft' => 'حذف المسودة', - 'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.', + 'pages_edit_delete_draft_confirm' => 'متيقِّن من رغبتك في حذف تغييرات صفحة المُسَوَّدَة؟ ستُفقد جميع تغييراتك، منذ آخر حفظ كامل، وسيتم تحديث المحرر بأحدث حالة حفظ للصفحة (غير مسودة).', 'pages_edit_discard_draft' => 'التخلص من المسودة', - 'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor', - 'pages_edit_switch_to_markdown_clean' => '(Clean Content)', - 'pages_edit_switch_to_markdown_stable' => '(Stable Content)', - 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)', + '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' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه الجديد -new WYSIWYG-', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(في اختبار ألف)', 'pages_edit_set_changelog' => 'تثبيت سجل التعديل', 'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت', 'pages_edit_enter_changelog' => 'أدخل سجل التعديل', - 'pages_editor_switch_title' => 'Switch Editor', - 'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?', - 'pages_editor_switch_consider_following' => 'Consider the following when changing editors:', - 'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.', - 'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.', - 'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.', + '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' => 'لن تستمر تغييرات العلامة أو سجل التغييرات، التي تم إجراؤها منذ الحفظ الأخير، عبر هذا التغيير.', 'pages_save' => 'حفظ الصفحة', 'pages_title' => 'عنوان الصفحة', 'pages_name' => 'اسم الصفحة', @@ -266,10 +266,10 @@ 'pages_md_insert_image' => 'إدخال صورة', 'pages_md_insert_link' => 'إدراج ارتباط الكيان', 'pages_md_insert_drawing' => 'إدخال رسمة', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', - 'pages_drawing_unsaved' => 'Unsaved Drawing Found', - 'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?', + 'pages_md_show_preview' => 'عرض المعاينة', + 'pages_md_sync_scroll' => 'مزامنة معاينة التمرير', + 'pages_drawing_unsaved' => 'تم العثور على رسم غير محفوظ', + 'pages_drawing_unsaved_confirm' => 'تم العثور على بيانات رسم غير محفوظة من محاولة حفظ رسم سابقة فاشلة. هل ترغب في استعادة هذا الرسم غير المحفوظ ومواصلة تحريره؟', 'pages_not_in_chapter' => 'صفحة ليست في فصل', 'pages_move' => 'نقل الصفحة', 'pages_copy' => 'نسخ الصفحة', @@ -279,17 +279,17 @@ 'pages_permissions_success' => 'تم تحديث أذونات الصفحة', 'pages_revision' => 'مراجعة', 'pages_revisions' => 'مراجعات الصفحة', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => 'تجد أدناه جميع الإصدارات السابقة لهذه الصفحة. يمكنك الاطلاع عليها ومقارنتها واستعادة الإصدارات القديمة إذا سمحت الصلاحيات بذلك. قد لا يظهر تاريخ الصفحة بالكامل هنا، إذ قد تُحذف الإصدارات القديمة تلقائيًا، وذلك حسب إعدادات النظام.', 'pages_revisions_named' => 'مراجعات صفحة :pageName', 'pages_revision_named' => 'مراجعة صفحة :pageName', - 'pages_revision_restored_from' => 'Restored from #:id; :summary', + 'pages_revision_restored_from' => 'تم الاستعادة من #:id; :summary', 'pages_revisions_created_by' => 'أنشئ بواسطة', 'pages_revisions_date' => 'تاريخ المراجعة', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'رَقْم المراجعة', 'pages_revisions_numbered' => 'مراجعة #:id', 'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات', - 'pages_revisions_editor' => 'Editor Type', + 'pages_revisions_editor' => 'نوع المحرر', 'pages_revisions_changelog' => 'سجل التعديل', 'pages_revisions_changes' => 'التعديلات', 'pages_revisions_current' => 'النسخة الحالية', @@ -297,20 +297,20 @@ 'pages_revisions_restore' => 'استرجاع', 'pages_revisions_none' => 'لا توجد مراجعات لهذه الصفحة', 'pages_copy_link' => 'نسخ الرابط', - 'pages_edit_content_link' => 'Jump to section in editor', - 'pages_pointer_enter_mode' => 'Enter section select mode', - 'pages_pointer_label' => 'Page Section Options', - 'pages_pointer_permalink' => 'Page Section Permalink', - 'pages_pointer_include_tag' => 'Page Section Include Tag', - 'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag', - 'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink', + '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' => 'System auto-update of internal links', + 'pages_references_update_revision' => 'التحديث التلقائي للنظام للروابط الداخلية', 'pages_initial_name' => 'صفحة جديدة', 'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.', 'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.', - 'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.', + 'pages_draft_page_changed_since_creation' => 'تم تحديث هذه الصفحة منذ إنشاء هذه المُسَوَّدَة. يُنصح بتجاهل هذه المُسَوَّدَة أو الحرص على عدم استبدال أي تغييرات في الصفحة.', 'pages_draft_edit_active' => [ 'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة', 'start_b' => ':userName بدأ بتعديل هذه الصفحة', @@ -318,44 +318,44 @@ 'time_b' => 'في آخر :minCount دقيقة/دقائق', 'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!', ], - 'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content', - 'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content', + 'pages_draft_discarded' => 'تم رفض المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.', + 'pages_draft_deleted' => 'تم حذف المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.', 'pages_specific' => 'صفحة محددة', 'pages_is_template' => 'قالب الصفحة', // Editor Sidebar - 'toggle_sidebar' => 'Toggle Sidebar', + 'toggle_sidebar' => 'تبديل الشريط الجانبي', 'page_tags' => 'وسوم الصفحة', 'chapter_tags' => 'وسوم الفصل', 'book_tags' => 'وسوم الكتاب', 'shelf_tags' => 'علامات الرف', 'tag' => 'وسم', 'tags' => 'وسوم', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'يمكن تطبيق الوسوم على المحتوى داخل النظام لتطبيق تصنيف مرن. يمكن أن تحتوي الوسوم على مفتاح وقيمة، مع العلم أن القيمة اختيارية. بعد تطبيقها، يمكن الاستعلام عن المحتوى باستخدام اسم الوسم وقيمته.', 'tag_name' => 'اسم العلامة', 'tag_value' => 'قيمة الوسم (اختياري)', 'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.", 'tags_add' => 'إضافة وسم آخر', 'tags_remove' => 'إزالة هذه العلامة', - 'tags_usages' => 'Total tag usages', - 'tags_assigned_pages' => 'Assigned to Pages', - 'tags_assigned_chapters' => 'Assigned to Chapters', - 'tags_assigned_books' => 'Assigned to Books', - 'tags_assigned_shelves' => 'Assigned to Shelves', - 'tags_x_unique_values' => ':count unique values', - 'tags_all_values' => 'All values', - 'tags_view_tags' => 'View Tags', - 'tags_view_existing_tags' => 'View existing tags', - 'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.', + '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' => 'Alternatively you can drag and drop a file here to upload it as an attachment.', + 'attachments_upload_drop' => 'وبدلاً من ذلك، يمكنك سحب المِلَفّ وإفلاته هنا لتحميله كمرفق.', 'attachments_set_link' => 'تحديد الرابط', 'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟', - 'attachments_dropzone' => 'Drop files here to upload', + 'attachments_dropzone' => 'قم بإسقاط الملفات هنا للتحميل', 'attachments_no_files' => 'لم تُرفع أي ملفات', 'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.', 'attachments_link_name' => 'اسم الرابط', @@ -398,13 +398,13 @@ 'comment_new' => 'تعليق جديد', 'comment_created' => 'تم التعليق :createDiff', 'comment_updated' => 'تم التحديث :updateDiff بواسطة :username', - 'comment_updated_indicator' => 'Updated', + 'comment_updated_indicator' => 'تم التحديث', 'comment_deleted_success' => 'تم حذف التعليق', 'comment_created_success' => 'تمت إضافة التعليق', 'comment_updated_success' => 'تم تحديث التعليق', 'comment_delete_confirm' => 'تأكيد حذف التعليق؟', 'comment_in_reply_to' => 'رداً على :commentId', - 'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.', + 'comment_editor_explain' => 'هذه هي التعليقات المُضافة على هذه الصفحة. يُمكنك إضافة التعليقات وإدارتها عند عرض الصفحة المحفوظة.', // Revision 'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟', @@ -412,49 +412,49 @@ 'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.', // Copy view - 'copy_consider' => 'Please consider the below when copying content.', - 'copy_consider_permissions' => 'Custom permission settings will not be copied.', - 'copy_consider_owner' => 'You will become the owner of all copied content.', - 'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.', - 'copy_consider_attachments' => 'Page attachments will not be copied.', - 'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.', + 'copy_consider' => 'يرجى مراعاة ما يلي عند نسخ المحتوى.', + 'copy_consider_permissions' => 'لن يتم نسخ إعدادات الصلاحيات المخصصة.', + 'copy_consider_owner' => 'سوف تصبح مالكًا لجميع المحتوى المنسوخ.', + 'copy_consider_images' => 'لن يتم تكرار ملفات صور الصفحة وستحتفظ الصور الأصلية بعلاقتها بالصفحة التي تم تحميلها إليها في الأصل.', + 'copy_consider_attachments' => 'لن يتم نسخ مرفقات الصفحة.', + 'copy_consider_access' => 'قد يؤدي تغيير الموقع أو المالك أو الصلاحيات إلى إمكانية وصول الأشخاص الذين لم يتمكنوا من الوصول إلى هذا المحتوى سابقًا.', // Conversions - 'convert_to_shelf' => 'Convert to Shelf', - 'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.', - 'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.', - 'convert_book' => 'Convert Book', - 'convert_book_confirm' => 'Are you sure you want to convert this book?', - 'convert_undo_warning' => 'This cannot be as easily undone.', - 'convert_to_book' => 'Convert to Book', - 'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.', - 'convert_chapter' => 'Convert Chapter', - 'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?', + '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', - 'references_none' => 'There are no tracked references to this item.', - 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.', + 'references' => 'مراجع', + 'references_none' => 'لا توجد مراجع متعقبة لهذا العنصر.', + 'references_to_desc' => 'تجد أدناه كل المحتوى المعروف في النظام المرتبط بهذا العنصر.', // Watch Options - 'watch' => 'Watch', - 'watch_title_default' => 'Default Preferences', - 'watch_desc_default' => 'Revert watching to just your default notification preferences.', - 'watch_title_ignore' => 'Ignore', - 'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.', - 'watch_title_new' => 'New Pages', - 'watch_desc_new' => 'Notify when any new page is created within this item.', - 'watch_title_updates' => 'All Page Updates', - 'watch_desc_updates' => 'Notify upon all new pages and page changes.', - 'watch_desc_updates_page' => 'Notify upon all page changes.', - 'watch_title_comments' => 'All Page Updates & Comments', - 'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.', - 'watch_desc_comments_page' => 'Notify upon page changes and new comments.', - 'watch_change_default' => 'Change default notification preferences', - 'watch_detail_ignore' => 'Ignoring notifications', - 'watch_detail_new' => 'Watching for new pages', - 'watch_detail_updates' => 'Watching new pages and updates', - 'watch_detail_comments' => 'Watching new pages, updates & comments', + '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' => 'Watching via parent book', 'watch_detail_parent_book_ignore' => 'Ignoring via parent book', 'watch_detail_parent_chapter' => 'Watching via parent chapter', diff --git a/lang/cs/settings.php b/lang/cs/settings.php index ab316a7c40c..e4708f6b9eb 100644 --- a/lang/cs/settings.php +++ b/lang/cs/settings.php @@ -90,7 +90,7 @@ 'sort_rule_details' => 'Podrobnosti pravidla pro řazení', 'sort_rule_details_desc' => 'Nastavte název pro toto pravidlo, který se zobrazí v seznamu při výběru řazení.', 'sort_rule_operations' => 'Možnosti řazení', - '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' => 'Přesunem ze seznamu dostupných operací nastavte akce řazení, které mají být provedeny. Operace se použijí v pořadí od shora dolů. Veškeré změny zde provedené budou při uložení aplikovány na všechny přiřazené knihy.', 'sort_rule_available_operations' => 'Dostupné operace', 'sort_rule_available_operations_empty' => 'Žádné zbývající operace', 'sort_rule_configured_operations' => 'Konfigurované operace', diff --git a/lang/et/settings.php b/lang/et/settings.php index 0a81c74b8a5..bfb8a51ac67 100644 --- a/lang/et/settings.php +++ b/lang/et/settings.php @@ -90,7 +90,7 @@ 'sort_rule_details' => 'Sorteerimisreegli andmed', 'sort_rule_details_desc' => 'Määra sorteerimisreegli nimi, mida kasutatakse reegli valimise loendites.', 'sort_rule_operations' => 'Sorteerimistoimingud', - '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' => 'Seadista sorteerimistoimingud, liigutades neid saadaval toimingute loendist. Toimingud rakendatakse järjest, ülevalt alla. Siin tehtud muudatused rakenduvad salvestamisel kõigile määratud raamatutele.', 'sort_rule_available_operations' => 'Saadaval toimingud', 'sort_rule_available_operations_empty' => 'Rohkem toiminguid pole', 'sort_rule_configured_operations' => 'Seadistatud toimingud', diff --git a/lang/fr/activities.php b/lang/fr/activities.php index 3b1fe2e42b5..b65134a2409 100644 --- a/lang/fr/activities.php +++ b/lang/fr/activities.php @@ -128,12 +128,12 @@ 'comment_delete' => 'Commentaire supprimé', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'règle de tri crée', + 'sort_rule_create_notification' => 'Règle de tri crée avec succès', + 'sort_rule_update' => 'règle de tri mise à jour', + 'sort_rule_update_notification' => 'Règle de tri mise à jour avec succès', + 'sort_rule_delete' => 'règle de tri supprimée', + 'sort_rule_delete_notification' => 'La règle de tri a été supprimée avec succès', // Other 'permissions_update' => 'a mis à jour les autorisations sur', diff --git a/lang/he/settings.php b/lang/he/settings.php index 1fd21292598..0e7efe048f4 100644 --- a/lang/he/settings.php +++ b/lang/he/settings.php @@ -170,7 +170,7 @@ 'roles_x_users_assigned' => ':count user assigned|:count users assigned', 'roles_x_permissions_provided' => ':count permission|:count permissions', 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_permissions_provided' => 'הרשאות שהוקצו', 'role_create' => 'צור תפקיד משתמש חדש', 'role_delete' => 'מחק תפקיד', 'role_delete_confirm' => 'פעולה זו תמחק את התפקיד: :roleName', @@ -193,8 +193,8 @@ 'role_manage_settings' => 'ניהול הגדרות יישום', 'role_export_content' => 'Export content', 'role_import_content' => 'Import content', - 'role_editor_change' => 'Change page editor', - 'role_notifications' => 'Receive & manage notifications', + 'role_editor_change' => 'שנה עורך עמודים', + 'role_notifications' => 'ניהול התראות', 'role_asset' => 'הרשאות משאבים', 'roles_system_warning' => 'שימו לב לכך שגישה לכל אחת משלושת ההרשאות הנ"ל יכולה לאפשר למשתמש לשנות את הפריווילגיות שלהם או של אחרים במערכת. הגדירו תפקידים להרשאות אלה למשתמשים בהם אתם בוטחים בלבד.', 'role_asset_desc' => 'הרשאות אלו שולטות בגישת ברירת המחדל למשאבים בתוך המערכת. הרשאות של ספרים, פרקים ודפים יגברו על הרשאות אלו.', diff --git a/lang/he/validation.php b/lang/he/validation.php index d7c12678365..2d4f8b30598 100644 --- a/lang/he/validation.php +++ b/lang/he/validation.php @@ -15,7 +15,7 @@ 'alpha_dash' => 'שדה :attribute יכול להכיל אותיות, מספרים ומקפים בלבד.', 'alpha_num' => 'שדה :attribute יכול להכיל אותיות ומספרים בלבד.', 'array' => 'שדה :attribute חייב להיות מערך.', - 'backup_codes' => 'The provided code is not valid or has already been used.', + 'backup_codes' => 'קוד שהוזן לא תקין או שכבר השתמשו בו.', 'before' => 'שדה :attribute חייב להיות תאריך לפני :date.', 'between' => [ 'numeric' => 'שדה :attribute חייב להיות בין :min ל-:max.', diff --git a/lang/hu/activities.php b/lang/hu/activities.php index 548488c40f2..43683930bea 100644 --- a/lang/hu/activities.php +++ b/lang/hu/activities.php @@ -30,7 +30,7 @@ // Books 'book_create' => 'létrehozott egy könyvet', 'book_create_notification' => 'Könyv sikeresen létrehozva', - 'book_create_from_chapter' => 'fejezet könyvvé alakítva', + 'book_create_from_chapter' => 'könyvvé alakította a fejezetet', 'book_create_from_chapter_notification' => 'Fejezet sikeresen könyvvé lett alakítva', 'book_update' => 'frissítette a könyvet', 'book_update_notification' => 'Könyv sikeresen frissítve', @@ -93,31 +93,31 @@ 'import_delete_notification' => 'Import successfully deleted', // Users - 'user_create' => 'felhasználó létrehozása', + 'user_create' => 'létrehozta a felhasználót', 'user_create_notification' => 'Felhasználó sikeresen létrehozva', - 'user_update' => 'felhasználó módosítása', + 'user_update' => 'frissítette a felhasználót', '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', // API Tokens - 'api_token_create' => 'létrehozott API token', + 'api_token_create' => 'létrehozta az API tokent', 'api_token_create_notification' => 'API token sikeresen létrehozva', - 'api_token_update' => 'frissített API token', + 'api_token_update' => 'frissítette az API tokent', 'api_token_update_notification' => 'API token sikeresen frissítve', - 'api_token_delete' => 'törölt API token', + 'api_token_delete' => 'törölte az API tokent', 'api_token_delete_notification' => 'API token sikeresen törölve', // Roles 'role_create' => 'szerepkört hozott létre', 'role_create_notification' => 'Szerepkör sikeresen létrehozva', - 'role_update' => 'frissített szerepkör', + 'role_update' => 'frissítette a szerepkört', 'role_update_notification' => 'Szerepkör sikeresen frissítve', - 'role_delete' => 'törölt szerepkör', + 'role_delete' => 'törölte a szerepkört', 'role_delete_notification' => 'Szerepkör sikeresen törölve', // Recycle Bin - 'recycle_bin_empty' => 'lomtár kiürítve', + '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', @@ -128,12 +128,12 @@ 'comment_delete' => 'megjegyzés törlése', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'létrehozta a rendezési szabályt', + 'sort_rule_create_notification' => 'Rendezési szabály sikeresen létrehozva', + 'sort_rule_update' => 'frissítette a rendezési szabályt', + 'sort_rule_update_notification' => 'Rendezési szabály sikeresen frissítve', + 'sort_rule_delete' => 'törölte a rendezési szabályt', + 'sort_rule_delete_notification' => 'Rendezési szabály sikeresen törölve', // Other 'permissions_update' => 'engedélyek frissítve', diff --git a/lang/hu/auth.php b/lang/hu/auth.php index db065453ec3..90669e2fba2 100644 --- a/lang/hu/auth.php +++ b/lang/hu/auth.php @@ -24,7 +24,7 @@ 'password_hint' => 'Legalább 8 karakter hosszú legyen', 'forgot_password' => 'Elfelejtett jelszó?', 'remember_me' => 'Emlékezzen rám', - 'ldap_email_hint' => 'Adjon meg egy e-mail címet amelyet a felhasználói fiókhoz szeretne használni.', + 'ldap_email_hint' => 'A fiókhoz használt email cím megadása.', 'create_account' => 'Fiók létrehozása', 'already_have_account' => 'Rendelkezik már felhasználói fiókkal?', 'dont_have_account' => 'Még nincs felhasználói fiókja?', @@ -33,7 +33,7 @@ '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őrizni kell a megadott email címet és a megerősítő gombra kell kattintani :appName eléréséhez.', + 'register_confirm' => 'Ellenőrizze a megadott e-mail 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.', @@ -45,28 +45,28 @@ // Password Reset 'reset_password' => 'Jelszó visszaállítása', - 'reset_password_send_instructions' => 'Meg kell adni az email címet amire egy jelszó visszaállító hivatkozás lesz elküldve.', + 'reset_password_send_instructions' => 'Adja meg az e-mail 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_success' => 'A jelszó sikeresen visszaállítva.', 'email_reset_subject' => ':appName jelszó visszaállítása', - '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 történt jelszó visszaállításra vonatkozó kérés, akkor nincs szükség további intézkedésre.', + '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_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_action' => 'Email megerősítése', - 'email_confirm_send_error' => 'Az email megerősítés kötelező, de a rendszer nem tudta elküldeni az emailt. Fel kell venni a kapcsolatot az adminisztrátorral és meg kell győződni róla, hogy az email beállítások megfelelőek.', - 'email_confirm_success' => 'Az e-mail címed sikeresen meg lett erősítve, most már be tudsz jelentkezni az e-mail címed használatával.', - 'email_confirm_resent' => 'Megerősítő email újraküldve. Ellenőrizni kell a bejövő üzeneteket.', + '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_thanks' => 'Köszönjük a megerősítést!', - 'email_confirm_thanks_desc' => 'Kérlek, várj egy pillanatot, amíg a megerősítésedet kezeljük. Ha nem kerülsz átirányításra 3 másodperc után, kattints a "Folytatás" linkre az alábbiakban 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ásodperc után, 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' => 'Rá kell kattintani a regisztráció után nem sokkal elküldött emailben található hivatkozásra.', + '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_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', diff --git a/lang/hu/components.php b/lang/hu/components.php index ef440e54084..6a54dd1b034 100644 --- a/lang/hu/components.php +++ b/lang/hu/components.php @@ -13,7 +13,7 @@ '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_all' => 'Összes', 'image_all_title' => 'Összes kép megtekintése', - 'image_book_title' => 'A könyv feltöltött képek megtekintése', + 'image_book_title' => 'A könyvhöz feltöltött képek megtekintése', 'image_page_title' => 'Az oldalra feltöltött képek megtekintése', 'image_search_hint' => 'Keresés kép neve alapján', 'image_uploaded' => 'Feltöltve ekkor: :uploadedDate', diff --git a/lang/hu/editor.php b/lang/hu/editor.php index 83ca9b06254..d5c6baed67f 100644 --- a/lang/hu/editor.php +++ b/lang/hu/editor.php @@ -36,10 +36,10 @@ 'paragraph' => 'Bekezdés', 'blockquote' => 'Idézet', 'inline_code' => 'Forráskód', - 'callouts' => 'Ábrafelirat', + 'callouts' => 'Szövegdobozok', 'callout_information' => 'Információ', - 'callout_success' => 'Sikeres', - 'callout_warning' => 'Figyelem', + 'callout_success' => 'Siker', + 'callout_warning' => 'Figyelmeztetés', 'callout_danger' => 'Veszély', 'bold' => 'Félkövér', 'italic' => 'Dőlt', diff --git a/lang/hu/entities.php b/lang/hu/entities.php index 3c9921812e5..d073d2b74dc 100644 --- a/lang/hu/entities.php +++ b/lang/hu/entities.php @@ -35,11 +35,11 @@ 'no_pages_recently_created' => 'Nincsenek legutóbb létrehozott oldalak', 'no_pages_recently_updated' => 'Nincsenek legutóbb frissített oldalak', 'export' => 'Exportálás', - 'export_html' => 'Webfájlt tartalmaz', + 'export_html' => 'Önálló weblap', 'export_pdf' => 'PDF fájl', 'export_text' => 'Egyszerű szövegfájl', 'export_md' => 'Markdown jegyzetek', - 'export_zip' => 'Portable ZIP', + 'export_zip' => 'Hordozható ZIP', 'default_template' => 'Alapértelmezett oldalsablon', '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', @@ -344,8 +344,8 @@ 'tags_assigned_shelves' => 'Polcokhoz Rendelt', 'tags_x_unique_values' => ':count egyedi érték', 'tags_all_values' => 'Összes érték', - 'tags_view_tags' => 'Címke megtekintése', - 'tags_view_existing_tags' => 'Címkék megtekintése', + 'tags_view_tags' => 'Címkék megtekintése', + 'tags_view_existing_tags' => 'Létező címkék megtekintése', 'tags_list_empty_hint' => 'A címkék hozzárendelhetők az oldalszerkesztő oldalsávján keresztül, vagy egy könyv, fejezet vagy polc adatainak szerkesztése közben.', '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.', @@ -447,7 +447,7 @@ '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' => 'Minden oldal frissítése és megjegyzése', + '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_change_default' => 'Az alapértelmezett értesítési beállítások módosítása', diff --git a/lang/hu/errors.php b/lang/hu/errors.php index 00f47ac40bc..8ee055e29f7 100644 --- a/lang/hu/errors.php +++ b/lang/hu/errors.php @@ -61,7 +61,7 @@ 'attachment_upload_error' => 'Hiba történt a melléklet feltöltésekor', // Pages - 'page_draft_autosave_fail' => 'Nem sikerült a vázlat mentése. Mentés előtt meg kell róla győződni, hogy van internetkapcsolat', + 'page_draft_autosave_fail' => 'Nem sikerült a vázlat mentése. Mentés előtt állítsd helyre az internetkapcsolatot', 'page_draft_delete_fail' => 'Nem sikerült törölni az oldalvázlatot és lekérni az aktuális oldal mentett tartalmat', 'page_custom_home_deletion' => 'Nem lehet oldalt törölni ha kezdőlapnak van beállítva', @@ -101,7 +101,7 @@ '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.', 'return_home' => 'Vissza a kezdőlapra', - 'error_occurred' => 'Hiba örtént', + 'error_occurred' => 'Hiba történt', 'app_down' => ':appName jelenleg nem üzemel', 'back_soon' => 'Hamarosan újra elérhető lesz.', diff --git a/lang/hu/preferences.php b/lang/hu/preferences.php index 8c78a1d0e9c..4251a28fbc7 100644 --- a/lang/hu/preferences.php +++ b/lang/hu/preferences.php @@ -7,17 +7,17 @@ return [ 'my_account' => 'Fiókom', - 'shortcuts' => 'Gyorsbillentyűk', - 'shortcuts_interface' => 'UI parancsikon beállításai', + 'shortcuts' => 'Billentyűparancsok', + 'shortcuts_interface' => 'UI billentyűparancsok beállításai', 'shortcuts_toggle_desc' => 'Itt engedélyezheti vagy letilthatja a navigációhoz és műveletekhez használt billentyűparancsokat.', - 'shortcuts_customize_desc' => 'Csak nyomja meg a kívánt billentyűkombinációt, miután kiválasztotta a parancsikon bevitelét.', + 'shortcuts_customize_desc' => 'Az alábbi billentyűparancsok testre szabhatók. Csak nyomja meg a kívánt billentyűkombinációt, miután kiválasztotta a billentyűparancshoz tartozó mezőt.', 'shortcuts_toggle_label' => 'A billentyűparancsok engedélyezve', 'shortcuts_section_navigation' => 'Navigáció', 'shortcuts_section_actions' => 'Gyakori műveletek', - 'shortcuts_save' => 'Gyorsbillentyűk mentése', + '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' => 'Kezelheted a billentyűparancsokat, amelyeket használhatsz a rendszerfelhasználói felület navigálásához.', + 'shortcuts_overview_desc' => 'A rendszerfelhaszná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.', @@ -37,10 +37,10 @@ '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í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_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. Ideális esetben a kép négyzet alakú és körülbelül 256px szélességű és magasságú legyen.', + '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_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.', diff --git a/lang/it/settings.php b/lang/it/settings.php index bfad07b1c41..3b57db1b160 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -90,7 +90,7 @@ 'sort_rule_details' => 'Dettagli della regola di ordinamento', 'sort_rule_details_desc' => 'Imposta un nome per questa regola di ordinamento, apparirà nelle liste quando gli utenti selezionano un ordinamento.', 'sort_rule_operations' => 'Operazioni di ordinamento', - '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' => 'Configurare le azioni di ordinamento da eseguire spostandole dall\'elenco delle operazioni disponibili. Al momento dell\'utilizzo, le operazioni saranno applicate in ordine, dall\'alto verso il basso. Le modifiche apportate qui saranno applicate a tutti i libri assegnati al momento del salvataggio.', 'sort_rule_available_operations' => 'Operazioni Disponibili', 'sort_rule_available_operations_empty' => 'Nessuna operazione rimanente', 'sort_rule_configured_operations' => 'Operazioni Configurate', diff --git a/lang/ja/activities.php b/lang/ja/activities.php index b2bbd89a58f..6f55098bbd9 100644 --- a/lang/ja/activities.php +++ b/lang/ja/activities.php @@ -85,11 +85,11 @@ 'webhook_delete_notification' => 'Webhookを削除しました', // Imports - 'import_create' => 'created import', + 'import_create' => 'がインポートを作成', 'import_create_notification' => 'インポートファイルが正常にアップロードされました', - 'import_run' => 'updated import', + 'import_run' => 'がインポートを更新', 'import_run_notification' => 'コンテンツが正常にインポートされました', - 'import_delete' => 'deleted import', + 'import_delete' => 'がインポートを削除', 'import_delete_notification' => 'インポートファイルが正常に削除されました', // Users @@ -128,12 +128,12 @@ 'comment_delete' => 'がコメントを削除', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/ja/editor.php b/lang/ja/editor.php index 4a131b5d9b1..6d4d8506e05 100644 --- a/lang/ja/editor.php +++ b/lang/ja/editor.php @@ -13,7 +13,7 @@ 'cancel' => '取消', 'save' => '保存', 'close' => '閉じる', - 'apply' => 'Apply', + 'apply' => '適用', 'undo' => '元に戻す', 'redo' => 'やり直し', 'left' => '左寄せ', @@ -148,7 +148,7 @@ 'url' => 'リンク先URL', 'text_to_display' => 'リンク元テキスト', 'title' => 'タイトル', - 'browse_links' => 'Browse links', + 'browse_links' => 'エンティティを参照', 'open_link' => 'リンクを開く', 'open_link_in' => 'リンク先の表示場所', 'open_link_current' => '同じウィンドウ', diff --git a/lang/ja/entities.php b/lang/ja/entities.php index 583d2e17d20..afd7765782e 100644 --- a/lang/ja/entities.php +++ b/lang/ja/entities.php @@ -166,9 +166,9 @@ '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_named' => 'ブック「:bookName」を並べ替え', 'books_sort_name' => '名前で並べ替え', 'books_sort_created' => '作成日で並べ替え', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index 14a890dfd38..1a0e64cb41d 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => '制限しない', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'ソート', + 'sorting_book_default' => 'ブックのデフォルトソート', + 'sorting_book_default_desc' => '新しいブックに適用するデフォルトのソートルールを選択します。これは既存のブックには影響しません。ルールはブックごとに上書きすることができます。', + 'sorting_rules' => 'ソートルール', + 'sorting_rules_desc' => 'これらはシステム内のコンテンツに適用できる事前定義のソート操作です。', + 'sort_rule_assigned_to_x_books' => ':count 個のブックに割当|: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' => 'チャプタを最後に', // Maintenance settings 'maint' => 'メンテナンス', diff --git a/lang/ko/activities.php b/lang/ko/activities.php index 84df1c1fda9..c1dbb93fd7b 100644 --- a/lang/ko/activities.php +++ b/lang/ko/activities.php @@ -131,7 +131,7 @@ 'sort_rule_create' => '정렬 규칙 생성', 'sort_rule_create_notification' => '정렬 규칙이 성공적으로 생성되었습니다', 'sort_rule_update' => '정렬 규칙 업데이트', - 'sort_rule_update_notification' => 'Sort rule successfully updated', + 'sort_rule_update_notification' => '정렬 규칙이 성공적으로 업데이트 되었습니다', 'sort_rule_delete' => '정렬 규칙 삭제', 'sort_rule_delete_notification' => '정렬 규칙이 성공적으로 삭제되었습니다', diff --git a/lang/ko/editor.php b/lang/ko/editor.php index f04611a925f..ac4d8d53d7a 100644 --- a/lang/ko/editor.php +++ b/lang/ko/editor.php @@ -13,7 +13,7 @@ 'cancel' => '취소', 'save' => '저장', 'close' => '닫기', - 'apply' => 'Apply', + 'apply' => '적용', 'undo' => '되돌리기', 'redo' => '다시 실행', 'left' => '왼쪽', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => '표시할 텍스트', 'title' => '제목', - 'browse_links' => 'Browse links', + 'browse_links' => '링크 찾기', 'open_link' => '링크 열기', 'open_link_in' => '다음에서 링크 열기...', 'open_link_current' => '현재 창', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 5c3af83eb77..b78333385fd 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -77,7 +77,7 @@ // Sorting Settings 'sorting' => '정렬', 'sorting_book_default' => '기본 책 정렬', - '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_book_default_desc' => '새로운 책에 적용할 기본 정렬 규칙을 선택하세요. 이 선택은 기존 책에는 영향을 주지 않고, 기존 책의 설정은 책마다 변경할 수 있습니다.', 'sorting_rules' => '정렬 규칙', 'sorting_rules_desc' => '현재 시스템에 미리 정의된 정렬 규칙의 목록입니다.', 'sort_rule_assigned_to_x_books' => ':count 책에 정렬 규칙 적용', @@ -90,7 +90,7 @@ 'sort_rule_details' => '정렬 규칙 세부사항', 'sort_rule_details_desc' => '이 정렬 규칙의 이름을 지어주세요. 이 이름은 사용자가 정렬할 때 나타납니다.', 'sort_rule_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' => '사용 가능한 정렬 규칙 없음', 'sort_rule_configured_operations' => '정렬 규칙 설정', diff --git a/lang/nb/activities.php b/lang/nb/activities.php index 4d184633ff4..fd4de53216e 100644 --- a/lang/nb/activities.php +++ b/lang/nb/activities.php @@ -129,12 +129,12 @@ 'comment_delete' => 'slettet kommentar', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'opprettet sorteringsregel', + 'sort_rule_create_notification' => 'Sorteringsregel opprettet med suksess', + 'sort_rule_update' => 'oppdatert sorteringsregel', + 'sort_rule_update_notification' => 'Sorteringsregel oppdatert med suksess', + 'sort_rule_delete' => 'slettet sorteringsregel', + 'sort_rule_delete_notification' => 'Sorteringsregel slettet med suksess', // Other 'permissions_update' => 'oppdaterte tilganger', diff --git a/lang/nb/editor.php b/lang/nb/editor.php index 6b1c5eb78ed..ddb266626a4 100644 --- a/lang/nb/editor.php +++ b/lang/nb/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Avbryt', 'save' => 'Lagre', 'close' => 'Lukk', - 'apply' => 'Apply', + 'apply' => 'Bruk', 'undo' => 'Angre', 'redo' => 'Gjør om', 'left' => 'Venstre', @@ -148,7 +148,7 @@ 'url' => 'Nettlenke', 'text_to_display' => 'Synlig tekst', 'title' => 'Tittel', - 'browse_links' => 'Browse links', + 'browse_links' => 'Bla gjennom linker', 'open_link' => 'Åpne lenke', 'open_link_in' => 'Åpne i ...', 'open_link_current' => 'Samme vindu', @@ -165,8 +165,8 @@ 'about' => 'Om tekstredigeringsprogrammet', 'about_title' => 'Om HDSEHDF-tekstredigeringsprogrammet', 'editor_license' => 'Tekstbehandlerlisens og opphavsrett', - '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' => 'Denne redaktoren er bygget som en fork av :lexicalLink, som er distribuert under MIT-lisensen.', + 'editor_lexical_license_link' => 'Full lisensinformasjon finner du her.', 'editor_tiny_license' => 'Denne tekstredigereren er laget med :tinyLink som er lisensiert under MIT.', 'editor_tiny_license_link' => 'Informasjon om opphavsrett og lisens for TinyMCE finnes her.', 'save_continue' => 'Lagre side og fortsett', diff --git a/lang/nb/entities.php b/lang/nb/entities.php index 36f85791f70..85faf1e852d 100644 --- a/lang/nb/entities.php +++ b/lang/nb/entities.php @@ -166,9 +166,9 @@ 'books_search_this' => 'Søk i boken', 'books_navigation' => 'Boknavigasjon', 'books_sort' => 'Sorter bokinnhold', - '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' => '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_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 182d23c3aa3..a719d41cc48 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begrensninger er satt', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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.', + 'sorting' => 'Sortering', + 'sorting_book_default' => 'Standard boksortering', + 'sorting_book_default_desc' => 'Velg standard sorteringsregelen som skal brukes for nye bøker. Dette vil ikke påvirke eksisterende bøker, og kan overstyres per bok.', + 'sorting_rules' => 'Sorteringsregler', + 'sorting_rules_desc' => 'Dette er forhåndsdefinerte sorteringsoperasjoner som kan brukes på innhold i systemet.', + 'sort_rule_assigned_to_x_books' => 'Tildelt til :count bok|Tildelt til :count bøker', + 'sort_rule_create' => 'Opprett sorteringsregel', + 'sort_rule_edit' => 'Rediger sorteringsregel', + 'sort_rule_delete' => 'Slett sorteringsregel', + 'sort_rule_delete_desc' => 'Fjern denne sorteringsregelen fra systemet. Bøker som bruker denne sorteringsregelen vil gå tilbake til manuell sortering.', + 'sort_rule_delete_warn_books' => 'Denne sorteringsregelen brukes for øyeblikket på :count bok/bøker. Er du sikker på at du vil slette denne?', + 'sort_rule_delete_warn_default' => 'Denne sorteringsregelen brukes for øyeblikket som standard for bøker. Er du sikker på at du vil slette denne?', + 'sort_rule_details' => 'Detaljer om sorteringsregel', + 'sort_rule_details_desc' => 'Angi et navn for denne sorteringsregelen, som vil vises i lister når brukerne velger en sorteringsmetode.', + 'sort_rule_operations' => 'Sorteringsoperasjoner', + 'sort_rule_operations_desc' => 'Konfigurer sorteringshandlinger ved å flytte dem fra listen over tilgjengelige operasjoner. Ved bruk vil operasjonene bli brukt i rekkefølge, fra topp til bunn. Eventuelle endringer gjort her vil bli brukt for alle tildelte bøker når du lagrer.', '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_configured_operations_empty' => 'Dra/legg til operasjoner fra listen "Tilgjengelige operasjoner"', 'sort_rule_op_asc' => '(Asc)', 'sort_rule_op_desc' => '(Desc)', - 'sort_rule_op_name' => 'Name - Alphabetical', + 'sort_rule_op_name' => 'Navn - Alfabetisk', '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', + 'sort_rule_op_chapters_first' => 'Kapitler først', + 'sort_rule_op_chapters_last' => 'Kapitler sist', // Maintenance settings 'maint' => 'Vedlikehold', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 79b277b60b4..3bba35cb554 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -19,7 +19,7 @@ 'app_name_desc' => 'Deze naam wordt getoond in de header en in alle door het systeem verstuurde e-mails.', 'app_name_header' => 'Toon naam in header', 'app_public_access' => 'Openbare toegang', - 'app_public_access_desc' => 'Door deze optie in te schakelen kunnen bezoekers, die niet ingelogd zijn, toegang krijgen tot de inhoud van uw BookStack omgeving.', + 'app_public_access_desc' => 'Door deze optie in te schakelen kunnen bezoekers die niet ingelogd zijn toegang krijgen tot de inhoud van uw BookStack-omgeving.', 'app_public_access_desc_guest' => 'De toegang voor publieke bezoekers kan worden ingesteld via de "Guest" gebruiker.', 'app_public_access_toggle' => 'Openbare toegang toestaan', 'app_public_viewing' => 'Publieke bezichtigingen toestaan?', @@ -28,14 +28,14 @@ 'app_secure_images_desc' => 'Om prestatieredenen zijn alle afbeeldingen openbaar. Deze optie voegt een willekeurige en moeilijk te raden tekst toe aan de URL\'s van de afbeeldingen. Zorg ervoor dat "directory indexes" niet ingeschakeld zijn om eenvoudige toegang te voorkomen.', 'app_default_editor' => 'Standaard Pagina Bewerker', 'app_default_editor_desc' => 'Selecteer welke bewerker standaard zal worden gebruikt bij het bewerken van nieuwe pagina\'s. Dit kan worden overschreven op paginaniveau als de rechten dat toestaan.', - 'app_custom_html' => 'HTML aan toevoegen', + 'app_custom_html' => 'Aangepaste inhoud voor het HTML head-element', 'app_custom_html_desc' => 'Alle hieronder toegevoegde data wordt aan het einde van de sectie van elke pagina toegevoegd. Gebruik dit om stijlen te overschrijven of analytische code toe te voegen.', 'app_custom_html_disabled_notice' => 'Bovenstaande wordt niet toegevoegd aan deze pagina om ervoor te zorgen dat je foutieve code steeds ongedaan kan maken.', 'app_logo' => 'Applicatielogo', 'app_logo_desc' => 'Dit wordt onder meer gebruikt in de kopbalk van de applicatie. Deze afbeelding dient 86px hoog te zijn. Grote afbeeldingen worden teruggeschaald.', - 'app_icon' => 'Applicatie Pictogram', + 'app_icon' => 'Applicatiepictogram', 'app_icon_desc' => 'Dit pictogram wordt gebruikt voor browsertabbladen en snelkoppelingspictogrammen. Dit dient een 256px vierkante PNG-afbeelding te zijn.', - 'app_homepage' => 'Applicatie Startpagina', + 'app_homepage' => 'Applicatie startpagina', 'app_homepage_desc' => 'Selecteer een weergave om weer te geven op de startpagina in plaats van de standaard weergave. Paginamachtigingen worden genegeerd voor geselecteerde pagina\'s.', 'app_homepage_select' => 'Selecteer een pagina', 'app_footer_links' => 'Voettekst hyperlinks', @@ -81,16 +81,16 @@ 'sorting_rules' => 'Sorteerregels', 'sorting_rules_desc' => 'Dit zijn vooraf ingestelde sorteeroperaties die kunnen worden toegepast op inhoud in het systeem.', 'sort_rule_assigned_to_x_books' => 'Toegepast op :count Boek|Toegepast op :count Boeken', - 'sort_rule_create' => 'Maakte Sorteerregel', - 'sort_rule_edit' => 'Bewerk Sorteerregel', - 'sort_rule_delete' => 'Verwijder Sorteerregel', + 'sort_rule_create' => 'Maak sorteerregel', + 'sort_rule_edit' => 'Bewerk sorteerregel', + 'sort_rule_delete' => 'Verwijder sorteerregel', 'sort_rule_delete_desc' => 'Verwijder deze sorteerregel uit het systeem. Boeken die deze regel gebruiken zullen terugvallen op handmatige sortering.', 'sort_rule_delete_warn_books' => 'Deze sorteerregel wordt momenteel gebruikt door :count boek(en). Weet je zeker dat je deze wilt verwijderen?', 'sort_rule_delete_warn_default' => 'Deze sorteerregel wordt gebruikt als standaardregel voor boeken. Weet je zeker dat je deze wilt verwijderen?', - 'sort_rule_details' => 'Details Sorteerregel', + 'sort_rule_details' => 'Details sorteerregel', 'sort_rule_details_desc' => 'Stel een naam in voor deze sorteerregel. Deze wordt weergegeven waar gebruikers een regel kunnen selecteren.', 'sort_rule_operations' => 'Sorteeroperaties', - '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' => 'Configureer de sorteeracties die moeten worden uitgevoerd door ze te verplaatsen van de lijst met beschikbare operaties. Bij gebruik worden de operaties toegepast in volgorde van boven naar beneden. Wijzigingen die hier worden gemaakt worden toegepast op alle toegewezen boeken.', 'sort_rule_available_operations' => 'Beschikbare Operaties', 'sort_rule_available_operations_empty' => 'Geen operaties over', 'sort_rule_configured_operations' => 'Ingestelde Operaties', @@ -107,8 +107,8 @@ // Maintenance settings 'maint' => 'Onderhoud', 'maint_image_cleanup' => 'Afbeeldingen opschonen', - 'maint_image_cleanup_desc' => 'Scant pagina- en revisie inhoud om te controleren welke afbeeldingen en tekeningen momenteel worden gebruikt en welke afbeeldingen overbodig zijn. Zorg ervoor dat je een volledige database- en afbeelding back-up maakt voordat je dit uitvoert.', - 'maint_delete_images_only_in_revisions' => 'Ook afbeeldingen verwijderen die alleen in oude pagina revisies bestaan', + 'maint_image_cleanup_desc' => 'Scant pagina- en revisieinhoud om te controleren welke afbeeldingen en tekeningen momenteel worden gebruikt en welke afbeeldingen overbodig zijn. Zorg ervoor dat je een volledige database- en afbeelding back-up maakt voordat je dit uitvoert.', + 'maint_delete_images_only_in_revisions' => 'Ook afbeeldingen verwijderen die alleen in oude paginarevisies bestaan', 'maint_image_cleanup_run' => 'Opschonen uitvoeren', 'maint_image_cleanup_warning' => ':count potentieel ongebruikte afbeeldingen gevonden. Weet je zeker dat je deze afbeeldingen wilt verwijderen?', 'maint_image_cleanup_success' => ':count potentieel ongebruikte afbeeldingen gevonden en verwijderd!', @@ -124,7 +124,7 @@ 'maint_recycle_bin_open' => 'Prullenbak openen', 'maint_regen_references' => 'Verwijzingen opnieuw genereren', 'maint_regen_references_desc' => 'Deze actie zal de kruisverwijzingen index binnen de database opnieuw opbouwen. Dit wordt doorgaans automatisch gedaan, maar deze actie kan nuttig zijn om oude inhoud of inhoud die via onofficiële methoden is toegevoegd te indexeren.', - 'maint_regen_references_success' => 'Verwijzingen index is opnieuw gegenereerd!', + 'maint_regen_references_success' => 'Verwijzingenindex is opnieuw gegenereerd!', 'maint_timeout_command_note' => 'Let op: Het uitvoeren van deze actie kan enige tijd in beslag nemen, wat in sommige webomgevingen kan leiden tot time-outs. Als alternatief kan deze actie ook worden uitgevoerd met een terminal-commando.', // Recycle Bin diff --git a/lang/nn/errors.php b/lang/nn/errors.php index 0619c4682fc..c56b7d07850 100644 --- a/lang/nn/errors.php +++ b/lang/nn/errors.php @@ -5,7 +5,7 @@ return [ // Permissions - 'permission' => 'Du har ikke tilgang til å se denne siden.', + 'permission' => 'Du har ikkje tilgang til å sjå denne sida.', 'permissionJson' => 'Du har ikke tilgang til å utføre denne handlingen.', // Auth diff --git a/lang/pl/activities.php b/lang/pl/activities.php index 60c109eda42..dc078ad57ab 100644 --- a/lang/pl/activities.php +++ b/lang/pl/activities.php @@ -85,7 +85,7 @@ 'webhook_delete_notification' => 'Webhook usunięty pomyślnie', // Imports - 'import_create' => 'created import', + 'import_create' => 'utworzono import', 'import_create_notification' => 'Import successfully uploaded', 'import_run' => 'updated import', 'import_run_notification' => 'Content successfully imported', diff --git a/lang/sv/notifications.php b/lang/sv/notifications.php index 9d2269ed87d..58418ddb01e 100644 --- a/lang/sv/notifications.php +++ b/lang/sv/notifications.php @@ -4,24 +4,24 @@ */ return [ - 'new_comment_subject' => 'New comment on page: :pageName', - 'new_comment_intro' => 'A user has commented on a page in :appName:', - 'new_page_subject' => 'New page: :pageName', - 'new_page_intro' => 'A new page has been created in :appName:', - 'updated_page_subject' => 'Updated page: :pageName', - 'updated_page_intro' => 'A page has been updated in :appName:', - 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'new_comment_subject' => 'Ny kommentar på sidan: :pageName', + 'new_comment_intro' => 'En användare har kommenterat en sida i :appName:', + 'new_page_subject' => 'Ny sida: :pageName', + 'new_page_intro' => 'En ny sida har blivit skapad i :appName:', + 'updated_page_subject' => 'Uppdaterad sida: :pageName', + 'updated_page_intro' => 'En sida har blivit uppdaterad i :appName:', + 'updated_page_debounce' => 'För att förhindra en massa notiser, så kommer det inte skickas nya notiser på ett tag för ytterligare ändringar till denna sida av samma skribent.', - 'detail_page_name' => 'Page Name:', - 'detail_page_path' => 'Page Path:', - 'detail_commenter' => 'Commenter:', - 'detail_comment' => 'Comment:', - 'detail_created_by' => 'Created By:', - 'detail_updated_by' => 'Updated By:', + 'detail_page_name' => 'Sidonamn:', + 'detail_page_path' => 'Sidosökväg:', + 'detail_commenter' => 'Kommentars-skapare:', + 'detail_comment' => 'Kommentar:', + 'detail_created_by' => 'Skapad av:', + 'detail_updated_by' => 'Uppdaterad av:', 'action_view_comment' => 'Visa kommentar', - 'action_view_page' => 'View Page', + 'action_view_page' => 'Visa sida', - 'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.', - 'footer_reason_link' => 'your notification preferences', + 'footer_reason' => 'Detta meddelande skickades till dig eftersom :link täcker denna typ av aktivitet för detta objekt.', + 'footer_reason_link' => 'dina notifikationspreferenser', ]; diff --git a/lang/tr/activities.php b/lang/tr/activities.php index ace52b36d4f..5bed9ea303b 100644 --- a/lang/tr/activities.php +++ b/lang/tr/activities.php @@ -50,8 +50,8 @@ 'bookshelf_delete_notification' => 'Kitaplık başarıyla silindi', // Revisions - 'revision_restore' => 'restored revision', - 'revision_delete' => 'deleted revision', + 'revision_restore' => 'geri yüklenen revizyon', + 'revision_delete' => 'silinmiş revizyon', 'revision_delete_notification' => 'Değişiklik başarıyla silindi', // Favourites @@ -64,17 +64,17 @@ // Auth 'auth_login' => 'oturum açıldı', 'auth_register' => 'yeni kullanıcı olarak kayıt yapıldı', - 'auth_password_reset_request' => 'requested user password reset', + 'auth_password_reset_request' => 'talep edilmiş kullanıcı parola sıfırlamaları', 'auth_password_reset_update' => 'Kullanıcı parolasını sıfırla', - 'mfa_setup_method' => 'configured MFA method', + 'mfa_setup_method' => 'uygulanan MFA yöntemi', 'mfa_setup_method_notification' => 'Çok aşamalı kimlik doğrulama yöntemi başarıyla yapılandırıldı', - 'mfa_remove_method' => 'removed MFA method', + 'mfa_remove_method' => 'kaldırılan MFA yöntemi', 'mfa_remove_method_notification' => 'Çok aşamalı kimlik doğrulama yöntemi başarıyla kaldırıldı', // Settings - 'settings_update' => 'updated settings', + 'settings_update' => 'güncellenmiş ayarlar', 'settings_update_notification' => 'Ayarlar başarıyla güncellendi', - 'maintenance_action_run' => 'ran maintenance action', + 'maintenance_action_run' => 'bakım işlemine başla', // Webhooks 'webhook_create' => 'web kancası oluşturuldu', @@ -93,7 +93,7 @@ 'import_delete_notification' => 'Import successfully deleted', // Users - 'user_create' => 'created user', + 'user_create' => 'oluşturan kullanıcı', 'user_create_notification' => 'Kullanıcı başarıyla oluşturuldu', 'user_update' => 'updated user', 'user_update_notification' => 'Kullanıcı başarıyla güncellendi', @@ -118,14 +118,14 @@ // Recycle Bin 'recycle_bin_empty' => 'emptied recycle bin', - 'recycle_bin_restore' => 'restored from recycle bin', - 'recycle_bin_destroy' => 'removed from recycle bin', + 'recycle_bin_restore' => 'çöp kutusundan geri getirilen', + 'recycle_bin_destroy' => 'çöp kutusundan kaldırılan', // Comments 'commented_on' => 'yorum yaptı', - 'comment_create' => 'added comment', - 'comment_update' => 'updated comment', - 'comment_delete' => 'deleted comment', + 'comment_create' => 'eklenen yorum', + 'comment_update' => 'güncellenen yorum', + 'comment_delete' => 'silinen yorum', // Sort Rules 'sort_rule_create' => 'created sort rule', From e980564fd6df6cd74d444cc3f2525d9abed3f1dc Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 16 Mar 2025 12:44:29 +0000 Subject: [PATCH 007/474] Updated translator & dependency attribution before release v25.02.1 --- .github/translators.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/translators.txt b/.github/translators.txt index 8e4eb271c06..af5f2a703c1 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -469,3 +469,12 @@ Raphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian yn (user99) :: Arabic Pavel Zlatarov (pzlatarov) :: Bulgarian ingelres :: French +mabdullah :: Arabic +Skrabák Csaba (kekcsi) :: Hungarian +Evert Meulie (Evert) :: Norwegian Bokmal +Jasper Backer (jasperb) :: Dutch +Alexandar Cavdarovski (ace.200112) :: Swedish +구닥다리TV (yjj8353) :: Korean +Onur Oskay (o.oskay) :: Turkish +Sébastien Merveille (SebastienMerv) :: French +Maxim Kouznetsov (masya.work) :: Hebrew From abe7467ae5f9341f2f5f9cad3b1a48724ed1a425 Mon Sep 17 00:00:00 2001 From: Bernardo Campos Date: Sun, 23 Mar 2025 12:29:29 -0300 Subject: [PATCH 008/474] Fix issue BookStackApp#5542 Sorting by name --- app/Sorting/SortSetOperationComparisons.php | 5 +++-- tests/Sorting/SortRuleTest.php | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/Sorting/SortSetOperationComparisons.php b/app/Sorting/SortSetOperationComparisons.php index e3465231573..e506e9a139d 100644 --- a/app/Sorting/SortSetOperationComparisons.php +++ b/app/Sorting/SortSetOperationComparisons.php @@ -2,6 +2,7 @@ namespace BookStack\Sorting; +use voku\helper\ASCII; use BookStack\Entities\Models\Chapter; use BookStack\Entities\Models\Entity; @@ -13,12 +14,12 @@ class SortSetOperationComparisons { public static function nameAsc(Entity $a, Entity $b): int { - return strtolower($a->name) <=> strtolower($b->name); + return strtolower(ASCII::to_transliterate($a->name)) <=> strtolower(ASCII::to_transliterate($b->name)); } public static function nameDesc(Entity $a, Entity $b): int { - return strtolower($b->name) <=> strtolower($a->name); + return strtolower(ASCII::to_transliterate($b->name)) <=> strtolower(ASCII::to_transliterate($a->name)); } public static function nameNumericAsc(Entity $a, Entity $b): int diff --git a/tests/Sorting/SortRuleTest.php b/tests/Sorting/SortRuleTest.php index e956f49df4b..b0f20cba5ae 100644 --- a/tests/Sorting/SortRuleTest.php +++ b/tests/Sorting/SortRuleTest.php @@ -198,6 +198,8 @@ public function test_name_alphabetical_ordering() $namesToAdd = [ "Beans", "bread", + "Éclaire", + "egg", "Milk", "pizza", "Tomato", From 5c6671b3bf5b582c2ed0e120bb8afb5f296a63e9 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 27 Mar 2025 14:13:18 +0000 Subject: [PATCH 009/474] Lexical: Fixed issues with content not saving Found that saving via Ctrl+Enter did not save as logic to load editor output into form was bypassed, which this fixes by ensuring submit events are raised during for this shortcut. Submit handling also gets a timeout added since, at least in FF, requestSubmit did not re-submit a form while in a submit event. --- resources/js/components/page-editor.js | 2 +- resources/js/components/wysiwyg-editor.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/resources/js/components/page-editor.js b/resources/js/components/page-editor.js index 64cd601a920..81378e944c0 100644 --- a/resources/js/components/page-editor.js +++ b/resources/js/components/page-editor.js @@ -112,7 +112,7 @@ export class PageEditor extends Component { } savePage() { - this.container.closest('form').submit(); + this.container.closest('form').requestSubmit(); } async saveDraft() { diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js index 56dbe8d7c5f..5a258190069 100644 --- a/resources/js/components/wysiwyg-editor.js +++ b/resources/js/components/wysiwyg-editor.js @@ -25,6 +25,7 @@ export class WysiwygEditor extends Component { textDirection: this.$opts.textDirection, translations, }); + window.wysiwyg = this.editor; }); let handlingFormSubmit = false; @@ -38,7 +39,9 @@ export class WysiwygEditor extends Component { handlingFormSubmit = true; this.editor.getContentAsHtml().then(html => { this.input.value = html; - this.input.form.submit(); + setTimeout(() => { + this.input.form.requestSubmit(); + }, 5); }); } else { handlingFormSubmit = false; From c03e44124a3312d6832f71103bda76ab66646a54 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 27 Mar 2025 14:56:32 +0000 Subject: [PATCH 010/474] Lexical: Fixed task list parsing Updated list DOM parsing to properly consider task list format set by other MD/WYSIWYG editors. --- .../js/wysiwyg/lexical/list/LexicalListNode.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts index 6edf0d64a2e..b5c83adddce 100644 --- a/resources/js/wysiwyg/lexical/list/LexicalListNode.ts +++ b/resources/js/wysiwyg/lexical/list/LexicalListNode.ts @@ -332,7 +332,19 @@ function isDomChecklist(domNode: HTMLElement) { } // if children are checklist items, the node is a checklist ul. Applicable for googledoc checklist pasting. for (const child of domNode.childNodes) { - if (isHTMLElement(child) && child.hasAttribute('aria-checked')) { + if (!isHTMLElement(child)) { + continue; + } + + if (child.hasAttribute('aria-checked')) { + return true; + } + + if (child.classList.contains('task-list-item')) { + return true; + } + + if (child.firstElementChild && child.firstElementChild.matches('input[type="checkbox"]')) { return true; } } From 62c8eb335785e41c730569ec34df7137f2f325f1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 27 Mar 2025 17:49:48 +0000 Subject: [PATCH 011/474] Lexical: Made list selections & intendting more reliable - Added handling to not include parent of top-most list range selection so that it's not also changed while not visually part of the selection range. - Fixed issue where list items could be left over after unnesting, due to empty checks/removals occuring before all child handling. - Added node sorting, applied to list items during nest operations so that selection range remains reliable. --- resources/js/wysiwyg/utils/lists.ts | 47 +++++++++++++++++++++++------ resources/js/wysiwyg/utils/nodes.ts | 24 +++++++++++++++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/resources/js/wysiwyg/utils/lists.ts b/resources/js/wysiwyg/utils/lists.ts index 005b05f9816..3deb9dfb6e9 100644 --- a/resources/js/wysiwyg/utils/lists.ts +++ b/resources/js/wysiwyg/utils/lists.ts @@ -1,6 +1,6 @@ import {$createTextNode, $getSelection, BaseSelection, LexicalEditor, TextNode} from "lexical"; import {$getBlockElementNodesInSelection, $selectNodes, $toggleSelection} from "./selection"; -import {nodeHasInset} from "./nodes"; +import {$sortNodes, nodeHasInset} from "./nodes"; import {$createListItemNode, $createListNode, $isListItemNode, $isListNode, ListItemNode} from "@lexical/list"; @@ -49,16 +49,11 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { } const laterSiblings = node.getNextSiblings(); - parentListItem.insertAfter(node); if (list.getChildren().length === 0) { list.remove(); } - if (parentListItem.getChildren().length === 0) { - parentListItem.remove(); - } - if (laterSiblings.length > 0) { const childList = $createListNode(list.getListType()); childList.append(...laterSiblings); @@ -69,23 +64,54 @@ export function $unnestListItem(node: ListItemNode): ListItemNode { list.remove(); } + if (parentListItem.getChildren().length === 0) { + parentListItem.remove(); + } + return node; } function getListItemsForSelection(selection: BaseSelection|null): (ListItemNode|null)[] { const nodes = selection?.getNodes() || []; - const listItemNodes = []; + let [start, end] = selection?.getStartEndPoints() || [null, null]; + + // Ensure we ignore parent list items of the top-most list item since, + // although technically part of the selection, from a user point of + // view the selection does not spread to encompass this outer element. + const itemsToIgnore: Set = new Set(); + if (selection && start) { + if (selection.isBackward() && end) { + [end, start] = [start, end]; + } + const startParents = start.getNode().getParents(); + let foundList = false; + for (const parent of startParents) { + if ($isListItemNode(parent)) { + if (foundList) { + itemsToIgnore.add(parent.getKey()); + } else { + foundList = true; + } + } + } + } + + const listItemNodes = []; outer: for (const node of nodes) { if ($isListItemNode(node)) { - listItemNodes.push(node); + if (!itemsToIgnore.has(node.getKey())) { + listItemNodes.push(node); + } continue; } const parents = node.getParents(); for (const parent of parents) { if ($isListItemNode(parent)) { - listItemNodes.push(parent); + if (!itemsToIgnore.has(parent.getKey())) { + listItemNodes.push(parent); + } continue outer; } } @@ -110,7 +136,8 @@ function $reduceDedupeListItems(listItems: (ListItemNode|null)[]): ListItemNode[ } } - return Object.values(listItemMap); + const items = Object.values(listItemMap); + return $sortNodes(items) as ListItemNode[]; } export function $setInsetForSelection(editor: LexicalEditor, change: number): void { diff --git a/resources/js/wysiwyg/utils/nodes.ts b/resources/js/wysiwyg/utils/nodes.ts index b5cc789550c..591232ea385 100644 --- a/resources/js/wysiwyg/utils/nodes.ts +++ b/resources/js/wysiwyg/utils/nodes.ts @@ -94,6 +94,30 @@ export function $getNearestNodeBlockParent(node: LexicalNode): LexicalNode|null return $findMatchingParent(node, isBlockNode); } +export function $sortNodes(nodes: LexicalNode[]): LexicalNode[] { + const idChain: string[] = []; + const addIds = (n: ElementNode) => { + for (const child of n.getChildren()) { + idChain.push(child.getKey()) + if ($isElementNode(child)) { + addIds(child) + } + } + }; + + const root = $getRoot(); + addIds(root); + + const sorted = Array.from(nodes); + sorted.sort((a, b) => { + const aIndex = idChain.indexOf(a.getKey()); + const bIndex = idChain.indexOf(b.getKey()); + return aIndex - bIndex; + }); + + return sorted; +} + export function nodeHasAlignment(node: object): node is NodeHasAlignment { return '__alignment' in node; } From 9bfcadd95f4c3917ff4aa4fd4b10bfa7ce4c6c09 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 28 Mar 2025 14:17:52 +0000 Subject: [PATCH 012/474] Lexical: Improved navigation around images/media - Added specific handling to move/insert-up/down on arrow press. - Prevented resize overlay from interrupting image node focus. --- .../lexical/rich-text/LexicalImageNode.ts | 2 +- .../js/wysiwyg/services/keyboard-handling.ts | 43 +++++++++++++++++-- resources/sass/_editor.scss | 2 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts index 9f42ad73204..40f4ab71165 100644 --- a/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts +++ b/resources/js/wysiwyg/lexical/rich-text/LexicalImageNode.ts @@ -133,7 +133,7 @@ export class ImageNode extends ElementNode { element.addEventListener('click', e => { _editor.update(() => { - $selectSingleNode(this); + this.select(); }); }); diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index ff6117b2b87..0ef0b81bff8 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -3,7 +3,7 @@ import { $createParagraphNode, $getSelection, $isDecoratorNode, - COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, + COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_BACKSPACE_COMMAND, KEY_DELETE_COMMAND, KEY_ENTER_COMMAND, KEY_TAB_COMMAND, @@ -43,7 +43,7 @@ function deleteSingleSelectedNode(editor: LexicalEditor) { } /** - * Insert a new empty node after the selection if the selection contains a single + * Insert a new empty node before/after the selection if the selection contains a single * selected node (like image, media etc...). */ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEvent|null): boolean { @@ -67,6 +67,37 @@ function insertAfterSingleSelectedNode(editor: LexicalEditor, event: KeyboardEve return false; } +function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: KeyboardEvent|null, after: boolean = true): boolean { + const selectionNodes = getLastSelection(editor)?.getNodes() || []; + if (!isSingleSelectedNode(selectionNodes)) { + return false; + } + + event?.preventDefault(); + + const node = selectionNodes[0]; + const nearestBlock = $getNearestNodeBlockParent(node) || node; + let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling(); + + requestAnimationFrame(() => { + editor.update(() => { + if (!target) { + target = $createParagraphNode(); + if (after) { + nearestBlock.insertAfter(target) + } else { + nearestBlock.insertBefore(target); + } + } + + target.selectStart(); + }); + }); + + + return true; +} + /** * Insert a new node after a details node, if inside a details node that's * the last element, and if the cursor is at the last block within the details node. @@ -199,8 +230,13 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { return handleInsetOnTab(context.editor, event); }, COMMAND_PRIORITY_LOW); + const unregisterUp = context.editor.registerCommand(KEY_ARROW_UP_COMMAND, (event): boolean => { + return focusAdjacentOrInsertForSingleSelectNode(context.editor, event, false); + }, COMMAND_PRIORITY_LOW); + const unregisterDown = context.editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (event): boolean => { - return insertAfterDetails(context.editor, event); + return insertAfterDetails(context.editor, event) + || focusAdjacentOrInsertForSingleSelectNode(context.editor, event, true) }, COMMAND_PRIORITY_LOW); return () => { @@ -208,6 +244,7 @@ export function registerKeyboardHandling(context: EditorUiContext): () => void { unregisterDelete(); unregisterEnter(); unregisterTab(); + unregisterUp(); unregisterDown(); }; } \ No newline at end of file diff --git a/resources/sass/_editor.scss b/resources/sass/_editor.scss index 9f7694e858c..35f11c5a279 100644 --- a/resources/sass/_editor.scss +++ b/resources/sass/_editor.scss @@ -370,8 +370,10 @@ body.editor-is-fullscreen { display: inline-block; outline: 2px dashed var(--editor-color-primary); direction: ltr; + pointer-events: none; } .editor-node-resizer-handle { + pointer-events: auto; position: absolute; display: block; width: 10px; From bb44334224d6eee53b229e1f59d63b4f1ef45e68 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 28 Mar 2025 18:29:00 +0000 Subject: [PATCH 013/474] Lexical: Added tests to cover recent changes Also updated list tests to new test process. --- .../lexical/core/__tests__/utils/index.ts | 4 + .../__tests__/unit/LexicalListNode.test.ts | 336 +++++++++--------- .../__tests__/keyboard-handling.test.ts | 33 +- .../js/wysiwyg/services/keyboard-handling.ts | 21 +- 4 files changed, 219 insertions(+), 175 deletions(-) diff --git a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts index d54a64ce89a..6a8e457248b 100644 --- a/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts +++ b/resources/js/wysiwyg/lexical/core/__tests__/utils/index.ts @@ -37,6 +37,7 @@ import {QuoteNode} from "@lexical/rich-text/LexicalQuoteNode"; import {DetailsNode} from "@lexical/rich-text/LexicalDetailsNode"; import {EditorUiContext} from "../../../../ui/framework/core"; import {EditorUIManager} from "../../../../ui/framework/manager"; +import {ImageNode} from "@lexical/rich-text/LexicalImageNode"; type TestEnv = { readonly container: HTMLDivElement; @@ -484,6 +485,9 @@ export function createTestContext(): EditorUiContext { const editor = createTestEditor({ namespace: 'testing', theme: {}, + nodes: [ + ImageNode, + ] }); editor.setRootElement(editorDOM); diff --git a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts index 8c7729dbff1..b85383e7da5 100644 --- a/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts +++ b/resources/js/wysiwyg/lexical/list/__tests__/unit/LexicalListNode.test.ts @@ -6,7 +6,7 @@ * */ import {ParagraphNode, TextNode} from 'lexical'; -import {initializeUnitTest} from 'lexical/__tests__/utils'; +import {createTestContext} from 'lexical/__tests__/utils'; import { $createListItemNode, @@ -16,6 +16,7 @@ import { ListItemNode, ListNode, } from '../..'; +import {$htmlToBlockNodes} from "../../../../utils/nodes"; const editorConfig = Object.freeze({ namespace: '', @@ -46,123 +47,122 @@ const editorConfig = Object.freeze({ }); describe('LexicalListNode tests', () => { - initializeUnitTest((testEnv) => { - test('ListNode.constructor', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - expect(listNode.getType()).toBe('list'); - expect(listNode.getTag()).toBe('ul'); - expect(listNode.getTextContent()).toBe(''); - }); - - // @ts-expect-error - expect(() => $createListNode()).toThrow(); + test('ListNode.constructor', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + expect(listNode.getType()).toBe('list'); + expect(listNode.getTag()).toBe('ul'); + expect(listNode.getTextContent()).toBe(''); }); - test('ListNode.getTag()', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const ulListNode = $createListNode('bullet', 1); - expect(ulListNode.getTag()).toBe('ul'); - const olListNode = $createListNode('number', 1); - expect(olListNode.getTag()).toBe('ol'); - const checkListNode = $createListNode('check', 1); - expect(checkListNode.getTag()).toBe('ul'); - }); + // @ts-expect-error + expect(() => $createListNode()).toThrow(); + }); + + test('ListNode.getTag()', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const ulListNode = $createListNode('bullet', 1); + expect(ulListNode.getTag()).toBe('ul'); + const olListNode = $createListNode('number', 1); + expect(olListNode.getTag()).toBe('ol'); + const checkListNode = $createListNode('check', 1); + expect(checkListNode.getTag()).toBe('ul'); }); + }); - test('ListNode.createDOM()', async () => { - const {editor} = testEnv; + test('ListNode.createDOM()', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - expect(listNode.createDOM(editorConfig).outerHTML).toBe( + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + expect(listNode.createDOM(editorConfig).outerHTML).toBe( '
    ', - ); - expect( + ); + expect( listNode.createDOM({ namespace: '', theme: { list: {}, }, }).outerHTML, - ).toBe('
      '); - expect( + ).toBe('
        '); + expect( listNode.createDOM({ namespace: '', theme: {}, }).outerHTML, - ).toBe('
          '); - }); + ).toBe('
            '); }); + }); - test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => { - const {editor} = testEnv; - - await editor.update(() => { - const listNode1 = $createListNode('bullet'); - const listNode2 = $createListNode('bullet'); - const listNode3 = $createListNode('bullet'); - const listNode4 = $createListNode('bullet'); - const listNode5 = $createListNode('bullet'); - const listNode6 = $createListNode('bullet'); - const listNode7 = $createListNode('bullet'); - - const listItem1 = $createListItemNode(); - const listItem2 = $createListItemNode(); - const listItem3 = $createListItemNode(); - const listItem4 = $createListItemNode(); - - listNode1.append(listItem1); - listItem1.append(listNode2); - listNode2.append(listItem2); - listItem2.append(listNode3); - listNode3.append(listItem3); - listItem3.append(listNode4); - listNode4.append(listItem4); - listNode4.append(listNode5); - listNode5.append(listNode6); - listNode6.append(listNode7); - - expect(listNode1.createDOM(editorConfig).outerHTML).toBe( + test('ListNode.createDOM() correctly applies classes to a nested ListNode', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode1 = $createListNode('bullet'); + const listNode2 = $createListNode('bullet'); + const listNode3 = $createListNode('bullet'); + const listNode4 = $createListNode('bullet'); + const listNode5 = $createListNode('bullet'); + const listNode6 = $createListNode('bullet'); + const listNode7 = $createListNode('bullet'); + + const listItem1 = $createListItemNode(); + const listItem2 = $createListItemNode(); + const listItem3 = $createListItemNode(); + const listItem4 = $createListItemNode(); + + listNode1.append(listItem1); + listItem1.append(listNode2); + listNode2.append(listItem2); + listItem2.append(listNode3); + listNode3.append(listItem3); + listItem3.append(listNode4); + listNode4.append(listItem4); + listNode4.append(listNode5); + listNode5.append(listNode6); + listNode6.append(listNode7); + + expect(listNode1.createDOM(editorConfig).outerHTML).toBe( '
              ', - ); - expect( + ); + expect( listNode1.createDOM({ namespace: '', theme: { list: {}, }, }).outerHTML, - ).toBe('
                '); - expect( + ).toBe('
                  '); + expect( listNode1.createDOM({ namespace: '', theme: {}, }).outerHTML, - ).toBe('
                    '); - expect(listNode2.createDOM(editorConfig).outerHTML).toBe( + ).toBe('
                      '); + expect(listNode2.createDOM(editorConfig).outerHTML).toBe( '
                        ', - ); - expect(listNode3.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode3.createDOM(editorConfig).outerHTML).toBe( '
                          ', - ); - expect(listNode4.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode4.createDOM(editorConfig).outerHTML).toBe( '
                            ', - ); - expect(listNode5.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode5.createDOM(editorConfig).outerHTML).toBe( '
                              ', - ); - expect(listNode6.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode6.createDOM(editorConfig).outerHTML).toBe( '
                                ', - ); - expect(listNode7.createDOM(editorConfig).outerHTML).toBe( + ); + expect(listNode7.createDOM(editorConfig).outerHTML).toBe( '
                                  ', - ); - expect( + ); + expect( listNode5.createDOM({ namespace: '', theme: { @@ -176,123 +176,135 @@ describe('LexicalListNode tests', () => { }, }, }).outerHTML, - ).toBe('
                                    '); - }); + ).toBe('
                                      '); }); + }); - test('ListNode.updateDOM()', async () => { - const {editor} = testEnv; + test('ListNode.updateDOM()', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - const domElement = listNode.createDOM(editorConfig); + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + const domElement = listNode.createDOM(editorConfig); - expect(domElement.outerHTML).toBe( + expect(domElement.outerHTML).toBe( '
                                        ', - ); + ); - const newListNode = $createListNode('number', 1); - const result = newListNode.updateDOM( + const newListNode = $createListNode('number', 1); + const result = newListNode.updateDOM( listNode, domElement, editorConfig, - ); + ); - expect(result).toBe(true); - expect(domElement.outerHTML).toBe( + expect(result).toBe(true); + expect(domElement.outerHTML).toBe( '
                                          ', - ); - }); + ); }); + }); - test('ListNode.append() should properly transform a ListItemNode', async () => { - const {editor} = testEnv; + test('ListNode.append() should properly transform a ListItemNode', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const listItemNode = new ListItemNode(); - const textNode = new TextNode('Hello'); + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const listItemNode = new ListItemNode(); + const textNode = new TextNode('Hello'); - listItemNode.append(textNode); - const nodesToAppend = [listItemNode]; + listItemNode.append(textNode); + const nodesToAppend = [listItemNode]; - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect(listNode.getFirstChild()).toBe(listItemNode); - expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); - }); + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect(listNode.getFirstChild()).toBe(listItemNode); + expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); }); + }); - test('ListNode.append() should properly transform a ListNode', async () => { - const {editor} = testEnv; + test('ListNode.append() should properly transform a ListNode', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const nestedListNode = new ListNode('bullet', 1); - const listItemNode = new ListItemNode(); - const textNode = new TextNode('Hello'); + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const nestedListNode = new ListNode('bullet', 1); + const listItemNode = new ListItemNode(); + const textNode = new TextNode('Hello'); - listItemNode.append(textNode); - nestedListNode.append(listItemNode); + listItemNode.append(textNode); + nestedListNode.append(listItemNode); - const nodesToAppend = [nestedListNode]; + const nodesToAppend = [nestedListNode]; - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect($isListItemNode(listNode.getFirstChild())).toBe(true); - expect(listNode.getFirstChild()!.getFirstChild()).toBe( + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect($isListItemNode(listNode.getFirstChild())).toBe(true); + expect(listNode.getFirstChild()!.getFirstChild()).toBe( nestedListNode, - ); - }); + ); }); + }); - test('ListNode.append() should properly transform a ParagraphNode', async () => { - const {editor} = testEnv; + test('ListNode.append() should properly transform a ParagraphNode', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = new ListNode('bullet', 1); - const paragraph = new ParagraphNode(); - const textNode = new TextNode('Hello'); - paragraph.append(textNode); - const nodesToAppend = [paragraph]; + await editor.update(() => { + const listNode = new ListNode('bullet', 1); + const paragraph = new ParagraphNode(); + const textNode = new TextNode('Hello'); + paragraph.append(textNode); + const nodesToAppend = [paragraph]; - expect(listNode.append(...nodesToAppend)).toBe(listNode); - expect($isListItemNode(listNode.getFirstChild())).toBe(true); - expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); - }); + expect(listNode.append(...nodesToAppend)).toBe(listNode); + expect($isListItemNode(listNode.getFirstChild())).toBe(true); + expect(listNode.getFirstChild()?.getTextContent()).toBe('Hello'); }); + }); - test('$createListNode()', async () => { - const {editor} = testEnv; + test('$createListNode()', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); - const createdListNode = $createListNode('bullet'); + await editor.update(() => { + const listNode = $createListNode('bullet', 1); + const createdListNode = $createListNode('bullet'); - expect(listNode.__type).toEqual(createdListNode.__type); - expect(listNode.__parent).toEqual(createdListNode.__parent); - expect(listNode.__tag).toEqual(createdListNode.__tag); - expect(listNode.__key).not.toEqual(createdListNode.__key); - }); + expect(listNode.__type).toEqual(createdListNode.__type); + expect(listNode.__parent).toEqual(createdListNode.__parent); + expect(listNode.__tag).toEqual(createdListNode.__tag); + expect(listNode.__key).not.toEqual(createdListNode.__key); }); + }); + + test('$isListNode()', async () => { + const {editor} = createTestContext(); + + await editor.update(() => { + const listNode = $createListNode('bullet', 1); - test('$isListNode()', async () => { - const {editor} = testEnv; + expect($isListNode(listNode)).toBe(true); + }); + }); - await editor.update(() => { - const listNode = $createListNode('bullet', 1); + test('$createListNode() with tag name (backward compatibility)', async () => { + const {editor} = createTestContext(); - expect($isListNode(listNode)).toBe(true); - }); + await editor.update(() => { + const numberList = $createListNode('number', 1); + const bulletList = $createListNode('bullet', 1); + expect(numberList.__listType).toBe('number'); + expect(bulletList.__listType).toBe('bullet'); }); + }); - test('$createListNode() with tag name (backward compatibility)', async () => { - const {editor} = testEnv; + test('importDOM handles old editor expected task list format', async () => { + const {editor} = createTestContext(); - await editor.update(() => { - const numberList = $createListNode('number', 1); - const bulletList = $createListNode('bullet', 1); - expect(numberList.__listType).toBe('number'); - expect(bulletList.__listType).toBe('bullet'); - }); + let list!: ListNode; + editor.update(() => { + const nodes = $htmlToBlockNodes(editor, `
                                          • A
                                          `); + list = nodes[0] as ListNode; }); + + expect(list).toBeInstanceOf(ListNode); + expect(list.getListType()).toBe('check'); }); }); diff --git a/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts b/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts index 736c3573c5b..cd4235f2f06 100644 --- a/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts +++ b/resources/js/wysiwyg/services/__tests__/keyboard-handling.test.ts @@ -1,7 +1,7 @@ import { createTestContext, destroyFromContext, dispatchKeydownEventForNode, - dispatchKeydownEventForSelectedNode, + dispatchKeydownEventForSelectedNode, expectNodeShapeToMatch, } from "lexical/__tests__/utils"; import { $createParagraphNode, $createTextNode, @@ -13,6 +13,7 @@ import {registerKeyboardHandling} from "../keyboard-handling"; import {registerRichText} from "@lexical/rich-text"; import {EditorUiContext} from "../../ui/framework/core"; import {$createListItemNode, $createListNode, ListItemNode, ListNode} from "@lexical/list"; +import {$createImageNode, ImageNode} from "@lexical/rich-text/LexicalImageNode"; describe('Keyboard-handling service tests', () => { @@ -127,4 +128,34 @@ describe('Keyboard-handling service tests', () => { expect(selectedNode?.getKey()).toBe(innerList.getChildren()[0].getKey()); }); }); + + test('Images: up on selected image creates new paragraph if none above', () => { + let image!: ImageNode; + editor.updateAndCommit(() => { + const root = $getRoot(); + const imageWrap = $createParagraphNode(); + image = $createImageNode('https://example.com/cat.png'); + imageWrap.append(image); + root.append(imageWrap); + image.select(); + }); + + expectNodeShapeToMatch(editor, [{ + type: 'paragraph', + children: [ + {type: 'image'} + ], + }]); + + dispatchKeydownEventForNode(image, editor, 'ArrowUp'); + + expectNodeShapeToMatch(editor, [{ + type: 'paragraph', + }, { + type: 'paragraph', + children: [ + {type: 'image'} + ], + }]); + }); }); \ No newline at end of file diff --git a/resources/js/wysiwyg/services/keyboard-handling.ts b/resources/js/wysiwyg/services/keyboard-handling.ts index 0ef0b81bff8..a7f1ec7f0b0 100644 --- a/resources/js/wysiwyg/services/keyboard-handling.ts +++ b/resources/js/wysiwyg/services/keyboard-handling.ts @@ -79,22 +79,19 @@ function focusAdjacentOrInsertForSingleSelectNode(editor: LexicalEditor, event: const nearestBlock = $getNearestNodeBlockParent(node) || node; let target = after ? nearestBlock.getNextSibling() : nearestBlock.getPreviousSibling(); - requestAnimationFrame(() => { - editor.update(() => { - if (!target) { - target = $createParagraphNode(); - if (after) { - nearestBlock.insertAfter(target) - } else { - nearestBlock.insertBefore(target); - } + editor.update(() => { + if (!target) { + target = $createParagraphNode(); + if (after) { + nearestBlock.insertAfter(target) + } else { + nearestBlock.insertBefore(target); } + } - target.selectStart(); - }); + target.selectStart(); }); - return true; } From 802f69cf35abba8ff75b0ef7bbed09bbcda62816 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 30 Mar 2025 17:36:48 +0100 Subject: [PATCH 014/474] Comments: Fixed missing comment timestamps Due to deleted code during Laravel 11 upgrade. Added test to cover. Closes #5555 --- resources/views/comments/comment.blade.php | 2 +- tests/Entity/CommentTest.php | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index b507a810bb1..2bf89d6832d 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -24,7 +24,7 @@ class="comment-box"> @else {{ trans('common.deleted_user') }} @endif -  {{ trans('entities.comment_created', ['createDiff' => $comment->created]) }} +  {{ trans('entities.comment_created', ['createDiff' => $comment->created_at->diffForHumans() ]) }} @if($comment->isUpdated()) diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 73136235ce0..9e019e3d148 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -214,4 +214,21 @@ public function test_comment_editor_js_loaded_with_create_or_edit_permissions() $resp->assertSee('window.editor_translations', false); $resp->assertSee('component="entity-selector"', false); } + + public function test_comment_displays_relative_times() + { + $page = $this->entities->page(); + $comment = Comment::factory()->create(['entity_id' => $page->id, 'entity_type' => $page->getMorphClass()]); + $comment->created_at = now()->subWeek(); + $comment->updated_at = now()->subDay(); + $comment->save(); + + $pageResp = $this->asAdmin()->get($page->getUrl()); + $html = $this->withHtml($pageResp); + + // Create date shows relative time as text to user + $html->assertElementContains('.comment-box', 'commented 1 week ago'); + // Updated indicator has full time as title + $html->assertElementContains('.comment-box span[title^="Updated ' . $comment->updated_at->format('Y-m-d') . '"]', 'Updated'); + } } From 1ba0d26fdd1e673dfcaefc87e2fa38f08cac61ad Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 2 Apr 2025 15:17:17 +0100 Subject: [PATCH 015/474] Sort Rules: Updated name comparison to not ignore non-ascii chars Related to #5550 and #5542 --- app/Sorting/SortSetOperationComparisons.php | 4 ++-- tests/Sorting/SortRuleTest.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/Sorting/SortSetOperationComparisons.php b/app/Sorting/SortSetOperationComparisons.php index e506e9a139d..cb20e1860f7 100644 --- a/app/Sorting/SortSetOperationComparisons.php +++ b/app/Sorting/SortSetOperationComparisons.php @@ -14,12 +14,12 @@ class SortSetOperationComparisons { public static function nameAsc(Entity $a, Entity $b): int { - return strtolower(ASCII::to_transliterate($a->name)) <=> strtolower(ASCII::to_transliterate($b->name)); + return strtolower(ASCII::to_transliterate($a->name, null)) <=> strtolower(ASCII::to_transliterate($b->name, null)); } public static function nameDesc(Entity $a, Entity $b): int { - return strtolower(ASCII::to_transliterate($b->name)) <=> strtolower(ASCII::to_transliterate($a->name)); + return strtolower(ASCII::to_transliterate($b->name, null)) <=> strtolower(ASCII::to_transliterate($a->name, null)); } public static function nameNumericAsc(Entity $a, Entity $b): int diff --git a/tests/Sorting/SortRuleTest.php b/tests/Sorting/SortRuleTest.php index b0f20cba5ae..4a9d3a7b33f 100644 --- a/tests/Sorting/SortRuleTest.php +++ b/tests/Sorting/SortRuleTest.php @@ -200,6 +200,8 @@ public function test_name_alphabetical_ordering() "bread", "Éclaire", "egg", + "É😀ire", + "É🫠ire", "Milk", "pizza", "Tomato", From 61173498933fe230c75de9893645744578dba48c Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 2 Apr 2025 15:30:31 +0100 Subject: [PATCH 016/474] Deps: Updated composer packages --- composer.lock | 278 +++++++++++++++++++++++++------------------------- 1 file changed, 139 insertions(+), 139 deletions(-) diff --git a/composer.lock b/composer.lock index 4dfc8ec85a4..e3e592852c1 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.342.6", + "version": "3.342.18", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "2ad93ef447289da27892707efa683c1ae7ced85f" + "reference": "e6c81bf323b082f8fe2b76d8d41e2614806d5892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2ad93ef447289da27892707efa683c1ae7ced85f", - "reference": "2ad93ef447289da27892707efa683c1ae7ced85f", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e6c81bf323b082f8fe2b76d8d41e2614806d5892", + "reference": "e6c81bf323b082f8fe2b76d8d41e2614806d5892", "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.342.6" + "source": "https://github.com/aws/aws-sdk-php/tree/3.342.18" }, - "time": "2025-03-14T18:01:18+00:00" + "time": "2025-04-01T18:15:02+00:00" }, { "name": "bacon/bacon-qr-code", @@ -855,16 +855,16 @@ }, { "name": "egulias/email-validator", - "version": "4.0.3", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "b115554301161fa21467629f1e1391c1936de517" + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/b115554301161fa21467629f1e1391c1936de517", - "reference": "b115554301161fa21467629f1e1391c1936de517", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", "shasum": "" }, "require": { @@ -910,7 +910,7 @@ ], "support": { "issues": "https://github.com/egulias/EmailValidator/issues", - "source": "https://github.com/egulias/EmailValidator/tree/4.0.3" + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" }, "funding": [ { @@ -918,7 +918,7 @@ "type": "github" } ], - "time": "2024-12-27T00:36:43+00:00" + "time": "2025-03-06T22:45:56+00:00" }, { "name": "firebase/php-jwt", @@ -1118,16 +1118,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.9.2", + "version": "7.9.3", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b" + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d281ed313b989f213357e3be1a179f02196ac99b", - "reference": "d281ed313b989f213357e3be1a179f02196ac99b", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", "shasum": "" }, "require": { @@ -1224,7 +1224,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.9.2" + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" }, "funding": [ { @@ -1240,20 +1240,20 @@ "type": "tidelift" } ], - "time": "2024-07-24T11:22:20+00:00" + "time": "2025-03-27T13:37:11+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.0.4", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", - "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", "shasum": "" }, "require": { @@ -1307,7 +1307,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.4" + "source": "https://github.com/guzzle/promises/tree/2.2.0" }, "funding": [ { @@ -1323,20 +1323,20 @@ "type": "tidelift" } ], - "time": "2024-10-17T10:06:22+00:00" + "time": "2025-03-27T13:27:01+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.7.0", + "version": "2.7.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", - "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", "shasum": "" }, "require": { @@ -1423,7 +1423,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.7.0" + "source": "https://github.com/guzzle/psr7/tree/2.7.1" }, "funding": [ { @@ -1439,7 +1439,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T11:15:46+00:00" + "time": "2025-03-27T12:30:47+00:00" }, { "name": "guzzlehttp/uri-template", @@ -1529,16 +1529,16 @@ }, { "name": "intervention/gif", - "version": "4.2.1", + "version": "4.2.2", "source": { "type": "git", "url": "https://github.com/Intervention/gif.git", - "reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49" + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Intervention/gif/zipball/6addac2c68b4bc0e37d0d3ccedda57eb84729c49", - "reference": "6addac2c68b4bc0e37d0d3ccedda57eb84729c49", + "url": "https://api.github.com/repos/Intervention/gif/zipball/5999eac6a39aa760fb803bc809e8909ee67b451a", + "reference": "5999eac6a39aa760fb803bc809e8909ee67b451a", "shasum": "" }, "require": { @@ -1546,7 +1546,7 @@ }, "require-dev": { "phpstan/phpstan": "^2.1", - "phpunit/phpunit": "^10.0 || ^11.0", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "slevomat/coding-standard": "~8.0", "squizlabs/php_codesniffer": "^3.8" }, @@ -1577,7 +1577,7 @@ ], "support": { "issues": "https://github.com/Intervention/gif/issues", - "source": "https://github.com/Intervention/gif/tree/4.2.1" + "source": "https://github.com/Intervention/gif/tree/4.2.2" }, "funding": [ { @@ -1593,7 +1593,7 @@ "type": "ko_fi" } ], - "time": "2025-01-05T10:52:39+00:00" + "time": "2025-03-29T07:46:21+00:00" }, { "name": "intervention/image", @@ -2014,16 +2014,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.3", + "version": "v2.0.4", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f" + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f", - "reference": "f379c13663245f7aa4512a7869f62eb14095f23f", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", "shasum": "" }, "require": { @@ -2071,20 +2071,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-02-11T15:03:05+00:00" + "time": "2025-03-19T13:51:03+00:00" }, { "name": "laravel/socialite", - "version": "v5.18.0", + "version": "v5.19.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "7809dc71250e074cd42970f0f803f2cddc04c5de" + "reference": "c40f843c5643fb6b089e46ce9794b8408bf08319" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/7809dc71250e074cd42970f0f803f2cddc04c5de", - "reference": "7809dc71250e074cd42970f0f803f2cddc04c5de", + "url": "https://api.github.com/repos/laravel/socialite/zipball/c40f843c5643fb6b089e46ce9794b8408bf08319", + "reference": "c40f843c5643fb6b089e46ce9794b8408bf08319", "shasum": "" }, "require": { @@ -2101,7 +2101,7 @@ "require-dev": { "mockery/mockery": "^1.0", "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", - "phpstan/phpstan": "^1.10", + "phpstan/phpstan": "^1.12.23", "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" }, "type": "library", @@ -2143,7 +2143,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-02-11T13:38:19+00:00" + "time": "2025-03-27T17:26:42+00:00" }, { "name": "laravel/tinker", @@ -3116,16 +3116,16 @@ }, { "name": "monolog/monolog", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4" + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/aef6ee73a77a66e404dd6540934a9ef1b3c855b4", - "reference": "aef6ee73a77a66e404dd6540934a9ef1b3c855b4", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", "shasum": "" }, "require": { @@ -3203,7 +3203,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.8.1" + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" }, "funding": [ { @@ -3215,7 +3215,7 @@ "type": "tidelift" } ], - "time": "2024-12-05T17:15:07+00:00" + "time": "2025-03-24T10:02:05+00:00" }, { "name": "mtdowling/jmespath.php", @@ -3285,16 +3285,16 @@ }, { "name": "nesbot/carbon", - "version": "3.8.6", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd" + "reference": "6d16a8a015166fe54e22c042e0805c5363aef50d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ff2f20cf83bd4d503720632ce8a426dc747bf7fd", - "reference": "ff2f20cf83bd4d503720632ce8a426dc747bf7fd", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6d16a8a015166fe54e22c042e0805c5363aef50d", + "reference": "6d16a8a015166fe54e22c042e0805c5363aef50d", "shasum": "" }, "require": { @@ -3387,7 +3387,7 @@ "type": "tidelift" } ], - "time": "2025-02-20T17:33:38+00:00" + "time": "2025-03-27T12:57:33+00:00" }, { "name": "nette/schema", @@ -3453,16 +3453,16 @@ }, { "name": "nette/utils", - "version": "v4.0.5", + "version": "v4.0.6", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + "reference": "ce708655043c7050eb050df361c5e313cf708309" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "url": "https://api.github.com/repos/nette/utils/zipball/ce708655043c7050eb050df361c5e313cf708309", + "reference": "ce708655043c7050eb050df361c5e313cf708309", "shasum": "" }, "require": { @@ -3533,9 +3533,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.5" + "source": "https://github.com/nette/utils/tree/v4.0.6" }, - "time": "2024-08-07T15:39:19+00:00" + "time": "2025-03-30T21:06:30+00:00" }, { "name": "nikic/php-parser", @@ -4698,16 +4698,16 @@ }, { "name": "ramsey/collection", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/ramsey/collection.git", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109" + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/collection/zipball/3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", - "reference": "3c5990b8a5e0b79cd1cf11c2dc1229e58e93f109", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", "shasum": "" }, "require": { @@ -4768,9 +4768,9 @@ ], "support": { "issues": "https://github.com/ramsey/collection/issues", - "source": "https://github.com/ramsey/collection/tree/2.1.0" + "source": "https://github.com/ramsey/collection/tree/2.1.1" }, - "time": "2025-03-02T04:48:29+00:00" + "time": "2025-03-22T05:38:12+00:00" }, { "name": "ramsey/uuid", @@ -4908,16 +4908,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v8.7.0", + "version": "v8.8.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf" + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/f414ff953002a9b18e3a116f5e462c56f21237cf", - "reference": "f414ff953002a9b18e3a116f5e462c56f21237cf", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/3de493bdddfd1f051249af725c7e0d2c38fed740", + "reference": "3de493bdddfd1f051249af725c7e0d2c38fed740", "shasum": "" }, "require": { @@ -4925,7 +4925,7 @@ "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" }, "require-dev": { - "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.40" + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -4967,9 +4967,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.7.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.8.0" }, - "time": "2024-10-27T17:38:32+00:00" + "time": "2025-03-23T17:59:05+00:00" }, { "name": "socialiteproviders/discord", @@ -5488,16 +5488,16 @@ }, { "name": "symfony/console", - "version": "v7.2.1", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3" + "reference": "e51498ea18570c062e7df29d05a7003585b19b88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/fefcc18c0f5d0efe3ab3152f15857298868dc2c3", - "reference": "fefcc18c0f5d0efe3ab3152f15857298868dc2c3", + "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88", + "reference": "e51498ea18570c062e7df29d05a7003585b19b88", "shasum": "" }, "require": { @@ -5561,7 +5561,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.1" + "source": "https://github.com/symfony/console/tree/v7.2.5" }, "funding": [ { @@ -5577,7 +5577,7 @@ "type": "tidelift" } ], - "time": "2024-12-11T03:49:26+00:00" + "time": "2025-03-12T08:11:12+00:00" }, { "name": "symfony/css-selector", @@ -5713,16 +5713,16 @@ }, { "name": "symfony/error-handler", - "version": "v7.2.4", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5" + "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/aabf79938aa795350c07ce6464dd1985607d95d5", - "reference": "aabf79938aa795350c07ce6464dd1985607d95d5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b", + "reference": "102be5e6a8e4f4f3eb3149bcbfa33a80d1ee374b", "shasum": "" }, "require": { @@ -5768,7 +5768,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.2.4" + "source": "https://github.com/symfony/error-handler/tree/v7.2.5" }, "funding": [ { @@ -5784,7 +5784,7 @@ "type": "tidelift" } ], - "time": "2025-02-02T20:27:07+00:00" + "time": "2025-03-03T07:12:39+00:00" }, { "name": "symfony/event-dispatcher", @@ -6008,16 +6008,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.2.3", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0" + "reference": "371272aeb6286f8135e028ca535f8e4d6f114126" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0", - "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/371272aeb6286f8135e028ca535f8e4d6f114126", + "reference": "371272aeb6286f8135e028ca535f8e4d6f114126", "shasum": "" }, "require": { @@ -6066,7 +6066,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.2.3" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.5" }, "funding": [ { @@ -6082,20 +6082,20 @@ "type": "tidelift" } ], - "time": "2025-01-17T10:56:55+00:00" + "time": "2025-03-25T15:54:33+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.4", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9f1103734c5789798fefb90e91de4586039003ed" + "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9f1103734c5789798fefb90e91de4586039003ed", - "reference": "9f1103734c5789798fefb90e91de4586039003ed", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b1fe91bc1fa454a806d3f98db4ba826eb9941a54", + "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54", "shasum": "" }, "require": { @@ -6180,7 +6180,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.2.4" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.5" }, "funding": [ { @@ -6196,7 +6196,7 @@ "type": "tidelift" } ], - "time": "2025-02-26T11:01:22+00:00" + "time": "2025-03-28T13:32:50+00:00" }, { "name": "symfony/mime", @@ -6920,16 +6920,16 @@ }, { "name": "symfony/process", - "version": "v7.2.4", + "version": "v7.2.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf" + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", - "reference": "d8f411ff3c7ddc4ae9166fb388d1190a2df5b5cf", + "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", + "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", "shasum": "" }, "require": { @@ -6961,7 +6961,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.4" + "source": "https://github.com/symfony/process/tree/v7.2.5" }, "funding": [ { @@ -6977,7 +6977,7 @@ "type": "tidelift" } ], - "time": "2025-02-05T08:33:46+00:00" + "time": "2025-03-13T12:21:46+00:00" }, { "name": "symfony/routing", @@ -8589,16 +8589,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.8", + "version": "2.1.11", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f" + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/f9adff3b87c03b12cc7e46a30a524648e497758f", - "reference": "f9adff3b87c03b12cc7e46a30a524648e497758f", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", "shasum": "" }, "require": { @@ -8643,7 +8643,7 @@ "type": "github" } ], - "time": "2025-03-09T09:30:48+00:00" + "time": "2025-03-24T13:45:00+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8970,16 +8970,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.12", + "version": "11.5.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "d42785840519401ed2113292263795eb4c0f95da" + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d42785840519401ed2113292263795eb4c0f95da", - "reference": "d42785840519401ed2113292263795eb4c0f95da", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", "shasum": "" }, "require": { @@ -8999,14 +8999,14 @@ "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", "sebastian/cli-parser": "^3.0.2", - "sebastian/code-unit": "^3.0.2", + "sebastian/code-unit": "^3.0.3", "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", "sebastian/environment": "^7.2.0", "sebastian/exporter": "^6.3.0", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", - "sebastian/type": "^5.1.0", + "sebastian/type": "^5.1.2", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" }, @@ -9051,7 +9051,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.12" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15" }, "funding": [ { @@ -9067,7 +9067,7 @@ "type": "tidelift" } ], - "time": "2025-03-07T07:31:03+00:00" + "time": "2025-03-23T16:02:11+00:00" }, { "name": "sebastian/cli-parser", @@ -9128,16 +9128,16 @@ }, { "name": "sebastian/code-unit", - "version": "3.0.2", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", - "reference": "ee88b0cdbe74cf8dd3b54940ff17643c0d6543ca", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { @@ -9173,7 +9173,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", "security": "https://github.com/sebastianbergmann/code-unit/security/policy", - "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -9181,7 +9181,7 @@ "type": "github" } ], - "time": "2024-12-12T09:59:06+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -9886,16 +9886,16 @@ }, { "name": "sebastian/type", - "version": "5.1.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac" + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/461b9c5da241511a2a0e8f240814fb23ce5c0aac", - "reference": "461b9c5da241511a2a0e8f240814fb23ce5c0aac", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "shasum": "" }, "require": { @@ -9931,7 +9931,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/type/issues", "security": "https://github.com/sebastianbergmann/type/security/policy", - "source": "https://github.com/sebastianbergmann/type/tree/5.1.0" + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" }, "funding": [ { @@ -9939,7 +9939,7 @@ "type": "github" } ], - "time": "2024-09-17T13:12:04+00:00" + "time": "2025-03-18T13:35:50+00:00" }, { "name": "sebastian/version", @@ -9997,16 +9997,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.11.3", + "version": "3.12.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10" + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", - "reference": "ba05f990e79cbe69b9f35c8c1ac8dca7eecc3a10", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630", "shasum": "" }, "require": { @@ -10073,11 +10073,11 @@ "type": "open_collective" }, { - "url": "https://thanks.dev/phpcsstandards", + "url": "https://thanks.dev/u/gh/phpcsstandards", "type": "thanks_dev" } ], - "time": "2025-01-23T17:04:15+00:00" + "time": "2025-03-18T05:04:51+00:00" }, { "name": "ssddanbrown/asserthtml", From 42cbd6adefafc053f5b666ff26b76e2843d665a1 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 2 Apr 2025 17:19:34 +0100 Subject: [PATCH 017/474] Updated translations with latest Crowdin changes (#5537) --- lang/ar/activities.php | 30 ++--- lang/ar/auth.php | 36 +++--- lang/ar/editor.php | 214 ++++++++++++++++---------------- lang/ar/entities.php | 8 +- lang/ar/errors.php | 26 ++-- lang/ar/notifications.php | 34 ++--- lang/ar/preferences.php | 76 ++++++------ lang/ar/settings.php | 206 +++++++++++++++--------------- lang/ar/validation.php | 8 +- lang/de/activities.php | 2 +- lang/de_informal/activities.php | 2 +- lang/fr/editor.php | 4 +- lang/fr/entities.php | 8 +- lang/fr/settings.php | 52 ++++---- lang/he/activities.php | 12 +- lang/he/auth.php | 12 +- lang/he/entities.php | 26 ++-- lang/pt_BR/activities.php | 12 +- lang/pt_BR/editor.php | 8 +- lang/pt_BR/entities.php | 6 +- lang/pt_BR/settings.php | 52 ++++---- lang/ru/activities.php | 12 +- lang/ru/common.php | 2 +- lang/ru/editor.php | 2 +- lang/ru/entities.php | 6 +- lang/ru/errors.php | 10 +- lang/ru/settings.php | 34 ++--- lang/sl/activities.php | 2 +- lang/sl/auth.php | 6 +- lang/sl/common.php | 12 +- lang/sl/entities.php | 16 +-- lang/sl/errors.php | 2 +- lang/sl/pagination.php | 4 +- lang/sl/settings.php | 8 +- lang/zh_CN/activities.php | 12 +- lang/zh_CN/editor.php | 4 +- lang/zh_CN/entities.php | 2 +- lang/zh_CN/settings.php | 2 +- lang/zh_TW/activities.php | 12 +- lang/zh_TW/editor.php | 8 +- lang/zh_TW/entities.php | 6 +- lang/zh_TW/settings.php | 56 ++++----- 42 files changed, 526 insertions(+), 526 deletions(-) diff --git a/lang/ar/activities.php b/lang/ar/activities.php index db807b015e4..bf5d4411fed 100644 --- a/lang/ar/activities.php +++ b/lang/ar/activities.php @@ -66,10 +66,10 @@ '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' => 'تمت إزالة طريقة متعددة العوامل بنجاح', + 'mfa_setup_method' => 'طريقة المصادقة متعددة العوامل المُهيأة', + 'mfa_setup_method_notification' => 'تم إعداد المصادقة متعددة العوامل بنجاح', + 'mfa_remove_method' => 'إزالة طريقة المصادقة متعددة العوامل', + 'mfa_remove_method_notification' => 'تمت إزالة المصادقة متعددة العوامل بنجاح', // Settings 'settings_update' => 'تحديث الإعدادات', @@ -77,12 +77,12 @@ '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 بنجاح', + 'webhook_create' => 'تم إنشاء خطاف ويب', + 'webhook_create_notification' => 'تم إنشاء خطاف ويب بنجاح', + 'webhook_update' => 'تم تحديث خطاف الويب', + 'webhook_update_notification' => 'تم تحديث خطاف الويب بنجاح', + 'webhook_delete' => 'حذف خطاف ويب', + 'webhook_delete_notification' => 'تم حذف خطاف الويب بنجاح', // Imports 'import_create' => 'تم إنشاء الاستيراد', @@ -94,7 +94,7 @@ // Users 'user_create' => 'إنشاء مستخدم', - 'user_create_notification' => 'تم انشاء الحساب', + 'user_create_notification' => 'تم إنشاء الحساب', 'user_update' => 'المستخدم المحدث', 'user_update_notification' => 'تم تحديث المستخدم بنجاح', 'user_delete' => 'المستخدم المحذوف', @@ -102,11 +102,11 @@ // API Tokens 'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-', - 'api_token_create_notification' => 'تم إنشاء رمز الـ API بنجاح', + 'api_token_create_notification' => 'تم إنشاء واجهة برمجة التطبيقات -API- بنجاح', 'api_token_update' => 'رمز واجهة برمجة التطبيقات المحدث', - 'api_token_update_notification' => 'تم تحديث رمز الـ API بنجاح', + 'api_token_update_notification' => 'تم تحديث رمز واجهة برمجة التطبيقات -API- بنجاح', 'api_token_delete' => 'رمز واجهة برمجة التطبيقات المحذوف', - 'api_token_delete_notification' => 'تم حذف رمز الـ API بنجاح', + 'api_token_delete_notification' => 'تم حذف رمز واجهة برمجة التطبيقات -API- بنجاح', // Roles 'role_create' => 'إنشاء صَلاحِيَة', @@ -136,5 +136,5 @@ 'sort_rule_delete_notification' => 'تم حذف قاعدة الفرز بنجاح', // Other - 'permissions_update' => 'تحديث الأذونات', + 'permissions_update' => 'تحديث الصلاحيات', ]; diff --git a/lang/ar/auth.php b/lang/ar/auth.php index aa406bcece4..3890589cd13 100644 --- a/lang/ar/auth.php +++ b/lang/ar/auth.php @@ -7,22 +7,22 @@ return [ 'failed' => 'البيانات المعطاة لا توافق سجلاتنا.', - 'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds seconds.', + 'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.', // Login & Register 'sign_up' => 'إنشاء حساب', 'log_in' => 'تسجيل الدخول', 'log_in_with' => 'تسجيل الدخول باستخدام :socialDriver', 'sign_up_with' => 'إنشاء حساب باستخدام :socialDriver', - 'logout' => 'تسجيل الخروج', + 'logout' => 'الخروج', 'name' => 'الاسم', 'username' => 'اسم المستخدم', 'email' => 'البريد الإلكتروني', - 'password' => 'كلمة المرور', - 'password_confirm' => 'تأكيد كلمة المرور', - 'password_hint' => 'يجب أن تحتوي كلمة المرور على 8 خانات على الأقل', - 'forgot_password' => 'نسيت كلمة المرور؟', + 'password' => 'كلمة السر', + 'password_confirm' => 'تأكيد كلمة السر', + 'password_hint' => 'يجب أن تحتوي كلمة السر على 8 خانات على الأقل', + 'forgot_password' => 'نسيت كلمة السر؟', 'remember_me' => 'تذكرني', 'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.', 'create_account' => 'إنشاء حساب', @@ -44,14 +44,14 @@ 'auto_init_start_link' => 'المتابعة مع المصادقة', // Password Reset - 'reset_password' => 'استعادة كلمة المرور', + 'reset_password' => 'استعادة كلمة السر', 'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.', 'reset_password_send_button' => 'أرسل رابط الاستعادة', - 'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة المرور إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.', - 'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.', - 'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName', - 'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.', - 'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم، فلا حاجة لاتخاذ أية خطوات.', + 'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة السر إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.', + 'reset_password_success' => 'تمت استعادة كلمة السر بنجاح.', + 'email_reset_subject' => 'استعد كلمة السر الخاصة بتطبيق :appName', + 'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة السر الخاصة بحسابكم.', + 'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة السر من قبلكم، فلا حاجة لاتخاذ أية خطوات.', // Email Confirmation 'email_confirm_subject' => 'تأكيد بريدكم الإلكتروني لتطبيق :appName', @@ -76,20 +76,20 @@ '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!', + 'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة السر سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.', + 'user_invite_page_confirm_button' => 'تأكيد كلمة السر', + 'user_invite_success_login' => 'تم تأكيد كلمة السر. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور المحددة للوصول إلى :appName!', // Multi-factor Authentication 'mfa_setup' => 'إعداد المصادقة متعددة العوامل', 'mfa_setup_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.', 'mfa_setup_configured' => 'تم إعداده مسبقاً', 'mfa_setup_reconfigure' => 'إعادة التكوين', - 'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العناصر هذه؟', + 'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟', 'mfa_setup_action' => 'إعداد (تنصيب)', 'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.', 'mfa_option_totp_title' => 'تطبيق الجوال', - 'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق محمول يدعم TOTP مثل Google Authenticator أو Authy أو Microsoft Authenticer.', + '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' => 'تأكيد وتمكين', @@ -98,7 +98,7 @@ '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_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة المرور المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.', 'mfa_gen_totp_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.', 'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد', 'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:', diff --git a/lang/ar/editor.php b/lang/ar/editor.php index 0ff5a5a18fc..c656ba5a007 100644 --- a/lang/ar/editor.php +++ b/lang/ar/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'إلغاء', 'save' => 'حفظ', 'close' => 'إغلاق', - 'apply' => 'Apply', + 'apply' => 'تطبيق', 'undo' => 'تراجع', 'redo' => 'إعادة التنفيذ', 'left' => 'يسار', @@ -25,7 +25,7 @@ 'width' => 'العرض', 'height' => 'الارتفاع', 'More' => 'المزيد', - 'select' => 'Select...', + 'select' => 'إختار...', // Toolbar 'formats' => 'التنسيقات', @@ -54,73 +54,73 @@ 'align_left' => 'محاذاة لليسار', 'align_center' => 'محاذاة بالمنتصف', 'align_right' => 'مُحاذاة لليمين', - 'align_justify' => 'Justify', + 'align_justify' => 'المحاذاة', 'list_bullet' => 'قائمة نقاط', 'list_numbered' => 'قائمة مرقمة', - 'list_task' => 'Task list', + 'list_task' => 'قائمة المهام', 'indent_increase' => 'زيادة البادئة', 'indent_decrease' => 'إنقاص البادئة', 'table' => 'جدول', 'insert_image' => 'ادراج صورة', - 'insert_image_title' => 'Insert/Edit Image', - 'insert_link' => 'Insert/edit link', - 'insert_link_title' => 'Insert/Edit Link', - 'insert_horizontal_line' => 'Insert horizontal line', - 'insert_code_block' => 'Insert code block', - 'edit_code_block' => 'Edit code block', - 'insert_drawing' => 'Insert/edit drawing', - 'drawing_manager' => 'Drawing manager', - 'insert_media' => 'Insert/edit media', - 'insert_media_title' => 'Insert/Edit Media', - 'clear_formatting' => 'Clear formatting', - 'source_code' => 'Source code', - 'source_code_title' => 'Source Code', - 'fullscreen' => 'Fullscreen', - 'image_options' => 'Image options', + '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', - 'table_properties_title' => 'Table Properties', - 'delete_table' => 'Delete table', - 'table_clear_formatting' => 'Clear table formatting', - 'resize_to_contents' => 'Resize to contents', - 'row_header' => 'Row header', - 'insert_row_before' => 'Insert row before', - 'insert_row_after' => 'Insert row after', - 'delete_row' => 'Delete row', - 'insert_column_before' => 'Insert column before', - 'insert_column_after' => 'Insert column after', - 'delete_column' => 'Delete column', - 'table_cell' => 'Cell', - 'table_row' => 'Row', - 'table_column' => 'Column', - 'cell_properties' => 'Cell properties', - 'cell_properties_title' => 'Cell Properties', - 'cell_type' => 'Cell type', - 'cell_type_cell' => 'Cell', - 'cell_scope' => 'Scope', - 'cell_type_header' => 'Header cell', - 'merge_cells' => 'Merge cells', - 'split_cell' => 'Split cell', - 'table_row_group' => 'Row Group', - 'table_column_group' => 'Column Group', - 'horizontal_align' => 'Horizontal align', - 'vertical_align' => 'Vertical align', - 'border_width' => 'Border width', - 'border_style' => 'Border style', - 'border_color' => 'Border color', - 'row_properties' => 'Row properties', - 'row_properties_title' => 'Row Properties', - 'cut_row' => 'Cut row', - 'copy_row' => 'Copy row', - 'paste_row_before' => 'Paste row before', - 'paste_row_after' => 'Paste row after', - 'row_type' => 'Row type', - 'row_type_header' => 'Header', - 'row_type_body' => 'Body', - 'row_type_footer' => 'Footer', - 'alignment' => 'Alignment', - 'cut_column' => 'Cut column', + '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' => 'لصق عمود بعد', @@ -128,54 +128,54 @@ 'cell_spacing' => 'تباعد الخلايا', 'caption' => 'الوصف', 'show_caption' => 'إظهار الوصف', - 'constrain' => 'Constrain proportions', - 'cell_border_solid' => 'Solid', - 'cell_border_dotted' => 'Dotted', - 'cell_border_dashed' => 'Dashed', - 'cell_border_double' => 'Double', - 'cell_border_groove' => 'Groove', - 'cell_border_ridge' => 'Ridge', - 'cell_border_inset' => 'Inset', - 'cell_border_outset' => 'Outset', - 'cell_border_none' => 'None', - 'cell_border_hidden' => 'Hidden', + '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' => 'Source', - 'alt_desc' => 'Alternative description', - 'embed' => 'Embed', - 'paste_embed' => 'Paste your embed code below:', - 'url' => 'URL', - 'text_to_display' => 'Text to display', - 'title' => 'Title', - 'browse_links' => 'Browse links', - 'open_link' => 'Open link', - 'open_link_in' => 'Open link in...', - 'open_link_current' => 'Current window', - 'open_link_new' => 'New window', - 'remove_link' => 'Remove link', - 'insert_collapsible' => 'Insert collapsible block', - 'collapsible_unwrap' => 'Unwrap', - 'edit_label' => 'Edit label', - 'toggle_open_closed' => 'Toggle open/closed', - 'collapsible_edit' => 'Edit collapsible block', - 'toggle_label' => 'Toggle label', + 'source' => 'المصدر', + 'alt_desc' => 'وصف بديل', + 'embed' => 'تضمين', + 'paste_embed' => 'قم بلصق الرموز المصدرية المضمنة الخاص بك أدناه:', + '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 the editor', - 'about_title' => 'About the WYSIWYG Editor', - 'editor_license' => 'Editor License & Copyright', - '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_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.', - 'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.', - 'save_continue' => 'Save Page & Continue', - 'callouts_cycle' => '(Keep pressing to toggle through types)', - 'link_selector' => 'Link to content', - 'shortcuts' => 'Shortcuts', - 'shortcut' => 'Shortcut', - 'shortcuts_intro' => 'The following shortcuts are available in the editor:', - 'windows_linux' => '(Windows/Linux)', - 'mac' => '(Mac)', - 'description' => 'Description', + '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' => '(ويندوز/لينكس)', + 'mac' => '(ماك)', + 'description' => 'الوصف', ]; diff --git a/lang/ar/entities.php b/lang/ar/entities.php index d012ca45944..aea8bf1e537 100644 --- a/lang/ar/entities.php +++ b/lang/ar/entities.php @@ -455,8 +455,8 @@ 'watch_detail_new' => 'ترقب الصفحات الجديدة', 'watch_detail_updates' => 'مشاهدة الصفحات الجديدة والتحديثات', 'watch_detail_comments' => 'مشاهدة الصفحات الجديدة والتحديثات والتعليقات', - 'watch_detail_parent_book' => 'Watching via parent book', - 'watch_detail_parent_book_ignore' => 'Ignoring via parent book', - 'watch_detail_parent_chapter' => 'Watching via parent chapter', - 'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter', + 'watch_detail_parent_book' => 'المشاهدة عبر الكتاب الرئيس', + 'watch_detail_parent_book_ignore' => 'التجاهل عبر الكتاب الرئيس', + 'watch_detail_parent_chapter' => 'المشاهدة عبر الفصل الرئيس', + 'watch_detail_parent_chapter_ignore' => 'التجاهل عبر الفصل الرئيس', ]; diff --git a/lang/ar/errors.php b/lang/ar/errors.php index 73e76a5cc01..b50c95c232f 100644 --- a/lang/ar/errors.php +++ b/lang/ar/errors.php @@ -10,7 +10,7 @@ // Auth 'error_user_exists_different_creds' => 'يوجد مستخدم ببيانات مختلفة مسجل بالنظام للبريد الإلكتروني :email.', - 'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details', + 'auth_pre_register_theme_prevention' => 'لم يتمكن حساب المستخدم من التسجيل للحصول على التفاصيل المقدمة', 'email_already_confirmed' => 'تم تأكيد البريد الإلكتروني من قبل, الرجاء محاولة تسجيل الدخول.', 'email_confirmation_invalid' => 'رابط التأكيد غير صحيح أو قد تم استخدامه من قبل, الرجاء محاولة التسجيل من جديد.', 'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.', @@ -37,7 +37,7 @@ 'social_driver_not_found' => 'لم يتم العثور على السوشيال درايفر "Social driver"', 'social_driver_not_configured' => 'لم يتم تهيئة إعدادات حسابك الاجتماعي بشكل صحيح.', 'invite_token_expired' => 'انتهت صلاحية رابط هذه الدعوة. يمكنك بدلاً من ذلك محاولة إعادة تعيين كلمة مرور حسابك.', - 'login_user_not_found' => 'A user for this action could not be found.', + 'login_user_not_found' => 'لم يتم العثور على مستخدم لهذا الإجراء.', // System 'path_not_writable' => 'لا يمكن الرفع إلى مسار :filePath. الرجاء التأكد من قابلية الكتابة إلى الخادم.', @@ -78,7 +78,7 @@ // Users 'users_cannot_delete_only_admin' => 'لا يمكن حذف المشرف الوحيد', 'users_cannot_delete_guest' => 'لا يمكن حذف المستخدم الضيف', - 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', + 'users_could_not_send_invite' => 'لم يتم إنشاء المستخدم بسبب فشل إرسال بريد الدعوة', // Roles 'role_cannot_be_edited' => 'لا يمكن تعديل هذا الدور', @@ -106,16 +106,16 @@ 'back_soon' => 'سيعود للعمل قريباً.', // 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_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' => 'لم أتمكن من قراءة المِلَفّ المضغوط -ZIP-.', + 'import_zip_cant_decode_data' => 'لم نتمكن من العثور على محتوى المِلَفّ المضغوط data.json وفك تشفيره.', + 'import_zip_no_data' => 'لا تتضمن بيانات المِلَفّ المضغوط أي محتوى متوقع للكتاب أو الفصل أو الصفحة.', + 'import_validation_failed' => 'فشل التحقق من صحة استيراد المِلَفّ المضغوط بسبب الأخطاء التالية:', + 'import_zip_failed_notification' => 'فشل استيراد المِلَفّ المضغوط.', + 'import_perms_books' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الكتب.', + 'import_perms_chapters' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الفصول.', + 'import_perms_pages' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصفحات.', + 'import_perms_images' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصور.', + 'import_perms_attachments' => 'أنت تفتقر إلى الصَّلاحِيَة المطلوب لإنشاء المرفقات.', // API errors 'api_no_authorization_found' => 'لم يتم العثور على رمز ترخيص مميز في الطلب', diff --git a/lang/ar/notifications.php b/lang/ar/notifications.php index 1afd23f1dc4..30a49a631cf 100644 --- a/lang/ar/notifications.php +++ b/lang/ar/notifications.php @@ -4,24 +4,24 @@ */ return [ - 'new_comment_subject' => 'New comment on page: :pageName', - 'new_comment_intro' => 'A user has commented on a page in :appName:', - 'new_page_subject' => 'New page: :pageName', - 'new_page_intro' => 'A new page has been created in :appName:', - 'updated_page_subject' => 'Updated page: :pageName', - 'updated_page_intro' => 'A page has been updated in :appName:', - 'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.', + 'new_comment_subject' => 'تعليق جديد على الصفحة: :pageName', + 'new_comment_intro' => 'قام أحد المستخدمين بالتعليق على صفحة في :appName:', + 'new_page_subject' => 'صفحة جديدة: :pageName', + 'new_page_intro' => 'تم إنشاء صفحة جديدة في :appName:', + 'updated_page_subject' => 'تم تحديث الصفحة: :pageName', + 'updated_page_intro' => 'تم تحديث الصفحة في :appName:', + 'updated_page_debounce' => 'لمنع تلقي عدد كبير من الإشعارات، لن يتم إرسال إشعارات إليك لفترة من الوقت لإجراء المزيد من التعديلات على هذه الصفحة بواسطة نفس المحرر.', - 'detail_page_name' => 'Page Name:', - 'detail_page_path' => 'Page Path:', - 'detail_commenter' => 'Commenter:', - 'detail_comment' => 'Comment:', - 'detail_created_by' => 'Created By:', - 'detail_updated_by' => 'Updated By:', + 'detail_page_name' => 'اسم الصفحة:', + 'detail_page_path' => 'مسار الصفحة:', + 'detail_commenter' => 'المُعَلِق:', + 'detail_comment' => 'التعليق:', + 'detail_created_by' => 'أنشئ من طرف:', + 'detail_updated_by' => 'تم التحديث بواسطة:', - 'action_view_comment' => 'View Comment', - 'action_view_page' => 'View Page', + 'action_view_comment' => 'عرض التعليق', + 'action_view_page' => 'عرض الصفحة', - 'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.', - 'footer_reason_link' => 'your notification preferences', + 'footer_reason' => 'لقد تم إرسال هذا الإشعار إليك لأن :link يغطي هذا النوع من النشاط لهذا العنصر.', + 'footer_reason_link' => 'إعدادات الإشعارات الخاصة بك', ]; diff --git a/lang/ar/preferences.php b/lang/ar/preferences.php index 2872f5f3c65..9158b4b375b 100644 --- a/lang/ar/preferences.php +++ b/lang/ar/preferences.php @@ -5,47 +5,47 @@ */ return [ - 'my_account' => 'My Account', + 'my_account' => 'حسابي', - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'UI Shortcut Preferences', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', - 'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.', + 'shortcuts' => 'الاختصارات', + 'shortcuts_interface' => 'خيارات اختصار واجهة المستخدم', + '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' => 'Notification Preferences', - 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', - 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', - 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', - 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', - 'notifications_save' => 'Save Preferences', - 'notifications_update_success' => 'Notification preferences have been updated!', - 'notifications_watched' => 'Watched & Ignored Items', - 'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.', + 'notifications' => 'إعدادات الإشعارات', + 'notifications_desc' => 'التحكم في إشعارات البريد الإلكتروني الذي تتلقاها عند إجراء نشاط معين داخل النظام.', + 'notifications_opt_own_page_changes' => 'إشعاري عند حدوث تغييرات في الصفحات التي أملكها', + 'notifications_opt_own_page_comments' => 'إشعاري بشأن التعليقات على الصفحات التي أملكها', + 'notifications_opt_comment_replies' => 'إشعاري عند الردود على تعليقاتي', + 'notifications_save' => 'حفظ اﻹعدادات', + 'notifications_update_success' => 'تم تحديث إعدادات الإشعارات!', + 'notifications_watched' => 'العناصر التي تمت مشاهدتها وتجاهلها', + 'notifications_watched_desc' => 'فيما يلي العناصر التي طُبِّقت عليها إعدادات ساعة مخصصة. لتحديث إعداداتك، استعرض العنصر ثم ابحث عن خيارات الساعة في الشريط الجانبي.', - 'auth' => 'Access & Security', - 'auth_change_password' => 'Change Password', - 'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.', - 'auth_change_password_success' => 'Password has been updated!', + 'auth' => 'الوصول و الأمان', + 'auth_change_password' => 'تغيير كلمة السر', + 'auth_change_password_desc' => 'غيّر كلمة السر التي تستخدمها لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.', + 'auth_change_password_success' => 'تم تحديث كلمة السر!', - 'profile' => 'Profile Details', - 'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.', - 'profile_view_public' => 'View Public Profile', - 'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.', - 'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.', - 'profile_email_no_permission' => 'Unfortunately you don\'t have permission to change your email address. If you want to change this, you\'d need to ask an administrator to change this for you.', - 'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.', - 'profile_admin_options' => 'Administrator Options', - 'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the "Settings > Users" area of the application.', + 'profile' => 'تفاصيل المِلَفّ الشخصي', + 'profile_desc' => 'إدارة تفاصيل حسابك الذي يمثلك أمام المستخدمين الآخرين، بالإضافة إلى التفاصيل المستخدمة للتواصل وتخصيص النظام.', + 'profile_view_public' => 'عرض المِلَفّ الشخصي العام', + 'profile_name_desc' => 'إعداد اسم العرض الخاص بك الذي سيكون مرئيًا للمستخدمين الآخرين في النظام من خلال النشاط الذي تقوم به والمحتوى الذي تملكه.', + 'profile_email_desc' => 'سيتم استخدام هذا البريد الإلكتروني للإشعارات، وبناءً على مصادقة النظام النشط، سيتم استخدام الوصول إلى النظام.', + 'profile_email_no_permission' => 'للأسف، ليس لديك إذن لتغيير عنوان بريدك الإلكتروني. إذا كنت ترغب في تغييره، فعليك طلب ذلك من أحد المسؤولين.', + 'profile_avatar_desc' => 'اختر صورةً تُمثّلك أمام الآخرين في النظام. يُفضّل أن تكون الصورة مربعةً، وعرضها وارتفاعها حوالي ٢٥٦ بكسل.', + 'profile_admin_options' => 'خيارات المسؤول', + 'profile_admin_options_desc' => 'يمكنك العثور على خيارات إضافية على مستوى المسؤول، مثل تلك الخاصة بإدارة تعيينات الأدوار، لحساب المستخدم الخاص بك في منطقة "الإعدادات > المستخدمون" في التطبيق.', - 'delete_account' => 'Delete Account', - 'delete_my_account' => 'Delete My Account', - 'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\'ve created, such as created pages and uploaded images, will remain.', - 'delete_my_account_warning' => 'Are you sure you want to delete your account?', + 'delete_account' => 'حذف الحساب', + 'delete_my_account' => 'حذف حسابي', + 'delete_my_account_desc' => 'سيؤدي هذا إلى حذف حساب المستخدم الخاص بك بالكامل من النظام. لن تتمكن من استعادة هذا الحساب أو التراجع عن هذا الإجراء. سيبقى المحتوى الذي أنشأته، مثل الصفحات التي أنشأتها والصور التي رفعتها، كما هي.', + 'delete_my_account_warning' => 'هل أنت متأكد أنك تريد حذف حسابك؟', ]; diff --git a/lang/ar/settings.php b/lang/ar/settings.php index f771fc85188..b7c8ad411fb 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -48,12 +48,12 @@ 'app_disable_comments_desc' => 'تعطيل التعليقات على جميع الصفحات داخل التطبيق. التعليقات الموجودة من الأصل لن تكون ظاهرة.', // Color settings - 'color_scheme' => 'Application Color Scheme', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', - 'app_color' => 'Primary Color', - 'link_color' => 'Default Link Color', - 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'color_scheme' => 'مخطط ألوان التطبيق', + 'color_scheme_desc' => 'حدّد الألوان المستخدمة في واجهة مستخدم التطبيق. يمكن ضبط الألوان بشكل منفصل للوضعين الداكن والفاتح لتناسب المظهر بشكل أفضل ولضمان وضوح النص.', + 'ui_colors_desc' => 'عيّن اللون الأساسي للتطبيق ولون الرابط الافتراضي. يُستخدم اللون الأساسي بشكل رئيس في شعار الصفحة الرئيسة والأزرار وزخارف الواجهة. أما اللون الافتراضي للرابط، فيُستخدم للروابط والإجراءات النصية، سواءً داخل المحتوى المكتوب أو في واجهة التطبيق.', + 'app_color' => 'اللون الأساسي', + 'link_color' => 'لون الرابط الافتراضي', + 'content_colors_desc' => 'حدّد ألوان جميع عناصر هيكل تنظيم الصفحة. يُنصح باختيار ألوان بنفس سطوع الألوان الافتراضية لسهولة القراءة.', 'bookshelf_color' => 'لون الرف', 'book_color' => 'لون الكتاب', 'chapter_color' => 'لون الفصل', @@ -75,40 +75,40 @@ 'reg_confirm_restrict_domain_placeholder' => 'لم يتم اختيار أي قيود', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'طريقة الترتيب', + 'sorting_book_default' => 'ترتيب الكتاب الافتراضي', + 'sorting_book_default_desc' => 'حدد قاعدة الترتيب الافتراضية لتطبيقها على الكتب الجديدة. لن يؤثر هذا على الكتب الحالية، ويمكن تجاوزه لكل كتاب على حدة.', + 'sorting_rules' => 'قواعد الترتيب', + 'sorting_rules_desc' => 'هذه هي عمليات الترتيب المحددة مسبقًا الذي يمكن تطبيقها على المحتوى الموجود في النظام.', + 'sort_rule_assigned_to_x_books' => 'مُعيَّن إلى :count كتاب|مُعيَّن إلى :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' => 'الفصول الأخيرة', // Maintenance settings 'maint' => 'الصيانة', 'maint_image_cleanup' => 'تنظيف الصور', 'maint_image_cleanup_desc' => 'مسح الصفحة ومراجعة المحتوى للتحقق من أي الصور والرسوم المستخدمة حاليًا وأي الصور زائدة عن الحاجة. تأكد من إنشاء قاعدة بيانات كاملة و نسخة احتياطية للصور قبل تشغيل هذا.', - 'maint_delete_images_only_in_revisions' => 'Also delete images that only exist in old page revisions', + 'maint_delete_images_only_in_revisions' => 'قم أيضًا بحذف الصور الموجودة فقط في مراجعات الصفحة القديمة', 'maint_image_cleanup_run' => 'بدء التنظيف', 'maint_image_cleanup_warning' => 'يوجد عدد :count من الصور المحتمل عدم استخدامها. تأكيد حذف الصور؟', 'maint_image_cleanup_success' => 'تم إيجاد وحذف عدد :count من الصور المحتمل عدم استخدامها!', @@ -122,16 +122,16 @@ 'maint_send_test_email_mail_text' => 'تهانينا! كما تلقيت إشعار هذا البريد الإلكتروني، يبدو أن إعدادات البريد الإلكتروني الخاص بك قد تم تكوينها بشكل صحيح.', 'maint_recycle_bin_desc' => 'تُرسل الأرفف والكتب والفصول والصفحات المحذوفة إلى سلة المحذوفات حتى يمكن استعادتها أو حذفها نهائيًا. قد يتم إزالة العناصر الأقدم في سلة المحذوفات تلقائيًا بعد فترة اعتمادًا على تكوين النظام.', 'maint_recycle_bin_open' => 'افتح سلة المحذوفات', - 'maint_regen_references' => 'Regenerate References', - 'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.', - 'maint_regen_references_success' => 'Reference index has been regenerated!', - 'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.', + 'maint_regen_references' => 'إعادة إنشاء المراجع', + 'maint_regen_references_desc' => 'سيعيد هذا الإجراء بناء فِهْرِس المراجع بين العناصر داخل قاعدة البيانات. عادةً ما يتم ذلك تلقائيًا، ولكنه قد يكون مفيدًا لفهرسة المحتوى القديم أو المحتوى المُضاف بطرق غير رسمية.', + 'maint_regen_references_success' => 'لقد تم تجديد فِهْرِس المرجع!', + 'maint_timeout_command_note' => 'ملاحظة: قد يستغرق تنفيذ هذا الإجراء بعض الوقت، مما قد يؤدي إلى مشاكل في مهلة التنفيذ في بعض بيئات الويب. كبديل، يمكن تنفيذ هذا الإجراء باستخدام سطر الأوامر.', // Recycle Bin 'recycle_bin' => 'سلة المحذوفات', 'recycle_bin_desc' => 'هنا يمكنك استعادة العناصر التي تم حذفها أو اختيار إزالتها نهائيا من النظام. هذه القائمة غير مصفاة خلافاً لقوائم الأنشطة المماثلة في النظام حيث يتم تطبيق عوامل تصفية الأذونات.', 'recycle_bin_deleted_item' => 'عنصر محذوف', - 'recycle_bin_deleted_parent' => 'Parent', + 'recycle_bin_deleted_parent' => 'اﻷب', 'recycle_bin_deleted_by' => 'حُذف بواسطة', 'recycle_bin_deleted_at' => 'وقت الحذف', 'recycle_bin_permanently_delete' => 'حُذف نهائيًا', @@ -139,12 +139,12 @@ 'recycle_bin_contents_empty' => 'سلة المحذوفات فارغة حاليًا', 'recycle_bin_empty' => 'إفراغ سلة المحذوفات', 'recycle_bin_empty_confirm' => 'سيؤدي هذا إلى إتلاف جميع العناصر الموجودة في سلة المحذوفات بشكل دائم بما في ذلك المحتوى الموجود داخل كل عنصر. هل أنت متأكد من أنك تريد إفراغ سلة المحذوفات؟', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', + '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' => 'Restore Parent', + 'recycle_bin_restore_parent' => 'استعادة اﻷب', 'recycle_bin_destroy_notification' => 'المحذوف: قُم بعد إجمالي العناصر من سلة المحذوفات.', 'recycle_bin_restore_notification' => 'المرتجع: قُم بعد إجمالي العناصر من سلة المحذوفات.', @@ -158,7 +158,7 @@ 'audit_table_user' => 'المستخدم', 'audit_table_event' => 'الحدث', 'audit_table_related' => 'العنصر أو التفاصيل ذات الصلة', - 'audit_table_ip' => 'IP Address', + 'audit_table_ip' => 'عنوان عُرف اﻹنترنت -IP-', 'audit_table_date' => 'تاريخ النشاط', 'audit_date_from' => 'نطاق التاريخ من', 'audit_date_to' => 'نطاق التاريخ إلى', @@ -166,11 +166,11 @@ // Role Settings 'roles' => 'الأدوار', 'role_user_roles' => 'أدوار المستخدمين', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => ':count user assigned|:count users assigned', - 'roles_x_permissions_provided' => ':count permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => 'تُستخدم الأدوار لتجميع المستخدمين ومنح أذونات النظام لأعضائها. عندما يكون المستخدم عضوًا في أدوار متعددة، تتراكم الصلاحيات الممنوحة، ويرث المستخدم جميع القدرات.', + 'roles_x_users_assigned' => ':count مستخدم معين|:count مستخدمين معينين', + 'roles_x_permissions_provided' => ':count إذن |:count إذونات', + 'roles_assigned_users' => 'المستخدمون المعينون', + 'roles_permissions_provided' => 'الصلاحيات المقدمة', 'role_create' => 'إنشاء دور جديد', 'role_delete' => 'حذف الدور', 'role_delete_confirm' => 'سيتم حذف الدور المسمى \':roleName\'.', @@ -181,7 +181,7 @@ 'role_details' => 'تفاصيل الدور', 'role_name' => 'اسم الدور', 'role_desc' => 'وصف مختصر للدور', - 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', + 'role_mfa_enforced' => 'يتطلب مصادقة متعددة العوامل', 'role_external_auth_id' => 'ربط الحساب بمواقع التواصل', 'role_system' => 'أذونات النظام', 'role_manage_users' => 'إدارة المستخدمين', @@ -191,15 +191,15 @@ 'role_manage_page_templates' => 'إدارة قوالب الصفحة', 'role_access_api' => 'الوصول إلى واجهة برمجة تطبيقات النظام API', 'role_manage_settings' => 'إدارة إعدادات التطبيق', - 'role_export_content' => 'Export content', - 'role_import_content' => 'Import content', - 'role_editor_change' => 'Change page editor', - 'role_notifications' => 'Receive & manage notifications', + 'role_export_content' => 'تصدير المحتوى', + 'role_import_content' => 'استيراد المحتوى', + 'role_editor_change' => 'تغيير محرر الصفحة', + 'role_notifications' => 'تلقي الإشعارات وإدارتها', 'role_asset' => 'أذونات الأصول', 'roles_system_warning' => 'اعلم أن الوصول إلى أي من الأذونات الثلاثة المذكورة أعلاه يمكن أن يسمح للمستخدم بتغيير امتيازاته الخاصة أو امتيازات الآخرين في النظام. قم بتعيين الأدوار مع هذه الأذونات فقط للمستخدمين الموثوق بهم.', 'role_asset_desc' => 'تتحكم هذه الأذونات في الوصول الافتراضي إلى الأصول داخل النظام. ستتجاوز الأذونات الخاصة بالكتب والفصول والصفحات هذه الأذونات.', 'role_asset_admins' => 'يُمنح المسؤولين حق الوصول تلقائيًا إلى جميع المحتويات ولكن هذه الخيارات قد تعرض خيارات واجهة المستخدم أو تخفيها.', - 'role_asset_image_view_note' => 'This relates to visibility within the image manager. Actual access of uploaded image files will be dependant upon system image storage option.', + 'role_asset_image_view_note' => 'يتعلق هذا بالرؤية داخل مدير الصور. يعتمد الوصول الفعلي لملفات الصور المُحمّلة على خِيار تخزين الصور في النظام.', 'role_all' => 'الكل', 'role_own' => 'ما يخص', 'role_controlled_by_asset' => 'يتحكم فيها الأصول التي يتم رفعها إلى', @@ -209,7 +209,7 @@ // Users 'users' => 'المستخدمون', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', + 'users_index_desc' => 'إنشاء وإدارة حسابات المستخدمين الفردية داخل النظام. يتم استخدام حسابات المستخدم لتسجيل الدخول وإسناد المحتوى والنشاط. صلاحيات الوصول هي أساسا قائمة على الأدوار ولكن ملكية محتوى المستخدم، من بين عوامل أخرى، قد تؤثر أيضا على صلاحيات والوصول إليها.', 'user_profile' => 'ملف المستخدم', 'users_add_new' => 'إضافة مستخدم جديد', 'users_search' => 'بحث عن مستخدم', @@ -220,20 +220,20 @@ 'users_role' => 'أدوار المستخدمين', 'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.', 'users_password' => 'كلمة مرور المستخدم', - 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.', + 'users_password_desc' => 'عيّن كلمة مرور لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.', 'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة المرور الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.', 'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم', 'users_external_auth_id' => 'ربط الحساب بمواقع التواصل', - 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', - 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', + 'users_external_auth_id_desc' => 'عند استخدام نظام مصادقة خارجي (مثل SAML2 أو OIDC أو LDAP)، يكون هذا هو المعرف الذي يربط مستخدم بوكستاك -BookStack- بحساب نظام المصادقة. يمكنك تجاهل هذا الحقل عند استخدام المصادقة الافتراضية عبر البريد الإلكتروني.', + 'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة المرور لهذا المستخدم.', 'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.', 'users_delete' => 'حذف المستخدم', 'users_delete_named' => 'حذف المستخدم :userName', 'users_delete_warning' => 'سيتم حذف المستخدم \':userName\' بشكل تام من النظام.', 'users_delete_confirm' => 'تأكيد حذف المستخدم؟', - 'users_migrate_ownership' => 'Migrate Ownership', - 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.', - 'users_none_selected' => 'No user selected', + 'users_migrate_ownership' => 'نقل الملكية', + 'users_migrate_ownership_desc' => 'حدد مستخدم هنا إذا كنت تريد أن يصبح مستخدم آخر مالك جميع العناصر التي يمتلكها هذا المستخدم حاليا.', + 'users_none_selected' => 'لم يتم تحديد مستخدم', 'users_edit' => 'تعديل المستخدم', 'users_edit_profile' => 'تعديل الملف', 'users_avatar' => 'صورة المستخدم', @@ -241,24 +241,24 @@ 'users_preferred_language' => 'اللغة المفضلة', 'users_preferred_language_desc' => 'سيؤدي هذا الخيار إلى تغيير اللغة المستخدمة لواجهة المستخدم الخاصة بالتطبيق. لن يؤثر هذا على أي محتوى قد أنشائه المستخدم.', 'users_social_accounts' => 'الحسابات الاجتماعية', - 'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.', + 'users_social_accounts_desc' => 'عرض حالة الحسابات الاجتماعية المرتبطة لهذا المستخدم. ويمكن استخدام الحسابات الاجتماعية بالإضافة إلى نظام التوثيق الرئيس للوصول إلى النظام.', 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', 'users_social_connect' => 'ربط الحساب', 'users_social_disconnect' => 'فصل الحساب', - 'users_social_status_connected' => 'Connected', - 'users_social_status_disconnected' => 'Disconnected', + 'users_social_status_connected' => 'متصل', + 'users_social_status_disconnected' => 'غير متصل', 'users_social_connected' => 'تم ربط حساب :socialAccount بملفك بنجاح.', 'users_social_disconnected' => 'تم فصل حساب :socialAccount من ملفك بنجاح.', 'users_api_tokens' => 'رموز الـ API', - 'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.', + 'users_api_tokens_desc' => 'أنشئ وأدر رموز الوصول المستخدمة للمصادقة باستخدام واجهة برمجة تطبيقات بوكستاك رِست -BookStack REST API-. تتم إدارة صلاحيات واجهة برمجة التطبيقات بواسطة المستخدم الذي ينتمي إليه الرمز.', 'users_api_tokens_none' => 'لم يتم إنشاء رموز API لهذا المستخدم', 'users_api_tokens_create' => 'قم بإنشاء رمز مميز', 'users_api_tokens_expires' => 'انتهاء مدة الصلاحية', 'users_api_tokens_docs' => 'وثائق API', - 'users_mfa' => 'Multi-Factor Authentication', - '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' => 'المصادقة متعددة العوامل', + 'users_mfa_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.', + 'users_mfa_x_methods' => ':count طريقة مُهيأة | :count طرق مُهيأة', + 'users_mfa_configure' => 'إعداد الطرق', // API Tokens 'user_api_token_create' => 'قم بإنشاء رمز API', @@ -279,42 +279,42 @@ 'user_api_token_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف رمز API؟', // Webhooks - 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => ':count trigger event|:count trigger events', - 'webhooks_create' => 'Create New Webhook', - 'webhooks_none_created' => 'No webhooks have yet been created.', - 'webhooks_edit' => 'Edit Webhook', - 'webhooks_save' => 'Save Webhook', - 'webhooks_details' => 'Webhook Details', - 'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.', - 'webhooks_events' => 'Webhook Events', - 'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.', - 'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.', - 'webhooks_events_all' => 'All system events', - 'webhooks_name' => 'Webhook Name', - 'webhooks_timeout' => 'Webhook Request Timeout (Seconds)', - 'webhooks_endpoint' => 'Webhook Endpoint', - 'webhooks_active' => 'Webhook Active', - 'webhook_events_table_header' => 'Events', - 'webhooks_delete' => 'Delete Webhook', - 'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.', - 'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?', - 'webhooks_format_example' => 'Webhook Format Example', - 'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.', - 'webhooks_status' => 'Webhook Status', - 'webhooks_last_called' => 'Last Called:', - 'webhooks_last_errored' => 'Last Errored:', - 'webhooks_last_error_message' => 'Last Error Message:', + 'webhooks' => 'خطافات الويب -Webhooks-', + 'webhooks_index_desc' => 'خطافات الويب هي طريقة لإرسال البيانات إلى الروابط الخارجية عندما تحدث بعض الإجراءات والأحداث داخل النظام الذي يسمح بالتكامل القائم على الأحداث مع المنصات الخارجية مثل نظم المراسلة أو الإشعار.', + 'webhooks_x_trigger_events' => ':count حدث تشغيل |:count أحداث تشغيل', + 'webhooks_create' => 'إنشاء خطاف ويب جديد', + 'webhooks_none_created' => 'لم يتم إنشاء أي خطافات ويب حتى الآن.', + 'webhooks_edit' => 'تحرير خطاف ويب', + 'webhooks_save' => 'حفظ خطاف ويب', + 'webhooks_details' => 'تفاصيل خطاف الويب', + 'webhooks_details_desc' => 'قم بتوفير اسم سهل الاستخدام ونقطة نهاية POST كموقع لإرسال بيانات خطافات الويب إليه.', + 'webhooks_events' => 'أحداث خطفات الويب', + 'webhooks_events_desc' => 'حدد جميع الأحداث التي يجب أن تشغل هذا الرابط ليتم استدعاؤها.', + 'webhooks_events_warning' => 'ضع في اعتبارك أن هذه الأحداث سيتم تشغيلها لجميع الأحداث المحددة، حتى إذا تم تطبيق صلاحيات مخصصة. تحقق أن استخدام خطاف الويب هذا لن يكشف عن محتوى سري.', + 'webhooks_events_all' => 'جميع أحداث النظام', + 'webhooks_name' => 'اسم خطاف الويب', + 'webhooks_timeout' => 'مهلة طلب خطاف الويب (بالثواني)', + 'webhooks_endpoint' => 'نقطة نهاية خطاف الويب', + 'webhooks_active' => 'خطاف الويب فعال', + 'webhook_events_table_header' => 'الأحداث', + 'webhooks_delete' => 'حذف خطاف الويب', + 'webhooks_delete_warning' => 'سيؤدي هذا إلى حذف خطاف الويب بالكامل، الذي يحمل اسم \':webhookName\'، من النظام.', + 'webhooks_delete_confirm' => 'هل أنت متيقِّن أنك تريد حذف هذا الخطاف؟', + 'webhooks_format_example' => 'مثال على تنسيق خطاف الويب', + 'webhooks_format_example_desc' => 'يتم إرسال بيانات خطاف الويب كطلب بوست -POST- إلى نقطة النهاية المكونة كجيسون -JSON- باتباع التنسيق أدناه. خصائص "ذات صلة" و "روابط" اختيارية و ستعتمد على نوع الحدث الذي تم تشغيله.', + 'webhooks_status' => 'حالة خطاف الويب', + 'webhooks_last_called' => 'آخر اتصال:', + 'webhooks_last_errored' => 'أخر خطأ:', + 'webhooks_last_error_message' => 'رسالة الخطأ الأخيرة:', // Licensing - 'licenses' => 'Licenses', - 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', - 'licenses_bookstack' => 'BookStack License', - 'licenses_php' => 'PHP Library Licenses', - 'licenses_js' => 'JavaScript Library Licenses', - 'licenses_other' => 'Other Licenses', - 'license_details' => 'License Details', + 'licenses' => 'الرخص', + 'licenses_desc' => 'هذه الصفحة تفصل معلومات الرخص لبوكستاك -BookStack- بالإضافة إلى المشاريع والمكتبات المستخدمة في بوكستاك. ولا يمكن استخدام العديد من المشاريع المدرجة إلا في سياق إنمائي.', + 'licenses_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. diff --git a/lang/ar/validation.php b/lang/ar/validation.php index f35c920348c..32a376865e9 100644 --- a/lang/ar/validation.php +++ b/lang/ar/validation.php @@ -105,10 +105,10 @@ 'url' => 'صيغة :attribute غير صالحة.', 'uploaded' => 'تعذر تحميل الملف. قد لا يقبل الخادم ملفات بهذا الحجم.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - '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_file_mime' => ':attribute بحاجة إلى الإشارة إلى مِلَفّ من نوع :validTypes، وجدت :foundType.', + 'zip_model_expected' => 'عنصر البيانات المتوقع ولكن ":type" تم العثور عليه.', + 'zip_unique' => 'يجب أن يكون :attribute فريداً لنوع الكائن داخل المِلَفّ المضغوط.', // Custom validation lines 'custom' => [ diff --git a/lang/de/activities.php b/lang/de/activities.php index 8f15cd04a9f..cfb21a4c97e 100644 --- a/lang/de/activities.php +++ b/lang/de/activities.php @@ -128,7 +128,7 @@ 'comment_delete' => 'Kommentar gelöscht', // Sort Rules - 'sort_rule_create' => 'created sort rule', + 'sort_rule_create' => 'Sortierregel erstellt', 'sort_rule_create_notification' => 'Sort rule successfully created', 'sort_rule_update' => 'updated sort rule', 'sort_rule_update_notification' => 'Sort rule successfully updated', diff --git a/lang/de_informal/activities.php b/lang/de_informal/activities.php index 7b5996aab27..3ebd8dfd2c1 100644 --- a/lang/de_informal/activities.php +++ b/lang/de_informal/activities.php @@ -128,7 +128,7 @@ 'comment_delete' => 'Kommentar gelöscht', // Sort Rules - 'sort_rule_create' => 'created sort rule', + 'sort_rule_create' => 'Sortierregel erstellt', 'sort_rule_create_notification' => 'Sort rule successfully created', 'sort_rule_update' => 'updated sort rule', 'sort_rule_update_notification' => 'Sort rule successfully updated', diff --git a/lang/fr/editor.php b/lang/fr/editor.php index 1296b7a78b0..fa620c52071 100644 --- a/lang/fr/editor.php +++ b/lang/fr/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Annuler', 'save' => 'Sauvegarder', 'close' => 'Fermer', - 'apply' => 'Apply', + 'apply' => 'Appliquer', 'undo' => 'Annuler', 'redo' => 'Rétablir', 'left' => 'Gauche', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => 'Texte à afficher', 'title' => 'Titre', - 'browse_links' => 'Browse links', + 'browse_links' => 'Parcourir les liens', 'open_link' => 'Ouvrir le lien', 'open_link_in' => 'Ouvrir le lien dans...', 'open_link_current' => 'Fenêtre actuelle', diff --git a/lang/fr/entities.php b/lang/fr/entities.php index e28609a7a3d..9992f7d7865 100644 --- a/lang/fr/entities.php +++ b/lang/fr/entities.php @@ -11,7 +11,7 @@ 'recently_updated_pages' => 'Pages mises à jour récemment', 'recently_created_chapters' => 'Chapitres créés récemment', 'recently_created_books' => 'Livres créés récemment', - 'recently_created_shelves' => 'Étagères créés récemment', + 'recently_created_shelves' => 'Étagères créées récemment', 'recently_update' => 'Mis à jour récemment', 'recently_viewed' => 'Vus récemment', 'recent_activity' => 'Activité récente', @@ -166,9 +166,9 @@ 'books_search_this' => 'Rechercher dans ce livre', 'books_navigation' => 'Navigation dans le livre', 'books_sort' => 'Trier les contenus du livre', - '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' => '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_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 301484afe21..fa125680b47 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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', + 'sorting' => 'Tri', + 'sorting_book_default' => 'Tri des livres par défaut', + 'sorting_book_default_desc' => 'Sélectionnez le tri par défaut à mettre en place sur les nouveaux livres. Cela n’affectera pas les livres existants, et peut être redéfini dans les livres.', + 'sorting_rules' => 'Règles de tri', + 'sorting_rules_desc' => 'Ce sont les opérations de tri qui peuvent être appliquées au contenu du système.', + 'sort_rule_assigned_to_x_books' => 'Assignée à :count livre|Assignée à :count livres', + 'sort_rule_create' => 'Créer une règle de tri', + 'sort_rule_edit' => 'Modifier une règle de tri', + 'sort_rule_delete' => 'Supprimer une règle de tri', + 'sort_rule_delete_desc' => 'Supprimer cette règle du système. Les livres l’utilisant précédemment reviendront au tri manuel.', + 'sort_rule_delete_warn_books' => 'Cette règle est actuellement utilisée par :count livre(s). Êtes-vous sûr·e de vouloir la supprimer ?', + 'sort_rule_delete_warn_default' => 'Cette règle est actuellement utilisée par défaut pour les livres. Êtes-vous sûr·e de vouloir la supprimer ?', + 'sort_rule_details' => 'Détails de la Règle de Tri', + 'sort_rule_details_desc' => 'Assignez un nom à cette règle, qui apparaîtra dans les listes servant à sélectionner une règle de tri.', + 'sort_rule_operations' => 'Opérations de tri', + 'sort_rule_operations_desc' => 'Configurez les actions de tri à appliquer en les déplaçant depuis la liste d’opérations. Lors de l’usage de cette règle, les opérations seront appliquées dans l’ordre, de haut en bas. Les changements effectués ici seront appliqués à tous les livres utilisant cette règle lors de la sauvegarde.', + 'sort_rule_available_operations' => 'Opérations disponibles', + 'sort_rule_available_operations_empty' => 'Aucune opération restante', + 'sort_rule_configured_operations' => 'Opérations sélectionnées', + 'sort_rule_configured_operations_empty' => 'Déplacez/ajoutez des opérations depuis la liste "Opérations disponibles"', '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', + 'sort_rule_op_name' => 'Nom - Alphabétique', + 'sort_rule_op_name_numeric' => 'Nom - Numérique', + 'sort_rule_op_created_date' => 'Date de création', + 'sort_rule_op_updated_date' => 'Date de mise à jour', + 'sort_rule_op_chapters_first' => 'Chapitres en premier', + 'sort_rule_op_chapters_last' => 'Chapitres en dernier', // Maintenance settings 'maint' => 'Maintenance', diff --git a/lang/he/activities.php b/lang/he/activities.php index 81fc84dc8fc..8dd244b7247 100644 --- a/lang/he/activities.php +++ b/lang/he/activities.php @@ -85,12 +85,12 @@ 'webhook_delete_notification' => 'Webook נמחק בהצלחה', // Imports - 'import_create' => 'created import', - 'import_create_notification' => 'Import successfully uploaded', - 'import_run' => 'updated import', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_create' => 'יבוא נוצר', + 'import_create_notification' => 'יבוא עודכן בהצלחה', + 'import_run' => 'יבוא עודכן', + 'import_run_notification' => 'תוכן יובא בהצלחה', + 'import_delete' => 'יבוא נמחק', + 'import_delete_notification' => 'יבוא נמחק בהצלחה', // Users 'user_create' => 'משתמש חדש נוצר', diff --git a/lang/he/auth.php b/lang/he/auth.php index 017602b4f2a..2948d5b2078 100644 --- a/lang/he/auth.php +++ b/lang/he/auth.php @@ -107,11 +107,11 @@ 'mfa_verify_access_desc' => 'חשבון המשתמש שלך דורש ממך לאת את הזהות שלך בשכבת הגנה נוספת על מנת לאפשר לך גישה. יש לאשר גישה דרך אחד האמצעים הקיימים על מנת להמשיך.', 'mfa_verify_no_methods' => 'אין אפשרויות אימות דו-שלבי מוגדרות', 'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.', - 'mfa_verify_use_totp' => 'Verify using a mobile app', - 'mfa_verify_use_backup_codes' => 'Verify using a backup code', - 'mfa_verify_backup_code' => 'Backup Code', - 'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:', - 'mfa_verify_backup_code_enter_here' => 'Enter backup code here', - 'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:', + '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' => 'Multi-factor method configured, Please now login again using the configured method.', ]; diff --git a/lang/he/entities.php b/lang/he/entities.php index 22a4ad0c29f..60b6011cd1a 100644 --- a/lang/he/entities.php +++ b/lang/he/entities.php @@ -25,11 +25,11 @@ 'meta_owned_name' => 'Owned by :user', 'meta_reference_count' => 'Referenced by :count item|Referenced by :count items', 'entity_select' => 'בחר יישות', - 'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item', + 'entity_select_lack_permission' => 'אין לך אישורים דרושים לבחירת פריט זה', 'images' => 'תמונות', 'my_recent_drafts' => 'הטיוטות האחרונות שלי', 'my_recently_viewed' => 'הנצפים לאחרונה שלי', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', + 'my_most_viewed_favourites' => 'האהובים הנצפים ביותר שלי', 'my_favourites' => 'המועדפים שלי', 'no_pages_viewed' => 'לא צפית בדפים כלשהם', 'no_pages_recently_created' => 'לא נוצרו דפים לאחרונה', @@ -40,28 +40,28 @@ 'export_text' => 'טקסט רגיל', 'export_md' => 'קובץ Markdown', 'export_zip' => 'Portable ZIP', - 'default_template' => 'Default Page Template', + 'default_template' => 'תבנית דף ברירת מחדל', 'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.', - 'default_template_select' => 'Select a template page', - 'import' => 'Import', - 'import_validate' => 'Validate Import', + 'default_template_select' => 'בחר דף תבנית', + 'import' => 'יבוא', + 'import_validate' => 'תקף יבוא', '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_select' => 'בחר קובץ ZIP להעלאה', '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' => 'המשך יבוא', '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_details' => 'פרטי יבוא', + 'import_run' => 'להריץ יבוא', 'import_size' => ':size Import ZIP Size', 'import_uploaded_at' => 'Uploaded :relativeTime', - 'import_uploaded_by' => 'Uploaded by', - 'import_location' => 'Import Location', + 'import_uploaded_by' => 'הועלה על ידי', + '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' => 'תקלות יבוא', 'import_errors_desc' => 'The follow errors occurred during the import attempt:', // Permissions and restrictions diff --git a/lang/pt_BR/activities.php b/lang/pt_BR/activities.php index 7b766d41afc..2a2d173c790 100644 --- a/lang/pt_BR/activities.php +++ b/lang/pt_BR/activities.php @@ -128,12 +128,12 @@ 'comment_delete' => 'Comentário deletado', // Sort Rules - 'sort_rule_create' => 'regra de classificação criada', - 'sort_rule_create_notification' => 'Regra de classificação criada com sucesso', - 'sort_rule_update' => 'regra de classificação atualizada', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'regra de classificação excluída', - 'sort_rule_delete_notification' => 'Regra de classificação excluída com sucesso', + 'sort_rule_create' => 'criou regra de ordenação', + 'sort_rule_create_notification' => 'Regra de ordenação criada com sucesso', + 'sort_rule_update' => 'atualizou regra de ordenação', + 'sort_rule_update_notification' => 'Regra de ordenação atualizada com sucesso', + 'sort_rule_delete' => 'excluiu regra de ordenação', + 'sort_rule_delete_notification' => 'Regra de ordenação excluída com sucesso', // Other 'permissions_update' => 'atualizou permissões', diff --git a/lang/pt_BR/editor.php b/lang/pt_BR/editor.php index 0ff86bf0f9b..d56584e6cab 100644 --- a/lang/pt_BR/editor.php +++ b/lang/pt_BR/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Cancelar', 'save' => 'Salvar', 'close' => 'Fechar', - 'apply' => 'Apply', + 'apply' => 'Aplicar', 'undo' => 'Desfazer', 'redo' => 'Refazer', 'left' => 'Esquerda', @@ -60,7 +60,7 @@ 'list_task' => 'Lista de tarefas', 'indent_increase' => 'Aumentar recuo', 'indent_decrease' => 'Diminuir recuo', - 'table' => 'Mesa', + 'table' => 'Tabela', 'insert_image' => 'Inserir Imagem', 'insert_image_title' => 'Inserir/Editar imagem', 'insert_link' => 'Inserir/editar link', @@ -91,7 +91,7 @@ 'insert_column_before' => 'Inserir coluna antes', 'insert_column_after' => 'Inserir coluna depois', 'delete_column' => 'Excluir coluna', - 'table_cell' => 'Celular', + 'table_cell' => 'Célula', 'table_row' => 'Linha', 'table_column' => 'Coluna', 'cell_properties' => 'Propriedades da célula', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => 'Texto de exibição', 'title' => 'Título', - 'browse_links' => 'Browse links', + 'browse_links' => 'Procurar links', 'open_link' => 'Link aberto', 'open_link_in' => 'Abrir link em...', 'open_link_current' => 'Janelas atuais', diff --git a/lang/pt_BR/entities.php b/lang/pt_BR/entities.php index 9ff98a27ade..37350e67705 100644 --- a/lang/pt_BR/entities.php +++ b/lang/pt_BR/entities.php @@ -166,9 +166,9 @@ 'books_search_this' => 'Pesquisar neste livro', 'books_navigation' => 'Navegação do Livro', 'books_sort' => 'Ordenar Conteúdos do Livro', - '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' => '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_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 f69edf4e2dc..39007cbccec 100644 --- a/lang/pt_BR/settings.php +++ b/lang/pt_BR/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida', // Sorting Settings - 'sorting' => 'Classificação', - 'sorting_book_default' => 'Classificação padrão de livros', - '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' => 'Regras de classificação', - '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', + 'sorting' => 'Ordenação', + 'sorting_book_default' => 'Ordenação padrão de livros', + 'sorting_book_default_desc' => 'Selecione a regra de ordenação padrão a ser aplicada a novos livros. Isso não afetará os livros existentes e pode ser substituído para cada livro individualmente.', + 'sorting_rules' => 'Regras de ordenação', + 'sorting_rules_desc' => 'Estas são operações de ordenação pré-definidas que podem ser aplicadas a conteúdos no sistema.', + 'sort_rule_assigned_to_x_books' => 'Atribuído a :count Livros|Atribuído a :count Livros', + 'sort_rule_create' => 'Criar Regra de Ordenação', + 'sort_rule_edit' => 'Editar Regra de Ordenação', + 'sort_rule_delete' => 'Excluir Regra de Ordenação', + 'sort_rule_delete_desc' => 'Remover esta regra de ordenação do sistema. Os livros usando este tipo serão revertidos para a ordenação manual.', + 'sort_rule_delete_warn_books' => 'Esta regra de ordenação está sendo usada atualmente em :count livro(s). Tem certeza de que deseja excluí-la?', + 'sort_rule_delete_warn_default' => 'Esta regra de ordenação é atualmente usada como padrão para livros. Tem certeza de que deseja excluí-la?', + 'sort_rule_details' => 'Detalhes das Regras de Ordenação', + 'sort_rule_details_desc' => 'Defina um nome para esta regra de ordenação, que aparecerá nas listas quando os usuários selecionarem uma ordenação.', + 'sort_rule_operations' => 'Operações de Ordenação', + 'sort_rule_operations_desc' => 'Configure as ações de ordenação a serem executadas movendo-as da lista de operações disponíveis. Ao usar, as operações serão aplicadas em ordem, de cima para baixo. Quaisquer alterações feitas aqui serão aplicadas a todos os livros atribuídos após salvar.', + 'sort_rule_available_operations' => 'Operações Disponíveis', + 'sort_rule_available_operations_empty' => 'Não há operações restantes', + 'sort_rule_configured_operations' => 'Operações configuradas', + 'sort_rule_configured_operations_empty' => 'Arrastar/adicionar operações da lista "Operações Disponíveis"', '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', + 'sort_rule_op_name' => 'Nome - Alfabético', + 'sort_rule_op_name_numeric' => 'Nome - Numérico', + 'sort_rule_op_created_date' => 'Data de Criação', + 'sort_rule_op_updated_date' => 'Data de Atualização', + 'sort_rule_op_chapters_first' => 'Capítulos Primeiro', + 'sort_rule_op_chapters_last' => 'Capítulos por Último', // Maintenance settings 'maint' => 'Manutenção', diff --git a/lang/ru/activities.php b/lang/ru/activities.php index 74564e2abfd..3a13ecf467f 100644 --- a/lang/ru/activities.php +++ b/lang/ru/activities.php @@ -128,12 +128,12 @@ 'comment_delete' => 'удалил комментарий', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/ru/common.php b/lang/ru/common.php index d3860bba461..de2da532d9a 100644 --- a/lang/ru/common.php +++ b/lang/ru/common.php @@ -109,5 +109,5 @@ 'terms_of_service' => 'Условия использования', // OpenSearch - 'opensearch_description' => 'Search :appName', + 'opensearch_description' => 'Поиск :appName', ]; diff --git a/lang/ru/editor.php b/lang/ru/editor.php index c2c38984c03..264ab7ec33e 100644 --- a/lang/ru/editor.php +++ b/lang/ru/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Отмена', 'save' => 'Сохранить', 'close' => 'Закрыть', - 'apply' => 'Apply', + 'apply' => 'Применить', 'undo' => 'Отменить', 'redo' => 'Повторить', 'left' => 'Слева', diff --git a/lang/ru/entities.php b/lang/ru/entities.php index 5ce4dea6043..2246be0435a 100644 --- a/lang/ru/entities.php +++ b/lang/ru/entities.php @@ -43,9 +43,9 @@ 'default_template' => 'Шаблон страницы по умолчанию', 'default_template_explain' => 'Назначить шаблон страницы, который будет использоваться в качестве содержимого по умолчанию для всех страниц, созданных в этом элементе. Имейте в виду, что это будет работать, только если создатель страницы имеет доступ к выбранной странице шаблона.', 'default_template_select' => 'Выберите страницу шаблона', - 'import' => '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_desc' => 'Импортировать книги, главы и страницы с помощью ZIP-файла, экспортированного из этого или другого источника. Выберите ZIP-файл, чтобы продолжить. После загрузки и проверки файла вы сможете настроить и подтвердить импорт в следующем окне.', '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', @@ -248,7 +248,7 @@ '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_desc' => '(In Alpha Testing)', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(В Альфа-тестировании)', 'pages_edit_set_changelog' => 'Задать список изменений', 'pages_edit_enter_changelog_desc' => 'Введите краткое описание внесенных изменений', 'pages_edit_enter_changelog' => 'Введите список изменений', diff --git a/lang/ru/errors.php b/lang/ru/errors.php index ceae383c676..e87c9093629 100644 --- a/lang/ru/errors.php +++ b/lang/ru/errors.php @@ -111,11 +111,11 @@ 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', '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_perms_books' => 'У вас недостаточно прав для создания книг.', + 'import_perms_chapters' => 'У вас недостаточно прав для создания глав.', + 'import_perms_pages' => 'У вас недостаточно прав для создания страниц.', + 'import_perms_images' => 'У вас недостаточно прав для создания изображений.', + 'import_perms_attachments' => 'У вас недостаточно прав для создания вложений.', // API errors 'api_no_authorization_found' => 'Отсутствует токен авторизации в запросе', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index ee78104ef7a..6cd29286982 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Без ограничений', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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', + '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' => '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' => 'Удалить правило сортировки', + '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_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_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_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' => 'Доступные операции', '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_configured_operations_empty' => 'Перетащите/добавьте операции из списка "Доступные операции"', + 'sort_rule_op_asc' => '(Возрастание)', + 'sort_rule_op_desc' => '(Убывание)', 'sort_rule_op_name' => 'Name - Alphabetical', - 'sort_rule_op_name_numeric' => 'Name - Numeric', + 'sort_rule_op_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', + 'sort_rule_op_chapters_last' => 'Главы в конце', // Maintenance settings 'maint' => 'Обслуживание', diff --git a/lang/sl/activities.php b/lang/sl/activities.php index 2e8943e4025..117c73ca186 100644 --- a/lang/sl/activities.php +++ b/lang/sl/activities.php @@ -59,7 +59,7 @@ 'favourite_remove_notification' => '":name" odstranjen iz priljubljenih', // Watching - 'watch_update_level_notification' => 'Nastavitve opazovanja uspešno posodobljene', + 'watch_update_level_notification' => 'Nastavitve spremljanja uspešno posodobljene', // Auth 'auth_login' => 'prijavljen', diff --git a/lang/sl/auth.php b/lang/sl/auth.php index 47b70df7f22..e06d823e7cd 100644 --- a/lang/sl/auth.php +++ b/lang/sl/auth.php @@ -27,10 +27,10 @@ 'ldap_email_hint' => 'Prosimo vpišite e-poštni naslov za ta račun.', 'create_account' => 'Ustvari račun', 'already_have_account' => 'Že imate račun?', - 'dont_have_account' => 'Nimate računa?', + 'dont_have_account' => 'Nimaš računa?', 'social_login' => 'Prijava z računi družbenih omrežij', 'social_registration' => 'Registracija z družbenim omrežjem', - 'social_registration_text' => 'Registrirajte in prijavite se za uporabo drugih možnosti.', + 'social_registration_text' => 'Registriraj in prijavi se z uporabo drugih možnosti.', 'register_thanks' => 'Hvala za registracijo!', 'register_confirm' => 'Prosimo preverite vaš e-poštni predal in kliknite na potrditveni gumb za dostop :appName.', @@ -40,7 +40,7 @@ // Login auto-initiation 'auto_init_starting' => 'Attempting Login', - 'auto_init_starting_desc' => 'We\'re contacting your authentication system to start the login process. If there\'s no progress after 5 seconds you can try clicking the link below.', + 'auto_init_starting_desc' => 'Kontaktiram tvoj sistem za preverjanje pristnosti za pričetek procesa prijave. V primeru, da se nič ne zgodi v naslednjih 5 sekundah, klikni spodnjo povezavo.', 'auto_init_start_link' => 'Proceed with authentication', // Password Reset diff --git a/lang/sl/common.php b/lang/sl/common.php index 4bad22048c6..29e2a0252cd 100644 --- a/lang/sl/common.php +++ b/lang/sl/common.php @@ -44,12 +44,12 @@ 'configure' => 'Configure', 'manage' => 'Manage', 'fullscreen' => 'Celozaslonski način', - 'favourite' => 'Favourite', - 'unfavourite' => 'Unfavourite', - 'next' => 'Next', - 'previous' => 'Previous', + 'favourite' => 'Priljubljeno', + 'unfavourite' => 'Ni priljubljeno', + 'next' => 'Naprej', + 'previous' => 'Nazaj', 'filter_active' => 'Active Filter:', - 'filter_clear' => 'Clear Filter', + 'filter_clear' => 'Počisti filter', 'download' => 'Download', 'open_in_tab' => 'Open in Tab', 'open' => 'Open', @@ -91,7 +91,7 @@ 'edit_profile' => 'Uredi profil', 'dark_mode' => 'Način temnega zaslona', 'light_mode' => 'Način svetlega zaslona', - 'global_search' => 'Global Search', + 'global_search' => 'Globalno iskanje', // Layout tabs 'tab_info' => 'Informacije', diff --git a/lang/sl/entities.php b/lang/sl/entities.php index f3c56223ba4..9a7b73488db 100644 --- a/lang/sl/entities.php +++ b/lang/sl/entities.php @@ -29,8 +29,8 @@ 'images' => 'Slike', 'my_recent_drafts' => 'Moji nedavni osnutki', 'my_recently_viewed' => 'Nedavno prikazano', - 'my_most_viewed_favourites' => 'My Most Viewed Favourites', - 'my_favourites' => 'My Favourites', + 'my_most_viewed_favourites' => 'Največkrat gledane priljubljene strani', + 'my_favourites' => 'Priljubljene', 'no_pages_viewed' => 'Niste si ogledali še nobene strani', 'no_pages_recently_created' => 'Nedavno ni bila ustvarjena nobena stran', 'no_pages_recently_updated' => 'Nedavno ni bila posodobljena nobena stran', @@ -66,7 +66,7 @@ // Permissions and restrictions 'permissions' => 'Dovoljenja', - 'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.', + 'permissions_desc' => 'Nastavi dovoljenja bolj podrobno, kot to določajo uporabniške vloge.', 'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.', 'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.', 'permissions_save' => 'Shrani dovoljenja', @@ -78,7 +78,7 @@ // Search 'search_results' => 'Rezultati iskanja', - 'search_total_results_found' => ':count najdenih rezultatov|:count skupaj najdenih rezultatov', + 'search_total_results_found' => ':count najdenih rezultatov|:count skupno najdenih rezultatov', 'search_clear' => 'Počisti iskanje', 'search_no_pages' => 'Nobena stran se ne ujema z vašim iskanjem', 'search_for_term' => 'Išči :term', @@ -101,7 +101,7 @@ 'search_created_before' => 'Ustvarjeno pred', 'search_created_after' => 'Ustvarjeno po', 'search_set_date' => 'Nastavi datum', - 'search_update' => 'Posodobi iskanje', + 'search_update' => 'Išči ponovno', // Shelves 'shelf' => 'Polica', @@ -141,7 +141,7 @@ 'books' => 'Knjige', 'x_books' => ':count Knjiga|:count Knjig', 'books_empty' => 'Ustvarjena ni bila nobena knjiga', - 'books_popular' => 'Priljubjene knjige', + 'books_popular' => 'Priljubljene knjige', 'books_recent' => 'Zadnje knjige', 'books_new' => 'Nove knjige', 'books_new_action' => 'Nova knjiga', @@ -220,7 +220,7 @@ 'page' => 'Stran', 'pages' => 'Strani', 'x_pages' => ':count Stran|:count Strani', - 'pages_popular' => 'Priljubjene strani', + 'pages_popular' => 'Priljubljene knjige', 'pages_new' => 'Nova stran', 'pages_attachments' => 'Priponke', 'pages_navigation' => 'Navigacija po strani', @@ -442,7 +442,7 @@ 'watch_desc_default' => 'Revert watching to just your default notification preferences.', 'watch_title_ignore' => 'Ignore', 'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.', - 'watch_title_new' => 'New Pages', + 'watch_title_new' => 'Nove strani', 'watch_desc_new' => 'Notify when any new page is created within this item.', 'watch_title_updates' => 'All Page Updates', 'watch_desc_updates' => 'Notify upon all new pages and page changes.', diff --git a/lang/sl/errors.php b/lang/sl/errors.php index 9a8759684c7..da80fd3910e 100644 --- a/lang/sl/errors.php +++ b/lang/sl/errors.php @@ -96,7 +96,7 @@ // Error pages '404_page_not_found' => 'Strani ni mogoče najti', 'sorry_page_not_found' => 'Oprostite, strani ki jo iščete, ni mogoče najti.', - 'sorry_page_not_found_permission_warning' => 'Če pričakujete, da ta stran obstaja, mogoče nimate pravic ogleda zanjo.', + 'sorry_page_not_found_permission_warning' => 'Stran ne obstaja ali pa za ogled nimaš ustreznih pravic.', 'image_not_found' => 'Image Not Found', 'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.', 'image_not_found_details' => 'If you expected this image to exist it might have been deleted.', diff --git a/lang/sl/pagination.php b/lang/sl/pagination.php index e04a98d89b5..80a1e1b4743 100644 --- a/lang/sl/pagination.php +++ b/lang/sl/pagination.php @@ -6,7 +6,7 @@ */ return [ - 'previous' => '« Predhodno', - 'next' => 'Naslednje »', + 'previous' => '« Nazaj', + 'next' => 'Naprej »', ]; diff --git a/lang/sl/settings.php b/lang/sl/settings.php index 5ca213a395d..c49b6b79b55 100644 --- a/lang/sl/settings.php +++ b/lang/sl/settings.php @@ -64,9 +64,9 @@ 'reg_settings' => 'registracija', 'reg_enable' => 'onemogočena registracija', 'reg_enable_toggle' => 'omogočena registracija', - 'reg_enable_desc' => 'Ko je registracija omogočena, se bo uporabnik lahko prijavil sam kot uporabnik aplikacije. Po registraciji je uporabniku dodeljena ena prevzeta vloga.', + 'reg_enable_desc' => 'Ko je registracija omogočena, se bo uporabnik lahko prijavil sam kot uporabnik aplikacije. Po registraciji je uporabniku dodeljena privzeta vloga.', 'reg_default_role' => 'prevzeta uporabniška vloga po registraciji', - 'reg_enable_external_warning' => 'Ta možnosti je ignorirana ko zunanja LDAP ali SAML avtentikacija je akitivna. Uporabniški računi za ne obstoječe uporabnike bodo avtomatsko izdelani, če avtentikacija zunanjih uporabljenih sistemov je uspešna.', + 'reg_enable_external_warning' => 'Ta možnosti je prezrta, ko je aktivno zunanja LDAP ali SAML preverjanje pristnosti. Uporabniški računi za neobstoječe uporabnike bodo avtomatično ustvarjeni po uspešnem zunanjem preverjanju pristnosti.', 'reg_email_confirmation' => 'potrditev e-pošte', 'reg_email_confirmation_toggle' => 'potrebna potrditev e-pošte', 'reg_confirm_email_desc' => 'Če uporabite omejitev domene, bo potrebna potrditev e-pošte in ta možnost bo prezrta.', @@ -167,7 +167,7 @@ 'roles' => 'Vloge', 'role_user_roles' => 'Vloge uporabnika', 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => ':count user assigned|:count users assigned', + 'roles_x_users_assigned' => ':count uporabnik v tej vlogi|:count uporabnikov v tej vlogi', 'roles_x_permissions_provided' => ':count permission|:count permissions', 'roles_assigned_users' => 'Assigned Users', 'roles_permissions_provided' => 'Provided Permissions', @@ -223,7 +223,7 @@ 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.', 'users_send_invite_text' => 'Uporabniku lahko pošljete e-poštno sporočilo s povabilom, ki mu omogoča, da nastavi svoje geslo, ali pa ga nastavite kar sami.', 'users_send_invite_option' => 'Pošlji uporabniku e-poštno povabilo', - 'users_external_auth_id' => 'Zunanje dokazilo ID', + 'users_external_auth_id' => 'Zunanji ID za preverjanje pristnosti (Authentication ID)', 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', 'users_system_public' => 'Ta uporabnik predstavlja vse gostujoče uporabnike, ki obiščejo vašo wiki stran. Za prijavo je ni mogoče uporabiti, ampak je dodeljena samodejno.', diff --git a/lang/zh_CN/activities.php b/lang/zh_CN/activities.php index 2ec271e0542..08f831f6102 100644 --- a/lang/zh_CN/activities.php +++ b/lang/zh_CN/activities.php @@ -128,12 +128,12 @@ 'comment_delete' => '评论已删除', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/zh_CN/editor.php b/lang/zh_CN/editor.php index 33e72f23f75..8eb335ea2f6 100644 --- a/lang/zh_CN/editor.php +++ b/lang/zh_CN/editor.php @@ -13,7 +13,7 @@ 'cancel' => '取消', 'save' => '保存', 'close' => '关闭', - 'apply' => 'Apply', + 'apply' => '应用', 'undo' => '撤销', 'redo' => '重做', 'left' => '左对齐', @@ -148,7 +148,7 @@ 'url' => '网址', 'text_to_display' => '要显示的文本', 'title' => '标题', - 'browse_links' => 'Browse links', + 'browse_links' => '浏览链接', 'open_link' => '打开链接', 'open_link_in' => '打开链接于……', 'open_link_current' => '覆盖当前窗口', diff --git a/lang/zh_CN/entities.php b/lang/zh_CN/entities.php index 433a8e21f4f..f7d22973ada 100644 --- a/lang/zh_CN/entities.php +++ b/lang/zh_CN/entities.php @@ -167,7 +167,7 @@ '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' => '自动排序选项', 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', 'books_sort_named' => '排序图书「:bookName」', 'books_sort_name' => '按名称排序', diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index ce4d8e68244..15297c804e6 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -75,7 +75,7 @@ 'reg_confirm_restrict_domain_placeholder' => '尚未设置限制', // Sorting Settings - 'sorting' => 'Sorting', + 'sorting' => '排序', 'sorting_book_default' => 'Default Book Sort', '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', diff --git a/lang/zh_TW/activities.php b/lang/zh_TW/activities.php index 07d53ea65d0..d33e4d4b742 100644 --- a/lang/zh_TW/activities.php +++ b/lang/zh_TW/activities.php @@ -128,12 +128,12 @@ 'comment_delete' => '已刪除之評論', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/zh_TW/editor.php b/lang/zh_TW/editor.php index 737d51851b7..9319e64a437 100644 --- a/lang/zh_TW/editor.php +++ b/lang/zh_TW/editor.php @@ -13,7 +13,7 @@ 'cancel' => '取消', 'save' => '保存', 'close' => '關閉', - 'apply' => 'Apply', + 'apply' => '套用', 'undo' => '復原', 'redo' => '重做', 'left' => '左側', @@ -148,7 +148,7 @@ 'url' => '網址', 'text_to_display' => '要顯示的文字', 'title' => '標題', - 'browse_links' => 'Browse links', + 'browse_links' => '瀏覽連結', 'open_link' => '開啟連結', 'open_link_in' => '打開連結於……', 'open_link_current' => '當前視窗', @@ -165,8 +165,8 @@ '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 分支出來的,而 :lexicalLink 是以 MIT 授權條款散佈。', + 'editor_lexical_license_link' => '完整的授權條款詳細資訊可在這裡找到。', 'editor_tiny_license' => '此編輯器是用 :tinyLink 構建的,基於 MIT 許可證。', 'editor_tiny_license_link' => 'TinyMCE 的版權和許可證詳細信息可以在這裡找到。', 'save_continue' => '保存頁面並繼續', diff --git a/lang/zh_TW/entities.php b/lang/zh_TW/entities.php index 4c72bafeb70..d3a684f8fd2 100644 --- a/lang/zh_TW/entities.php +++ b/lang/zh_TW/entities.php @@ -166,9 +166,9 @@ '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_named' => '排序書本 :bookName', 'books_sort_name' => '按名稱排序', 'books_sort_created' => '按建立時間排序', diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php index a20956bc06a..ec2682cf173 100644 --- a/lang/zh_TW/settings.php +++ b/lang/zh_TW/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => '尚未設定限制', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => '排序', + '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' => '最後一章', // Maintenance settings 'maint' => '維護', From fa566f156ad8998d67a63be2856dafc7ce277d88 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 2 Apr 2025 17:30:43 +0100 Subject: [PATCH 018/474] Updated translator & dependency attribution before release v25.02.2 --- .github/translators.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/translators.txt b/.github/translators.txt index af5f2a703c1..e38cb8a5115 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -478,3 +478,9 @@ Alexandar Cavdarovski (ace.200112) :: Swedish Onur Oskay (o.oskay) :: Turkish Sébastien Merveille (SebastienMerv) :: French Maxim Kouznetsov (masya.work) :: Hebrew +neodvisnost :: Slovenian +Soubi Agatsuma (bisouya) :: Hebrew +Ilya Shaulov (ishaulov) :: Russian +Konstantin Bobkov (b.konstantv) :: Russian +Ruben Sutter (rubensutter) :: German +jellium :: French From 8d159f77e440a57c8fdba5afb234d378d11856f6 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 18 Apr 2025 15:01:57 +0100 Subject: [PATCH 019/474] Comments: Started logic for content references Adds button for comments to pointer. Adds logic to generate a content reference point. --- resources/js/components/pointer.js | 56 +++++++++++++++++-- resources/js/services/dom.ts | 21 +++++++ resources/js/services/util.ts | 21 +++++++ resources/sass/_pages.scss | 11 ++-- resources/views/pages/parts/pointer.blade.php | 23 +++++--- 5 files changed, 113 insertions(+), 19 deletions(-) diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.js index 292b923e551..997df329a84 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.js @@ -1,6 +1,9 @@ import * as DOM from '../services/dom.ts'; import {Component} from './component'; import {copyTextToClipboard} from '../services/clipboard.ts'; +import {el} from "../wysiwyg/utils/dom"; +import {cyrb53} from "../services/util"; +import {normalizeNodeTextOffsetToParent} from "../services/dom.ts"; export class Pointer extends Component { @@ -12,13 +15,16 @@ export class Pointer extends Component { this.includeInput = this.$refs.includeInput; this.includeButton = this.$refs.includeButton; this.sectionModeButton = this.$refs.sectionModeButton; + this.commentButton = this.$refs.commentButton; this.modeToggles = this.$manyRefs.modeToggle; this.modeSections = this.$manyRefs.modeSection; this.pageId = this.$opts.pageId; // Instance variables this.showing = false; - this.isSelection = false; + this.isMakingSelection = false; + this.targetElement = null; + this.targetSelectionRange = null; this.setupListeners(); } @@ -41,7 +47,7 @@ export class Pointer extends Component { // Hide pointer when clicking away DOM.onEvents(document.body, ['click', 'focus'], () => { - if (!this.showing || this.isSelection) return; + if (!this.showing || this.isMakingSelection) return; this.hidePointer(); }); @@ -70,11 +76,17 @@ export class Pointer extends Component { this.modeToggles.find(b => b !== event.target).focus(); }); + + if (this.commentButton) { + DOM.onSelect(this.commentButton, this.createCommentAtPointer.bind(this)); + } } hidePointer() { this.pointer.style.display = null; this.showing = false; + this.targetElement = null; + this.targetSelectionRange = null; } /** @@ -84,7 +96,9 @@ export class Pointer extends Component { * @param {Boolean} keyboardMode */ showPointerAtTarget(element, xPosition, keyboardMode) { - this.updateForTarget(element); + this.targetElement = element; + this.targetSelectionRange = window.getSelection()?.getRangeAt(0); + this.updateDomForTarget(element); this.pointer.style.display = 'block'; const targetBounds = element.getBoundingClientRect(); @@ -98,10 +112,10 @@ export class Pointer extends Component { this.pointer.style.top = `${yOffset}px`; this.showing = true; - this.isSelection = true; + this.isMakingSelection = true; setTimeout(() => { - this.isSelection = false; + this.isMakingSelection = false; }, 100); const scrollListener = () => { @@ -119,7 +133,7 @@ export class Pointer extends Component { * Update the pointer inputs/content for the given target element. * @param {?Element} element */ - updateForTarget(element) { + updateDomForTarget(element) { const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`); const includeTag = `{{@${this.pageId}#${element.id}}}`; @@ -152,4 +166,34 @@ export class Pointer extends Component { }); } + createCommentAtPointer(event) { + if (!this.targetElement) { + return; + } + + const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, ''); + const refId = this.targetElement.id; + const hash = cyrb53(normalisedElemHtml); + let range = ''; + if (this.targetSelectionRange) { + const commonContainer = this.targetSelectionRange.commonAncestorContainer; + if (this.targetElement.contains(commonContainer)) { + const start = normalizeNodeTextOffsetToParent( + this.targetSelectionRange.startContainer, + this.targetSelectionRange.startOffset, + this.targetElement + ); + const end = normalizeNodeTextOffsetToParent( + this.targetSelectionRange.endContainer, + this.targetSelectionRange.endOffset, + this.targetElement + ); + range = `${start}-${end}`; + } + } + + const reference = `${refId}:${hash}:${range}`; + console.log(reference); + } + } diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index c88827bac40..779b4854773 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -178,3 +178,24 @@ export function htmlToDom(html: string): HTMLElement { return firstChild; } + +export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number { + if (!parentElement.contains(node)) { + throw new Error('ParentElement must be a prent of element'); + } + + let normalizedOffset = offset; + let currentNode: Node|null = node.nodeType === Node.TEXT_NODE ? + node : node.childNodes[offset]; + + while (currentNode !== parentElement && currentNode) { + if (currentNode.previousSibling) { + currentNode = currentNode.previousSibling; + normalizedOffset += (currentNode.textContent?.length || 0); + } else { + currentNode = currentNode.parentNode; + } + } + + return normalizedOffset; +} diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts index c5a5d2db804..1a6fa55b6b0 100644 --- a/resources/js/services/util.ts +++ b/resources/js/services/util.ts @@ -144,4 +144,25 @@ function getVersion(): string { export function importVersioned(moduleName: string): Promise { const importPath = window.baseUrl(`dist/${moduleName}.js?version=${getVersion()}`); return import(importPath); +} + +/* + cyrb53 (c) 2018 bryc (github.com/bryc) + License: Public domain (or MIT if needed). Attribution appreciated. + A fast and simple 53-bit string hash function with decent collision resistance. + Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. + Taken from: https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js +*/ +export function cyrb53(str: string, seed: number = 0): string { + let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; + for(let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + return (4294967296 * (2097151 & h2) + (h1 >>> 0)) as string; } \ No newline at end of file diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 45e58ffc865..de783705763 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -183,7 +183,6 @@ body.tox-fullscreen, body.markdown-fullscreen { } input, button, a { position: relative; - border-radius: 0; height: 28px; font-size: 12px; vertical-align: top; @@ -194,17 +193,19 @@ body.tox-fullscreen, body.markdown-fullscreen { border: 1px solid #DDD; @include mixins.lightDark(border-color, #ddd, #000); color: #666; - width: 160px; - z-index: 40; - padding: 5px 10px; + width: 180px; + z-index: 58; + padding: 5px; + border-radius: 0; } .text-button { @include mixins.lightDark(color, #444, #AAA); } .input-group .button { line-height: 1; - margin: 0 0 0 -4px; + margin: 0 0 0 -5px; box-shadow: none; + border-radius: 0; } a.button { margin: 0; diff --git a/resources/views/pages/parts/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php index 56f36cb75f3..77fc763827e 100644 --- a/resources/views/pages/parts/pointer.blade.php +++ b/resources/views/pages/parts/pointer.blade.php @@ -6,14 +6,14 @@ tabindex="-1" aria-label="{{ trans('entities.pages_pointer_label') }}" class="pointer-container"> -
                                          -
                                          +
                                          +
                                          - +
                                          - @if(userCan('page-update', $page)) - @icon('edit') - @endif +
                                          + @if(userCan('page-update', $page)) + @icon('edit') + @endif + @if($commentTree->enabled() && userCan('comment-create-all')) + + @endif +
                                          From add238fe9fb3d3626e8acd323bd32f91edb2797e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 18 Apr 2025 20:42:56 +0100 Subject: [PATCH 020/474] Comments & Pointer: Converted components to typescript Made changes for dom and translation services for easier usage considering types. trans_choice updated to allow default count replacement data as per Laravel's default behaviour. --- .../{page-comments.js => page-comments.ts} | 82 +++++++++++++------ .../js/components/{pointer.js => pointer.ts} | 53 +++++++----- resources/js/services/dom.ts | 8 +- resources/js/services/translations.ts | 1 + 4 files changed, 98 insertions(+), 46 deletions(-) rename resources/js/components/{page-comments.js => page-comments.ts} (68%) rename resources/js/components/{pointer.js => pointer.ts} (79%) diff --git a/resources/js/components/page-comments.js b/resources/js/components/page-comments.ts similarity index 68% rename from resources/js/components/page-comments.js rename to resources/js/components/page-comments.ts index 8f023836b09..a19d2c7d4b1 100644 --- a/resources/js/components/page-comments.js +++ b/resources/js/components/page-comments.ts @@ -2,8 +2,38 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; +export interface CommentReplyEvent extends Event { + detail: { + id: string; // ID of comment being replied to + element: HTMLElement; // Container for comment replied to + } +} + export class PageComments extends Component { + private elem: HTMLElement; + private pageId: number; + private container: HTMLElement; + private commentCountBar: HTMLElement; + private commentsTitle: HTMLElement; + private addButtonContainer: HTMLElement; + private replyToRow: HTMLElement; + private formContainer: HTMLElement; + private form: HTMLFormElement; + private formInput: HTMLInputElement; + private formReplyLink: HTMLAnchorElement; + private addCommentButton: HTMLElement; + private hideFormButton: HTMLElement; + private removeReplyToButton: HTMLElement; + private wysiwygLanguage: string; + private wysiwygTextDirection: string; + private wysiwygEditor: any = null; + private createdText: string; + private countText: string; + private parentId: number | null = null; + private contentReference: string = ''; + private formReplyText: string = ''; + setup() { this.elem = this.$el; this.pageId = Number(this.$opts.pageId); @@ -15,9 +45,9 @@ export class PageComments extends Component { this.addButtonContainer = this.$refs.addButtonContainer; this.replyToRow = this.$refs.replyToRow; this.formContainer = this.$refs.formContainer; - this.form = this.$refs.form; - this.formInput = this.$refs.formInput; - this.formReplyLink = this.$refs.formReplyLink; + this.form = this.$refs.form as HTMLFormElement; + this.formInput = this.$refs.formInput as HTMLInputElement; + this.formReplyLink = this.$refs.formReplyLink as HTMLAnchorElement; this.addCommentButton = this.$refs.addCommentButton; this.hideFormButton = this.$refs.hideFormButton; this.removeReplyToButton = this.$refs.removeReplyToButton; @@ -25,26 +55,23 @@ export class PageComments extends Component { // WYSIWYG options this.wysiwygLanguage = this.$opts.wysiwygLanguage; this.wysiwygTextDirection = this.$opts.wysiwygTextDirection; - this.wysiwygEditor = null; // Translations this.createdText = this.$opts.createdText; this.countText = this.$opts.countText; - // Internal State - this.parentId = null; this.formReplyText = this.formReplyLink?.textContent || ''; this.setupListeners(); } - setupListeners() { + protected setupListeners(): void { this.elem.addEventListener('page-comment-delete', () => { setTimeout(() => this.updateCount(), 1); this.hideForm(); }); - this.elem.addEventListener('page-comment-reply', event => { + this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => { this.setReply(event.detail.id, event.detail.element); }); @@ -56,7 +83,7 @@ export class PageComments extends Component { } } - saveComment(event) { + protected saveComment(event): void { event.preventDefault(); event.stopPropagation(); @@ -68,10 +95,11 @@ export class PageComments extends Component { const reqData = { html: this.wysiwygEditor.getContent(), parent_id: this.parentId || null, + content_reference: this.contentReference || '', }; window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { - const newElem = htmlToDom(resp.data); + const newElem = htmlToDom(resp.data as string); if (reqData.parent_id) { this.formContainer.after(newElem); @@ -91,20 +119,21 @@ export class PageComments extends Component { loading.remove(); } - updateCount() { + protected updateCount(): void { const count = this.getCommentCount(); - this.commentsTitle.textContent = window.$trans.choice(this.countText, count, {count}); + this.commentsTitle.textContent = window.$trans.choice(this.countText, count); } - resetForm() { + protected resetForm(): void { this.removeEditor(); this.formInput.value = ''; this.parentId = null; + this.contentReference = ''; this.replyToRow.toggleAttribute('hidden', true); this.container.append(this.formContainer); } - showForm() { + protected showForm(): void { this.removeEditor(); this.formContainer.toggleAttribute('hidden', false); this.addButtonContainer.toggleAttribute('hidden', true); @@ -112,7 +141,7 @@ export class PageComments extends Component { this.loadEditor(); } - hideForm() { + protected hideForm(): void { this.resetForm(); this.formContainer.toggleAttribute('hidden', true); if (this.getCommentCount() > 0) { @@ -123,7 +152,7 @@ export class PageComments extends Component { this.addButtonContainer.toggleAttribute('hidden', false); } - loadEditor() { + protected loadEditor(): void { if (this.wysiwygEditor) { this.wysiwygEditor.focus(); return; @@ -134,42 +163,49 @@ export class PageComments extends Component { containerElement: this.formInput, darkMode: document.documentElement.classList.contains('dark-mode'), textDirection: this.wysiwygTextDirection, + drawioUrl: '', + pageId: 0, translations: {}, - translationMap: window.editor_translations, + translationMap: (window as Record).editor_translations, }); - window.tinymce.init(config).then(editors => { + (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); } - removeEditor() { + protected removeEditor(): void { if (this.wysiwygEditor) { this.wysiwygEditor.remove(); this.wysiwygEditor = null; } } - getCommentCount() { + protected getCommentCount(): number { return this.container.querySelectorAll('[component="page-comment"]').length; } - setReply(commentLocalId, commentElement) { + protected setReply(commentLocalId, commentElement): void { const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); targetFormLocation.append(this.formContainer); this.showForm(); this.parentId = commentLocalId; this.replyToRow.toggleAttribute('hidden', false); - this.formReplyLink.textContent = this.formReplyText.replace('1234', this.parentId); + this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId)); this.formReplyLink.href = `#comment${this.parentId}`; } - removeReplyTo() { + protected removeReplyTo(): void { this.parentId = null; this.replyToRow.toggleAttribute('hidden', true); this.container.append(this.formContainer); this.showForm(); } + public startNewComment(contentReference: string): void { + this.removeReplyTo(); + this.contentReference = contentReference; + } + } diff --git a/resources/js/components/pointer.js b/resources/js/components/pointer.ts similarity index 79% rename from resources/js/components/pointer.js rename to resources/js/components/pointer.ts index 997df329a84..c3883b7b554 100644 --- a/resources/js/components/pointer.js +++ b/resources/js/components/pointer.ts @@ -1,18 +1,33 @@ import * as DOM from '../services/dom.ts'; import {Component} from './component'; import {copyTextToClipboard} from '../services/clipboard.ts'; -import {el} from "../wysiwyg/utils/dom"; import {cyrb53} from "../services/util"; import {normalizeNodeTextOffsetToParent} from "../services/dom.ts"; +import {PageComments} from "./page-comments"; export class Pointer extends Component { + protected showing: boolean = false; + protected isMakingSelection: boolean = false; + protected targetElement: HTMLElement|null = null; + protected targetSelectionRange: Range|null = null; + + protected pointer: HTMLElement; + protected linkInput: HTMLInputElement; + protected linkButton: HTMLElement; + protected includeInput: HTMLInputElement; + protected includeButton: HTMLElement; + protected sectionModeButton: HTMLElement; + protected commentButton: HTMLElement; + protected modeToggles: HTMLElement[]; + protected modeSections: HTMLElement[]; + protected pageId: string; + setup() { - this.container = this.$el; this.pointer = this.$refs.pointer; - this.linkInput = this.$refs.linkInput; + this.linkInput = this.$refs.linkInput as HTMLInputElement; this.linkButton = this.$refs.linkButton; - this.includeInput = this.$refs.includeInput; + this.includeInput = this.$refs.includeInput as HTMLInputElement; this.includeButton = this.$refs.includeButton; this.sectionModeButton = this.$refs.sectionModeButton; this.commentButton = this.$refs.commentButton; @@ -20,12 +35,6 @@ export class Pointer extends Component { this.modeSections = this.$manyRefs.modeSection; this.pageId = this.$opts.pageId; - // Instance variables - this.showing = false; - this.isMakingSelection = false; - this.targetElement = null; - this.targetSelectionRange = null; - this.setupListeners(); } @@ -36,7 +45,7 @@ export class Pointer extends Component { // Select all contents on input click DOM.onSelect([this.includeInput, this.linkInput], event => { - event.target.select(); + (event.target as HTMLInputElement).select(); event.stopPropagation(); }); @@ -58,9 +67,10 @@ export class Pointer extends Component { const pageContent = document.querySelector('.page-content'); DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { event.stopPropagation(); - const targetEl = event.target.closest('[id^="bkmrk"]'); + const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]'); if (targetEl && window.getSelection().toString().length > 0) { - this.showPointerAtTarget(targetEl, event.pageX, false); + const xPos = (event instanceof MouseEvent) ? event.pageX : 0; + this.showPointerAtTarget(targetEl, xPos, false); } }); @@ -69,12 +79,14 @@ export class Pointer extends Component { // Toggle between pointer modes DOM.onSelect(this.modeToggles, event => { + const targetToggle = (event.target as HTMLElement); for (const section of this.modeSections) { - const show = !section.contains(event.target); + const show = !section.contains(targetToggle); section.toggleAttribute('hidden', !show); } - this.modeToggles.find(b => b !== event.target).focus(); + const otherToggle = this.modeToggles.find(b => b !== targetToggle); + otherToggle && otherToggle.focus(); }); if (this.commentButton) { @@ -83,7 +95,7 @@ export class Pointer extends Component { } hidePointer() { - this.pointer.style.display = null; + this.pointer.style.removeProperty('display'); this.showing = false; this.targetElement = null; this.targetSelectionRange = null; @@ -97,7 +109,7 @@ export class Pointer extends Component { */ showPointerAtTarget(element, xPosition, keyboardMode) { this.targetElement = element; - this.targetSelectionRange = window.getSelection()?.getRangeAt(0); + this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null; this.updateDomForTarget(element); this.pointer.style.display = 'block'; @@ -120,7 +132,7 @@ export class Pointer extends Component { const scrollListener = () => { this.hidePointer(); - window.removeEventListener('scroll', scrollListener, {passive: true}); + window.removeEventListener('scroll', scrollListener); }; element.parentElement.insertBefore(this.pointer, element); @@ -142,7 +154,7 @@ export class Pointer extends Component { // Update anchor if present const editAnchor = this.pointer.querySelector('#pointer-edit'); - if (editAnchor && element) { + if (editAnchor instanceof HTMLAnchorElement && element) { const {editHref} = editAnchor.dataset; const elementId = element.id; @@ -193,7 +205,8 @@ export class Pointer extends Component { } const reference = `${refId}:${hash}:${range}`; - console.log(reference); + const pageComments = window.$components.first('page-comments') as PageComments; + pageComments.startNewComment(reference); } } diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 779b4854773..537af816a90 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -44,9 +44,11 @@ export function forEach(selector: string, callback: (el: Element) => any) { /** * Helper to listen to multiple DOM events */ -export function onEvents(listenerElement: Element, events: string[], callback: (e: Event) => any): void { - for (const eventName of events) { - listenerElement.addEventListener(eventName, callback); +export function onEvents(listenerElement: Element|null, events: string[], callback: (e: Event) => any): void { + if (listenerElement) { + for (const eventName of events) { + listenerElement.addEventListener(eventName, callback); + } } } diff --git a/resources/js/services/translations.ts b/resources/js/services/translations.ts index b37dbdfb074..821c34f18f7 100644 --- a/resources/js/services/translations.ts +++ b/resources/js/services/translations.ts @@ -10,6 +10,7 @@ export class Translator { * to use. Similar format at Laravel's 'trans_choice' helper. */ choice(translation: string, count: number, replacements: Record = {}): string { + replacements = Object.assign({}, replacements, {count: String(count)}); const splitText = translation.split('|'); const exactCountRegex = /^{([0-9]+)}/; const rangeRegex = /^\[([0-9]+),([0-9*]+)]/; From 5e3c3ad634cb7de4a13041292611c7aaafaa2f30 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 18 Apr 2025 21:13:49 +0100 Subject: [PATCH 021/474] Comments: Added back-end content reference handling Also added archived property, to be added. --- app/Activity/CommentRepo.php | 3 +- .../Controllers/CommentController.php | 3 +- app/Activity/Models/Comment.php | 2 ++ .../Activity/Models/CommentFactory.php | 2 ++ ..._content_refs_and_archived_to_comments.php | 30 +++++++++++++++++++ resources/js/components/page-comments.ts | 2 +- tests/Entity/CommentTest.php | 26 ++++++++++++++++ 7 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index 3336e17e988..c488350ca24 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -20,7 +20,7 @@ public function getById(int $id): Comment /** * Create a new comment on an entity. */ - public function create(Entity $entity, string $html, ?int $parent_id): Comment + public function create(Entity $entity, string $html, ?int $parent_id, string $content_ref): Comment { $userId = user()->id; $comment = new Comment(); @@ -30,6 +30,7 @@ public function create(Entity $entity, string $html, ?int $parent_id): Comment $comment->updated_by = $userId; $comment->local_id = $this->getNextLocalId($entity); $comment->parent_id = $parent_id; + $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $content_ref) === 1 ? $content_ref : ''; $entity->comments()->save($comment); ActivityService::add(ActivityType::COMMENT_CREATE, $comment); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 52ccc823864..26208006784 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -26,6 +26,7 @@ public function savePageComment(Request $request, int $pageId) $input = $this->validate($request, [ 'html' => ['required', 'string'], 'parent_id' => ['nullable', 'integer'], + 'content_ref' => ['string'], ]); $page = $this->pageQueries->findVisibleById($pageId); @@ -40,7 +41,7 @@ public function savePageComment(Request $request, int $pageId) // Create a new comment. $this->checkPermission('comment-create-all'); - $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null); + $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $input['content_ref']); return view('comments.comment-branch', [ 'readOnly' => false, diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index d0385d3962f..91cea4fe0e3 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -19,6 +19,8 @@ * @property int $entity_id * @property int $created_by * @property int $updated_by + * @property string $content_ref + * @property bool $archived */ class Comment extends Model implements Loggable { diff --git a/database/factories/Activity/Models/CommentFactory.php b/database/factories/Activity/Models/CommentFactory.php index efbd183b31d..844bc399381 100644 --- a/database/factories/Activity/Models/CommentFactory.php +++ b/database/factories/Activity/Models/CommentFactory.php @@ -27,6 +27,8 @@ public function definition() 'html' => $html, 'parent_id' => null, 'local_id' => 1, + 'content_ref' => '', + 'archived' => false, ]; } } diff --git a/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php b/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php new file mode 100644 index 00000000000..794201dec8a --- /dev/null +++ b/database/migrations/2025_04_18_215145_add_content_refs_and_archived_to_comments.php @@ -0,0 +1,30 @@ +string('content_ref'); + $table->boolean('archived')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comments', function (Blueprint $table) { + $table->dropColumn('content_ref'); + $table->dropColumn('archived'); + }); + } +}; diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index a19d2c7d4b1..45f8d6a9f6d 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -95,7 +95,7 @@ export class PageComments extends Component { const reqData = { html: this.wysiwygEditor.getContent(), parent_id: this.parentId || null, - content_reference: this.contentReference || '', + content_ref: this.contentReference || '', }; window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => { diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 9e019e3d148..973b2b81d8d 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -33,6 +33,32 @@ public function test_add_comment() $this->assertActivityExists(ActivityType::COMMENT_CREATE); } + public function test_add_comment_stores_content_reference_only_if_format_valid() + { + $validityByRefs = [ + 'bkmrk-my-title:4589284922:4-3' => true, + 'bkmrk-my-title:4589284922:' => true, + 'bkmrk-my-title:4589284922:abc' => false, + 'my-title:4589284922:' => false, + 'bkmrk-my-title-4589284922:' => false, + ]; + + $page = $this->entities->page(); + + foreach ($validityByRefs as $ref => $valid) { + $this->asAdmin()->postJson("/comment/$page->id", [ + 'html' => '

                                          My comment

                                          ', + 'parent_id' => null, + 'content_ref' => $ref, + ]); + + if ($valid) { + $this->assertDatabaseHas('comments', ['entity_id' => $page->id, 'content_ref' => $ref]); + } else { + $this->assertDatabaseMissing('comments', ['entity_id' => $page->id, 'content_ref' => $ref]); + } + } + } public function test_comment_edit() { From 2e7544a865a2b8ca7fdd3e32bdd86746a1a62512 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 19 Apr 2025 12:46:47 +0100 Subject: [PATCH 022/474] Comments: Converted comment component to TS --- .../{page-comment.js => page-comment.ts} | 47 +++++++++++++------ resources/views/comments/comment.blade.php | 2 +- 2 files changed, 34 insertions(+), 15 deletions(-) rename resources/js/components/{page-comment.js => page-comment.ts} (70%) diff --git a/resources/js/components/page-comment.js b/resources/js/components/page-comment.ts similarity index 70% rename from resources/js/components/page-comment.js rename to resources/js/components/page-comment.ts index 8c0a8b33e54..b2e2bac2784 100644 --- a/resources/js/components/page-comment.js +++ b/resources/js/components/page-comment.ts @@ -4,33 +4,51 @@ import {buildForInput} from '../wysiwyg-tinymce/config'; export class PageComment extends Component { + protected commentId: string; + protected commentLocalId: string; + protected commentContentRef: string; + protected deletedText: string; + protected updatedText: string; + + protected wysiwygEditor: any = null; + protected wysiwygLanguage: string; + protected wysiwygTextDirection: string; + + protected container: HTMLElement; + protected contentContainer: HTMLElement; + protected form: HTMLFormElement; + protected formCancel: HTMLElement; + protected editButton: HTMLElement; + protected deleteButton: HTMLElement; + protected replyButton: HTMLElement; + protected input: HTMLInputElement; + setup() { // Options this.commentId = this.$opts.commentId; this.commentLocalId = this.$opts.commentLocalId; - this.commentParentId = this.$opts.commentParentId; + this.commentContentRef = this.$opts.commentContentRef; this.deletedText = this.$opts.deletedText; this.updatedText = this.$opts.updatedText; // Editor reference and text options - this.wysiwygEditor = null; this.wysiwygLanguage = this.$opts.wysiwygLanguage; this.wysiwygTextDirection = this.$opts.wysiwygTextDirection; // Element references this.container = this.$el; this.contentContainer = this.$refs.contentContainer; - this.form = this.$refs.form; + this.form = this.$refs.form as HTMLFormElement; this.formCancel = this.$refs.formCancel; this.editButton = this.$refs.editButton; this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; - this.input = this.$refs.input; + this.input = this.$refs.input as HTMLInputElement; this.setupListeners(); } - setupListeners() { + protected setupListeners(): void { if (this.replyButton) { this.replyButton.addEventListener('click', () => this.$emit('reply', { id: this.commentLocalId, @@ -49,12 +67,12 @@ export class PageComment extends Component { } } - toggleEditMode(show) { + protected toggleEditMode(show: boolean) : void { this.contentContainer.toggleAttribute('hidden', show); this.form.toggleAttribute('hidden', !show); } - startEdit() { + protected startEdit() : void { this.toggleEditMode(true); if (this.wysiwygEditor) { @@ -67,29 +85,30 @@ export class PageComment extends Component { containerElement: this.input, darkMode: document.documentElement.classList.contains('dark-mode'), textDirection: this.wysiwygTextDirection, + drawioUrl: '', + pageId: 0, translations: {}, - translationMap: window.editor_translations, + translationMap: (window as Record).editor_translations, }); - window.tinymce.init(config).then(editors => { + (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); } - async update(event) { + protected async update(event: Event): Promise { event.preventDefault(); const loading = this.showLoading(); this.form.toggleAttribute('hidden', true); const reqData = { html: this.wysiwygEditor.getContent(), - parent_id: this.parentId || null, }; try { const resp = await window.$http.put(`/comment/${this.commentId}`, reqData); - const newComment = htmlToDom(resp.data); + const newComment = htmlToDom(resp.data as string); this.container.replaceWith(newComment); window.$events.success(this.updatedText); } catch (err) { @@ -100,7 +119,7 @@ export class PageComment extends Component { } } - async delete() { + protected async delete(): Promise { this.showLoading(); await window.$http.delete(`/comment/${this.commentId}`); @@ -109,7 +128,7 @@ export class PageComment extends Component { window.$events.success(this.deletedText); } - showLoading() { + protected showLoading(): HTMLElement { const loading = getLoading(); loading.classList.add('px-l'); this.container.append(loading); diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 2bf89d6832d..c3578293a5a 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -4,7 +4,7 @@
                                          Date: Sat, 19 Apr 2025 14:07:52 +0100 Subject: [PATCH 023/474] Comments: Added inline comment marker/highlight logic --- resources/js/components/page-comment.ts | 47 ++++++++++++++++++++- resources/js/components/pointer.ts | 6 +-- resources/js/services/dom.ts | 56 +++++++++++++++++++++++++ resources/js/services/util.ts | 2 +- resources/sass/_pages.scss | 21 ++++++++++ 5 files changed, 126 insertions(+), 6 deletions(-) diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index b2e2bac2784..f4d295b95a3 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -1,6 +1,7 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom.ts'; +import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; +import {el} from "../wysiwyg/utils/dom"; export class PageComment extends Component { @@ -46,6 +47,7 @@ export class PageComment extends Component { this.input = this.$refs.input as HTMLInputElement; this.setupListeners(); + this.positionForReference(); } protected setupListeners(): void { @@ -135,4 +137,47 @@ export class PageComment extends Component { return loading; } + protected positionForReference() { + if (!this.commentContentRef) { + return; + } + + const [refId, refHash, refRange] = this.commentContentRef.split(':'); + const refEl = document.getElementById(refId); + if (!refEl) { + // TODO - Show outdated marker for comment + return; + } + + const actualHash = hashElement(refEl); + if (actualHash !== refHash) { + // TODO - Show outdated marker for comment + return; + } + + const refElBounds = refEl.getBoundingClientRect(); + let bounds = refElBounds; + const [rangeStart, rangeEnd] = refRange.split('-'); + if (rangeStart && rangeEnd) { + const range = new Range(); + const relStart = findTargetNodeAndOffset(refEl, Number(rangeStart)); + const relEnd = findTargetNodeAndOffset(refEl, Number(rangeEnd)); + if (relStart && relEnd) { + range.setStart(relStart.node, relStart.offset); + range.setEnd(relEnd.node, relEnd.offset); + bounds = range.getBoundingClientRect(); + } + } + + const relLeft = bounds.left - refElBounds.left; + const relTop = bounds.top - refElBounds.top; + // TODO - Extract to class, Use theme color + const marker = el('div', { + class: 'content-comment-highlight', + style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;` + }, ['']); + + refEl.style.position = 'relative'; + refEl.append(marker); + } } diff --git a/resources/js/components/pointer.ts b/resources/js/components/pointer.ts index c3883b7b554..d84186d872d 100644 --- a/resources/js/components/pointer.ts +++ b/resources/js/components/pointer.ts @@ -1,8 +1,7 @@ import * as DOM from '../services/dom.ts'; import {Component} from './component'; import {copyTextToClipboard} from '../services/clipboard.ts'; -import {cyrb53} from "../services/util"; -import {normalizeNodeTextOffsetToParent} from "../services/dom.ts"; +import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts"; import {PageComments} from "./page-comments"; export class Pointer extends Component { @@ -183,9 +182,8 @@ export class Pointer extends Component { return; } - const normalisedElemHtml = this.targetElement.outerHTML.replace(/\s{2,}/g, ''); const refId = this.targetElement.id; - const hash = cyrb53(normalisedElemHtml); + const hash = hashElement(this.targetElement); let range = ''; if (this.targetSelectionRange) { const commonContainer = this.targetSelectionRange.commonAncestorContainer; diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 537af816a90..661ed7ca3e5 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -1,3 +1,5 @@ +import {cyrb53} from "./util"; + /** * Check if the given param is a HTMLElement */ @@ -181,6 +183,9 @@ export function htmlToDom(html: string): HTMLElement { return firstChild; } +/** + * For the given node and offset, return an adjusted offset that's relative to the given parent element. + */ export function normalizeNodeTextOffsetToParent(node: Node, offset: number, parentElement: HTMLElement): number { if (!parentElement.contains(node)) { throw new Error('ParentElement must be a prent of element'); @@ -201,3 +206,54 @@ export function normalizeNodeTextOffsetToParent(node: Node, offset: number, pare return normalizedOffset; } + +/** + * Find the target child node and adjusted offset based on a parent node and text offset. + * Returns null if offset not found within the given parent node. + */ +export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number): ({node: Node, offset: number}|null) { + if (offset === 0) { + return { node: parentNode, offset: 0 }; + } + + let currentOffset = 0; + let currentNode = null; + + for (let i = 0; i < parentNode.childNodes.length; i++) { + currentNode = parentNode.childNodes[i]; + + if (currentNode.nodeType === Node.TEXT_NODE) { + // For text nodes, count the length of their content + // Returns if within range + const textLength = currentNode.textContent.length; + if (currentOffset + textLength >= offset) { + return { + node: currentNode, + offset: offset - currentOffset + }; + } + + currentOffset += textLength; + } else if (currentNode.nodeType === Node.ELEMENT_NODE) { + // Otherwise, if an element, track the text length and search within + // if in range for the target offset + const elementTextLength = currentNode.textContent.length; + if (currentOffset + elementTextLength >= offset) { + return findTargetNodeAndOffset(currentNode, offset - currentOffset); + } + + currentOffset += elementTextLength; + } + } + + // Return null if not found within range + return null; +} + +/** + * Create a hash for the given HTML element. + */ +export function hashElement(element: HTMLElement): string { + const normalisedElemHtml = element.outerHTML.replace(/\s{2,}/g, ''); + return cyrb53(normalisedElemHtml); +} \ No newline at end of file diff --git a/resources/js/services/util.ts b/resources/js/services/util.ts index 1a6fa55b6b0..61a02a3d24d 100644 --- a/resources/js/services/util.ts +++ b/resources/js/services/util.ts @@ -164,5 +164,5 @@ export function cyrb53(str: string, seed: number = 0): string { h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - return (4294967296 * (2097151 & h2) + (h1 >>> 0)) as string; + return String((4294967296 * (2097151 & h2) + (h1 >>> 0))); } \ No newline at end of file diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index de783705763..1fe22b9c4e2 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -219,6 +219,27 @@ body.tox-fullscreen, body.markdown-fullscreen { } } +// Page inline comments +.content-comment-highlight { + position: absolute; + left: 0; + top: 0; + width: 0; + height: 0; + user-select: none; + pointer-events: none; + &:after { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--color-primary); + opacity: 0.25; + } +} + // Page editor sidebar toolbox .floating-toolbox { @include mixins.lightDark(background-color, #FFF, #222); From 5bfba281fc0c57556d65092013b5663efd350b3e Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 21 Apr 2025 14:04:41 +0100 Subject: [PATCH 024/474] Comments: Started inline comment display windows --- lang/en/entities.php | 1 + resources/js/components/page-comment.ts | 66 ++++++++++++++++++++-- resources/sass/_animations.scss | 22 ++++++++ resources/sass/_pages.scss | 46 +++++++++++++++ resources/views/comments/comment.blade.php | 1 + 5 files changed, 132 insertions(+), 4 deletions(-) diff --git a/lang/en/entities.php b/lang/en/entities.php index a74785eaacd..9ce684ac71b 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -402,6 +402,7 @@ 'comment_deleted_success' => 'Comment deleted', 'comment_created_success' => 'Comment added', 'comment_updated_success' => 'Comment updated', + 'comment_view' => 'View comment', 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', 'comment_in_reply_to' => 'In reply to :commentId', 'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.', diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index f4d295b95a3..5a148c25885 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -3,6 +3,8 @@ import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../se import {buildForInput} from '../wysiwyg-tinymce/config'; import {el} from "../wysiwyg/utils/dom"; +import commentIcon from "@icons/comment.svg" + export class PageComment extends Component { protected commentId: string; @@ -10,6 +12,7 @@ export class PageComment extends Component { protected commentContentRef: string; protected deletedText: string; protected updatedText: string; + protected viewCommentText: string; protected wysiwygEditor: any = null; protected wysiwygLanguage: string; @@ -31,6 +34,7 @@ export class PageComment extends Component { this.commentContentRef = this.$opts.commentContentRef; this.deletedText = this.$opts.deletedText; this.updatedText = this.$opts.updatedText; + this.viewCommentText = this.$opts.viewCommentText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -171,13 +175,67 @@ export class PageComment extends Component { const relLeft = bounds.left - refElBounds.left; const relTop = bounds.top - refElBounds.top; - // TODO - Extract to class, Use theme color - const marker = el('div', { + + const marker = el('button', { + type: 'button', + class: 'content-comment-marker', + title: this.viewCommentText, + }); + marker.innerHTML = commentIcon; + marker.addEventListener('click', event => { + this.showCommentAtMarker(marker); + }); + + const markerWrap = el('div', { class: 'content-comment-highlight', style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;` - }, ['']); + }, [marker]); refEl.style.position = 'relative'; - refEl.append(marker); + refEl.append(markerWrap); + } + + protected showCommentAtMarker(marker: HTMLElement): void { + + marker.hidden = true; + const readClone = this.container.closest('.comment-branch').cloneNode(true) as HTMLElement; + const toRemove = readClone.querySelectorAll('.actions, form'); + for (const el of toRemove) { + el.remove(); + } + + const close = el('button', {type: 'button'}, ['x']); + const jump = el('button', {type: 'button'}, ['Jump to thread']); + + const commentWindow = el('div', { + class: 'content-comment-window' + }, [ + el('div', { + class: 'content-comment-window-actions', + }, [jump, close]), + el('div', { + class: 'content-comment-window-content', + }, [readClone]), + ]); + + marker.parentElement.append(commentWindow); + + const closeAction = () => { + commentWindow.remove(); + marker.hidden = false; + }; + + close.addEventListener('click', closeAction.bind(this)); + + jump.addEventListener('click', () => { + closeAction(); + this.container.scrollIntoView({behavior: 'smooth'}); + const highlightTarget = this.container.querySelector('.header') as HTMLElement; + highlightTarget.classList.add('anim-highlight'); + highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) + }); + + // TODO - Position wrapper sensibly + // TODO - Movement control? } } diff --git a/resources/sass/_animations.scss b/resources/sass/_animations.scss index f1aa3139b8e..ccbe36161b6 100644 --- a/resources/sass/_animations.scss +++ b/resources/sass/_animations.scss @@ -67,4 +67,26 @@ animation-duration: 180ms; animation-delay: 0s; animation-timing-function: cubic-bezier(.62, .28, .23, .99); +} + +@keyframes highlight { + 0% { + background-color: var(--color-primary-light); + } + 33% { + background-color: transparent; + } + 66% { + background-color: var(--color-primary-light); + } + 100% { + background-color: transparent; + } +} + +.anim-highlight { + animation-name: highlight; + animation-duration: 2s; + animation-delay: 0s; + animation-timing-function: linear; } \ No newline at end of file diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 1fe22b9c4e2..ac2d195b4e8 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -239,6 +239,52 @@ body.tox-fullscreen, body.markdown-fullscreen { opacity: 0.25; } } +.content-comment-window { + font-size: vars.$fs-m; + line-height: 1.4; + position: relative; + z-index: 90; + pointer-events: all; + min-width: min(340px, 80vw); + background-color: #FFF; + //border: 1px solid var(--color-primary); + box-shadow: vars.$bs-hover; + border-radius: 4px; + overflow: hidden; +} +.content-comment-window-actions { + background-color: var(--color-primary); + color: #FFF; + display: flex; + align-items: center; + justify-content: end; +} +.content-comment-window-content { + padding: vars.$xs; + max-height: 200px; + overflow-y: scroll; +} +.content-comment-marker { + position: absolute; + right: -16px; + top: -16px; + pointer-events: all; + width: min(1.5em, 32px); + height: min(1.5em, 32px); + border-radius: min(calc(1.5em / 2), 32px); + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-primary); + box-shadow: vars.$bs-hover; + color: #FFF; + cursor: pointer; + z-index: 90; + svg { + fill: #FFF; + width: 80%; + } +} // Page editor sidebar toolbox .floating-toolbox { diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index c3578293a5a..1886dad51e4 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -7,6 +7,7 @@ option:page-comment:comment-content-ref="{{ $comment->content_ref }}" option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" + option:page-comment:view-comment-text="{{ trans('entities.comment_view') }}" option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}" option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" id="comment{{$comment->local_id}}" From f656a82fe7a2be253ee53c3af8d27edd35617bd2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 24 Apr 2025 13:21:23 +0100 Subject: [PATCH 025/474] Comments: Styled content comments & improved interaction --- lang/en/entities.php | 1 + resources/js/components/page-comment.ts | 52 +++++++++++++++---- resources/sass/_components.scss | 27 ++++++++++ resources/sass/_pages.scss | 29 +++++++++-- .../views/comments/comment-branch.blade.php | 2 +- resources/views/comments/comment.blade.php | 2 + 6 files changed, 98 insertions(+), 15 deletions(-) diff --git a/lang/en/entities.php b/lang/en/entities.php index 9ce684ac71b..f9fab8ebfd3 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -403,6 +403,7 @@ 'comment_created_success' => 'Comment added', 'comment_updated_success' => 'Comment updated', 'comment_view' => 'View comment', + 'comment_jump_to_thread' => 'Jump to thread', 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', 'comment_in_reply_to' => 'In reply to :commentId', 'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.', diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 5a148c25885..9192c7c56af 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -4,6 +4,13 @@ import {buildForInput} from '../wysiwyg-tinymce/config'; import {el} from "../wysiwyg/utils/dom"; import commentIcon from "@icons/comment.svg" +import closeIcon from "@icons/close.svg" + +/** + * Track the close function for the current open marker so it can be closed + * when another is opened so we only show one marker comment thread at one time. + */ +let openMarkerClose: Function|null = null; export class PageComment extends Component { @@ -13,6 +20,8 @@ export class PageComment extends Component { protected deletedText: string; protected updatedText: string; protected viewCommentText: string; + protected jumpToThreadText: string; + protected closeText: string; protected wysiwygEditor: any = null; protected wysiwygLanguage: string; @@ -35,6 +44,8 @@ export class PageComment extends Component { this.deletedText = this.$opts.deletedText; this.updatedText = this.$opts.updatedText; this.viewCommentText = this.$opts.viewCommentText; + this.jumpToThreadText = this.$opts.jumpToThreadText; + this.closeText = this.$opts.closeText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -130,7 +141,7 @@ export class PageComment extends Component { await window.$http.delete(`/comment/${this.commentId}`); this.$emit('delete'); - this.container.closest('.comment-branch').remove(); + this.container.closest('.comment-branch')?.remove(); window.$events.success(this.deletedText); } @@ -196,16 +207,22 @@ export class PageComment extends Component { } protected showCommentAtMarker(marker: HTMLElement): void { - + // Hide marker and close existing marker windows + if (openMarkerClose) { + openMarkerClose(); + } marker.hidden = true; - const readClone = this.container.closest('.comment-branch').cloneNode(true) as HTMLElement; + + // Build comment window + const readClone = (this.container.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement; const toRemove = readClone.querySelectorAll('.actions, form'); for (const el of toRemove) { el.remove(); } - const close = el('button', {type: 'button'}, ['x']); - const jump = el('button', {type: 'button'}, ['Jump to thread']); + const close = el('button', {type: 'button', title: this.closeText}); + close.innerHTML = (closeIcon as string); + const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]); const commentWindow = el('div', { class: 'content-comment-window' @@ -214,19 +231,29 @@ export class PageComment extends Component { class: 'content-comment-window-actions', }, [jump, close]), el('div', { - class: 'content-comment-window-content', + class: 'content-comment-window-content comment-container-compact comment-container-super-compact', }, [readClone]), ]); - marker.parentElement.append(commentWindow); + marker.parentElement?.append(commentWindow); + // Handle interaction within window const closeAction = () => { commentWindow.remove(); marker.hidden = false; + window.removeEventListener('click', windowCloseAction); + openMarkerClose = null; }; - close.addEventListener('click', closeAction.bind(this)); + const windowCloseAction = (event: MouseEvent) => { + if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) { + closeAction(); + } + }; + window.addEventListener('click', windowCloseAction); + openMarkerClose = closeAction; + close.addEventListener('click', closeAction.bind(this)); jump.addEventListener('click', () => { closeAction(); this.container.scrollIntoView({behavior: 'smooth'}); @@ -235,7 +262,12 @@ export class PageComment extends Component { highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) }); - // TODO - Position wrapper sensibly - // TODO - Movement control? + // Position window within bounds + const commentWindowBounds = commentWindow.getBoundingClientRect(); + const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect(); + if (contentBounds && commentWindowBounds.right > contentBounds.right) { + const diff = commentWindowBounds.right - contentBounds.right; + commentWindow.style.left = `-${diff}px`; + } } } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 58d39d3ee6e..26b0518275b 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -746,6 +746,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { height: calc(100% - vars.$m); } +.comment-branch .comment-box { + margin-bottom: vars.$m; +} + .comment-branch .comment-branch .comment-branch .comment-branch .comment-thread-indicator { display: none; } @@ -761,6 +765,7 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { } .comment-container-compact .comment-box { + margin-bottom: vars.$xs; .meta { font-size: 0.8rem; } @@ -778,6 +783,28 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { width: vars.$m; } +.comment-container-super-compact .comment-box { + .meta { + font-size: 12px; + } + .avatar { + width: 18px; + margin-inline-end: 2px !important; + } + .content { + padding: vars.$xxs vars.$s; + line-height: 1.2; + } + .content p { + font-size: 12px; + } +} + +.comment-container-super-compact .comment-thread-indicator { + width: (vars.$xs + 3px); + margin-inline-start: 3px; +} + #tag-manager .drag-card { max-width: 500px; } diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index ac2d195b4e8..be5a0f7c360 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -242,12 +242,13 @@ body.tox-fullscreen, body.markdown-fullscreen { .content-comment-window { font-size: vars.$fs-m; line-height: 1.4; - position: relative; - z-index: 90; + position: absolute; + top: calc(100% + 3px); + left: 0; + z-index: 92; pointer-events: all; min-width: min(340px, 80vw); background-color: #FFF; - //border: 1px solid var(--color-primary); box-shadow: vars.$bs-hover; border-radius: 4px; overflow: hidden; @@ -258,9 +259,24 @@ body.tox-fullscreen, body.markdown-fullscreen { display: flex; align-items: center; justify-content: end; + gap: vars.$xs; + button { + color: #FFF; + font-size: 12px; + padding: vars.$xs; + line-height: 1; + cursor: pointer; + } + button[data-action="jump"] { + text-decoration: underline; + } + svg { + fill: currentColor; + width: 12px; + } } .content-comment-window-content { - padding: vars.$xs; + padding: vars.$xs vars.$s vars.$xs vars.$xs; max-height: 200px; overflow-y: scroll; } @@ -280,11 +296,16 @@ body.tox-fullscreen, body.markdown-fullscreen { color: #FFF; cursor: pointer; z-index: 90; + transform: scale(1); + transition: transform ease-in-out 120ms; svg { fill: #FFF; width: 80%; } } +.page-content [id^="bkmrk-"]:hover .content-comment-marker { + transform: scale(1.15); +} // Page editor sidebar toolbox .floating-toolbox { diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php index 78d19ac3ea4..83fa4b5c595 100644 --- a/resources/views/comments/comment-branch.blade.php +++ b/resources/views/comments/comment-branch.blade.php @@ -1,5 +1,5 @@
                                          -
                                          +
                                          @include('comments.comment', ['comment' => $branch['comment']])
                                          diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 1886dad51e4..5b79da4ac68 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -8,6 +8,8 @@ option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" option:page-comment:view-comment-text="{{ trans('entities.comment_view') }}" + option:page-comment:jump-to-thread-text="{{ trans('entities.comment_jump_to_thread') }}" + option:page-comment:close-text="{{ trans('common.close') }}" option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}" option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" id="comment{{$comment->local_id}}" From c61ce8dee4ea474b16564c3d3337bac25fba1f46 Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 12:45:09 +0530 Subject: [PATCH 026/474] Implement functionality to export a book, along with its pages and chapters, as a ZIP file. --- .../Controllers/BookExportApiController.php | 18 +++++++++++++++++- .../Controllers/ChapterExportApiController.php | 12 +++++++++++- .../Controllers/PageExportApiController.php | 14 +++++++++++++- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/Exports/Controllers/BookExportApiController.php b/app/Exports/Controllers/BookExportApiController.php index 164946b0c78..431afef143d 100644 --- a/app/Exports/Controllers/BookExportApiController.php +++ b/app/Exports/Controllers/BookExportApiController.php @@ -4,6 +4,7 @@ use BookStack\Entities\Queries\BookQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,19 @@ public function exportMarkdown(int $id) return $this->download()->directly($markdown, $book->slug . '.md'); } -} + + + /** + * Export a book to a contained ZIP export file. + * @throws NotFoundException + */ + public function exportZip(int $id, ZipExportBuilder $builder) + { + $book = $this->queries->findVisibleByIdOrFail($id); + $bookName= $book->getShortName(); + + $zip = $builder->buildForBook($book); + + return $this->download()->streamedFileDirectly($zip, $bookName . '.zip', filesize($zip), true); + } +} \ No newline at end of file diff --git a/app/Exports/Controllers/ChapterExportApiController.php b/app/Exports/Controllers/ChapterExportApiController.php index 9914e2b7fbe..58df4c9b087 100644 --- a/app/Exports/Controllers/ChapterExportApiController.php +++ b/app/Exports/Controllers/ChapterExportApiController.php @@ -4,6 +4,7 @@ use BookStack\Entities\Queries\ChapterQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,13 @@ public function exportMarkdown(int $id) return $this->download()->directly($markdown, $chapter->slug . '.md'); } -} + + public function exportZip(int $id, ZipExportBuilder $builder) + { + $chapter = $this->queries->findVisibleByIdOrFail($id); + $chapterName= $chapter->getShortName(); + $zip = $builder->buildForChapter($chapter); + + return $this->download()->streamedFileDirectly($zip, $chapterName . '.zip', filesize($zip), true); + } +} \ No newline at end of file diff --git a/app/Exports/Controllers/PageExportApiController.php b/app/Exports/Controllers/PageExportApiController.php index c6e20b615d2..ef564da3e5c 100644 --- a/app/Exports/Controllers/PageExportApiController.php +++ b/app/Exports/Controllers/PageExportApiController.php @@ -4,6 +4,7 @@ use BookStack\Entities\Queries\PageQueries; use BookStack\Exports\ExportFormatter; +use BookStack\Exports\ZipExports\ZipExportBuilder; use BookStack\Http\ApiController; use Throwable; @@ -63,4 +64,15 @@ public function exportMarkdown(int $id) return $this->download()->directly($markdown, $page->slug . '.md'); } -} + + + + public function exportZip(int $id, ZipExportBuilder $builder) + { + $page = $this->queries->findVisibleByIdOrFail($id); + $pageSlug = $page->slug; + $zip = $builder->buildForPage($page); + + return $this->download()->streamedFileDirectly($zip, $pageSlug . '.zip', filesize($zip), true); + } +} \ No newline at end of file From 5fa728f28a0c4f09649da7b653dd66260e49a596 Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 12:48:34 +0530 Subject: [PATCH 027/474] Develop functionality to import ZIP files. Create an API controller and define a route entry for handling the import process. Implement logic to read the list of files within the ZIP, process the directory structure, and automatically create associated pages, chapters, and books based on the ZIP file's contents. --- .../Controllers/ImportApiController.php | 121 ++++++++++++++++++ routes/api.php | 6 + 2 files changed, 127 insertions(+) create mode 100644 app/Exports/Controllers/ImportApiController.php diff --git a/app/Exports/Controllers/ImportApiController.php b/app/Exports/Controllers/ImportApiController.php new file mode 100644 index 00000000000..682d340b3b4 --- /dev/null +++ b/app/Exports/Controllers/ImportApiController.php @@ -0,0 +1,121 @@ +middleware('can:content-import'); + } + + /** + * List existing imports visible to the user. + */ + public function list(): JsonResponse + { + $imports = $this->imports->getVisibleImports(); + + return response()->json([ + 'status' => 'success', + 'imports' => $imports, + ]); + } + + /** + * Upload, validate and store an import file. + */ + public function upload(Request $request): JsonResponse + { + $this->validate($request, [ + 'file' => ['required', ...AttachmentService::getFileValidationRules()] + ]); + + $file = $request->file('file'); + + try { + $import = $this->imports->storeFromUpload($file); + } catch (ZipValidationException $exception) { + return response()->json([ + 'status' => 'error', + 'message' => 'Validation failed', + 'errors' => $exception->errors, + ], 422); + } + + return response()->json([ + 'status' => 'success', + 'import' => $import, + ], 201); + } + + /** + * Show details of a pending import. + */ + public function read(int $id): JsonResponse + { + $import = $this->imports->findVisible($id); + + return response()->json([ + 'status' => 'success', + 'import' => $import, + 'data' => $import->decodeMetadata(), + ]); + } + + /** + * Run the import process. + */ + public function create(int $id, Request $request): JsonResponse + { + $import = $this->imports->findVisible($id); + $parent = null; + + if ($import->type === 'page' || $import->type === 'chapter') { + $data = $this->validate($request, [ + 'parent' => ['required', 'string'], + ]); + $parent = $data['parent']; + } + + try { + $entity = $this->imports->runImport($import, $parent); + } catch (ZipImportException $exception) { + return response()->json([ + 'status' => 'error', + 'message' => 'Import failed', + 'errors' => $exception->errors, + ], 500); + } + + return response()->json([ + 'status' => 'success', + 'entity' => $entity, + ]); + } + + /** + * Delete a pending import. + */ + public function delete(int $id): JsonResponse + { + $import = $this->imports->findVisible($id); + $this->imports->deleteImport($import); + + return response()->json([ + 'status' => 'success', + 'message' => 'Import deleted successfully', + ]); + } +} \ No newline at end of file diff --git a/routes/api.php b/routes/api.php index 71036485597..bd00ea4b098 100644 --- a/routes/api.php +++ b/routes/api.php @@ -92,3 +92,9 @@ Route::put('content-permissions/{contentType}/{contentId}', [ContentPermissionApiController::class, 'update']); Route::get('audit-log', [AuditLogApiController::class, 'list']); + +Route::get('import', [ExportControllers\ImportApiController::class, 'list']); +Route::post('import', [ExportControllers\ImportApiController::class, 'upload']); +Route::get('import/{id}', [ExportControllers\ImportApiController::class, 'read']); +Route::post('import/{id}/create', [ExportControllers\ImportApiController::class, 'create']); +Route::delete('import/{id}', [ExportControllers\ImportApiController::class, 'destroy']); \ No newline at end of file From 64da80cbf4611f0e6a1700e6e2fa399389ee50da Mon Sep 17 00:00:00 2001 From: "nchoudhary@logicmines.in" Date: Fri, 25 Apr 2025 13:00:06 +0530 Subject: [PATCH 028/474] added routes for zip export --- routes/api.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/api.php b/routes/api.php index bd00ea4b098..7bc7d7d44c1 100644 --- a/routes/api.php +++ b/routes/api.php @@ -36,6 +36,7 @@ Route::get('books/{id}/export/pdf', [ExportControllers\BookExportApiController::class, 'exportPdf']); Route::get('books/{id}/export/plaintext', [ExportControllers\BookExportApiController::class, 'exportPlainText']); Route::get('books/{id}/export/markdown', [ExportControllers\BookExportApiController::class, 'exportMarkdown']); +Route::get('books/{id}/export/zip', [ExportControllers\BookExportApiController::class, 'exportZip']); Route::get('chapters', [EntityControllers\ChapterApiController::class, 'list']); Route::post('chapters', [EntityControllers\ChapterApiController::class, 'create']); @@ -46,6 +47,7 @@ Route::get('chapters/{id}/export/pdf', [ExportControllers\ChapterExportApiController::class, 'exportPdf']); Route::get('chapters/{id}/export/plaintext', [ExportControllers\ChapterExportApiController::class, 'exportPlainText']); Route::get('chapters/{id}/export/markdown', [ExportControllers\ChapterExportApiController::class, 'exportMarkdown']); +Route::get('chapters/{id}/export/zip', [ExportControllers\ChapterExportApiController::class, 'exportZip']); Route::get('pages', [EntityControllers\PageApiController::class, 'list']); Route::post('pages', [EntityControllers\PageApiController::class, 'create']); @@ -57,6 +59,7 @@ Route::get('pages/{id}/export/pdf', [ExportControllers\PageExportApiController::class, 'exportPdf']); Route::get('pages/{id}/export/plaintext', [ExportControllers\PageExportApiController::class, 'exportPlainText']); Route::get('pages/{id}/export/markdown', [ExportControllers\PageExportApiController::class, 'exportMarkdown']); +Route::get('pages/{id}/export/zip', [ExportControllers\PageExportApiController::class, 'exportZip']); Route::get('image-gallery', [ImageGalleryApiController::class, 'list']); Route::post('image-gallery', [ImageGalleryApiController::class, 'create']); From ecda4e1d6f42108fef9c62ff4a9a73a056caa089 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 26 Apr 2025 21:05:54 +0100 Subject: [PATCH 029/474] Comments: Added reference marker to comments --- resources/icons/bookmark.svg | 1 + resources/js/components/page-comment.ts | 17 +++++++--- resources/js/components/page-display.js | 3 ++ resources/sass/_components.scss | 38 ++++++++++++++++++++++ resources/sass/_pages.scss | 3 ++ resources/views/comments/comment.blade.php | 5 +++ 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 resources/icons/bookmark.svg diff --git a/resources/icons/bookmark.svg b/resources/icons/bookmark.svg new file mode 100644 index 00000000000..30e487c5219 --- /dev/null +++ b/resources/icons/bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 9192c7c56af..24964bf5cba 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -5,6 +5,7 @@ import {el} from "../wysiwyg/utils/dom"; import commentIcon from "@icons/comment.svg" import closeIcon from "@icons/close.svg" +import {PageDisplay} from "./page-display"; /** * Track the close function for the current open marker so it can be closed @@ -35,6 +36,7 @@ export class PageComment extends Component { protected deleteButton: HTMLElement; protected replyButton: HTMLElement; protected input: HTMLInputElement; + protected contentRefLink: HTMLLinkElement|null; setup() { // Options @@ -60,6 +62,7 @@ export class PageComment extends Component { this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; this.input = this.$refs.input as HTMLInputElement; + this.contentRefLink = (this.$refs.contentRef || null) as HTMLLinkElement|null; this.setupListeners(); this.positionForReference(); @@ -153,21 +156,20 @@ export class PageComment extends Component { } protected positionForReference() { - if (!this.commentContentRef) { + if (!this.commentContentRef || !this.contentRefLink) { return; } const [refId, refHash, refRange] = this.commentContentRef.split(':'); const refEl = document.getElementById(refId); if (!refEl) { - // TODO - Show outdated marker for comment + this.contentRefLink.classList.add('outdated', 'missing'); return; } const actualHash = hashElement(refEl); if (actualHash !== refHash) { - // TODO - Show outdated marker for comment - return; + this.contentRefLink.classList.add('outdated'); } const refElBounds = refEl.getBoundingClientRect(); @@ -204,6 +206,13 @@ export class PageComment extends Component { refEl.style.position = 'relative'; refEl.append(markerWrap); + + this.contentRefLink.href = `#${refEl.id}`; + this.contentRefLink.addEventListener('click', (event: MouseEvent) => { + const pageDisplayComponent = window.$components.get('page-display')[0] as PageDisplay; + event.preventDefault(); + pageDisplayComponent.goToText(refId); + }); } protected showCommentAtMarker(marker: HTMLElement): void { diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index d3ac78a4ad1..13670c4bf50 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -57,6 +57,9 @@ export class PageDisplay extends Component { } } + /** + * @public + */ goToText(text) { const idElem = document.getElementById(text); diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 26b0518275b..5486d611288 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -746,6 +746,44 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { height: calc(100% - vars.$m); } +.comment-reference-indicator-wrap a { + float: left; + margin-top: vars.$xs; + font-size: 12px; + display: inline-block; + font-weight: bold; + position: relative; + border-radius: 4px; + overflow: hidden; + padding: 2px 6px 2px 0; + margin-inline-end: vars.$xs; + color: var(--color-link); + span { + display: none; + } + &.outdated span { + display: inline; + } + &.outdated.missing { + color: var(--color-warning); + pointer-events: none; + } + svg { + width: 24px; + margin-inline-end: 0; + } + &:after { + background-color: currentColor; + content: ''; + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + opacity: 0.15; + } +} + .comment-branch .comment-box { margin-bottom: vars.$m; } diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index be5a0f7c360..dbdcc06656d 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -280,6 +280,9 @@ body.tox-fullscreen, body.markdown-fullscreen { max-height: 200px; overflow-y: scroll; } +.content-comment-window-content .comment-reference-indicator-wrap { + display: none; +} .content-comment-marker { position: absolute; right: -16px; diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 5b79da4ac68..7cc84a54cf1 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -77,6 +77,11 @@ class="comment-box"> @icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}

                                          @endif + @if($comment->content_ref) + + @endif {!! $commentHtml !!}
                                          From e8f44186a8ebfac6789800211cb5a947991bf971 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sun, 27 Apr 2025 16:51:24 +0100 Subject: [PATCH 030/474] Comments: Split out page comment reference logic to own component Started support for editor view. Moved comment elements to be added relative to content area instad of specific target reference element. Added relocating on screen size change. --- resources/js/components/editor-toolbox.js | 13 + resources/js/components/index.ts | 1 + .../js/components/page-comment-reference.ts | 223 ++++++++++++++++++ resources/js/components/page-comment.ts | 149 +----------- resources/js/components/page-display.js | 3 - resources/js/services/events.ts | 11 + resources/sass/_content.scss | 1 + resources/views/comments/comment.blade.php | 11 +- 8 files changed, 256 insertions(+), 156 deletions(-) create mode 100644 resources/js/components/page-comment-reference.ts diff --git a/resources/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.js index ddb4ff39c76..95339328542 100644 --- a/resources/js/components/editor-toolbox.js +++ b/resources/js/components/editor-toolbox.js @@ -10,6 +10,10 @@ export class EditorToolbox extends Component { this.toggleButton = this.$refs.toggle; this.editorWrapEl = this.container.closest('.page-editor'); + // State + this.open = false; + this.tab = ''; + this.setupListeners(); // Set the first tab as active on load @@ -34,6 +38,8 @@ export class EditorToolbox extends Component { const isOpen = this.container.classList.contains('open'); this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); this.editorWrapEl.classList.toggle('toolbox-open', isOpen); + this.open = isOpen; + this.emitState(); } setActiveTab(tabName, openToolbox = false) { @@ -54,6 +60,13 @@ export class EditorToolbox extends Component { if (openToolbox && !this.container.classList.contains('open')) { this.toggle(); } + + this.tab = tabName; + this.emitState(); + } + + emitState() { + this.$emit('change', {tab: this.tab, open: this.open}); } } diff --git a/resources/js/components/index.ts b/resources/js/components/index.ts index 10b8025db63..63e1ad0dbf7 100644 --- a/resources/js/components/index.ts +++ b/resources/js/components/index.ts @@ -36,6 +36,7 @@ export {NewUserPassword} from './new-user-password'; export {Notification} from './notification'; export {OptionalInput} from './optional-input'; export {PageComment} from './page-comment'; +export {PageCommentReference} from './page-comment-reference'; export {PageComments} from './page-comments'; export {PageDisplay} from './page-display'; export {PageEditor} from './page-editor'; diff --git a/resources/js/components/page-comment-reference.ts b/resources/js/components/page-comment-reference.ts new file mode 100644 index 00000000000..72e3dbe480d --- /dev/null +++ b/resources/js/components/page-comment-reference.ts @@ -0,0 +1,223 @@ +import {Component} from "./component"; +import {findTargetNodeAndOffset, hashElement} from "../services/dom"; +import {el} from "../wysiwyg/utils/dom"; +import commentIcon from "@icons/comment.svg"; +import closeIcon from "@icons/close.svg"; +import {scrollAndHighlightElement} from "../services/util"; + +/** + * Track the close function for the current open marker so it can be closed + * when another is opened so we only show one marker comment thread at one time. + */ +let openMarkerClose: Function|null = null; + +export class PageCommentReference extends Component { + protected link: HTMLLinkElement; + protected reference: string; + protected markerWrap: HTMLElement|null = null; + + protected viewCommentText: string; + protected jumpToThreadText: string; + protected closeText: string; + + setup() { + this.link = this.$el as HTMLLinkElement; + this.reference = this.$opts.reference; + this.viewCommentText = this.$opts.viewCommentText; + this.jumpToThreadText = this.$opts.jumpToThreadText; + this.closeText = this.$opts.closeText; + + // Show within page display area if seen + const pageContentArea = document.querySelector('.page-content'); + if (pageContentArea instanceof HTMLElement) { + this.updateMarker(pageContentArea); + } + + // Handle editor view to show on comments toolbox view + window.addEventListener('editor-toolbox-change', (event) => { + const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab; + const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open; + if (tabName === 'comments' && isOpen) { + this.showForEditor(); + } else { + this.hideMarker(); + } + }); + } + + protected showForEditor() { + const contentWrap = document.querySelector('.editor-content-wrap'); + if (contentWrap instanceof HTMLElement) { + this.updateMarker(contentWrap); + } + + const onChange = () => { + this.hideMarker(); + setTimeout(() => { + window.$events.remove('editor-html-change', onChange); + }, 1); + }; + + window.$events.listen('editor-html-change', onChange); + } + + protected updateMarker(contentContainer: HTMLElement) { + // Reset link and existing marker + this.link.classList.remove('outdated', 'missing'); + if (this.markerWrap) { + this.markerWrap.remove(); + } + + const [refId, refHash, refRange] = this.reference.split(':'); + const refEl = document.getElementById(refId); + if (!refEl) { + this.link.classList.add('outdated', 'missing'); + return; + } + + const refCloneToAssess = refEl.cloneNode(true) as HTMLElement; + const toRemove = refCloneToAssess.querySelectorAll('[data-lexical-text]'); + refCloneToAssess.removeAttribute('style'); + for (const el of toRemove) { + el.after(...el.childNodes); + el.remove(); + } + + const actualHash = hashElement(refCloneToAssess); + if (actualHash !== refHash) { + this.link.classList.add('outdated'); + } + + const marker = el('button', { + type: 'button', + class: 'content-comment-marker', + title: this.viewCommentText, + }); + marker.innerHTML = commentIcon; + marker.addEventListener('click', event => { + this.showCommentAtMarker(marker); + }); + + this.markerWrap = el('div', { + class: 'content-comment-highlight', + }, [marker]); + + contentContainer.append(this.markerWrap); + this.positionMarker(refEl, refRange); + + this.link.href = `#${refEl.id}`; + this.link.addEventListener('click', (event: MouseEvent) => { + event.preventDefault(); + scrollAndHighlightElement(refEl); + }); + + window.addEventListener('resize', () => { + this.positionMarker(refEl, refRange); + }); + } + + protected positionMarker(targetEl: HTMLElement, range: string) { + if (!this.markerWrap) { + return; + } + + const markerParent = this.markerWrap.parentElement as HTMLElement; + const parentBounds = markerParent.getBoundingClientRect(); + let targetBounds = targetEl.getBoundingClientRect(); + const [rangeStart, rangeEnd] = range.split('-'); + if (rangeStart && rangeEnd) { + const range = new Range(); + const relStart = findTargetNodeAndOffset(targetEl, Number(rangeStart)); + const relEnd = findTargetNodeAndOffset(targetEl, Number(rangeEnd)); + if (relStart && relEnd) { + range.setStart(relStart.node, relStart.offset); + range.setEnd(relEnd.node, relEnd.offset); + targetBounds = range.getBoundingClientRect(); + } + } + + const relLeft = targetBounds.left - parentBounds.left; + const relTop = (targetBounds.top - parentBounds.top) + markerParent.scrollTop; + + this.markerWrap.style.left = `${relLeft}px`; + this.markerWrap.style.top = `${relTop}px`; + this.markerWrap.style.width = `${targetBounds.width}px`; + this.markerWrap.style.height = `${targetBounds.height}px`; + } + + protected hideMarker() { + // Hide marker and close existing marker windows + if (openMarkerClose) { + openMarkerClose(); + } + this.markerWrap?.remove(); + } + + protected showCommentAtMarker(marker: HTMLElement): void { + // Hide marker and close existing marker windows + if (openMarkerClose) { + openMarkerClose(); + } + marker.hidden = true; + + // Locate relevant comment + const commentBox = this.link.closest('.comment-box') as HTMLElement; + + // Build comment window + const readClone = (commentBox.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement; + const toRemove = readClone.querySelectorAll('.actions, form'); + for (const el of toRemove) { + el.remove(); + } + + const close = el('button', {type: 'button', title: this.closeText}); + close.innerHTML = (closeIcon as string); + const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]); + + const commentWindow = el('div', { + class: 'content-comment-window' + }, [ + el('div', { + class: 'content-comment-window-actions', + }, [jump, close]), + el('div', { + class: 'content-comment-window-content comment-container-compact comment-container-super-compact', + }, [readClone]), + ]); + + marker.parentElement?.append(commentWindow); + + // Handle interaction within window + const closeAction = () => { + commentWindow.remove(); + marker.hidden = false; + window.removeEventListener('click', windowCloseAction); + openMarkerClose = null; + }; + + const windowCloseAction = (event: MouseEvent) => { + if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) { + closeAction(); + } + }; + window.addEventListener('click', windowCloseAction); + + openMarkerClose = closeAction; + close.addEventListener('click', closeAction.bind(this)); + jump.addEventListener('click', () => { + closeAction(); + commentBox.scrollIntoView({behavior: 'smooth'}); + const highlightTarget = commentBox.querySelector('.header') as HTMLElement; + highlightTarget.classList.add('anim-highlight'); + highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) + }); + + // Position window within bounds + const commentWindowBounds = commentWindow.getBoundingClientRect(); + const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect(); + if (contentBounds && commentWindowBounds.right > contentBounds.right) { + const diff = commentWindowBounds.right - contentBounds.right; + commentWindow.style.left = `-${diff}px`; + } + } +} \ No newline at end of file diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 24964bf5cba..11ad769b1d0 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -1,28 +1,13 @@ import {Component} from './component'; -import {findTargetNodeAndOffset, getLoading, hashElement, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; -import {el} from "../wysiwyg/utils/dom"; - -import commentIcon from "@icons/comment.svg" -import closeIcon from "@icons/close.svg" -import {PageDisplay} from "./page-display"; - -/** - * Track the close function for the current open marker so it can be closed - * when another is opened so we only show one marker comment thread at one time. - */ -let openMarkerClose: Function|null = null; export class PageComment extends Component { protected commentId: string; protected commentLocalId: string; - protected commentContentRef: string; protected deletedText: string; protected updatedText: string; - protected viewCommentText: string; - protected jumpToThreadText: string; - protected closeText: string; protected wysiwygEditor: any = null; protected wysiwygLanguage: string; @@ -36,18 +21,13 @@ export class PageComment extends Component { protected deleteButton: HTMLElement; protected replyButton: HTMLElement; protected input: HTMLInputElement; - protected contentRefLink: HTMLLinkElement|null; setup() { // Options this.commentId = this.$opts.commentId; this.commentLocalId = this.$opts.commentLocalId; - this.commentContentRef = this.$opts.commentContentRef; this.deletedText = this.$opts.deletedText; this.updatedText = this.$opts.updatedText; - this.viewCommentText = this.$opts.viewCommentText; - this.jumpToThreadText = this.$opts.jumpToThreadText; - this.closeText = this.$opts.closeText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -62,10 +42,8 @@ export class PageComment extends Component { this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; this.input = this.$refs.input as HTMLInputElement; - this.contentRefLink = (this.$refs.contentRef || null) as HTMLLinkElement|null; this.setupListeners(); - this.positionForReference(); } protected setupListeners(): void { @@ -154,129 +132,4 @@ export class PageComment extends Component { this.container.append(loading); return loading; } - - protected positionForReference() { - if (!this.commentContentRef || !this.contentRefLink) { - return; - } - - const [refId, refHash, refRange] = this.commentContentRef.split(':'); - const refEl = document.getElementById(refId); - if (!refEl) { - this.contentRefLink.classList.add('outdated', 'missing'); - return; - } - - const actualHash = hashElement(refEl); - if (actualHash !== refHash) { - this.contentRefLink.classList.add('outdated'); - } - - const refElBounds = refEl.getBoundingClientRect(); - let bounds = refElBounds; - const [rangeStart, rangeEnd] = refRange.split('-'); - if (rangeStart && rangeEnd) { - const range = new Range(); - const relStart = findTargetNodeAndOffset(refEl, Number(rangeStart)); - const relEnd = findTargetNodeAndOffset(refEl, Number(rangeEnd)); - if (relStart && relEnd) { - range.setStart(relStart.node, relStart.offset); - range.setEnd(relEnd.node, relEnd.offset); - bounds = range.getBoundingClientRect(); - } - } - - const relLeft = bounds.left - refElBounds.left; - const relTop = bounds.top - refElBounds.top; - - const marker = el('button', { - type: 'button', - class: 'content-comment-marker', - title: this.viewCommentText, - }); - marker.innerHTML = commentIcon; - marker.addEventListener('click', event => { - this.showCommentAtMarker(marker); - }); - - const markerWrap = el('div', { - class: 'content-comment-highlight', - style: `left: ${relLeft}px; top: ${relTop}px; width: ${bounds.width}px; height: ${bounds.height}px;` - }, [marker]); - - refEl.style.position = 'relative'; - refEl.append(markerWrap); - - this.contentRefLink.href = `#${refEl.id}`; - this.contentRefLink.addEventListener('click', (event: MouseEvent) => { - const pageDisplayComponent = window.$components.get('page-display')[0] as PageDisplay; - event.preventDefault(); - pageDisplayComponent.goToText(refId); - }); - } - - protected showCommentAtMarker(marker: HTMLElement): void { - // Hide marker and close existing marker windows - if (openMarkerClose) { - openMarkerClose(); - } - marker.hidden = true; - - // Build comment window - const readClone = (this.container.closest('.comment-branch') as HTMLElement).cloneNode(true) as HTMLElement; - const toRemove = readClone.querySelectorAll('.actions, form'); - for (const el of toRemove) { - el.remove(); - } - - const close = el('button', {type: 'button', title: this.closeText}); - close.innerHTML = (closeIcon as string); - const jump = el('button', {type: 'button', 'data-action': 'jump'}, [this.jumpToThreadText]); - - const commentWindow = el('div', { - class: 'content-comment-window' - }, [ - el('div', { - class: 'content-comment-window-actions', - }, [jump, close]), - el('div', { - class: 'content-comment-window-content comment-container-compact comment-container-super-compact', - }, [readClone]), - ]); - - marker.parentElement?.append(commentWindow); - - // Handle interaction within window - const closeAction = () => { - commentWindow.remove(); - marker.hidden = false; - window.removeEventListener('click', windowCloseAction); - openMarkerClose = null; - }; - - const windowCloseAction = (event: MouseEvent) => { - if (!(marker.parentElement as HTMLElement).contains(event.target as HTMLElement)) { - closeAction(); - } - }; - window.addEventListener('click', windowCloseAction); - - openMarkerClose = closeAction; - close.addEventListener('click', closeAction.bind(this)); - jump.addEventListener('click', () => { - closeAction(); - this.container.scrollIntoView({behavior: 'smooth'}); - const highlightTarget = this.container.querySelector('.header') as HTMLElement; - highlightTarget.classList.add('anim-highlight'); - highlightTarget.addEventListener('animationend', () => highlightTarget.classList.remove('anim-highlight')) - }); - - // Position window within bounds - const commentWindowBounds = commentWindow.getBoundingClientRect(); - const contentBounds = document.querySelector('.page-content')?.getBoundingClientRect(); - if (contentBounds && commentWindowBounds.right > contentBounds.right) { - const diff = commentWindowBounds.right - contentBounds.right; - commentWindow.style.left = `-${diff}px`; - } - } } diff --git a/resources/js/components/page-display.js b/resources/js/components/page-display.js index 13670c4bf50..d3ac78a4ad1 100644 --- a/resources/js/components/page-display.js +++ b/resources/js/components/page-display.js @@ -57,9 +57,6 @@ export class PageDisplay extends Component { } } - /** - * @public - */ goToText(text) { const idElem = document.getElementById(text); diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts index be9fba7eca5..7dae6dc29d9 100644 --- a/resources/js/services/events.ts +++ b/resources/js/services/events.ts @@ -24,6 +24,17 @@ export class EventManager { this.listeners[eventName].push(callback); } + /** + * Remove an event listener which is using the given callback for the given event name. + */ + remove(eventName: string, callback: Function): void { + const listeners = this.listeners[eventName] || []; + const index = listeners.indexOf(callback); + if (index !== -1) { + listeners.splice(index, 1); + } + } + /** * Emit an event for public use. * Sends the event via the native DOM event handling system. diff --git a/resources/sass/_content.scss b/resources/sass/_content.scss index b0176d64ef1..aba1556a983 100644 --- a/resources/sass/_content.scss +++ b/resources/sass/_content.scss @@ -11,6 +11,7 @@ max-width: 840px; margin: 0 auto; overflow-wrap: break-word; + position: relative; .align-left { text-align: left; } diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 7cc84a54cf1..5310b2fe4c5 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -4,12 +4,8 @@
                                          @endif @if($comment->content_ref) @endif {!! $commentHtml !!} From 8bdf948743016f0461e589759130cbb50e46ab20 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 28 Apr 2025 15:37:09 +0100 Subject: [PATCH 031/474] Comments: Added archive endpoints, messages, Js actions and tests --- app/Activity/CommentRepo.php | 27 +++++++++ .../Controllers/CommentController.php | 36 +++++++++++ lang/en/common.php | 2 + lang/en/entities.php | 2 + resources/icons/archive.svg | 1 + resources/js/components/page-comment.ts | 19 +++++- resources/views/comments/comment.blade.php | 7 +++ routes/web.php | 2 + tests/Entity/CommentTest.php | 60 +++++++++++++++++++ 9 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 resources/icons/archive.svg diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index c488350ca24..866368ee649 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -53,6 +53,33 @@ public function update(Comment $comment, string $html): Comment return $comment; } + + /** + * Archive an existing comment. + */ + public function archive(Comment $comment): Comment + { + $comment->archived = true; + $comment->save(); + + ActivityService::add(ActivityType::COMMENT_UPDATE, $comment); + + return $comment; + } + + /** + * Un-archive an existing comment. + */ + public function unarchive(Comment $comment): Comment + { + $comment->archived = false; + $comment->save(); + + ActivityService::add(ActivityType::COMMENT_UPDATE, $comment); + + return $comment; + } + /** * Delete a comment from the system. */ diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 26208006784..7a290ebabc5 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -75,6 +75,42 @@ public function update(Request $request, int $commentId) ]); } + /** + * Mark a comment as archived. + */ + public function archive(int $id) + { + $comment = $this->commentRepo->getById($id); + if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->showPermissionError(); + } + + $this->commentRepo->archive($comment); + + return view('comments.comment', [ + 'comment' => $comment, + 'readOnly' => false, + ]); + } + + /** + * Unmark a comment as archived. + */ + public function unarchive(int $id) + { + $comment = $this->commentRepo->getById($id); + if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { + $this->showPermissionError(); + } + + $this->commentRepo->unarchive($comment); + + return view('comments.comment', [ + 'comment' => $comment, + 'readOnly' => false, + ]); + } + /** * Delete a comment from the system. */ diff --git a/lang/en/common.php b/lang/en/common.php index b05169bb2c4..06a9e855ce3 100644 --- a/lang/en/common.php +++ b/lang/en/common.php @@ -30,6 +30,8 @@ 'create' => 'Create', 'update' => 'Update', 'edit' => 'Edit', + 'archive' => 'Archive', + 'unarchive' => 'Un-Archive', 'sort' => 'Sort', 'move' => 'Move', 'copy' => 'Copy', diff --git a/lang/en/entities.php b/lang/en/entities.php index f9fab8ebfd3..141e75b5f35 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -402,6 +402,8 @@ 'comment_deleted_success' => 'Comment deleted', 'comment_created_success' => 'Comment added', 'comment_updated_success' => 'Comment updated', + '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' => 'Are you sure you want to delete this comment?', diff --git a/resources/icons/archive.svg b/resources/icons/archive.svg new file mode 100644 index 00000000000..90a4f35b7e9 --- /dev/null +++ b/resources/icons/archive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 11ad769b1d0..d2cbd21d1db 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -8,6 +8,7 @@ export class PageComment extends Component { protected commentLocalId: string; protected deletedText: string; protected updatedText: string; + protected archiveText: string; protected wysiwygEditor: any = null; protected wysiwygLanguage: string; @@ -20,6 +21,7 @@ export class PageComment extends Component { protected editButton: HTMLElement; protected deleteButton: HTMLElement; protected replyButton: HTMLElement; + protected archiveButton: HTMLElement; protected input: HTMLInputElement; setup() { @@ -27,7 +29,8 @@ export class PageComment extends Component { this.commentId = this.$opts.commentId; this.commentLocalId = this.$opts.commentLocalId; this.deletedText = this.$opts.deletedText; - this.updatedText = this.$opts.updatedText; + this.deletedText = this.$opts.deletedText; + this.archiveText = this.$opts.archiveText; // Editor reference and text options this.wysiwygLanguage = this.$opts.wysiwygLanguage; @@ -41,6 +44,7 @@ export class PageComment extends Component { this.editButton = this.$refs.editButton; this.deleteButton = this.$refs.deleteButton; this.replyButton = this.$refs.replyButton; + this.archiveButton = this.$refs.archiveButton; this.input = this.$refs.input as HTMLInputElement; this.setupListeners(); @@ -63,6 +67,10 @@ export class PageComment extends Component { if (this.deleteButton) { this.deleteButton.addEventListener('click', this.delete.bind(this)); } + + if (this.archiveButton) { + this.archiveButton.addEventListener('click', this.archive.bind(this)); + } } protected toggleEditMode(show: boolean) : void { @@ -126,6 +134,15 @@ export class PageComment extends Component { window.$events.success(this.deletedText); } + protected async archive(): Promise { + this.showLoading(); + const isArchived = this.archiveButton.dataset.isArchived === 'true'; + + await window.$http.put(`/comment/${this.commentId}/${isArchived ? 'unarchive' : 'archive'}`); + this.$emit('archive'); + window.$events.success(this.archiveText); + } + protected showLoading(): HTMLElement { const loading = getLoading(); loading.classList.add('px-l'); diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 5310b2fe4c5..58e057140a0 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -6,6 +6,7 @@ option:page-comment:comment-local-id="{{ $comment->local_id }}" option:page-comment:updated-text="{{ trans('entities.comment_updated_success') }}" option:page-comment:deleted-text="{{ trans('entities.comment_deleted_success') }}" + option:page-comment:archive-text="{{ $comment->archived ? trans('entities.comment_unarchive_success') : trans('entities.comment_archive_success') }}" option:page-comment:wysiwyg-language="{{ $locale->htmlLang() }}" option:page-comment:wysiwyg-text-direction="{{ $locale->htmlDirection() }}" id="comment{{$comment->local_id}}" @@ -37,6 +38,12 @@ class="comment-box"> @if(userCan('comment-create-all')) @endif + @if(userCan('comment-update', $comment) || userCan('comment-delete', $comment)) + + @endif @if(userCan('comment-update', $comment)) @endif diff --git a/routes/web.php b/routes/web.php index 8184725834c..ea3efe1ac77 100644 --- a/routes/web.php +++ b/routes/web.php @@ -179,6 +179,8 @@ // Comments Route::post('/comment/{pageId}', [ActivityControllers\CommentController::class, 'savePageComment']); + Route::put('/comment/{id}/archive', [ActivityControllers\CommentController::class, 'archive']); + Route::put('/comment/{id}/unarchive', [ActivityControllers\CommentController::class, 'unarchive']); Route::put('/comment/{id}', [ActivityControllers\CommentController::class, 'update']); Route::delete('/comment/{id}', [ActivityControllers\CommentController::class, 'destroy']); diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index 973b2b81d8d..baf0d392beb 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -106,6 +106,66 @@ public function test_comment_delete() $this->assertActivityExists(ActivityType::COMMENT_DELETE); } + public function test_comment_archive_and_unarchive() + { + $this->asAdmin(); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $comment->refresh(); + + $this->put("/comment/$comment->id/archive"); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'archived' => true, + ]); + + $this->assertActivityExists(ActivityType::COMMENT_UPDATE); + + $this->put("/comment/$comment->id/unarchive"); + + $this->assertDatabaseHas('comments', [ + 'id' => $comment->id, + 'archived' => false, + ]); + + $this->assertActivityExists(ActivityType::COMMENT_UPDATE); + } + + public function test_archive_endpoints_require_delete_or_edit_permissions() + { + $viewer = $this->users->viewer(); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $comment->refresh(); + + $endpoints = ["/comment/$comment->id/archive", "/comment/$comment->id/unarchive"]; + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $this->assertPermissionError($resp); + } + + $this->permissions->grantUserRolePermissions($viewer, ['comment-delete-all']); + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $resp->assertOk(); + } + + $this->permissions->removeUserRolePermissions($viewer, ['comment-delete-all']); + $this->permissions->grantUserRolePermissions($viewer, ['comment-update-all']); + + foreach ($endpoints as $endpoint) { + $resp = $this->actingAs($viewer)->put($endpoint); + $resp->assertOk(); + } + } + public function test_scripts_cannot_be_injected_via_comment_html() { $page = $this->entities->page(); From 099f6104d07faffdcb2bd9793b249055eb4795b2 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 28 Apr 2025 20:09:18 +0100 Subject: [PATCH 032/474] Comments: Started archive display, created mode for tree node --- app/Activity/CommentRepo.php | 10 ++++++ .../Controllers/CommentController.php | 19 +++++----- app/Activity/Tools/CommentTree.php | 35 +++++++++++++------ app/Activity/Tools/CommentTreeNode.php | 23 ++++++++++++ lang/en/entities.php | 1 + resources/js/components/page-comment.ts | 6 ++-- resources/js/components/page-comments.ts | 16 +++++++++ .../views/comments/comment-branch.blade.php | 7 ++-- resources/views/comments/comment.blade.php | 2 +- resources/views/comments/comments.blade.php | 19 +++++++--- 10 files changed, 110 insertions(+), 28 deletions(-) create mode 100644 app/Activity/Tools/CommentTreeNode.php diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index 866368ee649..bf162f68ae6 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -4,6 +4,8 @@ use BookStack\Activity\Models\Comment; use BookStack\Entities\Models\Entity; +use BookStack\Exceptions\NotifyException; +use BookStack\Exceptions\PrettyException; use BookStack\Facades\Activity as ActivityService; use BookStack\Util\HtmlDescriptionFilter; @@ -59,6 +61,10 @@ public function update(Comment $comment, string $html): Comment */ public function archive(Comment $comment): Comment { + if ($comment->parent_id) { + throw new NotifyException('Only top-level comments can be archived.'); + } + $comment->archived = true; $comment->save(); @@ -72,6 +78,10 @@ public function archive(Comment $comment): Comment */ public function unarchive(Comment $comment): Comment { + if ($comment->parent_id) { + throw new NotifyException('Only top-level comments can be un-archived.'); + } + $comment->archived = false; $comment->save(); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 7a290ebabc5..7f16c17ffce 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -3,6 +3,8 @@ namespace BookStack\Activity\Controllers; use BookStack\Activity\CommentRepo; +use BookStack\Activity\Tools\CommentTree; +use BookStack\Activity\Tools\CommentTreeNode; use BookStack\Entities\Queries\PageQueries; use BookStack\Http\Controller; use Illuminate\Http\Request; @@ -45,10 +47,7 @@ public function savePageComment(Request $request, int $pageId) return view('comments.comment-branch', [ 'readOnly' => false, - 'branch' => [ - 'comment' => $comment, - 'children' => [], - ] + 'branch' => new CommentTreeNode($comment, 0, []), ]); } @@ -81,15 +80,17 @@ public function update(Request $request, int $commentId) public function archive(int $id) { $comment = $this->commentRepo->getById($id); + $this->checkOwnablePermission('page-view', $comment->entity); if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { $this->showPermissionError(); } $this->commentRepo->archive($comment); - return view('comments.comment', [ - 'comment' => $comment, + $tree = new CommentTree($comment->entity); + return view('comments.comment-branch', [ 'readOnly' => false, + 'branch' => $tree->getCommentNodeForId($id), ]); } @@ -99,15 +100,17 @@ public function archive(int $id) public function unarchive(int $id) { $comment = $this->commentRepo->getById($id); + $this->checkOwnablePermission('page-view', $comment->entity); if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) { $this->showPermissionError(); } $this->commentRepo->unarchive($comment); - return view('comments.comment', [ - 'comment' => $comment, + $tree = new CommentTree($comment->entity); + return view('comments.comment-branch', [ 'readOnly' => false, + 'branch' => $tree->getCommentNodeForId($id), ]); } diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php index 16f6804ea42..13afc92521d 100644 --- a/app/Activity/Tools/CommentTree.php +++ b/app/Activity/Tools/CommentTree.php @@ -9,7 +9,7 @@ class CommentTree { /** * The built nested tree structure array. - * @var array{comment: Comment, depth: int, children: array}[] + * @var CommentTreeNode[] */ protected array $tree; protected array $comments; @@ -36,9 +36,25 @@ public function count(): int return count($this->comments); } - public function get(): array + public function getActive(): array { - return $this->tree; + return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived); + } + + public function getArchived(): array + { + return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived); + } + + public function getCommentNodeForId(int $commentId): ?CommentTreeNode + { + foreach ($this->tree as $node) { + if ($node->comment->id === $commentId) { + return $node; + } + } + + return null; } public function canUpdateAny(): bool @@ -54,6 +70,7 @@ public function canUpdateAny(): bool /** * @param Comment[] $comments + * @return CommentTreeNode[] */ protected function createTree(array $comments): array { @@ -77,26 +94,22 @@ protected function createTree(array $comments): array $tree = []; foreach ($childMap[0] ?? [] as $childId) { - $tree[] = $this->createTreeForId($childId, 0, $byId, $childMap); + $tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap); } return $tree; } - protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array + protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode { $childIds = $childMap[$id] ?? []; $children = []; foreach ($childIds as $childId) { - $children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap); + $children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap); } - return [ - 'comment' => $byId[$id], - 'depth' => $depth, - 'children' => $children, - ]; + return new CommentTreeNode($byId[$id], $depth, $children); } protected function loadComments(): array diff --git a/app/Activity/Tools/CommentTreeNode.php b/app/Activity/Tools/CommentTreeNode.php new file mode 100644 index 00000000000..7b280bd2d95 --- /dev/null +++ b/app/Activity/Tools/CommentTreeNode.php @@ -0,0 +1,23 @@ +comment = $comment; + $this->depth = $depth; + $this->children = $children; + } +} diff --git a/lang/en/entities.php b/lang/en/entities.php index 141e75b5f35..cda58e65bdf 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -392,6 +392,7 @@ 'comment' => 'Comment', 'comments' => 'Comments', 'comment_add' => 'Add Comment', + 'comment_archived' => ':count Archived Comment|:count Archived Comments', 'comment_placeholder' => 'Leave a comment here', 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', 'comment_save' => 'Save Comment', diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index d2cbd21d1db..82cb95f13de 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -137,10 +137,12 @@ export class PageComment extends Component { protected async archive(): Promise { this.showLoading(); const isArchived = this.archiveButton.dataset.isArchived === 'true'; + const action = isArchived ? 'unarchive' : 'archive'; - await window.$http.put(`/comment/${this.commentId}/${isArchived ? 'unarchive' : 'archive'}`); - this.$emit('archive'); + const response = await window.$http.put(`/comment/${this.commentId}/${action}`); + this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); window.$events.success(this.archiveText); + this.container.closest('.comment-branch')?.remove(); } protected showLoading(): HTMLElement { diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index 45f8d6a9f6d..083919b826f 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -9,6 +9,12 @@ export interface CommentReplyEvent extends Event { } } +export interface ArchiveEvent extends Event { + detail: { + new_thread_dom: HTMLElement; + } +} + export class PageComments extends Component { private elem: HTMLElement; @@ -17,6 +23,7 @@ export class PageComments extends Component { private commentCountBar: HTMLElement; private commentsTitle: HTMLElement; private addButtonContainer: HTMLElement; + private archiveContainer: HTMLElement; private replyToRow: HTMLElement; private formContainer: HTMLElement; private form: HTMLFormElement; @@ -43,6 +50,7 @@ export class PageComments extends Component { this.commentCountBar = this.$refs.commentCountBar; this.commentsTitle = this.$refs.commentsTitle; this.addButtonContainer = this.$refs.addButtonContainer; + this.archiveContainer = this.$refs.archiveContainer; this.replyToRow = this.$refs.replyToRow; this.formContainer = this.$refs.formContainer; this.form = this.$refs.form as HTMLFormElement; @@ -75,6 +83,14 @@ export class PageComments extends Component { this.setReply(event.detail.id, event.detail.element); }); + this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => { + this.archiveContainer.append(event.detail.new_thread_dom); + }); + + this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => { + this.container.append(event.detail.new_thread_dom) + }); + if (this.form) { this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); this.hideFormButton.addEventListener('click', this.hideForm.bind(this)); diff --git a/resources/views/comments/comment-branch.blade.php b/resources/views/comments/comment-branch.blade.php index 83fa4b5c595..658c33219c3 100644 --- a/resources/views/comments/comment-branch.blade.php +++ b/resources/views/comments/comment-branch.blade.php @@ -1,13 +1,16 @@ +{{-- +$branch CommentTreeNode +--}}
                                          - @include('comments.comment', ['comment' => $branch['comment']]) + @include('comments.comment', ['comment' => $branch->comment])
                                          - @foreach($branch['children'] as $childBranch) + @foreach($branch->children as $childBranch) @include('comments.comment-branch', ['branch' => $childBranch]) @endforeach
                                          diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index 58e057140a0..fe61bf1a4f6 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -38,7 +38,7 @@ class="comment-box"> @if(userCan('comment-create-all')) @endif - @if(userCan('comment-update', $comment) || userCan('comment-delete', $comment)) + @if(!$comment->parent_id && (userCan('comment-update', $comment) || userCan('comment-delete', $comment))) @endif
                                          -
                                          - @foreach($commentTree->get() as $branch) +
                                          + @foreach($commentTree->getActive() as $branch) @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) @endforeach
                                          @@ -27,14 +27,25 @@ class="button outline">{{ trans('entities.comment_add') }} @if(userCan('comment-create-all')) @include('comments.create') @if (!$commentTree->empty()) -
                                          +
                                          + + + + class="button outline ml-auto">{{ trans('entities.comment_add') }}
                                          @endif @endif +
                                          + @foreach($commentTree->getArchived() as $branch) + @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) + @endforeach +
                                          + @if(userCan('comment-create-all') || $commentTree->canUpdateAny()) @push('body-end') From e7dcc2dcdf9e709523705a2b9ea9ffb7dedda59b Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Wed, 30 Apr 2025 17:42:09 +0100 Subject: [PATCH 033/474] Comments: Moved to tab UI, Converted tabs component to ts --- app/Activity/Tools/CommentTree.php | 12 +++- lang/en/entities.php | 5 +- resources/js/components/page-comment.ts | 2 +- resources/js/components/page-comments.ts | 35 ++++++--- resources/js/components/{tabs.js => tabs.ts} | 25 ++++--- resources/sass/_components.scss | 7 ++ resources/views/comments/comments.blade.php | 76 +++++++++++++------- resources/views/pages/show.blade.php | 6 -- 8 files changed, 115 insertions(+), 53 deletions(-) rename resources/js/components/{tabs.js => tabs.ts} (78%) diff --git a/app/Activity/Tools/CommentTree.php b/app/Activity/Tools/CommentTree.php index 13afc92521d..a05a9d24726 100644 --- a/app/Activity/Tools/CommentTree.php +++ b/app/Activity/Tools/CommentTree.php @@ -28,7 +28,7 @@ public function enabled(): bool public function empty(): bool { - return count($this->tree) === 0; + return count($this->getActive()) === 0; } public function count(): int @@ -41,11 +41,21 @@ public function getActive(): array return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived); } + public function activeThreadCount(): int + { + return count($this->getActive()); + } + public function getArchived(): array { return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived); } + public function archivedThreadCount(): int + { + return count($this->getArchived()); + } + public function getCommentNodeForId(int $commentId): ?CommentTreeNode { foreach ($this->tree as $node) { diff --git a/lang/en/entities.php b/lang/en/entities.php index cda58e65bdf..c70658c0176 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -392,9 +392,10 @@ 'comment' => 'Comment', 'comments' => 'Comments', 'comment_add' => 'Add Comment', - 'comment_archived' => ':count Archived Comment|:count Archived Comments', + 'comment_none' => 'No comments to display', 'comment_placeholder' => 'Leave a comment here', - 'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments', + 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', + 'comment_archived_count' => ':count Archived', 'comment_save' => 'Save Comment', 'comment_new' => 'New Comment', 'comment_created' => 'commented :createDiff', diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index 82cb95f13de..12485b80728 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -140,8 +140,8 @@ export class PageComment extends Component { const action = isArchived ? 'unarchive' : 'archive'; const response = await window.$http.put(`/comment/${this.commentId}/${action}`); - this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); window.$events.success(this.archiveText); + this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); this.container.closest('.comment-branch')?.remove(); } diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index 083919b826f..2482c9dcb7d 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -1,6 +1,7 @@ import {Component} from './component'; import {getLoading, htmlToDom} from '../services/dom.ts'; import {buildForInput} from '../wysiwyg-tinymce/config'; +import {Tabs} from "./tabs"; export interface CommentReplyEvent extends Event { detail: { @@ -21,7 +22,8 @@ export class PageComments extends Component { private pageId: number; private container: HTMLElement; private commentCountBar: HTMLElement; - private commentsTitle: HTMLElement; + private activeTab: HTMLElement; + private archivedTab: HTMLElement; private addButtonContainer: HTMLElement; private archiveContainer: HTMLElement; private replyToRow: HTMLElement; @@ -37,6 +39,7 @@ export class PageComments extends Component { private wysiwygEditor: any = null; private createdText: string; private countText: string; + private archivedCountText: string; private parentId: number | null = null; private contentReference: string = ''; private formReplyText: string = ''; @@ -48,7 +51,8 @@ export class PageComments extends Component { // Element references this.container = this.$refs.commentContainer; this.commentCountBar = this.$refs.commentCountBar; - this.commentsTitle = this.$refs.commentsTitle; + this.activeTab = this.$refs.activeTab; + this.archivedTab = this.$refs.archivedTab; this.addButtonContainer = this.$refs.addButtonContainer; this.archiveContainer = this.$refs.archiveContainer; this.replyToRow = this.$refs.replyToRow; @@ -67,6 +71,7 @@ export class PageComments extends Component { // Translations this.createdText = this.$opts.createdText; this.countText = this.$opts.countText; + this.archivedCountText = this.$opts.archivedCountText; this.formReplyText = this.formReplyLink?.textContent || ''; @@ -85,10 +90,12 @@ export class PageComments extends Component { this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => { this.archiveContainer.append(event.detail.new_thread_dom); + setTimeout(() => this.updateCount(), 1); }); this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => { - this.container.append(event.detail.new_thread_dom) + this.container.append(event.detail.new_thread_dom); + setTimeout(() => this.updateCount(), 1); }); if (this.form) { @@ -136,8 +143,10 @@ export class PageComments extends Component { } protected updateCount(): void { - const count = this.getCommentCount(); - this.commentsTitle.textContent = window.$trans.choice(this.countText, count); + const activeCount = this.getActiveThreadCount(); + this.activeTab.textContent = window.$trans.choice(this.countText, activeCount); + const archivedCount = this.getArchivedThreadCount(); + this.archivedTab.textContent = window.$trans.choice(this.archivedCountText, archivedCount); } protected resetForm(): void { @@ -155,12 +164,18 @@ export class PageComments extends Component { this.addButtonContainer.toggleAttribute('hidden', true); this.formContainer.scrollIntoView({behavior: 'smooth', block: 'nearest'}); this.loadEditor(); + + // Ensure the active comments tab is displaying + const tabs = window.$components.firstOnElement(this.elem, 'tabs'); + if (tabs instanceof Tabs) { + tabs.show('comment-tab-panel-active'); + } } protected hideForm(): void { this.resetForm(); this.formContainer.toggleAttribute('hidden', true); - if (this.getCommentCount() > 0) { + if (this.getActiveThreadCount() > 0) { this.elem.append(this.addButtonContainer); } else { this.commentCountBar.append(this.addButtonContainer); @@ -198,8 +213,12 @@ export class PageComments extends Component { } } - protected getCommentCount(): number { - return this.container.querySelectorAll('[component="page-comment"]').length; + protected getActiveThreadCount(): number { + return this.container.querySelectorAll(':scope > .comment-branch:not([hidden])').length; + } + + protected getArchivedThreadCount(): number { + return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length; } protected setReply(commentLocalId, commentElement): void { diff --git a/resources/js/components/tabs.js b/resources/js/components/tabs.ts similarity index 78% rename from resources/js/components/tabs.js rename to resources/js/components/tabs.ts index f0fc058ced7..56405b8c78e 100644 --- a/resources/js/components/tabs.js +++ b/resources/js/components/tabs.ts @@ -19,18 +19,25 @@ import {Component} from './component'; */ export class Tabs extends Component { + protected container: HTMLElement; + protected tabList: HTMLElement; + protected tabs: HTMLElement[]; + protected panels: HTMLElement[]; + + protected activeUnder: number; + protected active: null|boolean = null; + setup() { this.container = this.$el; - this.tabList = this.container.querySelector('[role="tablist"]'); + this.tabList = this.container.querySelector('[role="tablist"]') as HTMLElement; this.tabs = Array.from(this.tabList.querySelectorAll('[role="tab"]')); this.panels = Array.from(this.container.querySelectorAll(':scope > [role="tabpanel"], :scope > * > [role="tabpanel"]')); this.activeUnder = this.$opts.activeUnder ? Number(this.$opts.activeUnder) : 10000; - this.active = null; this.container.addEventListener('click', event => { - const tab = event.target.closest('[role="tab"]'); - if (tab && this.tabs.includes(tab)) { - this.show(tab.getAttribute('aria-controls')); + const tab = (event.target as HTMLElement).closest('[role="tab"]'); + if (tab instanceof HTMLElement && this.tabs.includes(tab)) { + this.show(tab.getAttribute('aria-controls') || ''); } }); @@ -40,7 +47,7 @@ export class Tabs extends Component { this.updateActiveState(); } - show(sectionId) { + public show(sectionId: string): void { for (const panel of this.panels) { panel.toggleAttribute('hidden', panel.id !== sectionId); } @@ -54,7 +61,7 @@ export class Tabs extends Component { this.$emit('change', {showing: sectionId}); } - updateActiveState() { + protected updateActiveState(): void { const active = window.innerWidth < this.activeUnder; if (active === this.active) { return; @@ -69,13 +76,13 @@ export class Tabs extends Component { this.active = active; } - activate() { + protected activate(): void { const panelToShow = this.panels.find(p => !p.hasAttribute('hidden')) || this.panels[0]; this.show(panelToShow.id); this.tabList.toggleAttribute('hidden', false); } - deactivate() { + protected deactivate(): void { for (const panel of this.panels) { panel.removeAttribute('hidden'); } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 5486d611288..d25fab299d9 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -802,6 +802,13 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { display: block; } +.comment-container .empty-state { + display: none; +} +.comment-container:not(:has([component="page-comment"])) .empty-state { + display: block; +} + .comment-container-compact .comment-box { margin-bottom: vars.$xs; .meta { diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 06e96cad689..882cfdf45ee 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -1,49 +1,73 @@ -
                                          -
                                          -
                                          {{ trans_choice('entities.comment_count', $commentTree->count(), ['count' => $commentTree->count()]) }}
                                          +
                                          +
                                          + + +
                                          @if ($commentTree->empty() && userCan('comment-create-all')) -
                                          +
                                          + class="button outline mb-m">{{ trans('entities.comment_add') }}
                                          @endif
                                          -
                                          - @foreach($commentTree->getActive() as $branch) - @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) - @endforeach -
                                          +
                                          +
                                          + @foreach($commentTree->getActive() as $branch) + @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) + @endforeach +
                                          - @if(userCan('comment-create-all')) - @include('comments.create') - @if (!$commentTree->empty()) -
                                          - - +

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

                                          - -
                                          + @if(userCan('comment-create-all')) + @include('comments.create') + @if (!$commentTree->empty()) +
                                          + +
                                          + @endif @endif - @endif +
                                          -
                                          + @if(userCan('comment-create-all') || $commentTree->canUpdateAny()) diff --git a/resources/views/pages/show.blade.php b/resources/views/pages/show.blade.php index e3a31dd5ebf..137d43bdb1a 100644 --- a/resources/views/pages/show.blade.php +++ b/resources/views/pages/show.blade.php @@ -28,12 +28,6 @@ class="page-content clearfix"> @include('entities.sibling-navigation', ['next' => $next, 'previous' => $previous]) @if ($commentTree->enabled()) - @if(($previous || $next)) - - @endif - @endif {!! $commentHtml !!} diff --git a/resources/views/comments/comments.blade.php b/resources/views/comments/comments.blade.php index 882cfdf45ee..51c08d69a85 100644 --- a/resources/views/comments/comments.blade.php +++ b/resources/views/comments/comments.blade.php @@ -36,7 +36,7 @@ class="button outline mb-m">{{ trans('entities.comment_add') }} tabindex="0" role="tabpanel" aria-labelledby="comment-tab-active" - class="comment-container"> + class="comment-container no-outline">
                                          @foreach($commentTree->getActive() as $branch) @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) @@ -63,7 +63,7 @@ class="button outline ml-auto">{{ trans('entities.comment_add') }} role="tabpanel" aria-labelledby="comment-tab-archived" hidden="hidden" - class="comment-container"> + class="comment-container no-outline"> @foreach($commentTree->getArchived() as $branch) @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => false]) @endforeach diff --git a/resources/views/comments/create.blade.php b/resources/views/comments/create.blade.php index 417f0c60602..134ed516425 100644 --- a/resources/views/comments/create.blade.php +++ b/resources/views/comments/create.blade.php @@ -12,6 +12,16 @@
                                          +
                                          From 1262083fcf48f522394d3a0cd7001be3e66eec53 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Sat, 3 May 2025 20:30:50 +0100 Subject: [PATCH 036/474] Images: Changed how new image permissions are set Removed default public visibility for images at the driver level, leaving only doing this as a specific action in the logic. Added try/catch around permission setting so that permission-incompatible environments won't fatally fail, but instead log a warning. Tested via a google cloud storage bucket FUSE mount, mounted under another user but with open 777 permissions. Related to #5269 --- app/Config/filesystems.php | 2 -- app/Uploads/ImageStorageDisk.php | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/Config/filesystems.php b/app/Config/filesystems.php index 08ae7b04776..ab507a2f8c6 100644 --- a/app/Config/filesystems.php +++ b/app/Config/filesystems.php @@ -32,7 +32,6 @@ 'local' => [ 'driver' => 'local', 'root' => public_path(), - 'visibility' => 'public', 'serve' => false, 'throw' => true, ], @@ -47,7 +46,6 @@ 'local_secure_images' => [ 'driver' => 'local', 'root' => storage_path('uploads/images/'), - 'visibility' => 'public', 'serve' => false, 'throw' => true, ], diff --git a/app/Uploads/ImageStorageDisk.php b/app/Uploads/ImageStorageDisk.php index da8bacb3447..8e364831f75 100644 --- a/app/Uploads/ImageStorageDisk.php +++ b/app/Uploads/ImageStorageDisk.php @@ -5,6 +5,8 @@ use BookStack\Util\FilePathNormalizer; use Illuminate\Contracts\Filesystem\Filesystem; use Illuminate\Filesystem\FilesystemAdapter; +use Illuminate\Support\Facades\Log; +use League\Flysystem\UnableToSetVisibility; use Symfony\Component\HttpFoundation\StreamedResponse; class ImageStorageDisk @@ -74,12 +76,19 @@ public function put(string $path, string $data, bool $makePublic = false): void $path = $this->adjustPathForDisk($path); $this->filesystem->put($path, $data); - // Set visibility when a non-AWS-s3, s3-like storage option is in use. - // Done since this call can break s3-like services but desired for other image stores. - // Attempting to set ACL during above put request requires different permissions - // hence would technically be a breaking change for actual s3 usage. + // Set public visibility to ensure public access on S3, or that the file is accessible + // to other processes (like web-servers) for local file storage options. + // We avoid attempting this for (non-AWS) s3-like systems (even in a try-catch) as + // we've always avoided setting permissions for s3-like due to potential issues, + // with docs advising setting pre-configured permissions instead. + // We also don't do this as the default filesystem/driver level as that can technically + // require different ACLs for S3, and this provides us more logical control. if ($makePublic && !$this->isS3Like()) { - $this->filesystem->setVisibility($path, 'public'); + try { + $this->filesystem->setVisibility($path, 'public'); + } catch (UnableToSetVisibility $e) { + Log::warning("Unable to set visibility for image upload with relative path: {$path}"); + } } } From 8821844c4aec3aa3e02d315829ebfccb8138d68a Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 May 2025 12:21:32 +0100 Subject: [PATCH 037/474] Exports: Fixed CSS file BOM mark breaking CSS variables in exports Adds a dummy CSS rule to break as the first rule, instead of our :root variables. Fixes #5576 --- resources/sass/_vars.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/resources/sass/_vars.scss b/resources/sass/_vars.scss index 26539656e7f..1a9590e730f 100644 --- a/resources/sass/_vars.scss +++ b/resources/sass/_vars.scss @@ -1,5 +1,12 @@ // Variables -/////////////// +// A range of SASS and plain CSS variables used in BookStack +//////////////////////////////////////////////////////////// + +// This is simply placed here at the top to prevent parsing/rendering issues +// where built CSS files may have a starting BOM mark which can break the first css rule +// when used inline, so this is here as a sacrifice in such scenarios instead of an important rule. +// Related: https://github.com/sass/dart-sass/issues/472 +.dummy-style {color: red;} // Screen breakpoints $bp-xxxl: 1700px; From 4f36cdd75793771abb8aed877fafc692aa269f82 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 May 2025 12:24:12 +0100 Subject: [PATCH 038/474] Updated translations with latest Crowdin changes (#5566) --- lang/ar/activities.php | 2 +- lang/ar/auth.php | 20 ++--- lang/ar/errors.php | 2 +- lang/ar/passwords.php | 8 +- lang/ar/settings.php | 6 +- lang/ar/validation.php | 2 +- lang/de/activities.php | 14 ++-- lang/de/editor.php | 4 +- lang/de/entities.php | 28 +++---- lang/de/passwords.php | 2 +- lang/de/preferences.php | 8 +- lang/de/settings.php | 74 +++++++++--------- lang/de/validation.php | 18 ++--- lang/de_informal/activities.php | 50 ++++++------ lang/de_informal/auth.php | 4 +- lang/de_informal/editor.php | 6 +- lang/de_informal/entities.php | 26 +++---- lang/de_informal/passwords.php | 2 +- lang/de_informal/preferences.php | 6 +- lang/de_informal/settings.php | 76 +++++++++--------- lang/de_informal/validation.php | 16 ++-- lang/et/entities.php | 2 +- lang/et/errors.php | 4 +- lang/id/settings.php | 130 +++++++++++++++---------------- lang/id/validation.php | 8 +- lang/pt_BR/editor.php | 4 +- lang/pt_BR/entities.php | 6 +- lang/pt_BR/errors.php | 2 +- lang/sv/activities.php | 24 +++--- lang/sv/common.php | 2 +- lang/sv/editor.php | 8 +- lang/sv/entities.php | 112 +++++++++++++------------- lang/sv/errors.php | 20 ++--- lang/sv/settings.php | 20 ++--- lang/uk/activities.php | 24 +++--- lang/uk/editor.php | 8 +- lang/uk/entities.php | 48 ++++++------ lang/uk/errors.php | 20 ++--- lang/uk/settings.php | 58 +++++++------- lang/uk/validation.php | 8 +- lang/zh_CN/settings.php | 50 ++++++------ 41 files changed, 466 insertions(+), 466 deletions(-) diff --git a/lang/ar/activities.php b/lang/ar/activities.php index bf5d4411fed..69da1213846 100644 --- a/lang/ar/activities.php +++ b/lang/ar/activities.php @@ -64,7 +64,7 @@ // Auth 'auth_login' => 'تم تسجيل الدخول', 'auth_register' => 'سجل كمستخدم جديد', - 'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة المرور', + 'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة السر', 'auth_password_reset_update' => 'إعادة تعيين كلمة مرور المستخدم', 'mfa_setup_method' => 'طريقة المصادقة متعددة العوامل المُهيأة', 'mfa_setup_method_notification' => 'تم إعداد المصادقة متعددة العوامل بنجاح', diff --git a/lang/ar/auth.php b/lang/ar/auth.php index 3890589cd13..fb6f3d1cf72 100644 --- a/lang/ar/auth.php +++ b/lang/ar/auth.php @@ -26,7 +26,7 @@ 'remember_me' => 'تذكرني', 'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.', 'create_account' => 'إنشاء حساب', - 'already_have_account' => 'لديك حساب بالفعل؟', + 'already_have_account' => 'لديك حساب مسبقاً؟', 'dont_have_account' => 'ليس لديك حساب؟', 'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي', 'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي', @@ -45,7 +45,7 @@ // Password Reset 'reset_password' => 'استعادة كلمة السر', - 'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.', + 'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة السر.', 'reset_password_send_button' => 'أرسل رابط الاستعادة', 'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة السر إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.', 'reset_password_success' => 'تمت استعادة كلمة السر بنجاح.', @@ -60,7 +60,7 @@ 'email_confirm_action' => 'تأكيد البريد الإلكتروني', 'email_confirm_send_error' => 'تأكيد البريد الإلكتروني مطلوب ولكن النظام لم يستطع إرسال الرسالة. تواصل مع مشرف النظام للتأكد من إعدادات البريد.', 'email_confirm_success' => 'تم تأكيد بريدك الإلكتروني! يمكنك الآن تسجيل الدخول باستخدام عنوان البريد الإلكتروني هذا.', - 'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد. الرجاء مراجعة صندوق الوارد', + 'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد، الرجاء مراجعة صندوق الوارد.', 'email_confirm_thanks' => 'شكرا للتأكيد!', 'email_confirm_thanks_desc' => 'الرجاء الانتظار لحظة بينما يتم التعامل مع التأكيد الخاص بك. إذا لم يتم إعادة توجيهك بعد 3 ثوان اضغط على الرابط "المتابعة" أدناه للمتابعة.', @@ -72,24 +72,24 @@ // User Invite 'user_invite_email_subject' => 'تمت دعوتك للانضمام إلى صفحة الحالة الخاصة بـ :app_name!', - 'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على %site%.', - 'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة مرور الحساب والحصول على الوصول:', + '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!', + 'user_invite_success_login' => 'تم تأكيد كلمة السر. يمكنك الآن تسجيل الدخول باستخدام كلمة السر المحددة للوصول إلى :appName !', // Multi-factor Authentication 'mfa_setup' => 'إعداد المصادقة متعددة العوامل', 'mfa_setup_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.', 'mfa_setup_configured' => 'تم إعداده مسبقاً', 'mfa_setup_reconfigure' => 'إعادة التكوين', - 'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟', - 'mfa_setup_action' => 'إعداد (تنصيب)', + 'mfa_setup_remove_confirmation' => 'متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟', + 'mfa_setup_action' => 'إعداد', 'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.', 'mfa_option_totp_title' => 'تطبيق الجوال', - 'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق جوال يدعم كلمة المرور المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft 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' => 'تأكيد وتمكين', @@ -98,7 +98,7 @@ '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_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.', 'mfa_gen_totp_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.', 'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد', 'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:', diff --git a/lang/ar/errors.php b/lang/ar/errors.php index b50c95c232f..4c6325cb3a1 100644 --- a/lang/ar/errors.php +++ b/lang/ar/errors.php @@ -16,7 +16,7 @@ 'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.', 'email_confirmation_awaiting' => 'عنوان البريد الإلكتروني للحساب قيد الاستخدام يحتاج إلى تأكيد', 'ldap_fail_anonymous' => 'فشل الوصول إلى LDAP باستخدام الربط المجهول', - 'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و password المعطاة', + 'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و كلمة السر المعطاة', 'ldap_extension_not_installed' => 'لم يتم تثبيت إضافة LDAP PHP', 'ldap_cannot_connect' => 'لا يمكن الاتصال بخادم ldap, فشل الاتصال المبدئي', 'saml_already_logged_in' => 'تم تسجيل الدخول بالفعل', diff --git a/lang/ar/passwords.php b/lang/ar/passwords.php index 4ab447d5d56..1e8a10ccc89 100644 --- a/lang/ar/passwords.php +++ b/lang/ar/passwords.php @@ -6,10 +6,10 @@ */ return [ - 'password' => 'يجب أن تتكون كلمة المرور من ستة أحرف على الأقل وأن تطابق التأكيد.', + 'password' => 'يجب أن تتكون كلمة السر من ستة أحرف على الأقل وأن تطابق التأكيد.', 'user' => "لم يتم العثور على مستخدم بعنوان البريد الإلكتروني المعطى.", - 'token' => 'رمز إعادة تعيين كلمة المرور غير صالح لعنوان هذا البريد الإلكتروني.', - 'sent' => 'تم إرسال رابط تجديد كلمة المرور إلى بريدكم الإلكتروني!', - 'reset' => 'تم تجديد كلمة المرور الخاصة بكم!', + 'token' => 'رمز إعادة تعيين كلمة السر غير صالح لعنوان هذا البريد الإلكتروني.', + 'sent' => 'تم إرسال رابط تجديد كلمة السر إلى بريدكم الإلكتروني!', + 'reset' => 'تم تجديد كلمة السر الخاصة بكم!', ]; diff --git a/lang/ar/settings.php b/lang/ar/settings.php index b7c8ad411fb..8e615fc9cb9 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -220,12 +220,12 @@ 'users_role' => 'أدوار المستخدمين', 'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.', 'users_password' => 'كلمة مرور المستخدم', - 'users_password_desc' => 'عيّن كلمة مرور لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.', - 'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة المرور الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.', + 'users_password_desc' => 'عيّن كلمة سر لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.', + 'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة السر الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.', 'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم', 'users_external_auth_id' => 'ربط الحساب بمواقع التواصل', 'users_external_auth_id_desc' => 'عند استخدام نظام مصادقة خارجي (مثل SAML2 أو OIDC أو LDAP)، يكون هذا هو المعرف الذي يربط مستخدم بوكستاك -BookStack- بحساب نظام المصادقة. يمكنك تجاهل هذا الحقل عند استخدام المصادقة الافتراضية عبر البريد الإلكتروني.', - 'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة المرور لهذا المستخدم.', + 'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة السر لهذا المستخدم.', 'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.', 'users_delete' => 'حذف المستخدم', 'users_delete_named' => 'حذف المستخدم :userName', diff --git a/lang/ar/validation.php b/lang/ar/validation.php index 32a376865e9..c27770fe3a9 100644 --- a/lang/ar/validation.php +++ b/lang/ar/validation.php @@ -113,7 +113,7 @@ // Custom validation lines 'custom' => [ 'password-confirm' => [ - 'required_with' => 'يجب تأكيد كلمة المرور', + 'required_with' => 'يجب تأكيد كلمة السر', ], ], diff --git a/lang/de/activities.php b/lang/de/activities.php index cfb21a4c97e..025cbaeffab 100644 --- a/lang/de/activities.php +++ b/lang/de/activities.php @@ -50,7 +50,7 @@ 'bookshelf_delete_notification' => 'Regal erfolgreich gelöscht', // Revisions - 'revision_restore' => 'widerherstellte Revision', + 'revision_restore' => 'stellte Revision wieder her:', 'revision_delete' => 'löschte Revision', 'revision_delete_notification' => 'Revision erfolgreich gelöscht', @@ -128,12 +128,12 @@ 'comment_delete' => 'Kommentar gelöscht', // Sort Rules - 'sort_rule_create' => 'Sortierregel erstellt', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'hat eine Sortierregel erstellt', + 'sort_rule_create_notification' => 'Sortierregel erfolgreich angelegt', + 'sort_rule_update' => 'hat eine Sortierregel aktualisiert', + 'sort_rule_update_notification' => 'Sortierregel erfolgreich aktualisiert', + 'sort_rule_delete' => 'hat eine Sortierregel gelöscht', + 'sort_rule_delete_notification' => 'Sortierregel erfolgreich gelöscht', // Other 'permissions_update' => 'hat die Berechtigungen aktualisiert', diff --git a/lang/de/editor.php b/lang/de/editor.php index 179c667844e..41ec74e52e1 100644 --- a/lang/de/editor.php +++ b/lang/de/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Abbrechen', 'save' => 'Speichern', 'close' => 'Schließen', - 'apply' => 'Apply', + 'apply' => 'Übernehmen', 'undo' => 'Rückgängig', 'redo' => 'Wiederholen', 'left' => 'Links', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => 'Anzuzeigender Text', 'title' => 'Titel', - 'browse_links' => 'Browse links', + 'browse_links' => 'Links durchsuchen', 'open_link' => 'Link öffnen', 'open_link_in' => 'Link öffnen in...', 'open_link_current' => 'Aktuelles Fenster', diff --git a/lang/de/entities.php b/lang/de/entities.php index 9377c7efcdc..767696eb35f 100644 --- a/lang/de/entities.php +++ b/lang/de/entities.php @@ -40,8 +40,8 @@ 'export_text' => 'Textdatei', 'export_md' => 'Markdown-Datei', 'export_zip' => 'Portable ZIP', - 'default_template' => 'Standard Seitenvorlage', - 'default_template_explain' => 'Zuweisen einer Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt wurden. Beachten Sie, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Zugriff auf die ausgewählte Vorlagen-Seite hat.', + 'default_template' => 'Standard-Seitenvorlage', + 'default_template_explain' => 'Bestimmen Sie eine Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt werden. Beachten Sie, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Lesezugriff auf die ausgewählte Vorlagen-Seite hat.', 'default_template_select' => 'Wählen Sie eine Seitenvorlage', 'import' => 'Import', 'import_validate' => 'Import validieren', @@ -87,7 +87,7 @@ 'search_terms' => 'Suchbegriffe', 'search_content_type' => 'Inhaltstyp', 'search_exact_matches' => 'Exakte Treffer', - 'search_tags' => 'Nach Schlagwort suchen', + 'search_tags' => 'Schlagwort-Suchen', 'search_options' => 'Optionen', 'search_viewed_by_me' => 'Schon von mir angesehen', 'search_not_viewed_by_me' => 'Noch nicht von mir angesehen', @@ -166,9 +166,9 @@ 'books_search_this' => 'Dieses Buch durchsuchen', 'books_navigation' => 'Buchnavigation', 'books_sort' => 'Buchinhalte sortieren', - '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' => '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_named' => 'Buch ":bookName" sortieren', 'books_sort_name' => 'Sortieren nach Namen', 'books_sort_created' => 'Sortieren nach Erstellungsdatum', @@ -243,12 +243,12 @@ 'pages_edit_delete_draft' => 'Entwurf löschen', 'pages_edit_delete_draft_confirm' => 'Sind Sie sicher, dass Sie Ihren Entwurf löschen möchten? Alle Ihre Änderungen seit dem letzten vollständigen Speichern gehen verloren und der Editor wird mit dem letzten Speicherzustand aktualisiert, der kein Entwurf ist.', 'pages_edit_discard_draft' => 'Entwurf verwerfen', - 'pages_edit_switch_to_markdown' => 'Zum Markdown Editor wechseln', - 'pages_edit_switch_to_markdown_clean' => '(gesäuberter Output)', - 'pages_edit_switch_to_markdown_stable' => '(html beibehalten)', - 'pages_edit_switch_to_wysiwyg' => 'Wechseln Sie zum WYSIWYG-Editor', - 'pages_edit_switch_to_new_wysiwyg' => 'Zu neuem WYSIWYG wechseln', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)', + 'pages_edit_switch_to_markdown' => 'Zum Markdown-Editor wechseln', + 'pages_edit_switch_to_markdown_clean' => '(Gesäuberter Inhalt)', + 'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)', + 'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln', + 'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha-Testphase)', 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen', 'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer Änderungen ein', 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben', @@ -332,7 +332,7 @@ 'tag' => 'Schlagwort', 'tags' => 'Schlagwörter', 'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.', - 'tag_name' => 'Schlagwort Name', + 'tag_name' => 'Schlagwortname', 'tag_value' => 'Inhalt (Optional)', 'tags_explain' => "Fügen Sie Schlagwörter hinzu, um Ihren Inhalt zu kategorisieren.\nSie können einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", 'tags_add' => 'Weiteres Schlagwort hinzufügen', @@ -392,7 +392,7 @@ 'comment' => 'Kommentar', 'comments' => 'Kommentare', 'comment_add' => 'Kommentieren', - 'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein (Markdown unterstützt)', + 'comment_placeholder' => 'Geben Sie hier Ihre Kommentare ein', 'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare', 'comment_save' => 'Kommentar speichern', 'comment_new' => 'Neuer Kommentar', diff --git a/lang/de/passwords.php b/lang/de/passwords.php index 5896297e08b..c93ccb4dd84 100644 --- a/lang/de/passwords.php +++ b/lang/de/passwords.php @@ -8,7 +8,7 @@ 'password' => 'Passwörter müssen aus mindestens acht Zeichen bestehen und mit der eingegebenen Wiederholung übereinstimmen.', 'user' => "Es wurde kein Benutzer mit dieser E-Mail-Adresse gefunden.", - 'token' => 'Der Link zum Zurücksetzen Ihres Passworts ist entweder ungültig oder abgelaufen.', + 'token' => 'Der Token zum Zurücksetzen des Passworts ist für diese E-Mail-Adresse ungültig.', 'sent' => 'Der Link zum Zurücksetzen Ihres Passwortes wurde Ihnen per E-Mail zugesendet.', 'reset' => 'Ihr Passwort wurde zurückgesetzt!', diff --git a/lang/de/preferences.php b/lang/de/preferences.php index 291e0f9f66d..26f0b05dacc 100644 --- a/lang/de/preferences.php +++ b/lang/de/preferences.php @@ -35,12 +35,12 @@ 'auth_change_password_success' => 'Das Passwort wurde aktualisiert!', 'profile' => 'Profildetails', - 'profile_desc' => 'Verwalten Sie die Details Ihres Kontos welche Sie gegenüber anderen Benutzern repräsentiert, zusätzlich zu den Details die für die Kommunikation und Personalisierung des Systems genutzt werden.', - 'profile_view_public' => 'Öffentliches Profil zeigen', - 'profile_name_desc' => 'Konfigurieren Sie Ihren Anzeigenamen, der durch die Aktivität, die Sie ausführen, für andere Benutzer und Ihre eigenen Inhalte im System sichtbar ist.', + 'profile_desc' => 'Verwalten Sie die Details Ihres Kontos, welches Sie gegenüber anderen Benutzern repräsentiert, zusätzlich zu den Details, die für die Kommunikation und Personalisierung des Systems genutzt werden.', + 'profile_view_public' => 'Öffentliches Profil anzeigen', + 'profile_name_desc' => 'Konfigurieren Sie Ihren Anzeigenamen, der durch die Aktivität, die Sie ausführen, und die Ihnen gehörenden Inhalte für andere Benutzer sichtbar ist.', 'profile_email_desc' => 'Diese E-Mail wird für Benachrichtigungen und, je nach aktiver Systemauthentifizierung, den Systemzugriff verwendet.', 'profile_email_no_permission' => 'Leider haben Sie nicht die Berechtigung, Ihre E-Mail-Adresse zu ändern. Wenn Sie diese ändern möchten, wenden Sie sich bitte an Ihren Administrator.', - 'profile_avatar_desc' => 'Wählen Sie ein Bild, dass anderen im System angezeigt wird, um Sie zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.', + 'profile_avatar_desc' => 'Wählen Sie ein Bild aus, das anderen im System angezeigt wird, um Sie zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.', 'profile_admin_options' => 'Administratoroptionen', 'profile_admin_options_desc' => 'Weitere Administrator-Optionen wie zum Beispiel die Verwaltung von Rollenzuweisungen für Ihr Benutzerkonto finden Sie im Bereich "Einstellungen > Benutzer" der Anwendung.', diff --git a/lang/de/settings.php b/lang/de/settings.php index c83d0ef3755..a6b6482e3ab 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -19,17 +19,17 @@ 'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.', 'app_name_header' => 'Anwendungsname im Header anzeigen?', 'app_public_access' => 'Öffentlicher Zugriff', - 'app_public_access_desc' => 'Wenn Sie diese Option aktivieren können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.', + 'app_public_access_desc' => 'Wenn Sie diese Option aktivieren, können Besucher, die nicht angemeldet sind, auf Inhalte in Ihrer BookStack-Instanz zugreifen.', 'app_public_access_desc_guest' => 'Der Zugang für öffentliche Besucher kann über den Benutzer "Guest" gesteuert werden.', 'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben', 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', 'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?', - 'app_secure_images_toggle' => 'Aktiviere höhere Sicherheit für Bild-Uploads', + 'app_secure_images_toggle' => 'Höhere Sicherheit für Bild-Uploads aktivieren', 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratende, Zeichenketten zu Bild-URLs hinzu. Stellen Sie sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', 'app_default_editor' => 'Standard-Seiten-Editor', 'app_default_editor_desc' => 'Wählen Sie aus, welcher Editor standardmäßig beim Bearbeiten neuer Seiten verwendet wird. Dies kann auf einer Seitenebene überschrieben werden, wenn es die Berechtigungen erlauben.', - 'app_custom_html' => 'Benutzerdefinierter HTML Inhalt', - 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics-Code hinzuzufügen.', + 'app_custom_html' => 'Benutzerdefinierter HTML-Head-Inhalt', + 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der -Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS-Styles anzupassen oder Analytics-Code hinzuzufügen.', 'app_custom_html_disabled_notice' => 'Benutzerdefinierte HTML-Kopfzeileninhalte sind auf dieser Einstellungsseite deaktiviert, um sicherzustellen, dass alle Änderungen rückgängig gemacht werden können.', 'app_logo' => 'Anwendungslogo', 'app_logo_desc' => 'Dies wird unter anderem in der Kopfzeile der Anwendung verwendet. Dieses Bild sollte 86px hoch sein. Große Bilder werden herunterskaliert.', @@ -76,34 +76,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'Sortierung', + 'sorting_book_default' => 'Standard-Buchsortierung', + 'sorting_book_default_desc' => 'Wählen Sie 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', + 'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.', + 'sort_rule_assigned_to_x_books' => ':count Buch zugewiesen|:count Büchern zugewiesen', + 'sort_rule_create' => 'Sortierregel erstellen', + 'sort_rule_edit' => 'Sortierregel bearbeiten', + 'sort_rule_delete' => 'Sortierregel löschen', + 'sort_rule_delete_desc' => 'Diese Sortierregel aus dem System entfernen. Bücher mit dieser Sortierung werden auf manuelle Sortierung zurückgesetzt.', + 'sort_rule_delete_warn_books' => 'Diese Sortierregel wird derzeit in :count Bücher(n) verwendet. Sind Sie sicher, dass Sie dies löschen möchten?', + 'sort_rule_delete_warn_default' => 'Diese Sortierregel wird derzeit als Standard für Bücher verwendet. Sind Sie sicher, dass Sie dies löschen möchten?', + 'sort_rule_details' => 'Sortierregel-Details', + 'sort_rule_details_desc' => 'Legen Sie einen Namen für diese Sortierregel fest, der in Listen erscheint, wenn Benutzer eine Sortierung auswählen.', + 'sort_rule_operations' => 'Sortierungs-Aktionen', + 'sort_rule_operations_desc' => 'Konfigurieren Sie die durchzuführenden Sortieraktionen durch Verschieben von der Liste der verfügbaren Aktionen. Bei der Verwendung werden die Aktionen von oben nach unten angewendet. Alle hier vorgenommenen Änderungen werden beim Speichern auf alle zugewiesenen Bücher angewendet.', + 'sort_rule_available_operations' => 'Verfügbare Aktionen', + 'sort_rule_available_operations_empty' => 'Keine verbleibenden Aktionen', + 'sort_rule_configured_operations' => 'Konfigurierte Aktionen', + 'sort_rule_configured_operations_empty' => 'Aktionen aus der Liste "Verfügbare Operationen" ziehen/hinzufügen', + 'sort_rule_op_asc' => '(Aufst.)', + 'sort_rule_op_desc' => '(Abst.)', + 'sort_rule_op_name' => 'Name - Alphabetisch', + 'sort_rule_op_name_numeric' => 'Name - Numerisch', + 'sort_rule_op_created_date' => 'Erstellungsdatum', + 'sort_rule_op_updated_date' => 'Aktualisierungsdatum', + 'sort_rule_op_chapters_first' => 'Kapitel zuerst', + 'sort_rule_op_chapters_last' => 'Kapitel zuletzt', // Maintenance settings 'maint' => 'Wartung', @@ -114,11 +114,11 @@ 'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchten Sie diese Bilder löschen?', 'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.', 'maint_image_cleanup_nothing_found' => 'Keine unbenutzen Bilder gefunden. Nichts zu löschen!', - 'maint_send_test_email' => 'Test Email versenden', - 'maint_send_test_email_desc' => 'Dies sendet eine Test E-Mail an Ihre in Ihrem Profil angegebene E-Mail-Adresse.', - 'maint_send_test_email_run' => 'Sende eine Test E-Mail', + 'maint_send_test_email' => 'Eine Test-E-Mail versenden', + 'maint_send_test_email_desc' => 'Dies sendet eine Test-E-Mail an Ihre in Ihrem Profil angegebene E-Mail-Adresse.', + 'maint_send_test_email_run' => 'Test-E-Mail senden', 'maint_send_test_email_success' => 'E-Mail wurde an :address gesendet', - 'maint_send_test_email_mail_subject' => 'Test E-Mail', + 'maint_send_test_email_mail_subject' => 'Test-E-Mail', 'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!', 'maint_send_test_email_mail_text' => 'Glückwunsch! Da Sie diese E-Mail Benachrichtigung erhalten haben, scheinen Ihre E-Mail-Einstellungen korrekt konfiguriert zu sein.', 'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Gegenstände im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.', @@ -159,7 +159,7 @@ 'audit_table_user' => 'Benutzer', 'audit_table_event' => 'Ereignis', 'audit_table_related' => 'Verknüpftes Element oder Detail', - 'audit_table_ip' => 'IP Adresse', + 'audit_table_ip' => 'IP-Adresse', 'audit_table_date' => 'Aktivitätsdatum', 'audit_date_from' => 'Zeitraum von', 'audit_date_to' => 'Zeitraum bis', diff --git a/lang/de/validation.php b/lang/de/validation.php index 10961eb7fb2..2ffad05298e 100644 --- a/lang/de/validation.php +++ b/lang/de/validation.php @@ -9,7 +9,7 @@ // Standard laravel validation lines 'accepted' => ':attribute muss akzeptiert werden.', - 'active_url' => ':attribute ist keine valide URL.', + 'active_url' => ':attribute ist keine gültige URL.', 'after' => ':attribute muss ein Datum nach :date sein.', 'alpha' => ':attribute kann nur Buchstaben enthalten.', 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', @@ -25,12 +25,12 @@ ], 'boolean' => ':attribute Feld muss wahr oder falsch sein.', 'confirmed' => ':attribute stimmt nicht überein.', - 'date' => ':attribute ist kein valides Datum.', + 'date' => ':attribute ist kein gültiges Datum.', 'date_format' => ':attribute entspricht nicht dem Format :format.', 'different' => ':attribute und :other müssen unterschiedlich sein.', 'digits' => ':attribute muss :digits Stellen haben.', 'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.', - 'email' => ':attribute muss eine valide E-Mail-Adresse sein.', + 'email' => ':attribute muss eine gültige E-Mail-Adresse sein.', 'ends_with' => ':attribute muss mit einem der folgenden Werte: :values enden', 'file' => ':attribute muss als gültige Datei angegeben werden.', 'filled' => ':attribute ist erforderlich.', @@ -51,8 +51,8 @@ 'image_extension' => ':attribute muss eine gültige und unterstützte Bild-Dateiendung haben.', 'in' => ':attribute ist ungültig.', 'integer' => ':attribute muss eine Zahl sein.', - 'ip' => ':attribute muss eine valide IP-Adresse sein.', - 'ipv4' => ':attribute muss eine gültige IPv4 Adresse sein.', + 'ip' => ':attribute muss eine gültige IP-Adresse sein.', + 'ipv4' => ':attribute muss eine gültige IPv4-Adresse sein.', 'ipv6' => ':attribute muss eine gültige IPv6-Adresse sein.', 'json' => 'Das Attribut muss eine gültige JSON-Zeichenfolge sein.', 'lt' => [ @@ -80,8 +80,8 @@ 'string' => ':attribute muss mindestens :min Zeichen lang sein.', 'array' => ':attribute muss mindesten :min Elemente enthalten.', ], - 'not_in' => ':attribute ist ungültig.', - 'not_regex' => ':attribute ist kein valides Format.', + 'not_in' => 'Das ausgewählte :attribute ist ungültig.', + 'not_regex' => ':attribute ist kein gültiges Format.', 'numeric' => ':attribute muss eine Zahl sein.', 'regex' => ':attribute ist in einem ungültigen Format.', 'required' => ':attribute ist erforderlich.', @@ -99,10 +99,10 @@ 'array' => ':attribute muss :size Elemente enthalten.', ], 'string' => ':attribute muss eine Zeichenkette sein.', - 'timezone' => ':attribute muss eine valide zeitzone sein.', + 'timezone' => ':attribute muss eine gültige Zeitzone sein.', 'totp' => 'Der angegebene Code ist ungültig oder abgelaufen.', 'unique' => ':attribute wird bereits verwendet.', - 'url' => ':attribute ist kein valides Format.', + 'url' => ':attribute ist kein gültiges Format.', '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.', diff --git a/lang/de_informal/activities.php b/lang/de_informal/activities.php index 3ebd8dfd2c1..990bfa0bc56 100644 --- a/lang/de_informal/activities.php +++ b/lang/de_informal/activities.php @@ -6,51 +6,51 @@ return [ // Pages - 'page_create' => 'erstellt Seite', + 'page_create' => 'erstellte Seite', 'page_create_notification' => 'Seite erfolgreich erstellt', - 'page_update' => 'Seite aktualisiert', + 'page_update' => 'aktualisierte Seite', 'page_update_notification' => 'Seite erfolgreich aktualisiert', - 'page_delete' => 'Seite gelöscht', + 'page_delete' => 'löschte Seite', 'page_delete_notification' => 'Seite erfolgreich gelöscht', - 'page_restore' => 'Seite wiederhergestellt', + 'page_restore' => 'stellte Seite wieder her', 'page_restore_notification' => 'Seite erfolgreich wiederhergestellt', - 'page_move' => 'Seite verschoben', + 'page_move' => 'verschob Seite', 'page_move_notification' => 'Seite erfolgreich verschoben', // Chapters - 'chapter_create' => 'Kapitel erstellt', + 'chapter_create' => 'erstellte Kapitel', 'chapter_create_notification' => 'Kapitel erfolgreich erstellt', - 'chapter_update' => 'Kapitel aktualisiert', + 'chapter_update' => 'aktualisierte Kapitel', 'chapter_update_notification' => 'Kapitel erfolgreich aktualisiert', - 'chapter_delete' => 'Kapitel gelöscht', + 'chapter_delete' => 'löschte Kapitel', 'chapter_delete_notification' => 'Kapitel erfolgreich gelöscht', - 'chapter_move' => 'Kapitel verschoben', + 'chapter_move' => 'verschob Kapitel', 'chapter_move_notification' => 'Kapitel erfolgreich verschoben', // Books - 'book_create' => 'Buch erstellt', + 'book_create' => 'erstellte Buch', 'book_create_notification' => 'Buch erfolgreich erstellt', - 'book_create_from_chapter' => 'Kapitel zu Buch umgewandelt', + 'book_create_from_chapter' => 'wandelte Kapitel zu Buch um', 'book_create_from_chapter_notification' => 'Kapitel erfolgreich in ein Buch umgewandelt', - 'book_update' => 'Buch aktualisiert', + 'book_update' => 'aktualisierte Buch', 'book_update_notification' => 'Buch erfolgreich aktualisiert', - 'book_delete' => 'Buch gelöscht', + 'book_delete' => 'löschte Buch', 'book_delete_notification' => 'Buch erfolgreich gelöscht', - 'book_sort' => 'Buch sortiert', + 'book_sort' => 'sortierte Buch', 'book_sort_notification' => 'Buch erfolgreich umsortiert', // Bookshelves - 'bookshelf_create' => 'Regal erstellt', + 'bookshelf_create' => 'erstellte Regal', 'bookshelf_create_notification' => 'Regal erfolgreich erstellt', - 'bookshelf_create_from_book' => 'Buch zu Regal umgewandelt', + 'bookshelf_create_from_book' => 'wandelte Buch zu Regal um', 'bookshelf_create_from_book_notification' => 'Buch erfolgreich zu einem Regal umgewandelt', - 'bookshelf_update' => 'Regal aktualisiert', + 'bookshelf_update' => 'aktualisierte Regal', 'bookshelf_update_notification' => 'Regal erfolgreich aktualisiert', - 'bookshelf_delete' => 'Regal gelöscht', + 'bookshelf_delete' => 'löschte Regal', 'bookshelf_delete_notification' => 'Regal erfolgreich gelöscht', // Revisions - 'revision_restore' => 'Revision wiederhergestellt', + 'revision_restore' => 'stellte Revision wieder her', 'revision_delete' => 'Revision gelöscht', 'revision_delete_notification' => 'Revision erfolgreich gelöscht', @@ -128,12 +128,12 @@ 'comment_delete' => 'Kommentar gelöscht', // Sort Rules - 'sort_rule_create' => 'Sortierregel erstellt', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'hat eine Sortierregel erstellt', + 'sort_rule_create_notification' => 'Sortierregel erfolgreich angelegt', + 'sort_rule_update' => 'hat eine Sortierregel aktualisiert', + 'sort_rule_update_notification' => 'Sortierregel erfolgreich aktualisiert', + 'sort_rule_delete' => 'hat eine Sortierregel gelöscht', + 'sort_rule_delete_notification' => 'Sortierregel erfolgreich gelöscht', // Other 'permissions_update' => 'aktualisierte Berechtigungen', diff --git a/lang/de_informal/auth.php b/lang/de_informal/auth.php index 779726f4bbe..17dcdbcb26f 100644 --- a/lang/de_informal/auth.php +++ b/lang/de_informal/auth.php @@ -33,7 +33,7 @@ 'social_registration_text' => 'Mit einem dieser Dienste registrieren oder anmelden', 'register_thanks' => 'Vielen Dank für deine Registrierung!', - 'register_confirm' => 'Bitte prüfe deinen Posteingang und bestätige die Registrierung.', + 'register_confirm' => 'Bitte prüfe deinen Posteingang und bestätige die Registrierung, um :appName nutzen zu können.', 'registrations_disabled' => 'Eine Registrierung ist momentan nicht möglich', 'registration_email_domain_invalid' => 'Du kannst dich mit dieser E-Mail nicht registrieren.', 'register_success' => 'Vielen Dank für deine Registrierung! Du bist jetzt registriert und eingeloggt.', @@ -82,7 +82,7 @@ // Multi-factor Authentication 'mfa_setup' => 'Multi-Faktor-Authentifizierung einrichten', - 'mfa_setup_desc' => 'Richte eine Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', + 'mfa_setup_desc' => 'Richte Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', 'mfa_setup_configured' => 'Bereits konfiguriert', 'mfa_setup_reconfigure' => 'Umkonfigurieren', 'mfa_setup_remove_confirmation' => 'Bist du sicher, dass du diese Multi-Faktor-Authentifizierungsmethode entfernen möchtest?', diff --git a/lang/de_informal/editor.php b/lang/de_informal/editor.php index 0567ed6c27e..3d6e24a230a 100644 --- a/lang/de_informal/editor.php +++ b/lang/de_informal/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Abbrechen', 'save' => 'Speichern', 'close' => 'Schließen', - 'apply' => 'Apply', + 'apply' => 'Übernehmen', 'undo' => 'Rückgängig machen', 'redo' => 'Wiederholen', 'left' => 'Links', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => 'Anzuzeigender Text', 'title' => 'Titel', - 'browse_links' => 'Browse links', + 'browse_links' => 'Links durchsuchen', 'open_link' => 'Link öffnen', 'open_link_in' => 'Link öffnen in...', 'open_link_current' => 'Aktuellem Fenster', @@ -163,7 +163,7 @@ // About view 'about' => 'Über den Editor', - 'about_title' => 'Über den WYSIWYG Editor', + 'about_title' => 'Über den WYSIWYG-Editor', 'editor_license' => 'Editorlizenz & Copyright', 'editor_lexical_license' => 'Dieser Editor wurde mithilfe von :lexicalLink erstellt, der unter der MIT-Lizenz bereitgestellt wird.', 'editor_lexical_license_link' => 'Vollständige Lizenzdetails findest du hier.', diff --git a/lang/de_informal/entities.php b/lang/de_informal/entities.php index 08f2b2927b8..9891bcb67e9 100644 --- a/lang/de_informal/entities.php +++ b/lang/de_informal/entities.php @@ -40,8 +40,8 @@ 'export_text' => 'Textdatei', 'export_md' => 'Markdown-Datei', 'export_zip' => 'Portable ZIP', - 'default_template' => 'Standard Seitenvorlage', - 'default_template_explain' => 'Zuweisen einer Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt wurden. Beachten Sie, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Zugriff auf die ausgewählte Vorlagen-Seite hat.', + 'default_template' => 'Standard-Seitenvorlage', + 'default_template_explain' => 'Bestimme eine Seitenvorlage, die als Standardinhalt für alle Seiten verwendet wird, die innerhalb dieses Elements erstellt werden. Beachte, dass dies nur dann verwendet wird, wenn der Ersteller der Seite Lesezugriff auf die ausgewählte Vorlagen-Seite hat.', 'default_template_select' => 'Wähle eine Seitenvorlage', 'import' => 'Importieren', 'import_validate' => 'Import validieren', @@ -87,7 +87,7 @@ 'search_terms' => 'Suchbegriffe', 'search_content_type' => 'Inhaltstyp', 'search_exact_matches' => 'Exakte Treffer', - 'search_tags' => 'Nach Schlagwort suchen', + 'search_tags' => 'Schlagwort-Suchen', 'search_options' => 'Optionen', 'search_viewed_by_me' => 'Schon von mir angesehen', 'search_not_viewed_by_me' => 'Noch nicht von mir angesehen', @@ -166,9 +166,9 @@ 'books_search_this' => 'Dieses Buch durchsuchen', 'books_navigation' => 'Buchnavigation', 'books_sort' => 'Buchinhalte sortieren', - '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' => '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_named' => 'Buch ":bookName" sortieren', 'books_sort_name' => 'Sortieren nach Namen', 'books_sort_created' => 'Sortieren nach Erstellungsdatum', @@ -243,12 +243,12 @@ 'pages_edit_delete_draft' => 'Entwurf löschen', 'pages_edit_delete_draft_confirm' => 'Bist du sicher, dass du deinen Entwurf löschen möchtest? Alle deine Änderungen seit dem letzten vollständigen Speichern gehen verloren und der Editor wird mit dem letzten Speicherzustand aktualisiert, der kein Entwurf ist.', 'pages_edit_discard_draft' => 'Entwurf verwerfen', - 'pages_edit_switch_to_markdown' => 'Zum Markdown Editor wechseln', - 'pages_edit_switch_to_markdown_clean' => '(Sauberer Inhalt)', + 'pages_edit_switch_to_markdown' => 'Zum Markdown-Editor wechseln', + 'pages_edit_switch_to_markdown_clean' => '(Gesäuberter Inhalt)', 'pages_edit_switch_to_markdown_stable' => '(Stabiler Inhalt)', - 'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG Editor wechseln', - 'pages_edit_switch_to_new_wysiwyg' => 'Wechsel zum neuen WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testphase)', + 'pages_edit_switch_to_wysiwyg' => 'Zum WYSIWYG-Editor wechseln', + 'pages_edit_switch_to_new_wysiwyg' => 'Zum neuen WYSIWYG wechseln', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha-Testphase)', 'pages_edit_set_changelog' => 'Änderungsprotokoll hinzufügen', 'pages_edit_enter_changelog_desc' => 'Bitte gib eine kurze Zusammenfassung deiner Änderungen ein', 'pages_edit_enter_changelog' => 'Änderungsprotokoll eingeben', @@ -332,7 +332,7 @@ 'tag' => 'Schlagwort', 'tags' => 'Schlagwörter', 'tags_index_desc' => 'Tags können auf Inhalte im System angewendet werden, um eine flexible Form der Kategorisierung anzuwenden. Tags können sowohl einen Schlüssel als auch einen Wert haben, wobei der Wert optional ist. Einmal angewendet, können Inhalte unter Verwendung des Tag-Namens und Wertes abgefragt werden.', - 'tag_name' => 'Schlagwort Name', + 'tag_name' => 'Schlagwortname', 'tag_value' => 'Inhalt (Optional)', 'tags_explain' => "Füge Schlagwörter hinzu, um ihren Inhalt zu kategorisieren.\nDu kannst einen erklärenden Inhalt hinzufügen, um eine genauere Unterteilung vorzunehmen.", 'tags_add' => 'Weiteres Schlagwort hinzufügen', @@ -392,7 +392,7 @@ 'comment' => 'Kommentar', 'comments' => 'Kommentare', 'comment_add' => 'Kommentieren', - 'comment_placeholder' => 'Gib hier deine Kommentare ein (Markdown unterstützt)', + 'comment_placeholder' => 'Gib hier deine Kommentare ein', 'comment_count' => '{0} Keine Kommentare|{1} 1 Kommentar|[2,*] :count Kommentare', 'comment_save' => 'Kommentar speichern', 'comment_new' => 'Neuer Kommentar', diff --git a/lang/de_informal/passwords.php b/lang/de_informal/passwords.php index e3606760bee..862382379fd 100644 --- a/lang/de_informal/passwords.php +++ b/lang/de_informal/passwords.php @@ -8,7 +8,7 @@ 'password' => 'Passwörter müssen aus mindestens acht Zeichen bestehen und mit der eingegebenen Wiederholung übereinstimmen.', 'user' => "Es wurde kein Benutzer mit dieser E-Mail-Adresse gefunden.", - 'token' => 'Der Token zum Zurücksetzen des Passworts für diese E-Mail-Adresse ist ungültig.', + 'token' => 'Der Token zum Zurücksetzen des Passworts ist für diese E-Mail-Adresse ungültig.', 'sent' => 'Wir haben dir einen Link zum Zurücksetzen des Passwortes per E-Mail geschickt!', 'reset' => 'Dein Passwort wurde zurückgesetzt!', diff --git a/lang/de_informal/preferences.php b/lang/de_informal/preferences.php index 8211e2336bf..07b83a8da3a 100644 --- a/lang/de_informal/preferences.php +++ b/lang/de_informal/preferences.php @@ -35,12 +35,12 @@ 'auth_change_password_success' => 'Das Passwort wurde aktualisiert!', 'profile' => 'Profildetails', - 'profile_desc' => 'Verwalte die Details für dein Konto, welche dich gegenüber anderen Benutzern repräsentieren, zusätzlich zu den Details, die für die Kommunikation und die Personalisierung des Systems verwendet werden.', + 'profile_desc' => 'Verwalte die Details deines Kontos, welches dich gegenüber anderen Benutzern repräsentiert, zusätzlich zu den Details, die für die Kommunikation und die Personalisierung des Systems genutzt werden.', 'profile_view_public' => 'Öffentliches Profil anzeigen', - 'profile_name_desc' => 'Konfiguriere deinen Anzeigenamen, der durch die Aktivität, die du durchführst, für andere Benutzer und deine eigenen Inhalte im System sichtbar ist.', + 'profile_name_desc' => 'Konfiguriere deinen Anzeigenamen, der durch die Aktivität, die du ausführst, und die dir gehörenden Inhalte für andere Benutzer sichtbar ist.', 'profile_email_desc' => 'Diese E-Mail wird für Benachrichtigungen und, je nach aktiver Systemauthentifizierung, den Systemzugriff verwendet.', 'profile_email_no_permission' => 'Leider hast du nicht die Berechtigung, deine E-Mail-Adresse zu ändern. Wenn du diese ändern möchtest, wende dich bitte an deinen Administrator.', - 'profile_avatar_desc' => 'Wähle ein Bild aus, dass anderen im System angezeigt wird, um dich zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.', + 'profile_avatar_desc' => 'Wähle ein Bild aus, das anderen im System angezeigt wird, um dich zu repräsentieren. Idealerweise sollte dieses Bild quadratisch und etwa 256px breit und hoch sein.', 'profile_admin_options' => 'Administratoroptionen', 'profile_admin_options_desc' => 'Weitere Administrator-Optionen, wie zum Beispiel die Verwaltung von Rollenzuweisungen, findest du in deinem Benutzerkonto im Bereich "Einstellungen > Benutzer" der Anwendung.', diff --git a/lang/de_informal/settings.php b/lang/de_informal/settings.php index fe8e88b89c1..0b7b74c0d4a 100644 --- a/lang/de_informal/settings.php +++ b/lang/de_informal/settings.php @@ -24,12 +24,12 @@ 'app_public_access_toggle' => 'Öffentlichen Zugriff erlauben', 'app_public_viewing' => 'Öffentliche Ansicht erlauben?', 'app_secure_images' => 'Erhöhte Sicherheit für hochgeladene Bilder aktivieren?', - 'app_secure_images_toggle' => 'Aktiviere Bild-Upload mit höherer Sicherheit', - 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratene, Zeichenketten zu Bild-URLs hinzu. Stelle sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', + 'app_secure_images_toggle' => 'Höhere Sicherheit für Bild-Uploads aktivieren', + 'app_secure_images_desc' => 'Aus Leistungsgründen sind alle Bilder öffentlich sichtbar. Diese Option fügt zufällige, schwer zu erratende, Zeichenketten zu Bild-URLs hinzu. Stelle sicher, dass Verzeichnisindizes deaktiviert sind, um einen einfachen Zugriff zu verhindern.', 'app_default_editor' => 'Standard Seiteneditor', 'app_default_editor_desc' => 'Wähle aus, welcher Editor bei der Bearbeitung neuer Seiten standardmäßig verwendet werden soll. Dies kann auf Seitenebene außer Kraft gesetzt werden, sofern die Berechtigungen dies zulassen.', - 'app_custom_html' => 'Benutzerdefinierter HTML Inhalt', - 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS Styles anzupassen oder Analytics-Code hinzuzufügen.', + 'app_custom_html' => 'Benutzerdefinierter HTML-Head-Inhalt', + 'app_custom_html_desc' => 'Jeder Inhalt, der hier hinzugefügt wird, wird am Ende der -Sektion jeder Seite eingefügt. Diese kann praktisch sein, um CSS-Styles anzupassen oder Analytics-Code hinzuzufügen.', 'app_custom_html_disabled_notice' => 'Benutzerdefinierte HTML-Kopfzeileninhalte sind auf dieser Einstellungsseite deaktiviert, um sicherzustellen, dass alle Änderungen rückgängig gemacht werden können.', 'app_logo' => 'Anwendungslogo', 'app_logo_desc' => 'Dies wird unter anderem in der Kopfzeile der Anwendung verwendet. Dieses Bild sollte 86px hoch sein. Große Bilder werden herunterskaliert.', @@ -76,34 +76,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'Sortierung', + 'sorting_book_default' => 'Standard-Buchsortierung', + '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', + 'sorting_rules_desc' => 'Dies sind vordefinierte Sortieraktionen, die auf Inhalte im System angewendet werden können.', + 'sort_rule_assigned_to_x_books' => ':count Buch zugewiesen|:count Büchern zugewiesen', + 'sort_rule_create' => 'Sortierregel erstellen', + 'sort_rule_edit' => 'Sortierregel bearbeiten', + 'sort_rule_delete' => 'Sortierregel löschen', + 'sort_rule_delete_desc' => 'Diese Sortierregel aus dem System entfernen. Bücher mit dieser Sortierung werden auf manuelle Sortierung zurückgesetzt.', + 'sort_rule_delete_warn_books' => 'Diese Sortierregel wird derzeit in :count Bücher(n) verwendet. Bist du sicher, dass du dies löschen möchtest?', + 'sort_rule_delete_warn_default' => 'Diese Sortierregel wird derzeit als Standard für Bücher verwendet. Bist du sicher, dass du dies löschen möchtest?', + 'sort_rule_details' => 'Sortierregel-Details', + 'sort_rule_details_desc' => 'Lege einen Namen für diese Sortierregel fest, der in Listen erscheint, wenn Benutzer eine Sortierung auswählen.', + 'sort_rule_operations' => 'Sortierungs-Aktionen', + 'sort_rule_operations_desc' => 'Konfiguriere die durchzuführenden Sortieraktionen durch Verschieben von der Liste der verfügbaren Aktionen. Bei der Verwendung werden die Aktionen von oben nach unten angewendet. Alle hier vorgenommenen Änderungen werden beim Speichern auf alle zugewiesenen Bücher angewendet.', + 'sort_rule_available_operations' => 'Verfügbare Aktionen', + 'sort_rule_available_operations_empty' => 'Keine verbleibenden Aktionen', + 'sort_rule_configured_operations' => 'Konfigurierte Aktionen', + 'sort_rule_configured_operations_empty' => 'Aktionen aus der Liste "Verfügbare Operationen" ziehen/hinzufügen', + 'sort_rule_op_asc' => '(Aufst.)', + 'sort_rule_op_desc' => '(Abst.)', + 'sort_rule_op_name' => 'Name - Alphabetisch', + 'sort_rule_op_name_numeric' => 'Name - Numerisch', + 'sort_rule_op_created_date' => 'Erstellungsdatum', + 'sort_rule_op_updated_date' => 'Aktualisierungsdatum', + 'sort_rule_op_chapters_first' => 'Kapitel zuerst', + 'sort_rule_op_chapters_last' => 'Kapitel zuletzt', // Maintenance settings 'maint' => 'Wartung', @@ -114,11 +114,11 @@ 'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest du diese Bilder löschen?', 'maint_image_cleanup_success' => ':count eventuell unbenutze Bilder wurden gefunden und gelöscht.', 'maint_image_cleanup_nothing_found' => 'Keine unbenutzen Bilder gefunden. Nichts zu löschen!', - 'maint_send_test_email' => 'Test E-Mail versenden', - 'maint_send_test_email_desc' => 'Dies sendet eine Test E-Mail an die in deinem Profil angegebene E-Mail-Adresse.', - 'maint_send_test_email_run' => 'Sende eine Test E-Mail', + 'maint_send_test_email' => 'Eine Test-E-Mail versenden', + 'maint_send_test_email_desc' => 'Dies sendet eine Test-E-Mail an die in deinem Profil angegebene E-Mail-Adresse.', + 'maint_send_test_email_run' => 'Test-E-Mail senden', 'maint_send_test_email_success' => 'E-Mail wurde an :address gesendet', - 'maint_send_test_email_mail_subject' => 'Test E-Mail', + 'maint_send_test_email_mail_subject' => 'Test-E-Mail', 'maint_send_test_email_mail_greeting' => 'E-Mail-Versand scheint zu funktionieren!', 'maint_send_test_email_mail_text' => 'Glückwunsch! Da du diese E-Mail Benachrichtigung erhalten hast, scheinen deine E-Mail-Einstellungen korrekt konfiguriert zu sein.', 'maint_recycle_bin_desc' => 'Gelöschte Regale, Bücher, Kapitel & Seiten werden in den Papierkorb verschoben, so dass sie wiederhergestellt oder dauerhaft gelöscht werden können. Ältere Einträge im Papierkorb können, in Abhängigkeit von der Systemkonfiguration, nach einer Weile automatisch entfernt werden.', @@ -159,7 +159,7 @@ 'audit_table_user' => 'Benutzer', 'audit_table_event' => 'Ereignis', 'audit_table_related' => 'Verknüpfter Eintrag oder Detail', - 'audit_table_ip' => 'IP Adresse', + 'audit_table_ip' => 'IP-Adresse', 'audit_table_date' => 'Aktivitätsdatum', 'audit_date_from' => 'Zeitraum von', 'audit_date_to' => 'Zeitraum bis', @@ -257,7 +257,7 @@ 'users_api_tokens_expires' => 'Endet', 'users_api_tokens_docs' => 'API Dokumentation', 'users_mfa' => 'Multi-Faktor-Authentifizierung', - 'users_mfa_desc' => 'Richte die Multi-Faktor-Authentifizierung als zusätzliche Sicherheitsstufe für dein Benutzerkonto ein.', + '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', diff --git a/lang/de_informal/validation.php b/lang/de_informal/validation.php index 0d26dfd298a..f7be5a016c3 100644 --- a/lang/de_informal/validation.php +++ b/lang/de_informal/validation.php @@ -9,7 +9,7 @@ // Standard laravel validation lines 'accepted' => ':attribute muss akzeptiert werden.', - 'active_url' => ':attribute ist keine valide URL.', + 'active_url' => ':attribute ist keine gültige URL.', 'after' => ':attribute muss ein Datum nach :date sein.', 'alpha' => ':attribute kann nur Buchstaben enthalten.', 'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.', @@ -25,12 +25,12 @@ ], 'boolean' => ':attribute Feld muss wahr oder falsch sein.', 'confirmed' => ':attribute stimmt nicht überein.', - 'date' => ':attribute ist kein valides Datum.', + 'date' => ':attribute ist kein gültiges Datum.', 'date_format' => ':attribute entspricht nicht dem Format :format.', 'different' => ':attribute und :other müssen unterschiedlich sein.', 'digits' => ':attribute muss :digits Stellen haben.', 'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.', - 'email' => ':attribute muss eine valide E-Mail-Adresse sein.', + 'email' => ':attribute muss eine gültige E-Mail-Adresse sein.', 'ends_with' => ':attribute muss mit einem der folgenden Werte: :values enden', 'file' => ':attribute muss als gültige Datei angegeben werden.', 'filled' => ':attribute ist erforderlich.', @@ -51,8 +51,8 @@ 'image_extension' => ':attribute muss eine gültige und unterstützte Bild-Dateiendung haben.', 'in' => ':attribute ist ungültig.', 'integer' => ':attribute muss eine Zahl sein.', - 'ip' => ':attribute muss eine valide IP-Adresse sein.', - 'ipv4' => ':attribute muss eine gültige IPv4 Adresse sein.', + 'ip' => ':attribute muss eine gültige IP-Adresse sein.', + 'ipv4' => ':attribute muss eine gültige IPv4-Adresse sein.', 'ipv6' => ':attribute muss eine gültige IPv6-Adresse sein.', 'json' => ':attribute muss ein gültiger JSON-String sein.', 'lt' => [ @@ -80,7 +80,7 @@ 'string' => ':attribute muss mindestens :min Zeichen lang sein.', 'array' => ':attribute muss mindesten :min Elemente enthalten.', ], - 'not_in' => ':attribute ist ungültig.', + 'not_in' => 'Das ausgewählte :attribute ist ungültig.', 'not_regex' => ':attribute ist kein gültiges Format.', 'numeric' => ':attribute muss eine Zahl sein.', 'regex' => ':attribute ist in einem ungültigen Format.', @@ -99,14 +99,14 @@ 'array' => ':attribute muss :size Elemente enthalten.', ], 'string' => ':attribute muss eine Zeichenkette sein.', - 'timezone' => ':attribute muss eine valide zeitzone sein.', + 'timezone' => ':attribute muss eine gültige Zeitzone sein.', 'totp' => 'Der angegebene Code ist ungültig oder abgelaufen.', 'unique' => ':attribute wird bereits verwendet.', 'url' => ':attribute ist kein valides Format.', '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_mime' => ':attribute muss auf eine Datei des Typs :validType verweisen, gefunden :foundType.', + '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/et/entities.php b/lang/et/entities.php index 6e47d2dfb7c..87b625ed7ab 100644 --- a/lang/et/entities.php +++ b/lang/et/entities.php @@ -248,7 +248,7 @@ 'pages_edit_switch_to_markdown_stable' => '(Stabiilne sisu)', 'pages_edit_switch_to_wysiwyg' => 'Kasuta WYSIWYG redaktorit', 'pages_edit_switch_to_new_wysiwyg' => 'Kasuta uut tekstiredaktorit', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(Alfa-testimisel)', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(alfatestimisel)', 'pages_edit_set_changelog' => 'Muudatuste logi', 'pages_edit_enter_changelog_desc' => 'Sisesta tehtud muudatuste lühikirjeldus', 'pages_edit_enter_changelog' => 'Salvesta muudatuste logi', diff --git a/lang/et/errors.php b/lang/et/errors.php index 8f4f9939667..49e755fccf1 100644 --- a/lang/et/errors.php +++ b/lang/et/errors.php @@ -10,7 +10,7 @@ // Auth 'error_user_exists_different_creds' => 'See e-posti aadress on juba seotud teise kasutajaga.', - 'auth_pre_register_theme_prevention' => 'Etteantud detailidega kasutajakontot ei saadud registreerida', + 'auth_pre_register_theme_prevention' => 'Etteantud detailidega kasutajakontot ei õnnestunud registreerida', 'email_already_confirmed' => 'E-posti aadress on juba kinnitatud. Proovi sisse logida.', 'email_confirmation_invalid' => 'Kinnituslink ei ole kehtiv või on seda juba kasutatud. Proovi uuesti registreeruda.', 'email_confirmation_expired' => 'Kinnituslink on aegunud. Sulle saadeti aadressi kinnitamiseks uus e-kiri.', @@ -78,7 +78,7 @@ // Users 'users_cannot_delete_only_admin' => 'Ainsat administraatorit ei saa kustutada', 'users_cannot_delete_guest' => 'Külaliskasutajat ei saa kustutada', - 'users_could_not_send_invite' => 'Kasutajat ei saadud luua, kuna kutse e-kirja saatmine ebaõnnestus', + 'users_could_not_send_invite' => 'Kasutajat ei õnnestunud luua, kuna kutse e-kirja saatmine ebaõnnestus', // Roles 'role_cannot_be_edited' => 'Seda rolli ei saa muuta', diff --git a/lang/id/settings.php b/lang/id/settings.php index 8c26cbe9755..53d1b576a30 100644 --- a/lang/id/settings.php +++ b/lang/id/settings.php @@ -9,8 +9,8 @@ // Common Messages 'settings' => 'Pengaturan', 'settings_save' => 'Simpan Pengaturan', - 'system_version' => 'System Version', - 'categories' => 'Categories', + 'system_version' => 'Versi Sistem', + 'categories' => 'Kategori', // App Settings 'app_customization' => 'Kustomisasi', @@ -26,15 +26,15 @@ 'app_secure_images' => 'Unggahan Gambar Keamanan Lebih Tinggi', 'app_secure_images_toggle' => 'Aktifkan unggahan gambar dengan keamanan lebih tinggi', 'app_secure_images_desc' => 'Untuk alasan performa, semua gambar bersifat publik. Opsi ini menambahkan string acak yang sulit ditebak di depan url gambar. Pastikan indeks direktori tidak diaktifkan untuk mencegah akses mudah.', - 'app_default_editor' => 'Default Page Editor', - 'app_default_editor_desc' => 'Select which editor will be used by default when editing new pages. This can be overridden at a page level where permissions allow.', + 'app_default_editor' => 'Editor Halaman Default', + 'app_default_editor_desc' => 'Pilih editor yang akan digunakan secara default saat mengedit halaman baru. Pengaturan ini dapat diganti di tingkat halaman di mana itu diizinkan.', 'app_custom_html' => 'Kustom Konten HTML Head', 'app_custom_html_desc' => 'Konten apa pun yang ditambahkan di sini akan dimasukkan ke bagian bawah bagian dari setiap halaman. Ini berguna untuk mengganti gaya atau menambahkan kode analitik.', 'app_custom_html_disabled_notice' => 'Kustom konten HTML Head dinonaktifkan pada halaman pengaturan ini untuk memastikan setiap perubahan yang mengganggu dapat dikembalikan.', 'app_logo' => 'Logo Aplikasi', - 'app_logo_desc' => 'This is used in the application header bar, among other areas. This image should be 86px in height. Large images will be scaled down.', - 'app_icon' => 'Application Icon', - 'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.', + 'app_logo_desc' => 'Ini digunakan di bilah tajuk aplikasi, di antara area lainnya. Gambar ini harus memiliki tinggi 86px. Gambar besar akan diperkecil.', + 'app_icon' => 'Ikon Aplikasi', + 'app_icon_desc' => 'Ikon digunakan pada tab browser dan ikon-ikon pintasan. Berkas harus berupa gambar IMG persegi dengan ukuran 256px.', 'app_homepage' => 'Beranda Aplikasi', 'app_homepage_desc' => 'Pilih tampilan untuk ditampilkan di beranda alih-alih tampilan default. Izin halaman diabaikan untuk halaman yang dipilih.', 'app_homepage_select' => 'Pilih halaman', @@ -48,12 +48,12 @@ 'app_disable_comments_desc' => 'Menonaktifkan komentar di semua halaman dalam aplikasi.
                                          Komentar yang ada tidak ditampilkan.', // Color settings - 'color_scheme' => 'Application Color Scheme', - 'color_scheme_desc' => 'Set the colors to use in the application user interface. Colors can be configured separately for dark and light modes to best fit the theme and ensure legibility.', - 'ui_colors_desc' => 'Set the application primary color and default link color. The primary color is mainly used for the header banner, buttons and interface decorations. The default link color is used for text-based links and actions, both within written content and in the application interface.', - 'app_color' => 'Primary Color', - 'link_color' => 'Default Link Color', - 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'color_scheme' => 'Skema Warna Aplikasi', + 'color_scheme_desc' => 'Atur warna-warna untuk digunakan dalam antar muka aplikasi. Warna-warna dapat diatur secara terpisah masing-masing untuk mode gelap dan terang agar sesuai dengan tema dan memastikan keterbacaan.', + 'ui_colors_desc' => 'Atur warna primer aplikasi dan warna tautan default. Warna primer terutama digunakan untuk bilah tajuk, tombol, dan dekorasi antarmuka. Warna tautan default digunakan untuk tautan dan tindakan berbasis teks, baik dalam konten tertulis maupun dalam antarmuka aplikasi.', + 'app_color' => 'Warna Utama', + 'link_color' => 'Warna Tautan Default', + 'content_colors_desc' => 'Tetapkan warna untuk semua elemen dalam hierarki organisasi halaman. Sebaiknya pilih warna dengan tingkat kecerahan yang mirip dengan warna default agar mudah dibaca.', 'bookshelf_color' => 'Warna Rak', 'book_color' => 'Warna Buku', 'chapter_color' => 'Warna Bab', @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Tidak ada batasan yang ditetapkan', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'Menyortir', + 'sorting_book_default' => 'Penyortiran Buku Default', + 'sorting_book_default_desc' => 'Pilih aturan sortir default yang akan diterapkan pada buku baru. Aturan ini tidak akan memengaruhi buku yang sudah ada, dan dapat diganti per buku.', + 'sorting_rules' => 'Aturan Penyortiran', + 'sorting_rules_desc' => 'Ini adalah operasi penyortiran yang telah ditetapkan sebelumnya yang dapat diterapkan pada konten dalam sistem.', + 'sort_rule_assigned_to_x_books' => 'Dikaitkan ke :count Buku|Dikaitkan ke :count Buku', + 'sort_rule_create' => 'Buat Aturan Penyortiran', + 'sort_rule_edit' => 'Mengedit Aturan Penyortiran', + 'sort_rule_delete' => 'Hapus Aturan Penyortiran', + 'sort_rule_delete_desc' => 'Hapus aturan sortir ini dari sistem. Buku yang menggunakan sortir ini akan kembali ke sortir manual.', + 'sort_rule_delete_warn_books' => 'Aturan sortir ini saat ini digunakan pada :count buku (banyak buku). Apakah Anda yakin ingin menghapus ini?', + 'sort_rule_delete_warn_default' => 'Aturan sortir ini saat ini digunakan sebagai aturan baku untuk buku. Apakah Anda yakin ingin menghapusnya?', + 'sort_rule_details' => 'Perincian Aturan Penyortiran', + 'sort_rule_details_desc' => 'Tetapkan nama untuk aturan pengurutan ini, yang akan muncul dalam daftar saat pengguna memilih pengurutan.', + 'sort_rule_operations' => 'Operasi Penyortiran', + 'sort_rule_operations_desc' => 'Konfigurasikan tindakan sortir yang akan dilakukan dengan memindahkannya dari daftar operasi yang tersedia. Setelah digunakan, operasi akan diterapkan secara berurutan, dari atas ke bawah. Setiap perubahan yang dibuat di sini akan diterapkan ke semua buku yang ditetapkan setelah disimpan.', + 'sort_rule_available_operations' => 'Operasi yang Tersedia', + 'sort_rule_available_operations_empty' => 'Tidak ada operasi yang tersisa', + 'sort_rule_configured_operations' => 'Operasi yang Sudah Dikonfigurasi', + 'sort_rule_configured_operations_empty' => 'Seret/tambahkan operasi dari daftar "Operasi yang Tersedia"', + 'sort_rule_op_asc' => '(Naik)', + 'sort_rule_op_desc' => '(Turun)', + 'sort_rule_op_name' => 'Nama - Alfabetis', + 'sort_rule_op_name_numeric' => 'Nama - Numerik', + 'sort_rule_op_created_date' => 'Tanggal Dibuat', + 'sort_rule_op_updated_date' => 'Tanggal Pembaruan', + 'sort_rule_op_chapters_first' => 'Bab di Urutan Pertama', + 'sort_rule_op_chapters_last' => 'Bab di Urutan Terakhir', // Maintenance settings 'maint' => 'Pemeliharaan', @@ -122,10 +122,10 @@ 'maint_send_test_email_mail_text' => 'Selamat! Saat Anda menerima pemberitahuan email ini, pengaturan email Anda tampaknya telah dikonfigurasi dengan benar.', 'maint_recycle_bin_desc' => 'Rak, buku, bab & halaman yang dihapus dikirim ke recycle bin sehingga dapat dipulihkan atau dihapus secara permanen. Item lama di recycle bin dapat dihapus secara otomatis setelah beberapa saat tergantung pada konfigurasi sistem.', 'maint_recycle_bin_open' => 'Buka Tempat Sampah', - 'maint_regen_references' => 'Regenerate References', - 'maint_regen_references_desc' => 'This action will rebuild the cross-item reference index within the database. This is usually handled automatically but this action can be useful to index old content or content added via unofficial methods.', - 'maint_regen_references_success' => 'Reference index has been regenerated!', - 'maint_timeout_command_note' => 'Note: This action can take time to run, which can lead to timeout issues in some web environments. As an alternative, this action be performed using a terminal command.', + 'maint_regen_references' => 'Perbarui Referensi', + 'maint_regen_references_desc' => 'Tindakan ini akan membangun kembali indeks referensi lintas item dalam basis data. Hal ini biasanya ditangani secara otomatis, tetapi tindakan ini dapat berguna untuk mengindeks konten lama atau konten yang ditambahkan melalui metode tidak resmi.', + 'maint_regen_references_success' => 'Indeks referensi telah diperbarui!', + 'maint_timeout_command_note' => 'Catatan: Tindakan ini memerlukan waktu untuk dijalankan, yang dapat menyebabkan masalah batas waktu di beberapa lingkungan web. Sebagai alternatif, tindakan ini dapat dilakukan menggunakan perintah terminal.', // Recycle Bin 'recycle_bin' => 'Tempat Sampah', @@ -139,7 +139,7 @@ 'recycle_bin_contents_empty' => 'Hapus Secara Permanen', 'recycle_bin_empty' => 'Kosongkan Tempat Sampah', 'recycle_bin_empty_confirm' => 'Ini akan menghancurkan secara permanen semua item di tempat sampah termasuk konten yang ada di dalam setiap item. Anda yakin ingin mengosongkan tempat sampah?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', + 'recycle_bin_destroy_confirm' => 'Tindakan ini akan menghapus item ini secara permanen dari sistem, beserta semua elemen turunan yang tercantum di bawah ini, dan Anda tidak akan dapat memulihkan konten ini. Apakah Anda yakin ingin menghapus item ini secara permanen?', 'recycle_bin_destroy_list' => 'Item yang akan Dihancurkan', 'recycle_bin_restore_list' => 'Item yang akan Dipulihkan', 'recycle_bin_restore_confirm' => 'Tindakan ini akan memulihkan item yang dihapus, termasuk semua elemen anak, ke lokasi aslinya. Jika lokasi asli telah dihapus, dan sekarang berada di keranjang sampah, item induk juga perlu dipulihkan.', @@ -158,7 +158,7 @@ 'audit_table_user' => 'Pengguna', 'audit_table_event' => 'Peristiwa', 'audit_table_related' => 'Item atau Detail Terkait', - 'audit_table_ip' => 'IP Address', + 'audit_table_ip' => 'Alamat IP', 'audit_table_date' => 'Tanggal Kegiatan', 'audit_date_from' => 'Rentang Tanggal Dari', 'audit_date_to' => 'Rentang Tanggal Sampai', @@ -166,11 +166,11 @@ // Role Settings 'roles' => 'Peran', 'role_user_roles' => 'Peran Pengguna', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => ':count user assigned|:count users assigned', - 'roles_x_permissions_provided' => ':count permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', + 'roles_index_desc' => 'Peran digunakan untuk mengelompokkan pengguna & memberikan izin sistem kepada anggotanya. Jika pengguna menjadi anggota beberapa peran, hak istimewa yang diberikan akan bertumpuk dan pengguna akan mewarisi semua kemampuan.', + 'roles_x_users_assigned' => ':count pengguna yang ditetapkan|:count pengguna-pengguna yang ditetapkan', + 'roles_x_permissions_provided' => ':count izin|:count izin-izin', + 'roles_assigned_users' => 'Pengguna yang Ditetapkan', + 'roles_permissions_provided' => 'Izin yang diberikan', 'role_create' => 'Buat Peran Baru', 'role_delete' => 'Hapus Peran', 'role_delete_confirm' => 'Ini akan menghapus peran dengan nama \':roleName\'.', @@ -181,7 +181,7 @@ 'role_details' => 'Detail Peran', 'role_name' => 'Nama peran', 'role_desc' => 'Deskripsi Singkat Peran', - 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', + 'role_mfa_enforced' => 'Membutuhkan Otentikasi Multi Faktor', 'role_external_auth_id' => 'Otentikasi Eksternal IDs', 'role_system' => 'Izin Sistem', 'role_manage_users' => 'Kelola pengguna', @@ -191,10 +191,10 @@ 'role_manage_page_templates' => 'Kelola template halaman', 'role_access_api' => 'Akses Sistem API', 'role_manage_settings' => 'Kelola setelan aplikasi', - 'role_export_content' => 'Export content', - 'role_import_content' => 'Import content', - 'role_editor_change' => 'Change page editor', - 'role_notifications' => 'Receive & manage notifications', + 'role_export_content' => 'Ekspor konten', + 'role_import_content' => 'Impor Konten', + 'role_editor_change' => 'Ubah editor halaman', + 'role_notifications' => 'Terima dan kelola notifikasi', 'role_asset' => 'Izin Aset', 'roles_system_warning' => 'Ketahuilah bahwa akses ke salah satu dari tiga izin di atas dapat memungkinkan pengguna untuk mengubah hak mereka sendiri atau orang lain dalam sistem. Hanya tetapkan peran dengan izin ini untuk pengguna tepercaya.', 'role_asset_desc' => 'Izin ini mengontrol akses default ke aset dalam sistem. Izin pada Buku, Bab, dan Halaman akan menggantikan izin ini.', @@ -245,8 +245,8 @@ 'users_social_accounts_info' => 'Di sini Anda dapat menghubungkan akun Anda yang lain untuk login yang lebih cepat dan mudah. Memutuskan akun di sini tidak mencabut akses resmi sebelumnya. Cabut akses dari pengaturan profil Anda pada akun sosial yang terhubung.', 'users_social_connect' => 'Hubungkan Akun', 'users_social_disconnect' => 'Putuskan Sambungan Akun', - 'users_social_status_connected' => 'Connected', - 'users_social_status_disconnected' => 'Disconnected', + 'users_social_status_connected' => 'Terhubung', + 'users_social_status_disconnected' => 'Terputus', 'users_social_connected' => ':socialAccount akun berhasil dilampirkan ke profil Anda.', 'users_social_disconnected' => ':socialAccount akun berhasil diputuskan dari profil Anda.', 'users_api_tokens' => 'Token API', @@ -255,7 +255,7 @@ 'users_api_tokens_create' => 'Buat Token', 'users_api_tokens_expires' => 'Kedaluwarsa', 'users_api_tokens_docs' => 'Dokumentasi API', - 'users_mfa' => 'Multi-Factor Authentication', + 'users_mfa' => 'Otentikasi Multi Faktor', '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', @@ -310,11 +310,11 @@ // Licensing 'licenses' => 'Licenses', 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', - 'licenses_bookstack' => 'BookStack License', - 'licenses_php' => 'PHP Library Licenses', - 'licenses_js' => 'JavaScript Library Licenses', - 'licenses_other' => 'Other Licenses', - 'license_details' => 'License Details', + 'licenses_bookstack' => 'Lisensi BookStack', + 'licenses_php' => 'Lisensi Pustaka PHP', + 'licenses_js' => 'Lisensi Pustaka JavaScript', + 'licenses_other' => 'Lisensi Lainnya', + 'license_details' => 'Perincian Lisensi', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. diff --git a/lang/id/validation.php b/lang/id/validation.php index d922113d984..d5ccb27093c 100644 --- a/lang/id/validation.php +++ b/lang/id/validation.php @@ -105,10 +105,10 @@ 'url' => ':attribute format tidak valid.', 'uploaded' => 'Berkas tidak dapat diunggah. Server mungkin tidak menerima berkas dengan ukuran ini.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - '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 perlu merujuk ke sebuah file yang terdapat di dalam arsip ZIP.', + 'zip_file_mime' => ':attribute seharusnya berupa file dengan tipe :validTypes, tapi yang Anda unggah bertipe :foundType.', + 'zip_model_expected' => 'Diharapkan sebuah objek data, namun yang ditemukan adalah \':type\'.', + 'zip_unique' => ':attribute harus bersifat unik untuk setiap jenis objek dalam file ZIP.', // Custom validation lines 'custom' => [ diff --git a/lang/pt_BR/editor.php b/lang/pt_BR/editor.php index d56584e6cab..ea4035a275c 100644 --- a/lang/pt_BR/editor.php +++ b/lang/pt_BR/editor.php @@ -68,8 +68,8 @@ 'insert_horizontal_line' => 'Insert horizontal line', 'insert_code_block' => 'Inserir/editar bloco de código', 'edit_code_block' => 'Editar bloco de código', - 'insert_drawing' => 'Inserir/editar desenho', - 'drawing_manager' => 'Gerente de desenho', + 'insert_drawing' => 'Inserir/editar diagrama', + 'drawing_manager' => 'Gerenciador de diagramas', 'insert_media' => 'Inserir/editar mídia', 'insert_media_title' => 'Inserir/Editar Mídia', 'clear_formatting' => 'Limpar formatação', diff --git a/lang/pt_BR/entities.php b/lang/pt_BR/entities.php index 37350e67705..ba536808e2c 100644 --- a/lang/pt_BR/entities.php +++ b/lang/pt_BR/entities.php @@ -265,11 +265,11 @@ 'pages_md_preview' => 'Pré-Visualização', 'pages_md_insert_image' => 'Inserir Imagem', 'pages_md_insert_link' => 'Inserir Link para Entidade', - 'pages_md_insert_drawing' => 'Inserir Desenho', + 'pages_md_insert_drawing' => 'Inserir Diagrama', 'pages_md_show_preview' => 'Mostrar pré-visualização', 'pages_md_sync_scroll' => 'Sincronizar pré-visualização', - 'pages_drawing_unsaved' => 'Desenho não-salvo encontrado', - 'pages_drawing_unsaved_confirm' => 'Foram encontrados dados não-salvos de uma tentativa anterior de salvar o desenho. Você gostaria de restaurá-los e continuar editando este desenho?', + 'pages_drawing_unsaved' => 'Diagrama não-salvo encontrado', + 'pages_drawing_unsaved_confirm' => 'Foram encontrados dados não-salvos de uma tentativa anterior de salvar o diagrama. Você gostaria de restaurá-los e continuar editando este diagrama?', 'pages_not_in_chapter' => 'Página não está dentro de um capítulo', 'pages_move' => 'Mover Página', 'pages_copy' => 'Copiar Página', diff --git a/lang/pt_BR/errors.php b/lang/pt_BR/errors.php index d99d871f10b..8dab893c799 100644 --- a/lang/pt_BR/errors.php +++ b/lang/pt_BR/errors.php @@ -54,7 +54,7 @@ 'image_upload_memory_limit' => 'Falha ao processar o ‘upload’ de imagem e/ou criar miniaturas devido a limites de recursos do sistema.', 'image_thumbnail_memory_limit' => 'Falha ao criar variações de tamanho de imagem devido a limites de recursos do sistema.', 'image_gallery_thumbnail_memory_limit' => 'Falha ao criar miniaturas da galeria devido aos limites de recursos do sistema.', - 'drawing_data_not_found' => 'Dados de desenho não puderam ser carregados. O arquivo de desenho pode não existir mais ou você não tenha permissão para acessá-lo.', + 'drawing_data_not_found' => 'Dados de diagrama não puderam ser carregados. O arquivo do diagrama pode não existir mais ou você não tenha permissão para acessá-lo.', // Attachments 'attachment_not_found' => 'Documento não encontrado', diff --git a/lang/sv/activities.php b/lang/sv/activities.php index eb5f4cb2896..3acf6d4ff8d 100644 --- a/lang/sv/activities.php +++ b/lang/sv/activities.php @@ -85,12 +85,12 @@ 'webhook_delete_notification' => 'Webhook har tagits bort', // Imports - 'import_create' => 'created import', - 'import_create_notification' => 'Import successfully uploaded', - 'import_run' => 'updated import', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_create' => 'import skapades', + 'import_create_notification' => 'Import har laddats upp', + 'import_run' => 'import uppdaterad', + 'import_run_notification' => 'Innehållet har importerats', + 'import_delete' => 'import borttagen', + 'import_delete_notification' => 'Importen har tagits bort', // Users 'user_create' => 'skapade användare', @@ -128,12 +128,12 @@ 'comment_delete' => 'raderad kommentar', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'sorteringsregel skapad', + 'sort_rule_create_notification' => 'Sorteringsregel har skapats', + 'sort_rule_update' => 'sorteringsregel uppdaterad', + 'sort_rule_update_notification' => 'Sorteringsregel har uppdaterats', + 'sort_rule_delete' => 'sorteringsregel borttagen', + 'sort_rule_delete_notification' => 'Sorteringsregel har tagits bort', // Other 'permissions_update' => 'uppdaterade behörigheter', diff --git a/lang/sv/common.php b/lang/sv/common.php index 8fe80899f0e..9ca2122dbb8 100644 --- a/lang/sv/common.php +++ b/lang/sv/common.php @@ -109,5 +109,5 @@ 'terms_of_service' => 'Användarvillkor', // OpenSearch - 'opensearch_description' => 'Search :appName', + 'opensearch_description' => 'Sök :appName', ]; diff --git a/lang/sv/editor.php b/lang/sv/editor.php index a35b5247e1a..a1e175ade04 100644 --- a/lang/sv/editor.php +++ b/lang/sv/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Avbryt', 'save' => 'Spara', 'close' => 'Stäng', - 'apply' => 'Apply', + 'apply' => 'Tillämpa', 'undo' => 'Ångra', 'redo' => 'Gör om', 'left' => 'Vänster', @@ -148,7 +148,7 @@ 'url' => 'URL', 'text_to_display' => 'Text som ska visas', 'title' => 'Titel', - 'browse_links' => 'Browse links', + 'browse_links' => 'Bläddra bland länkar', 'open_link' => 'Öppna länk', 'open_link_in' => 'Öppna länk i...', 'open_link_current' => 'Aktuellt fönster', @@ -165,8 +165,8 @@ 'about' => 'Om redigeraren', 'about_title' => 'Om WYSIWYG-redigeraren', 'editor_license' => 'Licens och upphovsrätt för redigerare', - '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' => 'Denna editor är byggd som en fork av :lexicalLink som distribueras under MIT-licensen.', + 'editor_lexical_license_link' => 'Fullständiga licensdetaljer hittas här.', 'editor_tiny_license' => 'Denna redigerare är byggd med :tinyLink som tillhandahålls under MIT licensen.', 'editor_tiny_license_link' => 'Upphovsrätten och licensuppgifterna för TinyMCE hittar du här.', 'save_continue' => 'Spara sida & fortsätt', diff --git a/lang/sv/entities.php b/lang/sv/entities.php index 60e6c7fa72a..e13ff95add8 100644 --- a/lang/sv/entities.php +++ b/lang/sv/entities.php @@ -39,30 +39,30 @@ 'export_pdf' => 'PDF-fil', 'export_text' => 'Textfil', 'export_md' => 'Markdown-fil', - 'export_zip' => 'Portable ZIP', + 'export_zip' => 'Portabel ZIP', 'default_template' => 'Förvald sidmall', - 'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.', - 'default_template_select' => 'Select a template page', - '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:', + 'default_template_explain' => 'Tilldela en sidmall som kommer att användas som standardinnehåll för alla sidor som skapats inom detta objekt. Tänk på att detta endast kommer att användas om skaparen av sidan har tillgång till den valda mallsidan.', + 'default_template_select' => 'Välj en sidmall', + 'import' => 'Importera', + 'import_validate' => 'Validera import', + 'import_desc' => 'Importera böcker, kapitel och sidor med hjälp av en portabel ZIP-export från samma eller en annan instans. Välj en ZIP-fil för att fortsätta. Efter att filen har laddats upp och validerats kan du konfigurera och bekräfta importen i nästa vy.', + 'import_zip_select' => 'Välj ZIP-fil att ladda upp', + 'import_zip_validation_errors' => 'Fel upptäcktes vid validering av den angivna ZIP-filen:', + 'import_pending' => 'Väntande importer', + 'import_pending_none' => 'Ingen import har påbörjats.', + 'import_continue' => 'Fortsätt import', + 'import_continue_desc' => 'Granska innehållet som skall importeras från den uppladdade ZIP-filen. När du är redo, kör importen för att lägga till innehållet. Den uppladdade ZIP-baserade importfilen kommer automatiskt att tas bort vid lyckad import.', + 'import_details' => 'Importdetaljer', + 'import_run' => 'Kör import', + 'import_size' => ':size ZIP-storlek', + 'import_uploaded_at' => 'Uppladdad :relativeTime', + 'import_uploaded_by' => 'Uppladdad av', + 'import_location' => 'Importplats', + 'import_location_desc' => 'Välj en målplats för ditt importerade innehåll. Du behöver relevanta behörigheter för att skapa på den plats du väljer.', + 'import_delete_confirm' => 'Är du säker på att du vill ta bort denna import?', + 'import_delete_desc' => 'Detta kommer att ta bort den uppladdade ZIP-baserade importfilen och kan inte ångras.', + 'import_errors' => 'Importfel', + 'import_errors_desc' => 'Följande fel inträffade under importförsöket:', // Permissions and restrictions 'permissions' => 'Rättigheter', @@ -130,7 +130,7 @@ 'shelves_permissions_updated' => 'Rättigheter för hylla uppdaterades', 'shelves_permissions_active' => 'Rättigheter för hylla aktiverade', 'shelves_permissions_cascade_warning' => 'Rättigheter för hyllor ärvs inte automatiskt ner till böckerna i hyllorna. Detta beror på att en bok kan finnas från flera hyllor. Rättigheter kan däremot kopieras ner till en bok i hyllan med hjälp av alternativet nedan.', - 'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.', + 'shelves_permissions_create' => 'Behörigheter för att skapa hyllor används endast för att kopiera behörigheter till underliggande böcker via åtgärden nedan. De styr inte möjligheten att skapa böcker.', 'shelves_copy_permissions_to_books' => 'Kopiera rättigheter till böcker', 'shelves_copy_permissions' => 'Kopiera rättigheter', 'shelves_copy_permissions_explain' => 'Detta kommer att tillämpa rättigheterna från den här hyllan på alla böcker den innehåller. Se till att eventuella ändringar sparats innan tillämpningen genomförs.', @@ -166,9 +166,9 @@ 'books_search_this' => 'Sök i boken', 'books_navigation' => 'Navigering', 'books_sort' => 'Sortera bokens innehåll', - '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' => '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_named' => 'Sortera boken :bookName', 'books_sort_name' => 'Sortera utifrån namn', 'books_sort_created' => 'Sortera utifrån skapelse', @@ -177,7 +177,7 @@ 'books_sort_chapters_last' => 'Kapitel sist', 'books_sort_show_other' => 'Visa andra böcker', 'books_sort_save' => 'Spara ordning', - 'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.', + 'books_sort_show_other_desc' => 'Lägg till andra böcker här för att inkludera dem i sorteringsåtgärden och möjliggöra enkel omorganisering mellan böcker.', 'books_sort_move_up' => 'Flytta upp', 'books_sort_move_down' => 'Flytta ned', 'books_sort_move_prev_book' => 'Gå till förgående bok', @@ -230,7 +230,7 @@ 'pages_delete_draft' => 'Ta bort utkast', 'pages_delete_success' => 'Sidan har tagits bort', 'pages_delete_draft_success' => 'Utkastet har tagits bort', - 'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.', + 'pages_delete_warning_template' => 'Denna sida används för närvarande som standardsidmall för en bok eller ett kapitel. Dessa böcker eller kapitel kommer inte längre ha någon tilldelad standardsidmall om sidan tas bort.', 'pages_delete_confirm' => 'Är du säker på att du vill ta bort den här sidan?', 'pages_delete_draft_confirm' => 'Är du säker på att du vill ta bort det här utkastet?', 'pages_editing_named' => 'Redigerar sida :pageName', @@ -241,14 +241,14 @@ 'pages_editing_page' => 'Redigerar sida', 'pages_edit_draft_save_at' => 'Utkastet sparades ', 'pages_edit_delete_draft' => 'Ta bort utkast', - 'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.', + 'pages_edit_delete_draft_confirm' => 'Är du säker på att du vill ta bort dina utkaständringar? Alla ändringar du gjort sedan den senaste fullständiga sparningen kommer att gå förlorade, och redigeraren kommer att uppdateras med det senaste icke-utkastet av sidan.', 'pages_edit_discard_draft' => 'Ta bort utkastet', 'pages_edit_switch_to_markdown' => 'Växla till Markdown-redigerare', 'pages_edit_switch_to_markdown_clean' => '(Rent innehåll)', 'pages_edit_switch_to_markdown_stable' => '(Stabilt innehåll)', 'pages_edit_switch_to_wysiwyg' => 'Växla till WYSIWYG-redigerare', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)', + 'pages_edit_switch_to_new_wysiwyg' => 'Växla till ny WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(Under alfatestning)', 'pages_edit_set_changelog' => 'Beskriv dina ändringar', 'pages_edit_enter_changelog_desc' => 'Ange en kort beskrivning av de ändringar du har gjort', 'pages_edit_enter_changelog' => 'Ändringslogg', @@ -266,10 +266,10 @@ 'pages_md_insert_image' => 'Infoga bild', 'pages_md_insert_link' => 'Infoga länk', 'pages_md_insert_drawing' => 'Infoga teckning', - 'pages_md_show_preview' => 'Show preview', + 'pages_md_show_preview' => 'Visa förhandsgranskning', 'pages_md_sync_scroll' => 'Sync preview scroll', - 'pages_drawing_unsaved' => 'Unsaved Drawing Found', - 'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?', + 'pages_drawing_unsaved' => 'Osparad ritning hittades', + 'pages_drawing_unsaved_confirm' => 'Osparade ritningsdata hittades från ett tidigare misslyckat sparförsök. Vill du återställa och fortsätta redigera den osparade ritningen?', 'pages_not_in_chapter' => 'Sidan ligger inte i något kapitel', 'pages_move' => 'Flytta sida', 'pages_copy' => 'Kopiera sida', @@ -279,14 +279,14 @@ 'pages_permissions_success' => 'Rättigheterna för sidan har uppdaterats', 'pages_revision' => 'Revidering', 'pages_revisions' => 'Sidrevisioner', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', + 'pages_revisions_desc' => 'Nedan listas alla tidigare versioner av denna sida. Du kan granska, jämföra och återställa äldre versioner om du har behörighet. Den fullständiga versionshistoriken kanske inte visas i sin helhet här, eftersom äldre versioner kan ha raderats automatiskt beroende på systemets konfiguration.', 'pages_revisions_named' => 'Sidrevisioner för :pageName', 'pages_revision_named' => 'Sidrevision för :pageName', 'pages_revision_restored_from' => 'Återställd från #:id; :summary', 'pages_revisions_created_by' => 'Skapad av', 'pages_revisions_date' => 'Revisionsdatum', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', + 'pages_revisions_sort_number' => 'Revisionsnummer', 'pages_revisions_numbered' => 'Revisions #:id', 'pages_revisions_numbered_changes' => 'Revision #:id ändringar', 'pages_revisions_editor' => 'Typ av redigerare', @@ -297,9 +297,9 @@ 'pages_revisions_restore' => 'Återställ', 'pages_revisions_none' => 'Sidan har inga revisioner', 'pages_copy_link' => 'Kopiera länk', - 'pages_edit_content_link' => 'Jump to section in editor', - 'pages_pointer_enter_mode' => 'Enter section select mode', - 'pages_pointer_label' => 'Page Section Options', + 'pages_edit_content_link' => 'Hoppa till sektionen i redigeraren', + 'pages_pointer_enter_mode' => 'Ange markeringsläge för sektion', + 'pages_pointer_label' => 'Alternativ för sidsektion', 'pages_pointer_permalink' => 'Page Section Permalink', 'pages_pointer_include_tag' => 'Page Section Include Tag', 'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag', @@ -318,20 +318,20 @@ 'time_b' => 'under de senaste :minCount minuterna', 'message' => ':start :time. Var försiktiga så att ni inte skriver över varandras ändringar!', ], - 'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content', - 'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content', + 'pages_draft_discarded' => 'Utkastet har tagits bort. Redigeringsverktyget har uppdaterats med aktuellt sidinnehåll', + 'pages_draft_deleted' => 'Utkastet har raderats. Redigeringsverktyget har uppdaterats med aktuellt sidinnehåll', 'pages_specific' => 'Specifik sida', 'pages_is_template' => 'Sidmall', // Editor Sidebar - 'toggle_sidebar' => 'Toggle Sidebar', + 'toggle_sidebar' => 'Visa/Dölj sidopanel', 'page_tags' => 'Sidtaggar', 'chapter_tags' => 'Kapiteltaggar', 'book_tags' => 'Boktaggar', 'shelf_tags' => 'Hylltaggar', 'tag' => 'Tagg', 'tags' => 'Taggar', - 'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.', + 'tags_index_desc' => 'Taggar kan användas på innehåll inom systemet för att skapa en flexibel form av kategorisering. Taggar kan ha både en nyckel och ett värde, där värdet är valfritt. När en tagg har tilldelats kan innehållet sökas fram med hjälp av taggens namn och värde.', 'tag_name' => 'Etikettnamn', 'tag_value' => 'Taggvärde (Frivilligt)', 'tags_explain' => "Lägg till taggar för att kategorisera ditt innehåll bättre. \n Du kan tilldela ett värde till en tagg för ännu bättre organisering.", @@ -355,7 +355,7 @@ 'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.', 'attachments_set_link' => 'Ange länk', 'attachments_delete' => 'Är du säker på att du vill ta bort bilagan?', - 'attachments_dropzone' => 'Drop files here to upload', + 'attachments_dropzone' => 'Släpp filer här för uppladdning', 'attachments_no_files' => 'Inga filer har laddats upp', 'attachments_explain_link' => 'Du kan bifoga en länk om du inte vill ladda upp en fil. Detta kan vara en länk till en annan sida eller till en fil i molnet.', 'attachments_link_name' => 'Länknamn', @@ -404,7 +404,7 @@ 'comment_updated_success' => 'Kommentaren har uppdaterats', 'comment_delete_confirm' => 'Är du säker på att du vill ta bort den här kommentaren?', 'comment_in_reply_to' => 'Som svar på :commentId', - 'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.', + 'comment_editor_explain' => 'Här är kommentarer som lämnats på denna sida. Kommentarer kan läggas till och hanteras när den sparade sidan visas.', // Revision 'revision_delete_confirm' => 'Är du säker på att du vill radera den här versionen?', @@ -438,20 +438,20 @@ // Watch Options 'watch' => 'Följ', - 'watch_title_default' => 'Default Preferences', + 'watch_title_default' => 'Standardinställningar', 'watch_desc_default' => 'Revert watching to just your default notification preferences.', 'watch_title_ignore' => 'Ignorera', - 'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.', + 'watch_desc_ignore' => 'Ignorera samtliga meddelanden, även sådana som styrs av användarens egna inställningar.', 'watch_title_new' => 'Nya sidor', - 'watch_desc_new' => 'Notify when any new page is created within this item.', - 'watch_title_updates' => 'All Page Updates', - 'watch_desc_updates' => 'Notify upon all new pages and page changes.', - 'watch_desc_updates_page' => 'Notify upon all page changes.', + 'watch_desc_new' => 'Meddela när en ny sida skapas inom detta objekt.', + 'watch_title_updates' => 'Alla siduppdateringar', + 'watch_desc_updates' => 'Meddela vid alla nya sidor och siduppdateringar.', + 'watch_desc_updates_page' => 'Meddela alla siduppdateringar.', 'watch_title_comments' => 'All Page Updates & Comments', - 'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.', - 'watch_desc_comments_page' => 'Notify upon page changes and new comments.', - 'watch_change_default' => 'Change default notification preferences', - 'watch_detail_ignore' => 'Ignoring notifications', + 'watch_desc_comments' => 'Meddela vid alla nya sidor, siduppdateringar och nya kommentarer.', + 'watch_desc_comments_page' => 'Meddela vid siduppdateringar och nya kommentarer.', + 'watch_change_default' => 'Ändra standardinställningar för meddelanden', + 'watch_detail_ignore' => 'Ignorera meddelanden', 'watch_detail_new' => 'Watching for new pages', 'watch_detail_updates' => 'Watching new pages and updates', 'watch_detail_comments' => 'Watching new pages, updates & comments', diff --git a/lang/sv/errors.php b/lang/sv/errors.php index 86ea215d02f..d65b7d0a013 100644 --- a/lang/sv/errors.php +++ b/lang/sv/errors.php @@ -53,7 +53,7 @@ 'image_upload_replace_type' => 'Image file replacements must be of the same type', 'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.', 'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.', - 'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.', + 'image_gallery_thumbnail_memory_limit' => 'Misslyckades att skapa galleriminiatyrer på grund av otillräckliga systemresurser.', 'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.', // Attachments @@ -106,16 +106,16 @@ 'back_soon' => 'Vi är snart tillbaka.', // Import - 'import_zip_cant_read' => 'Could not read ZIP file.', + 'import_zip_cant_read' => 'Kunde inte läsa ZIP-filen.', '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_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_validation_failed' => 'ZIP-filen kunde inte valideras med fel:', + 'import_zip_failed_notification' => 'Det gick inte att importera ZIP-fil.', + 'import_perms_books' => 'Du saknar behörighet att skapa böcker.', + 'import_perms_chapters' => 'Du saknar behörighet att skapa kapitel.', + 'import_perms_pages' => 'Du saknar behörighet att skapa sidor.', + 'import_perms_images' => 'Du saknar behörighet för att skapa bilder.', + 'import_perms_attachments' => 'Du saknar behörighet att skapa bilagor.', // API errors 'api_no_authorization_found' => 'Ingen auktoriseringstoken hittades på denna begäran', @@ -129,5 +129,5 @@ 'maintenance_test_email_failure' => 'Ett fel uppstod när ett test mail skulle skickas:', // HTTP errors - 'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts', + 'http_ssr_url_no_match' => 'URL matchar inte de konfigurerade tillåtna SSR-värdarna', ]; diff --git a/lang/sv/settings.php b/lang/sv/settings.php index f5b87786d7c..4af72c7f79a 100644 --- a/lang/sv/settings.php +++ b/lang/sv/settings.php @@ -53,7 +53,7 @@ 'ui_colors_desc' => 'Ange applikationens primära färg och standard färg för länkar. Den primära färgen används främst för huvudrubriken, knappar och gränssnitt dekorationer. Standardfärgen på länken används för textbaserade länkar och åtgärder, både inom skriftligt innehåll och i applikationsgränssnittet.', 'app_color' => 'Primärfärg', 'link_color' => 'Standardfärg för länkar', - 'content_colors_desc' => 'Set colors for all elements in the page organisation hierarchy. Choosing colors with a similar brightness to the default colors is recommended for readability.', + 'content_colors_desc' => 'Ange färger för alla element i sidans organisationshierarki. Det rekommenderas att välja färger med liknande ljusstyrka som standardfärgerna för att bibehålla läsbarheten.', 'bookshelf_color' => 'Hyllfärg', 'book_color' => 'Bokfärg', 'chapter_color' => 'Kapitelfärg', @@ -75,19 +75,19 @@ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begränsning inställd', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'Sorterar', + 'sorting_book_default' => 'Standard boksortering', + 'sorting_book_default_desc' => 'Välj standard sorteringsregel som skall tillämpas på nya böcker. Detta påverkar inte befintliga böcker och kan åsidosättas per bok.', + 'sorting_rules' => 'Sorteringsregler', '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_assigned_to_x_books' => 'Tilldelad till :count bok|Tilldelad till :count böcker', + 'sort_rule_create' => 'Skapa sorteringsregel', + 'sort_rule_edit' => 'Redigera sorteringsregel', + 'sort_rule_delete' => 'Ta bort sorteringsregel', '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' => 'Detaljer för sorteringsregler', '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.', diff --git a/lang/uk/activities.php b/lang/uk/activities.php index fc076b510f0..e8b77e0d430 100644 --- a/lang/uk/activities.php +++ b/lang/uk/activities.php @@ -85,12 +85,12 @@ 'webhook_delete_notification' => 'Вебхуки успішно видалено', // Imports - 'import_create' => 'created import', - 'import_create_notification' => 'Import successfully uploaded', - 'import_run' => 'updated import', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_create' => 'створений імпорт', + 'import_create_notification' => 'Імпорт успішно завантажений', + 'import_run' => 'оновлений імпорт', + 'import_run_notification' => 'Контент успішно імпортовано', + 'import_delete' => 'видалений імпорт', + 'import_delete_notification' => 'Імпорт успішно видалено', // Users 'user_create' => 'створений користувач', @@ -128,12 +128,12 @@ 'comment_delete' => 'видалений коментар', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + '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/uk/editor.php b/lang/uk/editor.php index e61d7f71967..f887b32031a 100644 --- a/lang/uk/editor.php +++ b/lang/uk/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'Скасувати', 'save' => 'Зберегти', 'close' => 'Закрити', - 'apply' => 'Apply', + 'apply' => 'Застосувати', 'undo' => 'Відмінити', 'redo' => 'Відновити', 'left' => 'Ліворуч', @@ -148,7 +148,7 @@ 'url' => 'Адреса URL', 'text_to_display' => 'Текст для показу', 'title' => 'Назва', - 'browse_links' => 'Browse links', + 'browse_links' => 'Переглянути посилання', 'open_link' => 'Відкрити посилання', 'open_link_in' => 'Відкрити посилання в...', 'open_link_current' => 'Поточне вікно', @@ -165,8 +165,8 @@ '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' => 'Тут можна знайти авторські та умови ліцензії.', 'save_continue' => 'Зберегти і продовжити', diff --git a/lang/uk/entities.php b/lang/uk/entities.php index 753ceb72767..577fd129633 100644 --- a/lang/uk/entities.php +++ b/lang/uk/entities.php @@ -39,30 +39,30 @@ 'export_pdf' => 'PDF файл', 'export_text' => 'Текстовий файл', 'export_md' => 'Файл розмітки', - 'export_zip' => 'Portable ZIP', + 'export_zip' => 'Портативний ZIP', 'default_template' => 'Типовий шаблон сторінки', 'default_template_explain' => 'Призначити шаблон сторінки, який буде використовуватися як типовий вміст для всіх сторінок, створених у цьому елементі. Майте на увазі, що ця сторінка буде використана лише у випадку, якщо вона має доступ до обраної сторінки шаблону.', 'default_template_select' => 'Виберіть сторінку шаблону', - '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:', + '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' => ':size Розмір Імпорту ZIP', + 'import_uploaded_at' => 'Завантажено :relativeTime', + 'import_uploaded_by' => 'Завантажено', + 'import_location' => 'Імпортувати місцезнаходження', + 'import_location_desc' => 'Вибір цільового розташування для імпортованого вмісту. Вам потрібні відповідні дозволи для створення локації, яке ви обрали.', + 'import_delete_confirm' => 'Ви впевнені, що бажаєте видалити цей імпорт?', + 'import_delete_desc' => 'Це видалить завантажений імпорт файлу ZIP, і його не можна буде скасувати.', + 'import_errors' => 'Помилки імпорту', + 'import_errors_desc' => 'Під час спроби імпорту відбулися наступні помилки:', // Permissions and restrictions 'permissions' => 'Дозволи', @@ -166,9 +166,9 @@ '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_named' => 'Сортувати книгу :bookName', 'books_sort_name' => 'Сортувати за назвою', 'books_sort_created' => 'Сортувати за датою створення', diff --git a/lang/uk/errors.php b/lang/uk/errors.php index 5c8148fece2..eed61e8a027 100644 --- a/lang/uk/errors.php +++ b/lang/uk/errors.php @@ -106,16 +106,16 @@ 'back_soon' => 'Він повернеться найближчим часом.', // 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_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' => 'Не вдалося прочитати ZIP-файл.', + 'import_zip_cant_decode_data' => 'Не вдалося знайти і розшифрувати контент ZIP data.json.', + 'import_zip_no_data' => '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' => 'У запиті не знайдено токен авторизації', diff --git a/lang/uk/settings.php b/lang/uk/settings.php index 8670996aeff..5ab70d1ae69 100644 --- a/lang/uk/settings.php +++ b/lang/uk/settings.php @@ -75,34 +75,34 @@ 'reg_confirm_restrict_domain_placeholder' => 'Не встановлено обмежень', // Sorting Settings - 'sorting' => 'Sorting', - 'sorting_book_default' => 'Default Book Sort', - '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' => 'Сортування', + 'sorting_book_default' => 'Типовий порядок сортування', + 'sorting_book_default_desc' => 'Виберіть правило сортування за замовчуванням для застосування нових книг. Це не вплине на існуючі книги, і може бути перевизначено для кожної книги.', + 'sorting_rules' => 'Сортувати правила', + 'sorting_rules_desc' => 'Це попередньо визначені операції сортування, які можуть бути застосовані до вмісту в системі.', + 'sort_rule_assigned_to_x_books' => 'Призначено :count книгу|Призначення на :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' => 'Розділи останні', // Maintenance settings 'maint' => 'Обслуговування', @@ -192,7 +192,7 @@ 'role_access_api' => 'Доступ до системного API', 'role_manage_settings' => 'Керування налаштуваннями програми', 'role_export_content' => 'Вміст експорту', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Імпортувати вміст', 'role_editor_change' => 'Змінити редактор сторінок', 'role_notifications' => 'Отримувати та керувати повідомленнями', 'role_asset' => 'Дозволи', diff --git a/lang/uk/validation.php b/lang/uk/validation.php index ae31bb237aa..6161aba671a 100644 --- a/lang/uk/validation.php +++ b/lang/uk/validation.php @@ -105,10 +105,10 @@ 'url' => 'Формат поля :attribute неправильний.', 'uploaded' => 'Не вдалося завантажити файл. Сервер може не приймати файли такого розміру.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - '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_mime' => 'Поле :attribute повинне посилатись на файл типу :validtypes, знайдений :foundType.', + 'zip_model_expected' => 'Очікувався об’єкт даних, але знайдено ":type".', + 'zip_unique' => 'Поле :attribute має бути унікальним для типу об\'єкта в ZIP.', // Custom validation lines 'custom' => [ diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index 15297c804e6..bc52b4ccc10 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -76,33 +76,33 @@ // Sorting Settings 'sorting' => '排序', - 'sorting_book_default' => 'Default Book Sort', - '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', + 'sorting_book_default' => '默认书卷排序', + 'sorting_book_default_desc' => '选择要应用于新书的默认排序规则。这不会影响现有书,并且可以每本书覆盖。', + 'sorting_rules' => '排序规则', + 'sorting_rules_desc' => '这些是预定义的排序操作,可应用于系统中的内容。', + 'sort_rule_assigned_to_x_books' => '分配给 :count Book|分配给 :count Books', + 'sort_rule_create' => '创建排序规则', + 'sort_rule_edit' => '编辑排序规则', + 'sort_rule_delete' => '删除排序规则', + 'sort_rule_delete_desc' => '从系统中删除这种排序规则。使用这种类型的书本将恢复到手动排序。', + 'sort_rule_delete_warn_books' => '此排序规则目前用于:count book(s)。您确定要删除吗?', + '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' => '(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', + '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' => '章节倒序', // Maintenance settings 'maint' => '维护', From 295532fa7a58b640e67648f130e3ea356b85cba5 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 May 2025 18:09:49 +0100 Subject: [PATCH 039/474] Deps: Updated PHP packages --- composer.lock | 409 +++++++++++++++++++++++++------------------------- 1 file changed, 208 insertions(+), 201 deletions(-) diff --git a/composer.lock b/composer.lock index e3e592852c1..b3604dd471a 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.342.18", + "version": "3.343.3", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "e6c81bf323b082f8fe2b76d8d41e2614806d5892" + "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/e6c81bf323b082f8fe2b76d8d41e2614806d5892", - "reference": "e6c81bf323b082f8fe2b76d8d41e2614806d5892", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979", + "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979", "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.342.18" + "source": "https://github.com/aws/aws-sdk-php/tree/3.343.3" }, - "time": "2025-04-01T18:15:02+00:00" + "time": "2025-05-02T18:04:58+00:00" }, { "name": "bacon/bacon-qr-code", @@ -922,16 +922,16 @@ }, { "name": "firebase/php-jwt", - "version": "v6.11.0", + "version": "v6.11.1", "source": { "type": "git", "url": "https://github.com/firebase/php-jwt.git", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712" + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/firebase/php-jwt/zipball/8f718f4dfc9c5d5f0c994cdfd103921b43592712", - "reference": "8f718f4dfc9c5d5f0c994cdfd103921b43592712", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", "shasum": "" }, "require": { @@ -979,9 +979,9 @@ ], "support": { "issues": "https://github.com/firebase/php-jwt/issues", - "source": "https://github.com/firebase/php-jwt/tree/v6.11.0" + "source": "https://github.com/firebase/php-jwt/tree/v6.11.1" }, - "time": "2025-01-23T05:11:06+00:00" + "time": "2025-04-09T20:32:01+00:00" }, { "name": "fruitcake/php-cors", @@ -1740,16 +1740,16 @@ }, { "name": "laravel/framework", - "version": "v11.44.2", + "version": "v11.44.7", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f85216c82cbd38b66d67ebd20ea762cb3751a4b4" + "reference": "00bc6ac91a6d577bf051c18ddaa638c0d221e1c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f85216c82cbd38b66d67ebd20ea762cb3751a4b4", - "reference": "f85216c82cbd38b66d67ebd20ea762cb3751a4b4", + "url": "https://api.github.com/repos/laravel/framework/zipball/00bc6ac91a6d577bf051c18ddaa638c0d221e1c7", + "reference": "00bc6ac91a6d577bf051c18ddaa638c0d221e1c7", "shasum": "" }, "require": { @@ -1951,7 +1951,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-03-12T14:34:30+00:00" + "time": "2025-04-25T12:40:47+00:00" }, { "name": "laravel/prompts", @@ -2075,16 +2075,16 @@ }, { "name": "laravel/socialite", - "version": "v5.19.0", + "version": "v5.20.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "c40f843c5643fb6b089e46ce9794b8408bf08319" + "reference": "30972c12a41f71abeb418bc9ff157da8d9231519" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/c40f843c5643fb6b089e46ce9794b8408bf08319", - "reference": "c40f843c5643fb6b089e46ce9794b8408bf08319", + "url": "https://api.github.com/repos/laravel/socialite/zipball/30972c12a41f71abeb418bc9ff157da8d9231519", + "reference": "30972c12a41f71abeb418bc9ff157da8d9231519", "shasum": "" }, "require": { @@ -2143,7 +2143,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2025-03-27T17:26:42+00:00" + "time": "2025-04-21T14:21:34+00:00" }, { "name": "laravel/tinker", @@ -2213,16 +2213,16 @@ }, { "name": "league/commonmark", - "version": "2.6.1", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad" + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d990688c91cedfb69753ffc2512727ec646df2ad", - "reference": "d990688c91cedfb69753ffc2512727ec646df2ad", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "shasum": "" }, "require": { @@ -2259,7 +2259,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -2316,7 +2316,7 @@ "type": "tidelift" } ], - "time": "2024-12-29T14:10:59+00:00" + "time": "2025-05-05T12:20:28+00:00" }, { "name": "league/config", @@ -3285,16 +3285,16 @@ }, { "name": "nesbot/carbon", - "version": "3.9.0", + "version": "3.9.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "6d16a8a015166fe54e22c042e0805c5363aef50d" + "reference": "ced71f79398ece168e24f7f7710462f462310d4d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/6d16a8a015166fe54e22c042e0805c5363aef50d", - "reference": "6d16a8a015166fe54e22c042e0805c5363aef50d", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d", + "reference": "ced71f79398ece168e24f7f7710462f462310d4d", "shasum": "" }, "require": { @@ -3387,7 +3387,7 @@ "type": "tidelift" } ], - "time": "2025-03-27T12:57:33+00:00" + "time": "2025-05-01T19:51:51+00:00" }, { "name": "nette/schema", @@ -4102,16 +4102,16 @@ }, { "name": "predis/predis", - "version": "v2.3.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9" + "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/bac46bfdb78cd6e9c7926c697012aae740cb9ec9", - "reference": "bac46bfdb78cd6e9c7926c697012aae740cb9ec9", + "url": "https://api.github.com/repos/predis/predis/zipball/f49e13ee3a2a825631562aa0223ac922ec5d058b", + "reference": "f49e13ee3a2a825631562aa0223ac922ec5d058b", "shasum": "" }, "require": { @@ -4120,6 +4120,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.3", "phpstan/phpstan": "^1.9", + "phpunit/phpcov": "^6.0 || ^8.0", "phpunit/phpunit": "^8.0 || ^9.4" }, "suggest": { @@ -4142,7 +4143,7 @@ "role": "Maintainer" } ], - "description": "A flexible and feature-complete Redis client for PHP.", + "description": "A flexible and feature-complete Redis/Valkey client for PHP.", "homepage": "http://github.com/predis/predis", "keywords": [ "nosql", @@ -4151,7 +4152,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v2.3.0" + "source": "https://github.com/predis/predis/tree/v2.4.0" }, "funding": [ { @@ -4159,7 +4160,7 @@ "type": "github" } ], - "time": "2024-11-21T20:00:02+00:00" + "time": "2025-04-30T15:16:02+00:00" }, { "name": "psr/clock", @@ -5189,16 +5190,16 @@ }, { "name": "socialiteproviders/okta", - "version": "4.4.0", + "version": "4.5.0", "source": { "type": "git", "url": "https://github.com/SocialiteProviders/Okta.git", - "reference": "5e47cd7b4c19da94ecafbd91fa430e4151c09806" + "reference": "b8b0d5ea72325ead82481524a48576aaf39356ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/5e47cd7b4c19da94ecafbd91fa430e4151c09806", - "reference": "5e47cd7b4c19da94ecafbd91fa430e4151c09806", + "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/b8b0d5ea72325ead82481524a48576aaf39356ed", + "reference": "b8b0d5ea72325ead82481524a48576aaf39356ed", "shasum": "" }, "require": { @@ -5235,7 +5236,7 @@ "issues": "https://github.com/socialiteproviders/providers/issues", "source": "https://github.com/socialiteproviders/providers" }, - "time": "2023-12-12T01:59:17+00:00" + "time": "2024-11-07T21:57:40+00:00" }, { "name": "socialiteproviders/twitch", @@ -5488,16 +5489,16 @@ }, { "name": "symfony/console", - "version": "v7.2.5", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e51498ea18570c062e7df29d05a7003585b19b88" + "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88", - "reference": "e51498ea18570c062e7df29d05a7003585b19b88", + "url": "https://api.github.com/repos/symfony/console/zipball/0e2e3f38c192e93e622e41ec37f4ca70cfedf218", + "reference": "0e2e3f38c192e93e622e41ec37f4ca70cfedf218", "shasum": "" }, "require": { @@ -5561,7 +5562,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.5" + "source": "https://github.com/symfony/console/tree/v7.2.6" }, "funding": [ { @@ -5577,7 +5578,7 @@ "type": "tidelift" } ], - "time": "2025-03-12T08:11:12+00:00" + "time": "2025-04-07T19:09:28+00:00" }, { "name": "symfony/css-selector", @@ -6008,16 +6009,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.2.5", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "371272aeb6286f8135e028ca535f8e4d6f114126" + "reference": "6023ec7607254c87c5e69fb3558255aca440d72b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/371272aeb6286f8135e028ca535f8e4d6f114126", - "reference": "371272aeb6286f8135e028ca535f8e4d6f114126", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6023ec7607254c87c5e69fb3558255aca440d72b", + "reference": "6023ec7607254c87c5e69fb3558255aca440d72b", "shasum": "" }, "require": { @@ -6066,7 +6067,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.2.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.2.6" }, "funding": [ { @@ -6082,20 +6083,20 @@ "type": "tidelift" } ], - "time": "2025-03-25T15:54:33+00:00" + "time": "2025-04-09T08:14:01+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.2.5", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54" + "reference": "f9dec01e6094a063e738f8945ef69c0cfcf792ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b1fe91bc1fa454a806d3f98db4ba826eb9941a54", - "reference": "b1fe91bc1fa454a806d3f98db4ba826eb9941a54", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f9dec01e6094a063e738f8945ef69c0cfcf792ec", + "reference": "f9dec01e6094a063e738f8945ef69c0cfcf792ec", "shasum": "" }, "require": { @@ -6180,7 +6181,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.2.5" + "source": "https://github.com/symfony/http-kernel/tree/v7.2.6" }, "funding": [ { @@ -6196,20 +6197,20 @@ "type": "tidelift" } ], - "time": "2025-03-28T13:32:50+00:00" + "time": "2025-05-02T09:04:03+00:00" }, { "name": "symfony/mime", - "version": "v7.2.4", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b" + "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/87ca22046b78c3feaff04b337f33b38510fd686b", - "reference": "87ca22046b78c3feaff04b337f33b38510fd686b", + "url": "https://api.github.com/repos/symfony/mime/zipball/706e65c72d402539a072d0d6ad105fff6c161ef1", + "reference": "706e65c72d402539a072d0d6ad105fff6c161ef1", "shasum": "" }, "require": { @@ -6264,7 +6265,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.2.4" + "source": "https://github.com/symfony/mime/tree/v7.2.6" }, "funding": [ { @@ -6280,11 +6281,11 @@ "type": "tidelift" } ], - "time": "2025-02-19T08:51:20+00:00" + "time": "2025-04-27T13:34:41+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -6343,7 +6344,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -6363,7 +6364,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -6421,7 +6422,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -6441,16 +6442,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", "shasum": "" }, "require": { @@ -6504,7 +6505,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" }, "funding": [ { @@ -6520,11 +6521,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-10T14:38:51+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6585,7 +6586,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -6605,19 +6606,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -6665,7 +6667,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -6681,20 +6683,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -6745,7 +6747,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -6761,11 +6763,11 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" }, { "name": "symfony/polyfill-php83", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -6821,7 +6823,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" }, "funding": [ { @@ -6841,7 +6843,7 @@ }, { "name": "symfony/polyfill-uuid", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -6900,7 +6902,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.32.0" }, "funding": [ { @@ -7145,16 +7147,16 @@ }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/a214fe7d62bd4df2a76447c67c6b26e1d5e74931", + "reference": "a214fe7d62bd4df2a76447c67c6b26e1d5e74931", "shasum": "" }, "require": { @@ -7212,7 +7214,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.2.6" }, "funding": [ { @@ -7228,20 +7230,20 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-04-20T20:18:16+00:00" }, { "name": "symfony/translation", - "version": "v7.2.4", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a" + "reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/283856e6981286cc0d800b53bd5703e8e363f05a", - "reference": "283856e6981286cc0d800b53bd5703e8e363f05a", + "url": "https://api.github.com/repos/symfony/translation/zipball/e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6", + "reference": "e7fd8e2a4239b79a0fd9fb1fef3e0e7f969c6dc6", "shasum": "" }, "require": { @@ -7307,7 +7309,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.2.4" + "source": "https://github.com/symfony/translation/tree/v7.2.6" }, "funding": [ { @@ -7323,7 +7325,7 @@ "type": "tidelift" } ], - "time": "2025-02-13T10:27:23+00:00" + "time": "2025-04-07T19:09:28+00:00" }, { "name": "symfony/translation-contracts", @@ -7479,16 +7481,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.2.3", + "version": "v7.2.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9c46038cd4ed68952166cf7001b54eb539184ccb", + "reference": "9c46038cd4ed68952166cf7001b54eb539184ccb", "shasum": "" }, "require": { @@ -7542,7 +7544,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.2.6" }, "funding": [ { @@ -7558,7 +7560,7 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:39:41+00:00" + "time": "2025-04-09T08:14:01+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -7617,16 +7619,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2" + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2", - "reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", "shasum": "" }, "require": { @@ -7685,7 +7687,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" }, "funding": [ { @@ -7697,7 +7699,7 @@ "type": "tidelift" } ], - "time": "2024-07-20T21:52:34+00:00" + "time": "2025-04-30T23:37:27+00:00" }, { "name": "voku/portable-ascii", @@ -7969,20 +7971,20 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "v2.0.1", + "version": "v2.1.1", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", - "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", "shasum": "" }, "require": { - "php": "^5.3|^7.0|^8.0" + "php": "^7.4|^8.0" }, "replace": { "cordoval/hamcrest-php": "*", @@ -7990,8 +7992,8 @@ "kodova/hamcrest-php": "*" }, "require-dev": { - "phpunit/php-file-iterator": "^1.4 || ^2.0", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -8014,22 +8016,22 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" }, - "time": "2020-07-09T08:09:16+00:00" + "time": "2025-04-30T06:54:44+00:00" }, { "name": "iamcal/sql-parser", - "version": "v0.5", + "version": "v0.6", "source": { "type": "git", "url": "https://github.com/iamcal/SQLParser.git", - "reference": "644fd994de3b54e5d833aecf406150aa3b66ca88" + "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/644fd994de3b54e5d833aecf406150aa3b66ca88", - "reference": "644fd994de3b54e5d833aecf406150aa3b66ca88", + "url": "https://api.github.com/repos/iamcal/SQLParser/zipball/947083e2dca211a6f12fb1beb67a01e387de9b62", + "reference": "947083e2dca211a6f12fb1beb67a01e387de9b62", "shasum": "" }, "require-dev": { @@ -8055,9 +8057,9 @@ "description": "MySQL schema parser", "support": { "issues": "https://github.com/iamcal/SQLParser/issues", - "source": "https://github.com/iamcal/SQLParser/tree/v0.5" + "source": "https://github.com/iamcal/SQLParser/tree/v0.6" }, - "time": "2024-03-22T22:46:32+00:00" + "time": "2025-03-17T16:59:46+00:00" }, { "name": "itsgoingd/clockwork", @@ -8137,40 +8139,40 @@ }, { "name": "larastan/larastan", - "version": "v3.2.0", + "version": "v3.4.0", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "d84d5a3b6536a586899ad6855a3e098473703690" + "reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/d84d5a3b6536a586899ad6855a3e098473703690", - "reference": "d84d5a3b6536a586899ad6855a3e098473703690", + "url": "https://api.github.com/repos/larastan/larastan/zipball/1042fa0c2ee490bb6da7381f3323f7292ad68222", + "reference": "1042fa0c2ee490bb6da7381f3323f7292ad68222", "shasum": "" }, "require": { "ext-json": "*", - "iamcal/sql-parser": "^0.5.0", - "illuminate/console": "^11.41.3 || ^12.0", - "illuminate/container": "^11.41.3 || ^12.0", - "illuminate/contracts": "^11.41.3 || ^12.0", - "illuminate/database": "^11.41.3 || ^12.0", - "illuminate/http": "^11.41.3 || ^12.0", - "illuminate/pipeline": "^11.41.3 || ^12.0", - "illuminate/support": "^11.41.3 || ^12.0", + "iamcal/sql-parser": "^0.6.0", + "illuminate/console": "^11.44.2 || ^12.4.1", + "illuminate/container": "^11.44.2 || ^12.4.1", + "illuminate/contracts": "^11.44.2 || ^12.4.1", + "illuminate/database": "^11.44.2 || ^12.4.1", + "illuminate/http": "^11.44.2 || ^12.4.1", + "illuminate/pipeline": "^11.44.2 || ^12.4.1", + "illuminate/support": "^11.44.2 || ^12.4.1", "php": "^8.2", - "phpstan/phpstan": "^2.1.3" + "phpstan/phpstan": "^2.1.11" }, "require-dev": { - "doctrine/coding-standard": "^12.0", - "laravel/framework": "^11.41.3 || ^12.0", - "mockery/mockery": "^1.6", - "nikic/php-parser": "^5.3", - "orchestra/canvas": "^v9.1.3 || ^10.0", - "orchestra/testbench-core": "^9.5.2 || ^10.0", - "phpstan/phpstan-deprecation-rules": "^2.0.0", - "phpunit/phpunit": "^10.5.35 || ^11.3.6" + "doctrine/coding-standard": "^13", + "laravel/framework": "^11.44.2 || ^12.7.2", + "mockery/mockery": "^1.6.12", + "nikic/php-parser": "^5.4", + "orchestra/canvas": "^v9.2.2 || ^10.0.1", + "orchestra/testbench-core": "^9.12.0 || ^10.1", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpunit/phpunit": "^10.5.35 || ^11.5.15" }, "suggest": { "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" @@ -8199,13 +8201,9 @@ { "name": "Can Vural", "email": "can9119@gmail.com" - }, - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" } ], - "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan wrapper for Laravel", + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan extension for Laravel", "keywords": [ "PHPStan", "code analyse", @@ -8218,7 +8216,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.2.0" + "source": "https://github.com/larastan/larastan/tree/v3.4.0" }, "funding": [ { @@ -8226,7 +8224,7 @@ "type": "github" } ], - "time": "2025-03-14T21:54:26+00:00" + "time": "2025-04-22T09:44:59+00:00" }, { "name": "mockery/mockery", @@ -8313,16 +8311,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -8361,7 +8359,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -8369,42 +8367,43 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.7.0", + "version": "v8.8.0", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26" + "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/586cb8181a257a2152b6a855ca8d9598878a1a26", - "reference": "586cb8181a257a2152b6a855ca8d9598878a1a26", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8", "shasum": "" }, "require": { - "filp/whoops": "^2.17.0", + "filp/whoops": "^2.18.0", "nunomaduro/termwind": "^2.3.0", "php": "^8.2.0", - "symfony/console": "^7.2.1" + "symfony/console": "^7.2.5" }, "conflict": { - "laravel/framework": "<11.39.1 || >=13.0.0", - "phpunit/phpunit": "<11.5.3 || >=12.0.0" + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" }, "require-dev": { - "larastan/larastan": "^2.10.0", - "laravel/framework": "^11.44.2", + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.2", + "laravel/framework": "^11.44.2 || ^12.6", "laravel/pint": "^1.21.2", "laravel/sail": "^1.41.0", "laravel/sanctum": "^4.0.8", "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0", - "pestphp/pest": "^3.7.4", - "sebastian/environment": "^6.1.0 || ^7.2.0" + "orchestra/testbench-core": "^9.12.0 || ^10.1", + "pestphp/pest": "^3.8.0", + "sebastian/environment": "^7.2.0 || ^8.0" }, "type": "library", "extra": { @@ -8467,7 +8466,7 @@ "type": "patreon" } ], - "time": "2025-03-14T22:37:40+00:00" + "time": "2025-04-03T14:33:09+00:00" }, { "name": "phar-io/manifest", @@ -8589,16 +8588,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.11", + "version": "2.1.14", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30" + "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8ca5f79a8f63c49b2359065832a654e1ec70ac30", - "reference": "8ca5f79a8f63c49b2359065832a654e1ec70ac30", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8f2e03099cac24ff3b379864d171c5acbfc6b9a2", + "reference": "8f2e03099cac24ff3b379864d171c5acbfc6b9a2", "shasum": "" }, "require": { @@ -8643,7 +8642,7 @@ "type": "github" } ], - "time": "2025-03-24T13:45:00+00:00" + "time": "2025-05-02T15:32:28+00:00" }, { "name": "phpunit/php-code-coverage", @@ -8970,16 +8969,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.15", + "version": "11.5.19", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" + "reference": "0da1ebcdbc4d5bd2d189cfe02846a89936d8dda5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", - "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/0da1ebcdbc4d5bd2d189cfe02846a89936d8dda5", + "reference": "0da1ebcdbc4d5bd2d189cfe02846a89936d8dda5", "shasum": "" }, "require": { @@ -8989,7 +8988,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -9051,7 +9050,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.19" }, "funding": [ { @@ -9062,12 +9061,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-03-23T16:02:11+00:00" + "time": "2025-05-02T06:56:52+00:00" }, { "name": "sebastian/cli-parser", @@ -9997,16 +10004,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.12.0", + "version": "3.12.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630" + "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/2d1b63db139c3c6ea0c927698e5160f8b3b8d630", - "reference": "2d1b63db139c3c6ea0c927698e5160f8b3b8d630", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", + "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", "shasum": "" }, "require": { @@ -10077,7 +10084,7 @@ "type": "thanks_dev" } ], - "time": "2025-03-18T05:04:51+00:00" + "time": "2025-04-13T04:10:18+00:00" }, { "name": "ssddanbrown/asserthtml", From ce1e20501cb9f1a4af81f5153a45ab05dbaaab23 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 5 May 2025 18:14:18 +0100 Subject: [PATCH 040/474] Updated translator & dependency attribution before release v25.02.3 --- .github/translators.txt | 3 +++ dev/licensing/js-library-licenses.txt | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/.github/translators.txt b/.github/translators.txt index e38cb8a5115..aab08d76413 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -484,3 +484,6 @@ Ilya Shaulov (ishaulov) :: Russian Konstantin Bobkov (b.konstantv) :: Russian Ruben Sutter (rubensutter) :: German jellium :: French +Qxlkdr :: Swedish +Hari (muhhari) :: Indonesian +仙君御 (xjy) :: Chinese Simplified diff --git a/dev/licensing/js-library-licenses.txt b/dev/licensing/js-library-licenses.txt index 52949e3cd27..14c0c8f4977 100644 --- a/dev/licensing/js-library-licenses.txt +++ b/dev/licensing/js-library-licenses.txt @@ -3656,6 +3656,13 @@ Copyright: Copyright (c) 2017-present Devon Govett Source: https://github.com/parcel-bundler/watcher.git Link: https://github.com/parcel-bundler/watcher.git ----------- +@parcel/watcher-linux-x64-musl +License: MIT +License File: node_modules/@parcel/watcher-linux-x64-musl/LICENSE +Copyright: Copyright (c) 2017-present Devon Govett +Source: https://github.com/parcel-bundler/watcher.git +Link: https://github.com/parcel-bundler/watcher.git +----------- @parcel/watcher License: MIT License File: node_modules/@parcel/watcher/LICENSE From 3e99ce4098cf45eafd8917e9b208b3b6a4aaa1ec Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 8 May 2025 15:53:25 +0100 Subject: [PATCH 041/474] Deps: Updated PHP packages Mainly to update termwind which was causing issues for users on Arch where a more recent libxml version was in use. --- composer.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/composer.lock b/composer.lock index b3604dd471a..04dfaa501b9 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.343.3", + "version": "3.343.6", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979" + "reference": "3746aca8cbed5f46beba850e0a480ef58e71b197" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979", - "reference": "d7ad5f6bdee792a16d2c9d5a3d9b56acfaaaf979", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3746aca8cbed5f46beba850e0a480ef58e71b197", + "reference": "3746aca8cbed5f46beba850e0a480ef58e71b197", "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.343.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.343.6" }, - "time": "2025-05-02T18:04:58+00:00" + "time": "2025-05-07T18:10:08+00:00" }, { "name": "bacon/bacon-qr-code", @@ -3597,31 +3597,31 @@ }, { "name": "nunomaduro/termwind", - "version": "v2.3.0", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/termwind.git", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda" + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/52915afe6a1044e8b9cee1bcff836fb63acf9cda", - "reference": "52915afe6a1044e8b9cee1bcff836fb63acf9cda", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", "shasum": "" }, "require": { "ext-mbstring": "*", "php": "^8.2", - "symfony/console": "^7.1.8" + "symfony/console": "^7.2.6" }, "require-dev": { - "illuminate/console": "^11.33.2", - "laravel/pint": "^1.18.2", + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", "mockery/mockery": "^1.6.12", - "pestphp/pest": "^2.36.0", - "phpstan/phpstan": "^1.12.11", - "phpstan/phpstan-strict-rules": "^1.6.1", - "symfony/var-dumper": "^7.1.8", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", "thecodingmachine/phpstan-strict-rules": "^1.0.0" }, "type": "library", @@ -3664,7 +3664,7 @@ ], "support": { "issues": "https://github.com/nunomaduro/termwind/issues", - "source": "https://github.com/nunomaduro/termwind/tree/v2.3.0" + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" }, "funding": [ { @@ -3680,7 +3680,7 @@ "type": "github" } ], - "time": "2024-11-21T10:39:51+00:00" + "time": "2025-05-08T08:14:37+00:00" }, { "name": "onelogin/php-saml", From a27df485bb19a2fbcddf8cac8ae2b367d23022e7 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 9 May 2025 12:14:28 +0100 Subject: [PATCH 042/474] Comments: Fixed display, added archive list support for editor toolbox --- lang/en/entities.php | 1 + resources/js/components/page-comment-reference.ts | 15 ++++++++++++++- resources/sass/_components.scss | 15 +++++++++++++++ .../views/pages/parts/toolbox-comments.blade.php | 15 +++++++++++++-- 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lang/en/entities.php b/lang/en/entities.php index e25a83299f3..6e616ded452 100644 --- a/lang/en/entities.php +++ b/lang/en/entities.php @@ -396,6 +396,7 @@ 'comment_placeholder' => 'Leave a comment here', 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', 'comment_archived_count' => ':count Archived', + 'comment_archived_threads' => 'Archived Threads', 'comment_save' => 'Save Comment', 'comment_new' => 'New Comment', 'comment_created' => 'commented :createDiff', diff --git a/resources/js/components/page-comment-reference.ts b/resources/js/components/page-comment-reference.ts index 5a71f0c513b..48fb8ee0a49 100644 --- a/resources/js/components/page-comment-reference.ts +++ b/resources/js/components/page-comment-reference.ts @@ -34,13 +34,26 @@ export class PageCommentReference extends Component { window.addEventListener('editor-toolbox-change', (event) => { const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab; const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open; - if (tabName === 'comments' && isOpen) { + if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { this.showForEditor(); } else { this.hideMarker(); } }); + // Handle visibility changes within editor toolbox archived details dropdown + window.addEventListener('toggle', event => { + if (event.target instanceof HTMLElement && event.target.contains(this.link)) { + window.requestAnimationFrame(() => { + if (this.link.checkVisibility()) { + this.showForEditor(); + } else { + this.hideMarker(); + } + }); + } + }, {capture: true}); + // Handle comments tab changes to hide/show markers & indicators window.addEventListener('tabs-change', event => { const sectionId = (event as {detail: {showing: string}}).detail.showing; diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index a86d31ce373..faeb2e05141 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -1202,4 +1202,19 @@ input.scroll-box-search, .scroll-box-header-item { } .scroll-box > li.empty-state:last-child { display: list-item; +} + +details.section-expander summary { + border-top: 1px solid #DDD; + font-weight: bold; + font-size: 12px; + color: #888; + cursor: pointer; + padding-block: vars.$xs; +} +details.section-expander:open summary { + margin-bottom: vars.$s; +} +details.section-expander { + border-bottom: 1px solid #DDD; } \ No newline at end of file diff --git a/resources/views/pages/parts/toolbox-comments.blade.php b/resources/views/pages/parts/toolbox-comments.blade.php index d632b85c689..72958a2fede 100644 --- a/resources/views/pages/parts/toolbox-comments.blade.php +++ b/resources/views/pages/parts/toolbox-comments.blade.php @@ -1,3 +1,6 @@ +{{-- +$comments - CommentTree +--}}

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

                                          @@ -5,11 +8,19 @@

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

                                          - @foreach($comments->get() as $branch) + @foreach($comments->getActive() as $branch) @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true]) @endforeach @if($comments->empty()) -

                                          {{ trans('common.no_items') }}

                                          +

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

                                          + @endif + @if($comments->archivedThreadCount() > 0) +
                                          + {{ trans('entities.comment_archived_threads') }} + @foreach($comments->getArchived() as $branch) + @include('comments.comment-branch', ['branch' => $branch, 'readOnly' => true]) + @endforeach +
                                          @endif
                                          \ No newline at end of file From f8c0aaff0326ac77195d854effd9315d80aa2bc4 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Fri, 9 May 2025 14:17:04 +0100 Subject: [PATCH 043/474] Comments: Checked content/arhived comment styles in dark mode Also added default non-clickable styles for scenarios for references which don't have an active content link. --- resources/sass/_components.scss | 9 ++++++++- resources/sass/_pages.scss | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index faeb2e05141..9e96b39fbb4 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -785,6 +785,10 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { top: 0; opacity: 0.15; } + &[href="#"] { + color: #444; + pointer-events: none; + } } .comment-branch .comment-box { @@ -836,7 +840,8 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { font-size: 12px; } .avatar { - width: 18px; + width: 22px; + height: 22px; margin-inline-end: 2px !important; } .content { @@ -1206,6 +1211,7 @@ input.scroll-box-search, .scroll-box-header-item { details.section-expander summary { border-top: 1px solid #DDD; + @include mixins.lightDark(border-color, #DDD, #000); font-weight: bold; font-size: 12px; color: #888; @@ -1217,4 +1223,5 @@ details.section-expander:open summary { } details.section-expander { border-bottom: 1px solid #DDD; + @include mixins.lightDark(border-color, #DDD, #000); } \ No newline at end of file diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index dbdcc06656d..621d08f45f2 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -248,7 +248,7 @@ body.tox-fullscreen, body.markdown-fullscreen { z-index: 92; pointer-events: all; min-width: min(340px, 80vw); - background-color: #FFF; + @include mixins.lightDark(background-color, #FFF, #222); box-shadow: vars.$bs-hover; border-radius: 4px; overflow: hidden; From 62f78f1c6d1f901cd986495a7271b983b60ec74d Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 12 May 2025 14:26:09 +0100 Subject: [PATCH 044/474] Comments: Split tests, added extra archive/reference tests --- app/Activity/CommentRepo.php | 4 +- tests/Entity/CommentDisplayTest.php | 134 ++++++++++++++++++ .../{CommentTest.php => CommentStoreTest.php} | 117 ++++----------- 3 files changed, 160 insertions(+), 95 deletions(-) create mode 100644 tests/Entity/CommentDisplayTest.php rename tests/Entity/{CommentTest.php => CommentStoreTest.php} (66%) diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index bf162f68ae6..c194e72168c 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -62,7 +62,7 @@ public function update(Comment $comment, string $html): Comment public function archive(Comment $comment): Comment { if ($comment->parent_id) { - throw new NotifyException('Only top-level comments can be archived.'); + throw new NotifyException('Only top-level comments can be archived.', '/', 400); } $comment->archived = true; @@ -79,7 +79,7 @@ public function archive(Comment $comment): Comment public function unarchive(Comment $comment): Comment { if ($comment->parent_id) { - throw new NotifyException('Only top-level comments can be un-archived.'); + throw new NotifyException('Only top-level comments can be un-archived.', '/', 400); } $comment->archived = false; diff --git a/tests/Entity/CommentDisplayTest.php b/tests/Entity/CommentDisplayTest.php new file mode 100644 index 00000000000..4e9640baeed --- /dev/null +++ b/tests/Entity/CommentDisplayTest.php @@ -0,0 +1,134 @@ +asAdmin(); + $page = $this->entities->page(); + + $this->postJson("/comment/$page->id", ['html' => '

                                          My new comment

                                          ']); + $this->postJson("/comment/$page->id", ['html' => '

                                          My new comment

                                          ']); + + $respHtml = $this->withHtml($this->get($page->getUrl())); + $respHtml->assertElementCount('.comment-branch', 3); + $respHtml->assertElementNotExists('.comment-branch .comment-branch'); + + $comment = $page->comments()->first(); + $resp = $this->postJson("/comment/$page->id", [ + 'html' => '

                                          My nested comment

                                          ', 'parent_id' => $comment->local_id + ]); + $resp->assertStatus(200); + + $respHtml = $this->withHtml($this->get($page->getUrl())); + $respHtml->assertElementCount('.comment-branch', 4); + $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment'); + } + + public function test_comments_are_visible_in_the_page_editor() + { + $page = $this->entities->page(); + + $this->asAdmin()->postJson("/comment/$page->id", ['html' => '

                                          My great comment to see in the editor

                                          ']); + + $respHtml = $this->withHtml($this->get($page->getUrl('/edit'))); + $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor'); + } + + public function test_comment_creator_name_truncated() + { + [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $this->actingAs($longNamedUser)->postJson("/comment/$page->id", $comment->getAttributes()); + + $pageResp = $this->asAdmin()->get($page->getUrl()); + $pageResp->assertSee('Wolfeschlegels…'); + } + + public function test_comment_editor_js_loaded_with_create_or_edit_permissions() + { + $editor = $this->users->editor(); + $page = $this->entities->page(); + + $resp = $this->actingAs($editor)->get($page->getUrl()); + $resp->assertSee('tinymce.min.js?', false); + $resp->assertSee('window.editor_translations', false); + $resp->assertSee('component="entity-selector"', false); + + $this->permissions->removeUserRolePermissions($editor, ['comment-create-all']); + $this->permissions->grantUserRolePermissions($editor, ['comment-update-own']); + + $resp = $this->actingAs($editor)->get($page->getUrl()); + $resp->assertDontSee('tinymce.min.js?', false); + $resp->assertDontSee('window.editor_translations', false); + $resp->assertDontSee('component="entity-selector"', false); + + Comment::factory()->create([ + 'created_by' => $editor->id, + 'entity_type' => 'page', + 'entity_id' => $page->id, + ]); + + $resp = $this->actingAs($editor)->get($page->getUrl()); + $resp->assertSee('tinymce.min.js?', false); + $resp->assertSee('window.editor_translations', false); + $resp->assertSee('component="entity-selector"', false); + } + + public function test_comment_displays_relative_times() + { + $page = $this->entities->page(); + $comment = Comment::factory()->create(['entity_id' => $page->id, 'entity_type' => $page->getMorphClass()]); + $comment->created_at = now()->subWeek(); + $comment->updated_at = now()->subDay(); + $comment->save(); + + $pageResp = $this->asAdmin()->get($page->getUrl()); + $html = $this->withHtml($pageResp); + + // Create date shows relative time as text to user + $html->assertElementContains('.comment-box', 'commented 1 week ago'); + // Updated indicator has full time as title + $html->assertElementContains('.comment-box span[title^="Updated ' . $comment->updated_at->format('Y-m-d') . '"]', 'Updated'); + } + + public function test_comment_displays_reference_if_set() + { + $page = $this->entities->page(); + $comment = Comment::factory()->make([ + 'content_ref' => 'bkmrk-a:abc:4-1', + 'local_id' => 10, + ]); + $page->comments()->save($comment); + + $html = $this->withHtml($this->asEditor()->get($page->getUrl())); + $html->assertElementExists('#comment10 .comment-reference-indicator-wrap a'); + } + + public function test_archived_comments_are_shown_in_their_own_container() + { + $page = $this->entities->page(); + $comment = Comment::factory()->make(['local_id' => 44]); + $page->comments()->save($comment); + + $html = $this->withHtml($this->asEditor()->get($page->getUrl())); + $html->assertElementExists('#comment-tab-panel-active #comment44'); + $html->assertElementNotExists('#comment-tab-panel-archived .comment-box'); + + $comment->archived = true; + $comment->save(); + + $html = $this->withHtml($this->asEditor()->get($page->getUrl())); + $html->assertElementExists('#comment-tab-panel-archived #comment44.comment-box'); + $html->assertElementNotExists('#comment-tab-panel-active #comment44'); + } +} diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentStoreTest.php similarity index 66% rename from tests/Entity/CommentTest.php rename to tests/Entity/CommentStoreTest.php index baf0d392beb..8b8a5d488b8 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentStoreTest.php @@ -7,7 +7,7 @@ use BookStack\Entities\Models\Page; use Tests\TestCase; -class CommentTest extends TestCase +class CommentStoreTest extends TestCase { public function test_add_comment() { @@ -166,6 +166,29 @@ public function test_archive_endpoints_require_delete_or_edit_permissions() } } + public function test_non_top_level_comments_cant_be_archived_or_unarchived() + { + $this->asAdmin(); + $page = $this->entities->page(); + + $comment = Comment::factory()->make(); + $page->comments()->save($comment); + $subComment = Comment::factory()->make(['parent_id' => $comment->id]); + $page->comments()->save($subComment); + $subComment->refresh(); + + $resp = $this->putJson("/comment/$subComment->id/archive"); + $resp->assertStatus(400); + + $this->assertDatabaseHas('comments', [ + 'id' => $subComment->id, + 'archived' => false, + ]); + + $resp = $this->putJson("/comment/$subComment->id/unarchive"); + $resp->assertStatus(400); + } + public function test_scripts_cannot_be_injected_via_comment_html() { $page = $this->entities->page(); @@ -225,96 +248,4 @@ public function test_comment_html_is_limited() 'html' => $expected, ]); } - - public function test_reply_comments_are_nested() - { - $this->asAdmin(); - $page = $this->entities->page(); - - $this->postJson("/comment/$page->id", ['html' => '

                                          My new comment

                                          ']); - $this->postJson("/comment/$page->id", ['html' => '

                                          My new comment

                                          ']); - - $respHtml = $this->withHtml($this->get($page->getUrl())); - $respHtml->assertElementCount('.comment-branch', 3); - $respHtml->assertElementNotExists('.comment-branch .comment-branch'); - - $comment = $page->comments()->first(); - $resp = $this->postJson("/comment/$page->id", [ - 'html' => '

                                          My nested comment

                                          ', 'parent_id' => $comment->local_id - ]); - $resp->assertStatus(200); - - $respHtml = $this->withHtml($this->get($page->getUrl())); - $respHtml->assertElementCount('.comment-branch', 4); - $respHtml->assertElementContains('.comment-branch .comment-branch', 'My nested comment'); - } - - public function test_comments_are_visible_in_the_page_editor() - { - $page = $this->entities->page(); - - $this->asAdmin()->postJson("/comment/$page->id", ['html' => '

                                          My great comment to see in the editor

                                          ']); - - $respHtml = $this->withHtml($this->get($page->getUrl('/edit'))); - $respHtml->assertElementContains('.comment-box .content', 'My great comment to see in the editor'); - } - - public function test_comment_creator_name_truncated() - { - [$longNamedUser] = $this->users->newUserWithRole(['name' => 'Wolfeschlegelsteinhausenbergerdorff'], ['comment-create-all', 'page-view-all']); - $page = $this->entities->page(); - - $comment = Comment::factory()->make(); - $this->actingAs($longNamedUser)->postJson("/comment/$page->id", $comment->getAttributes()); - - $pageResp = $this->asAdmin()->get($page->getUrl()); - $pageResp->assertSee('Wolfeschlegels…'); - } - - public function test_comment_editor_js_loaded_with_create_or_edit_permissions() - { - $editor = $this->users->editor(); - $page = $this->entities->page(); - - $resp = $this->actingAs($editor)->get($page->getUrl()); - $resp->assertSee('tinymce.min.js?', false); - $resp->assertSee('window.editor_translations', false); - $resp->assertSee('component="entity-selector"', false); - - $this->permissions->removeUserRolePermissions($editor, ['comment-create-all']); - $this->permissions->grantUserRolePermissions($editor, ['comment-update-own']); - - $resp = $this->actingAs($editor)->get($page->getUrl()); - $resp->assertDontSee('tinymce.min.js?', false); - $resp->assertDontSee('window.editor_translations', false); - $resp->assertDontSee('component="entity-selector"', false); - - Comment::factory()->create([ - 'created_by' => $editor->id, - 'entity_type' => 'page', - 'entity_id' => $page->id, - ]); - - $resp = $this->actingAs($editor)->get($page->getUrl()); - $resp->assertSee('tinymce.min.js?', false); - $resp->assertSee('window.editor_translations', false); - $resp->assertSee('component="entity-selector"', false); - } - - public function test_comment_displays_relative_times() - { - $page = $this->entities->page(); - $comment = Comment::factory()->create(['entity_id' => $page->id, 'entity_type' => $page->getMorphClass()]); - $comment->created_at = now()->subWeek(); - $comment->updated_at = now()->subDay(); - $comment->save(); - - $pageResp = $this->asAdmin()->get($page->getUrl()); - $html = $this->withHtml($pageResp); - - // Create date shows relative time as text to user - $html->assertElementContains('.comment-box', 'commented 1 week ago'); - // Updated indicator has full time as title - $html->assertElementContains('.comment-box span[title^="Updated ' . $comment->updated_at->format('Y-m-d') . '"]', 'Updated'); - } } From 8f92b6f21b2412005e138c1482f32c158ff69204 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Mon, 12 May 2025 15:31:55 +0100 Subject: [PATCH 045/474] Comments: Fixed a range of TS errors + other - Migrated toolbox component to TS - Aligned how custom event types are managed - Fixed PHP use of content_ref where not provided --- app/Activity/CommentRepo.php | 6 +- .../Controllers/CommentController.php | 3 +- .../{editor-toolbox.js => editor-toolbox.ts} | 43 +++++---- .../js/components/page-comment-reference.ts | 36 ++++---- resources/js/components/page-comment.ts | 62 ++++++++----- resources/js/components/page-comments.ts | 88 ++++++++----------- resources/js/components/pointer.ts | 46 +++++----- resources/js/components/tabs.ts | 17 ++-- resources/js/services/dom.ts | 6 +- resources/js/services/events.ts | 9 +- 10 files changed, 167 insertions(+), 149 deletions(-) rename resources/js/components/{editor-toolbox.js => editor-toolbox.ts} (61%) diff --git a/app/Activity/CommentRepo.php b/app/Activity/CommentRepo.php index c194e72168c..7005f8fcf83 100644 --- a/app/Activity/CommentRepo.php +++ b/app/Activity/CommentRepo.php @@ -22,7 +22,7 @@ public function getById(int $id): Comment /** * Create a new comment on an entity. */ - public function create(Entity $entity, string $html, ?int $parent_id, string $content_ref): Comment + public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment { $userId = user()->id; $comment = new Comment(); @@ -31,8 +31,8 @@ public function create(Entity $entity, string $html, ?int $parent_id, string $co $comment->created_by = $userId; $comment->updated_by = $userId; $comment->local_id = $this->getNextLocalId($entity); - $comment->parent_id = $parent_id; - $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $content_ref) === 1 ? $content_ref : ''; + $comment->parent_id = $parentId; + $comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : ''; $entity->comments()->save($comment); ActivityService::add(ActivityType::COMMENT_CREATE, $comment); diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index 7f16c17ffce..479d57c4db9 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -43,7 +43,8 @@ public function savePageComment(Request $request, int $pageId) // Create a new comment. $this->checkPermission('comment-create-all'); - $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $input['content_ref']); + $contentRef = $input['content_ref'] ?? ''; + $comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef); return view('comments.comment-branch', [ 'readOnly' => false, diff --git a/resources/js/components/editor-toolbox.js b/resources/js/components/editor-toolbox.ts similarity index 61% rename from resources/js/components/editor-toolbox.js rename to resources/js/components/editor-toolbox.ts index 95339328542..60bdde05efb 100644 --- a/resources/js/components/editor-toolbox.js +++ b/resources/js/components/editor-toolbox.ts @@ -1,39 +1,49 @@ import {Component} from './component'; +export interface EditorToolboxChangeEventData { + tab: string; + open: boolean; +} + export class EditorToolbox extends Component { + protected container!: HTMLElement; + protected buttons!: HTMLButtonElement[]; + protected contentElements!: HTMLElement[]; + protected toggleButton!: HTMLElement; + protected editorWrapEl!: HTMLElement; + + protected open: boolean = false; + protected tab: string = ''; + setup() { // Elements this.container = this.$el; - this.buttons = this.$manyRefs.tabButton; + this.buttons = this.$manyRefs.tabButton as HTMLButtonElement[]; this.contentElements = this.$manyRefs.tabContent; this.toggleButton = this.$refs.toggle; - this.editorWrapEl = this.container.closest('.page-editor'); - - // State - this.open = false; - this.tab = ''; + this.editorWrapEl = this.container.closest('.page-editor') as HTMLElement; this.setupListeners(); // Set the first tab as active on load - this.setActiveTab(this.contentElements[0].dataset.tabContent); + this.setActiveTab(this.contentElements[0].dataset.tabContent || ''); } - setupListeners() { + protected setupListeners(): void { // Toolbox toggle button click this.toggleButton.addEventListener('click', () => this.toggle()); // Tab button click - this.container.addEventListener('click', event => { - const button = event.target.closest('button'); - if (this.buttons.includes(button)) { - const name = button.dataset.tab; + this.container.addEventListener('click', (event: MouseEvent) => { + const button = (event.target as HTMLElement).closest('button'); + if (button instanceof HTMLButtonElement && this.buttons.includes(button)) { + const name = button.dataset.tab || ''; this.setActiveTab(name, true); } }); } - toggle() { + protected toggle(): void { this.container.classList.toggle('open'); const isOpen = this.container.classList.contains('open'); this.toggleButton.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); @@ -42,7 +52,7 @@ export class EditorToolbox extends Component { this.emitState(); } - setActiveTab(tabName, openToolbox = false) { + protected setActiveTab(tabName: string, openToolbox: boolean = false): void { // Set button visibility for (const button of this.buttons) { button.classList.remove('active'); @@ -65,8 +75,9 @@ export class EditorToolbox extends Component { this.emitState(); } - emitState() { - this.$emit('change', {tab: this.tab, open: this.open}); + protected emitState(): void { + const data: EditorToolboxChangeEventData = {tab: this.tab, open: this.open}; + this.$emit('change', data); } } diff --git a/resources/js/components/page-comment-reference.ts b/resources/js/components/page-comment-reference.ts index 48fb8ee0a49..009e806c104 100644 --- a/resources/js/components/page-comment-reference.ts +++ b/resources/js/components/page-comment-reference.ts @@ -4,6 +4,8 @@ import {el} from "../wysiwyg/utils/dom"; import commentIcon from "@icons/comment.svg"; import closeIcon from "@icons/close.svg"; import {debounce, scrollAndHighlightElement} from "../services/util"; +import {EditorToolboxChangeEventData} from "./editor-toolbox"; +import {TabsChangeEvent} from "./tabs"; /** * Track the close function for the current open marker so it can be closed @@ -12,13 +14,13 @@ import {debounce, scrollAndHighlightElement} from "../services/util"; let openMarkerClose: Function|null = null; export class PageCommentReference extends Component { - protected link: HTMLLinkElement; - protected reference: string; + protected link!: HTMLLinkElement; + protected reference!: string; protected markerWrap: HTMLElement|null = null; - protected viewCommentText: string; - protected jumpToThreadText: string; - protected closeText: string; + protected viewCommentText!: string; + protected jumpToThreadText!: string; + protected closeText!: string; setup() { this.link = this.$el as HTMLLinkElement; @@ -31,15 +33,15 @@ export class PageCommentReference extends Component { this.showForDisplay(); // Handle editor view to show on comments toolbox view - window.addEventListener('editor-toolbox-change', (event) => { - const tabName: string = (event as {detail: {tab: string, open: boolean}}).detail.tab; - const isOpen = (event as {detail: {tab: string, open: boolean}}).detail.open; - if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { - this.showForEditor(); - } else { - this.hideMarker(); - } - }); + window.addEventListener('editor-toolbox-change', ((event: CustomEvent) => { + const tabName: string = event.detail.tab; + const isOpen = event.detail.open; + if (tabName === 'comments' && isOpen && this.link.checkVisibility()) { + this.showForEditor(); + } else { + this.hideMarker(); + } + }) as EventListener); // Handle visibility changes within editor toolbox archived details dropdown window.addEventListener('toggle', event => { @@ -55,8 +57,8 @@ export class PageCommentReference extends Component { }, {capture: true}); // Handle comments tab changes to hide/show markers & indicators - window.addEventListener('tabs-change', event => { - const sectionId = (event as {detail: {showing: string}}).detail.showing; + window.addEventListener('tabs-change', ((event: CustomEvent) => { + const sectionId = event.detail.showing; if (!sectionId.startsWith('comment-tab-panel')) { return; } @@ -67,7 +69,7 @@ export class PageCommentReference extends Component { } else { this.hideMarker(); } - }); + }) as EventListener); } public showForDisplay() { diff --git a/resources/js/components/page-comment.ts b/resources/js/components/page-comment.ts index ce35cdc4a24..0c3e19f4b1a 100644 --- a/resources/js/components/page-comment.ts +++ b/resources/js/components/page-comment.ts @@ -1,29 +1,39 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom'; import {buildForInput} from '../wysiwyg-tinymce/config'; import {PageCommentReference} from "./page-comment-reference"; +import {HttpError} from "../services/http"; + +export interface PageCommentReplyEventData { + id: string; // ID of comment being replied to + element: HTMLElement; // Container for comment replied to +} + +export interface PageCommentArchiveEventData { + new_thread_dom: HTMLElement; +} export class PageComment extends Component { - protected commentId: string; - protected commentLocalId: string; - protected deletedText: string; - protected updatedText: string; - protected archiveText: string; + protected commentId!: string; + protected commentLocalId!: string; + protected deletedText!: string; + protected updatedText!: string; + protected archiveText!: string; protected wysiwygEditor: any = null; - protected wysiwygLanguage: string; - protected wysiwygTextDirection: string; - - protected container: HTMLElement; - protected contentContainer: HTMLElement; - protected form: HTMLFormElement; - protected formCancel: HTMLElement; - protected editButton: HTMLElement; - protected deleteButton: HTMLElement; - protected replyButton: HTMLElement; - protected archiveButton: HTMLElement; - protected input: HTMLInputElement; + protected wysiwygLanguage!: string; + protected wysiwygTextDirection!: string; + + protected container!: HTMLElement; + protected contentContainer!: HTMLElement; + protected form!: HTMLFormElement; + protected formCancel!: HTMLElement; + protected editButton!: HTMLElement; + protected deleteButton!: HTMLElement; + protected replyButton!: HTMLElement; + protected archiveButton!: HTMLElement; + protected input!: HTMLInputElement; setup() { // Options @@ -53,10 +63,11 @@ export class PageComment extends Component { protected setupListeners(): void { if (this.replyButton) { - this.replyButton.addEventListener('click', () => this.$emit('reply', { + const data: PageCommentReplyEventData = { id: this.commentLocalId, element: this.container, - })); + }; + this.replyButton.addEventListener('click', () => this.$emit('reply', data)); } if (this.editButton) { @@ -95,10 +106,10 @@ export class PageComment extends Component { drawioUrl: '', pageId: 0, translations: {}, - translationMap: (window as Record).editor_translations, + translationMap: (window as unknown as Record).editor_translations, }); - (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { + (window as unknown as {tinymce: {init: (arg0: Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); @@ -120,7 +131,9 @@ export class PageComment extends Component { window.$events.success(this.updatedText); } catch (err) { console.error(err); - window.$events.showValidationErrors(err); + if (err instanceof HttpError) { + window.$events.showValidationErrors(err); + } this.form.toggleAttribute('hidden', false); loading.remove(); } @@ -151,7 +164,8 @@ export class PageComment extends Component { const response = await window.$http.put(`/comment/${this.commentId}/${action}`); window.$events.success(this.archiveText); - this.$emit(action, {new_thread_dom: htmlToDom(response.data as string)}); + const eventData: PageCommentArchiveEventData = {new_thread_dom: htmlToDom(response.data as string)}; + this.$emit(action, eventData); const branch = this.container.closest('.comment-branch') as HTMLElement; const references = window.$components.allWithinElement(branch, 'page-comment-reference'); diff --git a/resources/js/components/page-comments.ts b/resources/js/components/page-comments.ts index 04c8125808c..94f5ab3bb8e 100644 --- a/resources/js/components/page-comments.ts +++ b/resources/js/components/page-comments.ts @@ -1,50 +1,38 @@ import {Component} from './component'; -import {getLoading, htmlToDom} from '../services/dom.ts'; +import {getLoading, htmlToDom} from '../services/dom'; import {buildForInput} from '../wysiwyg-tinymce/config'; import {Tabs} from "./tabs"; import {PageCommentReference} from "./page-comment-reference"; import {scrollAndHighlightElement} from "../services/util"; - -export interface CommentReplyEvent extends Event { - detail: { - id: string; // ID of comment being replied to - element: HTMLElement; // Container for comment replied to - } -} - -export interface ArchiveEvent extends Event { - detail: { - new_thread_dom: HTMLElement; - } -} +import {PageCommentArchiveEventData, PageCommentReplyEventData} from "./page-comment"; export class PageComments extends Component { - private elem: HTMLElement; - private pageId: number; - private container: HTMLElement; - private commentCountBar: HTMLElement; - private activeTab: HTMLElement; - private archivedTab: HTMLElement; - private addButtonContainer: HTMLElement; - private archiveContainer: HTMLElement; - private replyToRow: HTMLElement; - private referenceRow: HTMLElement; - private formContainer: HTMLElement; - private form: HTMLFormElement; - private formInput: HTMLInputElement; - private formReplyLink: HTMLAnchorElement; - private formReferenceLink: HTMLAnchorElement; - private addCommentButton: HTMLElement; - private hideFormButton: HTMLElement; - private removeReplyToButton: HTMLElement; - private removeReferenceButton: HTMLElement; - private wysiwygLanguage: string; - private wysiwygTextDirection: string; + private elem!: HTMLElement; + private pageId!: number; + private container!: HTMLElement; + private commentCountBar!: HTMLElement; + private activeTab!: HTMLElement; + private archivedTab!: HTMLElement; + private addButtonContainer!: HTMLElement; + private archiveContainer!: HTMLElement; + private replyToRow!: HTMLElement; + private referenceRow!: HTMLElement; + private formContainer!: HTMLElement; + private form!: HTMLFormElement; + private formInput!: HTMLInputElement; + private formReplyLink!: HTMLAnchorElement; + private formReferenceLink!: HTMLAnchorElement; + private addCommentButton!: HTMLElement; + private hideFormButton!: HTMLElement; + private removeReplyToButton!: HTMLElement; + private removeReferenceButton!: HTMLElement; + private wysiwygLanguage!: string; + private wysiwygTextDirection!: string; private wysiwygEditor: any = null; - private createdText: string; - private countText: string; - private archivedCountText: string; + private createdText!: string; + private countText!: string; + private archivedCountText!: string; private parentId: number | null = null; private contentReference: string = ''; private formReplyText: string = ''; @@ -92,19 +80,19 @@ export class PageComments extends Component { this.hideForm(); }); - this.elem.addEventListener('page-comment-reply', (event: CommentReplyEvent) => { + this.elem.addEventListener('page-comment-reply', ((event: CustomEvent) => { this.setReply(event.detail.id, event.detail.element); - }); + }) as EventListener); - this.elem.addEventListener('page-comment-archive', (event: ArchiveEvent) => { + this.elem.addEventListener('page-comment-archive', ((event: CustomEvent) => { this.archiveContainer.append(event.detail.new_thread_dom); setTimeout(() => this.updateCount(), 1); - }); + }) as EventListener); - this.elem.addEventListener('page-comment-unarchive', (event: ArchiveEvent) => { + this.elem.addEventListener('page-comment-unarchive', ((event: CustomEvent) => { this.container.append(event.detail.new_thread_dom); setTimeout(() => this.updateCount(), 1); - }); + }) as EventListener); if (this.form) { this.removeReplyToButton.addEventListener('click', this.removeReplyTo.bind(this)); @@ -115,7 +103,7 @@ export class PageComments extends Component { } } - protected saveComment(event): void { + protected saveComment(event: SubmitEvent): void { event.preventDefault(); event.stopPropagation(); @@ -209,10 +197,10 @@ export class PageComments extends Component { drawioUrl: '', pageId: 0, translations: {}, - translationMap: (window as Record).editor_translations, + translationMap: (window as unknown as Record).editor_translations, }); - (window as {tinymce: {init: (Object) => Promise}}).tinymce.init(config).then(editors => { + (window as unknown as {tinymce: {init: (arg0: Object) => Promise}}).tinymce.init(config).then(editors => { this.wysiwygEditor = editors[0]; setTimeout(() => this.wysiwygEditor.focus(), 50); }); @@ -233,11 +221,11 @@ export class PageComments extends Component { return this.archiveContainer.querySelectorAll(':scope > .comment-branch').length; } - protected setReply(commentLocalId, commentElement): void { - const targetFormLocation = commentElement.closest('.comment-branch').querySelector('.comment-branch-children'); + protected setReply(commentLocalId: string, commentElement: HTMLElement): void { + const targetFormLocation = (commentElement.closest('.comment-branch') as HTMLElement).querySelector('.comment-branch-children') as HTMLElement; targetFormLocation.append(this.formContainer); this.showForm(); - this.parentId = commentLocalId; + this.parentId = Number(commentLocalId); this.replyToRow.toggleAttribute('hidden', false); this.formReplyLink.textContent = this.formReplyText.replace('1234', String(this.parentId)); this.formReplyLink.href = `#comment${this.parentId}`; diff --git a/resources/js/components/pointer.ts b/resources/js/components/pointer.ts index d84186d872d..4b927045aae 100644 --- a/resources/js/components/pointer.ts +++ b/resources/js/components/pointer.ts @@ -1,7 +1,7 @@ -import * as DOM from '../services/dom.ts'; +import * as DOM from '../services/dom'; import {Component} from './component'; -import {copyTextToClipboard} from '../services/clipboard.ts'; -import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom.ts"; +import {copyTextToClipboard} from '../services/clipboard'; +import {hashElement, normalizeNodeTextOffsetToParent} from "../services/dom"; import {PageComments} from "./page-comments"; export class Pointer extends Component { @@ -11,16 +11,16 @@ export class Pointer extends Component { protected targetElement: HTMLElement|null = null; protected targetSelectionRange: Range|null = null; - protected pointer: HTMLElement; - protected linkInput: HTMLInputElement; - protected linkButton: HTMLElement; - protected includeInput: HTMLInputElement; - protected includeButton: HTMLElement; - protected sectionModeButton: HTMLElement; - protected commentButton: HTMLElement; - protected modeToggles: HTMLElement[]; - protected modeSections: HTMLElement[]; - protected pageId: string; + protected pointer!: HTMLElement; + protected linkInput!: HTMLInputElement; + protected linkButton!: HTMLElement; + protected includeInput!: HTMLInputElement; + protected includeButton!: HTMLElement; + protected sectionModeButton!: HTMLElement; + protected commentButton!: HTMLElement; + protected modeToggles!: HTMLElement[]; + protected modeSections!: HTMLElement[]; + protected pageId!: string; setup() { this.pointer = this.$refs.pointer; @@ -67,7 +67,7 @@ export class Pointer extends Component { DOM.onEvents(pageContent, ['mouseup', 'keyup'], event => { event.stopPropagation(); const targetEl = (event.target as HTMLElement).closest('[id^="bkmrk"]'); - if (targetEl && window.getSelection().toString().length > 0) { + if (targetEl instanceof HTMLElement && (window.getSelection() || '').toString().length > 0) { const xPos = (event instanceof MouseEvent) ? event.pageX : 0; this.showPointerAtTarget(targetEl, xPos, false); } @@ -102,11 +102,8 @@ export class Pointer extends Component { /** * Move and display the pointer at the given element, targeting the given screen x-position if possible. - * @param {Element} element - * @param {Number} xPosition - * @param {Boolean} keyboardMode */ - showPointerAtTarget(element, xPosition, keyboardMode) { + showPointerAtTarget(element: HTMLElement, xPosition: number, keyboardMode: boolean) { this.targetElement = element; this.targetSelectionRange = window.getSelection()?.getRangeAt(0) || null; this.updateDomForTarget(element); @@ -134,7 +131,7 @@ export class Pointer extends Component { window.removeEventListener('scroll', scrollListener); }; - element.parentElement.insertBefore(this.pointer, element); + element.parentElement?.insertBefore(this.pointer, element); if (!keyboardMode) { window.addEventListener('scroll', scrollListener, {passive: true}); } @@ -142,9 +139,8 @@ export class Pointer extends Component { /** * Update the pointer inputs/content for the given target element. - * @param {?Element} element */ - updateDomForTarget(element) { + updateDomForTarget(element: HTMLElement) { const permaLink = window.baseUrl(`/link/${this.pageId}#${element.id}`); const includeTag = `{{@${this.pageId}#${element.id}}}`; @@ -158,13 +154,13 @@ export class Pointer extends Component { const elementId = element.id; // Get the first 50 characters. - const queryContent = element.textContent && element.textContent.substring(0, 50); + const queryContent = (element.textContent || '').substring(0, 50); editAnchor.href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`; } } enterSectionSelectMode() { - const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]')); + const sections = Array.from(document.querySelectorAll('.page-content [id^="bkmrk"]')) as HTMLElement[]; for (const section of sections) { section.setAttribute('tabindex', '0'); } @@ -172,12 +168,12 @@ export class Pointer extends Component { sections[0].focus(); DOM.onEnterPress(sections, event => { - this.showPointerAtTarget(event.target, 0, true); + this.showPointerAtTarget(event.target as HTMLElement, 0, true); this.pointer.focus(); }); } - createCommentAtPointer(event) { + createCommentAtPointer() { if (!this.targetElement) { return; } diff --git a/resources/js/components/tabs.ts b/resources/js/components/tabs.ts index 56405b8c78e..a03d37cd48c 100644 --- a/resources/js/components/tabs.ts +++ b/resources/js/components/tabs.ts @@ -1,5 +1,9 @@ import {Component} from './component'; +export interface TabsChangeEvent { + showing: string; +} + /** * Tabs * Uses accessible attributes to drive its functionality. @@ -19,12 +23,12 @@ import {Component} from './component'; */ export class Tabs extends Component { - protected container: HTMLElement; - protected tabList: HTMLElement; - protected tabs: HTMLElement[]; - protected panels: HTMLElement[]; + protected container!: HTMLElement; + protected tabList!: HTMLElement; + protected tabs!: HTMLElement[]; + protected panels!: HTMLElement[]; - protected activeUnder: number; + protected activeUnder!: number; protected active: null|boolean = null; setup() { @@ -58,7 +62,8 @@ export class Tabs extends Component { tab.setAttribute('aria-selected', selected ? 'true' : 'false'); } - this.$emit('change', {showing: sectionId}); + const data: TabsChangeEvent = {showing: sectionId}; + this.$emit('change', data); } protected updateActiveState(): void { diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 77c19a76105..c3817536c85 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -225,7 +225,7 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number) if (currentNode.nodeType === Node.TEXT_NODE) { // For text nodes, count the length of their content // Returns if within range - const textLength = currentNode.textContent.length; + const textLength = (currentNode.textContent || '').length; if (currentOffset + textLength >= offset) { return { node: currentNode, @@ -237,9 +237,9 @@ export function findTargetNodeAndOffset(parentNode: HTMLElement, offset: number) } else if (currentNode.nodeType === Node.ELEMENT_NODE) { // Otherwise, if an element, track the text length and search within // if in range for the target offset - const elementTextLength = currentNode.textContent.length; + const elementTextLength = (currentNode.textContent || '').length; if (currentOffset + elementTextLength >= offset) { - return findTargetNodeAndOffset(currentNode, offset - currentOffset); + return findTargetNodeAndOffset(currentNode as HTMLElement, offset - currentOffset); } currentOffset += elementTextLength; diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts index 7dae6dc29d9..6045d51f823 100644 --- a/resources/js/services/events.ts +++ b/resources/js/services/events.ts @@ -1,7 +1,9 @@ import {HttpError} from "./http"; +type Listener = (data: any) => void; + export class EventManager { - protected listeners: Record void)[]> = {}; + protected listeners: Record = {}; protected stack: {name: string, data: {}}[] = []; /** @@ -27,7 +29,7 @@ export class EventManager { /** * Remove an event listener which is using the given callback for the given event name. */ - remove(eventName: string, callback: Function): void { + remove(eventName: string, callback: Listener): void { const listeners = this.listeners[eventName] || []; const index = listeners.indexOf(callback); if (index !== -1) { @@ -64,8 +66,7 @@ export class EventManager { /** * Notify of standard server-provided validation errors. */ - showValidationErrors(responseErr: {status?: number, data?: object}): void { - if (!responseErr.status) return; + showValidationErrors(responseErr: HttpError): void { if (responseErr.status === 422 && responseErr.data) { const message = Object.values(responseErr.data).flat().join('\n'); this.error(message); From 32b29fcdfc9e54416d5c01cf54b8a9df6753d326 Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Tue, 13 May 2025 12:03:15 +0100 Subject: [PATCH 046/474] Comments: Fixed pointer display, Fixed translation test --- resources/js/services/__tests__/translations.test.ts | 5 +++++ resources/js/services/translations.ts | 2 +- resources/sass/_pages.scss | 12 +++++------- resources/views/pages/parts/pointer.blade.php | 10 +++++----- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/resources/js/services/__tests__/translations.test.ts b/resources/js/services/__tests__/translations.test.ts index 043f1745ff6..5014051ab04 100644 --- a/resources/js/services/__tests__/translations.test.ts +++ b/resources/js/services/__tests__/translations.test.ts @@ -58,6 +58,11 @@ describe('Translations Service', () => { expect(caseB).toEqual('an orange angry big dinosaur'); }); + test('it provides count as a replacement by default', () => { + const caseA = $trans.choice(`:count cats|:count dogs`, 4); + expect(caseA).toEqual('4 dogs'); + }); + test('not provided replacements are left as-is', () => { const caseA = $trans.choice(`An :a dog`, 5, {}); expect(caseA).toEqual('An :a dog'); diff --git a/resources/js/services/translations.ts b/resources/js/services/translations.ts index 821c34f18f7..f548a51d1d1 100644 --- a/resources/js/services/translations.ts +++ b/resources/js/services/translations.ts @@ -10,7 +10,7 @@ export class Translator { * to use. Similar format at Laravel's 'trans_choice' helper. */ choice(translation: string, count: number, replacements: Record = {}): string { - replacements = Object.assign({}, replacements, {count: String(count)}); + replacements = Object.assign({}, {count: String(count)}, replacements); const splitText = translation.split('|'); const exactCountRegex = /^{([0-9]+)}/; const rangeRegex = /^\[([0-9]+),([0-9*]+)]/; diff --git a/resources/sass/_pages.scss b/resources/sass/_pages.scss index 621d08f45f2..83aec46f093 100755 --- a/resources/sass/_pages.scss +++ b/resources/sass/_pages.scss @@ -158,11 +158,7 @@ body.tox-fullscreen, body.markdown-fullscreen { border-radius: 4px; box-shadow: 0 0 12px 1px rgba(0, 0, 0, 0.1); @include mixins.lightDark(background-color, #fff, #333); - width: 275px; - - &.is-page-editable { - width: 328px; - } + width: 328px; &:before { position: absolute; @@ -193,7 +189,8 @@ body.tox-fullscreen, body.markdown-fullscreen { border: 1px solid #DDD; @include mixins.lightDark(border-color, #ddd, #000); color: #666; - width: 180px; + width: auto; + flex: 1; z-index: 58; padding: 5px; border-radius: 0; @@ -203,7 +200,8 @@ body.tox-fullscreen, body.markdown-fullscreen { } .input-group .button { line-height: 1; - margin: 0 0 0 -5px; + margin-inline-start: -1px; + margin-block: 0; box-shadow: none; border-radius: 0; } diff --git a/resources/views/pages/parts/pointer.blade.php b/resources/views/pages/parts/pointer.blade.php index 77fc763827e..f6487b66600 100644 --- a/resources/views/pages/parts/pointer.blade.php +++ b/resources/views/pages/parts/pointer.blade.php @@ -6,21 +6,21 @@ tabindex="-1" aria-label="{{ trans('entities.pages_pointer_label') }}" class="pointer-container"> -
                                          -
                                          +
                                          +
                                          -
                                          +
                                          -