]> BookStack Code Mirror - bookstack/commitdiff
Merge branch 'add-priority' into development
authorDan Brown <redacted>
Mon, 21 Aug 2023 14:43:16 +0000 (15:43 +0100)
committerDan Brown <redacted>
Mon, 21 Aug 2023 14:43:16 +0000 (15:43 +0100)
151 files changed:
.github/translators.txt
app/Activity/ActivityType.php
app/Activity/CommentRepo.php
app/Activity/Controllers/WatchController.php [new file with mode: 0644]
app/Activity/DispatchWebhookJob.php
app/Activity/Models/Comment.php
app/Activity/Models/Watch.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/BaseNotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/NotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php [new file with mode: 0644]
app/Activity/Notifications/MessageParts/LinkedMailMessageLine.php [new file with mode: 0644]
app/Activity/Notifications/MessageParts/ListMessageLine.php [new file with mode: 0644]
app/Activity/Notifications/Messages/BaseActivityNotification.php [new file with mode: 0644]
app/Activity/Notifications/Messages/CommentCreationNotification.php [new file with mode: 0644]
app/Activity/Notifications/Messages/PageCreationNotification.php [new file with mode: 0644]
app/Activity/Notifications/Messages/PageUpdateNotification.php [new file with mode: 0644]
app/Activity/Notifications/NotificationManager.php [new file with mode: 0644]
app/Activity/Tools/ActivityLogger.php
app/Activity/Tools/EntityWatchers.php [new file with mode: 0644]
app/Activity/Tools/UserEntityWatchOptions.php [new file with mode: 0644]
app/Activity/Tools/WatchedParentDetails.php [new file with mode: 0644]
app/Activity/Tools/WebhookFormatter.php
app/Activity/WatchLevels.php [new file with mode: 0644]
app/Api/ApiToken.php
app/App/Providers/AppServiceProvider.php
app/Entities/Controllers/BookController.php
app/Entities/Controllers/ChapterController.php
app/Entities/Controllers/PageController.php
app/Entities/Queries/TopFavourites.php
app/Http/Controller.php
app/Permissions/PermissionApplicator.php
app/Permissions/PermissionsRepo.php
app/Settings/StatusController.php
app/Settings/UserNotificationPreferences.php [new file with mode: 0644]
app/Theming/ThemeEvents.php
app/Users/Controllers/RoleController.php
app/Users/Controllers/UserPreferencesController.php
app/Users/Models/User.php
composer.lock
database/factories/Api/ApiTokenFactory.php [new file with mode: 0644]
database/migrations/2023_06_25_181952_remove_bookshelf_create_entity_permissions.php
database/migrations/2023_07_25_124945_add_receive_notifications_role_permissions.php [new file with mode: 0644]
database/migrations/2023_07_31_104430_create_watches_table.php [new file with mode: 0644]
database/seeders/DummyContentSeeder.php
dev/docs/development.md
lang/ar/entities.php
lang/bg/entities.php
lang/bs/entities.php
lang/ca/entities.php
lang/cs/entities.php
lang/cy/entities.php
lang/da/entities.php
lang/de/entities.php
lang/de_informal/entities.php
lang/el/entities.php
lang/en/activities.php
lang/en/common.php
lang/en/entities.php
lang/en/notifications.php [new file with mode: 0644]
lang/en/preferences.php
lang/en/settings.php
lang/es/entities.php
lang/es_AR/entities.php
lang/et/entities.php
lang/eu/entities.php
lang/fa/entities.php
lang/fr/activities.php
lang/fr/entities.php
lang/fr/errors.php
lang/he/entities.php
lang/hr/editor.php
lang/hr/entities.php
lang/hr/passwords.php
lang/hr/preferences.php
lang/hr/settings.php
lang/hu/entities.php
lang/id/entities.php
lang/it/entities.php
lang/ja/activities.php
lang/ja/entities.php
lang/ka/entities.php
lang/ko/entities.php
lang/lt/entities.php
lang/lv/entities.php
lang/nb/entities.php
lang/nl/activities.php
lang/nl/common.php
lang/nl/components.php
lang/nl/entities.php
lang/nl/errors.php
lang/pl/entities.php
lang/pt/activities.php
lang/pt/entities.php
lang/pt/errors.php
lang/pt_BR/entities.php
lang/ro/entities.php
lang/ru/entities.php
lang/sk/entities.php
lang/sl/entities.php
lang/sv/entities.php
lang/tr/activities.php
lang/tr/auth.php
lang/tr/common.php
lang/tr/components.php
lang/tr/editor.php
lang/tr/entities.php
lang/tr/errors.php
lang/tr/preferences.php
lang/tr/settings.php
lang/uk/entities.php
lang/uz/entities.php
lang/vi/entities.php
lang/zh_CN/entities.php
lang/zh_TW/entities.php
package-lock.json
package.json
resources/icons/user-preferences.svg [new file with mode: 0644]
resources/icons/watch-ignore.svg [new file with mode: 0644]
resources/icons/watch.svg [moved from resources/icons/view.svg with 85% similarity]
resources/js/components/dropdown.js
resources/sass/_layout.scss
resources/sass/_lists.scss
resources/sass/_text.scss
resources/sass/_variables.scss
resources/views/books/show.blade.php
resources/views/chapters/show.blade.php
resources/views/common/header.blade.php
resources/views/entities/icon-link.blade.php [new file with mode: 0644]
resources/views/entities/meta.blade.php
resources/views/entities/watch-action.blade.php [new file with mode: 0644]
resources/views/entities/watch-controls.blade.php [new file with mode: 0644]
resources/views/form/entity-permissions-row.blade.php
resources/views/form/entity-permissions.blade.php
resources/views/pages/revision.blade.php
resources/views/pages/show.blade.php
resources/views/settings/audit.blade.php
resources/views/settings/roles/parts/form.blade.php
resources/views/shelves/show.blade.php
resources/views/users/preferences/index.blade.php [new file with mode: 0644]
resources/views/users/preferences/notifications.blade.php [new file with mode: 0644]
resources/views/vendor/notifications/email.blade.php
routes/web.php
tests/Actions/WebhookCallTest.php
tests/Activity/WatchTest.php [new file with mode: 0644]
tests/Entity/CommentTest.php
tests/Helpers/UserRoleProvider.php
tests/Permissions/EntityPermissionsTest.php
tests/TestCase.php
tests/User/UserPreferencesTest.php

index 83841050b3140019ec765c5c13088252c23b433e..74279263fdeae2e12bd6a2ce934fed9ce156f8d3 100644 (file)
@@ -342,3 +342,5 @@ Eugene Pershin (SilentEugene) :: Russian
 周盛道 (zhoushengdao) :: Chinese Simplified
 hamidreza amini (hamidrezaamini2022) :: Persian
 Tomislav Kraljević (tomislav.kraljevic) :: Croatian
+Taygun Yıldırım (yildirimtaygun) :: Turkish
+robing29 :: German
index 3018df1d3d0e1e7c60ee29d52cdbb291f7c84e95..09b2ae73c561e9b8be68292f370d13afa5dd4de9 100644 (file)
@@ -27,6 +27,10 @@ class ActivityType
     const BOOKSHELF_DELETE = 'bookshelf_delete';
 
     const COMMENTED_ON = 'commented_on';
+    const COMMENT_CREATE = 'comment_create';
+    const COMMENT_UPDATE = 'comment_update';
+    const COMMENT_DELETE = 'comment_delete';
+
     const PERMISSIONS_UPDATE = 'permissions_update';
 
     const REVISION_RESTORE = 'revision_restore';
index 2aabab79de3b8af0e01cf500ea65e7db53cf7fd4..ce2950e4d798b47c7f19286dc556f9e8557eb5dd 100644 (file)
@@ -33,6 +33,7 @@ class CommentRepo
         $comment->parent_id = $parent_id;
 
         $entity->comments()->save($comment);
+        ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
         ActivityService::add(ActivityType::COMMENTED_ON, $entity);
 
         return $comment;
@@ -48,6 +49,8 @@ class CommentRepo
         $comment->html = $this->commentToHtml($text);
         $comment->save();
 
+        ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
+
         return $comment;
     }
 
@@ -57,6 +60,8 @@ class CommentRepo
     public function delete(Comment $comment): void
     {
         $comment->delete();
+
+        ActivityService::add(ActivityType::COMMENT_DELETE, $comment);
     }
 
     /**
diff --git a/app/Activity/Controllers/WatchController.php b/app/Activity/Controllers/WatchController.php
new file mode 100644 (file)
index 0000000..3d7e181
--- /dev/null
@@ -0,0 +1,65 @@
+<?php
+
+namespace BookStack\Activity\Controllers;
+
+use BookStack\Activity\Tools\UserEntityWatchOptions;
+use BookStack\App\Model;
+use BookStack\Entities\Models\Entity;
+use BookStack\Http\Controller;
+use Exception;
+use Illuminate\Http\Request;
+use Illuminate\Validation\ValidationException;
+
+class WatchController extends Controller
+{
+    public function update(Request $request)
+    {
+        $this->checkPermission('receive-notifications');
+        $this->preventGuestAccess();
+
+        $requestData = $this->validate($request, [
+            'level' => ['required', 'string'],
+        ]);
+
+        $watchable = $this->getValidatedModelFromRequest($request);
+        $watchOptions = new UserEntityWatchOptions(user(), $watchable);
+        $watchOptions->updateLevelByName($requestData['level']);
+
+        $this->showSuccessNotification(trans('activities.watch_update_level_notification'));
+
+        return redirect()->back();
+    }
+
+    /**
+     * @throws ValidationException
+     * @throws Exception
+     */
+    protected function getValidatedModelFromRequest(Request $request): Entity
+    {
+        $modelInfo = $this->validate($request, [
+            'type' => ['required', 'string'],
+            'id'   => ['required', 'integer'],
+        ]);
+
+        if (!class_exists($modelInfo['type'])) {
+            throw new Exception('Model not found');
+        }
+
+        /** @var Model $model */
+        $model = new $modelInfo['type']();
+        if (!$model instanceof Entity) {
+            throw new Exception('Model not an entity');
+        }
+
+        $modelInstance = $model->newQuery()
+            ->where('id', '=', $modelInfo['id'])
+            ->first(['id', 'name', 'owned_by']);
+
+        $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
+        if (is_null($modelInstance) || $inaccessibleEntity) {
+            throw new Exception('Model instance not found');
+        }
+
+        return $modelInstance;
+    }
+}
index 5e6380f439d6f2eb29403e26905b9a4be9e71988..f2330c4faf967f257fd94534b98d2adf9b448439 100644 (file)
@@ -24,27 +24,23 @@ class DispatchWebhookJob implements ShouldQueue
     use SerializesModels;
 
     protected Webhook $webhook;
-    protected string $event;
     protected User $initiator;
     protected int $initiatedTime;
-
-    /**
-     * @var string|Loggable
-     */
-    protected $detail;
+    protected array $webhookData;
 
     /**
      * Create a new job instance.
      *
      * @return void
      */
-    public function __construct(Webhook $webhook, string $event, $detail)
+    public function __construct(Webhook $webhook, string $event, Loggable|string $detail)
     {
         $this->webhook = $webhook;
-        $this->event = $event;
-        $this->detail = $detail;
         $this->initiator = user();
         $this->initiatedTime = time();
+
+        $themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $event, $this->webhook, $detail, $this->initiator, $this->initiatedTime);
+        $this->webhookData =  $themeResponse ?? WebhookFormatter::getDefault($event, $this->webhook, $detail, $this->initiator, $this->initiatedTime)->format();
     }
 
     /**
@@ -54,15 +50,13 @@ class DispatchWebhookJob implements ShouldQueue
      */
     public function handle()
     {
-        $themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime);
-        $webhookData = $themeResponse ?? WebhookFormatter::getDefault($this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime)->format();
         $lastError = null;
 
         try {
             $response = Http::asJson()
                 ->withOptions(['allow_redirects' => ['strict' => true]])
                 ->timeout($this->webhook->timeout)
-                ->post($this->webhook->endpoint, $webhookData);
+                ->post($this->webhook->endpoint, $this->webhookData);
         } catch (\Exception $exception) {
             $lastError = $exception->getMessage();
             Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
index e513bdf3d57cffcb4fd87aa44b1dcfab68a4b32b..bcbed6c56f03d7fd3dfe753b395c9dc6d2fc5d5e 100644 (file)
@@ -5,6 +5,7 @@ namespace BookStack\Activity\Models;
 use BookStack\App\Model;
 use BookStack\Users\Models\HasCreatorAndUpdater;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\MorphTo;
 
 /**
@@ -13,8 +14,10 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
  * @property string   $html
  * @property int|null $parent_id
  * @property int      $local_id
+ * @property string   $entity_type
+ * @property int      $entity_id
  */
-class Comment extends Model
+class Comment extends Model implements Loggable
 {
     use HasFactory;
     use HasCreatorAndUpdater;
@@ -30,6 +33,14 @@ class Comment extends Model
         return $this->morphTo('entity');
     }
 
+    /**
+     * Get the parent comment this is in reply to (if existing).
+     */
+    public function parent(): BelongsTo
+    {
+        return $this->belongsTo(Comment::class);
+    }
+
     /**
      * Check if a comment has been updated since creation.
      */
@@ -40,21 +51,22 @@ class Comment extends Model
 
     /**
      * Get created date as a relative diff.
-     *
-     * @return mixed
      */
-    public function getCreatedAttribute()
+    public function getCreatedAttribute(): string
     {
         return $this->created_at->diffForHumans();
     }
 
     /**
      * Get updated date as a relative diff.
-     *
-     * @return mixed
      */
-    public function getUpdatedAttribute()
+    public function getUpdatedAttribute(): string
     {
         return $this->updated_at->diffForHumans();
     }
+
+    public function logDescriptor(): string
+    {
+        return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
+    }
 }
diff --git a/app/Activity/Models/Watch.php b/app/Activity/Models/Watch.php
new file mode 100644 (file)
index 0000000..dfb72cc
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+
+namespace BookStack\Activity\Models;
+
+use BookStack\Activity\WatchLevels;
+use BookStack\Permissions\Models\JointPermission;
+use Carbon\Carbon;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
+
+/**
+ * @property int $id
+ * @property int $user_id
+ * @property int $watchable_id
+ * @property string $watchable_type
+ * @property int $level
+ * @property Carbon $created_at
+ * @property Carbon $updated_at
+ */
+class Watch extends Model
+{
+    protected $guarded = [];
+
+    public function watchable(): MorphTo
+    {
+        return $this->morphTo();
+    }
+
+    public function jointPermissions(): HasMany
+    {
+        return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
+            ->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');
+    }
+
+    public function getLevelName(): string
+    {
+        return WatchLevels::levelValueToName($this->level);
+    }
+
+    public function ignoring(): bool
+    {
+        return $this->level === WatchLevels::IGNORE;
+    }
+}
diff --git a/app/Activity/Notifications/Handlers/BaseNotificationHandler.php b/app/Activity/Notifications/Handlers/BaseNotificationHandler.php
new file mode 100644 (file)
index 0000000..b5f339b
--- /dev/null
@@ -0,0 +1,42 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
+use BookStack\Entities\Models\Entity;
+use BookStack\Permissions\PermissionApplicator;
+use BookStack\Users\Models\User;
+
+abstract class BaseNotificationHandler implements NotificationHandler
+{
+    /**
+     * @param class-string<BaseActivityNotification> $notification
+     * @param int[] $userIds
+     */
+    protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void
+    {
+        $users = User::query()->whereIn('id', array_unique($userIds))->get();
+
+        foreach ($users as $user) {
+            // Prevent sending to the user that initiated the activity
+            if ($user->id === $initiator->id) {
+                continue;
+            }
+
+            // Prevent sending of the user does not have notification permissions
+            if (!$user->can('receive-notifications')) {
+                continue;
+            }
+
+            // Prevent sending if the user does not have access to the related content
+            $permissions = new PermissionApplicator($user);
+            if (!$permissions->checkOwnableUserAccess($relatedModel, 'view')) {
+                continue;
+            }
+
+            // Send the notification
+            $user->notify(new $notification($detail, $initiator));
+        }
+    }
+}
diff --git a/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php b/app/Activity/Notifications/Handlers/CommentCreationNotificationHandler.php
new file mode 100644 (file)
index 0000000..bc12c85
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\Models\Activity;
+use BookStack\Activity\Models\Comment;
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Page;
+use BookStack\Settings\UserNotificationPreferences;
+use BookStack\Users\Models\User;
+
+class CommentCreationNotificationHandler extends BaseNotificationHandler
+{
+    public function handle(Activity $activity, Loggable|string $detail, User $user): void
+    {
+        if (!($detail instanceof Comment)) {
+            throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
+        }
+
+        // Main watchers
+        /** @var Page $page */
+        $page = $detail->entity;
+        $watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
+        $watcherIds = $watchers->getWatcherUserIds();
+
+        // Page owner if user preferences allow
+        if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
+            $userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
+            if ($userNotificationPrefs->notifyOnOwnPageComments()) {
+                $watcherIds[] = $page->owned_by;
+            }
+        }
+
+        // Parent comment creator if preferences allow
+        $parentComment = $detail->parent()->first();
+        if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
+            $parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
+            if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
+                $watcherIds[] = $parentComment->created_by;
+            }
+        }
+
+        $this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
+    }
+}
diff --git a/app/Activity/Notifications/Handlers/NotificationHandler.php b/app/Activity/Notifications/Handlers/NotificationHandler.php
new file mode 100644 (file)
index 0000000..8c54986
--- /dev/null
@@ -0,0 +1,17 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\Models\Activity;
+use BookStack\Activity\Models\Loggable;
+use BookStack\Users\Models\User;
+
+interface NotificationHandler
+{
+    /**
+     * Run this handler.
+     * Provides the activity, related activity detail/model
+     * along with the user that triggered the activity.
+     */
+    public function handle(Activity $activity, string|Loggable $detail, User $user): void;
+}
diff --git a/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php b/app/Activity/Notifications/Handlers/PageCreationNotificationHandler.php
new file mode 100644 (file)
index 0000000..2492021
--- /dev/null
@@ -0,0 +1,24 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\Models\Activity;
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\PageCreationNotification;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Page;
+use BookStack\Users\Models\User;
+
+class PageCreationNotificationHandler extends BaseNotificationHandler
+{
+    public function handle(Activity $activity, Loggable|string $detail, User $user): void
+    {
+        if (!($detail instanceof Page)) {
+            throw new \InvalidArgumentException("Detail for page create notifications must be a page");
+        }
+
+        $watchers = new EntityWatchers($detail, WatchLevels::NEW);
+        $this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail, $detail);
+    }
+}
diff --git a/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php b/app/Activity/Notifications/Handlers/PageUpdateNotificationHandler.php
new file mode 100644 (file)
index 0000000..744aba1
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Handlers;
+
+use BookStack\Activity\ActivityType;
+use BookStack\Activity\Models\Activity;
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
+use BookStack\Activity\Tools\EntityWatchers;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Page;
+use BookStack\Settings\UserNotificationPreferences;
+use BookStack\Users\Models\User;
+
+class PageUpdateNotificationHandler extends BaseNotificationHandler
+{
+    public function handle(Activity $activity, Loggable|string $detail, User $user): void
+    {
+        if (!($detail instanceof Page)) {
+            throw new \InvalidArgumentException("Detail for page update notifications must be a page");
+        }
+
+        // Get last update from activity
+        $lastUpdate = $detail->activity()
+            ->where('type', '=', ActivityType::PAGE_UPDATE)
+            ->where('id', '!=', $activity->id)
+            ->latest('created_at')
+            ->first();
+
+        // Return if the same user has already updated the page in the last 15 mins
+        if ($lastUpdate && $lastUpdate->user_id === $user->id) {
+            if ($lastUpdate->created_at->gt(now()->subMinutes(15))) {
+                return;
+            }
+        }
+
+        // Get active watchers
+        $watchers = new EntityWatchers($detail, WatchLevels::UPDATES);
+        $watcherIds = $watchers->getWatcherUserIds();
+
+        // Add page owner if preferences allow
+        if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
+            $userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
+            if ($userNotificationPrefs->notifyOnOwnPageChanges()) {
+                $watcherIds[] = $detail->owned_by;
+            }
+        }
+
+        $this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail, $detail);
+    }
+}
diff --git a/app/Activity/Notifications/MessageParts/LinkedMailMessageLine.php b/app/Activity/Notifications/MessageParts/LinkedMailMessageLine.php
new file mode 100644 (file)
index 0000000..8f6a4e2
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace BookStack\Activity\Notifications\MessageParts;
+
+use Illuminate\Contracts\Support\Htmlable;
+
+/**
+ * A line of text with linked text included, intended for use
+ * in MailMessages. The line should have a ':link' placeholder for
+ * where the link should be inserted within the line.
+ */
+class LinkedMailMessageLine implements Htmlable
+{
+    public function __construct(
+        protected string $url,
+        protected string $line,
+        protected string $linkText,
+    ) {
+    }
+
+    public function toHtml(): string
+    {
+        $link = '<a href="' . e($this->url) . '">' . e($this->linkText) . '</a>';
+        return str_replace(':link', $link, e($this->line));
+    }
+}
diff --git a/app/Activity/Notifications/MessageParts/ListMessageLine.php b/app/Activity/Notifications/MessageParts/ListMessageLine.php
new file mode 100644 (file)
index 0000000..f808d25
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace BookStack\Activity\Notifications\MessageParts;
+
+use Illuminate\Contracts\Support\Htmlable;
+
+/**
+ * A bullet point list of content, where the keys of the given list array
+ * are bolded header elements, and the values follow.
+ */
+class ListMessageLine implements Htmlable
+{
+    public function __construct(
+        protected array $list
+    ) {
+    }
+
+    public function toHtml(): string
+    {
+        $list = [];
+        foreach ($this->list as $header => $content) {
+            $list[] = '<strong>' . e($header) . '</strong> ' . e($content);
+        }
+        return implode("<br>\n", $list);
+    }
+}
diff --git a/app/Activity/Notifications/Messages/BaseActivityNotification.php b/app/Activity/Notifications/Messages/BaseActivityNotification.php
new file mode 100644 (file)
index 0000000..eb6eb0c
--- /dev/null
@@ -0,0 +1,63 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\MessageParts\LinkedMailMessageLine;
+use BookStack\Users\Models\User;
+use Illuminate\Bus\Queueable;
+use Illuminate\Notifications\Messages\MailMessage;
+use Illuminate\Notifications\Notification;
+
+abstract class BaseActivityNotification extends Notification
+{
+    use Queueable;
+
+    public function __construct(
+        protected Loggable|string $detail,
+        protected User $user,
+    ) {
+    }
+
+    /**
+     * Get the notification's delivery channels.
+     *
+     * @param  mixed  $notifiable
+     * @return array
+     */
+    public function via($notifiable)
+    {
+        return ['mail'];
+    }
+
+    /**
+     * Get the mail representation of the notification.
+     */
+    abstract public function toMail(mixed $notifiable): MailMessage;
+
+    /**
+     * Get the array representation of the notification.
+     *
+     * @param  mixed  $notifiable
+     * @return array
+     */
+    public function toArray($notifiable)
+    {
+        return [
+            'activity_detail' => $this->detail,
+            'activity_creator' => $this->user,
+        ];
+    }
+
+    /**
+     * Build the common reason footer line used in mail messages.
+     */
+    protected function buildReasonFooterLine(): LinkedMailMessageLine
+    {
+        return new LinkedMailMessageLine(
+            url('/preferences/notifications'),
+            trans('notifications.footer_reason'),
+            trans('notifications.footer_reason_link'),
+        );
+    }
+}
diff --git a/app/Activity/Notifications/Messages/CommentCreationNotification.php b/app/Activity/Notifications/Messages/CommentCreationNotification.php
new file mode 100644 (file)
index 0000000..ce35872
--- /dev/null
@@ -0,0 +1,30 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Models\Comment;
+use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class CommentCreationNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Comment $comment */
+        $comment = $this->detail;
+        /** @var Page $page */
+        $page = $comment->entity;
+
+        return (new MailMessage())
+            ->subject(trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()]))
+            ->line(trans('notifications.new_comment_intro', ['appName' => setting('app-name')]))
+            ->line(new ListMessageLine([
+                trans('notifications.detail_page_name') => $page->name,
+                trans('notifications.detail_commenter') => $this->user->name,
+                trans('notifications.detail_comment') => strip_tags($comment->html),
+            ]))
+            ->action(trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))
+            ->line($this->buildReasonFooterLine());
+    }
+}
diff --git a/app/Activity/Notifications/Messages/PageCreationNotification.php b/app/Activity/Notifications/Messages/PageCreationNotification.php
new file mode 100644 (file)
index 0000000..068f95a
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class PageCreationNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Page $page */
+        $page = $this->detail;
+
+        return (new MailMessage())
+            ->subject(trans('notifications.new_page_subject', ['pageName' => $page->getShortName()]))
+            ->line(trans('notifications.new_page_intro', ['appName' => setting('app-name')]))
+            ->line(new ListMessageLine([
+                trans('notifications.detail_page_name') => $page->name,
+                trans('notifications.detail_created_by') => $this->user->name,
+            ]))
+            ->action(trans('notifications.action_view_page'), $page->getUrl())
+            ->line($this->buildReasonFooterLine());
+    }
+}
diff --git a/app/Activity/Notifications/Messages/PageUpdateNotification.php b/app/Activity/Notifications/Messages/PageUpdateNotification.php
new file mode 100644 (file)
index 0000000..c4a6de0
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace BookStack\Activity\Notifications\Messages;
+
+use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
+use BookStack\Entities\Models\Page;
+use Illuminate\Notifications\Messages\MailMessage;
+
+class PageUpdateNotification extends BaseActivityNotification
+{
+    public function toMail(mixed $notifiable): MailMessage
+    {
+        /** @var Page $page */
+        $page = $this->detail;
+
+        return (new MailMessage())
+            ->subject(trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()]))
+            ->line(trans('notifications.updated_page_intro', ['appName' => setting('app-name')]))
+            ->line(new ListMessageLine([
+                trans('notifications.detail_page_name') => $page->name,
+                trans('notifications.detail_updated_by') => $this->user->name,
+            ]))
+            ->line(trans('notifications.updated_page_debounce'))
+            ->action(trans('notifications.action_view_page'), $page->getUrl())
+            ->line($this->buildReasonFooterLine());
+    }
+}
diff --git a/app/Activity/Notifications/NotificationManager.php b/app/Activity/Notifications/NotificationManager.php
new file mode 100644 (file)
index 0000000..294f56e
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+namespace BookStack\Activity\Notifications;
+
+use BookStack\Activity\ActivityType;
+use BookStack\Activity\Models\Activity;
+use BookStack\Activity\Models\Loggable;
+use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
+use BookStack\Activity\Notifications\Handlers\NotificationHandler;
+use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
+use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
+use BookStack\Users\Models\User;
+
+class NotificationManager
+{
+    /**
+     * @var class-string<NotificationHandler>[]
+     */
+    protected array $handlers = [];
+
+    public function handle(Activity $activity, string|Loggable $detail, User $user): void
+    {
+        $activityType = $activity->type;
+        $handlersToRun = $this->handlers[$activityType] ?? [];
+        foreach ($handlersToRun as $handlerClass) {
+            /** @var NotificationHandler $handler */
+            $handler = new $handlerClass();
+            $handler->handle($activity, $detail, $user);
+        }
+    }
+
+    /**
+     * @param class-string<NotificationHandler> $handlerClass
+     */
+    public function registerHandler(string $activityType, string $handlerClass): void
+    {
+        if (!isset($this->handlers[$activityType])) {
+            $this->handlers[$activityType] = [];
+        }
+
+        if (!in_array($handlerClass, $this->handlers[$activityType])) {
+            $this->handlers[$activityType][] = $handlerClass;
+        }
+    }
+
+    public function loadDefaultHandlers(): void
+    {
+        $this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);
+        $this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);
+        $this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);
+    }
+}
index 105f9c4082de99cc0e284f369b496d160b4009a5..adda36c1b813a3f2728ae4c3eec64416fd1e8b64 100644 (file)
@@ -6,6 +6,7 @@ use BookStack\Activity\DispatchWebhookJob;
 use BookStack\Activity\Models\Activity;
 use BookStack\Activity\Models\Loggable;
 use BookStack\Activity\Models\Webhook;
+use BookStack\Activity\Notifications\NotificationManager;
 use BookStack\Entities\Models\Entity;
 use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
@@ -14,12 +15,16 @@ use Illuminate\Support\Facades\Log;
 
 class ActivityLogger
 {
+    public function __construct(
+        protected NotificationManager $notifications
+    ) {
+        $this->notifications->loadDefaultHandlers();
+    }
+
     /**
      * Add a generic activity event to the database.
-     *
-     * @param string|Loggable $detail
      */
-    public function add(string $type, $detail = '')
+    public function add(string $type, string|Loggable $detail = ''): void
     {
         $detailToStore = ($detail instanceof Loggable) ? $detail->logDescriptor() : $detail;
 
@@ -35,6 +40,7 @@ class ActivityLogger
 
         $this->setNotification($type);
         $this->dispatchWebhooks($type, $detail);
+        $this->notifications->handle($activity, $detail, user());
         Theme::dispatch(ThemeEvents::ACTIVITY_LOGGED, $type, $detail);
     }
 
@@ -55,7 +61,7 @@ class ActivityLogger
      * and instead uses the 'extra' field with the entities name.
      * Used when an entity is deleted.
      */
-    public function removeEntity(Entity $entity)
+    public function removeEntity(Entity $entity): void
     {
         $entity->activity()->update([
             'detail'       => $entity->name,
@@ -76,10 +82,7 @@ class ActivityLogger
         }
     }
 
-    /**
-     * @param string|Loggable $detail
-     */
-    protected function dispatchWebhooks(string $type, $detail): void
+    protected function dispatchWebhooks(string $type, string|Loggable $detail): void
     {
         $webhooks = Webhook::query()
             ->whereHas('trackedEvents', function (Builder $query) use ($type) {
@@ -98,7 +101,7 @@ class ActivityLogger
      * Log out a failed login attempt, Providing the given username
      * as part of the message if the '%u' string is used.
      */
-    public function logFailedLogin(string $username)
+    public function logFailedLogin(string $username): void
     {
         $message = config('logging.failed_login.message');
         if (!$message) {
diff --git a/app/Activity/Tools/EntityWatchers.php b/app/Activity/Tools/EntityWatchers.php
new file mode 100644 (file)
index 0000000..1ab53cb
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\Models\Watch;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use Illuminate\Database\Eloquent\Builder;
+
+class EntityWatchers
+{
+    /**
+     * @var int[]
+     */
+    protected array $watchers = [];
+
+    /**
+     * @var int[]
+     */
+    protected array $ignorers = [];
+
+    public function __construct(
+        protected Entity $entity,
+        protected int $watchLevel,
+    ) {
+        $this->build();
+    }
+
+    public function getWatcherUserIds(): array
+    {
+        return $this->watchers;
+    }
+
+    public function isUserIgnoring(int $userId): bool
+    {
+        return in_array($userId, $this->ignorers);
+    }
+
+    protected function build(): void
+    {
+        $watches = $this->getRelevantWatches();
+
+        // Sort before de-duping, so that the order looped below follows book -> chapter -> page ordering
+        usort($watches, function (Watch $watchA, Watch $watchB) {
+            $entityTypeDiff = $watchA->watchable_type <=> $watchB->watchable_type;
+            return $entityTypeDiff === 0 ? ($watchA->user_id <=> $watchB->user_id) : $entityTypeDiff;
+        });
+
+        // De-dupe by user id to get their most relevant level
+        $levelByUserId = [];
+        foreach ($watches as $watch) {
+            $levelByUserId[$watch->user_id] = $watch->level;
+        }
+
+        // Populate the class arrays
+        $this->watchers = array_keys(array_filter($levelByUserId, fn(int $level) => $level >= $this->watchLevel));
+        $this->ignorers = array_keys(array_filter($levelByUserId, fn(int $level) => $level === 0));
+    }
+
+    /**
+     * @return Watch[]
+     */
+    protected function getRelevantWatches(): array
+    {
+        /** @var Entity[] $entitiesInvolved */
+        $entitiesInvolved = array_filter([
+            $this->entity,
+            $this->entity instanceof BookChild ? $this->entity->book : null,
+            $this->entity instanceof Page ? $this->entity->chapter : null,
+        ]);
+
+        $query = Watch::query()->where(function (Builder $query) use ($entitiesInvolved) {
+            foreach ($entitiesInvolved as $entity) {
+                $query->orWhere(function (Builder $query) use ($entity) {
+                    $query->where('watchable_type', '=', $entity->getMorphClass())
+                        ->where('watchable_id', '=', $entity->id);
+                });
+            }
+        });
+
+        return $query->get([
+            'level', 'watchable_id', 'watchable_type', 'user_id'
+        ])->all();
+    }
+}
diff --git a/app/Activity/Tools/UserEntityWatchOptions.php b/app/Activity/Tools/UserEntityWatchOptions.php
new file mode 100644 (file)
index 0000000..231204f
--- /dev/null
@@ -0,0 +1,131 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\Models\Watch;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\BookChild;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+use BookStack\Users\Models\User;
+use Illuminate\Database\Eloquent\Builder;
+
+class UserEntityWatchOptions
+{
+    protected ?array $watchMap = null;
+
+    public function __construct(
+        protected User $user,
+        protected Entity $entity,
+    ) {
+    }
+
+    public function canWatch(): bool
+    {
+        return $this->user->can('receive-notifications') && !$this->user->isDefault();
+    }
+
+    public function getWatchLevel(): string
+    {
+        return WatchLevels::levelValueToName($this->getWatchLevelValue());
+    }
+
+    public function isWatching(): bool
+    {
+        return $this->getWatchLevelValue() !== WatchLevels::DEFAULT;
+    }
+
+    public function getWatchedParent(): ?WatchedParentDetails
+    {
+        $watchMap = $this->getWatchMap();
+        unset($watchMap[$this->entity->getMorphClass()]);
+
+        if (isset($watchMap['chapter'])) {
+            return new WatchedParentDetails('chapter', $watchMap['chapter']);
+        }
+
+        if (isset($watchMap['book'])) {
+            return new WatchedParentDetails('book', $watchMap['book']);
+        }
+
+        return null;
+    }
+
+    public function updateLevelByName(string $level): void
+    {
+        $levelValue = WatchLevels::levelNameToValue($level);
+        $this->updateLevelByValue($levelValue);
+    }
+
+    public function updateLevelByValue(int $level): void
+    {
+        if ($level < 0) {
+            $this->remove();
+            return;
+        }
+
+        $this->updateLevel($level);
+    }
+
+    public function getWatchMap(): array
+    {
+        if (!is_null($this->watchMap)) {
+            return $this->watchMap;
+        }
+
+        $entities = [$this->entity];
+        if ($this->entity instanceof BookChild) {
+            $entities[] = $this->entity->book;
+        }
+        if ($this->entity instanceof Page && $this->entity->chapter) {
+            $entities[] = $this->entity->chapter;
+        }
+
+        $query = Watch::query()
+            ->where('user_id', '=', $this->user->id)
+            ->where(function (Builder $subQuery) use ($entities) {
+                foreach ($entities as $entity) {
+                    $subQuery->orWhere(function (Builder $whereQuery) use ($entity) {
+                        $whereQuery->where('watchable_type', '=', $entity->getMorphClass())
+                        ->where('watchable_id', '=', $entity->id);
+                    });
+                }
+            });
+
+        $this->watchMap = $query->get(['watchable_type', 'level'])
+            ->pluck('level', 'watchable_type')
+            ->toArray();
+
+        return $this->watchMap;
+    }
+
+    protected function getWatchLevelValue()
+    {
+        return $this->getWatchMap()[$this->entity->getMorphClass()] ?? WatchLevels::DEFAULT;
+    }
+
+    protected function updateLevel(int $levelValue): void
+    {
+        Watch::query()->updateOrCreate([
+            'watchable_id' => $this->entity->id,
+            'watchable_type' => $this->entity->getMorphClass(),
+            'user_id' => $this->user->id,
+        ], [
+            'level' => $levelValue,
+        ]);
+        $this->watchMap = null;
+    }
+
+    protected function remove(): void
+    {
+        $this->entityQuery()->delete();
+        $this->watchMap = null;
+    }
+
+    protected function entityQuery(): Builder
+    {
+        return Watch::query()->where('watchable_id', '=', $this->entity->id)
+            ->where('watchable_type', '=', $this->entity->getMorphClass())
+            ->where('user_id', '=', $this->user->id);
+    }
+}
diff --git a/app/Activity/Tools/WatchedParentDetails.php b/app/Activity/Tools/WatchedParentDetails.php
new file mode 100644 (file)
index 0000000..5a881c0
--- /dev/null
@@ -0,0 +1,19 @@
+<?php
+
+namespace BookStack\Activity\Tools;
+
+use BookStack\Activity\WatchLevels;
+
+class WatchedParentDetails
+{
+    public function __construct(
+        public string $type,
+        public int $level,
+    ) {
+    }
+
+    public function ignoring(): bool
+    {
+        return $this->level === WatchLevels::IGNORE;
+    }
+}
index 4237139780a03f0912b9bb5aae3db56950134bc4..6ccb084b8de769b2856020b101beffb527199ef1 100644 (file)
@@ -17,18 +17,14 @@ class WebhookFormatter
     protected string $event;
     protected User $initiator;
     protected int $initiatedTime;
-
-    /**
-     * @var string|Loggable
-     */
-    protected $detail;
+    protected string|Loggable $detail;
 
     /**
      * @var array{condition: callable(string, Model):bool, format: callable(Model):void}[]
      */
     protected $modelFormatters = [];
 
-    public function __construct(string $event, Webhook $webhook, $detail, User $initiator, int $initiatedTime)
+    public function __construct(string $event, Webhook $webhook, string|Loggable $detail, User $initiator, int $initiatedTime)
     {
         $this->webhook = $webhook;
         $this->event = $event;
diff --git a/app/Activity/WatchLevels.php b/app/Activity/WatchLevels.php
new file mode 100644 (file)
index 0000000..de3c5e1
--- /dev/null
@@ -0,0 +1,91 @@
+<?php
+
+namespace BookStack\Activity;
+
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Entity;
+use BookStack\Entities\Models\Page;
+
+class WatchLevels
+{
+    /**
+     * Default level, No specific option set
+     * Typically not a stored status
+     */
+    const DEFAULT = -1;
+
+    /**
+     * Ignore all notifications.
+     */
+    const IGNORE = 0;
+
+    /**
+     * Watch for new content.
+     */
+    const NEW = 1;
+
+    /**
+     * Watch for updates and new content
+     */
+    const UPDATES = 2;
+
+    /**
+     * Watch for comments, updates and new content.
+     */
+    const COMMENTS = 3;
+
+    /**
+     * Get all the possible values as an option_name => value array.
+     * @returns array<string, int>
+     */
+    public static function all(): array
+    {
+        $options = [];
+        foreach ((new \ReflectionClass(static::class))->getConstants() as $name => $value) {
+            $options[strtolower($name)] = $value;
+        }
+
+        return $options;
+    }
+
+    /**
+     * Get the watch options suited for the given entity.
+     * @returns array<string, int>
+     */
+    public static function allSuitedFor(Entity $entity): array
+    {
+        $options = static::all();
+
+        if ($entity instanceof Page) {
+            unset($options['new']);
+        } elseif ($entity instanceof Bookshelf) {
+            return [];
+        }
+
+        return $options;
+    }
+
+    /**
+     * Convert the given name to a level value.
+     * Defaults to default value if the level does not exist.
+     */
+    public static function levelNameToValue(string $level): int
+    {
+        return static::all()[$level] ?? static::DEFAULT;
+    }
+
+    /**
+     * Convert the given int level value to a level name.
+     * Defaults to 'default' level name if not existing.
+     */
+    public static function levelValueToName(int $level): string
+    {
+        foreach (static::all() as $name => $value) {
+            if ($level === $value) {
+                return $name;
+            }
+        }
+
+        return 'default';
+    }
+}
index b462eaae956945c703644f7b49e7b30c8bb6e2e5..5c2d591e4083783d2ca6e75f86a0263ef38d9ec3 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Api;
 
 use BookStack\Activity\Models\Loggable;
 use BookStack\Users\Models\User;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Support\Carbon;
@@ -20,6 +21,8 @@ use Illuminate\Support\Carbon;
  */
 class ApiToken extends Model implements Loggable
 {
+    use HasFactory;
+
     protected $fillable = ['name', 'expires_at'];
     protected $casts = [
         'expires_at' => 'date:Y-m-d',
index 0c0895bf45309624c9aedd6d439be1f8eb860aa9..deb664ba697d2ff639b42b00c812f4503d1286dc 100644 (file)
@@ -9,6 +9,7 @@ use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Models\Chapter;
 use BookStack\Entities\Models\Page;
 use BookStack\Exceptions\BookStackExceptionHandlerPage;
+use BookStack\Permissions\PermissionApplicator;
 use BookStack\Settings\SettingService;
 use BookStack\Util\CspService;
 use GuzzleHttp\Client;
@@ -79,5 +80,9 @@ class AppServiceProvider extends ServiceProvider
                 'timeout' => 3,
             ]);
         });
+
+        $this->app->singleton(PermissionApplicator::class, function ($app) {
+            return new PermissionApplicator(null);
+        });
     }
 }
index dcd1af5a187910f3f2e78013cf5eb23d0a447acd..55d28c6847e32a1227cb4663902e3388fbb27524 100644 (file)
@@ -5,6 +5,7 @@ namespace BookStack\Entities\Controllers;
 use BookStack\Activity\ActivityQueries;
 use BookStack\Activity\ActivityType;
 use BookStack\Activity\Models\View;
+use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Bookshelf;
 use BookStack\Entities\Repos\BookRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -138,6 +139,7 @@ class BookController extends Controller
             'current'           => $book,
             'bookChildren'      => $bookChildren,
             'bookParentShelves' => $bookParentShelves,
+            'watchOptions'      => new UserEntityWatchOptions(user(), $book),
             'activity'          => $activities->entityActivity($book, 20, 1),
             'referenceCount'    => $this->referenceFetcher->getPageReferenceCountToEntity($book),
         ]);
index 7dcb669038bca6d770c59b1da206a4f3a74c8afe..ee1df05819aae47c6e3a991480f0f891e7f3e4e7 100644 (file)
@@ -3,6 +3,7 @@
 namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\Models\View;
+use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Repos\ChapterRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -81,6 +82,7 @@ class ChapterController extends Controller
             'chapter'        => $chapter,
             'current'        => $chapter,
             'sidebarTree'    => $sidebarTree,
+            'watchOptions'   => new UserEntityWatchOptions(user(), $chapter),
             'pages'          => $pages,
             'next'           => $nextPreviousLocator->getNext(),
             'previous'       => $nextPreviousLocator->getPrevious(),
index e96d41bb1b445bd290c0f13b04f6b09ccd638b95..6249310650494d19c157bd6b56616d017fb12df1 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Entities\Controllers;
 
 use BookStack\Activity\Models\View;
 use BookStack\Activity\Tools\CommentTree;
+use BookStack\Activity\Tools\UserEntityWatchOptions;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Repos\PageRepo;
 use BookStack\Entities\Tools\BookContents;
@@ -151,6 +152,7 @@ class PageController extends Controller
             'sidebarTree'     => $sidebarTree,
             'commentTree'     => $commentTree,
             'pageNav'         => $pageNav,
+            'watchOptions'    => new UserEntityWatchOptions(user(), $page),
             'next'            => $nextPreviousLocator->getNext(),
             'previous'        => $nextPreviousLocator->getPrevious(),
             'referenceCount'  => $this->referenceFetcher->getPageReferenceCountToEntity($page),
index 3f8d2e62e134f2da936c6054cf5f314d79522a3b..cbccf35b054eceedd863e3edde10647b7462e650 100644 (file)
@@ -10,7 +10,7 @@ class TopFavourites extends EntityQuery
     public function run(int $count, int $skip = 0)
     {
         $user = user();
-        if (is_null($user) || $user->isDefault()) {
+        if ($user->isDefault()) {
             return collect();
         }
 
index 78b899d25559fd63370fc62342b54e95d9ab6330..584cea3aa74cd8ccffa7d5e366376b19ff8718dd 100644 (file)
@@ -66,6 +66,16 @@ abstract class Controller extends BaseController
         }
     }
 
+    /**
+     * Prevent access for guest users beyond this point.
+     */
+    protected function preventGuestAccess(): void
+    {
+        if (!signedInUser()) {
+            $this->showPermissionError();
+        }
+    }
+
     /**
      * Check the current user's permissions against an ownable item otherwise throw an exception.
      */
index b4fafaa9ee1756f6604d5cc20f46e2091ed5f787..a796bdaeee4e70e42776bcada7e726d8344b405e 100644 (file)
@@ -8,7 +8,6 @@ use BookStack\Entities\Models\Page;
 use BookStack\Permissions\Models\EntityPermission;
 use BookStack\Users\Models\HasCreatorAndUpdater;
 use BookStack\Users\Models\HasOwner;
-use BookStack\Users\Models\Role;
 use BookStack\Users\Models\User;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Query\Builder as QueryBuilder;
@@ -16,6 +15,11 @@ use InvalidArgumentException;
 
 class PermissionApplicator
 {
+    public function __construct(
+        protected ?User $user = null
+    ) {
+    }
+
     /**
      * Checks if an entity has a restriction set upon it.
      *
@@ -173,7 +177,7 @@ class PermissionApplicator
      */
     protected function currentUser(): User
     {
-        return user();
+        return $this->user ?? user();
     }
 
     /**
index 889a6ea0891133e932c3e8dba98cbc92fef2d2ba..b41612968b4692629a9a7392f563416c59b741c3 100644 (file)
@@ -12,12 +12,11 @@ use Illuminate\Database\Eloquent\Collection;
 
 class PermissionsRepo
 {
-    protected JointPermissionBuilder $permissionBuilder;
     protected array $systemRoles = ['admin', 'public'];
 
-    public function __construct(JointPermissionBuilder $permissionBuilder)
-    {
-        $this->permissionBuilder = $permissionBuilder;
+    public function __construct(
+        protected JointPermissionBuilder $permissionBuilder
+    ) {
     }
 
     /**
index 5193bc50d027f1d2ddb68f91de0e89fe53a4b07e..e4321bf30dba59e2ada9a2973f5a99d0e86aae0e 100644 (file)
@@ -20,10 +20,11 @@ class StatusController extends Controller
                 return DB::table('migrations')->count() > 0;
             }),
             'cache' => $this->trueWithoutError(function () {
-                $rand = Str::random();
-                Cache::add('status_test', $rand);
+                $rand = Str::random(12);
+                $key = "status_test_{$rand}";
+                Cache::add($key, $rand);
 
-                return Cache::pull('status_test') === $rand;
+                return Cache::pull($key) === $rand;
             }),
             'session' => $this->trueWithoutError(function () {
                 $rand = Str::random();
diff --git a/app/Settings/UserNotificationPreferences.php b/app/Settings/UserNotificationPreferences.php
new file mode 100644 (file)
index 0000000..5b267b5
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+
+namespace BookStack\Settings;
+
+use BookStack\Users\Models\User;
+
+class UserNotificationPreferences
+{
+    public function __construct(
+        protected User $user
+    ) {
+    }
+
+    public function notifyOnOwnPageChanges(): bool
+    {
+        return $this->getNotificationSetting('own-page-changes');
+    }
+
+    public function notifyOnOwnPageComments(): bool
+    {
+        return $this->getNotificationSetting('own-page-comments');
+    }
+
+    public function notifyOnCommentReplies(): bool
+    {
+        return $this->getNotificationSetting('comment-replies');
+    }
+
+    public function updateFromSettingsArray(array $settings)
+    {
+        $allowList = ['own-page-changes', 'own-page-comments', 'comment-replies'];
+        foreach ($settings as $setting => $status) {
+            if (!in_array($setting, $allowList)) {
+                continue;
+            }
+
+            $value = $status === 'true' ? 'true' : 'false';
+            setting()->putUser($this->user, 'notifications#' . $setting, $value);
+        }
+    }
+
+    protected function getNotificationSetting(string $key): bool
+    {
+        return setting()->getUser($this->user, 'notifications#' . $key);
+    }
+}
index 994c3ec0dbaacfb97524aca906b890e503cc1f70..4b56b2f56c119c596e2d8684a1e2569d65428bb7 100644 (file)
@@ -132,11 +132,12 @@ class ThemeEvents
      * If the listener returns a non-null value, that will be used as the POST data instead
      * of the system default.
      *
-     * @param string                                $event
-     * @param \BookStack\Activity\Models\Webhook            $webhook
+     * @param string                                     $event
+     * @param \BookStack\Activity\Models\Webhook         $webhook
      * @param string|\BookStack\Activity\Models\Loggable $detail
-     * @param \BookStack\Users\Models\User                  $initiator
-     * @param int                                   $initiatedTime
+     * @param \BookStack\Users\Models\User               $initiator
+     * @param int                                        $initiatedTime
+     * @returns array|null
      */
     const WEBHOOK_CALL_BEFORE = 'webhook_call_before';
 }
index f6472e4de51d53b0c234568989ac68022406912c..0052d829dd905c0006d5615c51fa64e3f60ea704 100644 (file)
@@ -13,11 +13,9 @@ use Illuminate\Http\Request;
 
 class RoleController extends Controller
 {
-    protected PermissionsRepo $permissionsRepo;
-
-    public function __construct(PermissionsRepo $permissionsRepo)
-    {
-        $this->permissionsRepo = $permissionsRepo;
+    public function __construct(
+        protected PermissionsRepo $permissionsRepo
+    ) {
     }
 
     /**
index b20a8aa37220678e496fa6855a216c076a2ccab7..503aeaeb0c9d23032746cf72fd67634678f2aa43 100644 (file)
@@ -2,18 +2,27 @@
 
 namespace BookStack\Users\Controllers;
 
+use BookStack\Activity\Models\Watch;
 use BookStack\Http\Controller;
+use BookStack\Permissions\PermissionApplicator;
+use BookStack\Settings\UserNotificationPreferences;
 use BookStack\Settings\UserShortcutMap;
 use BookStack\Users\UserRepo;
 use Illuminate\Http\Request;
 
 class UserPreferencesController extends Controller
 {
-    protected UserRepo $userRepo;
+    public function __construct(
+        protected UserRepo $userRepo
+    ) {
+    }
 
-    public function __construct(UserRepo $userRepo)
+    /**
+     * Show the overview for user preferences.
+     */
+    public function index()
     {
-        $this->userRepo = $userRepo;
+        return view('users.preferences.index');
     }
 
     /**
@@ -24,6 +33,8 @@ class UserPreferencesController extends Controller
         $shortcuts = UserShortcutMap::fromUserPreferences();
         $enabled = setting()->getForCurrentUser('ui-shortcuts-enabled', false);
 
+        $this->setPageTitle(trans('preferences.shortcuts_interface'));
+
         return view('users.preferences.shortcuts', [
             'shortcuts' => $shortcuts,
             'enabled' => $enabled,
@@ -47,6 +58,46 @@ class UserPreferencesController extends Controller
         return redirect('/preferences/shortcuts');
     }
 
+    /**
+     * Show the notification preferences for the current user.
+     */
+    public function showNotifications(PermissionApplicator $permissions)
+    {
+        $this->checkPermission('receive-notifications');
+        $this->preventGuestAccess();
+
+        $preferences = (new UserNotificationPreferences(user()));
+
+        $query = Watch::query()->where('user_id', '=', user()->id);
+        $query = $permissions->restrictEntityRelationQuery($query, 'watches', 'watchable_id', 'watchable_type');
+        $watches = $query->with('watchable')->paginate(20);
+
+        $this->setPageTitle(trans('preferences.notifications'));
+        return view('users.preferences.notifications', [
+            'preferences' => $preferences,
+            'watches' => $watches,
+        ]);
+    }
+
+    /**
+     * Update the notification preferences for the current user.
+     */
+    public function updateNotifications(Request $request)
+    {
+        $this->checkPermission('receive-notifications');
+        $this->preventGuestAccess();
+        $data = $this->validate($request, [
+           'preferences' => ['required', 'array'],
+           'preferences.*' => ['required', 'string'],
+        ]);
+
+        $preferences = (new UserNotificationPreferences(user()));
+        $preferences->updateFromSettingsArray($data['preferences']);
+        $this->showSuccessNotification(trans('preferences.notifications_update_success'));
+
+        return redirect('/preferences/notifications');
+    }
+
     /**
      * Update the preferred view format for a list view of the given type.
      */
@@ -123,7 +174,7 @@ class UserPreferencesController extends Controller
     {
         $validated = $this->validate($request, [
             'language' => ['required', 'string', 'max:20'],
-            'active'   => ['required', 'bool'],
+            'active' => ['required', 'bool'],
         ]);
 
         $currentFavoritesStr = setting()->getForCurrentUser('code-language-favourites', '');
index 08cab69fb9206c793cde08cf08518f345188e5a0..be3e9b9b38e468ea0003de1cba22ebdd3976c2aa 100644 (file)
@@ -88,8 +88,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
 
     /**
      * This holds the default user when loaded.
-     *
-     * @var null|User
      */
     protected static ?User $defaultUser = null;
 
@@ -107,6 +105,11 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
         return static::$defaultUser;
     }
 
+    public static function clearDefault(): void
+    {
+        static::$defaultUser = null;
+    }
+
     /**
      * Check if the user is the default public user.
      */
index 3f7567a5760b9fd79aeccb9661c91213f1ef9954..04232b1a686afbd551bcdaf77db653985ab34c79 100644 (file)
@@ -8,16 +8,16 @@
     "packages": [
         {
             "name": "aws/aws-crt-php",
-            "version": "v1.2.1",
+            "version": "v1.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/awslabs/aws-crt-php.git",
-                "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5"
+                "reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/1926277fc71d253dfa820271ac5987bdb193ccf5",
-                "reference": "1926277fc71d253dfa820271ac5987bdb193ccf5",
+                "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/2f1dc7b7eda080498be96a4a6d683a41583030e9",
+                "reference": "2f1dc7b7eda080498be96a4a6d683a41583030e9",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/awslabs/aws-crt-php/issues",
-                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.1"
+                "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.2"
             },
-            "time": "2023-03-24T20:22:19+00:00"
+            "time": "2023-07-20T16:49:55+00:00"
         },
         {
             "name": "aws/aws-sdk-php",
-            "version": "3.269.0",
+            "version": "3.279.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/aws/aws-sdk-php.git",
-                "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78"
+                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
-                "reference": "6d759ef9f24f0c7f271baf8014f41fc0cfdfbf78",
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/ebd5e47c5be0425bb5cf4f80737850ed74767107",
+                "reference": "ebd5e47c5be0425bb5cf4f80737850ed74767107",
                 "shasum": ""
             },
             "require": {
                 "ext-pcre": "*",
                 "ext-simplexml": "*",
                 "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
-                "guzzlehttp/promises": "^1.4.0",
+                "guzzlehttp/promises": "^1.4.0 || ^2.0",
                 "guzzlehttp/psr7": "^1.9.1 || ^2.4.5",
                 "mtdowling/jmespath.php": "^2.6",
-                "php": ">=5.5"
+                "php": ">=7.2.5",
+                "psr/http-message": "^1.0 || ^2.0"
             },
             "require-dev": {
                 "andrewsville/php-token-reflection": "^1.4",
@@ -98,9 +99,8 @@
                 "ext-sockets": "*",
                 "nette/neon": "^2.3",
                 "paragonie/random_compat": ">= 2",
-                "phpunit/phpunit": "^4.8.35 || ^5.6.3 || ^9.5",
+                "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5",
                 "psr/cache": "^1.0",
-                "psr/http-message": "^1.0",
                 "psr/simple-cache": "^1.0",
                 "sebastian/comparator": "^1.2.3 || ^4.0",
                 "yoast/phpunit-polyfills": "^1.0"
             "support": {
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.269.0"
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.279.2"
             },
-            "time": "2023-04-26T18:21:04+00:00"
+            "time": "2023-08-18T18:13:09+00:00"
         },
         {
             "name": "bacon/bacon-qr-code",
         },
         {
             "name": "doctrine/dbal",
-            "version": "3.6.4",
+            "version": "3.6.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/doctrine/dbal.git",
-                "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f"
+                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/doctrine/dbal/zipball/19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
-                "reference": "19f0dec95edd6a3c3c5ff1d188ea94c6b7fc903f",
+                "url": "https://api.github.com/repos/doctrine/dbal/zipball/63646ffd71d1676d2f747f871be31b7e921c7864",
+                "reference": "63646ffd71d1676d2f747f871be31b7e921c7864",
                 "shasum": ""
             },
             "require": {
             "require-dev": {
                 "doctrine/coding-standard": "12.0.0",
                 "fig/log-test": "^1",
-                "jetbrains/phpstorm-stubs": "2022.3",
-                "phpstan/phpstan": "1.10.14",
+                "jetbrains/phpstorm-stubs": "2023.1",
+                "phpstan/phpstan": "1.10.29",
                 "phpstan/phpstan-strict-rules": "^1.5",
-                "phpunit/phpunit": "9.6.7",
+                "phpunit/phpunit": "9.6.9",
                 "psalm/plugin-phpunit": "0.18.4",
+                "slevomat/coding-standard": "8.13.1",
                 "squizlabs/php_codesniffer": "3.7.2",
                 "symfony/cache": "^5.4|^6.0",
                 "symfony/console": "^4.4|^5.4|^6.0",
             ],
             "support": {
                 "issues": "https://github.com/doctrine/dbal/issues",
-                "source": "https://github.com/doctrine/dbal/tree/3.6.4"
+                "source": "https://github.com/doctrine/dbal/tree/3.6.6"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-06-15T07:40:12+00:00"
+            "time": "2023-08-17T05:38:17+00:00"
         },
         {
             "name": "doctrine/deprecations",
         },
         {
             "name": "dragonmantank/cron-expression",
-            "version": "v3.3.2",
+            "version": "v3.3.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/dragonmantank/cron-expression.git",
-                "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8"
+                "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8",
-                "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8",
+                "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
+                "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/dragonmantank/cron-expression/issues",
-                "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2"
+                "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-09-10T18:51:20+00:00"
+            "time": "2023-08-10T19:36:49+00:00"
         },
         {
             "name": "egulias/email-validator",
         },
         {
             "name": "guzzlehttp/promises",
-            "version": "1.5.3",
+            "version": "2.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/promises.git",
-                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e"
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/promises/zipball/67ab6e18aaa14d753cc148911d273f6e6cb6721e",
-                "reference": "67ab6e18aaa14d753cc148911d273f6e6cb6721e",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
+                "reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
                 "shasum": ""
             },
             "require": {
-                "php": ">=5.5"
+                "php": "^7.2.5 || ^8.0"
             },
             "require-dev": {
-                "symfony/phpunit-bridge": "^4.4 || ^5.1"
+                "bamarni/composer-bin-plugin": "^1.8.1",
+                "phpunit/phpunit": "^8.5.29 || ^9.5.23"
             },
             "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "bin-links": true,
+                    "forward-command": false
+                }
+            },
             "autoload": {
-                "files": [
-                    "src/functions_include.php"
-                ],
                 "psr-4": {
                     "GuzzleHttp\\Promise\\": "src/"
                 }
             ],
             "support": {
                 "issues": "https://github.com/guzzle/promises/issues",
-                "source": "https://github.com/guzzle/promises/tree/1.5.3"
+                "source": "https://github.com/guzzle/promises/tree/2.0.1"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-05-21T12:31:43+00:00"
+            "time": "2023-08-03T15:11:55+00:00"
         },
         {
             "name": "guzzlehttp/psr7",
-            "version": "2.5.0",
+            "version": "2.6.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/psr7.git",
-                "reference": "b635f279edd83fc275f822a1188157ffea568ff6"
+                "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/guzzle/psr7/zipball/b635f279edd83fc275f822a1188157ffea568ff6",
-                "reference": "b635f279edd83fc275f822a1188157ffea568ff6",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/8bd7c33a0734ae1c5d074360512beb716bef3f77",
+                "reference": "8bd7c33a0734ae1c5d074360512beb716bef3f77",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/guzzle/psr7/issues",
-                "source": "https://github.com/guzzle/psr7/tree/2.5.0"
+                "source": "https://github.com/guzzle/psr7/tree/2.6.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-04-17T16:11:26+00:00"
+            "time": "2023-08-03T15:06:02+00:00"
         },
         {
             "name": "guzzlehttp/uri-template",
         },
         {
             "name": "laravel/framework",
-            "version": "v9.52.10",
+            "version": "v9.52.15",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-                "reference": "858add225ce88a76c43aec0e7866288321ee0ee9"
+                "reference": "e3350e87a52346af9cc655a3012d2175d2d05ad7"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/framework/zipball/858add225ce88a76c43aec0e7866288321ee0ee9",
-                "reference": "858add225ce88a76c43aec0e7866288321ee0ee9",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/e3350e87a52346af9cc655a3012d2175d2d05ad7",
+                "reference": "e3350e87a52346af9cc655a3012d2175d2d05ad7",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-            "time": "2023-06-27T13:25:54+00:00"
+            "time": "2023-08-08T14:28:40+00:00"
         },
         {
             "name": "laravel/serializable-closure",
-            "version": "v1.3.0",
+            "version": "v1.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/serializable-closure.git",
-                "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37"
+                "reference": "e5a3057a5591e1cfe8183034b0203921abe2c902"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f23fe9d4e95255dacee1bf3525e0810d1a1b0f37",
-                "reference": "f23fe9d4e95255dacee1bf3525e0810d1a1b0f37",
+                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/e5a3057a5591e1cfe8183034b0203921abe2c902",
+                "reference": "e5a3057a5591e1cfe8183034b0203921abe2c902",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/serializable-closure/issues",
                 "source": "https://github.com/laravel/serializable-closure"
             },
-            "time": "2023-01-30T18:31:20+00:00"
+            "time": "2023-07-14T13:56:28+00:00"
         },
         {
             "name": "laravel/socialite",
-            "version": "v5.6.3",
+            "version": "v5.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/socialite.git",
-                "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e"
+                "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/00ea7f8630673ea49304fc8a9fca5a64eb838c7e",
-                "reference": "00ea7f8630673ea49304fc8a9fca5a64eb838c7e",
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/50148edf24b6cd3e428aa9bc06a5d915b24376bb",
+                "reference": "50148edf24b6cd3e428aa9bc06a5d915b24376bb",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/laravel/socialite/issues",
                 "source": "https://github.com/laravel/socialite"
             },
-            "time": "2023-06-06T13:42:43+00:00"
+            "time": "2023-07-14T14:22:58+00:00"
         },
         {
             "name": "laravel/tinker",
         },
         {
             "name": "league/html-to-markdown",
-            "version": "5.1.0",
+            "version": "5.1.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/html-to-markdown.git",
-                "reference": "e0fc8cf07bdabbcd3765341ecb50c34c271d64e1"
+                "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/e0fc8cf07bdabbcd3765341ecb50c34c271d64e1",
-                "reference": "e0fc8cf07bdabbcd3765341ecb50c34c271d64e1",
+                "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd",
+                "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd",
                 "shasum": ""
             },
             "require": {
             },
             "require-dev": {
                 "mikehaertl/php-shellcommand": "^1.1.0",
-                "phpstan/phpstan": "^0.12.99",
+                "phpstan/phpstan": "^1.8.8",
                 "phpunit/phpunit": "^8.5 || ^9.2",
                 "scrutinizer/ocular": "^1.6",
-                "unleashedtech/php-coding-standard": "^2.7",
-                "vimeo/psalm": "^4.22"
+                "unleashedtech/php-coding-standard": "^2.7 || ^3.0",
+                "vimeo/psalm": "^4.22 || ^5.0"
             },
             "bin": [
                 "bin/html-to-markdown"
             ],
             "support": {
                 "issues": "https://github.com/thephpleague/html-to-markdown/issues",
-                "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.0"
+                "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-03-02T17:24:08+00:00"
+            "time": "2023-07-12T21:21:09+00:00"
         },
         {
             "name": "league/mime-type-detection",
-            "version": "1.11.0",
+            "version": "1.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/thephpleague/mime-type-detection.git",
-                "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd"
+                "reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ff6248ea87a9f116e78edd6002e39e5128a0d4dd",
-                "reference": "ff6248ea87a9f116e78edd6002e39e5128a0d4dd",
+                "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/a6dfb1194a2946fcdc1f38219445234f65b35c96",
+                "reference": "a6dfb1194a2946fcdc1f38219445234f65b35c96",
                 "shasum": ""
             },
             "require": {
                 "ext-fileinfo": "*",
-                "php": "^7.2 || ^8.0"
+                "php": "^7.4 || ^8.0"
             },
             "require-dev": {
                 "friendsofphp/php-cs-fixer": "^3.2",
                 "phpstan/phpstan": "^0.12.68",
-                "phpunit/phpunit": "^8.5.8 || ^9.3"
+                "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
             },
             "type": "library",
             "autoload": {
             "description": "Mime-type detection for Flysystem",
             "support": {
                 "issues": "https://github.com/thephpleague/mime-type-detection/issues",
-                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.11.0"
+                "source": "https://github.com/thephpleague/mime-type-detection/tree/1.13.0"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2022-04-17T13:12:02+00:00"
+            "time": "2023-08-05T12:09:49+00:00"
         },
         {
             "name": "league/oauth1-client",
         },
         {
             "name": "masterminds/html5",
-            "version": "2.8.0",
+            "version": "2.8.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Masterminds/html5-php.git",
-                "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3"
+                "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3",
-                "reference": "3c5d5a56d56f48a1ca08a0670f0f80c1dad368f3",
+                "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f47dcf3c70c584de14f21143c55d9939631bc6cf",
+                "reference": "f47dcf3c70c584de14f21143c55d9939631bc6cf",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/Masterminds/html5-php/issues",
-                "source": "https://github.com/Masterminds/html5-php/tree/2.8.0"
+                "source": "https://github.com/Masterminds/html5-php/tree/2.8.1"
             },
-            "time": "2023-04-26T07:27:39+00:00"
+            "time": "2023-05-10T11:58:31+00:00"
         },
         {
             "name": "monolog/monolog",
         },
         {
             "name": "nesbot/carbon",
-            "version": "2.68.1",
+            "version": "2.69.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/briannesbitt/Carbon.git",
-                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da"
+                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4f991ed2a403c85efbc4f23eb4030063fdbe01da",
-                "reference": "4f991ed2a403c85efbc4f23eb4030063fdbe01da",
+                "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4308217830e4ca445583a37d1bf4aff4153fa81c",
+                "reference": "4308217830e4ca445583a37d1bf4aff4153fa81c",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
                 "php": "^7.1.8 || ^8.0",
+                "psr/clock": "^1.0",
                 "symfony/polyfill-mbstring": "^1.0",
                 "symfony/polyfill-php80": "^1.16",
                 "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0"
             },
+            "provide": {
+                "psr/clock-implementation": "1.0"
+            },
             "require-dev": {
                 "doctrine/dbal": "^2.0 || ^3.1.4",
                 "doctrine/orm": "^2.7",
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-06-20T18:29:04+00:00"
+            "time": "2023-08-03T09:00:52+00:00"
         },
         {
             "name": "nette/schema",
-            "version": "v1.2.3",
+            "version": "v1.2.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nette/schema.git",
-                "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f"
+                "reference": "c9ff517a53903b3d4e29ec547fb20feecb05b8ab"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nette/schema/zipball/abbdbb70e0245d5f3bf77874cea1dfb0c930d06f",
-                "reference": "abbdbb70e0245d5f3bf77874cea1dfb0c930d06f",
+                "url": "https://api.github.com/repos/nette/schema/zipball/c9ff517a53903b3d4e29ec547fb20feecb05b8ab",
+                "reference": "c9ff517a53903b3d4e29ec547fb20feecb05b8ab",
                 "shasum": ""
             },
             "require": {
                 "nette/utils": "^2.5.7 || ^3.1.5 ||  ^4.0",
-                "php": ">=7.1 <8.3"
+                "php": "7.1 - 8.3"
             },
             "require-dev": {
                 "nette/tester": "^2.3 || ^2.4",
             ],
             "support": {
                 "issues": "https://github.com/nette/schema/issues",
-                "source": "https://github.com/nette/schema/tree/v1.2.3"
+                "source": "https://github.com/nette/schema/tree/v1.2.4"
             },
-            "time": "2022-10-13T01:24:26+00:00"
+            "time": "2023-08-05T18:56:25+00:00"
         },
         {
             "name": "nette/utils",
-            "version": "v4.0.0",
+            "version": "v4.0.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nette/utils.git",
-                "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e"
+                "reference": "9124157137da01b1f5a5a22d6486cb975f26db7e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nette/utils/zipball/cacdbf5a91a657ede665c541eda28941d4b09c1e",
-                "reference": "cacdbf5a91a657ede665c541eda28941d4b09c1e",
+                "url": "https://api.github.com/repos/nette/utils/zipball/9124157137da01b1f5a5a22d6486cb975f26db7e",
+                "reference": "9124157137da01b1f5a5a22d6486cb975f26db7e",
                 "shasum": ""
             },
             "require": {
-                "php": ">=8.0 <8.3"
+                "php": ">=8.0 <8.4"
             },
             "conflict": {
                 "nette/finder": "<3",
             },
             "require-dev": {
                 "jetbrains/phpstorm-attributes": "dev-master",
-                "nette/tester": "^2.4",
+                "nette/tester": "^2.5",
                 "phpstan/phpstan": "^1.0",
                 "tracy/tracy": "^2.9"
             },
             ],
             "support": {
                 "issues": "https://github.com/nette/utils/issues",
-                "source": "https://github.com/nette/utils/tree/v4.0.0"
+                "source": "https://github.com/nette/utils/tree/v4.0.1"
             },
-            "time": "2023-02-02T10:41:53+00:00"
+            "time": "2023-07-30T15:42:21+00:00"
         },
         {
             "name": "nikic/php-parser",
-            "version": "v4.16.0",
+            "version": "v4.17.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nikic/PHP-Parser.git",
-                "reference": "19526a33fb561ef417e822e85f08a00db4059c17"
+                "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
-                "reference": "19526a33fb561ef417e822e85f08a00db4059c17",
+                "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
+                "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/nikic/PHP-Parser/issues",
-                "source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
+                "source": "https://github.com/nikic/PHP-Parser/tree/v4.17.1"
             },
-            "time": "2023-06-25T14:52:30+00:00"
+            "time": "2023-08-13T19:53:39+00:00"
         },
         {
             "name": "nunomaduro/termwind",
         },
         {
             "name": "phpseclib/phpseclib",
-            "version": "3.0.20",
+            "version": "3.0.21",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpseclib/phpseclib.git",
-                "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67"
+                "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67",
-                "reference": "543a1da81111a0bfd6ae7bbc2865c5e89ed3fc67",
+                "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
+                "reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/phpseclib/phpseclib/issues",
-                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.20"
+                "source": "https://github.com/phpseclib/phpseclib/tree/3.0.21"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-06-13T06:30:34+00:00"
+            "time": "2023-07-09T15:24:48+00:00"
         },
         {
             "name": "pragmarx/google2fa",
         },
         {
             "name": "predis/predis",
-            "version": "v2.2.0",
+            "version": "v2.2.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/predis/predis.git",
-                "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d"
+                "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/predis/predis/zipball/33b70b971a32b0d28b4f748b0547593dce316e0d",
-                "reference": "33b70b971a32b0d28b4f748b0547593dce316e0d",
+                "url": "https://api.github.com/repos/predis/predis/zipball/5f2b410a74afaff296a87a494e4c5488cf9fab57",
+                "reference": "5f2b410a74afaff296a87a494e4c5488cf9fab57",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/predis/predis/issues",
-                "source": "https://github.com/predis/predis/tree/v2.2.0"
+                "source": "https://github.com/predis/predis/tree/v2.2.1"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-06-14T10:37:31+00:00"
+            "time": "2023-08-15T23:01:46+00:00"
         },
         {
             "name": "psr/cache",
             },
             "time": "2021-02-03T23:26:27+00:00"
         },
+        {
+            "name": "psr/clock",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/clock.git",
+                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+                "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Clock\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for reading the clock.",
+            "homepage": "https://github.com/php-fig/clock",
+            "keywords": [
+                "clock",
+                "now",
+                "psr",
+                "psr-20",
+                "time"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/clock/issues",
+                "source": "https://github.com/php-fig/clock/tree/1.0.0"
+            },
+            "time": "2022-11-25T14:36:26+00:00"
+        },
         {
             "name": "psr/container",
             "version": "2.0.2",
         },
         {
             "name": "psy/psysh",
-            "version": "v0.11.18",
+            "version": "v0.11.20",
             "source": {
                 "type": "git",
                 "url": "https://github.com/bobthecow/psysh.git",
-                "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec"
+                "reference": "0fa27040553d1d280a67a4393194df5228afea5b"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
-                "reference": "4f00ee9e236fa6a48f4560d1300b9c961a70a7ec",
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/0fa27040553d1d280a67a4393194df5228afea5b",
+                "reference": "0fa27040553d1d280a67a4393194df5228afea5b",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/bobthecow/psysh/issues",
-                "source": "https://github.com/bobthecow/psysh/tree/v0.11.18"
+                "source": "https://github.com/bobthecow/psysh/tree/v0.11.20"
             },
-            "time": "2023-05-23T02:31:11+00:00"
+            "time": "2023-07-31T14:32:22+00:00"
         },
         {
             "name": "ralouphie/getallheaders",
         },
         {
             "name": "socialiteproviders/discord",
-            "version": "4.1.2",
+            "version": "4.2.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Discord.git",
-                "reference": "11f6a8ded5b1948723886f2e5413b91139fcce6b"
+                "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/11f6a8ded5b1948723886f2e5413b91139fcce6b",
-                "reference": "11f6a8ded5b1948723886f2e5413b91139fcce6b",
+                "url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/c71c379acfdca5ba4aa65a3db5ae5222852a919c",
+                "reference": "c71c379acfdca5ba4aa65a3db5ae5222852a919c",
                 "shasum": ""
             },
             "require": {
                 "issues": "https://github.com/socialiteproviders/providers/issues",
                 "source": "https://github.com/socialiteproviders/providers"
             },
-            "time": "2023-02-01T08:54:49+00:00"
+            "time": "2023-07-24T23:28:47+00:00"
         },
         {
             "name": "socialiteproviders/gitlab",
         },
         {
             "name": "socialiteproviders/okta",
-            "version": "4.2.1",
+            "version": "4.3.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SocialiteProviders/Okta.git",
-                "reference": "7c0b7522423943131f680e74123b71ccd3989541"
+                "reference": "e5fb62035bfa0ccdbc8facf4cf205428fc502edb"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/7c0b7522423943131f680e74123b71ccd3989541",
-                "reference": "7c0b7522423943131f680e74123b71ccd3989541",
+                "url": "https://api.github.com/repos/SocialiteProviders/Okta/zipball/e5fb62035bfa0ccdbc8facf4cf205428fc502edb",
+                "reference": "e5fb62035bfa0ccdbc8facf4cf205428fc502edb",
                 "shasum": ""
             },
             "require": {
                 "ext-json": "*",
-                "php": "^7.2 || ^8.0",
+                "php": "^7.4 || ^8.0",
                 "socialiteproviders/manager": "~4.0"
             },
             "type": "library",
                 "issues": "https://github.com/socialiteproviders/providers/issues",
                 "source": "https://github.com/socialiteproviders/providers"
             },
-            "time": "2022-03-14T23:25:14+00:00"
+            "time": "2022-09-06T03:39:26+00:00"
         },
         {
             "name": "socialiteproviders/slack",
             "support": {
                 "source": "https://github.com/SocialiteProviders/Slack/tree/4.1.1"
             },
+            "abandoned": "laravel/socialite",
             "time": "2021-03-26T04:10:10+00:00"
         },
         {
         },
         {
             "name": "filp/whoops",
-            "version": "2.15.2",
+            "version": "2.15.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/filp/whoops.git",
-                "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73"
+                "reference": "c83e88a30524f9360b11f585f71e6b17313b7187"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/filp/whoops/zipball/aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
-                "reference": "aac9304c5ed61bf7b1b7a6064bf9806ab842ce73",
+                "url": "https://api.github.com/repos/filp/whoops/zipball/c83e88a30524f9360b11f585f71e6b17313b7187",
+                "reference": "c83e88a30524f9360b11f585f71e6b17313b7187",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/filp/whoops/issues",
-                "source": "https://github.com/filp/whoops/tree/2.15.2"
+                "source": "https://github.com/filp/whoops/tree/2.15.3"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-04-12T12:00:00+00:00"
+            "time": "2023-07-13T12:00:00+00:00"
         },
         {
             "name": "hamcrest/hamcrest-php",
         },
         {
             "name": "mockery/mockery",
-            "version": "1.6.2",
+            "version": "1.6.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/mockery/mockery.git",
-                "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191"
+                "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/mockery/mockery/zipball/13a7fa2642c76c58fa2806ef7f565344c817a191",
-                "reference": "13a7fa2642c76c58fa2806ef7f565344c817a191",
+                "url": "https://api.github.com/repos/mockery/mockery/zipball/b8e0bb7d8c604046539c1115994632c74dcb361e",
+                "reference": "b8e0bb7d8c604046539c1115994632c74dcb361e",
                 "shasum": ""
             },
             "require": {
                 "hamcrest/hamcrest-php": "^2.0.1",
                 "lib-pcre": ">=7.0",
-                "php": "^7.4 || ^8.0"
+                "php": ">=7.3"
             },
             "conflict": {
                 "phpunit/phpunit": "<8.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^8.5 || ^9.3",
-                "psalm/plugin-phpunit": "^0.18",
-                "vimeo/psalm": "^5.9"
+                "phpunit/phpunit": "^8.5 || ^9.6.10",
+                "psalm/plugin-phpunit": "^0.18.4",
+                "symplify/easy-coding-standard": "^11.5.0",
+                "vimeo/psalm": "^4.30"
             },
             "type": "library",
-            "extra": {
-                "branch-alias": {
-                    "dev-main": "1.6.x-dev"
-                }
-            },
             "autoload": {
                 "files": [
                     "library/helpers.php",
                 {
                     "name": "Pádraic Brady",
                     "email": "[email protected]",
-                    "homepage": "http://blog.astrumfutura.com"
+                    "homepage": "https://github.com/padraic",
+                    "role": "Author"
                 },
                 {
                     "name": "Dave Marshall",
                     "email": "[email protected]",
-                    "homepage": "http://davedevelopment.co.uk"
+                    "homepage": "https://davedevelopment.co.uk",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Nathanael Esayeas",
+                    "email": "[email protected]",
+                    "homepage": "https://github.com/ghostwriter",
+                    "role": "Lead Developer"
                 }
             ],
             "description": "Mockery is a simple yet flexible PHP mock object framework",
                 "testing"
             ],
             "support": {
+                "docs": "https://docs.mockery.io/",
                 "issues": "https://github.com/mockery/mockery/issues",
-                "source": "https://github.com/mockery/mockery/tree/1.6.2"
+                "rss": "https://github.com/mockery/mockery/releases.atom",
+                "security": "https://github.com/mockery/mockery/security/advisories",
+                "source": "https://github.com/mockery/mockery"
             },
-            "time": "2023-06-07T09:07:52+00:00"
+            "time": "2023-08-09T00:03:52+00:00"
         },
         {
             "name": "myclabs/deep-copy",
         },
         {
             "name": "nunomaduro/larastan",
-            "version": "v2.6.3",
+            "version": "v2.6.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/nunomaduro/larastan.git",
-                "reference": "73e5be5f5c732212ce6ca77ffd2753a136f36a23"
+                "reference": "6c5e8820f3db6397546f3ce48520af9d312aed27"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/73e5be5f5c732212ce6ca77ffd2753a136f36a23",
-                "reference": "73e5be5f5c732212ce6ca77ffd2753a136f36a23",
+                "url": "https://api.github.com/repos/nunomaduro/larastan/zipball/6c5e8820f3db6397546f3ce48520af9d312aed27",
+                "reference": "6c5e8820f3db6397546f3ce48520af9d312aed27",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/nunomaduro/larastan/issues",
-                "source": "https://github.com/nunomaduro/larastan/tree/v2.6.3"
+                "source": "https://github.com/nunomaduro/larastan/tree/v2.6.4"
             },
             "funding": [
                 {
                     "type": "patreon"
                 }
             ],
-            "time": "2023-06-13T21:39:27+00:00"
+            "time": "2023-07-29T12:13:13+00:00"
         },
         {
             "name": "phar-io/manifest",
         },
         {
             "name": "phpstan/phpstan",
-            "version": "1.10.23",
+            "version": "1.10.29",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-                "reference": "65ab678d1248a8bc6fde456f0d7ff3562a61a4cd"
+                "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/65ab678d1248a8bc6fde456f0d7ff3562a61a4cd",
-                "reference": "65ab678d1248a8bc6fde456f0d7ff3562a61a4cd",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
+                "reference": "ee5d8f2d3977fb09e55603eee6fb53bdd76ee9c1",
                 "shasum": ""
             },
             "require": {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-07-04T13:32:44+00:00"
+            "time": "2023-08-14T13:24:11+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
-            "version": "9.2.26",
+            "version": "9.2.27",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
-                "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1"
+                "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
-                "reference": "443bc6912c9bd5b409254a40f4b0f4ced7c80ea1",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b0a88255cb70d52653d80c890bd7f38740ea50d1",
+                "reference": "b0a88255cb70d52653d80c890bd7f38740ea50d1",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
-                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.26"
+                "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.27"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2023-03-06T12:58:08+00:00"
+            "time": "2023-07-26T13:44:30+00:00"
         },
         {
             "name": "phpunit/php-file-iterator",
         },
         {
             "name": "phpunit/phpunit",
-            "version": "9.6.9",
+            "version": "9.6.11",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "a9aceaf20a682aeacf28d582654a1670d8826778"
+                "reference": "810500e92855eba8a7a5319ae913be2da6f957b0"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a9aceaf20a682aeacf28d582654a1670d8826778",
-                "reference": "a9aceaf20a682aeacf28d582654a1670d8826778",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/810500e92855eba8a7a5319ae913be2da6f957b0",
+                "reference": "810500e92855eba8a7a5319ae913be2da6f957b0",
                 "shasum": ""
             },
             "require": {
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.9"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.11"
             },
             "funding": [
                 {
                     "type": "tidelift"
                 }
             ],
-            "time": "2023-06-11T06:13:56+00:00"
+            "time": "2023-08-19T07:10:56+00:00"
         },
         {
             "name": "sebastian/cli-parser",
         },
         {
             "name": "sebastian/global-state",
-            "version": "5.0.5",
+            "version": "5.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/global-state.git",
-                "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2"
+                "reference": "bde739e7565280bda77be70044ac1047bc007e34"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2",
-                "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bde739e7565280bda77be70044ac1047bc007e34",
+                "reference": "bde739e7565280bda77be70044ac1047bc007e34",
                 "shasum": ""
             },
             "require": {
             ],
             "support": {
                 "issues": "https://github.com/sebastianbergmann/global-state/issues",
-                "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5"
+                "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.6"
             },
             "funding": [
                 {
                     "type": "github"
                 }
             ],
-            "time": "2022-02-14T08:28:10+00:00"
+            "time": "2023-08-02T09:26:13+00:00"
         },
         {
             "name": "sebastian/lines-of-code",
diff --git a/database/factories/Api/ApiTokenFactory.php b/database/factories/Api/ApiTokenFactory.php
new file mode 100644 (file)
index 0000000..adf2fff
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+
+namespace Database\Factories\Api;
+
+use BookStack\Api\ApiToken;
+use BookStack\Users\Models\User;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Str;
+
+class ApiTokenFactory extends Factory
+{
+    protected $model = ApiToken::class;
+
+    public function definition(): array
+    {
+        return [
+            'token_id' => Str::random(10),
+            'secret' => Str::random(12),
+            'name' => $this->faker->name(),
+            'expires_at' => Carbon::now()->addYear(),
+            'created_at' => Carbon::now(),
+            'updated_at' => Carbon::now(),
+            'user_id' => User::factory(),
+        ];
+    }
+}
index efb65972b5f815cd4f14caf4eddfb9e363c7a91b..b5dcaee751c27bae829eabd835eed52131c1fcc2 100644 (file)
@@ -12,9 +12,10 @@ return new class extends Migration
      */
     public function up()
     {
-        DB::table('entity_permissions')
-            ->where('entity_type', '=', 'bookshelf')
-            ->update(['create' => 0]);
+        // Note: v23.06.2
+        // Migration removed since change to remove bookshelf create permissions was reverted.
+        // Create permissions were removed as incorrectly thought to be unused, but they did
+        // have a use via shelf permission copy-down to books.
     }
 
     /**
diff --git a/database/migrations/2023_07_25_124945_add_receive_notifications_role_permissions.php b/database/migrations/2023_07_25_124945_add_receive_notifications_role_permissions.php
new file mode 100644 (file)
index 0000000..4872e42
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+
+use Carbon\Carbon;
+use Illuminate\Database\Migrations\Migration;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        // Create new receive-notifications permission and assign to admin role
+        $permissionId = DB::table('role_permissions')->insertGetId([
+            'name'         => 'receive-notifications',
+            'display_name' => 'Receive & Manage Notifications',
+            'created_at'   => Carbon::now()->toDateTimeString(),
+            'updated_at'   => Carbon::now()->toDateTimeString(),
+        ]);
+
+        $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id;
+        DB::table('permission_role')->insert([
+            'role_id' => $adminRoleId,
+            'permission_id' => $permissionId,
+        ]);
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        $permission = DB::table('role_permissions')
+            ->where('name', '=', 'receive-notifications')
+            ->first();
+
+        if ($permission) {
+            DB::table('permission_role')->where([
+                'permission_id' => $permission->id,
+            ])->delete();
+        }
+
+        DB::table('role_permissions')
+            ->where('name', '=', 'receive-notifications')
+            ->delete();
+    }
+};
diff --git a/database/migrations/2023_07_31_104430_create_watches_table.php b/database/migrations/2023_07_31_104430_create_watches_table.php
new file mode 100644 (file)
index 0000000..e2a5c20
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('watches', function (Blueprint $table) {
+            $table->increments('id');
+            $table->integer('user_id')->index();
+            $table->integer('watchable_id');
+            $table->string('watchable_type', 100);
+            $table->tinyInteger('level', false, true)->index();
+            $table->timestamps();
+
+            $table->index(['watchable_id', 'watchable_type'], 'watchable_index');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('watches');
+    }
+};
index 0f030d671fad8e61c138e6c3846eda9052b8c6a6..47e8d1d7c1d32e4ed1c14b86601edfa7df736a77 100644 (file)
@@ -27,6 +27,8 @@ class DummyContentSeeder extends Seeder
         // Create an editor user
         $editorUser = User::factory()->create();
         $editorRole = Role::getRole('editor');
+        $additionalEditorPerms = ['receive-notifications', 'comment-create-all'];
+        $editorRole->permissions()->syncWithoutDetaching(RolePermission::whereIn('name', $additionalEditorPerms)->pluck('id'));
         $editorUser->attachRole($editorRole);
 
         // Create a viewer user
index a68ae50b47e8598d1fdeec41dbfc80b0d0acbcc8..44a077fa8f959cc1330debf91ec923d9064e59aa 100644 (file)
@@ -3,7 +3,7 @@
 All development on BookStack is currently done on the `development` branch. 
 When it's time for a release the `development` branch is merged into release with built & minified CSS & JS then tagged at its version. Here are the current development requirements:
 
-* [Node.js](https://nodejs.org/en/) v16.0+
+* [Node.js](https://nodejs.org/en/) v18.0+
 
 ## Building CSS & JavaScript Assets
 
index 1e602c0784004c27caa4cbadc7c3ebf4f7f7754a..b08dae6239fa61880b6dd6f5858479a66e87d402 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index d6e1362e6d83e2de1c8db6aa6adf6a3b2231d44f..c4107e111121d89303e75fe8199bd32e429aaaf1 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index 5b8c925608f517dacd82944c082f281db2e4e459..7e34d22e70db49d726379697ade6d1f25e7742dc 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copy Permissions to Books',
     'shelves_copy_permissions' => '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.',
index 290e09165947b1c4c166c2916f41a0c6ea6950b9..2a7f06a84209cb52fcf299e54557603d3bd620b1 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copia els permisos als llibres',
     'shelves_copy_permissions' => 'Copia els permisos',
     '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.',
index 1940fd16e3a84b42fa15d89047abb78b84255565..cbf92c80f41a911cfddc88f2cb05866563445d5a 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Oprávnění knihovny byla aktualizována',
     'shelves_permissions_active' => 'Oprávnění knihovny byla aktivována',
     'shelves_permissions_cascade_warning' => 'Oprávnění v Knihovnách nejsou automaticky kaskádována do obsažených knih. To proto, že kniha může existovat ve více Knihovnách. Oprávnění však lze zkopírovat do podřízených knih pomocí níže uvedené možnosti.',
+    '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_copy_permissions_to_books' => 'Kopírovat oprávnění na knihy',
     'shelves_copy_permissions' => 'Kopírovat oprávnění',
     'shelves_copy_permissions_explain' => 'Toto použije aktuální nastavení oprávnění knihovny na všechny knihy v ní obsažené. Před aktivací se ujistěte, že byly uloženy všechny změny oprávnění této knihovny.',
index 8cd7e925f8e7ef71710d9d45e113be9f6399c5d2..4fb043aa9d06a4ade958301c7b6eea54c77eb7a0 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copy Permissions to Books',
     'shelves_copy_permissions' => '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.',
index c07cabb26ab759fed75fcea60da08ace4a71cf82..bc899d7fc34b4b46680cf10526729acda755a5d6 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Kopier tilladelser til bøger',
     'shelves_copy_permissions' => 'Kopier tilladelser',
     '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.',
index 22c44686db03c6b18bef3f930857a717fc9108cb..d2f79016354602b8113d1fefd0ad4f6f3cfb6454 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Regalberechtigungen aktualisiert',
     'shelves_permissions_active' => 'Regalberechtigungen aktiv',
     'shelves_permissions_cascade_warning' => 'Berechtigungen für Regale werden nicht automatisch auf die enthaltenen Bücher übertragen. Das liegt daran, dass ein Buch in mehreren Regalen vorhanden sein kann. Berechtigungen können jedoch auf untergeordnete Bücher kopiert werden, indem Sie die unten stehende Option verwenden.',
+    '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_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
     'shelves_copy_permissions' => 'Berechtigungen kopieren',
     'shelves_copy_permissions_explain' => 'Dadurch werden die aktuellen Berechtigungseinstellungen dieses Regals auf alle darin enthaltenen Bücher angewendet. Vergewissern Sie sich vor der Aktivierung, dass alle Änderungen an den Berechtigungen für dieses Regal gespeichert wurden.',
index 9e6508112bd2edacc4b61be97f123df055ed30e9..949bc1473557eabb52df2f60506bef6db9775c80 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Regalberechtigungen aktualisiert',
     'shelves_permissions_active' => 'Regalberechtigungen aktiv',
     'shelves_permissions_cascade_warning' => 'Berechtigungen für Regale werden nicht automatisch auf die enthaltenen Bücher übertragen. Das liegt daran, dass ein Buch in mehreren Regalen vorhanden sein kann. Berechtigungen können jedoch auf untergeordnete Bücher kopiert werden, indem du die unten stehende Option verwendest.',
+    '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_copy_permissions_to_books' => 'Kopiere die Berechtigungen zum Buch',
     'shelves_copy_permissions' => 'Berechtigungen kopieren',
     'shelves_copy_permissions_explain' => 'Dadurch werden die aktuellen Berechtigungseinstellungen dieses Regals auf alle darin enthaltenen Bücher angewendet. Vergewissere dich vor der Aktivierung, dass alle Änderungen an den Berechtigungen für dieses Regal gespeichert wurden.',
index d840ec951d5d2aa89939238eae69294db5378fa1..07c18c048f240525eb56cca481cdd753bd1ccb8b 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Bookshelf Permissions Updated',
     'shelves_permissions_active' => 'Bookshelf Permissions Active',
     'shelves_permissions_cascade_warning' => 'Permissions on bookshelves 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_copy_permissions_to_books' => 'Αντιγραφή δικαιωμάτων στα βιβλία',
     'shelves_copy_permissions' => 'Αντιγραφή Δικαιωμάτων',
     'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this bookshelf to all books contained within. Before activating, ensure any changes to the permissions of this bookshelf have been saved.',
index e71a490de4696d4924e354db69751e746f0dbac4..d5b55c03dcb399205e0afa81853ca9c1fae32bf6 100644 (file)
@@ -58,6 +58,9 @@ return [
     'favourite_add_notification' => '":name" has been added to your favourites',
     'favourite_remove_notification' => '":name" has been removed from your favourites',
 
+    // Watching
+    'watch_update_level_notification' => 'Watch preferences successfully updated',
+
     // Auth
     'auth_login' => 'logged in',
     'auth_register' => 'registered as new user',
@@ -110,7 +113,12 @@ return [
     'recycle_bin_restore' => 'restored from recycle bin',
     'recycle_bin_destroy' => 'removed from recycle bin',
 
-    // Other
+    // Comments
     'commented_on'                => 'commented on',
+    'comment_create'              => 'added comment',
+    'comment_update'              => 'updated comment',
+    'comment_delete'              => 'deleted comment',
+
+    // Other
     'permissions_update'          => 'updated permissions',
 ];
index de7937b2be406a00fb344bb079b0773a77b5533c..47b74d5b6eb87a9aa912849b5affe0970f5f70a8 100644 (file)
@@ -42,6 +42,7 @@ return [
     'remove' => 'Remove',
     'add' => 'Add',
     'configure' => 'Configure',
+    'manage' => 'Manage',
     'fullscreen' => 'Fullscreen',
     'favourite' => 'Favourite',
     'unfavourite' => 'Unfavourite',
index 8cd7e925f8e7ef71710d9d45e113be9f6399c5d2..b1b0e5236903a357072f2077aaf0554e27307efc 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copy Permissions to Books',
     'shelves_copy_permissions' => '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.',
@@ -402,4 +403,28 @@ return [
     'references' => 'References',
     'references_none' => 'There are no tracked references to this item.',
     'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+
+    // 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_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',
 ];
diff --git a/lang/en/notifications.php b/lang/en/notifications.php
new file mode 100644 (file)
index 0000000..5539ae9
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Text used for activity-based notifications.
+ */
+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.',
+
+    'detail_page_name' => 'Page Name:',
+    'detail_commenter' => 'Commenter:',
+    'detail_comment' => 'Comment:',
+    'detail_created_by' => 'Created By:',
+    'detail_updated_by' => 'Updated By:',
+
+    'action_view_comment' => 'View Comment',
+    'action_view_page' => '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',
+];
index e9a47461b3d18a42a68699d729acf99b07b9eda5..118e8ba820f5dd6d4249832fcca696a44504e585 100644 (file)
@@ -5,6 +5,8 @@
  */
 
 return [
+    'preferences' => 'Preferences',
+
     'shortcuts' => 'Shortcuts',
     'shortcuts_interface' => 'Interface Keyboard Shortcuts',
     'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.',
@@ -15,4 +17,17 @@ return [
     '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!',
-];
\ No newline at end of file
+    'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.',
+
+    '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.',
+
+    'profile_overview_desc' => ' Manage your user profile details including preferred language and authentication options.',
+];
index c110e89921ad97fa132e8d88305956469d3cf43c..8821c77f0d1e99ecb9097c6e956aa9e56c85216c 100644 (file)
@@ -163,6 +163,7 @@ return [
     'role_manage_settings' => 'Manage app settings',
     'role_export_content' => 'Export content',
     'role_editor_change' => 'Change page editor',
+    'role_notifications' => 'Receive & manage notifications',
     'role_asset' => 'Asset Permissions',
     'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
     'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
index aa6a7946d7c22d4e52c990823b4785f84cdae145..f37a3539d02e26f56c8c56d84ad8211bf74e9bdc 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permisos del estante actualizados',
     'shelves_permissions_active' => 'Permisos del estante activos',
     'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros que contengan. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción a continuación.',
+    '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_copy_permissions_to_books' => 'Copiar permisos a los libros',
     'shelves_copy_permissions' => 'Copiar permisos',
     'shelves_copy_permissions_explain' => 'Esto aplicará los permisos de este estante para todos sus libros. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante han sido guardados.',
index 3d2ed45b453fb919dbf890d60e2e148ba595b3dd..4b7fb006b392e149dc5815870bfad07128368701 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permisos del Estante Actualizados',
     'shelves_permissions_active' => 'Permisos Activos del Estante',
     'shelves_permissions_cascade_warning' => 'Los permisos en los estantes no se aplican automáticamente a los libros que contengan. Esto se debe a que un libro puede existir en múltiples estantes. Sin embargo, los permisos pueden ser aplicados a los libros del estante utilizando la opción de abajo.',
+    '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_copy_permissions_to_books' => 'Copiar Permisos a los Libros',
     'shelves_copy_permissions' => 'Copiar Permisos',
     'shelves_copy_permissions_explain' => 'Esta acción aplicará los permisos del estante a todos los libros dentro del mismo. Antes de activarlo, asegúrese de que todos los cambios de permisos para este estante fueron guardados.',
index 1a237857b122858f051a399055279833dff22948..f47e46ea8163f38aa7b1aef63661398053bc2385 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Riiuli õigused muudetud',
     'shelves_permissions_active' => 'Riiuli õigused on aktiivsed',
     'shelves_permissions_cascade_warning' => 'Riiuli õigused ei rakendu automaatselt sellel olevatele raamatutele, kuna raamat võib olla korraga mitmel riiulil. Alloleva valiku abil saab aga riiuli õigused kopeerida raamatutele.',
+    '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_copy_permissions_to_books' => 'Kopeeri õigused raamatutele',
     'shelves_copy_permissions' => 'Kopeeri õigused',
     'shelves_copy_permissions_explain' => 'See rakendab riiuli praegused õigused kõigile sellel olevatele raamatutele. Enne jätkamist veendu, et riiuli õiguste muudatused oleks salvestatud.',
index 0b744a7f900f8aee158813c0a96a9cb85a565b92..d5ab5c9ea61469ad9b0d88a2c60b8562532148a0 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Apalategi baimenak eguneratuta',
     'shelves_permissions_active' => 'Apalategi baimenak aktibatuta',
     'shelves_permissions_cascade_warning' => 'Apaletako baimenak ez dira automatikoki hauen barneko liburuetan gordeko. Liburu bat apalategi askotan egon daitekeelako. Hala ere, baimenak apalategiko liburutara kopiatu daitezke, behean agertzen den aukera erabiliz.',
+    '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_copy_permissions_to_books' => 'Kopiatu baimenak liburura',
     'shelves_copy_permissions' => 'Gorde baimenak',
     'shelves_copy_permissions_explain' => 'Honek apalategi honen egungo baimen-konfigurazioa aplikatuko die barruan dauden liburu guztiei. Aktibatu aurretik, ziurtatu apaletan aldaketak gorde direla.',
index 0709ff3d97a0d27d00ba4501ce06c0fbca6cdc86..1f0ad28e948e484ea79ba9200e4b1b2fb1b4894b 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'مجوزهای کانال بروزرسانی شد',
     'shelves_permissions_active' => 'مجوزهای قفسه فعال است',
     'shelves_permissions_cascade_warning' => 'مجوزهای موجود در قفسه‌ها به طور خودکار به کتاب‌های حاوی اطلاق نمی‌شوند. دلیل آن این است که یک کتاب می تواند در چندین قفسه وجود داشته باشد. با این حال، مجوزها را می‌توان با استفاده از گزینه پایین همین صفحه در کتاب‌های فرزند کپی کرد.',
+    '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_copy_permissions_to_books' => 'کپی مجوزها در کتابها',
     'shelves_copy_permissions' => 'کپی مجوزها',
     'shelves_copy_permissions_explain' => 'با این کار تنظیمات مجوز فعلی این قفسه برای همه کتاب‌های موجود در آن اعمال می‌شود. قبل از فعال کردن، مطمئن شوید که هر گونه تغییر در مجوزهای این قفسه، ذخیره شده است.',
index 4ccaeb7457e40a917a192a20694e3d004629d130..8baf3d385abfdf39e834a5a30a850e4b5cefbfb9 100644 (file)
@@ -15,7 +15,7 @@ return [
     'page_restore'                => 'a restauré la page',
     'page_restore_notification'   => 'Page restaurée avec succès',
     'page_move'                   => 'a déplacé la page',
-    'page_move_notification'      => 'Page successfully moved',
+    'page_move_notification'      => 'Page déplacée avec succès',
 
     // Chapters
     'chapter_create'              => 'a créé le chapitre',
@@ -25,7 +25,7 @@ return [
     'chapter_delete'              => 'a supprimé le chapitre',
     'chapter_delete_notification' => 'Chapitre supprimé avec succès',
     'chapter_move'                => 'a déplacé le chapitre',
-    'chapter_move_notification' => 'Chapter successfully moved',
+    'chapter_move_notification' => 'Chapitre déplacé avec succès',
 
     // Books
     'book_create'                 => 'a créé un livre',
@@ -50,28 +50,28 @@ return [
     'bookshelf_delete_notification'    => 'Étagère supprimée avec succès',
 
     // Revisions
-    'revision_restore' => 'restored revision',
-    'revision_delete' => 'deleted revision',
-    'revision_delete_notification' => 'Revision successfully deleted',
+    'revision_restore' => 'a restauré la révision',
+    'revision_delete' => 'révision supprimée',
+    'revision_delete_notification' => 'Révision supprimée avec succès',
 
     // Favourites
     'favourite_add_notification' => '":name" a été ajouté dans vos favoris',
     'favourite_remove_notification' => '":name" a été supprimé de vos favoris',
 
     // Auth
-    'auth_login' => 'logged in',
-    'auth_register' => 'registered as new user',
-    'auth_password_reset_request' => 'requested user password reset',
-    'auth_password_reset_update' => 'reset user password',
-    'mfa_setup_method' => 'configured MFA method',
+    'auth_login' => 'connecté',
+    'auth_register' => 'enregistré en tant que nouvel utilisateur',
+    'auth_password_reset_request' => 'demande de réinitialisation du mot de passe',
+    'auth_password_reset_update' => 'réinitialisation du mot de passe',
+    'mfa_setup_method' => 'méthode MFA configurée',
     'mfa_setup_method_notification' => 'Méthode multi-facteurs configurée avec succès',
-    'mfa_remove_method' => 'removed MFA method',
+    'mfa_remove_method' => 'méthode MFA supprimée',
     'mfa_remove_method_notification' => 'Méthode multi-facteurs supprimée avec succès',
 
     // Settings
-    'settings_update' => 'updated settings',
-    'settings_update_notification' => 'Settings successfully updated',
-    'maintenance_action_run' => 'ran maintenance action',
+    'settings_update' => 'paramètres mis à jour',
+    'settings_update_notification' => 'Paramètres mis à jour avec succès',
+    'maintenance_action_run' => 'exécuter l\'action de maintenance',
 
     // Webhooks
     'webhook_create' => 'Créer un Webhook',
@@ -82,33 +82,33 @@ return [
     'webhook_delete_notification' => 'Webhook supprimé avec succès',
 
     // Users
-    'user_create' => 'created user',
-    'user_create_notification' => 'User successfully created',
-    'user_update' => 'updated user',
+    'user_create' => 'utilisateur créé',
+    'user_create_notification' => 'Utilisateur créé avec succès',
+    'user_update' => 'utilisateur mis à jour',
     'user_update_notification' => 'Utilisateur mis à jour avec succès',
-    'user_delete' => 'deleted user',
+    'user_delete' => 'utilisateur supprimé',
     'user_delete_notification' => 'Utilisateur supprimé avec succès',
 
     // API Tokens
-    'api_token_create' => 'created api token',
-    'api_token_create_notification' => 'API token successfully created',
-    'api_token_update' => 'updated api token',
-    'api_token_update_notification' => 'API token successfully updated',
-    'api_token_delete' => 'deleted api token',
-    'api_token_delete_notification' => 'API token successfully deleted',
+    'api_token_create' => 'jeton d\'api créé',
+    'api_token_create_notification' => 'Jeton d\'API créé avec succès',
+    'api_token_update' => 'jeton d\'api mis à jour',
+    'api_token_update_notification' => 'Jeton d\'API mis à jour avec succès',
+    'api_token_delete' => 'jeton d\'api supprimé',
+    'api_token_delete_notification' => 'Jeton d\'API supprimé avec succès',
 
     // Roles
-    'role_create' => 'created role',
+    'role_create' => 'rôle créé',
     'role_create_notification' => 'Rôle créé avec succès',
-    'role_update' => 'updated role',
+    'role_update' => 'rôle mis à jour',
     'role_update_notification' => 'Rôle mis à jour avec succès',
-    'role_delete' => 'deleted role',
+    'role_delete' => 'rôle supprimé',
     'role_delete_notification' => 'Rôle supprimé avec succès',
 
     // Recycle Bin
-    'recycle_bin_empty' => 'emptied recycle bin',
-    'recycle_bin_restore' => 'restored from recycle bin',
-    'recycle_bin_destroy' => 'removed from recycle bin',
+    'recycle_bin_empty' => 'corbeille vidée',
+    'recycle_bin_restore' => 'restauré à partir de la corbeille',
+    'recycle_bin_destroy' => 'supprimé de la corbeille',
 
     // Other
     'commented_on'                => 'a commenté',
index a8019c1896519e7da8dc25d348067a5db8be57eb..b12c4b3485f7809322ced49c3b9b0feb39b468f4 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permissions de l\'étagère mises à jour',
     'shelves_permissions_active' => 'Permissions de l\'étagère activées',
     'shelves_permissions_cascade_warning' => 'Les permissions sur les étagères ne sont pas automatiquement recopiées aux livres qu\'elles contiennent, car un livre peut exister dans plusieurs étagères. Les permissions peuvent cependant être recopiées vers les livres contenus en utilisant l\'option ci-dessous.',
+    '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_copy_permissions_to_books' => 'Copier les permissions vers les livres',
     'shelves_copy_permissions' => 'Copier les permissions',
     'shelves_copy_permissions_explain' => 'Ceci va appliquer les permissions actuelles de cette étagère à tous les livres qu\'elle contient. Avant de continuer, assurez-vous que toutes les permissions de cette étagère ont été sauvegardées.',
@@ -213,7 +214,7 @@ return [
     'pages_editing_page' => 'Modification de la page',
     'pages_edit_draft_save_at' => 'Brouillon enregistré le ',
     'pages_edit_delete_draft' => 'Supprimer le brouillon',
-    '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' => 'Êtes-vous sûr de vouloir supprimer vos modifications de page brouillon ? Toutes vos modifications, depuis la dernière sauvegarde complète, seront perdues et l\'éditeur sera mis à jour avec l\'état de sauvegarde de la dernière page non-brouillon.',
     'pages_edit_discard_draft' => 'Jeter le brouillon',
     'pages_edit_switch_to_markdown' => 'Basculer vers l\'éditeur Markdown',
     'pages_edit_switch_to_markdown_clean' => '(Contenu nettoyé)',
@@ -286,8 +287,8 @@ return [
         'time_b' => 'dans les :minCount dernières minutes',
         'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
     ],
-    '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' => 'Brouillon annulé ! L\'éditeur a été mis à jour avec le contenu de la page actuelle',
+    'pages_draft_deleted' => 'Brouillon supprimé ! L\'éditeur a été mis à jour avec le contenu de la page actuelle',
     'pages_specific' => 'Page spécifique',
     'pages_is_template' => 'Modèle de page',
 
@@ -365,13 +366,13 @@ return [
     'comment_new' => 'Nouveau commentaire',
     'comment_created' => 'commenté :createDiff',
     'comment_updated' => 'Mis à jour :updateDiff par :username',
-    'comment_updated_indicator' => 'Updated',
+    'comment_updated_indicator' => 'Mis à jour',
     'comment_deleted_success' => 'Commentaire supprimé',
     'comment_created_success' => 'Commentaire ajouté',
     'comment_updated_success' => 'Commentaire mis à jour',
     'comment_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer ce commentaire ?',
     'comment_in_reply_to' => 'En réponse à :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' => 'Voici les commentaires qui ont été laissés sur cette page. Les commentaires peuvent être ajoutés et gérés en visualisant la page enregistrée.',
 
     // Revision
     'revision_delete_confirm' => 'Êtes-vous sûr de vouloir supprimer cette révision ?',
index 8a18949b0a9abb4b573c79311206cd99ed1a5b9a..4c2700d16897ac5f1d84f8b5bf4cbe3997749bb4 100644 (file)
@@ -58,7 +58,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être enregistré. Vérifiez votre connexion internet',
-    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
+    'page_draft_delete_fail' => 'Impossible de supprimer le brouillon et de récupérer le contenu sauvegardé de la page actuelle',
     'page_custom_home_deletion' => 'Impossible de supprimer une page définie comme page d\'accueil',
 
     // Entities
index 2222bf0bb463c68913e9b9876d894bde8defff1d..63e5fe3a39ba6afcf81ccccc0a467b4eba96e499 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index 8e49c58d84f0adfb01d7b21827bade0fb3933803..a5372c87d3d32ffe5c0955d15a9fdac9093a9d04 100644 (file)
@@ -9,7 +9,7 @@ return [
     // General editor terms
     'general' => 'Općenito',
     'advanced' => 'Napredno',
-    'none' => 'None',
+    'none' => 'Ništa',
     'cancel' => 'Otkaži',
     'save' => 'Spremi',
     'close' => 'Zatvori',
@@ -28,147 +28,147 @@ return [
 
     // Toolbar
     'formats' => 'Formati',
-    'header_large' => 'Large Header',
-    'header_medium' => 'Medium Header',
-    'header_small' => 'Small Header',
-    'header_tiny' => 'Tiny Header',
-    'paragraph' => 'Paragraph',
-    'blockquote' => 'Blockquote',
-    'inline_code' => 'Inline code',
-    'callouts' => 'Callouts',
-    'callout_information' => 'Information',
-    'callout_success' => 'Success',
-    'callout_warning' => 'Warning',
-    'callout_danger' => 'Danger',
-    'bold' => 'Bold',
-    'italic' => 'Italic',
-    'underline' => 'Underline',
-    'strikethrough' => 'Strikethrough',
-    'superscript' => 'Superscript',
-    'subscript' => 'Subscript',
-    'text_color' => 'Text color',
-    'custom_color' => 'Custom color',
-    'remove_color' => 'Remove color',
-    'background_color' => 'Background color',
-    'align_left' => 'Align left',
-    'align_center' => 'Align center',
-    'align_right' => 'Align right',
-    'align_justify' => 'Justify',
-    'list_bullet' => 'Bullet list',
-    'list_numbered' => 'Numbered list',
-    'list_task' => 'Task list',
-    'indent_increase' => 'Increase indent',
-    'indent_decrease' => 'Decrease indent',
-    'table' => 'Table',
-    'insert_image' => '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',
+    'header_large' => 'Veliki Naslov',
+    'header_medium' => 'Srednji Naslov',
+    'header_small' => 'Mali Naslov',
+    'header_tiny' => 'Malecki Naslov',
+    'paragraph' => 'Odlomak',
+    'blockquote' => 'Blok citat',
+    'inline_code' => 'Ugrađeni Kod',
+    'callouts' => 'Zabilješke',
+    'callout_information' => 'Informacija',
+    'callout_success' => 'Uspjeh',
+    'callout_warning' => 'Upozorenje',
+    'callout_danger' => 'Opasnost',
+    'bold' => 'Podebljano',
+    'italic' => 'Kurziv',
+    'underline' => 'Podcrtano',
+    'strikethrough' => 'Precrtano',
+    'superscript' => 'Gornji indeks',
+    'subscript' => 'Donji indeks',
+    'text_color' => 'Boja teksta',
+    'custom_color' => 'Prilagođena boja',
+    'remove_color' => 'Ukloni boju',
+    'background_color' => 'Boja pozadine',
+    'align_left' => 'Poravnaj lijevo',
+    'align_center' => 'Poravnaj po sredini',
+    'align_right' => 'Poravnaj desno',
+    'align_justify' => 'Poravnaj obostrano',
+    'list_bullet' => 'Popis s točkama',
+    'list_numbered' => 'Numerirani popis',
+    'list_task' => 'Lista zadataka',
+    'indent_increase' => 'Povećaj uvlaku',
+    'indent_decrease' => 'Smanji uvlaku',
+    'table' => 'Tablica',
+    'insert_image' => 'Umetni sliku',
+    'insert_image_title' => 'Umetni/Editiraj sliku',
+    'insert_link' => 'Umetni/editiraj poveznicu',
+    'insert_link_title' => 'Umetni/Editiraj poveznicu',
+    'insert_horizontal_line' => 'Ubaci vodoravnu liniju',
+    'insert_code_block' => 'Ubacivanje kodnog bloka',
+    'edit_code_block' => 'Editiraj kodni blok',
+    'insert_drawing' => 'Umetni/editiraj crtež',
+    'drawing_manager' => 'Upravitelj crteža',
+    'insert_media' => 'Umetni/editiraj medij',
+    'insert_media_title' => 'Umetni/Editiraj medij',
+    'clear_formatting' => 'Očisti sva oblikovanja',
+    'source_code' => 'Izvorni kod',
+    'source_code_title' => 'Izvorni Kod',
+    'fullscreen' => 'Puni zaslon',
+    'image_options' => 'Mogućnosti slike',
 
     // Tables
-    'table_properties' => 'Table properties',
-    'table_properties_title' => 'Table Properties',
-    'delete_table' => 'Delete table',
-    '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',
-    'copy_column' => 'Copy column',
-    'paste_column_before' => 'Paste column before',
-    'paste_column_after' => 'Paste column after',
-    'cell_padding' => 'Cell padding',
-    'cell_spacing' => 'Cell spacing',
-    'caption' => 'Caption',
-    'show_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',
+    'table_properties' => 'Svojstva tablice',
+    'table_properties_title' => 'Svojstva Tablice',
+    'delete_table' => 'Obriši tablicu',
+    'insert_row_before' => 'Umetni redak prije',
+    'insert_row_after' => 'Umetni redak poslije',
+    'delete_row' => 'Izbriši red',
+    'insert_column_before' => 'Umetni stupac prije',
+    'insert_column_after' => 'Umetni stupac poslije',
+    'delete_column' => 'Izbriši stupac',
+    'table_cell' => 'Ćelija',
+    'table_row' => 'Red',
+    'table_column' => 'Stupac',
+    'cell_properties' => 'Svojstava ćelija',
+    'cell_properties_title' => 'Svojstava Ćelija',
+    'cell_type' => 'Vrsta Ćelije',
+    'cell_type_cell' => 'Ćelija',
+    'cell_scope' => 'Obuhvat',
+    'cell_type_header' => 'Naslovna Ćelija',
+    'merge_cells' => 'Spoji ćelije',
+    'split_cell' => 'Razdvoji ćelije',
+    'table_row_group' => 'Grupa Redaka',
+    'table_column_group' => 'Grupa Stupaca',
+    'horizontal_align' => 'Vodoravno poravnanje',
+    'vertical_align' => 'Uspravno poravnanje',
+    'border_width' => 'Širina obruba',
+    'border_style' => 'Stil obruba',
+    'border_color' => 'Boja obruba',
+    'row_properties' => 'Svojstava redaka',
+    'row_properties_title' => 'Svojstava Redaka',
+    'cut_row' => 'Izreži redak',
+    'copy_row' => 'Kopiraj redak',
+    'paste_row_before' => 'Zalijepi redak prije',
+    'paste_row_after' => 'Zalijepi redak nakon',
+    'row_type' => 'Vrsta redka',
+    'row_type_header' => 'Zaglavlje',
+    'row_type_body' => 'Tijelo',
+    'row_type_footer' => 'Podnožje',
+    'alignment' => 'Poravnanje',
+    'cut_column' => 'Izreži stupac',
+    'copy_column' => 'Kopiraj stupac',
+    'paste_column_before' => 'Zalijepi stupac prije',
+    'paste_column_after' => 'Zalijepi stupac poslije',
+    'cell_padding' => 'Unutarnji razmak ćelije',
+    'cell_spacing' => 'Razmak između ćelija',
+    'caption' => 'Natpis',
+    'show_caption' => 'Prikaži natpis',
+    'constrain' => 'Ograniči proporcije',
+    'cell_border_solid' => 'Puno',
+    'cell_border_dotted' => 'Točkasto',
+    'cell_border_dashed' => 'Isprekidano',
+    'cell_border_double' => 'Dvostruko',
+    'cell_border_groove' => 'Ponavljajući uzorak',
+    'cell_border_ridge' => 'Rub',
+    'cell_border_inset' => 'Unutarnji rub',
+    'cell_border_outset' => 'Vanjski rub',
+    'cell_border_none' => 'Ništa',
+    'cell_border_hidden' => 'Skriveno',
 
     // Images, links, details/summary & embed
-    'source' => 'Source',
-    'alt_desc' => 'Alternative description',
-    'embed' => 'Embed',
-    'paste_embed' => 'Paste your embed code below:',
+    'source' => 'Izvor',
+    'alt_desc' => 'Alternativni opis',
+    'embed' => 'Umetnuto',
+    'paste_embed' => 'Zalijepite svoj ugrađeni kod u nastavku:',
     'url' => 'URL',
-    'text_to_display' => 'Text to display',
-    'title' => 'Title',
-    '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',
+    'text_to_display' => 'Tekst za prikaz',
+    'title' => 'Naslov',
+    'open_link' => 'Otvori poveznicu',
+    'open_link_in' => 'Otvori poveznicu u...',
+    'open_link_current' => 'Trenutni prozor',
+    'open_link_new' => 'Novi prozor',
+    'remove_link' => 'Ukloni poveznicu',
+    'insert_collapsible' => 'Umetni skupljajući blok',
+    'collapsible_unwrap' => 'Odmotaj',
+    'edit_label' => 'Izmjeni oznaku',
+    'toggle_open_closed' => 'Promijeni otvoreno/zatvoreno',
+    'collapsible_edit' => 'Izmjeni skupljajući blok',
+    'toggle_label' => 'Promijeni oznaku',
 
     // About view
-    'about' => 'About the editor',
-    'about_title' => 'About the WYSIWYG Editor',
-    'editor_license' => 'Editor License & Copyright',
-    '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:',
+    'about' => 'O Editoru',
+    'about_title' => 'O WYSIWYG Editoru',
+    'editor_license' => 'Licenca i autorsko pravo uređivača',
+    'editor_tiny_license' => 'Ovaj uređivač je izrađen pomoću: tinyLink koji je dostupan pod MIT licencom.',
+    'editor_tiny_license_link' => 'Detalji o autorskim pravima i licenci za TinyMCE mogu se pronaći ovdje.',
+    'save_continue' => 'Spremi Stranicu i Nastavi',
+    'callouts_cycle' => '(Nastavite pritiskati kako biste prelazili kroz vrste)',
+    'link_selector' => 'Poveznica na sadržaj',
+    'shortcuts' => 'Prečaci',
+    'shortcut' => 'Prečac',
+    'shortcuts_intro' => 'Sljedeći prečaci su dostupni u uređivaču:',
     'windows_linux' => '(Windows/Linux)',
     'mac' => '(Mac)',
-    'description' => 'Description',
+    'description' => 'Opis',
 ];
index 0fb9da20709ce7e453128f2a3244c8caba752be0..ab637ec0e5f63ba37ac6706061bd0ba9be21d7b5 100644 (file)
@@ -23,34 +23,34 @@ return [
     'meta_updated' => 'Ažurirano :timeLength',
     'meta_updated_name' => 'Ažurirano :timeLength od :user',
     'meta_owned_name' => 'Vlasništvo :user',
-    'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
+    'meta_reference_page_count' => 'Referencirano na :count stranici|Referencirano na :count stranica',
     'entity_select' => 'Odaberi subjekt',
-    'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
+    'entity_select_lack_permission' => 'Nemate potrebne ovlasti za odabir ovog elementa',
     'images' => 'Slike',
     'my_recent_drafts' => 'Nedavne skice',
     'my_recently_viewed' => 'Nedavno viđeno',
-    'my_most_viewed_favourites' => 'My Most Viewed Favourites',
-    'my_favourites' => 'My Favourites',
+    'my_most_viewed_favourites' => 'Moji Najviše Pregledani Favoriti',
+    'my_favourites' => 'Moji Favoriti',
     'no_pages_viewed' => 'Niste pogledali nijednu stranicu',
     'no_pages_recently_created' => 'Nema nedavno stvorenih stranica',
     'no_pages_recently_updated' => 'Nema nedavno ažuriranih stranica',
     'export' => 'Izvoz',
     'export_html' => 'Web File',
-    'export_pdf' => 'PDF File',
+    'export_pdf' => 'PDF Datoteka',
     'export_text' => 'Text File',
-    'export_md' => 'Markdown File',
+    'export_md' => 'Markdown Datoteka',
 
     // Permissions and restrictions
     'permissions' => 'Dopuštenja',
-    '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' => 'Postavi dozvole ovdje kako bi prebrisao zadane dozvole koje su dodeljene korisničkim ulogama.',
+    'permissions_book_cascade' => 'Dozvole postavljene na knjige će se automatski prenositi na podređena poglavlja i stranice, osim ako imaju definirane posebne dozvole.',
+    'permissions_chapter_cascade' => 'Dozvole postavljene na poglavlja će se automatski prenositi na podređene stranice, osim ako imaju definirane posebne dozvole.',
     'permissions_save' => 'Spremi dopuštenje',
     'permissions_owner' => 'Vlasnik',
-    '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_role_everyone_else' => 'Svi Ostali',
+    'permissions_role_everyone_else_desc' => 'Postavi dozvole za sve uloge koje nisu posebno prebrisane.',
+    'permissions_role_override' => 'Prebriši dozvole za ulogu',
+    'permissions_inherit_defaults' => 'Naslijedi zadane postavke',
 
     // Search
     'search_results' => 'Pretraži rezultate',
@@ -93,23 +93,24 @@ return [
     'shelves_save' => 'Spremi policu',
     'shelves_books' => 'Knjige na ovoj polici',
     'shelves_add_books' => 'Dodaj knjige na ovu policu',
-    'shelves_drag_books' => 'Drag books below to add them to this shelf',
+    'shelves_drag_books' => 'Povucite knjige ispod kako biste ih dodali na ovu policu',
     'shelves_empty_contents' => 'Ova polica još nema dodijeljene knjige',
     'shelves_edit_and_assign' => 'Uredi policu za dodavanje knjiga',
-    '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_edit_named' => 'Uredi Policu :name',
+    'shelves_edit' => 'Uredi Policu',
+    'shelves_delete' => 'Izbriši Policu',
+    'shelves_delete_named' => 'Izbriši Policu :name',
+    'shelves_delete_explain' => "Ovo će izbrisati policu pod nazivom \":name\". Knjige koje se nalaze na polici neće biti izbrisane.",
+    'shelves_delete_confirmation' => 'Jeste li sigurni da želite obrisati policu?',
+    'shelves_permissions' => 'Dozvole za policu',
+    'shelves_permissions_updated' => 'Ažurirana dopuštenja za Policu',
+    'shelves_permissions_active' => 'Aktivna Dopuštenja za Policu',
+    'shelves_permissions_cascade_warning' => 'Dozvole na policama se automatski ne prenose na knjige koje se nalaze na njima. To je zato što se jedna knjiga može nalaziti na više polica. Međutim, dozvole se mogu kopirati na podređene knjige koristeći opciju koja se nalazi ispod.',
+    '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_copy_permissions_to_books' => 'Kopiraj dopuštenja za knjige',
     'shelves_copy_permissions' => 'Kopiraj dopuštenja',
-    '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' => 'Ovo će primijeniti trenutne postavke dozvola ove police na sve knjige koje se nalaze na njoj. Prije aktiviranja, provjerite jeste li spremili sve promjene dozvola na ovoj polici.',
+    'shelves_copy_permission_success' => 'Dozvole s police kopirane su na :count knjiga',
 
     // Books
     'book' => 'Knjiga',
@@ -141,7 +142,7 @@ return [
     'books_search_this' => 'Traži knjigu',
     'books_navigation' => 'Navigacija knjige',
     'books_sort' => 'Razvrstaj sadržaj knjige',
-    '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.',
+    'books_sort_desc' => 'Pomaknite poglavlja i stranice unutar knjige kako biste reorganizirali njen sadržaj. Možete dodati i druge knjige što omogućuje jednostavno premještanje poglavlja i stranica između knjiga.',
     'books_sort_named' => 'Razvrstaj knjigu :bookName',
     'books_sort_name' => 'Razvrstaj po imenu',
     'books_sort_created' => 'Razvrstaj po datumu nastanka',
@@ -150,19 +151,19 @@ return [
     'books_sort_chapters_last' => 'Zadnja poglavlja',
     'books_sort_show_other' => 'Pokaži ostale knjige',
     'books_sort_save' => 'Spremi novi poredak',
-    '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' => 'Dodajte druge knjige ovdje kako biste ih uključili u sortiranje i omogućili jednostavno premeštanje između knjiga.',
+    'books_sort_move_up' => 'Pomakni gore',
+    'books_sort_move_down' => 'Pomakni dolje',
+    'books_sort_move_prev_book' => 'Pomakni na Prethodnu Knjigu',
+    'books_sort_move_next_book' => 'Pomakni na Slijedeću Knjigu',
+    'books_sort_move_prev_chapter' => 'Pomakni na Prethodno Poglavlje',
+    'books_sort_move_next_chapter' => 'Pomakni na Slijedeće Poglavlje',
+    'books_sort_move_book_start' => 'Pomakni na Početak Knjige',
+    'books_sort_move_book_end' => 'Pomakni na Kraj Knjige',
+    'books_sort_move_before_chapter' => 'Pomakni Prije Poglavlja',
+    'books_sort_move_after_chapter' => 'Pomakni Nakon Poglavlja',
+    'books_copy' => 'Kopiraj Knjigu',
+    'books_copy_success' => 'Knjiga je uspješno kopirana',
 
     // Chapters
     'chapter' => 'Poglavlje',
@@ -180,14 +181,14 @@ return [
     'chapters_save' => 'Spremi poglavlje',
     'chapters_move' => 'Premjesti poglavlje',
     'chapters_move_named' => 'Premjesti poglavlje :chapterName',
-    'chapters_copy' => 'Copy Chapter',
-    'chapters_copy_success' => 'Chapter successfully copied',
+    'chapters_copy' => 'Kopiraj Poglavlje',
+    'chapters_copy_success' => 'Poglavlje je uspješno kopirano',
     'chapters_permissions' => 'Dopuštenja za poglavlje',
     'chapters_empty' => 'U ovom poglavlju nema stranica.',
     'chapters_permissions_active' => 'Aktivna dopuštenja za poglavlje',
     'chapters_permissions_success' => 'Ažurirana dopuštenja za poglavlje',
     'chapters_search_this' => 'Pretraži poglavlje',
-    'chapter_sort_book' => 'Sort Book',
+    'chapter_sort_book' => 'Sortiraj knjigu',
 
     // Pages
     'page' => 'Stranica',
@@ -213,21 +214,21 @@ return [
     'pages_editing_page' => 'Uređivanje stranice',
     'pages_edit_draft_save_at' => 'Nacrt spremljen kao',
     'pages_edit_delete_draft' => 'Izbriši nacrt',
-    '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' => 'Jeste li sigurni da želite izbrisati promjene na stranici koje niste spremljene? Sve promjene koje ste napravili od posljednjeg potpunog spremanja bit će izgubljene, a uređivač će biti ažuriran s najnovijim spremljenim stanjem stranice.',
     'pages_edit_discard_draft' => 'Odbaci nacrt',
-    '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_markdown' => 'Prebacite se na Markdown uređivač',
+    'pages_edit_switch_to_markdown_clean' => '(Čisti Sadržaj)',
+    'pages_edit_switch_to_markdown_stable' => '(Stabilan Sadržaj)',
+    'pages_edit_switch_to_wysiwyg' => 'Prebaci se na WYSIWYG uređivač',
     'pages_edit_set_changelog' => 'Postavi dnevnik promjena',
     'pages_edit_enter_changelog_desc' => 'Ukratko opišite promjene koje ste napravili',
     'pages_edit_enter_changelog' => 'Unesi dnevnik promjena',
-    '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' => 'Promijeni Uređivač',
+    'pages_editor_switch_are_you_sure' => 'Jeste li sigurni da želite promijeniti uređivač za ovu stranicu?',
+    'pages_editor_switch_consider_following' => 'Kada mijenjate uređivače, uzmite u obzir sljedeće faktore:',
+    'pages_editor_switch_consideration_a' => 'Nakon što se spremi, nova opcija uređivača bit će korištena od strane svih budućih urednika, uključujući one koji možda neće moći sami promijeniti vrstu uređivača.',
+    'pages_editor_switch_consideration_b' => 'To može potencijalno dovesti do gubitka detalja i sintakse u određenim situacijama.',
+    'pages_editor_switch_consideration_c' => 'Promjene oznaka ili dnevnika promjena napravljene nakon posljednjeg spremanja neće se zadržati nakon ove promjene.',
     'pages_save' => 'Spremi stranicu',
     'pages_title' => 'Naslov stranice',
     'pages_name' => 'Ime stranice',
@@ -236,8 +237,8 @@ return [
     'pages_md_insert_image' => 'Umetni sliku',
     'pages_md_insert_link' => 'Umetni poveznicu',
     'pages_md_insert_drawing' => 'Umetni crtež',
-    'pages_md_show_preview' => 'Show preview',
-    'pages_md_sync_scroll' => 'Sync preview scroll',
+    'pages_md_show_preview' => 'Prikaži pregled',
+    'pages_md_sync_scroll' => 'Sinkroniziraj pomicanje pregleda',
     'pages_not_in_chapter' => 'Stranica nije u poglavlju',
     'pages_move' => 'Premjesti stranicu',
     'pages_copy' => 'Kopiraj stranicu',
@@ -247,17 +248,17 @@ return [
     'pages_permissions_success' => 'Ažurirana dopuštenja stranice',
     'pages_revision' => 'Revizija',
     'pages_revisions' => 'Revizija stranice',
-    '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' => 'Ispod su navedene sve prošle revizije ove stranice. Možete pregledati, usporediti i obnoviti stare verzije stranice ako dozvole to omogućuju. Cjelokupna povijest stranice možda nije potpuno prikazana ovdje, budući da, ovisno o konfiguraciji sustava, stare revizije mogu biti automatski izbrisane.',
     'pages_revisions_named' => 'Revizije stranice :pageName',
     'pages_revision_named' => 'Revizija stranice :pageName',
     'pages_revision_restored_from' => 'Oporavak iz #:id; :summary',
     'pages_revisions_created_by' => 'Stvoreno od',
     'pages_revisions_date' => 'Datum revizije',
     'pages_revisions_number' => '#',
-    'pages_revisions_sort_number' => 'Revision Number',
+    'pages_revisions_sort_number' => 'Broj revizije',
     'pages_revisions_numbered' => 'Revizija #:id',
     'pages_revisions_numbered_changes' => 'Revizija #:id Promjene',
-    'pages_revisions_editor' => 'Editor Type',
+    'pages_revisions_editor' => 'Vrsta uređivača',
     'pages_revisions_changelog' => 'Dnevnik promjena',
     'pages_revisions_changes' => 'Promjene',
     'pages_revisions_current' => 'Trenutna verzija',
@@ -265,20 +266,20 @@ return [
     'pages_revisions_restore' => 'Vrati',
     'pages_revisions_none' => 'Ova stranica nema revizija',
     'pages_copy_link' => 'Kopiraj poveznicu',
-    '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' => 'Skoči na odjeljak u uređivaču',
+    'pages_pointer_enter_mode' => 'Uđi u način odabira odjeljaka',
+    'pages_pointer_label' => 'Opcije odjeljka stranice',
+    'pages_pointer_permalink' => 'Permalink Odjeljka Stranice',
+    'pages_pointer_include_tag' => 'Uključi oznaku odjeljka stranice',
+    'pages_pointer_toggle_link' => 'Način permalinka, pritisnite za prikaz oznake uključivanja (include tag)',
+    'pages_pointer_toggle_include' => 'Način oznake uključivanja, Pritisnite za prikaz permalinka',
     'pages_permissions_active' => 'Aktivna dopuštenja stranice',
     'pages_initial_revision' => 'Početno objavljivanje',
-    'pages_references_update_revision' => 'System auto-update of internal links',
+    'pages_references_update_revision' => 'Automatsko ažuriranje internih veza sustava',
     'pages_initial_name' => 'Nova stranica',
     'pages_editing_draft_notification' => 'Uređujete nacrt stranice posljednji put spremljen :timeDiff.',
     'pages_draft_edited_notification' => 'Ova je stranica u međuvremenu ažurirana. Preporučujemo da odbacite ovaj nacrt.',
-    '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' => 'Ova stranica je ažurirana nakon što je ovaj nacrt stvoren. Preporučuje se da odbacite ovaj nacrt ili budite oprezni da ne prepišete nikakve promjene na stranici.',
     'pages_draft_edit_active' => [
         'start_a' => ':count korisnika koji uređuju ovu stranicu',
         'start_b' => ':userName je počeo uređivati ovu stranicu',
@@ -286,8 +287,8 @@ return [
         'time_b' => 'u zadnjih :minCount minuta',
         'message' => ':start :time. Pripazite na uzajamna ažuriranja!',
     ],
-    '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' => 'Nacrt je odbačen! Uređivač je ažuriran s trenutnim sadržajem stranice',
+    'pages_draft_deleted' => 'Nacrt je izbrisan! Uređivač je ažuriran s trenutnim sadržajem stranice',
     'pages_specific' => 'Predlošci stranice',
     'pages_is_template' => 'Predložak stranice',
 
@@ -297,32 +298,32 @@ return [
     'book_tags' => 'Oznake knjiga',
     'shelf_tags' => 'Oznake polica',
     'tag' => 'Oznaka',
-    'tags' =>  '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.',
-    'tag_name' =>  'Tag Name',
+    'tags' =>  'Oznake',
+    'tags_index_desc' => 'Oznake se mogu primijeniti na sadržaj unutar sustava kako bi se primijenila fleksibilna forma kategorizacije. Oznake mogu imati ključ i vrijednost, pri čemu je vrijednost opcionalna. Nakon primjene, sadržaj se može pretraživati koristeći ime oznake i vrijednost.',
+    'tag_name' =>  'Naziv Oznake',
     'tag_value' => 'Oznaka vrijednosti (neobavezno)',
-    'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.",
+    'tags_explain' => "Dodajte neke oznake kako biste bolje kategorizirali svoj sadržaj. \n Možete dodijeliti vrijednost oznaci za organizaciju s više detalja.",
     'tags_add' => 'Dodaj oznaku',
     'tags_remove' => 'Makni oznaku',
-    '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' => 'Ukupna upotreba oznaka',
+    'tags_assigned_pages' => 'Dodijeljeno Stranicama',
+    'tags_assigned_chapters' => 'Dodijeljeno Poglavljima',
+    'tags_assigned_books' => 'Dodijeljeno Knjigama',
+    'tags_assigned_shelves' => 'Dodijeljeno Policama',
+    'tags_x_unique_values' => ':count jedinstvenih vrijednosti',
+    'tags_all_values' => 'Sve vrijednosti',
+    'tags_view_tags' => 'Pregledaj Oznake',
+    'tags_view_existing_tags' => 'Pregledaj postojeće oznake',
+    'tags_list_empty_hint' => 'Oznake se mogu dodijeliti putem bočne trake uređivača stranice ili prilikom uređivanja detalja knjige, poglavlja ili police.',
     'attachments' => 'Prilozi',
     'attachments_explain' => 'Dodajte datoteke ili poveznice za prikaz na vašoj stranici. Vidljive su na rubnoj oznaci stranice.',
     'attachments_explain_instant_save' => 'Promjene se automatski spremaju.',
     'attachments_upload' => 'Dodaj datoteku',
     'attachments_link' => 'Dodaj poveznicu',
-    'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
+    'attachments_upload_drop' => 'Alternativno, možete povući i ispustiti datoteku ovdje kako biste je pričvrstili.',
     'attachments_set_link' => 'Postavi poveznicu',
     'attachments_delete' => 'Jeste li sigurni da želite izbrisati ovu stavku?',
-    'attachments_dropzone' => 'Drop files here to upload',
+    'attachments_dropzone' => 'Ovdje spustite datoteke za učitavanje',
     'attachments_no_files' => 'Nijedna datoteka nije prenesena',
     'attachments_explain_link' => 'Možete dodati poveznicu ako ne želite prenijeti datoteku. Poveznica može voditi na drugu stranicu ili datoteku.',
     'attachments_link_name' => 'Ime poveznice',
@@ -365,13 +366,13 @@ return [
     'comment_new' => 'Novi komentar',
     'comment_created' => 'komentirano :createDiff',
     'comment_updated' => 'Ažurirano :updateDiff od :username',
-    'comment_updated_indicator' => 'Updated',
+    'comment_updated_indicator' => 'Ažurirano',
     'comment_deleted_success' => 'Izbrisani komentar',
     'comment_created_success' => 'Dodani komentar',
     'comment_updated_success' => 'Ažurirani komentar',
     'comment_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovaj komentar?',
     'comment_in_reply_to' => 'Odgovor na :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' => 'Evo komentara koji su ostavljeni na ovoj stranici. Komentari se mogu dodavati i upravljati prilikom pregleda spremljene stranice.',
 
     // Revision
     'revision_delete_confirm' => 'Jeste li sigurni da želite izbrisati ovaj ispravak?',
@@ -379,27 +380,27 @@ return [
     'revision_cannot_delete_latest' => 'Posljednji ispravak se ne može izbrisati.',
 
     // 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' => 'Molimo vas da uzmete u obzir sljedeće prilikom kopiranja sadržaja.',
+    'copy_consider_permissions' => 'Prilagođene postavke dozvola neće biti kopirane.',
+    'copy_consider_owner' => 'Postat ćete vlasnikom svih kopiranih sadržaja.',
+    'copy_consider_images' => 'Slikovne datoteke stranice neće biti duplicirane, a originalne slike će zadržati svoj odnos prema stranici na koju su prvobitno prenesene.',
+    'copy_consider_attachments' => 'Privici stranice neće biti kopirani.',
+    'copy_consider_access' => 'Promjena lokacije, vlasnika ili dozvola može rezultirati pristupom ovom sadržaju osobama koje ranije nisu imale pristup.',
 
     // 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' => 'Pretvori u Policu',
+    'convert_to_shelf_contents_desc' => 'Možete pretvoriti ovu knjigu u novu policu s istim sadržajem. Poglavlja koja se nalaze unutar ove knjige bit će pretvorena u nove knjige. Ako ova knjiga sadrži bilo koje stranice koje nisu u poglavlju, ova knjiga će biti preimenovana i sadržavati takve stranice, a postat će dio nove police.',
+    'convert_to_shelf_permissions_desc' => 'Sve dozvole postavljene na ovu knjigu bit će kopirane na novu policu i sve nove podređene knjige koje nemaju vlastite dozvole. Napomena: Dozvole na policama se ne prenose automatski na sadržaj unutar njih, kao što je slučaj s knjigama.',
+    'convert_book' => 'Pretvori u Knjigu',
+    'convert_book_confirm' => 'Jeste li sigurni da želite pretvoriti ovu knjigu?',
+    'convert_undo_warning' => 'Ovo se ne može tako lako poništiti.',
+    'convert_to_book' => 'Pretvori u Knjigu',
+    'convert_to_book_desc' => 'Možete pretvoriti ovo poglavlje u novu knjigu s istim sadržajem. Sve postavljene dozvole na ovom poglavlju bit će kopirane u novu knjigu, ali nasljedne dozvole iz nadređene knjige neće biti kopirane, što može rezultirati promjenom kontrole pristupa.',
+    'convert_chapter' => 'Pretvori Poglavlje',
+    'convert_chapter_confirm' => 'Jeste li sigurni da želite pretvoriti ovo poglavlje?',
 
     // References
-    'references' => 'References',
-    'references_none' => 'There are no tracked references to this item.',
-    'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+    'references' => 'Reference',
+    'references_none' => 'Nema praćenih referenci na ovu stavku.',
+    'references_to_desc' => 'U nastavku su prikazane sve poznate stranice u sustavu koje se povezuju s ovom stavkom.',
 ];
index 51104aba7d1c66b549511995bca4c4626dc783fe..976139eb8b58f6d9355cef42a2e04e57fa3b9c55 100644 (file)
@@ -7,7 +7,7 @@
 return [
 
     'password' => 'Lozinka mora imati najmanje 8 znakova i biti potvrđena.',
-    'user' => "We can't find a user with that e-mail address.",
+    'user' => "Ne možemo pronaći korisnika s tom adresom e-pošte.",
     'token' => 'Ponovno postavljanje lozinke nemoguće putem ove adrese.',
     'sent' => 'Na vašu email adresu poslana je poveznica za ponovno postavljanje!',
     'reset' => 'Vaša je lozinka ponovno postavljena!',
index e9a47461b3d18a42a68699d729acf99b07b9eda5..706846aaf38d6cab42f1c0950b3e4fec272a87bd 100644 (file)
@@ -5,14 +5,14 @@
  */
 
 return [
-    'shortcuts' => 'Shortcuts',
-    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
-    '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' => 'Prečaci',
+    'shortcuts_interface' => 'Prečaci tipkovnice u Sučelju',
+    'shortcuts_toggle_desc' => 'Ovdje možete omogućiti ili onemogućiti prečace tastature u korisničkom sučelju sustava koji se koriste za navigaciju i akcije.',
+    'shortcuts_customize_desc' => 'Možete prilagoditi svaki od prečaca u nastavku. Samo pritisnite željenu kombinaciju tipki nakon odabira polja za unos prečaca.',
+    'shortcuts_toggle_label' => 'Prečaci tipkovnice omogućeni',
+    'shortcuts_section_navigation' => 'Navigacija',
+    'shortcuts_section_actions' => 'Uobičajene radnje',
+    'shortcuts_save' => 'Spremi prečace',
+    'shortcuts_overlay_desc' => 'Napomena: Kada su prečaci tastature omogućeni, dostupan je pomoćni prikaz preko pritiska na znak "?" koji će istaknuti dostupne prečace za radnje trenutno vidljive na zaslonu.',
+    'shortcuts_update_success' => 'Postavke prečaca su ažurirane!',
 ];
\ No newline at end of file
index d573b8673ecb9a004c0bf3aaf742959771fe605b..1777dd50b1b58fe2bcc49ae14c1d0a12631283fd 100644 (file)
@@ -9,8 +9,8 @@ return [
     // Common Messages
     'settings' => 'Postavke',
     'settings_save' => 'Spremi postavke',
-    'system_version' => 'System Version',
-    'categories' => 'Categories',
+    'system_version' => 'Sistemska Verzija',
+    'categories' => 'Kategorije',
 
     // App Settings
     'app_customization' => 'Prilagođavanje',
@@ -26,15 +26,15 @@ return [
     'app_secure_images' => 'Visoka razina sigurnosti prijenosa slika',
     'app_secure_images_toggle' => 'Omogući visoku sigurnost prijenosa slika',
     'app_secure_images_desc' => 'Zbog specifične izvedbe sve su slike javne. Osigurajte da indeksi direktorija nisu omogućeni kako bi se spriječio neovlašten pristup.',
-    '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' => 'Zadani Uređivač Stranica',
+    'app_default_editor_desc' => 'Odaberite koji uređivač će se koristiti kao zadani prilikom uređivanja novih stranica. Ovo se može prebrisati na razini pojedine stranice ukoliko dozvole to omogućuju.',
     'app_custom_html' => 'Prilagođeni HTML sadržaj',
     'app_custom_html_desc' => 'Sav sadržaj dodan ovdje bit će umetnut na dno <glavne> stranice. To je korisno za stiliziranje i dodavanje analitičkog koda.',
     'app_custom_html_disabled_notice' => 'Prilagođeni HTML je onemogućen kako bi se osiguralo vraćanje promjena u slučaju kvara.',
     'app_logo' => 'Logo aplikacije',
-    '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' => 'Ovo se koristi u zaglavlju aplikacije, među ostalim područjima. Ova slika treba biti visoka 86 piksela. Velike slike bit će smanjene.',
+    'app_icon' => 'Ikona Aplikacije',
+    'app_icon_desc' => 'Ova ikona se koristi za kartice preglednika i ikone prečaca. Trebala bi biti PNG slika kvadratnog oblika sa dimenzijama 256 piksela.',
     'app_homepage' => 'Glavna stranica aplikacije',
     'app_homepage_desc' => 'Odaberite prikaz svoje glavne stranice umjesto već zadane. Za odabrane stranice ne vrijede zadana dopuštenja.',
     'app_homepage_select' => 'Odaberi stranicu',
@@ -48,12 +48,12 @@ return [
     'app_disable_comments_desc' => 'Onemogući komentare za sve stranice u aplikaciji. <br> Postojeći komentari nisu prikazani.',
 
     // 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' => 'Paleta Boje Aplikacije',
+    'color_scheme_desc' => 'Postavite boje koje će se koristiti u korisničkom sučelju aplikacije. Boje se mogu konfigurirati zasebno za tamni i svijetli način rada kako bi se najbolje uklopile u temu i osigurale čitljivost.',
+    'ui_colors_desc' => 'Postavite primarnu boju aplikacije i zadane boje veza. Primarna boja se uglavnom koristi za zaglavlje trake, gumbe i dekoracije sučelja. Zadana boja veza se koristi za tekstualne veze i radnje, kako unutar sadržaja tako i u korisničkom sučelju aplikacije.',
+    'app_color' => 'Primarnab Boja',
+    'link_color' => 'Zadana Boja Veze',
+    'content_colors_desc' => 'Postavite boje za sve elemente u hijerarhiji organizacije stranica. Preporučuje se odabir boja slične svjetlini kao zadane boje radi bolje čitljivosti.',
     'bookshelf_color' => 'Boja police',
     'book_color' => 'Boja knjige',
     'chapter_color' => 'Boja poglavlja',
@@ -77,7 +77,7 @@ return [
     // Maintenance settings
     'maint' => 'Održavanje',
     'maint_image_cleanup' => 'Čišćenje slika',
-    'maint_image_cleanup_desc' => 'Scans page & revision content to check which images and drawings are currently in use and which images are redundant. Ensure you create a full database and image backup before running this.',
+    'maint_image_cleanup_desc' => 'Skenirajte sadržaj stranice i revizije kako biste provjerili koje slike i crteži se trenutno koriste i koje slike su suvišne. Pobrinite se napraviti potpunu sigurnosnu kopiju baze podataka i slika prije pokretanja ovog procesa.',
     'maint_delete_images_only_in_revisions' => 'Izbriši slike koje postoje u prijašnjim revizijama',
     'maint_image_cleanup_run' => 'Pokreni čišćenje',
     'maint_image_cleanup_warning' => ':count moguće neiskorištene slike. Jeste li sigurni da želite izbrisati ove slike?',
@@ -92,16 +92,16 @@ return [
     'maint_send_test_email_mail_text' => 'Čestitamo! Ako ste primili ovaj e mail znači da ćete ga moći koristiti.',
     'maint_recycle_bin_desc' => 'Izbrisane police, knjige, poglavlja i stranice poslane su u Recycle bin i mogu biti vraćene ili trajno izbrisane. Starije stavke bit će automatski izbrisane nakon nekog vremena što ovisi o konfiguraciji sustava.',
     'maint_recycle_bin_open' => 'Otvori Recycle Bin',
-    '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' => 'Regeneriraj Reference',
+    'maint_regen_references_desc' => 'Ova akcija će ponovno izgraditi indeks prekriženih referenci između stavki unutar baze podataka. Obično se to automatski obavlja, ali ova akcija može biti korisna za indeksiranje starih sadržaja ili sadržaja dodanih putem neoficijelnih metoda.',
+    'maint_regen_references_success' => 'Indeks referenci je ponovno izgrađen!',
+    'maint_timeout_command_note' => 'Napomena: Ova radnja može potrajati, što može dovesti do problema s prekidom veze (timeout) u nekim web okruženjima. Kao alternativa, ova radnja se može izvršiti putem naredbe u terminalu.',
 
     // Recycle Bin
-    'recycle_bin' => 'Recycle Bin',
+    'recycle_bin' => 'Koš za smeće',
     'recycle_bin_desc' => 'Ovdje možete vratiti izbrisane stavke ili ih trajno ukloniti iz sustava. Popis nije filtriran kao što su to popisi u kojima su omogućeni filteri.',
     'recycle_bin_deleted_item' => 'Izbrisane stavke',
-    'recycle_bin_deleted_parent' => 'Parent',
+    'recycle_bin_deleted_parent' => 'Nadređeni',
     'recycle_bin_deleted_by' => 'Izbrisano od',
     'recycle_bin_deleted_at' => 'Vrijeme brisanja',
     'recycle_bin_permanently_delete' => 'Trajno izbrisano',
@@ -114,7 +114,7 @@ return [
     'recycle_bin_restore_list' => 'Stavke koje treba vratiti',
     'recycle_bin_restore_confirm' => 'Ova radnja vraća izbrisane stavke i njene podređene elemente na prvobitnu lokaciju. Ako je nadređena stavka izbrisana i nju treba vratiti.',
     'recycle_bin_restore_deleted_parent' => 'S obzirom da je nadređena stavka obrisana najprije treba vratiti nju.',
-    'recycle_bin_restore_parent' => 'Restore Parent',
+    'recycle_bin_restore_parent' => 'Vrati Nadređenog',
     'recycle_bin_destroy_notification' => 'Ukupno izbrisane :count stavke iz Recycle Bin',
     'recycle_bin_restore_notification' => 'Ukupno vraćene :count stavke iz Recycle Bin',
 
@@ -128,7 +128,7 @@ return [
     'audit_table_user' => 'Korisnik',
     'audit_table_event' => 'Događaj',
     'audit_table_related' => 'Povezana stavka ili detalj',
-    'audit_table_ip' => 'IP Address',
+    'audit_table_ip' => 'IP Adresa',
     'audit_table_date' => 'Datum aktivnosti',
     'audit_date_from' => 'Rangiraj datum od',
     'audit_date_to' => 'Rangiraj datum do',
@@ -136,22 +136,22 @@ return [
     // Role Settings
     'roles' => 'Uloge',
     'role_user_roles' => 'Uloge korisnika',
-    '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' => 'Uloge se koriste za grupiranje korisnika i dodjeljivanje sistemskih dozvola njihovim članovima. Kada je korisnik član više uloga, privilegije se zbrajaju i korisnik nasljeđuje sve sposobnosti svake uloge.',
+    'roles_x_users_assigned' => ':count korisnik dodijeljen|:count korisnika dodijeljeno',
+    'roles_x_permissions_provided' => ':count dozvola|:count dozvole',
+    'roles_assigned_users' => 'Odredi Korisnika',
+    'roles_permissions_provided' => 'Dostavljene Dozvole',
     'role_create' => 'Stvori novu ulogu',
     'role_delete' => 'Izbriši ulogu',
     'role_delete_confirm' => 'Ovo će izbrisati ulogu povezanu s imenom \':roleName\'.',
     'role_delete_users_assigned' => 'Ova uloga dodijeljena je :userCount. Ako želite premjestiti korisnike iz ove uloge odaberite novu ulogu u nastavku.',
-    'role_delete_no_migration' => "Don't migrate users",
+    'role_delete_no_migration' => "Ne migriraj korisnike",
     'role_delete_sure' => 'Jeste li sigurni da želite obrisati ovu ulogu?',
     'role_edit' => 'Uredi ulogu',
     'role_details' => 'Detalji uloge',
     'role_name' => 'Ime uloge',
     'role_desc' => 'Kratki opis uloge',
-    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
+    'role_mfa_enforced' => 'Zahtijeva višestruku provjeru autentičnosti',
     'role_external_auth_id' => 'Autorizacija',
     'role_system' => 'Dopuštenja sustava',
     'role_manage_users' => 'Upravljanje korisnicima',
@@ -161,13 +161,13 @@ return [
     'role_manage_page_templates' => 'Upravljanje predlošcima stranica',
     'role_access_api' => 'API pristup',
     'role_manage_settings' => 'Upravljanje postavkama aplikacija',
-    'role_export_content' => 'Export content',
-    'role_editor_change' => 'Change page editor',
+    'role_export_content' => 'Izvoz sadržaja',
+    'role_editor_change' => 'Promijeni uređivač stranica',
     'role_asset' => 'Upravljanje vlasništvom',
     'roles_system_warning' => 'Uzmite u obzir da pristup bilo kojem od ovih dopuštenja dozvoljavate korisniku upravljanje dopuštenjima ostalih u sustavu. Ova dopuštenja dodijelite pouzdanim korisnicima.',
     'role_asset_desc' => 'Ova dopuštenja kontroliraju zadane pristupe. Dopuštenja za knjige, poglavlja i stranice ih poništavaju.',
     'role_asset_admins' => 'Administratori automatski imaju pristup svim sadržajima, ali ove opcije mogu prikazati ili sakriti korisnička sučelja.',
-    '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' => 'Ovo se odnosi na vidljivost unutar upravitelja slika. Stvarni pristup uploadiranim slikovnim datotekama ovisit će o odabranim opcijama pohrane slika u sustavu.',
     'role_all' => 'Sve',
     'role_own' => 'Vlastito',
     'role_controlled_by_asset' => 'Kontrolirano od strane vlasnika',
@@ -177,7 +177,7 @@ return [
 
     // Users
     'users' => 'Korisnici',
-    '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' => 'Kreirajte i upravljajte pojedinačnim korisničkim računima unutar sustava. Korisnički računi koriste se za prijavu i pripisivanje sadržaja i aktivnosti. Dozvole pristupa temelje se uglavnom na ulogama, ali vlasništvo korisničkog sadržaja, među ostalim faktorima, također može utjecati na dozvole i pristup.',
     'user_profile' => 'Profil korisnika',
     'users_add_new' => 'Dodajte novog korisnika',
     'users_search' => 'Pretražite korisnike',
@@ -188,7 +188,7 @@ return [
     'users_role' => 'Uloge korisnika',
     'users_role_desc' => 'Odaberite koje će uloge biti dodijeljene ovom korisniku. Ako korisnik ima više uloga njihova će se dopuštenja prilagoditi.',
     'users_password' => 'Lozinka korisnika',
-    'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.',
+    'users_password_desc' => 'Postavite lozinku koja se koristi za prijavu u aplikaciju. Lozinka mora imati najmanje 8 znakova.',
     'users_send_invite_text' => 'Možete odabrati slanje e maila korisniku i dozvoliti mu da postavi svoju lozinku ili vi to možete učiniti za njega.',
     'users_send_invite_option' => 'Pošaljite pozivnicu korisniku putem emaila',
     'users_external_auth_id' => 'Vanjska autorizacija',
@@ -219,10 +219,10 @@ return [
     'users_api_tokens_create' => 'Stvori token',
     'users_api_tokens_expires' => 'Isteklo',
     'users_api_tokens_docs' => 'API dokumentacija',
-    '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' => 'Višefaktorska Provjera Vjerodostojnosti',
+    'users_mfa_desc' => 'Postavite višestruku provjeru autentičnosti kao dodatni sloj sigurnosti za svoj korisnički račun.',
+    'users_mfa_x_methods' => ':count metoda konfigurirano|:count metode konfigurirane',
+    'users_mfa_configure' => 'Konfiguriraj Metode',
 
     // API Tokens
     'user_api_token_create' => 'Stvori API token',
@@ -232,9 +232,9 @@ return [
     'user_api_token_expiry_desc' => 'Postavite datum kada token istječe. Ostavljanjem ovog polja praznim automatski se postavlja dugoročno razdoblje.',
     'user_api_token_create_secret_message' => 'Odmah nakon kreiranja tokena prikazat će se "Token ID" i "Token Secret". To će se prikazati samo jednom i zato preporučujemo da ga spremite na sigurno.',
     'user_api_token' => 'API token',
-    'user_api_token_id' => 'Token ID',
+    'user_api_token_id' => 'ID Tokena',
     'user_api_token_id_desc' => 'Ovaj sistemski generiran identifikator ne može se uređivati i bit će potreban pri API zahtjevima.',
-    'user_api_token_secret' => 'Token Secret',
+    'user_api_token_secret' => 'Token Tajna',
     'user_api_token_secret_desc' => 'Ovaj sistemski generirani Token Secret trebat ćete za API zahtjev. Prikazuje se samo prvi put i zato ga spremite na sigurno.',
     'user_api_token_created' => 'Token kreiran :timeAgo',
     'user_api_token_updated' => 'Token ažuriran :timeAgo',
@@ -245,7 +245,7 @@ return [
     // Webhooks
     'webhooks' => 'Web-dojavnici',
     'webhooks_index_desc' => 'Web-dojavnici su način slanja podataka na vanjske URL-ove kada se određene radnje i događaji dogode unutar sustava, omogućujući integraciju temeljenu na događajima s vanjskim platformama poput sustava za slanje poruka ili obavijesti.',
-    'webhooks_x_trigger_events' => ':count trigger event|:count trigger events',
+    'webhooks_x_trigger_events' => ':count događaj okidača|:count događaji okidača',
     'webhooks_create' => 'Kreiraj Novi Web-dojavnik',
     'webhooks_none_created' => 'Nijedan web-dojavnik nije još kreiran.',
     'webhooks_edit' => 'Uredi Web-dojavnik',
@@ -253,23 +253,23 @@ return [
     'webhooks_details' => 'Detalji Web-dojavnika',
     'webhooks_details_desc' => 'Navedite korisnički prijateljsko ime i POST krajnju točku kao lokaciju na koju će se slati podaci web-dojavnika.',
     'webhooks_events' => 'Događaji Web-dojavnika',
-    '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_events_desc' => 'Odaberite sve događaje koji bi trebali pokrenuti poziv za ovaj web-dojavnik.',
+    'webhooks_events_warning' => 'Imajte na umu da će se ovi događaji pokrenuti za sve odabrane događaje, čak i ako se primjenjuju prilagođene dozvole. Osigurajte se da upotreba ovog web-dojavnika neće otkriti povjerljiv sadržaj.',
+    'webhooks_events_all' => 'Svi sistemski događaji',
     'webhooks_name' => 'Naziv Web-dojavnika',
-    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
-    'webhooks_endpoint' => 'Webhook Endpoint',
+    'webhooks_timeout' => 'Vremensko ograničenje zahtjeva web-dojavnika (u sek.)',
+    'webhooks_endpoint' => 'Odredište Web-dojavnika',
     'webhooks_active' => 'Web-dojavnik Aktivan',
-    'webhook_events_table_header' => 'Events',
+    'webhook_events_table_header' => 'Događaji',
     'webhooks_delete' => 'Izbriši Web-dojavnik',
-    '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_delete_warning' => 'Ovo će u potpunosti izbrisati web-dojavnik pod nazivom ":webhookName" iz sustava.',
+    'webhooks_delete_confirm' => 'Jeste li sigurni da želite obrisati ovaj Web-dojavnik?',
+    'webhooks_format_example' => 'Primjer formata web-dojavnika',
+    'webhooks_format_example_desc' => 'Podaci web-dojavnika se šalju kao POST zahtjev na konfiguriranu krajnju točku kao JSON prema sljedećem formatu. Svojstva "related_item" i "url" su opcionalna i ovise o vrsti pokrenutog događaja.',
     'webhooks_status' => 'Status Web-dojavnika',
-    'webhooks_last_called' => 'Last Called:',
-    'webhooks_last_errored' => 'Last Errored:',
-    'webhooks_last_error_message' => 'Last Error Message:',
+    'webhooks_last_called' => 'Zadnji Poziv:',
+    'webhooks_last_errored' => 'Zadnja pogreška:',
+    'webhooks_last_error_message' => 'Posljednja poruka o pogrešci:',
 
 
     //! If editing translations files directly please ignore this in all
index 2097ce8eaf437e909e29230abb1a574795f4c069..35613cff6cb2eaa468f4aa120ed5cb4cd61f67a5 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Jogosultság másolása könyvekre',
     'shelves_copy_permissions' => 'Jogosultság másolása',
     '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.',
index 49ae1ba1144eb037465bb2f17949771ac45f6c6d..ce39b07233c45cf60fa76334a58616849b18ca4e 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Salin Izin ke Buku',
     'shelves_copy_permissions' => 'Salin Izin',
     '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.',
index f53afe87f50fda1a4b6317d3f0b7cb2190776fd0..e8941f63065298c1c6b4a6c3b50fea5cf3cb5f7e 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permessi Libreria Aggiornati',
     'shelves_permissions_active' => 'Permessi Libreria Attivi',
     'shelves_permissions_cascade_warning' => 'I permessi delle librerie non si estendono automaticamente ai libri contenuti. Questo perché un libro può essere presente su più scaffali. I permessi possono comunque essere copiati ai libri al suo interno usando l\'opzione sottostante.',
+    '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_copy_permissions_to_books' => 'Copia Permessi ai Libri',
     'shelves_copy_permissions' => 'Copia Permessi',
     'shelves_copy_permissions_explain' => 'Verranno applicati tutti i permessi della libreria ai libri al suo interno. Prima dell\'attivazione, assicurati che ogni permesso di questa libreria sia salvato.',
index 32bf60c372a85dcb654c126b8fd02d927cabf0c2..dc226fa6847f5bf5ffb4befdec2c8acc10e9e527 100644 (file)
@@ -36,8 +36,8 @@ return [
     'book_update_notification'    => 'ブックを更新しました',
     'book_delete'                 => 'がブックを削除:',
     'book_delete_notification'    => 'ブックを削除しました',
-    'book_sort'                   => 'がブックの並び順を変更:',
-    'book_sort_notification'      => 'ブックが再度並び変えられました',
+    'book_sort'                   => 'がブックの並び順を変更:',
+    'book_sort_notification'      => 'ブック内の並び順を変更しました',
 
     // Bookshelves
     'bookshelf_create'            => 'が本棚を作成:',
@@ -50,8 +50,8 @@ return [
     'bookshelf_delete_notification'    => '本棚を削除しました',
 
     // Revisions
-    'revision_restore' => 'restored revision',
-    'revision_delete' => 'deleted revision',
+    'revision_restore' => 'がリビジョンを復元',
+    'revision_delete' => 'がリビジョンを削除',
     'revision_delete_notification' => 'リビジョンを削除しました',
 
     // Favourites
@@ -59,56 +59,56 @@ return [
     'favourite_remove_notification' => '":name"がお気に入りから削除されました',
 
     // Auth
-    'auth_login' => 'logged in',
-    'auth_register' => 'registered as new user',
-    'auth_password_reset_request' => 'requested user password reset',
-    'auth_password_reset_update' => 'reset user password',
-    'mfa_setup_method' => 'configured MFA method',
+    'auth_login' => 'がログイン',
+    'auth_register' => 'が新規ユーザ登録',
+    'auth_password_reset_request' => 'がパスワードリセットを要求',
+    'auth_password_reset_update' => 'がパスワードをリセット',
+    'mfa_setup_method' => 'が多要素認証を設定',
     'mfa_setup_method_notification' => '多要素認証を設定しました',
-    'mfa_remove_method' => 'removed MFA method',
+    'mfa_remove_method' => 'が多要素認証を削除',
     'mfa_remove_method_notification' => '多要素認証を解除しました',
 
     // Settings
-    'settings_update' => 'updated settings',
+    'settings_update' => 'が設定を更新',
     'settings_update_notification' => '設定を更新しました',
-    'maintenance_action_run' => 'ran maintenance action',
+    'maintenance_action_run' => 'がメンテナンス作業を実施',
 
     // Webhooks
-    'webhook_create' => 'がWebhookを作成:',
+    'webhook_create' => 'がWebhookを作成',
     'webhook_create_notification' => 'Webhookを作成しました',
-    'webhook_update' => 'がWebhookを更新:',
+    'webhook_update' => 'がWebhookを更新',
     'webhook_update_notification' => 'Webhookを更新しました',
-    'webhook_delete' => 'がWebhookを削除:',
+    'webhook_delete' => 'がWebhookを削除',
     'webhook_delete_notification' => 'Webhookを削除しました',
 
     // Users
-    'user_create' => 'created user',
+    'user_create' => 'がユーザを作成',
     'user_create_notification' => 'ユーザーを作成しました',
-    'user_update' => 'updated user',
+    'user_update' => 'がユーザを更新',
     'user_update_notification' => 'ユーザーを更新しました',
-    'user_delete' => 'deleted user',
+    'user_delete' => 'がユーザを削除',
     '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トークンを更新',
     'api_token_update_notification' => 'APIトークンを更新しました',
-    'api_token_delete' => 'deleted api token',
+    'api_token_delete' => 'がAPIトークンを削除',
     'api_token_delete_notification' => 'APIトークンを削除しました',
 
     // Roles
-    'role_create' => 'created role',
+    'role_create' => 'が役割を作成',
     'role_create_notification' => '役割を作成しました',
-    'role_update' => 'updated role',
+    'role_update' => 'が役割を更新',
     'role_update_notification' => '役割を更新しました',
-    'role_delete' => 'deleted role',
+    'role_delete' => 'が役割を削除',
     'role_delete_notification' => '役割を削除しました',
 
     // Recycle Bin
-    'recycle_bin_empty' => 'emptied recycle bin',
-    'recycle_bin_restore' => 'restored from recycle bin',
-    'recycle_bin_destroy' => 'removed from recycle bin',
+    'recycle_bin_empty' => 'がゴミ箱を空にしました',
+    'recycle_bin_restore' => 'がゴミ箱から復元',
+    'recycle_bin_destroy' => 'がゴミ箱から完全に削除',
 
     // Other
     'commented_on'                => 'がコメント:',
index 3baa403543ee3d2531fa4565baa3243016d422ed..89da95e2dde005424760036da5c216b50ca43190 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => '本棚の権限を更新しました',
     'shelves_permissions_active' => '本棚の権限は有効です',
     'shelves_permissions_cascade_warning' => '本棚の権限は含まれる本には自動的に継承されません。これは、1つのブックが複数の本棚に存在する可能性があるためです。ただし、以下のオプションを使用すると権限を子ブックにコピーできます。',
+    '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_copy_permissions_to_books' => 'ブックに権限をコピー',
     'shelves_copy_permissions' => '権限をコピー',
     'shelves_copy_permissions_explain' => 'これにより、この本棚の現在の権限設定を本棚に含まれるすべてのブックに適用します。有効にする前に、この本棚の権限への変更が保存されていることを確認してください。',
index 8cd7e925f8e7ef71710d9d45e113be9f6399c5d2..4fb043aa9d06a4ade958301c7b6eea54c77eb7a0 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copy Permissions to Books',
     'shelves_copy_permissions' => '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.',
index ebb9ab19942e20f83f4fb394326942ab615707bf..5b3476543360cb29d4c04106021e8f6472c538d5 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index de554279e5a641614da6269a48ba08314788b022..5d07b8474a46925b9d9ddf8283245b45de43da75 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Kopijuoti leidimus knygoms',
     'shelves_copy_permissions' => 'Kopijuoti leidimus',
     '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.',
index ba5880b8117a669700138c65435bc9de0d83a4d3..095f20ff548db9c4ac38f797ed195b26a40e354e 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Plaukta atļaujas atjauninātas',
     'shelves_permissions_active' => 'Plaukta atļaujas ir aktīvas',
     'shelves_permissions_cascade_warning' => 'Plauktu piekļuves tiesības netiek automātiski piešķirtas tajā esošajām grāmatām. Tas ir tāpēc, ka grāmata var vienlaicīgi atrasties vairākos plauktos. Tomēr piekļuves tiesības var nokopēt uz plauktam pievienotajām grāmatām, izmantojot zemāk atrodamo opciju.',
+    '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_copy_permissions_to_books' => 'Kopēt grāmatplaukta atļaujas uz grāmatām',
     'shelves_copy_permissions' => 'Kopēt atļaujas',
     'shelves_copy_permissions_explain' => 'Pašreizējās plaukta piekļuves tiesības tiks piemērotas visām tajā esošajām grāmatām. Pirms ieslēgšanas pārliecinieties, ka visas izmaiņas plaukta piekļuves tiesības ir saglabātas.',
index 7c62a7b44e8b3fd168e9d470c7dd022537b5c0c7..4906411dc2daf11b951d1423ac796a3bb505ef46 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Oppdaterte hyllerettigheter',
     'shelves_permissions_active' => 'Aktiverte hyllerettigheter',
     'shelves_permissions_cascade_warning' => 'Rettigheter på en hylle blir ikke automatisk arvet av bøker på hylla. Dette er fordi en bok kan finnes på flere hyller samtidig. Rettigheter kan likevel kopieres til bøker på hylla ved å bruke alternativene under.',
+    '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_copy_permissions_to_books' => 'Kopier tilganger til bøkene på hylla',
     'shelves_copy_permissions' => 'Kopier tilganger',
     'shelves_copy_permissions_explain' => 'Dette vil kopiere rettighetene på denne hylla til alle bøkene som er plassert på den. Før du starter kopieringen bør du sjekke at rettighetene på hylla er lagret først.',
index 1b0680b7ab63bead110211edbe9dab82550ff467..2eedd43d27cd8c454288fea1550394a5af2d87de 100644 (file)
@@ -15,7 +15,7 @@ return [
     'page_restore'                => 'herstelde pagina',
     'page_restore_notification'   => 'Pagina succesvol hersteld',
     'page_move'                   => 'verplaatste pagina',
-    'page_move_notification'      => 'Page successfully moved',
+    'page_move_notification'      => 'Pagina met succes verplaatst',
 
     // Chapters
     'chapter_create'              => 'maakte hoofdstuk',
@@ -25,7 +25,7 @@ return [
     'chapter_delete'              => 'verwijderde hoofdstuk',
     'chapter_delete_notification' => 'Hoofdstuk succesvol verwijderd',
     'chapter_move'                => 'verplaatste hoofdstuk',
-    'chapter_move_notification' => 'Chapter successfully moved',
+    'chapter_move_notification' => 'Hoofdstuk met succes verplaatst',
 
     // Books
     'book_create'                 => 'maakte boek',
@@ -50,28 +50,28 @@ return [
     'bookshelf_delete_notification'    => 'Boekenplank is succesvol verwijderd',
 
     // Revisions
-    'revision_restore' => 'restored revision',
-    'revision_delete' => 'deleted revision',
-    'revision_delete_notification' => 'Revision successfully deleted',
+    'revision_restore' => 'herstelde revisie',
+    'revision_delete' => 'verwijderde revisie',
+    'revision_delete_notification' => 'Revisie met succes verwijderd',
 
     // Favourites
     'favourite_add_notification' => '":name" is toegevoegd aan je favorieten',
     'favourite_remove_notification' => '":name" is verwijderd uit je favorieten',
 
     // Auth
-    'auth_login' => 'logged in',
-    'auth_register' => 'registered as new user',
-    'auth_password_reset_request' => 'requested user password reset',
-    'auth_password_reset_update' => 'reset user password',
-    'mfa_setup_method' => 'configured MFA method',
+    'auth_login' => 'heeft ingelogd',
+    'auth_register' => 'geregistreerd als nieuwe gebruiker',
+    'auth_password_reset_request' => 'heeft een nieuw gebruikerswachtwoord aangevraagd',
+    'auth_password_reset_update' => 'heeft zijn gebruikerswachtwoord opnieuw ingesteld',
+    'mfa_setup_method' => 'heeft zijn meervoudige verificatie methode ingesteld',
     'mfa_setup_method_notification' => 'Meervoudige verificatie methode is succesvol geconfigureerd',
-    'mfa_remove_method' => 'removed MFA method',
+    'mfa_remove_method' => 'heeft zijn meervoudige verificatie methode verwijderd',
     'mfa_remove_method_notification' => 'Meervoudige verificatie methode is succesvol verwijderd',
 
     // Settings
-    'settings_update' => 'updated settings',
-    'settings_update_notification' => 'Settings successfully updated',
-    'maintenance_action_run' => 'ran maintenance action',
+    'settings_update' => 'heeft de instellingen bijgewerkt',
+    'settings_update_notification' => 'Instellingen met succes bijgewerkt',
+    'maintenance_action_run' => 'heeft onderhoud uitgevoerd',
 
     // Webhooks
     'webhook_create' => 'webhook aangemaakt',
@@ -82,33 +82,33 @@ return [
     'webhook_delete_notification' => 'Webhook succesvol verwijderd',
 
     // Users
-    'user_create' => 'created user',
-    'user_create_notification' => 'User successfully created',
-    'user_update' => 'updated user',
+    'user_create' => 'heeft gebruiker aangemaakt',
+    'user_create_notification' => 'Gebruiker met succes aangemaakt',
+    'user_update' => 'heeft gebruiker bijgewerkt',
     'user_update_notification' => 'Gebruiker succesvol bijgewerkt',
-    'user_delete' => 'deleted user',
+    'user_delete' => 'heeft gebruiker verwijderd',
     'user_delete_notification' => 'Gebruiker succesvol verwijderd',
 
     // API Tokens
-    'api_token_create' => 'created api token',
-    'api_token_create_notification' => 'API token successfully created',
-    'api_token_update' => 'updated api token',
-    'api_token_update_notification' => 'API token successfully updated',
-    'api_token_delete' => 'deleted api token',
-    'api_token_delete_notification' => 'API token successfully deleted',
+    'api_token_create' => 'heeft API token aangemaakt',
+    'api_token_create_notification' => 'API token met succes aangemaakt',
+    'api_token_update' => 'heeft API token bijgewerkt',
+    'api_token_update_notification' => 'API token met succes bijgewerkt',
+    'api_token_delete' => 'heeft API token verwijderd',
+    'api_token_delete_notification' => 'API token met succes verwijderd',
 
     // Roles
-    'role_create' => 'created role',
+    'role_create' => 'heeft rol aangemaakt',
     'role_create_notification' => 'Rol succesvol aangemaakt',
-    'role_update' => 'updated role',
+    'role_update' => 'heeft rol bijgewerkt',
     'role_update_notification' => 'Rol succesvol bijgewerkt',
-    'role_delete' => 'deleted role',
+    'role_delete' => 'heeft rol verwijderd',
     'role_delete_notification' => 'Rol succesvol verwijderd',
 
     // Recycle Bin
-    'recycle_bin_empty' => 'emptied recycle bin',
-    'recycle_bin_restore' => 'restored from recycle bin',
-    'recycle_bin_destroy' => 'removed from recycle bin',
+    'recycle_bin_empty' => 'leegde prullenbak',
+    'recycle_bin_restore' => 'is van prullenbak hersteld',
+    'recycle_bin_destroy' => 'is van prullenbak verwijderd',
 
     // Other
     'commented_on'                => 'reageerde op',
index e6b22e0cbc7556433c9d921725a54e9bdeb86846..8f43f363aa06d32e680616e570b4ea1c141060fc 100644 (file)
@@ -6,7 +6,7 @@ return [
 
     // Buttons
     'cancel' => 'Annuleer',
-    'close' => 'Close',
+    'close' => 'Sluit',
     'confirm' => 'Bevestig',
     'back' => 'Terug',
     'save' => 'Opslaan',
index e7a5b96eb4a86d031136ed9ca67c118f7c8623af..168d106b59796d9dae5680df98ce22c85fe7fbd9 100644 (file)
@@ -6,8 +6,8 @@ return [
 
     // Image Manager
     'image_select' => 'Selecteer Afbeelding',
-    'image_list' => 'Image List',
-    'image_details' => 'Image Details',
+    'image_list' => 'Afbeeldingslijst',
+    'image_details' => 'Afbeelding details',
     'image_upload' => 'Upload afbeelding',
     'image_intro' => 'Hier kan je eerder geüploade afbeeldingen selecteren en beheren.',
     'image_intro_upload' => 'Sleep een afbeeldingsbestand naar dit venster of gebruik de "Upload afbeelding"-knop om een afbeelding te uploaden.',
@@ -17,9 +17,9 @@ return [
     'image_page_title' => 'Bekijk afbeeldingen geüpload naar deze pagina',
     'image_search_hint' => 'Zoek op afbeeldingsnaam',
     'image_uploaded' => 'Geüpload op :uploadedDate',
-    'image_uploaded_by' => 'Uploaded by :userName',
-    'image_uploaded_to' => 'Uploaded to :pageLink',
-    'image_updated' => 'Updated :updateDate',
+    'image_uploaded_by' => 'Geüpload door :userName',
+    'image_uploaded_to' => 'Geüpload naar :pageLink',
+    'image_updated' => ':updateDate bijgewerkt',
     'image_load_more' => 'Laad meer',
     'image_image_name' => 'Afbeeldingsnaam',
     'image_delete_used' => 'Deze afbeelding is op onderstaande pagina\'s in gebruik.',
@@ -32,8 +32,8 @@ return [
     'image_upload_success' => 'Afbeelding succesvol geüpload',
     'image_update_success' => 'Afbeeldingsdetails succesvol bijgewerkt',
     'image_delete_success' => 'Afbeelding succesvol verwijderd',
-    'image_replace' => 'Replace Image',
-    'image_replace_success' => 'Image file successfully updated',
+    'image_replace' => 'Vervang Afbeelding',
+    'image_replace_success' => 'Afbeelding succesvol bijgewerkt',
 
     // Code Editor
     'code_editor' => 'Bewerk Code',
index 3c71032b79de55593b5c69a27d2fdfd016453135..7c35d838f8c41c686c5c3b3c9dfbd77be2801758 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Boekenplank Machtigingen Bijgewerkt',
     'shelves_permissions_active' => 'Machtigingen op Boekenplank Actief',
     'shelves_permissions_cascade_warning' => 'De ingestelde machtigingen op deze boekenplank worden niet automatisch toegepast op de boeken van deze boekenplank. Dit is omdat een boek toegekend kan worden op meerdere boekenplanken. De machtigingen van deze boekenplank kunnen echter wel gekopieerd worden naar de boeken van deze boekenplank via de optie hieronder.',
+    '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_copy_permissions_to_books' => 'Kopieer Machtigingen naar Boeken',
     'shelves_copy_permissions' => 'Kopieer Machtigingen',
     'shelves_copy_permissions_explain' => 'Met deze actie worden de machtigingen van deze boekenplank gekopieërd naar alle boeken van deze boekenplank. Voor je deze actie uitvoert, moet je ervoor zorgen dat alle wijzigingen in de machtigingen van deze boekenplank zijn opgeslagen.',
@@ -213,7 +214,7 @@ return [
     'pages_editing_page' => 'Concept bewerken',
     'pages_edit_draft_save_at' => 'Concept opgeslagen op ',
     'pages_edit_delete_draft' => 'Concept verwijderen',
-    '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' => 'Weet je zeker dat je de wijzigingen in je concept wilt verwijderen? Al je wijzigingen sinds de laatste succesvolle bewaring gaan verloren en de editor wordt bijgewerkt met de meest recente niet-concept versie van de pagina.',
     'pages_edit_discard_draft' => 'Concept verwijderen',
     'pages_edit_switch_to_markdown' => 'Verander naar Markdown Bewerker',
     'pages_edit_switch_to_markdown_clean' => '(Schoongemaakte Inhoud)',
@@ -265,13 +266,13 @@ return [
     'pages_revisions_restore' => 'Herstellen',
     'pages_revisions_none' => 'Deze pagina heeft geen revisies',
     'pages_copy_link' => 'Link kopiëren',
-    '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' => 'Spring naar sectie in editor',
+    'pages_pointer_enter_mode' => 'Open selectiemodus per onderdeel',
+    'pages_pointer_label' => 'Pagina Onderdeel Opties',
+    'pages_pointer_permalink' => 'Pagina Onderdeel Permalink',
+    'pages_pointer_include_tag' => 'Pagina Onderdeel Tag Toevoeging',
+    'pages_pointer_toggle_link' => 'Permalink modus, Druk om Tag Toevoeging te tonen',
+    'pages_pointer_toggle_include' => 'Tag Toevoeging modus, Druk om Permalink te tonen',
     'pages_permissions_active' => 'Pagina Machtigingen Actief',
     'pages_initial_revision' => 'Eerste publicatie',
     'pages_references_update_revision' => 'Automatische systeemupdate van interne links',
@@ -286,8 +287,8 @@ return [
         'time_b' => 'in de laatste :minCount minuten',
         'message' => ':start :time. Let op om elkaars updates niet te overschrijven!',
     ],
-    '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' => 'Concept verwporpen! De editor is bijgewerkt met de huidige inhoud van de pagina',
+    'pages_draft_deleted' => 'Concept verwijderd! De editor is bijgewerkt met de huidige inhoud van de pagina',
     'pages_specific' => 'Specifieke pagina',
     'pages_is_template' => 'Paginasjabloon',
 
@@ -365,13 +366,13 @@ return [
     'comment_new' => 'Nieuwe reactie',
     'comment_created' => 'reactie gegeven :createDiff',
     'comment_updated' => 'Updatet :updateDiff door :username',
-    'comment_updated_indicator' => 'Updated',
+    'comment_updated_indicator' => 'Bijgewerkt',
     'comment_deleted_success' => 'Reactie verwijderd',
     'comment_created_success' => 'Reactie toegevoegd',
     'comment_updated_success' => 'Reactie bijgewerkt',
     'comment_delete_confirm' => 'Weet je zeker dat je deze reactie wilt verwijderen?',
     'comment_in_reply_to' => 'Als antwoord op :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' => 'Hier zijn de opmerkingen die zijn achtergelaten op deze pagina. Opmerkingen kunnen worden toegevoegd en beheerd wanneer u de opgeslagen pagina bekijkt.',
 
     // Revision
     'revision_delete_confirm' => 'Weet u zeker dat u deze revisie wilt verwijderen?',
index 9290734d55955b635f2ffe5d465b7ecc468930f8..5e8fa569293a96ed037ae9f364f1aa151d900a54 100644 (file)
@@ -49,7 +49,7 @@ return [
     // Drawing & Images
     'image_upload_error' => 'Er is een fout opgetreden bij het uploaden van de afbeelding',
     'image_upload_type_error' => 'Het geüploade afbeeldingstype is ongeldig',
-    'image_upload_replace_type' => 'Image file replacements must be of the same type',
+    'image_upload_replace_type' => 'Afbeeldingen moeten van hetzelfde type zijn',
     'drawing_data_not_found' => 'De gegevens van de tekening konden niet worden geladen. Het tekenbestand bestaat misschien niet meer of u hebt geen machtiging om het te openen.',
 
     // Attachments
@@ -58,7 +58,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Kon het concept niet opslaan. Zorg ervoor dat je een werkende internetverbinding hebt',
-    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
+    'page_draft_delete_fail' => 'Het is niet gelukt om het concept van de pagina te verwijderen en de opgeslagen inhoud van de huidige pagina op te halen',
     'page_custom_home_deletion' => 'Een pagina die als startpagina is ingesteld, kan niet verwijderd worden',
 
     // Entities
index ac89150b62a09ac18e8878c2bfd81a0029d47c0a..557e847c2551de4e2e82c02bd91e139d04dc2748 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Uprawnienia półki zostały zaktualizowane',
     'shelves_permissions_active' => 'Uprawnienia półki są aktywne',
     'shelves_permissions_cascade_warning' => 'Uprawnienia na półkach nie są automatycznie nakładane na zawartych w nich książkach. Dzieje się tak dlatego, że książka może istnieć na wielu półkach. Uprawnienia można jednak skopiować do książek podrzędnych, korzystając z opcji znajdującej się poniżej.',
+    '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_copy_permissions_to_books' => 'Skopiuj uprawnienia do książek',
     'shelves_copy_permissions' => 'Skopiuj uprawnienia',
     'shelves_copy_permissions_explain' => 'To spowoduje zastosowanie obecnych ustawień uprawnień tej półki na wszystkich książkach w niej zawartych. Przed aktywacją upewnij się, że wszelkie zmiany w uprawnieniach tej półki zostały zapisane.',
index 90dc383fcc54074849de8d37d99dd17b8a89fe4f..05e5b5fef412cbd6b761d32f7ee5dc7d0c031ce9 100644 (file)
@@ -15,7 +15,7 @@ return [
     'page_restore'                => 'página restaurada',
     'page_restore_notification'   => 'Página restaurada com sucesso',
     'page_move'                   => 'página movida',
-    'page_move_notification'      => 'Page successfully moved',
+    'page_move_notification'      => 'Página movida com sucesso',
 
     // Chapters
     'chapter_create'              => 'capítulo criado',
@@ -25,7 +25,7 @@ return [
     'chapter_delete'              => 'capítulo excluído',
     'chapter_delete_notification' => 'Capítulo excluído com sucesso',
     'chapter_move'                => 'capítulo movido',
-    'chapter_move_notification' => 'Chapter successfully moved',
+    'chapter_move_notification' => 'Capítulo movido com sucesso',
 
     // Books
     'book_create'                 => 'livro criado',
@@ -50,28 +50,28 @@ return [
     'bookshelf_delete_notification'    => 'Estante eliminada com sucesso',
 
     // Revisions
-    'revision_restore' => 'restored revision',
-    'revision_delete' => 'deleted revision',
-    'revision_delete_notification' => 'Revision successfully deleted',
+    'revision_restore' => 'revisão restaurada',
+    'revision_delete' => 'revisão eliminada',
+    'revision_delete_notification' => 'Revisão eliminada com sucesso',
 
     // Favourites
     'favourite_add_notification' => '":name" foi adicionado aos seus favoritos',
     'favourite_remove_notification' => '":name" foi removido dos seus favoritos',
 
     // Auth
-    'auth_login' => 'logged in',
-    'auth_register' => 'registered as new user',
-    'auth_password_reset_request' => 'requested user password reset',
-    'auth_password_reset_update' => 'reset user password',
-    'mfa_setup_method' => 'configured MFA method',
+    'auth_login' => 'sessão iniciada',
+    'auth_register' => 'registado como novo utilizador',
+    'auth_password_reset_request' => 'pedido a redefinição da palavra-passe',
+    'auth_password_reset_update' => 'redifinir palavra-passe do utilizador',
+    'mfa_setup_method' => 'configurar método de duplo fator',
     'mfa_setup_method_notification' => 'Método de autenticação por múltiplos-fatores configurado com sucesso',
-    'mfa_remove_method' => 'removed MFA method',
+    'mfa_remove_method' => 'método de duplo fator removido',
     'mfa_remove_method_notification' => 'Método de autenticação por múltiplos-fatores removido com sucesso',
 
     // Settings
-    'settings_update' => 'updated settings',
-    'settings_update_notification' => 'Settings successfully updated',
-    'maintenance_action_run' => 'ran maintenance action',
+    'settings_update' => 'configurações atualizadas',
+    'settings_update_notification' => 'Configurações atualizadas com sucesso',
+    'maintenance_action_run' => 'ação de manutenção executada',
 
     // Webhooks
     'webhook_create' => 'webhook criado',
@@ -82,33 +82,33 @@ return [
     'webhook_delete_notification' => 'Webhook criado com sucesso',
 
     // Users
-    'user_create' => 'created user',
-    'user_create_notification' => 'User successfully created',
-    'user_update' => 'updated user',
+    'user_create' => 'utilizador criado',
+    'user_create_notification' => 'Utilizador criado com sucesso',
+    'user_update' => 'utilizador atualizado',
     'user_update_notification' => 'Utilizador atualizado com sucesso',
-    'user_delete' => 'deleted user',
+    'user_delete' => 'utilizador eliminado',
     'user_delete_notification' => 'Utilizador removido com sucesso',
 
     // API Tokens
-    'api_token_create' => 'created api token',
-    'api_token_create_notification' => 'API token successfully created',
-    'api_token_update' => 'updated api token',
-    'api_token_update_notification' => 'API token successfully updated',
-    'api_token_delete' => 'deleted api token',
-    'api_token_delete_notification' => 'API token successfully deleted',
+    'api_token_create' => 'api token criado',
+    'api_token_create_notification' => 'API token criado com sucesso',
+    'api_token_update' => 'api token atualizado',
+    'api_token_update_notification' => 'API token atualizado com sucesso',
+    'api_token_delete' => 'api token eliminado',
+    'api_token_delete_notification' => 'API token atualizado com sucesso',
 
     // Roles
-    'role_create' => 'created role',
+    'role_create' => 'cargo criado',
     'role_create_notification' => 'Cargo criado com sucesso',
-    'role_update' => 'updated role',
+    'role_update' => 'cargo atualizado',
     'role_update_notification' => 'Cargo atualizado com sucesso',
-    'role_delete' => 'deleted role',
+    'role_delete' => 'cargo eliminado',
     'role_delete_notification' => 'Cargo excluído com sucesso',
 
     // Recycle Bin
-    'recycle_bin_empty' => 'emptied recycle bin',
-    'recycle_bin_restore' => 'restored from recycle bin',
-    'recycle_bin_destroy' => 'removed from recycle bin',
+    'recycle_bin_empty' => 'reciclagem vazia',
+    'recycle_bin_restore' => 'restaurado da reciclagem',
+    'recycle_bin_destroy' => 'removido da reciclagem',
 
     // Other
     'commented_on'                => 'comentado a',
index fe890b3d5c46f2c85273222a9b8aad179c32bd9e..30e4ec35ecbafaf47ed5aeac5ec3071f6e2bf6f7 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permissões da Estante Atualizada',
     'shelves_permissions_active' => 'Permissões da Estante Ativas',
     'shelves_permissions_cascade_warning' => 'As permissões nas estantes não são passadas automaticamente em efeito dominó para os livros contidos. Isto acontece porque um livro pode existir em várias estantes. As permissões podem, no entanto, ser copiadas para livros filhos usando a opção abaixo.',
+    '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_copy_permissions_to_books' => 'Copiar Permissões para Livros',
     'shelves_copy_permissions' => 'Copiar Permissões',
     'shelves_copy_permissions_explain' => 'Isto aplicará as configurações de permissões atuais desta estante a todos os livros nela contidos. Antes de ativar, assegure-se de que quaisquer alterações nas permissões desta estante foram guardadas.',
@@ -213,7 +214,7 @@ return [
     'pages_editing_page' => 'A Editar Página',
     'pages_edit_draft_save_at' => 'Rascunho guardado em ',
     'pages_edit_delete_draft' => 'Eliminar Rascunho',
-    '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' => 'Tem a certeza que deseja eliminar o rascunha da página? Todas as alterações, desde o último carregamento, será perdido e o editor será atualizado com o último estado guardo da página.',
     'pages_edit_discard_draft' => 'Descartar Rascunho',
     'pages_edit_switch_to_markdown' => 'Alternar para o editor Markdown',
     'pages_edit_switch_to_markdown_clean' => '(Conteúdo Limitado)',
@@ -286,8 +287,8 @@ return [
         'time_b' => 'nos últimos :minCount minutos',
         'message' => ':start :time. Tenha cuidado para não sobrescrever atualizações de outras pessoas!',
     ],
-    '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' => 'Rascunho eliminado! O editor foi atualizado com o conteúdo atual da página',
+    'pages_draft_deleted' => 'Rascunho eliminado! O editor foi atualizado com o conteúdo atual da página',
     'pages_specific' => 'Página Específica',
     'pages_is_template' => 'Modelo de Página',
 
@@ -365,13 +366,13 @@ return [
     'comment_new' => 'Comentário Novo',
     'comment_created' => 'comentado :createDiff',
     'comment_updated' => 'A editar :updateDiff por :username',
-    'comment_updated_indicator' => 'Updated',
+    'comment_updated_indicator' => 'Atualizado',
     'comment_deleted_success' => 'Comentário removido',
     'comment_created_success' => 'Comentário adicionado',
     'comment_updated_success' => 'Comentário editado',
     'comment_delete_confirm' => 'Tem a certeza de que deseja eliminar este comentário?',
     'comment_in_reply_to' => 'Em resposta à :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' => 'Aqui estão os comentários que foram deixados nesta página. Comentários podem ser adicionados e geridos ao visualizar a página guardada.',
 
     // Revision
     'revision_delete_confirm' => 'Tem a certeza de que deseja eliminar esta revisão?',
index 0f6569fb04023b5647d3d1e1bc8347947306f35c..f6ecceb0730e213cc0332ca27ea1903256798b3d 100644 (file)
@@ -58,7 +58,7 @@ return [
 
     // Pages
     'page_draft_autosave_fail' => 'Falha ao tentar guardar o rascunho. Certifique-se que a conexão de Internet está funcional antes de tentar guardar esta página',
-    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
+    'page_draft_delete_fail' => 'Eliminação do rascunho de página e importação do conteúdo salvo da página falhou',
     'page_custom_home_deletion' => 'Não é possível eliminar uma página que está definida como página inicial',
 
     // Entities
index 4780bc08b31a56133480bca512ec4466bc60b121..5d3c47c19b149ae5af63d36acf751ab7b5503f92 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permissões de prateleira atualizadas',
     'shelves_permissions_active' => 'Permissões de prateleira ativas',
     'shelves_permissions_cascade_warning' => 'As permissões nas prateleiras não são automaticamente em cascata para os livros contidos. Isso ocorre porque um livro pode existir em várias prateleiras. No entanto, as permissões podem ser copiadas para livros filhos usando a opção encontrada abaixo.',
+    '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_copy_permissions_to_books' => 'Copiar Permissões para Livros',
     'shelves_copy_permissions' => 'Copiar Permissões',
     'shelves_copy_permissions_explain' => 'Isso aplicará as configurações de permissão atuais desta estante a todos os livros contidos nela. Antes de ativar, verifique se todas as alterações nas permissões desta prateleira foram salvas.',
index e2fa874bf03858a70c932f657a6e451b5148a656..cce7cb290bde63bcb34c9156bd96a8d2a8c82b50 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Permisiunile raftului au fost actualizate',
     '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_copy_permissions_to_books' => 'Copiază permisiunile către cărți',
     'shelves_copy_permissions' => 'Copiază permisiunile',
     '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.',
index 442716710b6b5d8680174c3b988aad44e72d253e..bca3788146e23761408fb531d39bdc1593737c9b 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Доступы к полке обновлены',
     'shelves_permissions_active' => 'Действующие разрешения полки',
     'shelves_permissions_cascade_warning' => 'Разрешения на полки не наследуются автоматически содержащимся в них книгам. Это происходит потому, что книга может находиться на нескольких полках. Однако разрешения могут быть установлены для книг полки с помощью опции, приведенной ниже.',
+    '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_copy_permissions_to_books' => 'Наследовать доступы книгам',
     'shelves_copy_permissions' => 'Копировать доступы',
     'shelves_copy_permissions_explain' => 'Это применит текущие настройки разрешений для этой полки ко всем книгам, содержащимся в ней. Перед активацией убедитесь, что все изменения разрешений этой полки были сохранены.',
index 5694f2baa3368e0a491d6e02c2d169cf5d4e66f2..b03b9fa831ca41f4a894b5fcf6599dd9fb87e87d 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Povolenia police aktualizované',
     'shelves_permissions_active' => 'Povolenia police aktívne',
     'shelves_permissions_cascade_warning' => 'Povolenia na poličkách sa automaticky nepriraďujú k obsiahnutým knihám. Je to preto, že kniha môže existovať na viacerých poličkách. Povolenia však možno skopírovať do kníh pomocou možnosti uvedenej nižšie.',
+    '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_copy_permissions_to_books' => 'Kopírovať oprávnenia pre knihy',
     'shelves_copy_permissions' => 'Kopírovať oprávnenia',
     'shelves_copy_permissions_explain' => 'Týmto sa použijú aktuálne nastavenia povolení tejto police na všetky knihy, ktoré obsahuje. Pred aktiváciou sa uistite, že všetky zmeny povolení tejto police boli uložené.',
index 128ecdd110a5b6aa0816c06ca1b5760007cb069a..958b65426093a10ffca0e6f990e5fd77af11bfd1 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Kopiraj dovoljenja na knjige',
     'shelves_copy_permissions' => 'Dovoljenja kopiranja',
     '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.',
index 5559534d5e199324543857e348267fce525257b1..c2f968adf56aaad893835b8cf6068dba759bcdec 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index 773e9e37597627612f85041574dd69b4618a74c2..0eff1dea659d0236d76c2a812ed140cefee1a55d 100644 (file)
@@ -15,7 +15,7 @@ return [
     'page_restore'                => 'sayfayı eski haline getirdi',
     'page_restore_notification'   => 'Sayfa Başarıyla Eski Haline Getirildi',
     'page_move'                   => 'sayfa taşındı',
-    'page_move_notification'      => 'Page successfully moved',
+    'page_move_notification'      => 'Sayfa başarıyla taşındı',
 
     // Chapters
     'chapter_create'              => 'bölüm oluşturdu',
@@ -25,13 +25,13 @@ return [
     'chapter_delete'              => 'bölümü sildi',
     'chapter_delete_notification' => 'Bölüm başarıyla silindi',
     'chapter_move'                => 'bölümü taşıdı',
-    'chapter_move_notification' => 'Chapter successfully moved',
+    'chapter_move_notification' => 'Bölüm başarıyla taşındı',
 
     // Books
     'book_create'                 => 'kitap oluşturdu',
     'book_create_notification'    => 'Kitap başarıyla oluşturuldu',
     'book_create_from_chapter'              => 'converted chapter to book',
-    'book_create_from_chapter_notification' => 'Chapter successfully converted to a book',
+    'book_create_from_chapter_notification' => 'Bölüm başarıyla kitaba dönüştürüldü',
     'book_update'                 => 'kitabı güncelledi',
     'book_update_notification'    => 'Kitap başarıyla güncellendi',
     'book_delete'                 => 'kitabı sildi',
@@ -41,26 +41,26 @@ return [
 
     // Bookshelves
     'bookshelf_create'            => 'kitaplık oluşturuldu',
-    'bookshelf_create_notification'    => 'Shelf successfully created',
+    'bookshelf_create_notification'    => 'Kitaplık başarıyla oluşturuldu',
     'bookshelf_create_from_book'    => 'converted book to shelf',
-    'bookshelf_create_from_book_notification'    => 'Book successfully converted to a shelf',
+    'bookshelf_create_from_book_notification'    => 'Kitap başarıyla kitaplığa dönüştürüldü',
     'bookshelf_update'                 => 'updated shelf',
-    'bookshelf_update_notification'    => 'Shelf successfully updated',
+    'bookshelf_update_notification'    => 'Kitaplık başarıyla güncellendi',
     'bookshelf_delete'                 => 'deleted shelf',
-    'bookshelf_delete_notification'    => 'Shelf successfully deleted',
+    'bookshelf_delete_notification'    => 'Kitaplık başarıyla silindi',
 
     // Revisions
     'revision_restore' => 'restored revision',
     'revision_delete' => 'deleted revision',
-    'revision_delete_notification' => 'Revision successfully deleted',
+    'revision_delete_notification' => 'Değişiklik başarıyla silindi',
 
     // Favourites
     'favourite_add_notification' => '":name" favorilerinize eklendi',
     'favourite_remove_notification' => '":name" favorilerinizden çıkarıldı',
 
     // Auth
-    'auth_login' => 'logged in',
-    'auth_register' => 'registered as new user',
+    '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_update' => 'reset user password',
     'mfa_setup_method' => 'configured MFA method',
@@ -70,7 +70,7 @@ return [
 
     // Settings
     'settings_update' => 'updated settings',
-    'settings_update_notification' => 'Settings successfully updated',
+    'settings_update_notification' => 'Ayarlar başarıyla güncellendi',
     'maintenance_action_run' => 'ran maintenance action',
 
     // Webhooks
@@ -83,7 +83,7 @@ return [
 
     // Users
     'user_create' => 'created user',
-    'user_create_notification' => 'User successfully created',
+    'user_create_notification' => 'Kullanıcı başarıyla oluşturuldu',
     'user_update' => 'updated user',
     'user_update_notification' => 'Kullanıcı başarıyla güncellendi',
     'user_delete' => 'deleted user',
@@ -91,19 +91,19 @@ return [
 
     // API Tokens
     'api_token_create' => 'created api token',
-    'api_token_create_notification' => 'API token successfully created',
+    'api_token_create_notification' => 'API anahtarı başarıyla oluşturuldu',
     'api_token_update' => 'updated api token',
-    'api_token_update_notification' => 'API token successfully updated',
+    'api_token_update_notification' => 'API anahtarı başarıyla güncellendi',
     'api_token_delete' => 'deleted api token',
-    'api_token_delete_notification' => 'API token successfully deleted',
+    'api_token_delete_notification' => 'API anahtarı başarıyla silindi',
 
     // Roles
     'role_create' => 'created role',
-    'role_create_notification' => 'Role successfully created',
+    'role_create_notification' => 'Rol başarıyla oluşturuldu',
     'role_update' => 'updated role',
-    'role_update_notification' => 'Role successfully updated',
+    'role_update_notification' => 'Rol başarıyla güncellendi',
     'role_delete' => 'deleted role',
-    'role_delete_notification' => 'Role successfully deleted',
+    'role_delete_notification' => 'Rol başarıyla silindi',
 
     // Recycle Bin
     'recycle_bin_empty' => 'emptied recycle bin',
index 0d04a4ab858a84b46045793abe48678c10d000da..50f4f60a955d13ae5b07e876a40dba588643d828 100644 (file)
@@ -39,9 +39,9 @@ return [
     'register_success' => 'Kaydolduğunuz için teşekkürler! Artık kayıtlı bir kullanıcı olarak giriş yaptınız.',
 
     // 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_start_link' => 'Proceed with authentication',
+    'auto_init_starting' => 'Oturum açılmaya çalışılıyor',
+    'auto_init_starting_desc' => 'Oturum açma işlemini başlatmak için kimlik doğrulama sisteminizle iletişime geçiyoruz. Eğer 5 saniye sonra herhangi bir ilerleme olmazsa aşağıdaki bağlantıya tıklamayı deneyebilirsiniz.',
+    'auto_init_start_link' => 'Kimlik doğrulama ile devam edin',
 
     // Password Reset
     'reset_password' => 'Şifreyi Sıfırla',
@@ -61,8 +61,8 @@ return [
     'email_confirm_send_error' => 'E-posta adresinin doğrulanması gerekiyor fakat sistem, doğrulama bağlantısını göndermeyi başaramadı. E-posta adresinin doğru bir şekilde ayarlığından emin olmak için yöneticiyle iletişime geçin.',
     'email_confirm_success' => 'Email hesabınız onaylandı. Email adresinizi kullanarak giriş yapabilirsiniz.',
     'email_confirm_resent' => 'Doğrulama e-postası tekrar gönderildi, lütfen gelen kutunuzu kontrol ediniz.',
-    'email_confirm_thanks' => 'Thanks for confirming!',
-    'email_confirm_thanks_desc' => 'Please wait a moment while your confirmation is handled. If you are not redirected after 3 seconds press the "Continue" link below to proceed.',
+    'email_confirm_thanks' => 'Onayladığınız için teşekkürler!',
+    'email_confirm_thanks_desc' => 'Lütfen onayınız işlenirken bir dakika bekleyin. Eğer 3 saniye sonra yönlendirilmediyseniz; devam etmek için aşağıdaki "Devam" linkine basınız.',
 
     'email_not_confirmed' => 'E-posta Adresi Doğrulanmadı',
     'email_not_confirmed_text' => 'E-posta adresiniz henüz doğrulanmadı.',
index 353ee753000c25ff6ae6000d68fa95319f01ab60..c08c4a193b358a42e2fb1b4d89741ce75cc51baf 100644 (file)
@@ -6,13 +6,13 @@ return [
 
     // Buttons
     'cancel' => 'İptal',
-    'close' => 'Close',
+    'close' => 'Kapat',
     'confirm' => 'Onayla',
     'back' => 'Geri',
     'save' => 'Kaydet',
     'continue' => 'Devam',
     'select' => 'Seç',
-    'toggle_all' => 'Hepsini Aç/Kapat',
+    'toggle_all' => 'Tümünü Aç/Kapat',
     'more' => 'Daha Fazla',
 
     // Form Labels
@@ -26,7 +26,7 @@ return [
     'actions' => 'İşlemler',
     'view' => 'Görüntüle',
     'view_all' => 'Hepsini Göster',
-    'new' => 'New',
+    'new' => 'Yeni',
     'create' => 'Oluştur',
     'update' => 'Güncelle',
     'edit' => 'Düzenle',
@@ -47,10 +47,10 @@ return [
     'unfavourite' => 'Favorilerden çıkar',
     'next' => 'Sonraki',
     'previous' => 'Önceki',
-    'filter_active' => 'Aktif filtre:',
+    'filter_active' => 'Aktif Filtre:',
     'filter_clear' => 'Filtreyi Kaldır',
-    'download' => 'Download',
-    'open_in_tab' => 'Open in Tab',
+    'download' => 'İndir',
+    'open_in_tab' => 'Sekmede aç',
 
     // Sort Options
     'sort_options' => 'Sıralama Seçenekleri',
@@ -60,11 +60,11 @@ return [
     'sort_name' => 'İsim',
     'sort_default' => 'Varsayılan',
     'sort_created_at' => 'Oluşturulma Tarihi',
-    'sort_updated_at' => 'Güncelleme Tarihi',
+    'sort_updated_at' => 'Güncellenme Tarihi',
 
     // Misc
     'deleted_user' => 'Silinmiş Kullanıcı',
-    'no_activity' => 'Gösterilecek eylem bulunamadı',
+    'no_activity' => 'Gösterilecek aktivite yok',
     'no_items' => 'Herhangi bir öge bulunamadı',
     'back_to_top' => 'Başa dön',
     'skip_to_main_content' => 'Ana içeriğe geç',
@@ -76,29 +76,29 @@ return [
     'default' => 'Varsayılan',
     'breadcrumb' => 'Gezinti Menüsü',
     'status' => 'Durum',
-    'status_active' => 'Etkin',
-    'status_inactive' => 'Devre dışı',
+    'status_active' => 'Aktif',
+    'status_inactive' => 'Aktif değil',
     'never' => 'Hiçbir zaman',
     'none' => 'Hiçbiri',
 
     // Header
-    'homepage' => 'Homepage',
+    'homepage' => 'Ana sayfa',
     'header_menu_expand' => 'Başlık Menüsünü Genişlet',
     'profile_menu' => 'Profil Menüsü',
     'view_profile' => 'Profili Görüntüle',
     'edit_profile' => 'Profili Düzenle',
-    'dark_mode' => 'Gece Modu',
+    'dark_mode' => 'Gece Teması',
     'light_mode' => 'Aydınlık Modu',
-    'global_search' => 'Global Search',
+    'global_search' => 'Genel Arama',
 
     // Layout tabs
     'tab_info' => 'Bilgi',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Sekme: İkincil Bilgileri Göster',
     'tab_content' => 'İçerik',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Sekme: Birincil Bilgileri Göster',
 
     // Email Content
-    'email_action_help' => '":actionText" butonuna tıklamada sorun yaşıyorsanız, aşağıda bulunan bağlantıyı kopyalayıp tarayıcınıza yapıştırın:',
+    'email_action_help' => 'Eğer ":actionText" butonuna tıklamakta zorluk çekiyorsanız, aşağıda bulunan linki kopyalayıp tarayıcınıza yapıştırabilirsiniz.',
     'email_rights' => 'Tüm hakları saklıdır',
 
     // Footer Link Options
index e459e825e8f8a0907ac246ff1c42b2db34f407a7..6f200e9463b529102c0c6750a71629af6654ae30 100644 (file)
@@ -6,34 +6,34 @@ return [
 
     // Image Manager
     'image_select' => 'Görsel Seç',
-    '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' => 'Görsel Listesi',
+    'image_details' => 'Görsel Detayları',
+    'image_upload' => 'Görsel Yükle',
+    'image_intro' => 'Burada sisteme daha önce yüklenmiş görselleri seçebilir veya yönetebilirsiniz.',
+    'image_intro_upload' => 'Bir resim dosyasını bu pencereye sürükleyerek veya yukarıdaki "Resim Yükle" düğmesini kullanarak yeni bir resim yükleyin.',
     'image_all' => 'Hepsi',
     'image_all_title' => 'Bütün görselleri görüntüle',
     'image_book_title' => 'Bu kitaba ait görselleri görüntüle',
     'image_page_title' => 'Bu sayfaya ait görselleri görüntüle',
     'image_search_hint' => 'Görsel adıyla ara',
     'image_uploaded' => ':uploadedDate tarihinde yüklendi',
-    'image_uploaded_by' => 'Uploaded by :userName',
-    'image_uploaded_to' => 'Uploaded to :pageLink',
-    'image_updated' => 'Updated :updateDate',
+    'image_uploaded_by' => ':userName tarafından yüklendi',
+    'image_uploaded_to' => ':pageLink \'e yüklendi',
+    'image_updated' => ':updateDate \'de güncellendi',
     'image_load_more' => 'Devamını Göster',
     'image_image_name' => 'Görsel Adı',
     'image_delete_used' => 'Bu görsel aşağıda bulunan sayfalarda kullanılmış.',
     'image_delete_confirm_text' => 'Bu resmi silmek istediğinizden emin misiniz?',
     'image_select_image' => 'Görsel Seç',
     'image_dropzone' => 'Görselleri sürükleyin ya da seçin',
-    'image_dropzone_drop' => 'Drop images here to upload',
+    'image_dropzone_drop' => 'Yüklemek için görselleri buraya bırakın',
     'images_deleted' => 'Görseller Silindi',
     'image_preview' => 'Görsel Ön İzlemesi',
     'image_upload_success' => 'Görsel başarıyla yüklendi',
     'image_update_success' => 'Görsel detayları başarıyla güncellendi',
     'image_delete_success' => 'Görsel başarıyla silindi',
-    'image_replace' => 'Replace Image',
-    'image_replace_success' => 'Image file successfully updated',
+    'image_replace' => 'Görseli Değiştir',
+    'image_replace_success' => 'Görsel dosyası başarıyla güncellendi',
 
     // Code Editor
     'code_editor' => 'Kodu Düzenle',
index 503049d004f139aa586350fcd3ac6e6467526902..6bdb315d6e806dab20977ce21d94b70e2407d591 100644 (file)
@@ -66,7 +66,7 @@ return [
     'insert_link_title' => 'Bağlantı Ekle/Düzenle',
     'insert_horizontal_line' => 'Yatay çizgi ekle',
     'insert_code_block' => 'Kod bloğu ekle',
-    'edit_code_block' => 'Edit code block',
+    'edit_code_block' => 'Kod bloğu düzenle',
     'insert_drawing' => 'Çizim ekle/düzenle',
     'drawing_manager' => 'Çizim yöneticisi',
     'insert_media' => 'Medya ekle/düzenle',
@@ -129,10 +129,10 @@ return [
     'cell_border_dotted' => 'Noktalı',
     'cell_border_dashed' => 'Kesik çizgili',
     'cell_border_double' => 'Çift',
-    'cell_border_groove' => 'Groove',
-    'cell_border_ridge' => 'Ridge',
-    'cell_border_inset' => 'Inset',
-    'cell_border_outset' => 'Outset',
+    'cell_border_groove' => 'Oyuk',
+    'cell_border_ridge' => 'Sırt',
+    'cell_border_inset' => 'İçe Dönük',
+    'cell_border_outset' => 'Dışa Dönük',
     'cell_border_none' => 'Hiçbiri',
     'cell_border_hidden' => 'Gizli',
 
@@ -144,23 +144,23 @@ return [
     'url' => 'URL',
     'text_to_display' => 'Görüntülenecek metin',
     'title' => 'Başlık',
-    'open_link' => 'Open link',
-    'open_link_in' => 'Open link in...',
+    'open_link' => 'Bağlantıyı aç',
+    'open_link_in' => 'Bağlantıyı şurada aç...',
     'open_link_current' => 'Geçerli pencere',
     'open_link_new' => 'Yeni pencere',
-    'remove_link' => 'Remove link',
-    'insert_collapsible' => 'Insert collapsible block',
-    'collapsible_unwrap' => 'Unwrap',
+    'remove_link' => 'Bağlantıyı kaldır',
+    'insert_collapsible' => 'Küçültülebilir blok ekle',
+    'collapsible_unwrap' => '',
     'edit_label' => 'Etiketi düzenle',
-    'toggle_open_closed' => 'Toggle open/closed',
-    'collapsible_edit' => 'Edit collapsible block',
+    'toggle_open_closed' => 'Değiştir açık/kapalı',
+    'collapsible_edit' => 'Küçültülebilir bloğu düzenle',
     'toggle_label' => 'Etiketleri aç/kapa',
 
     // About view
     'about' => 'Editör hakkında',
     'about_title' => 'WYSIWYG editor hakkında',
     'editor_license' => 'Editor Lisans ve Telif Hakkı',
-    'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
+    'editor_tiny_license' => 'Bu düzenleyici, MIT lisansı altında sağlanan :tinyLink kullanılarak oluşturulmuştur.',
     'editor_tiny_license_link' => 'TinyMCE telif ve lisans bilgilerini burada bulabilirsiniz.',
     'save_continue' => 'Kaydet & Devam Et',
     'callouts_cycle' => '(Türler arasında geçiş için basmaya devam ediniz)',
index c90f30eefccfe9300cb6e7ecb7885df9f5f63b19..2288f882d5cf83e2ed779f76f6c408281bef005f 100644 (file)
@@ -22,10 +22,10 @@ return [
     'meta_created_name' => ':user tarafından :timeLength oluşturuldu',
     'meta_updated' => ':timeLength güncellendi',
     'meta_updated_name' => ':user tarafından :timeLength güncellendi',
-    'meta_owned_name' => 'Owned by :user',
+    'meta_owned_name' => ':user kişisine ait',
     'meta_reference_page_count' => 'Referenced on :count page|Referenced on :count pages',
     'entity_select' => 'Öge Seçimi',
-    'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
+    'entity_select_lack_permission' => 'Bu öğeyi seçmek için gerekli izinlere sahip değilsiniz',
     'images' => 'Görseller',
     'my_recent_drafts' => 'Son Taslaklarım',
     'my_recently_viewed' => 'Son Görüntülediklerim',
@@ -38,19 +38,19 @@ return [
     'export_html' => 'Web Dosyası',
     'export_pdf' => 'PDF Dosyası',
     'export_text' => 'Düz Metin Dosyası',
-    'export_md' => 'Markdown File',
+    'export_md' => 'Markdown Dosyası',
 
     // Permissions and restrictions
     'permissions' => 'İzinler',
-    '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' => 'Kullanıcı rolleri tarafından sağlanan varsayılan izinleri geçersiz kılmak için izinleri buradan ayarlayın.',
+    'permissions_book_cascade' => 'Kitaplarda ayarlanan izinler, kendi izinleri tanımlanmadığı sürece otomatik olarak alt bölümlere ve sayfalara aktarılır.',
+    'permissions_chapter_cascade' => 'Bölümlerde ayarlanan izinler, kendi izinleri tanımlanmadığı sürece otomatik olarak alt sayfalara aktarılır.',
     'permissions_save' => 'İzinleri Kaydet',
     'permissions_owner' => 'Sahip',
-    'permissions_role_everyone_else' => 'Everyone Else',
-    'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
+    'permissions_role_everyone_else' => 'Diğer Herkes',
+    'permissions_role_everyone_else_desc' => 'Özellikle geçersiz kılınmamış tüm roller için izinleri ayarlayın.',
     'permissions_role_override' => 'Override permissions for role',
-    'permissions_inherit_defaults' => 'Inherit defaults',
+    'permissions_inherit_defaults' => 'Varsayılanları devral',
 
     // Search
     'search_results' => 'Arama Sonuçları',
@@ -70,7 +70,7 @@ return [
     'search_permissions_set' => 'İzinler ayarlanmış',
     'search_created_by_me' => 'Oluşturduklarım',
     'search_updated_by_me' => 'Güncellediklerim',
-    'search_owned_by_me' => 'Owned by me',
+    'search_owned_by_me' => 'Bana ait',
     'search_date_options' => 'Tarih Seçenekleri',
     'search_updated_before' => 'Önce güncellendi',
     'search_updated_after' => 'Sonra güncellendi',
@@ -93,23 +93,24 @@ return [
     'shelves_save' => 'Kitaplığı Kaydet',
     'shelves_books' => 'Bu kitaplıktaki kitaplar',
     'shelves_add_books' => 'Bu kitaplığa kitap ekle',
-    'shelves_drag_books' => 'Drag books below to add them to this shelf',
+    'shelves_drag_books' => 'Kitapları, bu kitaplığa eklemek için aşağıya sürükleyin',
     'shelves_empty_contents' => 'Bu kitaplıkta hiç kitap bulunamadı',
     'shelves_edit_and_assign' => 'Kitap eklemek için kitaplığı düzenleyin',
-    '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_edit_named' => ':name Kitaplığını Düzenle',
+    'shelves_edit' => 'Kitaplığı Düzenle',
+    'shelves_delete' => 'Kitaplığı Sil',
+    'shelves_delete_named' => ':name Kitaplığını Sil',
+    'shelves_delete_explain' => "Bu işlem ':name' isimli kitaplığı silecektir. İçerdiği kitaplar silinmeyecektir.",
+    'shelves_delete_confirmation' => 'Bu kitaplığı silmek istediğinize emin misiniz?',
+    'shelves_permissions' => 'Kitaplık İzinleri',
+    'shelves_permissions_updated' => 'Kitaplık İzinleri Güncellendi',
+    'shelves_permissions_active' => 'Kitaplık İzinleri Aktif',
+    'shelves_permissions_cascade_warning' => 'Kitaplıktaki izinler otomatik olarak içerilen kitaplara kademelendirilmez. Bunun nedeni, bir kitabın birden fazla kitaplıkta bulunabilmesidir. Ancak izinler, aşağıda bulunan seçenek kullanılarak alt kitaplara kopyalanabilir.',
+    '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_copy_permissions_to_books' => 'İzinleri Kitaplara Kopyala',
     'shelves_copy_permissions' => 'İzinleri Kopyala',
-    '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' => 'Bu işlem sonucunda kitaplığınızın izinleri, içerdiği kitaplara da aynen uygulanır. Aktifleştirmeden önce bu kitaplığa ait izinleri kaydettiğinizden emin olun.',
+    'shelves_copy_permission_success' => 'Kitaplık izinleri :count adet kitaba kopyalandı',
 
     // Books
     'book' => 'Kitap',
@@ -141,7 +142,7 @@ return [
     'books_search_this' => 'Bu kitapta ara',
     'books_navigation' => 'Kitap Navigasyonu',
     'books_sort' => 'Kitap İçeriklerini Sırala',
-    '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.',
+    'books_sort_desc' => 'İçeriğini yeniden düzenlemek için bir kitap içindeki bölümleri ve sayfaları taşıyın. Kitaplar arasında bölümlerin ve sayfaların kolayca taşınmasını sağlayan başka kitaplar eklenebilir.',
     'books_sort_named' => ':bookName Kitabını Sırala',
     'books_sort_name' => 'İsme Göre Sırala',
     'books_sort_created' => 'Oluşturulma Tarihine Göre Sırala',
@@ -150,15 +151,15 @@ return [
     'books_sort_chapters_last' => 'En Son Bölümler',
     'books_sort_show_other' => 'Diğer Kitapları Göster',
     'books_sort_save' => 'Yeni Düzeni Kaydet',
-    '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_show_other_desc' => 'Sıralama işlemine dahil etmek ve kitaplar arası yeniden düzenlemeyi kolaylaştırmak için diğer kitapları buraya ekleyin.',
+    'books_sort_move_up' => 'Yukarı Taşı',
+    'books_sort_move_down' => 'Aşağı Taşı',
     '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_prev_chapter' => 'Önceki Bölüme Git',
+    'books_sort_move_next_chapter' => 'Sonraki Bölüme Git',
+    'books_sort_move_book_start' => 'Kitap Başlangıcına Git',
+    'books_sort_move_book_end' => 'Kitap Sonuna Git',
     'books_sort_move_before_chapter' => 'Move to Before Chapter',
     'books_sort_move_after_chapter' => 'Move to After Chapter',
     'books_copy' => 'Kitabı Kopyala',
@@ -187,7 +188,7 @@ return [
     'chapters_permissions_active' => 'Bölüm İzinleri Aktif',
     'chapters_permissions_success' => 'Bölüm İzinleri Güncellendi',
     'chapters_search_this' => 'Bu bölümde ara',
-    'chapter_sort_book' => 'Sort Book',
+    'chapter_sort_book' => 'Kitap Sırala',
 
     // Pages
     'page' => 'Sayfa',
@@ -213,18 +214,18 @@ return [
     'pages_editing_page' => 'Sayfa Düzenleniyor',
     'pages_edit_draft_save_at' => 'Taslak kaydedildi ',
     'pages_edit_delete_draft' => 'Taslağı Sil',
-    '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' => 'Taslak sayfa değişikliklerinizi silmek istediğinizden emin misiniz? Son tam kaydetmeden bu yana yaptığınız tüm değişiklikler kaybolacak ve düzenleyici en son sayfanın taslak olmayan kaydetme durumuyla güncellenecektir.',
     'pages_edit_discard_draft' => 'Taslağı Yoksay',
-    '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_markdown' => 'Markdown Editörüne Geç',
+    'pages_edit_switch_to_markdown_clean' => '(Temiz İçerik)',
+    'pages_edit_switch_to_markdown_stable' => '(Kararlı İçerik)',
+    'pages_edit_switch_to_wysiwyg' => 'WYSIWYG düzenleyiciye geç',
     'pages_edit_set_changelog' => 'Değişim Günlüğünü Ayarla',
     'pages_edit_enter_changelog_desc' => 'Yaptığınız değişiklikler hakkında kısa bir açıklama girin',
     'pages_edit_enter_changelog' => 'Değişim Günlüğünü Yazın',
-    '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_title' => 'Düzenleyici Değiştir',
+    'pages_editor_switch_are_you_sure' => 'Bu sayfa için düzenleyiciyi değiştirmek istediğinizden emin misiniz?',
+    'pages_editor_switch_consider_following' => 'Düzenleyiciyi değiştirirken aşağıdakilere dikkat edin:',
     '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.',
@@ -236,7 +237,7 @@ return [
     'pages_md_insert_image' => 'Görsel Ekle',
     'pages_md_insert_link' => 'Öge Bağlantısı Ekle',
     'pages_md_insert_drawing' => 'Çizim Ekle',
-    'pages_md_show_preview' => 'Show preview',
+    'pages_md_show_preview' => 'Önizlemeyi göster',
     'pages_md_sync_scroll' => 'Sync preview scroll',
     'pages_not_in_chapter' => 'Bu sayfa, bir bölüme ait değil',
     'pages_move' => 'Sayfayı Taşı',
@@ -254,10 +255,10 @@ return [
     'pages_revisions_created_by' => 'Revize Eden',
     'pages_revisions_date' => 'Revizyon Tarihi',
     'pages_revisions_number' => '#',
-    'pages_revisions_sort_number' => 'Revision Number',
+    'pages_revisions_sort_number' => 'Revizyon Numarası',
     'pages_revisions_numbered' => 'Revizyon #:id',
     'pages_revisions_numbered_changes' => 'Revizyon #:id Değişiklikleri',
-    'pages_revisions_editor' => 'Editor Type',
+    'pages_revisions_editor' => 'Düzenleyici Türü',
     'pages_revisions_changelog' => 'Değişim Günlüğü',
     'pages_revisions_changes' => 'Değişiklikler',
     'pages_revisions_current' => 'Şimdiki Sürüm',
@@ -265,7 +266,7 @@ return [
     'pages_revisions_restore' => 'Geri Dön',
     'pages_revisions_none' => 'Bu sayfaya ait herhangi bir revizyon bulunamadı',
     'pages_copy_link' => 'Bağlantıyı kopyala',
-    'pages_edit_content_link' => 'Jump to section in editor',
+    'pages_edit_content_link' => 'Düzenleyicide bölüme atla',
     'pages_pointer_enter_mode' => 'Enter section select mode',
     'pages_pointer_label' => 'Page Section Options',
     'pages_pointer_permalink' => 'Page Section Permalink',
@@ -304,11 +305,11 @@ return [
     'tags_explain' => "İçeriğinizi daha iyi kategorize etmek için etiket ekleyin. Etiketlere değer atayarak daha derinlemesine bir düzen elde edebilirsiniz.",
     'tags_add' => 'Başka etiket ekle',
     'tags_remove' => 'Bu etiketi sil',
-    '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_usages' => 'Toplam etiket kullanımı',
+    'tags_assigned_pages' => 'Sayfalara Atandı',
+    'tags_assigned_chapters' => 'Bölümlere Atandı',
+    'tags_assigned_books' => 'Kitaplara Atandı',
+    'tags_assigned_shelves' => 'Kitaplıklara Atandı',
     'tags_x_unique_values' => ':count unique values',
     'tags_all_values' => 'Tüm değerler',
     'tags_view_tags' => 'Etiketleri Göster',
@@ -322,7 +323,7 @@ return [
     'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
     'attachments_set_link' => 'Bağlantıyı Ata',
     'attachments_delete' => 'Bu eki silmek istediğinize emin misiniz?',
-    'attachments_dropzone' => 'Drop files here to upload',
+    'attachments_dropzone' => 'Yüklemek için dosyaları buraya bırakın',
     'attachments_no_files' => 'Hiçbir dosya yüklenmedi',
     'attachments_explain_link' => 'Eğer dosya yüklememeyi tercih ederseniz bağlantı ekleyebilirsiniz. Bu bağlantı başka bir sayfanın veya bulut depolamadaki bir dosyanın bağlantısı olabilir.',
     'attachments_link_name' => 'Bağlantı Adı',
@@ -365,13 +366,13 @@ return [
     'comment_new' => 'Yeni Yorum',
     'comment_created' => ':createDiff yorum yaptı',
     'comment_updated' => ':username tarafından :updateDiff güncellendi',
-    'comment_updated_indicator' => 'Updated',
+    'comment_updated_indicator' => 'Güncellendi',
     'comment_deleted_success' => 'Yorum silindi',
     'comment_created_success' => 'Yorum gönderildi',
     'comment_updated_success' => 'Yorum güncellendi',
     'comment_delete_confirm' => 'Bu yorumu silmek istediğinize emin misiniz?',
     'comment_in_reply_to' => ':commentId yorumuna yanıt olarak',
-    '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' => 'İşte bu sayfaya bırakılan yorumlar. Kaydedilen sayfayı görüntülerken yorumlar eklenebilir ve yönetilebilir.',
 
     // Revision
     'revision_delete_confirm' => 'Bu revizyonu silmek istediğinize emin misiniz?',
@@ -387,19 +388,19 @@ return [
     'copy_consider_access' => 'Konum, sahiplik veya izinlerde yapılan bir değişiklik önceden erişimi olmayanlara erişim hakkı kazandırabilir.',
 
     // 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' => 'Kitaplığa Dünüştür',
+    'convert_to_shelf_contents_desc' => 'Bu kitabı aynı içeriğe sahip yeni bir rafa dönüştürebilirsiniz. Bu kitapta yer alan bölümler yeni kitaplığa dönüştürülecektir. Bu kitapta bölüm olmayan herhangi bir sayfa varsa, bu kitap yeniden adlandırılacak ve bu sayfaları içerecek ve bu kitap yeni kitaplığın bir parçası haline gelecektir.',
+    'convert_to_shelf_permissions_desc' => 'Bu kitapta ayarlanan tüm izinler yeni kitaplığa ve kendi izinleri uygulanmayan tüm yeni alt kitaplara kopyalanacaktır. Kitaplıklardaki izinlerin, kitaplarda olduğu gibi içindeki içeriğe otomatik olarak kademelendirilmediğini unutmayın.',
+    'convert_book' => 'Kitabı Dönüştür',
+    'convert_book_confirm' => 'Bu kitabı dönüştürmek istediğinize emin misiniz?',
+    'convert_undo_warning' => 'Bu kolay kolay geri alınamaz.',
+    'convert_to_book' => 'Kitaba Dönüştür',
+    'convert_to_book_desc' => 'Bu bölümü aynı içeriğe sahip yeni bir kitaba dönüştürebilirsiniz. Bu bölümde ayarlanan tüm izinler yeni kitaba kopyalanacaktır, ancak ana kitaptan devralınan izinler kopyalanmayacaktır, bu da erişim kontrolünün değişmesine neden olabilir.',
+    'convert_chapter' => 'Bölümü Dönüştür',
+    'convert_chapter_confirm' => 'Bu bölümü dönüştürmek istediğinizden emin misiniz?',
 
     // References
-    'references' => 'References',
-    'references_none' => 'There are no tracked references to this item.',
-    'references_to_desc' => 'Shown below are all the known pages in the system that link to this item.',
+    'references' => 'Referanslar',
+    'references_none' => 'Bu öğeye ilişkin takip edilen bir referans bulunmamaktadır.',
+    'references_to_desc' => 'Aşağıda, sistemde bu öğeye bağlantı veren bilinen tüm sayfalar gösterilmektedir.',
 ];
index 879375570b53b89263e30c3239d1dd8f7a3d3f92..455fe80395a61f6062e64b9f76a499d85b6b34cf 100644 (file)
@@ -23,10 +23,10 @@ return [
     'saml_no_email_address' => 'Harici kimlik doğrulama sisteminden gelen veriler, bu kullanıcının e-posta adresini içermiyor',
     'saml_invalid_response_id' => 'Harici doğrulama sistemi tarafından sağlanan bir veri talebi, bu uygulama tarafından başlatılan bir işlem tarafından tanınamadı. Giriş yaptıktan sonra geri dönmek bu soruna yol açmış olabilir.',
     'saml_fail_authed' => ':system kullanarak giriş yapma başarısız oldu; sistem, başarılı bir kimlik doğrulama sağlayamadı',
-    'oidc_already_logged_in' => 'Already logged in',
-    'oidc_user_not_registered' => 'The user :name is not registered and automatic registration is disabled',
-    'oidc_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system',
-    'oidc_fail_authed' => 'Login using :system failed, system did not provide successful authorization',
+    'oidc_already_logged_in' => 'Zaten oturum açılmış',
+    'oidc_user_not_registered' => ':name adlı kullanıcı kayıtlı değil ve otomatik kaydolma devre dışı bırakılmış',
+    'oidc_no_email_address' => 'Harici kimlik doğrulama sisteminden gelen veriler, bu kullanıcının e-posta adresini içermiyor',
+    'oidc_fail_authed' => ':system kullanarak giriş yapma başarısız oldu; sistem, başarılı bir kimlik doğrulama sağlayamadı',
     'social_no_action_defined' => 'Herhangi bir eylem tanımlanmamış',
     'social_login_bad_response' => ":socialAccount girişi sırasında bir hata meydana geldi: \n:error",
     'social_account_in_use' => 'Bu :socialAccount zaten kullanımda, :socialAccount hesabıyla giriş yapmayı deneyin.',
@@ -49,21 +49,21 @@ return [
     // Drawing & Images
     'image_upload_error' => 'Görsel yüklenirken bir hata meydana geldi',
     'image_upload_type_error' => 'Yüklemeye çalıştığınız dosya türü geçersizdir',
-    'image_upload_replace_type' => 'Image file replacements must be of the same type',
-    '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.',
+    'image_upload_replace_type' => 'Görsel dosyası değişimleri, aynı dosya uzantı tipinde olmalı',
+    'drawing_data_not_found' => 'Çizim verileri yüklenemedi. Çizim dosyası artık mevcut olmayabilir veya erişim izniniz olmayabilir.',
 
     // Attachments
     'attachment_not_found' => 'Ek bulunamadı',
-    'attachment_upload_error' => 'An error occurred uploading the attachment file',
+    'attachment_upload_error' => 'Ekler yüklenirken bir hata oluştu',
 
     // Pages
     'page_draft_autosave_fail' => 'Taslak kaydetme başarısız oldu. Bu sayfayı kaydetmeden önce internet bağlantınız olduğundan emin olun',
-    'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content',
+    'page_draft_delete_fail' => 'Sayfa taslağı silinemedi ve geçerli sayfanın kayıtlı içeriği getirilemedi',
     'page_custom_home_deletion' => 'Bu sayfa, "Ana Sayfa" olarak ayarlandığı için silinemez',
 
     // Entities
     'entity_not_found' => 'Öge bulunamadı',
-    'bookshelf_not_found' => 'Shelf not found',
+    'bookshelf_not_found' => 'Kitaplık bulunamadı',
     'book_not_found' => 'Kitap bulunamadı',
     'page_not_found' => 'Sayfa bulunamadı',
     'chapter_not_found' => 'Bölüm bulunamadı',
@@ -92,9 +92,9 @@ return [
     '404_page_not_found' => 'Sayfa Bulunamadı',
     'sorry_page_not_found' => 'Üzgünüz, aradığınız sayfa bulunamıyor.',
     'sorry_page_not_found_permission_warning' => 'Bu sayfanın var olduğunu düşünüyorsanız, görüntüleme iznine sahip olmayabilirsiniz.',
-    '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.',
+    'image_not_found' => 'Görsel Bulunamadı',
+    'image_not_found_subtitle' => 'Üzgünüz, aradığınız görsel dosyası bulunamadı.',
+    'image_not_found_details' => 'Bu resmin var olmasını bekliyorsanız silinmiş olabilir.',
     'return_home' => 'Ana sayfaya dön',
     'error_occurred' => 'Bir Hata Oluştu',
     'app_down' => ':appName şu anda erişilemez durumda',
index e9a47461b3d18a42a68699d729acf99b07b9eda5..fef326ff352e7ee7ae3e838bb2f0f6c039aece51 100644 (file)
@@ -5,14 +5,14 @@
  */
 
 return [
-    'shortcuts' => 'Shortcuts',
-    'shortcuts_interface' => 'Interface Keyboard Shortcuts',
-    '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' => 'Kısayollar',
+    'shortcuts_interface' => 'Klavye Kısayollarını Görüntüle',
+    'shortcuts_toggle_desc' => 'Burada, gezinme ve eylemler için kullanılan klavye sistem arayüzü kısayollarını etkinleştirebilir veya devre dışı bırakabilirsiniz.',
+    'shortcuts_customize_desc' => 'Aşağıdaki kısayolların her birini özelleştirebilirsiniz. Bir kısayol için girişi seçtikten sonra istediğiniz tuş kombinasyonuna basmanız yeterlidir.',
+    'shortcuts_toggle_label' => 'Klavye kısayolları etkinleştirildi',
+    'shortcuts_section_navigation' => 'Navigasyon',
+    'shortcuts_section_actions' => 'Ortak Eylemler',
+    'shortcuts_save' => 'Kısayolları Kaydet',
+    'shortcuts_overlay_desc' => 'Not: Kısayollar etkinleştirildiğinde, "?" tuşuna basılarak o anda ekranda görünen eylemler için mevcut kısayolları vurgulayan bir yardımcı yer paylaşımı kullanılabilir.',
+    'shortcuts_update_success' => 'Kısayol tercihleri güncellendi!',
 ];
\ No newline at end of file
index d722638814fc4be88653a91ff9534d37f5b52285..af7130887f61eedacce774e0fc0093ce3f965948 100644 (file)
@@ -33,7 +33,7 @@ return [
     'app_custom_html_disabled_notice' => 'Olası hatalı değişikliklerin geriye alınabilmesi için bu sayfanın özelleştirilmiş HTML "head" içeriği devre dışı bırakıldı.',
     'app_logo' => 'Uygulama Logosu',
     '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' => 'Uygulama Simgesi',
     'app_icon_desc' => 'This icon is used for browser tabs and shortcut icons. This should be a 256px square PNG image.',
     'app_homepage' => 'Ana Sayfa',
     'app_homepage_desc' => 'Varsayılan görünüm yerine ana sayfada görünmesi için bir görünüm seçin. Sayfa izinleri, burada seçeceğiniz sayfalar için yok sayılacaktır.',
@@ -51,8 +51,8 @@ return [
     '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',
+    'app_color' => 'Birincil Renk',
+    'link_color' => 'Varsayılan Bağlantı Rengi',
     '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.',
     'bookshelf_color' => 'Raf Rengi',
     'book_color' => 'Kitap Rengi',
@@ -92,10 +92,10 @@ return [
     'maint_send_test_email_mail_text' => 'Tebrikler! Eğer bu e-posta bildirimini alıyorsanız, e-posta ayarlarınız doğru bir şekilde ayarlanmış demektir.',
     'maint_recycle_bin_desc' => 'Silinen raflar, kitaplar, bölümler ve sayfalar geri dönüşüm kutusuna gönderilir, böylece geri yüklenebilir veya kalıcı olarak silinebilir. Geri dönüşüm kutusundaki daha eski öğeler, sistem yapılandırmasına bağlı olarak bir süre sonra otomatik olarak kaldırılabilir.',
     'maint_recycle_bin_open' => 'Geri Dönüşüm Kutusunu Aç',
-    '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' => 'Referansları Yeniden Oluştur',
+    'maint_regen_references_desc' => 'Bu eylem, veritabanındaki çapraz öğe referans dizinini yeniden oluşturur. Bu işlem genellikle otomatik olarak gerçekleştirilir ancak bu eylem eski içerikleri veya resmi olmayan yöntemlerle eklenen içerikleri indekslemek için yararlı olabilir.',
+    'maint_regen_references_success' => 'Referans dizini yeniden oluşturuldu!',
+    'maint_timeout_command_note' => 'Not: Bu eylemin çalışması zaman alabilir, bu da bazı web ortamlarında zaman aşımı sorunlarına yol açabilir. Alternatif olarak, bu eylem bir terminal komutu kullanılarak gerçekleştirilebilir.',
 
     // Recycle Bin
     'recycle_bin' => 'Geri Dönüşüm Kutusu',
@@ -248,28 +248,28 @@ return [
     '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_edit' => 'Webhook\'u Düzenle',
+    'webhooks_save' => 'Webhook\'u Kaydet',
+    'webhooks_details' => 'Webhook Detayları',
     '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' => 'Webhook Olayları',
     '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_events_all' => 'Tüm sistem olayları',
+    'webhooks_name' => 'Webhook Adı',
+    'webhooks_timeout' => 'Webhook İsteği Zaman Aşımı (Saniyeler)',
     'webhooks_endpoint' => 'Webhook Endpoint',
-    'webhooks_active' => 'Webhook Active',
+    'webhooks_active' => 'Webhook Aktif',
     'webhook_events_table_header' => 'Etkinlikler',
     'webhooks_delete' => 'Web Kancasını Sil',
-    'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
+    'webhooks_delete_warning' => 'Bu, \':webhookName\' adıyla bu webhook, sistemden tamamen silecektir.',
     'webhooks_delete_confirm' => 'Bu web kancası silmek istediğinize emin misiniz?',
-    '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_format_example' => 'Webhook Biçimlendirme Örneği',
+    'webhooks_format_example_desc' => 'Webhook verileri, yapılandırılmış uç noktaya aşağıdaki formatı izleyen JSON olarak bir POST isteği olarak gönderilir. "related_item" ve "url" özellikleri isteğe bağlıdır ve tetiklenen olayın türüne bağlı olacaktır.',
+    'webhooks_status' => 'Webhook Durumu',
     'webhooks_last_called' => 'Last Called:',
     'webhooks_last_errored' => 'Last Errored:',
-    'webhooks_last_error_message' => 'Last Error Message:',
+    'webhooks_last_error_message' => 'Son Hata Mesajı:',
 
 
     //! If editing translations files directly please ignore this in all
index 2d4068c5295c354dec6b601f0a0cb6d7904e1c07..63889b26a6584360a87ecbfdee0d4ae1ce0f9588 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => 'Дозволи полиці оновлено',
     'shelves_permissions_active' => 'Дозволи полиці активні',
     'shelves_permissions_cascade_warning' => 'Дозволи на полицях не каскадують автоматично до вміщених книг. Це тому, що книга може стояти на кількох полицях. Однак дозволи можна скопіювати до дочірніх книг за допомогою наведеної нижче опції.',
+    '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_copy_permissions_to_books' => 'Копіювати дозволи на книги',
     'shelves_copy_permissions' => 'Копіювати дозволи',
     'shelves_copy_permissions_explain' => 'Це застосує поточні налаштування дозволів цієї полиці до всіх книг, які містяться в ній. Перед активацією переконайтеся, що будь-які зміни в дозволах цієї полиці збережено.',
index fadfaf9090dfd5109c2a8911f7b8dacd5b2e0e41..f0125cf744de66fe4b623edc5f22815dd37f4cad 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Copy Permissions to Books',
     'shelves_copy_permissions' => '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.',
index afe07c048f7a02375bf5fe3dabfac68f97057364..542a819d92af244f28584f2605bd0c63d7514579 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_copy_permissions_to_books' => 'Sao chép các quyền cho sách',
     'shelves_copy_permissions' => 'Sao chép các quyền',
     '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.',
index 44b6b91cb1f154e337ea43463d420c7084c6f644..d06ebccdffb0742df886f879812ae5517afc7752 100644 (file)
@@ -106,6 +106,7 @@ return [
     'shelves_permissions_updated' => '书架权限已更新',
     'shelves_permissions_active' => '书架权限已启用',
     'shelves_permissions_cascade_warning' => '书架上的权限不会自动应用到书架里的图书上,这是因为图书可以在多个书架上存在。使用下面的选项可以将权限复制到书架里的图书上。',
+    '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_copy_permissions_to_books' => '将权限复制到图书',
     'shelves_copy_permissions' => '复制权限',
     'shelves_copy_permissions_explain' => '此操作会将此书架的当前权限设置应用于其中包含的所有图书上。 启用前请确保已保存对此书架权限的任何更改。',
index c717486dd96697bc903f4457eb77032806ebfd3a..10f927919ad1594b11a8bbe4f0bc8701ae69bfe6 100644 (file)
@@ -106,6 +106,7 @@ return [
     '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_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.',
index 50987602f1ec13c14d9378d7c12fa849ccb0bfcf..c8645fa4cb8ed05369de0cf02929d9ef26c5c823 100644 (file)
@@ -4,21 +4,22 @@
   "requires": true,
   "packages": {
     "": {
+      "name": "bookstack",
       "dependencies": {
-        "@codemirror/commands": "^6.2.2",
-        "@codemirror/lang-css": "^6.1.1",
-        "@codemirror/lang-html": "^6.4.3",
-        "@codemirror/lang-javascript": "^6.1.6",
+        "@codemirror/commands": "^6.2.4",
+        "@codemirror/lang-css": "^6.2.1",
+        "@codemirror/lang-html": "^6.4.5",
+        "@codemirror/lang-javascript": "^6.1.9",
         "@codemirror/lang-json": "^6.0.1",
-        "@codemirror/lang-markdown": "^6.1.1",
+        "@codemirror/lang-markdown": "^6.2.0",
         "@codemirror/lang-php": "^6.0.1",
         "@codemirror/lang-xml": "^6.0.2",
-        "@codemirror/language": "^6.6.0",
-        "@codemirror/legacy-modes": "^6.3.2",
-        "@codemirror/state": "^6.2.0",
-        "@codemirror/theme-one-dark": "^6.1.1",
-        "@codemirror/view": "^6.9.4",
-        "@lezer/highlight": "^1.1.4",
+        "@codemirror/language": "^6.9.0",
+        "@codemirror/legacy-modes": "^6.3.3",
+        "@codemirror/state": "^6.2.1",
+        "@codemirror/theme-one-dark": "^6.1.2",
+        "@codemirror/view": "^6.16.0",
+        "@lezer/highlight": "^1.1.6",
         "@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
         "@ssddanbrown/codemirror-lang-twig": "^1.0.0",
         "codemirror": "^6.0.1",
         "sortablejs": "^1.15.0"
       },
       "devDependencies": {
-        "@lezer/generator": "^1.2.2",
+        "@lezer/generator": "^1.4.2",
         "chokidar-cli": "^3.0",
-        "esbuild": "^0.17.16",
-        "eslint": "^8.38.0",
+        "esbuild": "^0.19",
+        "eslint": "^8.47.0",
         "eslint-config-airbnb-base": "^15.0.0",
-        "eslint-plugin-import": "^2.27.5",
+        "eslint-plugin-import": "^2.28.1",
         "livereload": "^0.9.3",
         "npm-run-all": "^4.1.5",
         "punycode": "^2.3.0",
-        "sass": "^1.62.0"
+        "sass": "^1.66.1"
+      }
+    },
+    "node_modules/@aashutoshrathi/word-wrap": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+      "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
       }
     },
     "node_modules/@codemirror/autocomplete": {
-      "version": "6.5.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.5.1.tgz",
-      "integrity": "sha512-/Sv9yJmqyILbZ26U4LBHnAtbikuVxWUp+rQ8BXuRGtxZfbfKOY/WPbsUtvSP2h0ZUZMlkxV/hqbKRFzowlA6xw==",
+      "version": "6.9.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.9.0.tgz",
+      "integrity": "sha512-Fbwm0V/Wn3BkEJZRhr0hi5BhCo5a7eBL6LYaliPjOSwCyfOpnjXY59HruSxOUNV+1OYer0Tgx1zRNQttjXyDog==",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
         "@codemirror/state": "^6.0.0",
@@ -58,9 +68,9 @@
       }
     },
     "node_modules/@codemirror/commands": {
-      "version": "6.2.2",
-      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.2.tgz",
-      "integrity": "sha512-s9lPVW7TxXrI/7voZ+HmD/yiAlwAYn9PH5SUVSUhsxXHhv4yl5eZ3KLntSoTynfdgVYM0oIpccQEWRBQgmNZyw==",
+      "version": "6.2.4",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.2.4.tgz",
+      "integrity": "sha512-42lmDqVH0ttfilLShReLXsDfASKLXzfyC36bzwcqzox9PlHulMcsUOfHXNo2X2aFMVNUoQ7j+d4q5bnfseYoOA==",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
         "@codemirror/state": "^6.2.0",
       }
     },
     "node_modules/@codemirror/lang-css": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.1.1.tgz",
-      "integrity": "sha512-P6jdNEHyRcqqDgbvHYyC9Wxkek0rnG3a9aVSRi4a7WrjPbQtBTaOmvYpXmm13zZMAatO4Oqpac+0QZs7sy+LnQ==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.2.1.tgz",
+      "integrity": "sha512-/UNWDNV5Viwi/1lpr/dIXJNWiwDxpw13I4pTUAsNxZdg6E0mI2kTQb0P2iHczg1Tu+H4EBgJR+hYhKiHKko7qg==",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
         "@codemirror/language": "^6.0.0",
         "@codemirror/state": "^6.0.0",
+        "@lezer/common": "^1.0.2",
         "@lezer/css": "^1.0.0"
       }
     },
     "node_modules/@codemirror/lang-html": {
-      "version": "6.4.3",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.3.tgz",
-      "integrity": "sha512-VKzQXEC8nL69Jg2hvAFPBwOdZNvL8tMFOrdFwWpU+wc6a6KEkndJ/19R5xSaglNX6v2bttm8uIEFYxdQDcIZVQ==",
+      "version": "6.4.5",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.5.tgz",
+      "integrity": "sha512-dUCSxkIw2G+chaUfw3Gfu5kkN83vJQN8gfQDp9iEHsIZluMJA0YJveT12zg/28BJx+uPsbQ6VimKCgx3oJrZxA==",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
         "@codemirror/lang-css": "^6.0.0",
       }
     },
     "node_modules/@codemirror/lang-javascript": {
-      "version": "6.1.6",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.6.tgz",
-      "integrity": "sha512-TTK28z+vJQY9GAefLTDDptI2LMqMfAiuTpt8s9SsNwocjVQ1v9yTzfReMf1hYhspQCdhfa7fdKnQJ78mKe/bHQ==",
+      "version": "6.1.9",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.1.9.tgz",
+      "integrity": "sha512-z3jdkcqOEBT2txn2a87A0jSy6Te3679wg/U8QzMeftFt+4KA6QooMwfdFzJiuC3L6fXKfTXZcDocoaxMYfGz0w==",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
         "@codemirror/language": "^6.6.0",
       }
     },
     "node_modules/@codemirror/lang-markdown": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.1.1.tgz",
-      "integrity": "sha512-n87Ms6Y5UYb1UkFu8sRzTLfq/yyF1y2AYiWvaVdbBQi5WDj1tFk5N+AKA+WC0Jcjc1VxvrCCM0iizjdYYi9sFQ==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.2.0.tgz",
+      "integrity": "sha512-deKegEQVzfBAcLPqsJEa+IxotqPVwWZi90UOEvQbfa01NTAw8jNinrykuYPTULGUj+gha0ZG2HBsn4s5d64Qrg==",
       "dependencies": {
+        "@codemirror/autocomplete": "^6.7.1",
         "@codemirror/lang-html": "^6.0.0",
         "@codemirror/language": "^6.3.0",
         "@codemirror/state": "^6.0.0",
       }
     },
     "node_modules/@codemirror/language": {
-      "version": "6.6.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.6.0.tgz",
-      "integrity": "sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==",
+      "version": "6.9.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.9.0.tgz",
+      "integrity": "sha512-nFu311/0ne/qGuGCL3oKuktBgzVOaxCHZPZv1tLSZkNjPYxxvkjSbzno3MlErG2tgw1Yw1yF8BxMCegeMXqpiw==",
       "dependencies": {
         "@codemirror/state": "^6.0.0",
         "@codemirror/view": "^6.0.0",
       }
     },
     "node_modules/@codemirror/legacy-modes": {
-      "version": "6.3.2",
-      "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.2.tgz",
-      "integrity": "sha512-ki5sqNKWzKi5AKvpVE6Cna4Q+SgxYuYVLAZFSsMjGBWx5qSVa+D+xipix65GS3f2syTfAD9pXKMX4i4p49eneQ==",
+      "version": "6.3.3",
+      "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.3.3.tgz",
+      "integrity": "sha512-X0Z48odJ0KIoh/HY8Ltz75/4tDYc9msQf1E/2trlxFaFFhgjpVHjZ/BCXe1Lk7s4Gd67LL/CeEEHNI+xHOiESg==",
       "dependencies": {
         "@codemirror/language": "^6.0.0"
       }
       }
     },
     "node_modules/@codemirror/state": {
-      "version": "6.2.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.0.tgz",
-      "integrity": "sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA=="
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.2.1.tgz",
+      "integrity": "sha512-RupHSZ8+OjNT38zU9fKH2sv+Dnlr8Eb8sl4NOnnqz95mCFTZUaiRP8Xv5MeeaG0px2b8Bnfe7YGwCV3nsBhbuw=="
     },
     "node_modules/@codemirror/theme-one-dark": {
-      "version": "6.1.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.1.tgz",
-      "integrity": "sha512-+CfzmScfJuD6uDF5bHJkAjWTQ2QAAHxODCPxUEgcImDYcJLT+4l5vLnBHmDVv46kCC5uUJGMrBJct2Z6JbvqyQ==",
+      "version": "6.1.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
+      "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
         "@codemirror/state": "^6.0.0",
       }
     },
     "node_modules/@codemirror/view": {
-      "version": "6.9.4",
-      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.9.4.tgz",
-      "integrity": "sha512-Ov2H9gwlGUxiH94zWxlLtTlyogSFaQDIYjtSEcfzgh7MkKmKVchkmr4JbtR5zBev3jY5DVtKvUC8yjd1bKW55A==",
+      "version": "6.16.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.16.0.tgz",
+      "integrity": "sha512-1Z2HkvkC3KR/oEZVuW9Ivmp8TWLzGEd8T8TA04TTwPvqogfkHBdYSlflytDOqmkUxM2d1ywTg7X2dU5mC+SXvg==",
       "dependencies": {
         "@codemirror/state": "^6.1.4",
         "style-mod": "^4.0.0",
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.17.tgz",
-      "integrity": "sha512-E6VAZwN7diCa3labs0GYvhEPL2M94WLF8A+czO8hfjREXxba8Ng7nM5VxV+9ihNXIY1iQO1XxUU4P7hbqbICxg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.2.tgz",
+      "integrity": "sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.17.tgz",
-      "integrity": "sha512-jaJ5IlmaDLFPNttv0ofcwy/cfeY4bh/n705Tgh+eLObbGtQBK3EPAu+CzL95JVE4nFAliyrnEu0d32Q5foavqg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz",
+      "integrity": "sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.17.tgz",
-      "integrity": "sha512-446zpfJ3nioMC7ASvJB1pszHVskkw4u/9Eu8s5yvvsSDTzYh4p4ZIRj0DznSl3FBF0Z/mZfrKXTtt0QCoFmoHA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.2.tgz",
+      "integrity": "sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.17.tgz",
-      "integrity": "sha512-m/gwyiBwH3jqfUabtq3GH31otL/0sE0l34XKpSIqR7NjQ/XHQ3lpmQHLHbG8AHTGCw8Ao059GvV08MS0bhFIJQ==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz",
+      "integrity": "sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.17.tgz",
-      "integrity": "sha512-4utIrsX9IykrqYaXR8ob9Ha2hAY2qLc6ohJ8c0CN1DR8yWeMrTgYFjgdeQ9LIoTOfLetXjuCu5TRPHT9yKYJVg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz",
+      "integrity": "sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.17.tgz",
-      "integrity": "sha512-4PxjQII/9ppOrpEwzQ1b0pXCsFLqy77i0GaHodrmzH9zq2/NEhHMAMJkJ635Ns4fyJPFOlHMz4AsklIyRqFZWA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz",
+      "integrity": "sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.17.tgz",
-      "integrity": "sha512-lQRS+4sW5S3P1sv0z2Ym807qMDfkmdhUYX30GRBURtLTrJOPDpoU0kI6pVz1hz3U0+YQ0tXGS9YWveQjUewAJw==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz",
+      "integrity": "sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.17.tgz",
-      "integrity": "sha512-biDs7bjGdOdcmIk6xU426VgdRUpGg39Yz6sT9Xp23aq+IEHDb/u5cbmu/pAANpDB4rZpY/2USPhCA+w9t3roQg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz",
+      "integrity": "sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg==",
       "cpu": [
         "arm"
       ],
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.17.tgz",
-      "integrity": "sha512-2+pwLx0whKY1/Vqt8lyzStyda1v0qjJ5INWIe+d8+1onqQxHLLi3yr5bAa4gvbzhZqBztifYEu8hh1La5+7sUw==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz",
+      "integrity": "sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.17.tgz",
-      "integrity": "sha512-IBTTv8X60dYo6P2t23sSUYym8fGfMAiuv7PzJ+0LcdAndZRzvke+wTVxJeCq4WgjppkOpndL04gMZIFvwoU34Q==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz",
+      "integrity": "sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.17.tgz",
-      "integrity": "sha512-WVMBtcDpATjaGfWfp6u9dANIqmU9r37SY8wgAivuKmgKHE+bWSuv0qXEFt/p3qXQYxJIGXQQv6hHcm7iWhWjiw==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz",
+      "integrity": "sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw==",
       "cpu": [
         "loong64"
       ],
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.17.tgz",
-      "integrity": "sha512-2kYCGh8589ZYnY031FgMLy0kmE4VoGdvfJkxLdxP4HJvWNXpyLhjOvxVsYjYZ6awqY4bgLR9tpdYyStgZZhi2A==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz",
+      "integrity": "sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg==",
       "cpu": [
         "mips64el"
       ],
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.17.tgz",
-      "integrity": "sha512-KIdG5jdAEeAKogfyMTcszRxy3OPbZhq0PPsW4iKKcdlbk3YE4miKznxV2YOSmiK/hfOZ+lqHri3v8eecT2ATwQ==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz",
+      "integrity": "sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw==",
       "cpu": [
         "ppc64"
       ],
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.17.tgz",
-      "integrity": "sha512-Cj6uWLBR5LWhcD/2Lkfg2NrkVsNb2sFM5aVEfumKB2vYetkA/9Uyc1jVoxLZ0a38sUhFk4JOVKH0aVdPbjZQeA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz",
+      "integrity": "sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw==",
       "cpu": [
         "riscv64"
       ],
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.17.tgz",
-      "integrity": "sha512-lK+SffWIr0XsFf7E0srBjhpkdFVJf3HEgXCwzkm69kNbRar8MhezFpkIwpk0qo2IOQL4JE4mJPJI8AbRPLbuOQ==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz",
+      "integrity": "sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g==",
       "cpu": [
         "s390x"
       ],
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.17.tgz",
-      "integrity": "sha512-XcSGTQcWFQS2jx3lZtQi7cQmDYLrpLRyz1Ns1DzZCtn898cWfm5Icx/DEWNcTU+T+tyPV89RQtDnI7qL2PObPg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz",
+      "integrity": "sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.17.tgz",
-      "integrity": "sha512-RNLCDmLP5kCWAJR+ItLM3cHxzXRTe4N00TQyQiimq+lyqVqZWGPAvcyfUBM0isE79eEZhIuGN09rAz8EL5KdLA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz",
+      "integrity": "sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.17.tgz",
-      "integrity": "sha512-PAXswI5+cQq3Pann7FNdcpSUrhrql3wKjj3gVkmuz6OHhqqYxKvi6GgRBoaHjaG22HV/ZZEgF9TlS+9ftHVigA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz",
+      "integrity": "sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.17.tgz",
-      "integrity": "sha512-V63egsWKnx/4V0FMYkr9NXWrKTB5qFftKGKuZKFIrAkO/7EWLFnbBZNM1CvJ6Sis+XBdPws2YQSHF1Gqf1oj/Q==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz",
+      "integrity": "sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.17.tgz",
-      "integrity": "sha512-YtUXLdVnd6YBSYlZODjWzH+KzbaubV0YVd6UxSfoFfa5PtNJNaW+1i+Hcmjpg2nEe0YXUCNF5bkKy1NnBv1y7Q==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz",
+      "integrity": "sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg==",
       "cpu": [
         "arm64"
       ],
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.17.tgz",
-      "integrity": "sha512-yczSLRbDdReCO74Yfc5tKG0izzm+lPMYyO1fFTcn0QNwnKmc3K+HdxZWLGKg4pZVte7XVgcFku7TIZNbWEJdeQ==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz",
+      "integrity": "sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA==",
       "cpu": [
         "ia32"
       ],
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.17.tgz",
-      "integrity": "sha512-FNZw7H3aqhF9OyRQbDDnzUApDXfC1N6fgBhkqEO2jvYCJ+DxMTfZVqg3AX0R1khg1wHTBRD5SdcibSJ+XF6bFg==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz",
+      "integrity": "sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw==",
       "cpu": [
         "x64"
       ],
       }
     },
     "node_modules/@eslint-community/regexpp": {
-      "version": "4.5.0",
-      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz",
-      "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==",
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz",
+      "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==",
       "dev": true,
       "engines": {
         "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
       }
     },
     "node_modules/@eslint/eslintrc": {
-      "version": "2.0.2",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz",
-      "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==",
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz",
+      "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==",
       "dev": true,
       "dependencies": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
-        "espree": "^9.5.1",
+        "espree": "^9.6.0",
         "globals": "^13.19.0",
         "ignore": "^5.2.0",
         "import-fresh": "^3.2.1",
       }
     },
     "node_modules/@eslint/js": {
-      "version": "8.38.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz",
-      "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==",
+      "version": "8.47.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz",
+      "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
     "node_modules/@humanwhocodes/config-array": {
-      "version": "0.11.8",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
-      "integrity": "sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==",
+      "version": "0.11.10",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz",
+      "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==",
       "dev": true,
       "dependencies": {
         "@humanwhocodes/object-schema": "^1.2.1",
       }
     },
     "node_modules/@lezer/generator": {
-      "version": "1.2.2",
-      "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.2.2.tgz",
-      "integrity": "sha512-O//eH9jTPM1GnbZruuD23xU68Pkuragonn1DEIom4Kt/eJN/QFt7Vzvp1YjV/XBmoUKC+2ySPgrA5fMF9FMM2g==",
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.4.2.tgz",
+      "integrity": "sha512-6fRwiGnx6Sv2I25YKeC5Mw9LtIA3Yq4FmVdRk0NP+M8tCsLUHSJd4KzrEzYqW3SKqOR/ZRlGiw+HAXMRygyCZA==",
       "dev": true,
       "dependencies": {
         "@lezer/common": "^1.0.2",
         "@lezer/lr": "^1.3.0"
       },
       "bin": {
-        "lezer-generator": "dist/lezer-generator.cjs"
+        "lezer-generator": "src/lezer-generator.cjs"
       }
     },
     "node_modules/@lezer/highlight": {
-      "version": "1.1.4",
-      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.4.tgz",
-      "integrity": "sha512-IECkFmw2l7sFcYXrV8iT9GeY4W0fU4CxX0WMwhmhMIVjoDdD1Hr6q3G2NqVtLg/yVe5n7i4menG3tJ2r4eCrPQ==",
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.1.6.tgz",
+      "integrity": "sha512-cmSJYa2us+r3SePpRCjN5ymCqCPv+zyXmDl0ciWtVaNiORT/MxM7ZgOMQZADD0o51qOaOg24qc/zBViOIwAjJg==",
       "dependencies": {
         "@lezer/common": "^1.0.0"
       }
       "dev": true
     },
     "node_modules/acorn": {
-      "version": "8.8.2",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
-      "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
+      "version": "8.10.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz",
+      "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==",
       "dev": true,
       "bin": {
         "acorn": "bin/acorn"
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array.prototype.findlastindex": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz",
+      "integrity": "sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4",
+        "es-shim-unscopables": "^1.0.0",
+        "get-intrinsic": "^1.1.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
     "node_modules/array.prototype.flat": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz",
       }
     },
     "node_modules/esbuild": {
-      "version": "0.17.17",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.17.tgz",
-      "integrity": "sha512-/jUywtAymR8jR4qsa2RujlAF7Krpt5VWi72Q2yuLD4e/hvtNcFQ0I1j8m/bxq238pf3/0KO5yuXNpuLx8BE1KA==",
+      "version": "0.19.2",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.2.tgz",
+      "integrity": "sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
         "node": ">=12"
       },
       "optionalDependencies": {
-        "@esbuild/android-arm": "0.17.17",
-        "@esbuild/android-arm64": "0.17.17",
-        "@esbuild/android-x64": "0.17.17",
-        "@esbuild/darwin-arm64": "0.17.17",
-        "@esbuild/darwin-x64": "0.17.17",
-        "@esbuild/freebsd-arm64": "0.17.17",
-        "@esbuild/freebsd-x64": "0.17.17",
-        "@esbuild/linux-arm": "0.17.17",
-        "@esbuild/linux-arm64": "0.17.17",
-        "@esbuild/linux-ia32": "0.17.17",
-        "@esbuild/linux-loong64": "0.17.17",
-        "@esbuild/linux-mips64el": "0.17.17",
-        "@esbuild/linux-ppc64": "0.17.17",
-        "@esbuild/linux-riscv64": "0.17.17",
-        "@esbuild/linux-s390x": "0.17.17",
-        "@esbuild/linux-x64": "0.17.17",
-        "@esbuild/netbsd-x64": "0.17.17",
-        "@esbuild/openbsd-x64": "0.17.17",
-        "@esbuild/sunos-x64": "0.17.17",
-        "@esbuild/win32-arm64": "0.17.17",
-        "@esbuild/win32-ia32": "0.17.17",
-        "@esbuild/win32-x64": "0.17.17"
+        "@esbuild/android-arm": "0.19.2",
+        "@esbuild/android-arm64": "0.19.2",
+        "@esbuild/android-x64": "0.19.2",
+        "@esbuild/darwin-arm64": "0.19.2",
+        "@esbuild/darwin-x64": "0.19.2",
+        "@esbuild/freebsd-arm64": "0.19.2",
+        "@esbuild/freebsd-x64": "0.19.2",
+        "@esbuild/linux-arm": "0.19.2",
+        "@esbuild/linux-arm64": "0.19.2",
+        "@esbuild/linux-ia32": "0.19.2",
+        "@esbuild/linux-loong64": "0.19.2",
+        "@esbuild/linux-mips64el": "0.19.2",
+        "@esbuild/linux-ppc64": "0.19.2",
+        "@esbuild/linux-riscv64": "0.19.2",
+        "@esbuild/linux-s390x": "0.19.2",
+        "@esbuild/linux-x64": "0.19.2",
+        "@esbuild/netbsd-x64": "0.19.2",
+        "@esbuild/openbsd-x64": "0.19.2",
+        "@esbuild/sunos-x64": "0.19.2",
+        "@esbuild/win32-arm64": "0.19.2",
+        "@esbuild/win32-ia32": "0.19.2",
+        "@esbuild/win32-x64": "0.19.2"
       }
     },
     "node_modules/escape-string-regexp": {
       }
     },
     "node_modules/eslint": {
-      "version": "8.38.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz",
-      "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==",
+      "version": "8.47.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz",
+      "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==",
       "dev": true,
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
-        "@eslint-community/regexpp": "^4.4.0",
-        "@eslint/eslintrc": "^2.0.2",
-        "@eslint/js": "8.38.0",
-        "@humanwhocodes/config-array": "^0.11.8",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.2",
+        "@eslint/js": "^8.47.0",
+        "@humanwhocodes/config-array": "^0.11.10",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@nodelib/fs.walk": "^1.2.8",
-        "ajv": "^6.10.0",
+        "ajv": "^6.12.4",
         "chalk": "^4.0.0",
         "cross-spawn": "^7.0.2",
         "debug": "^4.3.2",
         "doctrine": "^3.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.1",
-        "eslint-visitor-keys": "^3.4.0",
-        "espree": "^9.5.1",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
         "esquery": "^1.4.2",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "find-up": "^5.0.0",
         "glob-parent": "^6.0.2",
         "globals": "^13.19.0",
-        "grapheme-splitter": "^1.0.4",
+        "graphemer": "^1.4.0",
         "ignore": "^5.2.0",
-        "import-fresh": "^3.0.0",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
         "is-path-inside": "^3.0.3",
-        "js-sdsl": "^4.1.4",
         "js-yaml": "^4.1.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "levn": "^0.4.1",
         "lodash.merge": "^4.6.2",
         "minimatch": "^3.1.2",
         "natural-compare": "^1.4.0",
-        "optionator": "^0.9.1",
+        "optionator": "^0.9.3",
         "strip-ansi": "^6.0.1",
-        "strip-json-comments": "^3.1.0",
         "text-table": "^0.2.0"
       },
       "bin": {
       }
     },
     "node_modules/eslint-config-airbnb-base/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
       }
     },
     "node_modules/eslint-plugin-import": {
-      "version": "2.27.5",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz",
-      "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==",
+      "version": "2.28.1",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.28.1.tgz",
+      "integrity": "sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==",
       "dev": true,
       "dependencies": {
         "array-includes": "^3.1.6",
+        "array.prototype.findlastindex": "^1.2.2",
         "array.prototype.flat": "^1.3.1",
         "array.prototype.flatmap": "^1.3.1",
         "debug": "^3.2.7",
         "doctrine": "^2.1.0",
         "eslint-import-resolver-node": "^0.3.7",
-        "eslint-module-utils": "^2.7.4",
+        "eslint-module-utils": "^2.8.0",
         "has": "^1.0.3",
-        "is-core-module": "^2.11.0",
+        "is-core-module": "^2.13.0",
         "is-glob": "^4.0.3",
         "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.6",
+        "object.groupby": "^1.0.0",
         "object.values": "^1.1.6",
-        "resolve": "^1.22.1",
-        "semver": "^6.3.0",
-        "tsconfig-paths": "^3.14.1"
+        "semver": "^6.3.1",
+        "tsconfig-paths": "^3.14.2"
       },
       "engines": {
         "node": ">=4"
       }
     },
     "node_modules/eslint-plugin-import/node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "dev": true,
       "bin": {
         "semver": "bin/semver.js"
       }
     },
     "node_modules/eslint-scope": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz",
-      "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==",
+      "version": "7.2.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+      "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
       "dev": true,
       "dependencies": {
         "esrecurse": "^4.3.0",
       }
     },
     "node_modules/eslint-visitor-keys": {
-      "version": "3.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz",
-      "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==",
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
       "dev": true,
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
     "node_modules/espree": {
-      "version": "9.5.1",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz",
-      "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==",
+      "version": "9.6.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+      "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
       "dev": true,
       "dependencies": {
-        "acorn": "^8.8.0",
+        "acorn": "^8.9.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^3.4.0"
+        "eslint-visitor-keys": "^3.4.1"
       },
       "engines": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
     "node_modules/get-intrinsic": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
-      "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
+      "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==",
       "dev": true,
       "dependencies": {
         "function-bind": "^1.1.1",
         "has": "^1.0.3",
+        "has-proto": "^1.0.1",
         "has-symbols": "^1.0.3"
       },
       "funding": {
       }
     },
     "node_modules/globals": {
-      "version": "13.20.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz",
-      "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==",
+      "version": "13.21.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz",
+      "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==",
       "dev": true,
       "dependencies": {
         "type-fest": "^0.20.2"
       "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
       "dev": true
     },
-    "node_modules/grapheme-splitter": {
-      "version": "1.0.4",
-      "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
-      "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
       "dev": true
     },
     "node_modules/has": {
       }
     },
     "node_modules/is-core-module": {
-      "version": "2.12.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz",
-      "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==",
+      "version": "2.13.0",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
+      "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==",
       "dev": true,
       "dependencies": {
         "has": "^1.0.3"
       "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
       "dev": true
     },
-    "node_modules/js-sdsl": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz",
-      "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==",
-      "dev": true,
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/js-sdsl"
-      }
-    },
     "node_modules/js-yaml": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
         "node": ">= 0.4"
       }
     },
+    "node_modules/object.fromentries": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.6.tgz",
+      "integrity": "sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.1.4",
+        "es-abstract": "^1.20.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/object.groupby": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.0.tgz",
+      "integrity": "sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw==",
+      "dev": true,
+      "dependencies": {
+        "call-bind": "^1.0.2",
+        "define-properties": "^1.2.0",
+        "es-abstract": "^1.21.2",
+        "get-intrinsic": "^1.2.1"
+      }
+    },
     "node_modules/object.values": {
       "version": "1.1.6",
       "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz",
       }
     },
     "node_modules/optionator": {
-      "version": "0.9.1",
-      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
-      "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+      "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
       "dev": true,
       "dependencies": {
+        "@aashutoshrathi/word-wrap": "^1.2.3",
         "deep-is": "^0.1.3",
         "fast-levenshtein": "^2.0.6",
         "levn": "^0.4.1",
         "prelude-ls": "^1.2.1",
-        "type-check": "^0.4.0",
-        "word-wrap": "^1.2.3"
+        "type-check": "^0.4.0"
       },
       "engines": {
         "node": ">= 0.8.0"
       }
     },
     "node_modules/sass": {
-      "version": "1.62.0",
-      "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.0.tgz",
-      "integrity": "sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg==",
+      "version": "1.66.1",
+      "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz",
+      "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==",
       "dev": true,
       "dependencies": {
         "chokidar": ">=3.0.0 <4.0.0",
       }
     },
     "node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "dev": true,
       "bin": {
         "semver": "bin/semver"
         "url": "https://github.com/sponsors/ljharb"
       }
     },
-    "node_modules/word-wrap": {
-      "version": "1.2.3",
-      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
-      "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
     "node_modules/wrap-ansi": {
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
index 499e32833e3f3abc009b914f3d59dddfa89db7f5..f9446ab3b0fccb281071735ce7d33d86ec592996 100644 (file)
     "fix": "eslint --fix \"resources/**/*.js\" \"resources/**/*.mjs\""
   },
   "devDependencies": {
-    "@lezer/generator": "^1.2.2",
+    "@lezer/generator": "^1.4.2",
     "chokidar-cli": "^3.0",
-    "esbuild": "^0.17.16",
-    "eslint": "^8.38.0",
+    "esbuild": "^0.19",
+    "eslint": "^8.47.0",
     "eslint-config-airbnb-base": "^15.0.0",
-    "eslint-plugin-import": "^2.27.5",
+    "eslint-plugin-import": "^2.28.1",
     "livereload": "^0.9.3",
     "npm-run-all": "^4.1.5",
     "punycode": "^2.3.0",
-    "sass": "^1.62.0"
+    "sass": "^1.66.1"
   },
   "dependencies": {
-    "@codemirror/commands": "^6.2.2",
-    "@codemirror/lang-css": "^6.1.1",
-    "@codemirror/lang-html": "^6.4.3",
-    "@codemirror/lang-javascript": "^6.1.6",
+    "@codemirror/commands": "^6.2.4",
+    "@codemirror/lang-css": "^6.2.1",
+    "@codemirror/lang-html": "^6.4.5",
+    "@codemirror/lang-javascript": "^6.1.9",
     "@codemirror/lang-json": "^6.0.1",
-    "@codemirror/lang-markdown": "^6.1.1",
+    "@codemirror/lang-markdown": "^6.2.0",
     "@codemirror/lang-php": "^6.0.1",
     "@codemirror/lang-xml": "^6.0.2",
-    "@codemirror/language": "^6.6.0",
-    "@codemirror/legacy-modes": "^6.3.2",
-    "@codemirror/state": "^6.2.0",
-    "@codemirror/theme-one-dark": "^6.1.1",
-    "@codemirror/view": "^6.9.4",
-    "@lezer/highlight": "^1.1.4",
+    "@codemirror/language": "^6.9.0",
+    "@codemirror/legacy-modes": "^6.3.3",
+    "@codemirror/state": "^6.2.1",
+    "@codemirror/theme-one-dark": "^6.1.2",
+    "@codemirror/view": "^6.16.0",
+    "@lezer/highlight": "^1.1.6",
     "@ssddanbrown/codemirror-lang-smarty": "^1.0.0",
     "@ssddanbrown/codemirror-lang-twig": "^1.0.0",
     "codemirror": "^6.0.1",
diff --git a/resources/icons/user-preferences.svg b/resources/icons/user-preferences.svg
new file mode 100644 (file)
index 0000000..5ae1773
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 24 24"><g><g><circle cx="10" cy="8" r="4"/><path d="M10.67,13.02C10.45,13.01,10.23,13,10,13c-2.42,0-4.68,0.67-6.61,1.82C2.51,15.34,2,16.32,2,17.35V20h9.26 C10.47,18.87,10,17.49,10,16C10,14.93,10.25,13.93,10.67,13.02z"/><path d="M20.75,16c0-0.22-0.03-0.42-0.06-0.63l1.14-1.01l-1-1.73l-1.45,0.49c-0.32-0.27-0.68-0.48-1.08-0.63L18,11h-2l-0.3,1.49 c-0.4,0.15-0.76,0.36-1.08,0.63l-1.45-0.49l-1,1.73l1.14,1.01c-0.03,0.21-0.06,0.41-0.06,0.63s0.03,0.42,0.06,0.63l-1.14,1.01 l1,1.73l1.45-0.49c0.32,0.27,0.68,0.48,1.08,0.63L16,21h2l0.3-1.49c0.4-0.15,0.76-0.36,1.08-0.63l1.45,0.49l1-1.73l-1.14-1.01 C20.72,16.42,20.75,16.22,20.75,16z M17,18c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S18.1,18,17,18z"/></g></g></svg>
\ No newline at end of file
diff --git a/resources/icons/watch-ignore.svg b/resources/icons/watch-ignore.svg
new file mode 100644 (file)
index 0000000..2c6ffc2
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/></svg>
\ No newline at end of file
similarity index 85%
rename from resources/icons/view.svg
rename to resources/icons/watch.svg
index c95c8875cdc36e2c9d7fe4b788628bdcf7c96c29..0be661912d73ebbe88db0a7297bc1a98e692299e 100644 (file)
@@ -1,4 +1,3 @@
 <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
-    <path d="M0 0h24v24H0z" fill="none"/>
     <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>
 </svg>
\ No newline at end of file
index b68f332b6f13a04a2d2db7fd706cfae0d42dc8bd..2c5919a37e755d9fa8e3facc0a8f226612dff468 100644 (file)
@@ -132,6 +132,7 @@ export class Dropdown extends Component {
 
         onSelect(this.toggle, event => {
             event.stopPropagation();
+            event.preventDefault();
             this.show(event);
             if (event instanceof KeyboardEvent) {
                 keyboardNavHandler.focusNext();
index a8604b81b18240edf43f32a2f0965d61b571f3d9..50776ea28143f5dce6cc527c74c16441b1e4f42b 100644 (file)
@@ -353,7 +353,7 @@ body.flexbox {
   margin-inline-end: $-xl;
   grid-template-columns: 1fr 4fr 1fr;
   grid-template-areas: "a b c";
-  grid-column-gap: $-xxl;
+  grid-column-gap: $-xl;
   .tri-layout-right {
     grid-area: c;
     min-width: 0;
@@ -378,6 +378,14 @@ body.flexbox {
     padding-inline-end: $-l;
   }
 }
+@include between($xxl, $xxxl) {
+  .tri-layout-container {
+    grid-template-columns: 1fr calc(940px + (2 * $-m)) 1fr;
+    grid-column-gap: $-s;
+    margin-inline-start: $-m;
+    margin-inline-end: $-m;
+  }
+}
 @include between($l, $xxl) {
   .tri-layout-left {
     position: sticky;
index ad0803e712596d4a753da39fa9401416b4907400..323551196585fbac2ec961016a53e7ba61a8e668 100644 (file)
@@ -672,7 +672,7 @@ ul.pagination {
   @include lightDark(color, #555, #eee);
   fill: currentColor;
   text-align: start !important;
-  max-height: 500px;
+  max-height: 80vh;
   overflow-y: auto;
   &.anchor-left {
     inset-inline-end: auto;
@@ -681,6 +681,10 @@ ul.pagination {
   &.wide {
     min-width: 220px;
   }
+  &.xl-limited {
+    width: 280px;
+    max-width: 100%;
+  }
   .text-muted {
     color: #999;
     fill: #999;
@@ -705,6 +709,11 @@ ul.pagination {
     white-space: nowrap;
     line-height: 1.4;
     cursor: pointer;
+    &.break-text {
+      white-space: normal;
+      word-wrap: break-word;
+      overflow-wrap: break-word;
+    }
     &:hover, &:focus {
       text-decoration: none;
       background-color: var(--color-primary-light);
index 7cade9607b92b2c2cdbaa1f02b24f110257fd2d1..a3e6f09ac53a7768e780d8f9c8a358930841fed5 100644 (file)
@@ -365,6 +365,7 @@ li.checkbox-item, li.task-list-item {
 }
 
 .break-text {
+  white-space: normal;
   word-wrap: break-word;
   overflow-wrap: break-word;
 }
index a3598e29c4cfee6da16dcb6e82758f38f1cad3a8..35586bf58303cbd3662bff609191b3f14371ade1 100644 (file)
@@ -2,6 +2,7 @@
 ///////////////
 
 // Screen breakpoints
+$xxxl: 1700px;
 $xxl: 1400px;
 $xl: 1100px;
 $l: 1000px;
index 8bb41c18b8f0a64b1e89000305fd79312ac01e29..75b01a379242cd97666172d8f7f0da72fc984a27 100644 (file)
@@ -70,7 +70,7 @@
     <div class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="blended-links">
-            @include('entities.meta', ['entity' => $book])
+            @include('entities.meta', ['entity' => $book, 'watchOptions' => $watchOptions])
             @if($book->hasPermissions())
                 <div class="active-restriction">
                     @if(userCan('restrictions-manage', $book))
 
             <hr class="primary-background">
 
+            @if($watchOptions->canWatch() && !$watchOptions->isWatching())
+                @include('entities.watch-action', ['entity' => $book])
+            @endif
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $book])
             @endif
index de52888dda2f63eeee4b77fd4d61644d532cb912..3cb512ccc532ae565ccc392a0ab75e0f18a099d5 100644 (file)
@@ -67,7 +67,7 @@
     <div class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="blended-links">
-            @include('entities.meta', ['entity' => $chapter])
+            @include('entities.meta', ['entity' => $chapter, 'watchOptions' => $watchOptions])
 
             @if($book->hasPermissions())
                 <div class="active-restriction">
 
             <hr class="primary-background"/>
 
+            @if($watchOptions->canWatch() && !$watchOptions->isWatching())
+                @include('entities.watch-action', ['entity' => $chapter])
+            @endif
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $chapter])
             @endif
index a8b711595ad85dcce6d80ca59246515fd238f91a..97a411d84541684150f5b81bd4e6b6c8e9a8825f 100644 (file)
                         </li>
                         <li><hr></li>
                         <li>
-                            <a href="{{ url('/preferences/shortcuts') }}" class="icon-item">
-                                @icon('shortcuts')
-                                <div>{{ trans('preferences.shortcuts') }}</div>
+                            <a href="{{ url('/preferences') }}" class="icon-item">
+                                @icon('user-preferences')
+                                <div>{{ trans('preferences.preferences') }}</div>
                             </a>
                         </li>
                         <li>
diff --git a/resources/views/entities/icon-link.blade.php b/resources/views/entities/icon-link.blade.php
new file mode 100644 (file)
index 0000000..a3e95fd
--- /dev/null
@@ -0,0 +1,7 @@
+<a href="{{ $entity->getUrl() }}" class="flex-container-row items-center">
+    <span role="presentation"
+          class="icon flex-none text-{{$entity->getType()}}">@icon($entity->getType())</span>
+    <div class="flex text-{{ $entity->getType() }}">
+        {{ $entity->name }}
+    </div>
+</a>
\ No newline at end of file
index 7ad61f1b1e711f8eb95145cf4d37aff8a2f20e30..2298be8bb27689ec2435828f7457ff2048942b51 100644 (file)
             </div>
         </a>
     @endif
+
+    @if($watchOptions?->canWatch())
+        @if($watchOptions->isWatching())
+            @include('entities.watch-controls', [
+                'entity' => $entity,
+                'watchLevel' => $watchOptions->getWatchLevel(),
+                'label' => trans('entities.watch_detail_' . $watchOptions->getWatchLevel()),
+                'ignoring' => $watchOptions->getWatchLevel() === 'ignore',
+            ])
+        @elseif($watchedParent = $watchOptions->getWatchedParent())
+            @include('entities.watch-controls', [
+                'entity' => $entity,
+                'watchLevel' => $watchOptions->getWatchLevel(),
+                'label' => trans('entities.watch_detail_parent_' . $watchedParent->type . ($watchedParent->ignoring() ? '_ignore' : '')),
+                'ignoring' => $watchedParent->ignoring(),
+            ])
+        @endif
+    @endif
 </div>
\ No newline at end of file
diff --git a/resources/views/entities/watch-action.blade.php b/resources/views/entities/watch-action.blade.php
new file mode 100644 (file)
index 0000000..34e2878
--- /dev/null
@@ -0,0 +1,13 @@
+<form action="{{ url('/watching/update') }}" method="POST">
+    {{ csrf_field() }}
+    {{ method_field('PUT') }}
+    <input type="hidden" name="type" value="{{ get_class($entity) }}">
+    <input type="hidden" name="id" value="{{ $entity->id }}">
+    <button type="submit"
+            name="level"
+            value="updates"
+            class="icon-list-item text-link">
+        <span>@icon('watch')</span>
+        <span>{{ trans('entities.watch') }}</span>
+    </button>
+</form>
\ No newline at end of file
diff --git a/resources/views/entities/watch-controls.blade.php b/resources/views/entities/watch-controls.blade.php
new file mode 100644 (file)
index 0000000..d24e120
--- /dev/null
@@ -0,0 +1,46 @@
+<div component="dropdown"
+     class="dropdown-container block my-xxs">
+    <a refs="dropdown@toggle" href="#" class="entity-meta-item my-none">
+        @icon(($ignoring ? 'watch-ignore' : 'watch'))
+        <span>{{ $label }}</span>
+    </a>
+    <form action="{{ url('/watching/update') }}" method="POST">
+        {{ method_field('PUT') }}
+        {{ csrf_field() }}
+        <input type="hidden" name="type" value="{{ get_class($entity) }}">
+        <input type="hidden" name="id" value="{{ $entity->id }}">
+
+        <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
+            @foreach(\BookStack\Activity\WatchLevels::allSuitedFor($entity) as $option => $value)
+                <li>
+                    <button name="level" value="{{ $option }}" class="icon-item">
+                        @if($watchLevel === $option)
+                            <span class="text-pos pt-m"
+                                  title="{{ trans('common.status_active') }}">@icon('check-circle')</span>
+                        @else
+                            <span title="{{ trans('common.status_inactive') }}"></span>
+                        @endif
+                        <div class="break-text">
+                            <div class="mb-xxs"><strong>{{ trans('entities.watch_title_' . $option) }}</strong></div>
+                            <div class="text-muted text-small">
+                                @if(trans()->has('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()))
+                                    {{ trans('entities.watch_desc_' . $option . '_' . $entity->getMorphClass()) }}
+                                @else
+                                    {{ trans('entities.watch_desc_' . $option) }}
+                                @endif
+                            </div>
+                        </div>
+                    </button>
+                </li>
+                <li>
+                    <hr class="my-none">
+                </li>
+            @endforeach
+            <li>
+                <a href="{{ url('/preferences/notifications') }}"
+                   target="_blank"
+                   class="text-item text-muted text-small break-text">{{ trans('entities.watch_change_default') }}</a>
+            </li>
+        </ul>
+    </form>
+</div>
\ No newline at end of file
index 5c2e8674178b95e5ed2e3f16a4ffda0f77be7e1f..a47027d795e42d592529979d495f674245f5f310 100644 (file)
@@ -44,11 +44,11 @@ $inheriting - Boolean if the current row should be marked as inheriting default
                 'disabled' => $inheriting
             ])
         </div>
-        @if($entityType !== 'page' && $entityType !== 'bookshelf')
+        @if($entityType !== 'page')
             <div class="px-l">
                 @include('form.custom-checkbox', [
                     'name' =>  'permissions[' . $role->id . '][create]',
-                    'label' => trans('common.create'),
+                    'label' => trans('common.create') . ($entityType === 'bookshelf' ? ' *'  : ''),
                     'value' => 'true',
                     'checked' => $permission->create,
                     'disabled' => $inheriting
index 8eddac8dea0e9a1300ea60e941a7b578af4c0cbc..cf5314ad4a17d1138fa5d00ef98da45788edd075 100644 (file)
 
     <hr class="mb-m">
 
-    <div class="text-right">
-        <a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
-        <button type="submit" class="button">{{ trans('entities.permissions_save') }}</button>
+    <div class="flex-container-row justify-space-between gap-m wrap">
+        <div class="flex min-width-m">
+            @if($model instanceof \BookStack\Entities\Models\Bookshelf)
+                <p class="small text-muted mb-none">
+                    * {{ trans('entities.shelves_permissions_create') }}
+                </p>
+            @endif
+        </div>
+        <div class="text-right">
+            <a href="{{ $model->getUrl() }}" class="button outline">{{ trans('common.cancel') }}</a>
+            <button type="submit" class="button">{{ trans('entities.permissions_save') }}</button>
+        </div>
     </div>
 </form>
\ No newline at end of file
index b3208c21131ef67890167b8fd8ec724e5191b45e..c0831ef595c7931e80446b7c49c37677f8ac2032 100644 (file)
@@ -4,7 +4,7 @@
     <div id="revision-details" class="entity-details mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="body text-small text-muted">
-            @include('entities.meta', ['entity' => $revision])
+            @include('entities.meta', ['entity' => $revision, 'watchOptions' => null])
         </div>
     </div>
 @stop
index ae6b273fe4fe1d208a2d0eade98e0933d67f7392..1cbb819804f27a09ee934a24ffb047ac3610d80f 100644 (file)
@@ -81,7 +81,7 @@
     <div id="page-details" class="entity-details mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="blended-links">
-            @include('entities.meta', ['entity' => $page])
+            @include('entities.meta', ['entity' => $page, 'watchOptions' => $watchOptions])
 
             @if($book->hasPermissions())
                 <div class="active-restriction">
 
             <hr class="primary-background"/>
 
+            @if($watchOptions->canWatch() && !$watchOptions->isWatching())
+                @include('entities.watch-action', ['entity' => $page])
+            @endif
             @if(signedInUser())
                 @include('entities.favourite-action', ['entity' => $page])
             @endif
index dd0874c9226ab029de817dcb27dcc12948342034..9f0572c1ab400f7209d2d0a8f5dc48ab97145039 100644 (file)
                                 :</strong> {{ $activity->type }}</div>
                         <div class="flex-3 px-m py-xxs min-width-l">
                             @if($activity->entity)
-                                <a href="{{ $activity->entity->getUrl() }}" class="flex-container-row items-center">
-                                    <span role="presentation"
-                                          class="icon flex-none text-{{$activity->entity->getType()}}">@icon($activity->entity->getType())</span>
-                                    <div class="flex text-{{ $activity->entity->getType() }}">
-                                        {{ $activity->entity->name }}
-                                    </div>
-                                </a>
+                                @include('entities.icon-link', ['entity' => $activity->entity])
                             @elseif($activity->detail && $activity->isForEntity())
                                 <div>
                                     {{ trans('settings.audit_deleted_item') }} <br>
index ac5c320d21571578e2c89b11d8b706930ecd6723..9fa76f2bfd7741ebd964e1cb1bff6282e4175500 100644 (file)
@@ -38,6 +38,7 @@
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'content-export', 'label' => trans('settings.role_export_content')])</div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'editor-change', 'label' => trans('settings.role_editor_change')])</div>
+                <div>@include('settings.roles.parts.checkbox', ['permission' => 'receive-notifications', 'label' => trans('settings.role_notifications')])</div>
             </div>
             <div>
                 <div>@include('settings.roles.parts.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
index 25b7a14fc1f1d0e983f60a061afe69c200f49ec0..8019a557f601ea17b589681b146780f1a842747e 100644 (file)
@@ -79,7 +79,7 @@
     <div id="details" class="mb-xl">
         <h5>{{ trans('common.details') }}</h5>
         <div class="blended-links">
-            @include('entities.meta', ['entity' => $shelf])
+            @include('entities.meta', ['entity' => $shelf, 'watchOptions' => null])
             @if($shelf->hasPermissions())
                 <div class="active-restriction">
                     @if(userCan('restrictions-manage', $shelf))
diff --git a/resources/views/users/preferences/index.blade.php b/resources/views/users/preferences/index.blade.php
new file mode 100644 (file)
index 0000000..6896283
--- /dev/null
@@ -0,0 +1,41 @@
+@extends('layouts.simple')
+
+@section('body')
+    <div class="container small my-xl">
+
+        <section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row">
+            <div>
+                <h2 class="list-heading">{{ trans('preferences.shortcuts_interface') }}</h2>
+                <p class="text-muted">{{ trans('preferences.shortcuts_overview_desc') }}</p>
+            </div>
+            <div class="text-right">
+                <a href="{{ url('/preferences/shortcuts') }}" class="button outline">{{ trans('common.manage') }}</a>
+            </div>
+        </section>
+
+        @if(signedInUser() && userCan('receive-notifications'))
+            <section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row">
+                <div>
+                    <h2 class="list-heading">{{ trans('preferences.notifications') }}</h2>
+                    <p class="text-muted">{{ trans('preferences.notifications_desc') }}</p>
+                </div>
+                <div class="text-right">
+                    <a href="{{ url('/preferences/notifications') }}" class="button outline">{{ trans('common.manage') }}</a>
+                </div>
+            </section>
+        @endif
+
+        @if(signedInUser())
+            <section class="card content-wrap auto-height items-center justify-space-between gap-m flex-container-row">
+                <div>
+                    <h2 class="list-heading">{{ trans('settings.users_edit_profile') }}</h2>
+                    <p class="text-muted">{{ trans('preferences.profile_overview_desc') }}</p>
+                </div>
+                <div class="text-right">
+                    <a href="{{ user()->getEditUrl() }}" class="button outline">{{ trans('common.manage') }}</a>
+                </div>
+            </section>
+        @endif
+
+    </div>
+@stop
diff --git a/resources/views/users/preferences/notifications.blade.php b/resources/views/users/preferences/notifications.blade.php
new file mode 100644 (file)
index 0000000..ae89c08
--- /dev/null
@@ -0,0 +1,73 @@
+@extends('layouts.simple')
+
+@section('body')
+    <div class="container small my-xl">
+
+        <section class="card content-wrap auto-height">
+            <form action="{{ url('/preferences/notifications') }}" method="post">
+                {{ method_field('put') }}
+                {{ csrf_field() }}
+
+                <h1 class="list-heading">{{ trans('preferences.notifications') }}</h1>
+                <p class="text-small text-muted">{{ trans('preferences.notifications_desc') }}</p>
+
+                <div class="flex-container-row wrap justify-space-between pb-m">
+                    <div class="toggle-switch-list min-width-l">
+                        <div>
+                            @include('form.toggle-switch', [
+                                'name' => 'preferences[own-page-changes]',
+                                'value' => $preferences->notifyOnOwnPageChanges(),
+                                'label' => trans('preferences.notifications_opt_own_page_changes'),
+                            ])
+                        </div>
+                        <div>
+                            @include('form.toggle-switch', [
+                                'name' => 'preferences[own-page-comments]',
+                                'value' => $preferences->notifyOnOwnPageComments(),
+                                'label' => trans('preferences.notifications_opt_own_page_comments'),
+                            ])
+                        </div>
+                        <div>
+                            @include('form.toggle-switch', [
+                                'name' => 'preferences[comment-replies]',
+                                'value' => $preferences->notifyOnCommentReplies(),
+                                'label' => trans('preferences.notifications_opt_comment_replies'),
+                            ])
+                        </div>
+                    </div>
+
+                    <div class="mt-auto">
+                        <button class="button">{{ trans('preferences.notifications_save') }}</button>
+                    </div>
+                </div>
+
+            </form>
+        </section>
+
+        <section class="card content-wrap auto-height">
+            <h2 class="list-heading">{{ trans('preferences.notifications_watched') }}</h2>
+            <p class="text-small text-muted">{{ trans('preferences.notifications_watched_desc') }}</p>
+
+            @if($watches->isEmpty())
+                <p class="text-muted italic">{{ trans('common.no_items') }}</p>
+            @else
+                <div class="item-list">
+                    @foreach($watches as $watch)
+                        <div class="flex-container-row justify-space-between item-list-row items-center wrap px-m py-s">
+                            <div class="py-xs px-s min-width-m">
+                                @include('entities.icon-link', ['entity' => $watch->watchable])
+                            </div>
+                            <div class="py-xs min-width-m text-m-right px-m">
+                                @icon('watch' . ($watch->ignoring() ? '-ignore' : ''))
+                                {{ trans('entities.watch_title_' . $watch->getLevelName()) }}
+                            </div>
+                        </div>
+                    @endforeach
+                </div>
+            @endif
+
+            <div class="my-m">{{ $watches->links() }}</div>
+        </section>
+
+    </div>
+@stop
index f73b87b597853ccf5becce9eacbf5c4c7db68048..88cdbd89024e4c1372e1b34b5016282dac4da6a9 100644 (file)
@@ -30,7 +30,7 @@
 $style = [
     /* Layout ------------------------------ */
 
-    'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;',
+    'body' => 'margin: 0; padding: 0; width: 100%; background-color: #F2F4F6;color:#444444;',
     'email-wrapper' => 'width: 100%; margin: 0; padding: 0; background-color: #F2F4F6;',
 
     /* Masthead ----------------------- */
@@ -54,8 +54,8 @@ $style = [
 
     'anchor' => 'color: '.setting('app-color').';overflow-wrap: break-word;word-wrap: break-word;word-break: break-all;word-break:break-word;',
     'header-1' => 'margin-top: 0; color: #2F3133; font-size: 19px; font-weight: bold; text-align: left;',
-    'paragraph' => 'margin-top: 0; color: #74787E; font-size: 16px; line-height: 1.5em;',
-    'paragraph-sub' => 'margin-top: 0; color: #74787E; font-size: 12px; line-height: 1.5em;',
+    'paragraph' => 'margin-top: 0; color: #444444; font-size: 16px; line-height: 1.5em;',
+    'paragraph-sub' => 'margin-top: 0; color: #444444; font-size: 12px; line-height: 1.5em;',
     'paragraph-center' => 'text-align: center;',
 
     /* Buttons ------------------------------ */
@@ -147,7 +147,7 @@ $style = [
 
                                                     <!-- Outro -->
                                                     @foreach ($outroLines as $line)
-                                                        <p style="{{ $style['paragraph'] }}">
+                                                        <p style="{{ $style['paragraph-sub'] }}">
                                                             {{ $line }}
                                                         </p>
                                                     @endforeach
index 74ee74a2c77c39c009cf311b8a5bcd77f5b0a0fe..c7fc92fc77da3bf5819dcc75cd558ce02756700d 100644 (file)
@@ -194,6 +194,9 @@ Route::middleware('auth')->group(function () {
     Route::post('/favourites/add', [ActivityControllers\FavouriteController::class, 'add']);
     Route::post('/favourites/remove', [ActivityControllers\FavouriteController::class, 'remove']);
 
+    // Watching
+    Route::put('/watching/update', [ActivityControllers\WatchController::class, 'update']);
+
     // Other Pages
     Route::get('/', [HomeController::class, 'index']);
     Route::get('/home', [HomeController::class, 'index']);
@@ -228,9 +231,11 @@ Route::middleware('auth')->group(function () {
     Route::delete('/settings/users/{id}', [UserControllers\UserController::class, 'destroy']);
 
     // User Preferences
-    Route::redirect('/preferences', '/');
+    Route::get('/preferences', [UserControllers\UserPreferencesController::class, 'index']);
     Route::get('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'showShortcuts']);
     Route::put('/preferences/shortcuts', [UserControllers\UserPreferencesController::class, 'updateShortcuts']);
+    Route::get('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'showNotifications']);
+    Route::put('/preferences/notifications', [UserControllers\UserPreferencesController::class, 'updateNotifications']);
     Route::patch('/preferences/change-view/{type}', [UserControllers\UserPreferencesController::class, 'changeView']);
     Route::patch('/preferences/change-sort/{type}', [UserControllers\UserPreferencesController::class, 'changeSort']);
     Route::patch('/preferences/change-expansion/{type}', [UserControllers\UserPreferencesController::class, 'changeExpansion']);
index f2def087b0c1b13b7860847f51e968e838ba1609..fc49a524eef2c40e99b5206cca6b241c50518c47 100644 (file)
@@ -6,6 +6,8 @@ use BookStack\Activity\ActivityType;
 use BookStack\Activity\DispatchWebhookJob;
 use BookStack\Activity\Models\Webhook;
 use BookStack\Activity\Tools\ActivityLogger;
+use BookStack\Api\ApiToken;
+use BookStack\Entities\Models\PageRevision;
 use BookStack\Users\Models\User;
 use Illuminate\Http\Client\Request;
 use Illuminate\Support\Facades\Bus;
@@ -46,6 +48,24 @@ class WebhookCallTest extends TestCase
         Bus::assertNotDispatched(DispatchWebhookJob::class);
     }
 
+    public function test_webhook_runs_for_delete_actions()
+    {
+        $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
+        Http::fake([
+            '*' => Http::response('', 500),
+        ]);
+
+        $user = $this->users->newUser();
+        $resp = $this->asAdmin()->delete($user->getEditUrl());
+        $resp->assertRedirect('/settings/users');
+
+        /** @var ApiToken $apiToken */
+        $editor = $this->users->editor();
+        $apiToken = ApiToken::factory()->create(['user_id' => $editor]);
+        $resp = $this->delete($editor->getEditUrl('/api-tokens/' . $apiToken->id));
+        $resp->assertRedirect($editor->getEditUrl('#api_tokens'));
+    }
+
     public function test_failed_webhook_call_logs_error()
     {
         $logger = $this->withTestLogger();
@@ -120,7 +140,7 @@ class WebhookCallTest extends TestCase
         $activityLogger->add($event, $detail);
     }
 
-    protected function newWebhook(array $attrs = [], array $events = ['all']): Webhook
+    protected function newWebhook(array $attrs, array $events): Webhook
     {
         /** @var Webhook $webhook */
         $webhook = Webhook::factory()->create($attrs);
diff --git a/tests/Activity/WatchTest.php b/tests/Activity/WatchTest.php
new file mode 100644 (file)
index 0000000..fd86029
--- /dev/null
@@ -0,0 +1,332 @@
+<?php
+
+namespace Tests\Activity;
+
+use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
+use BookStack\Activity\Notifications\Messages\PageCreationNotification;
+use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
+use BookStack\Activity\Tools\UserEntityWatchOptions;
+use BookStack\Activity\WatchLevels;
+use BookStack\Entities\Models\Entity;
+use BookStack\Settings\UserNotificationPreferences;
+use Illuminate\Support\Facades\Notification;
+use Tests\TestCase;
+
+class WatchTest extends TestCase
+{
+    public function test_watch_action_exists_on_entity_unless_active()
+    {
+        $editor = $this->users->editor();
+        $this->actingAs($editor);
+
+        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementContains('form[action$="/watching/update"] button.icon-list-item', 'Watch');
+
+            $watchOptions = new UserEntityWatchOptions($editor, $entity);
+            $watchOptions->updateLevelByValue(WatchLevels::COMMENTS);
+
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+    }
+
+    public function test_watch_action_only_shows_with_permission()
+    {
+        $viewer = $this->users->viewer();
+        $this->actingAs($viewer);
+
+        $entities = [$this->entities->book(), $this->entities->chapter(), $this->entities->page()];
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementNotExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+
+        $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
+
+        /** @var Entity $entity */
+        foreach ($entities as $entity) {
+            $resp = $this->get($entity->getUrl());
+            $this->withHtml($resp)->assertElementExists('form[action$="/watching/update"] button.icon-list-item');
+        }
+    }
+
+    public function test_watch_update()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+
+        $this->actingAs($editor)->get($book->getUrl());
+        $resp = $this->put('/watching/update', [
+            'type' => get_class($book),
+            'id' => $book->id,
+            'level' => 'comments'
+        ]);
+
+        $resp->assertRedirect($book->getUrl());
+        $this->assertSessionHas('success');
+        $this->assertDatabaseHas('watches', [
+            'watchable_id' => $book->id,
+            'watchable_type' => $book->getMorphClass(),
+            'user_id' => $editor->id,
+            'level' => WatchLevels::COMMENTS,
+        ]);
+
+        $resp = $this->put('/watching/update', [
+            'type' => get_class($book),
+            'id' => $book->id,
+            'level' => 'default'
+        ]);
+        $resp->assertRedirect($book->getUrl());
+        $this->assertDatabaseMissing('watches', [
+            'watchable_id' => $book->id,
+            'watchable_type' => $book->getMorphClass(),
+            'user_id' => $editor->id,
+        ]);
+    }
+
+    public function test_watch_update_fails_for_guest()
+    {
+        $this->setSettings(['app-public' => 'true']);
+        $guest = $this->users->guest();
+        $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
+        $book = $this->entities->book();
+
+        $resp = $this->put('/watching/update', [
+            'type' => get_class($book),
+            'id' => $book->id,
+            'level' => 'comments'
+        ]);
+
+        $this->assertPermissionError($resp);
+        $guest->unsetRelations();
+    }
+
+    public function test_watch_detail_display_reflects_state()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        $chapter = $book->chapters()->first();
+        $page = $chapter->pages()->first();
+
+        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::UPDATES);
+
+        $this->actingAs($editor)->get($book->getUrl())->assertSee('Watching new pages and updates');
+        $this->get($chapter->getUrl())->assertSee('Watching via parent book');
+        $this->get($page->getUrl())->assertSee('Watching via parent book');
+
+        (new UserEntityWatchOptions($editor, $chapter))->updateLevelByValue(WatchLevels::COMMENTS);
+        $this->get($chapter->getUrl())->assertSee('Watching new pages, updates & comments');
+        $this->get($page->getUrl())->assertSee('Watching via parent chapter');
+
+        (new UserEntityWatchOptions($editor, $page))->updateLevelByValue(WatchLevels::UPDATES);
+        $this->get($page->getUrl())->assertSee('Watching new pages and updates');
+    }
+
+    public function test_watch_detail_ignore_indicator_cascades()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
+
+        $this->actingAs($editor)->get($book->getUrl())->assertSee('Ignoring notifications');
+        $this->get($book->chapters()->first()->getUrl())->assertSee('Ignoring via parent book');
+        $this->get($book->pages()->first()->getUrl())->assertSee('Ignoring via parent book');
+    }
+
+    public function test_watch_option_menu_shows_current_active_state()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+        $options = new UserEntityWatchOptions($editor, $book);
+
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementNotExists('form[action$="/watching/update"] svg[data-icon="check-circle"]');
+
+        $options->updateLevelByValue(WatchLevels::COMMENTS);
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[value="comments"] svg[data-icon="check-circle"]');
+
+        $options->updateLevelByValue(WatchLevels::IGNORE);
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[value="ignore"] svg[data-icon="check-circle"]');
+    }
+
+    public function test_watch_option_menu_limits_options_for_pages()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->bookHasChaptersAndPages();
+        (new UserEntityWatchOptions($editor, $book))->updateLevelByValue(WatchLevels::IGNORE);
+
+        $respHtml = $this->withHtml($this->actingAs($editor)->get($book->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="new"]');
+
+        $respHtml = $this->withHtml($this->get($book->pages()->first()->getUrl()));
+        $respHtml->assertElementExists('form[action$="/watching/update"] button[name="level"][value="updates"]');
+        $respHtml->assertElementNotExists('form[action$="/watching/update"] button[name="level"][value="new"]');
+    }
+
+    public function test_notify_own_page_changes()
+    {
+        $editor = $this->users->editor();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $prefs = new UserNotificationPreferences($editor);
+        $prefs->updateFromSettingsArray(['own-page-changes' => 'true']);
+
+        $notifications = Notification::fake();
+
+        $this->asAdmin();
+        $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
+        $notifications->assertSentTo($editor, PageUpdateNotification::class);
+    }
+
+    public function test_notify_own_page_comments()
+    {
+        $editor = $this->users->editor();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $prefs = new UserNotificationPreferences($editor);
+        $prefs->updateFromSettingsArray(['own-page-comments' => 'true']);
+
+        $notifications = Notification::fake();
+
+        $this->asAdmin()->post("/comment/{$entities['page']->id}", [
+            'text' => 'My new comment'
+        ]);
+        $notifications->assertSentTo($editor, CommentCreationNotification::class);
+    }
+
+    public function test_notify_comment_replies()
+    {
+        $editor = $this->users->editor();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $prefs = new UserNotificationPreferences($editor);
+        $prefs->updateFromSettingsArray(['comment-replies' => 'true']);
+
+        $notifications = Notification::fake();
+
+        $this->actingAs($editor)->post("/comment/{$entities['page']->id}", [
+            'text' => 'My new comment'
+        ]);
+        $comment = $entities['page']->comments()->first();
+
+        $this->asAdmin()->post("/comment/{$entities['page']->id}", [
+            'text' => 'My new comment response',
+            'parent_id' => $comment->id,
+        ]);
+        $notifications->assertSentTo($editor, CommentCreationNotification::class);
+    }
+
+    public function test_notify_watch_parent_book_ignore()
+    {
+        $editor = $this->users->editor();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $watches = new UserEntityWatchOptions($editor, $entities['book']);
+        $prefs = new UserNotificationPreferences($editor);
+        $watches->updateLevelByValue(WatchLevels::IGNORE);
+        $prefs->updateFromSettingsArray(['own-page-changes' => 'true', 'own-page-comments' => true]);
+
+        $notifications = Notification::fake();
+
+        $this->asAdmin()->post("/comment/{$entities['page']->id}", [
+            'text' => 'My new comment response',
+        ]);
+        $this->entities->updatePage($entities['page'], ['name' => 'My updated page', 'html' => 'Hello']);
+        $notifications->assertNothingSent();
+    }
+
+    public function test_notify_watch_parent_book_comments()
+    {
+        $notifications = Notification::fake();
+        $editor = $this->users->editor();
+        $admin = $this->users->admin();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $watches = new UserEntityWatchOptions($editor, $entities['book']);
+        $watches->updateLevelByValue(WatchLevels::COMMENTS);
+
+        // Comment post
+        $this->actingAs($admin)->post("/comment/{$entities['page']->id}", [
+            'text' => 'My new comment response',
+        ]);
+
+        $notifications->assertSentTo($editor, function (CommentCreationNotification $notification) use ($editor, $admin, $entities) {
+            $mail = $notification->toMail($editor);
+            $mailContent = html_entity_decode(strip_tags($mail->render()));
+            return $mail->subject === 'New comment on page: ' . $entities['page']->getShortName()
+                && str_contains($mailContent, 'View Comment')
+                && str_contains($mailContent, 'Page Name: ' . $entities['page']->name)
+                && str_contains($mailContent, 'Commenter: ' . $admin->name)
+                && str_contains($mailContent, 'Comment: My new comment response');
+        });
+    }
+
+    public function test_notify_watch_parent_book_updates()
+    {
+        $notifications = Notification::fake();
+        $editor = $this->users->editor();
+        $admin = $this->users->admin();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $watches = new UserEntityWatchOptions($editor, $entities['book']);
+        $watches->updateLevelByValue(WatchLevels::UPDATES);
+
+        $this->actingAs($admin);
+        $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
+
+        $notifications->assertSentTo($editor, function (PageUpdateNotification $notification) use ($editor, $admin) {
+            $mail = $notification->toMail($editor);
+            $mailContent = html_entity_decode(strip_tags($mail->render()));
+            return $mail->subject === 'Updated page: Updated page'
+                && str_contains($mailContent, 'View Page')
+                && str_contains($mailContent, 'Page Name: Updated page')
+                && str_contains($mailContent, 'Updated By: ' . $admin->name)
+                && str_contains($mailContent, 'you won\'t be sent notifications for further edits to this page by the same editor');
+        });
+
+        // Test debounce
+        $notifications = Notification::fake();
+        $this->entities->updatePage($entities['page'], ['name' => 'Updated page', 'html' => 'new page content']);
+        $notifications->assertNothingSentTo($editor);
+    }
+
+    public function test_notify_watch_parent_book_new()
+    {
+        $notifications = Notification::fake();
+        $editor = $this->users->editor();
+        $admin = $this->users->admin();
+        $entities = $this->entities->createChainBelongingToUser($editor);
+        $watches = new UserEntityWatchOptions($editor, $entities['book']);
+        $watches->updateLevelByValue(WatchLevels::NEW);
+
+        $this->actingAs($admin)->get($entities['chapter']->getUrl('/create-page'));
+        $page = $entities['chapter']->pages()->where('draft', '=', true)->first();
+        $this->post($page->getUrl(), ['name' => 'My new page', 'html' => 'My new page content']);
+
+        $notifications->assertSentTo($editor, function (PageCreationNotification $notification) use ($editor, $admin) {
+            $mail = $notification->toMail($editor);
+            $mailContent = html_entity_decode(strip_tags($mail->render()));
+            return $mail->subject === 'New page: My new page'
+                && str_contains($mailContent, 'View Page')
+                && str_contains($mailContent, 'Page Name: My new page')
+                && str_contains($mailContent, 'Created By: ' . $admin->name);
+        });
+    }
+
+    public function test_notifications_not_sent_if_lacking_view_permission_for_related_item()
+    {
+        $notifications = Notification::fake();
+        $editor = $this->users->editor();
+        $page = $this->entities->page();
+
+        $watches = new UserEntityWatchOptions($editor, $page);
+        $watches->updateLevelByValue(WatchLevels::COMMENTS);
+        $this->permissions->disableEntityInheritedPermissions($page);
+
+        $this->asAdmin()->post("/comment/{$page->id}", [
+            'text' => 'My new comment response',
+        ])->assertOk();
+
+        $notifications->assertNothingSentTo($editor);
+    }
+}
index b3e9f3cd0ed40413336204fedb0e4e632587e174..0a71bb6eff4ab6167f70f21d9a610a6ab576829f 100644 (file)
@@ -2,6 +2,7 @@
 
 namespace Tests\Entity;
 
+use BookStack\Activity\ActivityType;
 use BookStack\Activity\Models\Comment;
 use BookStack\Entities\Models\Page;
 use Tests\TestCase;
@@ -29,6 +30,8 @@ class CommentTest extends TestCase
             'text'        => $comment->text,
             'parent_id'   => 2,
         ]);
+
+        $this->assertActivityExists(ActivityType::COMMENT_CREATE);
     }
 
     public function test_comment_edit()
@@ -53,6 +56,8 @@ class CommentTest extends TestCase
             'text'      => $newText,
             'entity_id' => $page->id,
         ]);
+
+        $this->assertActivityExists(ActivityType::COMMENT_UPDATE);
     }
 
     public function test_comment_delete()
@@ -71,6 +76,8 @@ class CommentTest extends TestCase
         $this->assertDatabaseMissing('comments', [
             'id' => $comment->id,
         ]);
+
+        $this->assertActivityExists(ActivityType::COMMENT_DELETE);
     }
 
     public function test_comments_converts_markdown_input_to_html()
index b86e9039407006d922029c06e0058efb91a88f5b..3b2da369de5235251e23574e0eb5bc80d07f2d90 100644 (file)
@@ -50,6 +50,14 @@ class UserRoleProvider
         return $user;
     }
 
+    /**
+     * Get the system "guest" user.
+     */
+    public function guest(): User
+    {
+        return User::getDefault();
+    }
+
     /**
      * Create a new fresh user without any relations.
      */
index 035546593d376bbf9a45d8467fb084b5d97e931c..6ea0257b81e5c1d6dced3c8c279c837e181fb122 100644 (file)
@@ -413,13 +413,14 @@ class EntityPermissionsTest extends TestCase
         $this->entityRestrictionFormTest(Page::class, 'Page Permissions', 'delete', '2');
     }
 
-    public function test_shelf_create_permission_not_visible()
+    public function test_shelf_create_permission_visible_with_notice()
     {
         $shelf = $this->entities->shelf();
 
         $resp = $this->asAdmin()->get($shelf->getUrl('/permissions'));
         $html = $this->withHtml($resp);
-        $html->assertElementNotExists('input[name$="[create]"]');
+        $html->assertElementExists('input[name$="[create]"]');
+        $resp->assertSee('Shelf create permissions are only used for copying permissions to child books using the action below.');
     }
 
     public function test_restricted_pages_not_visible_in_book_navigation_on_pages()
index 322ab037032f76ea9adb7b3002f7c2f845f1309d..0ab0792bd00228dfb306b7216e7f61a66be894e3 100644 (file)
@@ -5,6 +5,7 @@ namespace Tests;
 use BookStack\Entities\Models\Entity;
 use BookStack\Settings\SettingService;
 use BookStack\Uploads\HttpFetcher;
+use BookStack\Users\Models\User;
 use GuzzleHttp\Client;
 use GuzzleHttp\Handler\MockHandler;
 use GuzzleHttp\HandlerStack;
@@ -46,6 +47,7 @@ abstract class TestCase extends BaseTestCase
         $this->permissions = new PermissionsProvider($this->users);
         $this->files = new FileProvider();
 
+        User::clearDefault();
         parent::setUp();
 
         // We can uncomment the below to run tests with failings upon deprecations.
index e47a259a5bad6279b1d2942c0025ec53b90a9ecc..9d72f4e1496e0e9c7ca5a60c073859f2f6781b4a 100644 (file)
@@ -2,10 +2,30 @@
 
 namespace Tests\User;
 
+use BookStack\Activity\Tools\UserEntityWatchOptions;
+use BookStack\Activity\WatchLevels;
 use Tests\TestCase;
 
 class UserPreferencesTest extends TestCase
 {
+    public function test_index_view()
+    {
+        $resp = $this->asEditor()->get('/preferences');
+        $resp->assertOk();
+        $resp->assertSee('Interface Keyboard Shortcuts');
+        $resp->assertSee('Edit Profile');
+    }
+
+    public function test_index_view_accessible_but_without_profile_and_notifications_for_guest_user()
+    {
+        $this->setSettings(['app-public' => 'true']);
+        $this->permissions->grantUserRolePermissions($this->users->guest(), ['receive-notifications']);
+        $resp = $this->get('/preferences');
+        $resp->assertOk();
+        $resp->assertSee('Interface Keyboard Shortcuts');
+        $resp->assertDontSee('Edit Profile');
+        $resp->assertDontSee('Notification');
+    }
     public function test_interface_shortcuts_updating()
     {
         $this->asEditor();
@@ -45,6 +65,80 @@ class UserPreferencesTest extends TestCase
         $this->withHtml($this->get('/'))->assertElementExists('body[component="shortcuts"]');
     }
 
+    public function test_notification_routes_requires_notification_permission()
+    {
+        $viewer = $this->users->viewer();
+        $resp = $this->actingAs($viewer)->get('/preferences/notifications');
+        $this->assertPermissionError($resp);
+
+        $resp = $this->put('/preferences/notifications');
+        $this->assertPermissionError($resp);
+
+        $this->permissions->grantUserRolePermissions($viewer, ['receive-notifications']);
+        $resp = $this->get('/preferences/notifications');
+        $resp->assertOk();
+        $resp->assertSee('Notification Preferences');
+    }
+
+    public function test_notification_preferences_updating()
+    {
+        $editor = $this->users->editor();
+
+        // View preferences with defaults
+        $resp = $this->actingAs($editor)->get('/preferences/notifications');
+        $resp->assertSee('Notification Preferences');
+
+        $html = $this->withHtml($resp);
+        $html->assertFieldHasValue('preferences[comment-replies]', 'false');
+
+        // Update preferences
+        $resp = $this->put('/preferences/notifications', [
+            'preferences' => ['comment-replies' => 'true'],
+        ]);
+
+        $resp->assertRedirect('/preferences/notifications');
+        $resp->assertSessionHas('success', 'Notification preferences have been updated!');
+
+        // View updates to preferences page
+        $resp = $this->get('/preferences/notifications');
+        $html = $this->withHtml($resp);
+        $html->assertFieldHasValue('preferences[comment-replies]', 'true');
+    }
+
+    public function test_notification_preferences_show_watches()
+    {
+        $editor = $this->users->editor();
+        $book = $this->entities->book();
+
+        $options = new UserEntityWatchOptions($editor, $book);
+        $options->updateLevelByValue(WatchLevels::COMMENTS);
+
+        $resp = $this->actingAs($editor)->get('/preferences/notifications');
+        $resp->assertSee($book->name);
+        $resp->assertSee('All Page Updates & Comments');
+
+        $options->updateLevelByValue(WatchLevels::DEFAULT);
+
+        $resp = $this->actingAs($editor)->get('/preferences/notifications');
+        $resp->assertDontSee($book->name);
+        $resp->assertDontSee('All Page Updates & Comments');
+    }
+
+    public function test_notification_preferences_not_accessible_to_guest()
+    {
+        $this->setSettings(['app-public' => 'true']);
+        $guest = $this->users->guest();
+        $this->permissions->grantUserRolePermissions($guest, ['receive-notifications']);
+
+        $resp = $this->get('/preferences/notifications');
+        $this->assertPermissionError($resp);
+
+        $resp = $this->put('/preferences/notifications', [
+            'preferences' => ['comment-replies' => 'true'],
+        ]);
+        $this->assertPermissionError($resp);
+    }
+
     public function test_update_sort_preference()
     {
         $editor = $this->users->editor();