diff --git a/.github/translators.txt b/.github/translators.txt index 6aa2be470f3..d58538d6c64 100644 --- a/.github/translators.txt +++ b/.github/translators.txt @@ -546,3 +546,11 @@ lonestan :: Russian Paul Kernstock (kernstock) :: German brtbr :: German; German Informal Ricardo Covelo (covelo12) :: Portuguese +Bojan Maksimovic (PolarniMeda) :: Serbian (Cyrillic) +Dian Prawira (wiradian84) :: Indonesian +Tim (timakai) :: Dutch; German Informal; French; Romanian; Catalan; Czech; Danish; German; Finnish; Hungarian; Italian; Japanese; Korean; Polish; Russian; Ukrainian; Chinese Simplified; Chinese Traditional; Portuguese, Brazilian; Persian; Spanish, Argentina; Croatian; Norwegian Nynorsk; Estonian; Uzbek; Norwegian Bokmal +dadda123 :: Swedish +Julien Muggli (JulienMuggli) :: French +nomoreshow :: Turkish +Qasem Talaee (qasem_talaee) :: Persian +ddeicide :: Bulgarian diff --git a/app/Access/Controllers/LoginController.php b/app/Access/Controllers/LoginController.php index 4694f22e4d3..fece3d88098 100644 --- a/app/Access/Controllers/LoginController.php +++ b/app/Access/Controllers/LoginController.php @@ -8,6 +8,7 @@ use BookStack\Exceptions\LoginAttemptException; use BookStack\Facades\Activity; use BookStack\Http\Controller; +use BookStack\Util\UrlComparison; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -186,7 +187,8 @@ protected function updateIntendedFromPrevious(): void { // Store the previous location for redirect after login $previous = url()->previous(''); - $isPreviousFromInstance = str_starts_with($previous, url('/')); + $comparison = new UrlComparison($previous, url('/')); + $isPreviousFromInstance = $comparison->originsMatch() && $comparison->pathsOverlap(); if (!$previous || !setting('app-public') || !$isPreviousFromInstance) { return; } diff --git a/app/Access/ExternalBaseUserProvider.php b/app/Access/ExternalBaseUserProvider.php index 2165fd4591e..8d82d28c8eb 100644 --- a/app/Access/ExternalBaseUserProvider.php +++ b/app/Access/ExternalBaseUserProvider.php @@ -3,11 +3,17 @@ namespace BookStack\Access; use BookStack\Users\Models\User; +use BookStack\Users\UserRepo; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Contracts\Auth\UserProvider; class ExternalBaseUserProvider implements UserProvider { + public function __construct( + protected UserRepo $userRepo, + ) { + } + /** * Retrieve a user by their unique identifier. */ @@ -42,11 +48,9 @@ public function updateRememberToken(Authenticatable $user, $token) /** * Retrieve a user by the given credentials. */ - public function retrieveByCredentials(array $credentials): ?Authenticatable + public function retrieveByCredentials(array $credentials): ?User { - return User::query() - ->where('external_auth_id', $credentials['external_auth_id']) - ->first(); + return $this->userRepo->getByExternalAuthId($credentials['external_auth_id']); } /** diff --git a/app/Access/Guards/ExternalBaseSessionGuard.php b/app/Access/Guards/ExternalBaseSessionGuard.php index 91239599ba9..4d263e581f8 100644 --- a/app/Access/Guards/ExternalBaseSessionGuard.php +++ b/app/Access/Guards/ExternalBaseSessionGuard.php @@ -30,7 +30,7 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * The user we last attempted to retrieve. */ - protected Authenticatable|null $lastAttempted; + protected Authenticatable|null $lastAttempted = null; /** * The session used by the guard. @@ -50,8 +50,12 @@ class ExternalBaseSessionGuard implements StatefulGuard /** * Create a new authentication guard. */ - public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService) - { + public function __construct( + string $name, + UserProvider $provider, + Session $session, + RegistrationService $registrationService + ) { $this->name = $name; $this->session = $session; $this->provider = $provider; @@ -203,7 +207,7 @@ protected function clearUserDataFromStorage(): void /** * Get the last user we attempted to authenticate. */ - public function getLastAttempted(): Authenticatable + public function getLastAttempted(): Authenticatable|null { return $this->lastAttempted; } diff --git a/app/Access/Guards/LdapSessionGuard.php b/app/Access/Guards/LdapSessionGuard.php index 9455d530dfe..f3628c897d8 100644 --- a/app/Access/Guards/LdapSessionGuard.php +++ b/app/Access/Guards/LdapSessionGuard.php @@ -83,6 +83,10 @@ public function attempt(array $credentials = [], $remember = false): bool } } + if (!($user instanceof User)) { + throw new LoginAttemptException('Could not find or create a user for LDAP login.'); + } + // Sync LDAP groups if required if ($this->ldapService->shouldSyncGroups()) { $this->ldapService->syncGroups($user, $username); diff --git a/app/Access/LoginService.php b/app/Access/LoginService.php index 769290c1648..46545f798b7 100644 --- a/app/Access/LoginService.php +++ b/app/Access/LoginService.php @@ -13,6 +13,8 @@ use BookStack\Theming\ThemeEvents; use BookStack\Users\Models\User; use Exception; +use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Support\Facades\Hash; class LoginService { @@ -171,10 +173,24 @@ public function attempt(array $credentials, string $method, bool $remember = fal } catch (LoginAttemptInvalidUserException $e) { // Catch and return false for non-login accounts // so it looks like a normal invalid login. - return false; + $result = false; } } + // Perform a dummy hash check to balance out the time of a login with an existing known user + // with that of a user not in the system (which we don't perform a hash check for in the above). + /** @var Authenticatable|null $lastAttempted */ + $lastAttempted = auth()->getLastAttempted(); + if (!$result && $lastAttempted === null) { + Hash::check($credentials['password'], '$2y$04$A.H9icXH4/lxLd9DHuaYqO/GVBd0OKetxyY0txmNfTAlPLVnTBx3y'); + } + + // Add some noise to request times on failed login attempts + if (!$result) { + $sleepMs = random_int(0, 250); + usleep($sleepMs * 1000); + } + return $result; } diff --git a/app/Access/Oidc/OidcJwtWithClaims.php b/app/Access/Oidc/OidcJwtWithClaims.php index 9d7eeead1a9..9763ab15864 100644 --- a/app/Access/Oidc/OidcJwtWithClaims.php +++ b/app/Access/Oidc/OidcJwtWithClaims.php @@ -131,8 +131,6 @@ protected function validateTokenSignature(): void } }, $this->keys); - $parsedKeys = array_filter($parsedKeys); - $contentToSign = $this->tokenParts[0] . '.' . $this->tokenParts[1]; /** @var OidcJwtSigningKey $parsedKey */ foreach ($parsedKeys as $parsedKey) { diff --git a/app/Access/Oidc/OidcService.php b/app/Access/Oidc/OidcService.php index a84bd320513..19120e2d3c3 100644 --- a/app/Access/Oidc/OidcService.php +++ b/app/Access/Oidc/OidcService.php @@ -80,6 +80,7 @@ public function processAuthorizeResponse(?string $authorizationCode): User $provider->setPkceCode($pkceCode); // Try to exchange authorization code for access token + /** @var OidcAccessToken $accessToken */ $accessToken = $provider->getAccessToken('authorization_code', [ 'code' => $authorizationCode, ]); diff --git a/app/Access/RegistrationService.php b/app/Access/RegistrationService.php index e47479e7991..e3ae9b2025c 100644 --- a/app/Access/RegistrationService.php +++ b/app/Access/RegistrationService.php @@ -52,9 +52,7 @@ protected function registrationAllowed(): bool */ public function findOrRegister(string $name, string $email, string $externalId): User { - $user = User::query() - ->where('external_auth_id', '=', $externalId) - ->first(); + $user = $this->userRepo->getByExternalAuthId($externalId); if (is_null($user)) { $userData = [ diff --git a/app/Activity/ActivityQueries.php b/app/Activity/ActivityQueries.php index d5b047937fb..a3383e80933 100644 --- a/app/Activity/ActivityQueries.php +++ b/app/Activity/ActivityQueries.php @@ -11,6 +11,7 @@ use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\User; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\MorphTo; use Illuminate\Database\Eloquent\Relations\Relation; @@ -27,6 +28,7 @@ public function __construct( */ public function latest(int $count = 20, int $page = 0): array { + /** @var Collection $activityList */ $activityList = $this->permissions ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->orderBy('created_at', 'desc') @@ -83,6 +85,7 @@ public function entityActivity(Entity $entity, int $count = 20, int $page = 1): */ public function userActivity(User $user, int $count = 20, int $page = 0): array { + /** @var Collection $activityList */ $activityList = $this->permissions ->restrictEntityRelationQuery(Activity::query(), 'activities', 'loggable_id', 'loggable_type') ->orderBy('created_at', 'desc') diff --git a/app/Activity/Controllers/CommentController.php b/app/Activity/Controllers/CommentController.php index f61a2c8df6e..8474d9eb1c7 100644 --- a/app/Activity/Controllers/CommentController.php +++ b/app/Activity/Controllers/CommentController.php @@ -59,8 +59,7 @@ public function update(Request $request, int $commentId) 'html' => ['required', 'string'], ]); - $comment = $this->commentRepo->getById($commentId); - $this->checkOwnablePermission(Permission::PageView, $comment->entity); + $comment = $this->commentRepo->getVisibleById($commentId); $this->checkOwnablePermission(Permission::CommentUpdate, $comment); $comment = $this->commentRepo->update($comment, $input['html']); @@ -76,8 +75,7 @@ public function update(Request $request, int $commentId) */ public function archive(int $id) { - $comment = $this->commentRepo->getById($id); - $this->checkOwnablePermission(Permission::PageView, $comment->entity); + $comment = $this->commentRepo->getVisibleById($id); if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { $this->showPermissionError(); } @@ -96,8 +94,7 @@ public function archive(int $id) */ public function unarchive(int $id) { - $comment = $this->commentRepo->getById($id); - $this->checkOwnablePermission(Permission::PageView, $comment->entity); + $comment = $this->commentRepo->getVisibleById($id); if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) { $this->showPermissionError(); } @@ -116,7 +113,7 @@ public function unarchive(int $id) */ public function destroy(int $id) { - $comment = $this->commentRepo->getById($id); + $comment = $this->commentRepo->getVisibleById($id); $this->checkOwnablePermission(Permission::CommentDelete, $comment); $this->commentRepo->delete($comment); diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 3faa76657b6..b49eb456488 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -3,6 +3,7 @@ namespace BookStack\Activity\Models; use BookStack\App\Model; +use BookStack\Entities\Models\Page; use BookStack\Permissions\Models\JointPermission; use BookStack\Permissions\PermissionApplicator; use BookStack\Users\Models\HasCreatorAndUpdater; @@ -40,6 +41,9 @@ class Comment extends Model implements Loggable, OwnableInterface /** * Get the entity that this comment belongs to. + * It's only pages right now hence the typing below. + * Would need a deeper audit if that changes as many areas assume this is always a page. + * @return MorphTo */ public function entity(): MorphTo { @@ -55,7 +59,9 @@ public function entity(): MorphTo // Ultimately, we could just align the method name to 'commentable' but that would be a potential // breaking change and not really worthwhile in a patch due to the risk of creating extra problems. - return $this->morphTo(null, 'commentable_type', 'commentable_id'); + /** @var MorphTo $relation */ + $relation = $this->morphTo(null, 'commentable_type', 'commentable_id'); + return $relation; } /** diff --git a/app/Activity/Tools/WebhookFormatter.php b/app/Activity/Tools/WebhookFormatter.php index cb4e9cb0a9c..111efd5d498 100644 --- a/app/Activity/Tools/WebhookFormatter.php +++ b/app/Activity/Tools/WebhookFormatter.php @@ -73,13 +73,17 @@ public function addDefaultModelFormatters(): void // Load entity owner, creator, updater details $this->addModelFormatter( fn ($event, $model) => ($model instanceof Entity), - fn ($model) => $model->load(['ownedBy', 'createdBy', 'updatedBy']) + function ($model) { + $model->load(['ownedBy', 'createdBy', 'updatedBy']); + } ); // Load revision detail for page update and create events $this->addModelFormatter( fn ($event, $model) => ($model instanceof Page && ($event === ActivityType::PAGE_CREATE || $event === ActivityType::PAGE_UPDATE)), - fn ($model) => $model->load('currentRevision') + function ($model) { + $model->load('currentRevision'); + } ); } diff --git a/app/Api/ApiDocsGenerator.php b/app/Api/ApiDocsGenerator.php index 53cb2890a7e..8d1be79c88f 100644 --- a/app/Api/ApiDocsGenerator.php +++ b/app/Api/ApiDocsGenerator.php @@ -138,8 +138,8 @@ protected function getValidationAsString($validation): string return $validation; } - if (is_object($validation) && method_exists($validation, '__toString')) { - return strval($validation); + if (is_object($validation) && $validation instanceof \Stringable) { + return $validation->__toString(); } if ($validation instanceof Password) { diff --git a/app/App/HomeController.php b/app/App/HomeController.php index 00e2db3df43..d8193e234f9 100644 --- a/app/App/HomeController.php +++ b/app/App/HomeController.php @@ -2,11 +2,8 @@ namespace BookStack\App; -use BookStack\Activity\ActivityQueries; use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\EntityQueries; -use BookStack\Entities\Queries\QueryRecentlyViewed; -use BookStack\Entities\Queries\QueryTopFavourites; use BookStack\Entities\Tools\PageContent; use BookStack\Http\Controller; use BookStack\Util\SimpleListOptions; @@ -24,49 +21,19 @@ public function __construct( */ public function index( Request $request, - ActivityQueries $activities, - QueryRecentlyViewed $recentlyViewed, - QueryTopFavourites $topFavourites, ) { - $activity = $activities->latest(10); - $draftPages = []; - - if ($this->isSignedIn()) { - $draftPages = $this->queries->pages->currentUserDraftsForList() - ->orderBy('updated_at', 'desc') - ->with('book') - ->take(6) - ->get(); - } - - $recentFactor = count($draftPages) > 0 ? 0.5 : 1; - $recents = $this->isSignedIn() ? - $recentlyViewed->run(12 * $recentFactor, 1) - : $this->queries->books->visibleForList()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get(); - $favourites = $topFavourites->run(6); - $recentlyUpdatedPages = $this->queries->pages->visibleForList() - ->where('draft', false) - ->orderBy('updated_at', 'desc') - ->take($favourites->count() > 0 ? 5 : 10) - ->get(); - - $homepageOptions = ['default', 'books', 'bookshelves', 'page']; - $homepageOption = setting('app-homepage-type', 'default'); - if (!in_array($homepageOption, $homepageOptions)) { - $homepageOption = 'default'; + $homepageType = setting('app-homepage-type'); + if (!in_array($homepageType, ['default', 'books', 'bookshelves', 'page'])) { + $homepageType = 'default'; } $commonData = [ - 'activity' => $activity, - 'recents' => $recents, - 'recentlyUpdatedPages' => $recentlyUpdatedPages, - 'draftPages' => $draftPages, - 'favourites' => $favourites, + 'homeView' => $homepageType, ]; // Add required list ordering & sorting for books & shelves views. - if ($homepageOption === 'bookshelves' || $homepageOption === 'books') { - $key = $homepageOption; + if ($homepageType === 'bookshelves' || $homepageType === 'books') { + $key = $homepageType; $view = setting()->getForCurrentUser($key . '_view_type'); $listOptions = SimpleListOptions::fromRequest($request, $key)->withSortOptions([ 'name' => trans('common.sort_name'), @@ -80,7 +47,7 @@ public function index( ]); } - if ($homepageOption === 'bookshelves') { + if ($homepageType === 'bookshelves') { $shelves = $this->queries->shelves->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000)); @@ -89,7 +56,7 @@ public function index( return view('home.shelves', $data); } - if ($homepageOption === 'books') { + if ($homepageType === 'books') { $books = $this->queries->books->visibleForListWithCover() ->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder()) ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000)); @@ -98,7 +65,7 @@ public function index( return view('home.books', $data); } - if ($homepageOption === 'page') { + if ($homepageType === 'page') { $homepageSetting = setting('app-homepage', '0:'); $id = intval(explode(':', $homepageSetting)[0]); /** @var Page $customHomepage */ diff --git a/app/App/Providers/AuthServiceProvider.php b/app/App/Providers/AuthServiceProvider.php index 6a816252131..8c71fee3ac3 100644 --- a/app/App/Providers/AuthServiceProvider.php +++ b/app/App/Providers/AuthServiceProvider.php @@ -10,6 +10,7 @@ use BookStack\Access\RegistrationService; use BookStack\Api\ApiTokenGuard; use BookStack\Users\Models\User; +use BookStack\Users\UserRepo; use Illuminate\Support\Facades\Auth; use Illuminate\Support\ServiceProvider; use Illuminate\Validation\Rules\Password; @@ -60,7 +61,7 @@ public function boot(): void public function register(): void { Auth::provider('external-users', function () { - return new ExternalBaseUserProvider(); + return new ExternalBaseUserProvider($this->app[UserRepo::class]); }); // Bind and provide the default system user as a singleton to the app instance when needed. diff --git a/app/App/Providers/ThemeServiceProvider.php b/app/App/Providers/ThemeServiceProvider.php index 671e5e1df74..9b0f7001e81 100644 --- a/app/App/Providers/ThemeServiceProvider.php +++ b/app/App/Providers/ThemeServiceProvider.php @@ -7,6 +7,7 @@ use BookStack\Theming\ThemeViews; use Illuminate\Support\Facades\Blade; use Illuminate\Support\ServiceProvider; +use Illuminate\View\FileViewFinder; class ThemeServiceProvider extends ServiceProvider { @@ -27,7 +28,11 @@ public function boot(): void // Boot up the theme system $themeService = $this->app->make(ThemeService::class); $viewFactory = $this->app->make('view'); - $themeViews = new ThemeViews($viewFactory->getFinder()); + $viewFinder = $viewFactory->getFinder(); + if (!($viewFinder instanceof FileViewFinder)) { + throw new \Exception('Only the file view finder is supported for the theme system'); + } + $themeViews = new ThemeViews($viewFinder); // Use a custom include so that we can insert theme views before/after includes. // This is done, even if no theme is active, so that view caching does not create problems diff --git a/app/App/Providers/ValidationRuleServiceProvider.php b/app/App/Providers/ValidationRuleServiceProvider.php index 1adc1ebd851..fc030263029 100644 --- a/app/App/Providers/ValidationRuleServiceProvider.php +++ b/app/App/Providers/ValidationRuleServiceProvider.php @@ -3,6 +3,7 @@ namespace BookStack\App\Providers; use BookStack\Uploads\ImageService; +use BookStack\Util\UrlFilter; use Illuminate\Support\Facades\Validator; use Illuminate\Support\ServiceProvider; @@ -21,10 +22,8 @@ public function boot(): void Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) { $cleanLinkName = strtolower(trim($value)); - $isJs = str_starts_with($cleanLinkName, 'javascript:'); - $isData = str_starts_with($cleanLinkName, 'data:'); - - return !$isJs && !$isData; + $filter = new UrlFilter($cleanLinkName); + return $filter->isAllowed(); }); } } diff --git a/app/App/Providers/ViewTweaksServiceProvider.php b/app/App/Providers/ViewTweaksServiceProvider.php index 6771e513fa6..ae41c1d953a 100644 --- a/app/App/Providers/ViewTweaksServiceProvider.php +++ b/app/App/Providers/ViewTweaksServiceProvider.php @@ -3,7 +3,11 @@ namespace BookStack\App\Providers; use BookStack\Entities\BreadcrumbsViewComposer; +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; use BookStack\Util\DateFormatter; +use BookStack\View\ViewBlockManager; +use BookStack\View\ViewBlockPreferences; use Illuminate\Pagination\Paginator; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\View; @@ -18,6 +22,10 @@ public function register() $app['config']->get('app.display_timezone'), ); }); + + $this->app->singleton(ViewBlockManager::class, function ($app) { + return new ViewBlockManager(new ViewBlockPreferences()); + }); } /** @@ -32,7 +40,10 @@ public function boot(): void View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class); // View Globals + $viewBlockManager = $this->app->make(ViewBlockManager::class); View::share('dates', $this->app->make(DateFormatter::class)); + View::share('viewBlocks', $viewBlockManager); + Theme::dispatch(ThemeEvents::VIEW_BLOCKS_REGISTER, $viewBlockManager); // Custom blade view directives Blade::directive('icon', function ($expression) { diff --git a/app/App/helpers.php b/app/App/helpers.php index 8f210ecafd4..45a84da8db7 100644 --- a/app/App/helpers.php +++ b/app/App/helpers.php @@ -6,6 +6,7 @@ use BookStack\Permissions\Permission; use BookStack\Permissions\PermissionApplicator; use BookStack\Settings\SettingService; +use BookStack\Users\Models\OwnableInterface; use BookStack\Users\Models\User; /** @@ -40,7 +41,7 @@ function user(): User * Check if the current user has a permission. If an ownable element * is passed in the jointPermissions are checked against that particular item. */ -function userCan(string|Permission $permission, ?Model $ownable = null): bool +function userCan(string|Permission $permission, (Model&OwnableInterface)|null $ownable = null): bool { if (is_null($ownable)) { return user()->can($permission); diff --git a/app/Config/setting-defaults.php b/app/Config/setting-defaults.php index 2f270b283a2..59425449516 100644 --- a/app/Config/setting-defaults.php +++ b/app/Config/setting-defaults.php @@ -32,6 +32,7 @@ 'page-draft-color-dark' => '#a66ce8', 'app-custom-head' => false, 'registration-enabled' => false, + 'app-homepage-type' => 'default', // User-level default settings 'user' => [ diff --git a/app/Console/Commands/InstallModuleCommand.php b/app/Console/Commands/InstallModuleCommand.php index 114bfb105d8..8e77474554e 100644 --- a/app/Console/Commands/InstallModuleCommand.php +++ b/app/Console/Commands/InstallModuleCommand.php @@ -7,6 +7,7 @@ use BookStack\Theming\ThemeModuleException; use BookStack\Theming\ThemeModuleManager; use BookStack\Theming\ThemeModuleZip; +use BookStack\Util\UrlComparison; use GuzzleHttp\Psr7\Request; use Illuminate\Console\Command; use Illuminate\Support\Str; @@ -199,7 +200,6 @@ protected function downloadModuleFile(string $location): string|null { $httpRequests = app()->make(HttpRequestService::class); $client = $httpRequests->buildClient(30, ['stream' => true]); - $originalUrl = parse_url($location); $currentLocation = $location; $maxRedirects = 3; $redirectCount = 0; @@ -212,12 +212,11 @@ protected function downloadModuleFile(string $location): string|null if ($statusCode >= 300 && $statusCode < 400 && $redirectCount < $maxRedirects) { $redirectLocation = $resp->getHeaderLine('Location'); if ($redirectLocation) { - $redirectUrl = parse_url($redirectLocation); - $redirectOriginMatches = ($originalUrl['host'] ?? '') === ($redirectUrl['host'] ?? '') - && ($originalUrl['scheme'] ?? '') === ($redirectUrl['scheme'] ?? '') - && ($originalUrl['port'] ?? '') === ($redirectUrl['port'] ?? ''); + $comparison = new UrlComparison($location, $redirectLocation); + $redirectOriginMatches = $comparison->originsMatch(); if (!$redirectOriginMatches) { + $redirectUrl = parse_url($redirectLocation); $redirectOrigin = ($redirectUrl['scheme'] ?? '') . '://' . ($redirectUrl['host'] ?? '') . (isset($redirectUrl['port']) ? ':' . $redirectUrl['port'] : ''); $this->info("The download URL is redirecting to a different site: {$redirectOrigin}"); $shouldContinue = $this->confirm("Do you trust downloading the module from this site?"); diff --git a/app/Entities/Controllers/BookController.php b/app/Entities/Controllers/BookController.php index 98470d91ce8..aa4f99daa6d 100644 --- a/app/Entities/Controllers/BookController.php +++ b/app/Entities/Controllers/BookController.php @@ -2,10 +2,8 @@ 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\Queries\BookQueries; use BookStack\Entities\Queries\BookshelfQueries; use BookStack\Entities\Queries\EntityQueries; @@ -19,7 +17,6 @@ use BookStack\Facades\Activity; use BookStack\Http\Controller; use BookStack\Permissions\Permission; -use BookStack\References\ReferenceFetcher; use BookStack\Util\DatabaseTransaction; use BookStack\Util\SimpleListOptions; use Illuminate\Http\Request; @@ -34,7 +31,6 @@ public function __construct( protected BookQueries $queries, protected EntityQueries $entityQueries, protected BookshelfQueries $shelfQueries, - protected ReferenceFetcher $referenceFetcher, ) { } @@ -53,9 +49,6 @@ public function index(Request $request) $books = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) ->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000)); - $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false; - $popular = $this->queries->popularForList()->take(4)->get(); - $new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get(); $this->shelfContext->clearShelfContext(); @@ -63,9 +56,6 @@ public function index(Request $request) return view('books.index', [ 'books' => $books, - 'recents' => $recents, - 'popular' => $popular, - 'new' => $new, 'view' => $view, 'listOptions' => $listOptions, ]); @@ -127,7 +117,7 @@ public function store(Request $request, ?string $shelfSlug = null) /** * Display the specified book. */ - public function show(Request $request, ActivityQueries $activities, string $slug) + public function show(Request $request, string $slug) { try { $book = $this->queries->findVisibleBySlugOrFail($slug); @@ -140,7 +130,6 @@ public function show(Request $request, ActivityQueries $activities, string $slug } $bookChildren = (new BookContents($book))->getTree(true); - $bookParentShelves = $book->shelves()->scopes('visible')->get(); View::incrementFor($book); if ($request->has('shelf')) { @@ -153,10 +142,6 @@ public function show(Request $request, ActivityQueries $activities, string $slug 'book' => $book, 'current' => $book, 'bookChildren' => $bookChildren, - 'bookParentShelves' => $bookParentShelves, - 'watchOptions' => new UserEntityWatchOptions(user(), $book), - 'activity' => $activities->entityActivity($book, 20, 1), - 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($book), ]); } diff --git a/app/Entities/Controllers/BookshelfController.php b/app/Entities/Controllers/BookshelfController.php index 1e8b26b5156..a918c48f185 100644 --- a/app/Entities/Controllers/BookshelfController.php +++ b/app/Entities/Controllers/BookshelfController.php @@ -2,7 +2,6 @@ namespace BookStack\Entities\Controllers; -use BookStack\Activity\ActivityQueries; use BookStack\Activity\Models\View; use BookStack\Entities\Queries\BookQueries; use BookStack\Entities\Queries\BookshelfQueries; @@ -13,7 +12,6 @@ use BookStack\Exceptions\NotFoundException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; -use BookStack\References\ReferenceFetcher; use BookStack\Util\SimpleListOptions; use Exception; use Illuminate\Http\Request; @@ -27,7 +25,6 @@ public function __construct( protected EntityQueries $entityQueries, protected BookQueries $bookQueries, protected ShelfContext $shelfContext, - protected ReferenceFetcher $referenceFetcher, ) { } @@ -46,21 +43,12 @@ public function index(Request $request) $shelves = $this->queries->visibleForListWithCover() ->orderBy($listOptions->getSort(), $listOptions->getOrder()) ->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000)); - $recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false; - $popular = $this->queries->popularForList()->get(); - $new = $this->queries->visibleForList() - ->orderBy('created_at', 'desc') - ->take(4) - ->get(); $this->shelfContext->clearShelfContext(); $this->setPageTitle(trans('entities.shelves')); return view('shelves.index', [ 'shelves' => $shelves, - 'recents' => $recents, - 'popular' => $popular, - 'new' => $new, 'view' => $view, 'listOptions' => $listOptions, ]); @@ -105,7 +93,7 @@ public function store(Request $request) * * @throws NotFoundException */ - public function show(Request $request, ActivityQueries $activities, string $slug) + public function show(Request $request, string $slug) { try { $shelf = $this->queries->findVisibleBySlugOrFail($slug); @@ -144,9 +132,7 @@ public function show(Request $request, ActivityQueries $activities, string $slug 'shelf' => $shelf, 'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks, 'view' => $view, - 'activity' => $activities->entityActivity($shelf, 20, 1), 'listOptions' => $listOptions, - 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($shelf), ]); } diff --git a/app/Entities/Controllers/ChapterController.php b/app/Entities/Controllers/ChapterController.php index db2391599ab..c089d357512 100644 --- a/app/Entities/Controllers/ChapterController.php +++ b/app/Entities/Controllers/ChapterController.php @@ -3,7 +3,6 @@ namespace BookStack\Entities\Controllers; use BookStack\Activity\Models\View; -use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Models\Book; use BookStack\Entities\Queries\ChapterQueries; use BookStack\Entities\Queries\EntityQueries; @@ -18,7 +17,6 @@ use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; -use BookStack\References\ReferenceFetcher; use BookStack\Util\DatabaseTransaction; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; @@ -30,7 +28,6 @@ public function __construct( protected ChapterRepo $chapterRepo, protected ChapterQueries $queries, protected EntityQueries $entityQueries, - protected ReferenceFetcher $referenceFetcher, ) { } @@ -87,10 +84,10 @@ public function show(string $bookSlug, string $chapterSlug) return redirect($chapter->getUrl()); } - $sidebarTree = (new BookContents($chapter->book))->getTree(); $pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get(); - $nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree); + $bookTree = (new BookContents($chapter->book))->getTree(); + $nextPreviousLocator = new NextPreviousContentLocator($chapter, $bookTree); View::incrementFor($chapter); $this->setPageTitle($chapter->getShortName()); @@ -99,12 +96,10 @@ public function show(string $bookSlug, string $chapterSlug) 'book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter, - 'sidebarTree' => $sidebarTree, - 'watchOptions' => new UserEntityWatchOptions(user(), $chapter), 'pages' => $pages, 'next' => $nextPreviousLocator->getNext(), 'previous' => $nextPreviousLocator->getPrevious(), - 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($chapter), + 'bookTree' => $bookTree, ]); } diff --git a/app/Entities/Controllers/PageApiController.php b/app/Entities/Controllers/PageApiController.php index 38042e67058..ca8989f0764 100644 --- a/app/Entities/Controllers/PageApiController.php +++ b/app/Entities/Controllers/PageApiController.php @@ -1,5 +1,7 @@ queries->visibleForList() ->addSelect(['created_by', 'updated_by', 'revision_count', 'editor']); @@ -69,7 +73,7 @@ public function list() * Any images included via base64 data URIs will be extracted and saved as gallery * images against the page during upload. */ - public function create(Request $request) + public function create(Request $request): JsonResponse { $this->validate($request, $this->rules['create']); @@ -102,9 +106,9 @@ public function create(Request $request) * Comments for the page are provided in a tree-structure representing the hierarchy of top-level * comments and replies, for both archived and active comments. */ - public function read(string $id) + public function read(string $id): JsonResponse { - $page = $this->queries->findVisibleByIdOrFail($id); + $page = $this->queries->findVisibleByIdOrFail(intval($id)); $page = $page->forJsonDisplay(); $commentTree = (new CommentTree($page)); @@ -124,11 +128,11 @@ public function read(string $id) * Providing a 'book_id' or 'chapter_id' property will essentially move * the page into that parent element if you have permissions to do so. */ - public function update(Request $request, string $id) + public function update(Request $request, string $id): JsonResponse { $requestData = $this->validate($request, $this->rules['update']); - $page = $this->queries->findVisibleByIdOrFail($id); + $page = $this->queries->findVisibleByIdOrFail(intval($id)); $this->checkOwnablePermission(Permission::PageUpdate, $page); $parent = null; @@ -161,9 +165,9 @@ public function update(Request $request, string $id) * Delete a page. * This will typically send the page to the recycle bin. */ - public function delete(string $id) + public function delete(string $id): Response { - $page = $this->queries->findVisibleByIdOrFail($id); + $page = $this->queries->findVisibleByIdOrFail(intval($id)); $this->checkOwnablePermission(Permission::PageDelete, $page); $this->pageRepo->destroy($page); diff --git a/app/Entities/Controllers/PageController.php b/app/Entities/Controllers/PageController.php index 82edfbc2763..10a46d58bab 100644 --- a/app/Entities/Controllers/PageController.php +++ b/app/Entities/Controllers/PageController.php @@ -4,9 +4,9 @@ use BookStack\Activity\Models\View; use BookStack\Activity\Tools\CommentTree; -use BookStack\Activity\Tools\UserEntityWatchOptions; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Chapter; +use BookStack\Entities\Models\Page; use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Queries\PageQueries; use BookStack\Entities\Repos\PageRepo; @@ -20,11 +20,11 @@ use BookStack\Exceptions\PermissionsException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; -use BookStack\References\ReferenceFetcher; use BookStack\Util\HtmlContentFilter; use BookStack\Util\HtmlContentFilterConfig; use Exception; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use Throwable; @@ -35,7 +35,6 @@ public function __construct( protected PageRepo $pageRepo, protected PageQueries $queries, protected EntityQueries $entityQueries, - protected ReferenceFetcher $referenceFetcher ) { } @@ -95,7 +94,7 @@ public function createAsGuest(Request $request, string $bookSlug, ?string $chapt } /** - * Show form to continue editing a draft page. + * Show a form to continue editing a draft page. * * @throws NotFoundException */ @@ -103,6 +102,7 @@ public function editDraft(Request $request, string $bookSlug, int $pageId) { $draft = $this->queries->findVisibleByIdOrFail($pageId); $this->checkOwnablePermission(Permission::PageCreate, $draft->getParent()); + $this->ensureDraftAccess($draft); $editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', '')); $this->setPageTitle(trans('entities.pages_edit_draft')); @@ -124,6 +124,7 @@ public function store(Request $request, string $bookSlug, int $pageId) $draftPage = $this->queries->findVisibleByIdOrFail($pageId); $this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent()); + $this->ensureDraftAccess($draftPage); $page = $this->pageRepo->publishDraft($draftPage, $request->all()); @@ -151,11 +152,10 @@ public function show(string $bookSlug, string $pageSlug) $pageContent = (new PageContent($page)); $page->html = $pageContent->render(); - $pageNav = $pageContent->getNavigation($page->html); - $sidebarTree = (new BookContents($page->book))->getTree(); + $bookTree = (new BookContents($page->book))->getTree(); $commentTree = (new CommentTree($page)); - $nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree); + $nextPreviousLocator = new NextPreviousContentLocator($page, $bookTree); View::incrementFor($page); $this->setPageTitle($page->getShortName()); @@ -164,13 +164,10 @@ public function show(string $bookSlug, string $pageSlug) 'page' => $page, 'book' => $page->book, 'current' => $page, - 'sidebarTree' => $sidebarTree, + 'bookTree' => $bookTree, 'commentTree' => $commentTree, - 'pageNav' => $pageNav, - 'watchOptions' => new UserEntityWatchOptions(user(), $page), 'next' => $nextPreviousLocator->getNext(), 'previous' => $nextPreviousLocator->getPrevious(), - 'referenceCount' => $this->referenceFetcher->getReferenceCountToEntity($page), ]); } @@ -235,6 +232,7 @@ public function update(Request $request, string $bookSlug, string $pageSlug) * Save a draft update as a revision. * * @throws NotFoundException + * @throws PermissionsException */ public function saveDraft(Request $request, int $pageId) { @@ -245,6 +243,10 @@ public function saveDraft(Request $request, int $pageId) return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500); } + if ($page->draft) { + $this->ensureDraftAccess($page); + } + $draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown'])); $warnings = (new PageEditActivity($page))->getWarningMessagesForDraft($draft); @@ -294,11 +296,14 @@ public function showDelete(string $bookSlug, string $pageSlug) * Show the deletion page for the specified page. * * @throws NotFoundException + * @throws PermissionsException */ public function showDeleteDraft(string $bookSlug, int $pageId) { $page = $this->queries->findVisibleByIdOrFail($pageId); $this->checkOwnablePermission(Permission::PageUpdate, $page); + $this->ensureDraftAccess($page); + $this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()])); $usedAsTemplate = $this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 || @@ -340,7 +345,9 @@ public function destroyDraft(string $bookSlug, int $pageId) $page = $this->queries->findVisibleByIdOrFail($pageId); $book = $page->book; $chapter = $page->chapter; + $this->checkOwnablePermission(Permission::PageUpdate, $page); + $this->ensureDraftAccess($page); $this->pageRepo->destroy($page); @@ -358,8 +365,8 @@ public function destroyDraft(string $bookSlug, int $pageId) */ public function showRecentlyUpdated() { - $visibleBelongsScope = function (BelongsTo $query) { - $query->scopes('visible'); + $visibleBelongsScope = function (Relation $relation): void { + $relation->scopes('visible'); }; $pages = $this->queries->visibleForList() @@ -470,4 +477,14 @@ public function copy(Request $request, Cloner $cloner, string $bookSlug, string return redirect($pageCopy->getUrl()); } + + /** + * @throws PermissionsException + */ + protected function ensureDraftAccess(Page $draft): void + { + if (!$draft->draft || $draft->created_by !== user()->id) { + throw new PermissionsException('This page is already published or does not belong to you.'); + } + } } diff --git a/app/Entities/Controllers/PageRevisionController.php b/app/Entities/Controllers/PageRevisionController.php index cc6b79bfe45..801aeed3ed3 100644 --- a/app/Entities/Controllers/PageRevisionController.php +++ b/app/Entities/Controllers/PageRevisionController.php @@ -177,7 +177,7 @@ public function destroy(string $bookSlug, string $pageSlug, int $revId) */ public function destroyUserDraft(string $pageId) { - $page = $this->pageQueries->findVisibleByIdOrFail($pageId); + $page = $this->pageQueries->findVisibleByIdOrFail(intval($pageId)); $this->revisionRepo->deleteDraftsForCurrentUser($page); return response('', 200); diff --git a/app/Entities/Models/Book.php b/app/Entities/Models/Book.php index 10f04695a5e..abb081d712f 100644 --- a/app/Entities/Models/Book.php +++ b/app/Entities/Models/Book.php @@ -20,10 +20,10 @@ * @property ?int $image_id * @property ?int $default_template_id * @property ?int $sort_rule_id - * @property \Illuminate\Database\Eloquent\Collection $chapters - * @property \Illuminate\Database\Eloquent\Collection $pages - * @property \Illuminate\Database\Eloquent\Collection $directPages - * @property \Illuminate\Database\Eloquent\Collection $shelves + * @property \Illuminate\Database\Eloquent\Collection $chapters + * @property \Illuminate\Database\Eloquent\Collection $pages + * @property \Illuminate\Database\Eloquent\Collection $directPages + * @property \Illuminate\Database\Eloquent\Collection $shelves * @property ?SortRule $sortRule */ class Book extends Entity implements HasDescriptionInterface, HasCoverInterface, HasDefaultTemplateInterface diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index 27cfccaa836..296bd3a6621 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -46,7 +46,7 @@ * @property int|null $created_by * @property int|null $updated_by * @property int|null $owned_by - * @property Collection $tags + * @property Collection $tags * * @method static Entity|Builder visible() * @method static Builder withLastView() diff --git a/app/Entities/Models/EntityTable.php b/app/Entities/Models/EntityTable.php index 5780162d1d2..ed5cdf88970 100644 --- a/app/Entities/Models/EntityTable.php +++ b/app/Entities/Models/EntityTable.php @@ -25,6 +25,9 @@ class EntityTable extends Model /** * Get the entities that are visible to the current user. + * Note: This only applies basic permission filtering which considers the core entity table. + * This will not filter on information from other tables such as page draft status. + * That should be done after applying this scope. */ public function scopeVisible(Builder $query): Builder { diff --git a/app/Entities/Models/Page.php b/app/Entities/Models/Page.php index d3a392da6fa..c38f33af3ca 100644 --- a/app/Entities/Models/Page.php +++ b/app/Entities/Models/Page.php @@ -24,8 +24,8 @@ * @property int $revision_count * @property string $editor * @property Chapter|null $chapter - * @property Collection $attachments - * @property Collection $revisions + * @property Collection $attachments + * @property Collection $revisions * @property PageRevision $currentRevision */ class Page extends BookChild diff --git a/app/Entities/Queries/EntityQueries.php b/app/Entities/Queries/EntityQueries.php index 3ffa0adf3db..ab7f975ae4d 100644 --- a/app/Entities/Queries/EntityQueries.php +++ b/app/Entities/Queries/EntityQueries.php @@ -81,6 +81,13 @@ public function visibleForList(): Builder })->leftJoin('entity_page_data', function (JoinClause $join) { $join->on('entity_page_data.page_id', '=', 'entities.id') ->where('entities.type', '=', 'page'); + })->where(function ($query) { + $query->whereNull('entity_page_data.draft') + ->orWhere('entity_page_data.draft', '=', 0) + ->orWhere(function ($query) { + $query->where('entity_page_data.draft', '=', 1) + ->where('entities.owned_by', '=', user()->id); + }); }); } diff --git a/app/Entities/Queries/QueryPopular.php b/app/Entities/Queries/QueryPopular.php index 065ae82ef82..1782070de86 100644 --- a/app/Entities/Queries/QueryPopular.php +++ b/app/Entities/Queries/QueryPopular.php @@ -6,6 +6,7 @@ use BookStack\Entities\EntityProvider; use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Permissions\PermissionApplicator; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; @@ -20,6 +21,7 @@ public function __construct( public function run(int $count, int $page, array $filterModels): Collection { + /** @var Builder $query */ $query = $this->permissions ->restrictEntityRelationQuery(View::query(), 'views', 'viewable_id', 'viewable_type') ->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count')) diff --git a/app/Entities/Queries/QueryRecentlyViewed.php b/app/Entities/Queries/QueryRecentlyViewed.php index f28b8f8652f..8ba1a57badd 100644 --- a/app/Entities/Queries/QueryRecentlyViewed.php +++ b/app/Entities/Queries/QueryRecentlyViewed.php @@ -5,6 +5,7 @@ use BookStack\Activity\Models\View; use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Permissions\PermissionApplicator; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; class QueryRecentlyViewed @@ -22,6 +23,7 @@ public function run(int $count, int $page): Collection return collect(); } + /** @var Builder $query */ $query = $this->permissions->restrictEntityRelationQuery( View::query(), 'views', diff --git a/app/Entities/Queries/QueryTopFavourites.php b/app/Entities/Queries/QueryTopFavourites.php index 6340e35ef18..2719e94bc8b 100644 --- a/app/Entities/Queries/QueryTopFavourites.php +++ b/app/Entities/Queries/QueryTopFavourites.php @@ -5,6 +5,7 @@ use BookStack\Activity\Models\Favourite; use BookStack\Entities\Tools\MixedEntityListLoader; use BookStack\Permissions\PermissionApplicator; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Query\JoinClause; class QueryTopFavourites @@ -22,6 +23,7 @@ public function run(int $count, int $skip = 0) return collect(); } + /** @var Builder $query */ $query = $this->permissions ->restrictEntityRelationQuery(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type') ->select('favourites.*') diff --git a/app/Entities/Tools/Markdown/CustomListItemRenderer.php b/app/Entities/Tools/Markdown/CustomListItemRenderer.php index 0c506d7f9b5..14cdaa2eaa4 100644 --- a/app/Entities/Tools/Markdown/CustomListItemRenderer.php +++ b/app/Entities/Tools/Markdown/CustomListItemRenderer.php @@ -25,9 +25,13 @@ public function __construct() */ public function render(Node $node, ChildNodeRendererInterface $childRenderer) { + if (!($node instanceof ListItem)) { + return null; + } + $listItem = $this->baseRenderer->render($node, $childRenderer); - if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) { + if ($this->startsTaskListItem($node) && $listItem instanceof HtmlElement) { $listItem->setAttribute('class', 'task-list-item'); } diff --git a/app/Entities/Tools/PageContent.php b/app/Entities/Tools/PageContent.php index 9fb4596f5a0..c66dcbe4151 100644 --- a/app/Entities/Tools/PageContent.php +++ b/app/Entities/Tools/PageContent.php @@ -421,28 +421,41 @@ public function getNavigation(string $htmlContent): array */ protected function headerNodesToLevelList(DOMNodeList $nodeList): array { - $tree = collect($nodeList)->map(function (DOMElement $header) { + $minLevel = 6; + + $headerDetails = array_map(function (DOMNode $header) use (&$minLevel) { + if (!$header instanceof DOMElement) { + return null; + } + $text = trim(str_replace("\xc2\xa0", ' ', $header->nodeValue)); $text = mb_substr($text, 0, 100); + if (empty($text)) { + return null; + } + + $level = intval(str_replace('h', '', $header->nodeName)); + if ($level < $minLevel) { + $minLevel = $level; + } + return [ 'nodeName' => strtolower($header->nodeName), - 'level' => intval(str_replace('h', '', $header->nodeName)), + 'level' => $level, 'link' => '#' . $header->getAttribute('id'), 'text' => $text, ]; - })->filter(function ($header) { - return mb_strlen($header['text']) > 0; - }); + }, [...$nodeList]); - // Shift headers if only smaller headers have been used - $levelChange = ($tree->pluck('level')->min() - 1); - $tree = $tree->map(function ($header) use ($levelChange) { - $header['level'] -= ($levelChange); + $filtered = array_values(array_filter($headerDetails)); - return $header; - }); + // Shift headers if only smaller headers have been used + $levelChange = ($minLevel - 1); + foreach ($filtered as $index => $header) { + $filtered[$index]['level'] -= $levelChange; + } - return $tree->toArray(); + return $filtered; } } diff --git a/app/Entities/Tools/PageIncludeParser.php b/app/Entities/Tools/PageIncludeParser.php index af7ed4fc6a1..16917f41c8e 100644 --- a/app/Entities/Tools/PageIncludeParser.php +++ b/app/Entities/Tools/PageIncludeParser.php @@ -192,10 +192,10 @@ protected function splitNodeAtChildNode(DOMElement $parentNode, DOMNode $domNode /** * Get the parent paragraph of the given node, if existing. */ - protected function getParentParagraph(DOMNode $parent): ?DOMNode + protected function getParentParagraph(DOMNode $parent): ?DOMElement { do { - if (strtolower($parent->nodeName) === 'p') { + if (strtolower($parent->nodeName) === 'p' && $parent instanceof DOMElement) { return $parent; } diff --git a/app/Entities/Tools/SlugGenerator.php b/app/Entities/Tools/SlugGenerator.php index 6eec84a91c1..ac003b97a8f 100644 --- a/app/Entities/Tools/SlugGenerator.php +++ b/app/Entities/Tools/SlugGenerator.php @@ -52,7 +52,7 @@ protected function formatNameAsSlug(string $name): string { $slug = Str::slug($name); if ($slug === '') { - $slug = substr(md5(rand(1, 500)), 0, 5); + $slug = substr(md5(strval(rand(1, 500))), 0, 5); } return $slug; diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php index 96645aebfa6..26769ecbbf2 100644 --- a/app/Entities/Tools/TrashCan.php +++ b/app/Entities/Tools/TrashCan.php @@ -358,6 +358,8 @@ protected function restoreEntity(Entity $entity): int $entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction); } + $entity->rebuildPermissions(); + return $count; } diff --git a/app/Exceptions/ApiAuthException.php b/app/Exceptions/ApiAuthException.php index 070f7a8df0b..55a6afae6a4 100644 --- a/app/Exceptions/ApiAuthException.php +++ b/app/Exceptions/ApiAuthException.php @@ -4,7 +4,7 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; -class ApiAuthException extends \Exception implements HttpExceptionInterface +class ApiAuthException extends \Exception implements HttpExceptionInterface, ShowsApiExceptionMessage { protected int $status; @@ -23,4 +23,9 @@ public function getHeaders(): array { return []; } + + public function getMessageForApi(): string + { + return $this->getMessage(); + } } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 97a2b0a4f24..eec50afb170 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -126,16 +126,25 @@ protected function renderApiException(Throwable $e): JsonResponse $headers = $e->getHeaders(); } - if ($e instanceof ModelNotFoundException) { - $code = 404; - } - $responseData = [ 'error' => [ - 'message' => $e->getMessage(), + 'message' => 'An error occurred', ], ]; + if ($e instanceof ModelNotFoundException) { + $responseData['error']['message'] = 'The requested resource could not be found.'; + $code = 404; + } + + if ($e instanceof ShowsApiExceptionMessage) { + $responseData['error']['message'] = $e->getMessageForApi(); + } + + if (app()->hasDebugModeEnabled()) { + $responseData['error']['message'] = $e->getMessage(); + } + if ($e instanceof ValidationException) { $responseData['error']['message'] = 'The given data was invalid.'; $responseData['error']['validation'] = $e->errors(); @@ -160,14 +169,4 @@ protected function unauthenticated($request, AuthenticationException $exception) return redirect()->guest('login'); } - - /** - * Convert a validation exception into a JSON response. - * - * @param Request $request - */ - protected function invalidJson($request, ValidationException $exception): JsonResponse - { - return response()->json($exception->errors(), $exception->status); - } } diff --git a/app/Exceptions/NotifyException.php b/app/Exceptions/NotifyException.php index d2fba30c4e9..b0514059d79 100644 --- a/app/Exceptions/NotifyException.php +++ b/app/Exceptions/NotifyException.php @@ -8,13 +8,13 @@ /** * An exception that is thrown to notify the user of something which went wrong. - * Typically these should be translated messages since they will be shown to the end user - * via a pop up notification error message in the UI. + * Typically, these should be translated messages since they will be shown to the end user + * via a pop-up notification error message in the UI. * * This exception is not intended to be used for internal system/application errors, * and therefore will not be logged by the exception handler. */ -class NotifyException extends Exception implements Responsable, HttpExceptionInterface +class NotifyException extends Exception implements Responsable, HttpExceptionInterface, ShowsApiExceptionMessage { public function __construct( string $message, @@ -61,4 +61,9 @@ public function toResponse($request) return redirect($this->redirectLocation); } + + public function getMessageForApi(): string + { + return $this->getMessage(); + } } diff --git a/app/Exceptions/PermissionsException.php b/app/Exceptions/PermissionsException.php index 64da55d21f4..ca5104df9a3 100644 --- a/app/Exceptions/PermissionsException.php +++ b/app/Exceptions/PermissionsException.php @@ -4,6 +4,10 @@ use Exception; -class PermissionsException extends Exception +class PermissionsException extends Exception implements ShowsApiExceptionMessage { + public function getMessageForApi(): string + { + return $this->getMessage(); + } } diff --git a/app/Exceptions/PrettyException.php b/app/Exceptions/PrettyException.php index 606085231f7..d8a9d315021 100644 --- a/app/Exceptions/PrettyException.php +++ b/app/Exceptions/PrettyException.php @@ -6,7 +6,7 @@ use Illuminate\Contracts\Support\Responsable; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; -class PrettyException extends Exception implements Responsable, HttpExceptionInterface +class PrettyException extends Exception implements Responsable, HttpExceptionInterface, ShowsApiExceptionMessage { protected ?string $subtitle = null; protected ?string $details = null; @@ -56,4 +56,16 @@ public function getHeaders(): array { return []; } + + public function getMessageForApi(): string + { + $message = $this->getMessage() . '.'; + if ($this->subtitle) { + $message .= " {$this->subtitle}."; + } + if ($this->details) { + $message .= " {$this->details}."; + } + return $message; + } } diff --git a/app/Exceptions/ShowsApiExceptionMessage.php b/app/Exceptions/ShowsApiExceptionMessage.php new file mode 100644 index 00000000000..66bf4388642 --- /dev/null +++ b/app/Exceptions/ShowsApiExceptionMessage.php @@ -0,0 +1,13 @@ +type === 'page' || $import->type === 'chapter') { $parentModel = $parent ? $this->entityQueries->findVisibleByStringIdentifier($parent) : null; + if ($parentModel && !($parentModel instanceof Book || $parentModel instanceof Chapter)) { + throw new ZipImportException(['Selected parent is not a book or chapter']); + } } DB::beginTransaction(); diff --git a/app/Exports/ZipExports/Models/ZipExportBook.php b/app/Exports/ZipExports/Models/ZipExportBook.php index ab3fd90ec1c..006a6b2d2f5 100644 --- a/app/Exports/ZipExports/Models/ZipExportBook.php +++ b/app/Exports/ZipExports/Models/ZipExportBook.php @@ -87,7 +87,7 @@ public static function validate(ZipValidationHelper $context, array $data): arra 'id' => ['nullable', 'int', $context->uniqueIdRule('book')], 'name' => ['required', 'string', 'min:1'], 'description_html' => ['nullable', 'string'], - 'cover' => ['nullable', 'string', $context->fileReferenceRule()], + 'cover' => ['nullable', 'string', $context->imageFileReferenceRule()], 'tags' => ['array'], 'pages' => ['array'], 'chapters' => ['array'], diff --git a/app/Exports/ZipExports/Models/ZipExportImage.php b/app/Exports/ZipExports/Models/ZipExportImage.php index 4c71af0c3a2..1432e6cf0d8 100644 --- a/app/Exports/ZipExports/Models/ZipExportImage.php +++ b/app/Exports/ZipExports/Models/ZipExportImage.php @@ -32,11 +32,10 @@ public function metadataOnly(): void public static function validate(ZipValidationHelper $context, array $data): array { - $acceptedImageTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp']; $rules = [ 'id' => ['nullable', 'int', $context->uniqueIdRule('image')], 'name' => ['required', 'string', 'min:1'], - 'file' => ['required', 'string', $context->fileReferenceRule($acceptedImageTypes)], + 'file' => ['required', 'string', $context->imageFileReferenceRule()], 'type' => ['required', 'string', Rule::in(['gallery', 'drawio'])], ]; diff --git a/app/Exports/ZipExports/ZipExportFiles.php b/app/Exports/ZipExports/ZipExportFiles.php index 8f0a6bd4031..0386d8278f9 100644 --- a/app/Exports/ZipExports/ZipExportFiles.php +++ b/app/Exports/ZipExports/ZipExportFiles.php @@ -80,7 +80,7 @@ protected function getAllFileNames(): array } /** - * Extract each of the ZIP export tracked files. + * Extract each of the ZIP export-tracked files. * Calls the given callback for each tracked file, passing a temporary * file reference of the file contents, and the zip-local tracked reference. */ diff --git a/app/Exports/ZipExports/ZipImportRunner.php b/app/Exports/ZipExports/ZipImportRunner.php index 9fa7dec3afe..41e9038adea 100644 --- a/app/Exports/ZipExports/ZipImportRunner.php +++ b/app/Exports/ZipExports/ZipImportRunner.php @@ -48,7 +48,7 @@ public function __construct( * Returns the top-level entity item which was imported. * @throws ZipImportException */ - public function run(Import $import, ?Entity $parent = null): Entity + public function run(Import $import, Book|Chapter|null $parent = null): Entity { $zipPath = $this->getZipPath($import); $reader = new ZipExportReader($zipPath); @@ -96,7 +96,7 @@ public function run(Import $import, ?Entity $parent = null): Entity /** * Revert any files which have been stored during this import process. - * Considers files only, and avoids the database under the + * Considers files only and avoids the database under the * assumption that the database may already have been * reverted as part of a transaction rollback. */ @@ -129,7 +129,7 @@ protected function importBook(ZipExportBook $exportBook, ZipExportReader $reader $book = $this->bookRepo->create([ 'name' => $exportBook->name, 'description_html' => $exportBook->description_html ?? '', - 'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader) : null, + 'image' => $exportBook->cover ? $this->zipFileToUploadedFile($exportBook->cover, $reader, true) : null, 'tags' => $this->exportTagsToInputArray($exportBook->tags), ]); @@ -227,18 +227,11 @@ protected function importAttachment(ZipExportAttachment $exportAttachment, Page protected function importImage(ZipExportImage $exportImage, Page $page, ZipExportReader $reader): Image { - $mime = $reader->sniffFileMime($exportImage->file); - $extension = explode('/', $mime)[1]; - - $file = $this->zipFileToUploadedFile($exportImage->file, $reader); + $file = $this->zipFileToUploadedFile($exportImage->file, $reader, true); $image = $this->imageService->saveNewFromUpload( $file, $exportImage->type, $page->id, - null, - null, - true, - $exportImage->name . '.' . $extension, ); $image->name = $exportImage->name; @@ -261,7 +254,7 @@ protected function exportTagsToInputArray(array $exportTags): array return $tags; } - protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile + protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader, bool $forceExtensionFromMime = false): UploadedFile { if (!$reader->fileWithinSizeLimit($fileName)) { throw new ZipImportException([ @@ -277,7 +270,16 @@ protected function zipFileToUploadedFile(string $fileName, ZipExportReader $read $this->tempFilesToCleanup[] = $tempPath; - return new UploadedFile($tempPath, $fileName); + $intendedUploadName = $fileName; + if ($forceExtensionFromMime) { + $mime = $reader->sniffFileMime($fileName); + $extension = explode('/', $mime)[1]; + if (!str_ends_with(strtolower($intendedUploadName), '.' . $extension)) { + $intendedUploadName .= '.' . $extension; + } + } + + return new UploadedFile($tempPath, $intendedUploadName); } /** diff --git a/app/Exports/ZipExports/ZipValidationHelper.php b/app/Exports/ZipExports/ZipValidationHelper.php index fd9cd784472..2bcb5fb9747 100644 --- a/app/Exports/ZipExports/ZipValidationHelper.php +++ b/app/Exports/ZipExports/ZipValidationHelper.php @@ -3,6 +3,7 @@ namespace BookStack\Exports\ZipExports; use BookStack\Exports\ZipExports\Models\ZipExportModel; +use BookStack\Uploads\ImageService; use Illuminate\Validation\Factory; class ZipValidationHelper @@ -38,6 +39,11 @@ public function fileReferenceRule(array $acceptedMimes = []): ZipFileReferenceRu return new ZipFileReferenceRule($this, $acceptedMimes); } + public function imageFileReferenceRule(): ZipFileReferenceRule + { + return new ZipFileReferenceRule($this, ImageService::getSupportedMimeTypes()); + } + public function uniqueIdRule(string $type): ZipUniqueIdRule { return new ZipUniqueIdRule($this, $type); diff --git a/app/Http/Controller.php b/app/Http/Controller.php index 796505795e5..09c38ee6123 100644 --- a/app/Http/Controller.php +++ b/app/Http/Controller.php @@ -7,6 +7,7 @@ use BookStack\Exceptions\NotifyException; use BookStack\Facades\Activity; use BookStack\Permissions\Permission; +use BookStack\Users\Models\OwnableInterface; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\JsonResponse; @@ -80,7 +81,7 @@ protected function preventGuestAccess(): void /** * Check the current user's permissions against an ownable item otherwise throw an exception. */ - protected function checkOwnablePermission(string|Permission $permission, Model $ownable, string $redirectLocation = '/'): void + protected function checkOwnablePermission(string|Permission $permission, Model&OwnableInterface $ownable, string $redirectLocation = '/'): void { if (!userCan($permission, $ownable)) { $this->showPermissionError($redirectLocation); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 00bf8cbe1c5..e7497e743c4 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -36,6 +36,7 @@ class Kernel extends HttpKernel \BookStack\Http\Middleware\CheckEmailConfirmed::class, \BookStack\Http\Middleware\RunThemeActions::class, \BookStack\Http\Middleware\Localization::class, + \BookStack\Http\Middleware\ClearPerRequestCaches::class, ], 'api' => [ \BookStack\Http\Middleware\ThrottleApiRequests::class, diff --git a/app/Http/Middleware/ClearPerRequestCaches.php b/app/Http/Middleware/ClearPerRequestCaches.php new file mode 100644 index 00000000000..5210062e7ce --- /dev/null +++ b/app/Http/Middleware/ClearPerRequestCaches.php @@ -0,0 +1,33 @@ +clearCaches(); + + return $next($request); + } + + protected function clearCaches(): void + { + $this->viewBlockManager->clearLocalCache(); + } +} diff --git a/app/Permissions/JointPermissionBuilder.php b/app/Permissions/JointPermissionBuilder.php index 94f18916d4a..101e3a00f29 100644 --- a/app/Permissions/JointPermissionBuilder.php +++ b/app/Permissions/JointPermissionBuilder.php @@ -70,7 +70,11 @@ public function rebuildForEntity(Entity $entity): void } if ($entity instanceof Chapter) { - foreach ($entity->pages as $page) { + $childPages = $entity->pages() + ->withTrashed() + ->select(['id', 'owned_by', 'book_id', 'chapter_id']) + ->get(); + foreach ($childPages as $page) { $entities[] = $page; } } @@ -101,6 +105,7 @@ public function rebuildForRole(Role $role) /** * Get a query for fetching a book with its children. + * @return Builder */ protected function bookFetchQuery(): Builder { @@ -117,9 +122,11 @@ protected function bookFetchQuery(): Builder /** * Build joint permissions for the given book and role combinations. + * @param EloquentCollection $books */ protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void { + /** @var EloquentCollection $entities */ $entities = clone $books; /** @var Book $book */ diff --git a/app/References/Reference.php b/app/References/Reference.php index df8a3a78932..602e19eab59 100644 --- a/app/References/Reference.php +++ b/app/References/Reference.php @@ -2,8 +2,8 @@ namespace BookStack\References; +use BookStack\App\Model; use BookStack\Permissions\Models\JointPermission; -use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\MorphTo; diff --git a/app/References/ReferenceFetcher.php b/app/References/ReferenceFetcher.php index 8588c6e2c8e..0c76b46982c 100644 --- a/app/References/ReferenceFetcher.php +++ b/app/References/ReferenceFetcher.php @@ -22,6 +22,7 @@ public function __construct( */ public function getReferencesToEntity(Entity $entity, bool $withContents = false): Collection { + /** @var Collection $references */ $references = $this->queryReferencesToEntity($entity)->get(); $this->mixedEntityListLoader->loadIntoRelations($references->all(), 'from', false, $withContents); @@ -37,6 +38,9 @@ public function getReferenceCountToEntity(Entity $entity): int return $this->queryReferencesToEntity($entity)->count(); } + /** + * @return Builder + */ protected function queryReferencesToEntity(Entity $entity): Builder { $baseQuery = Reference::query() diff --git a/app/Search/SearchRunner.php b/app/Search/SearchRunner.php index d49cb439a6b..60d111d0839 100644 --- a/app/Search/SearchRunner.php +++ b/app/Search/SearchRunner.php @@ -4,6 +4,7 @@ use BookStack\Entities\EntityProvider; use BookStack\Entities\Models\Entity; +use BookStack\Entities\Models\EntityTable; use BookStack\Entities\Queries\EntityQueries; use BookStack\Entities\Tools\EntityHydrator; use BookStack\Permissions\PermissionApplicator; @@ -90,6 +91,8 @@ public function searchChapter(int $chapterId, string $searchString): Collection /** * Get a page of result data from the given query based on the provided page parameters. + * @param EloquentBuilder $query + * @return Collection */ protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int $count): Collection { @@ -106,6 +109,7 @@ protected function getPageOfDataFromQuery(EloquentBuilder $query, int $page, int /** * Create a search query for an entity. * @param string[] $entityTypes + * @return EloquentBuilder */ protected function buildQuery(SearchOptions $searchOpts, array $entityTypes): EloquentBuilder { diff --git a/app/Settings/SettingService.php b/app/Settings/SettingService.php index e0b13618012..e3112e9c7d9 100644 --- a/app/Settings/SettingService.php +++ b/app/Settings/SettingService.php @@ -243,9 +243,9 @@ public function putForCurrentUser(string $key, string $value): bool /** * Convert a setting key into a user-specific key. */ - protected function userKey(string $userId, string $key = ''): string + protected function userKey(int $userId, string $key = ''): string { - return 'user:' . $userId . ':' . $key; + return 'user:' . strval($userId) . ':' . $key; } /** @@ -264,10 +264,18 @@ public function remove(string $key): void } } + /** + * Remove a user-specific setting from the database, for the current access user. + */ + public function removeForCurrentUser(string $key): void + { + $this->remove($this->userKey(user()->id, $key)); + } + /** * Delete settings for a given user id. */ - public function deleteUserSettings(string $userId): void + public function deleteUserSettings(int $userId): void { Setting::query() ->where('setting_key', 'like', $this->userKey($userId) . '%') diff --git a/app/Sorting/SortRule.php b/app/Sorting/SortRule.php index bf53365a201..ffe2d411534 100644 --- a/app/Sorting/SortRule.php +++ b/app/Sorting/SortRule.php @@ -38,21 +38,35 @@ public function setOperations(array $options): void $this->sequence = implode(',', $values); } + /** + * @inheritDoc + */ public function logDescriptor(): string { return "({$this->id}) {$this->name}"; } + /** + * Get the URL to where this rule can be managed. + */ public function getUrl(): string { return url("/settings/sorting/rules/{$this->id}"); } + /** + * Get the books which are specifically set to use this sort rule. + * @return HasMany + */ public function books(): HasMany { return $this->hasMany(Book::class, 'entity_container_data.sort_rule_id', 'id'); } + /** + * Get all the available sort rules, ordered by name, with the number of books using each. + * @return Collection + */ public static function allByName(): Collection { return static::query() diff --git a/app/Theming/ThemeEvents.php b/app/Theming/ThemeEvents.php index 511a9c1de7a..46fa7c2c08f 100644 --- a/app/Theming/ThemeEvents.php +++ b/app/Theming/ThemeEvents.php @@ -180,6 +180,15 @@ class ThemeEvents */ const THEME_REGISTER_VIEWS = 'theme_register_views'; + /** + * View blocks register event. + * Runs once a ViewBlockManager instance is available so that custom blocks can be registered + * for use within user-configurable layouts in the system. + * + * @param \BookStack\View\ViewBlockManager $manager + */ + const VIEW_BLOCKS_REGISTER = 'view_blocks_register'; + /** * Web before middleware action. * Runs before the request is handled but after all other middleware apart from those diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php index 05227243a76..1619bebbcdd 100644 --- a/app/Uploads/Attachment.php +++ b/app/Uploads/Attachment.php @@ -10,6 +10,7 @@ use BookStack\Users\Models\HasCreatorAndUpdater; use BookStack\Users\Models\OwnableInterface; use BookStack\Users\Models\User; +use BookStack\Util\UrlFilter; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -71,7 +72,7 @@ public function jointPermissions(): HasMany public function getUrl($openInline = false): string { if ($this->external && !str_starts_with($this->path, 'http')) { - return $this->path; + return (new UrlFilter($this->path))->clean(); } return url('/attachments/' . $this->id . ($openInline ? '?open=true' : '')); diff --git a/app/Uploads/AttachmentService.php b/app/Uploads/AttachmentService.php index dabd537292f..3de84bdd0a8 100644 --- a/app/Uploads/AttachmentService.php +++ b/app/Uploads/AttachmentService.php @@ -102,7 +102,7 @@ public function saveNewFromLink(string $name, string $link, int $page_id): Attac /** * Updates the ordering for a listing of attached files. */ - public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId) + public function updateFileOrderWithinPage(array $attachmentOrder, int $pageId) { foreach ($attachmentOrder as $index => $attachmentId) { Attachment::query()->where('uploaded_to', '=', $pageId) diff --git a/app/Uploads/Base64UriMimeRule.php b/app/Uploads/Base64UriMimeRule.php new file mode 100644 index 00000000000..d49e6c1e0c2 --- /dev/null +++ b/app/Uploads/Base64UriMimeRule.php @@ -0,0 +1,47 @@ +translate(['mime' => $this->requiredMime]); + return; + } + + $imageData = base64_decode($imageDataEncoded); + if (empty($imageData)) { + $fail('validation.base64_uri_mime')->translate(['mime' => $this->requiredMime]); + return; + } + + $sniffer = new WebSafeMimeSniffer(); + $mime = $sniffer->sniff($imageData); + + if ($mime !== $this->requiredMime) { + $fail('validation.base64_uri_mime')->translate(['mime' => $this->requiredMime]); + } + } +} diff --git a/app/Uploads/Controllers/AttachmentApiController.php b/app/Uploads/Controllers/AttachmentApiController.php index 2448b79b5d5..f540833f0d4 100644 --- a/app/Uploads/Controllers/AttachmentApiController.php +++ b/app/Uploads/Controllers/AttachmentApiController.php @@ -24,7 +24,7 @@ public function __construct( /** * Get a listing of attachments visible to the user. - * The external property indicates whether the attachment is simple a link. + * The external property indicates whether the attachment is simply a link. * A false value for the external property would indicate a file upload. */ public function list() @@ -39,7 +39,7 @@ public function list() * An uploaded_to value must be provided containing an ID of the page * that this upload will be related to. * - * If you're uploading a file the POST data should be provided via + * If you're uploading a file, the POST data should be provided via * a multipart/form-data type request instead of JSON. * * @throws ValidationException @@ -71,7 +71,7 @@ public function create(Request $request) } /** - * Get the details & content of a single attachment of the given ID. + * Get the details and content of a single attachment of the given ID. * The attachment link or file content is provided via a 'content' property. * For files the content will be base64 encoded. * @@ -120,7 +120,7 @@ public function read(string $id) /** * Update the details of a single attachment. - * As per the create endpoint, if a file is being provided as the attachment content + * As per the create endpoint, if a file is being provided as the attachment content, * the request should be formatted as a multipart/form-data request instead of JSON. * * @throws ValidationException @@ -134,9 +134,12 @@ public function update(Request $request, string $id) $page = $attachment->page; if ($requestData['uploaded_to'] ?? false) { - $pageId = $request->input('uploaded_to'); - $page = $this->pageQueries->findVisibleByIdOrFail($pageId); - $attachment->uploaded_to = $requestData['uploaded_to']; + $pageId = intval($requestData['uploaded_to']); + if ($pageId !== $page->id) { + $this->checkOwnablePermission(Permission::PageUpdate, $page); + $page = $this->pageQueries->findVisibleByIdOrFail($pageId); + $attachment->uploaded_to = $pageId; + } } $this->checkOwnablePermission(Permission::PageView, $page); diff --git a/app/Uploads/Controllers/AttachmentController.php b/app/Uploads/Controllers/AttachmentController.php index bdd96d6ad7b..5e128bc3466 100644 --- a/app/Uploads/Controllers/AttachmentController.php +++ b/app/Uploads/Controllers/AttachmentController.php @@ -11,6 +11,7 @@ use BookStack\Permissions\Permission; use BookStack\Uploads\Attachment; use BookStack\Uploads\AttachmentService; +use BookStack\Util\UrlFilter; use Exception; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Http\Request; @@ -92,8 +93,9 @@ public function getUpdateForm(string $attachmentId) /** @var Attachment $attachment */ $attachment = Attachment::query()->findOrFail($attachmentId); + $this->checkOwnablePermission(Permission::PageView, $attachment->page); $this->checkOwnablePermission(Permission::PageUpdate, $attachment->page); - $this->checkOwnablePermission(Permission::AttachmentCreate, $attachment); + $this->checkOwnablePermission(Permission::AttachmentUpdate, $attachment); return view('attachments.manager-edit-form', [ 'attachment' => $attachment, @@ -221,7 +223,8 @@ public function get(Request $request, string $attachmentId) } if ($attachment->external) { - return redirect($attachment->path); + $url = (new UrlFilter($attachment->path))->clean(); + return redirect($url); } $fileName = $attachment->getFileName(); diff --git a/app/Uploads/Controllers/DrawioImageController.php b/app/Uploads/Controllers/DrawioImageController.php index 53ae7900408..3c2d9aa589c 100644 --- a/app/Uploads/Controllers/DrawioImageController.php +++ b/app/Uploads/Controllers/DrawioImageController.php @@ -6,6 +6,7 @@ use BookStack\Exceptions\ImageUploadException; use BookStack\Http\Controller; use BookStack\Permissions\Permission; +use BookStack\Uploads\Base64UriMimeRule; use BookStack\Uploads\ImageRepo; use BookStack\Uploads\ImageResizer; use BookStack\Util\OutOfMemoryHandler; @@ -57,7 +58,7 @@ public function create(Request $request) { $this->checkPermission(Permission::ImageCreateAll); $validated = $this->validate($request, [ - 'image' => ['required', 'string'], + 'image' => ['required', 'string', new Base64UriMimeRule('image/png')], 'uploaded_to' => ['required', 'integer'], ]); diff --git a/app/Uploads/Controllers/ImageController.php b/app/Uploads/Controllers/ImageController.php index da67639c16d..164d10e6bd6 100644 --- a/app/Uploads/Controllers/ImageController.php +++ b/app/Uploads/Controllers/ImageController.php @@ -153,7 +153,7 @@ public function rebuildThumbnails(string $id) } /** - * Check related page permission and ensure type is drawio or gallery. + * Check related page permission and ensure the type is drawio or gallery. * @throws NotifyException */ protected function checkImagePermission(Image $image): void diff --git a/app/Uploads/Controllers/ImageGalleryApiController.php b/app/Uploads/Controllers/ImageGalleryApiController.php index 6a72e4c30e4..abaf6939ff5 100644 --- a/app/Uploads/Controllers/ImageGalleryApiController.php +++ b/app/Uploads/Controllers/ImageGalleryApiController.php @@ -97,7 +97,7 @@ public function create(Request $request) */ public function read(string $id) { - $image = Image::query()->scopes(['visible'])->findOrFail($id); + $image = $this->imageRepo->getVisiblePageImageById($id); return response()->json($this->formatForSingleResponse($image)); } @@ -108,7 +108,7 @@ public function read(string $id) */ public function readData(string $id) { - $image = Image::query()->scopes(['visible'])->findOrFail($id); + $image = $this->imageRepo->getVisiblePageImageById($id); return $this->imageService->streamImageFromStorageResponse('gallery', $image->path); } @@ -141,8 +141,7 @@ public function readDataForUrl(Request $request) public function update(Request $request, string $id) { $data = $this->validate($request, $this->rules()['update']); - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission(Permission::PageView, $image->getPage()); + $image = $this->imageRepo->getVisiblePageImageById($id); $this->checkOwnablePermission(Permission::ImageUpdate, $image); $this->imageRepo->updateImageDetails($image, $data); @@ -160,16 +159,16 @@ public function update(Request $request, string $id) */ public function delete(string $id) { - $image = $this->imageRepo->getById($id); - $this->checkOwnablePermission(Permission::PageView, $image->getPage()); + $image = $this->imageRepo->getVisiblePageImageById($id); $this->checkOwnablePermission(Permission::ImageDelete, $image); + $this->imageRepo->destroyImage($image); return response('', 204); } /** - * Format the given image model for single-result display. + * Format the given image model for a single-result display. */ protected function formatForSingleResponse(Image $image): array { diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 81b6db6fd22..879e0043a1e 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -39,6 +39,7 @@ public function jointPermissions(): HasMany /** * Scope the query to just the images visible to the user based upon the * user visibility of the uploaded_to page. + * This limits results to just page-based images (gallery and drawio types). */ public function scopeVisible(Builder $query): Builder { diff --git a/app/Uploads/ImageRepo.php b/app/Uploads/ImageRepo.php index e87e22b3a3c..ef3ed9e1f89 100644 --- a/app/Uploads/ImageRepo.php +++ b/app/Uploads/ImageRepo.php @@ -27,6 +27,13 @@ public function getById($id): Image return Image::query()->findOrFail($id); } + public function getVisiblePageImageById($id): Image + { + return Image::query() + ->scopes('visible') + ->findOrFail($id); + } + /** * Execute a paginated query, returning in a standard format. * Also runs the query through the restriction system. diff --git a/app/Uploads/ImageService.php b/app/Uploads/ImageService.php index ed640913ec3..1a66be5ff4a 100644 --- a/app/Uploads/ImageService.php +++ b/app/Uploads/ImageService.php @@ -4,6 +4,8 @@ use BookStack\Entities\Queries\EntityQueries; use BookStack\Exceptions\ImageUploadException; +use BookStack\Exceptions\PrettyException; +use BookStack\Http\DownloadResponseFactory; use Exception; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -34,9 +36,8 @@ public function saveNewFromUpload( ?int $resizeWidth = null, ?int $resizeHeight = null, bool $keepRatio = true, - string $imageName = '', ): Image { - $imageName = $imageName ?: $uploadedFile->getClientOriginalName(); + $imageName = $uploadedFile->getClientOriginalName(); $imageData = file_get_contents($uploadedFile->getRealPath()); if ($resizeWidth !== null || $resizeHeight !== null) { @@ -368,7 +369,18 @@ public function streamImageFromStorageResponse(string $imageType, string $path): { $disk = $this->storage->getDisk($imageType); - return $disk->response($path); + $stream = $disk->stream($path); + $fileSize = $disk->size($path); + $imageName = basename($path); + $downloadResponseFactory = new DownloadResponseFactory(request()); + $response = $downloadResponseFactory->streamedInline($stream, $imageName, $fileSize); + + $contentType = $response->headers->get('Content-Type'); + if (!str_starts_with($contentType, 'image/')) { + throw new PrettyException('Invalid non-image file type when streaming from storage', 415); + } + + return $response; } /** @@ -378,6 +390,14 @@ public function streamImageFromStorageResponse(string $imageType, string $path): */ public static function isExtensionSupported(string $extension): bool { - return in_array($extension, static::$supportedExtensions); + return in_array(strtolower($extension), static::$supportedExtensions); + } + + /** + * Get all mime-types for images formats which BookStack supports. + */ + public static function getSupportedMimeTypes(): array + { + return array_map(fn($ext) => 'image/' . $ext, static::$supportedExtensions); } } diff --git a/app/Uploads/ImageStorage.php b/app/Uploads/ImageStorage.php index abf2b429b1c..43f448ff1bb 100644 --- a/app/Uploads/ImageStorage.php +++ b/app/Uploads/ImageStorage.php @@ -2,6 +2,7 @@ namespace BookStack\Uploads; +use BookStack\Exceptions\ImageUploadException; use Illuminate\Filesystem\FilesystemManager; use Illuminate\Support\Str; @@ -45,6 +46,7 @@ public function usingSecureImages(): bool /** * Clean up an image file name to be both URL and storage safe. + * @throws ImageUploadException */ public function cleanImageFileName(string $name): string { @@ -54,6 +56,10 @@ public function cleanImageFileName(string $name): string $name = implode('-', $nameParts); $name = Str::slug($name); + if (!ImageService::isExtensionSupported($extension)) { + throw new ImageUploadException('Non supported image extension used when storing image: ' . $extension); + } + if (strlen($name) === 0) { $name = Str::random(10); } diff --git a/app/Uploads/ImageStorageDisk.php b/app/Uploads/ImageStorageDisk.php index 36d6721de6c..ad314c5c89b 100644 --- a/app/Uploads/ImageStorageDisk.php +++ b/app/Uploads/ImageStorageDisk.php @@ -68,6 +68,14 @@ public function stream(string $path): mixed return $this->filesystem->readStream($this->adjustPathForDisk($path)); } + /** + * Get the size of the file at the given path. + */ + public function size(string $path): int + { + return $this->filesystem->size($this->adjustPathForDisk($path)); + } + /** * Save the given image data at the given path. Can choose to set * the image as public which will update its visibility after saving. diff --git a/app/Users/Controllers/RoleApiController.php b/app/Users/Controllers/RoleApiController.php index 93ecc549bb4..6f0c59651a5 100644 --- a/app/Users/Controllers/RoleApiController.php +++ b/app/Users/Controllers/RoleApiController.php @@ -87,7 +87,7 @@ public function create(Request $request) */ public function read(string $id) { - $role = $this->permissionsRepo->getRoleById($id); + $role = $this->permissionsRepo->getRoleById(intval($id)); $this->singleFormatter($role); return response()->json($role); diff --git a/app/Users/Controllers/RoleController.php b/app/Users/Controllers/RoleController.php index b9f06dace84..56a6c7416ca 100644 --- a/app/Users/Controllers/RoleController.php +++ b/app/Users/Controllers/RoleController.php @@ -94,7 +94,7 @@ public function store(Request $request) public function edit(string $id) { $this->checkPermission(Permission::UserRolesManage); - $role = $this->permissionsRepo->getRoleById($id); + $role = $this->permissionsRepo->getRoleById(intval($id)); $this->setPageTitle(trans('settings.role_edit')); @@ -129,7 +129,7 @@ public function update(Request $request, string $id) public function showDelete(string $id) { $this->checkPermission(Permission::UserRolesManage); - $role = $this->permissionsRepo->getRoleById($id); + $role = $this->permissionsRepo->getRoleById(intval($id)); $roles = $this->permissionsRepo->getAllRolesExcept($role); $blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]); $roles->prepend($blankRole); @@ -151,7 +151,7 @@ public function delete(Request $request, string $id) try { $migrateRoleId = intval($request->input('migrate_role_id') ?: "0"); - $this->permissionsRepo->deleteRole($id, $migrateRoleId); + $this->permissionsRepo->deleteRole(intval($id), $migrateRoleId); } catch (PermissionsException $e) { $this->showErrorNotification($e->getMessage()); diff --git a/app/Users/Controllers/UserAccountController.php b/app/Users/Controllers/UserAccountController.php index 21816d5b89b..71d8ca78dfc 100644 --- a/app/Users/Controllers/UserAccountController.php +++ b/app/Users/Controllers/UserAccountController.php @@ -10,6 +10,7 @@ use BookStack\Settings\UserShortcutMap; use BookStack\Uploads\ImageRepo; use BookStack\Users\UserRepo; +use BookStack\View\ViewBlockManager; use Closure; use Illuminate\Http\Request; use Illuminate\Validation\Rules\Password; @@ -159,6 +160,37 @@ public function updateNotifications(Request $request) return redirect('/my-account/notifications'); } + /** + * Show the view for the "Interface Preferences" user account area. + */ + public function showInterface(ViewBlockManager $viewBlockManager) + { + $this->setPageTitle(trans('preferences.interface')); + + return view('users.account.interface', [ + 'category' => 'interface', + 'namedLocations' => $viewBlockManager->getNamedLocations(), + ]); + } + + /** + * Handle the submission of the interface preferences form. + */ + public function updateInterface(Request $request) + { + $this->preventAccessInDemoMode(); + + $user = user(); + $validated = $this->validate($request, [ + 'language' => ['string', 'max:15', 'alpha_dash'], + 'display_mode' => ['string', 'max:15', 'alpha_dash'], + ]); + + $this->userRepo->update($user, $validated, userCan(Permission::UsersManage)); + + return redirect('/my-account/interface'); + } + /** * Show the view for the "Access & Security" account options. */ diff --git a/app/Users/Controllers/UserApiController.php b/app/Users/Controllers/UserApiController.php index ebc17e262f3..07eefadc7b1 100644 --- a/app/Users/Controllers/UserApiController.php +++ b/app/Users/Controllers/UserApiController.php @@ -110,7 +110,7 @@ public function create(Request $request) */ public function read(string $id) { - $user = $this->userRepo->getById($id); + $user = $this->userRepo->getById(intval($id)); $this->singleFormatter($user); return response()->json($user); @@ -124,8 +124,8 @@ public function read(string $id) */ public function update(Request $request, string $id) { - $data = $this->validate($request, $this->rules($id)['update']); - $user = $this->userRepo->getById($id); + $data = $this->validate($request, $this->rules(intval($id))['update']); + $user = $this->userRepo->getById(intval($id)); $this->userRepo->update($user, $data, userCan(Permission::UsersManage)); $this->singleFormatter($user); @@ -140,7 +140,7 @@ public function update(Request $request, string $id) */ public function delete(Request $request, string $id) { - $user = $this->userRepo->getById($id); + $user = $this->userRepo->getById(intval($id)); $newOwnerId = $request->input('migrate_ownership_id', null); $this->userRepo->destroy($user, $newOwnerId); diff --git a/app/Users/UserRepo.php b/app/Users/UserRepo.php index 2c0897ceffb..e8b3c27e030 100644 --- a/app/Users/UserRepo.php +++ b/app/Users/UserRepo.php @@ -51,6 +51,25 @@ public function getBySlug(string $slug): User return User::query()->where('slug', '=', $slug)->firstOrFail(); } + /** + * Get a user by their external auth ID value. + * Returns null if no matching user found. + */ + public function getByExternalAuthId(string $externalId): User|null + { + // We only really expect at most one user from the search, but as an extra layer of defence against database + // normalisation we search possible matches exactly against the value. + $users = User::query()->where('external_auth_id', '=', $externalId)->get(); + + foreach ($users as $user) { + if ($user->external_auth_id === $externalId) { + return $user; + } + } + + return null; + } + /** * Create a new basic instance of user with the given pre-validated data. * @@ -103,7 +122,7 @@ public function create(array $data, bool $sendInvite = false): User /** * Update the given user with the given data, but do not create an activity. * - * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array, language: ?string} $data + * @param array{name: ?string, email: ?string, external_auth_id: ?string, password: ?string, roles: ?array, language: ?string, display_mode: ?string} $data * * @throws UserUpdateException */ @@ -134,6 +153,11 @@ public function updateWithoutActivity(User $user, array $data, bool $manageUsers setting()->putUser($user, 'language', $data['language']); } + if (!empty($data['display_mode'])) { + $value = $data['display_mode'] === 'dark' ? 'true' : 'false'; + setting()->putUser($user, 'dark-mode-enabled', $value); + } + $user->save(); return $user; diff --git a/app/Util/DatabaseTransaction.php b/app/Util/DatabaseTransaction.php index e36bd2ef310..2e57b6479a1 100644 --- a/app/Util/DatabaseTransaction.php +++ b/app/Util/DatabaseTransaction.php @@ -3,6 +3,7 @@ namespace BookStack\Util; use Closure; +use Illuminate\Database\Connection; use Illuminate\Support\Facades\DB; use Throwable; @@ -24,7 +25,7 @@ class DatabaseTransaction { /** - * @param (Closure(static): TReturn) $callback + * @param (Closure(Connection): TReturn) $callback */ public function __construct( protected Closure $callback diff --git a/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php b/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php index 87ed5add28e..77fa304e6f7 100644 --- a/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php +++ b/app/Util/HtmlPurifier/ConfiguredHtmlPurifier.php @@ -3,13 +3,17 @@ namespace BookStack\Util\HtmlPurifier; use BookStack\App\AppVersion; +use BookStack\Util\HtmlPurifier\Filters\UriEnsureScheme; use BookStack\Util\HtmlPurifier\Filters\UriLimitFileProtocolToAnchors; +use BookStack\Util\HtmlPurifier\Filters\UriMakeAbsolute; +use BookStack\Util\UrlFilter; use HTMLPurifier; use HTMLPurifier_Config; use HTMLPurifier_DefinitionCache_Serializer; use HTMLPurifier_HTML5Config; use HTMLPurifier_HTMLDefinition; use HTMLPurifier_URIDefinition; +use HTMLPurifier_URISchemeRegistry; /** * Provides a configured HTML Purifier instance. @@ -38,6 +42,9 @@ public function __construct() $this->configureHtmlDefinition($htmlDef); } + $registry = HTMLPurifier_URISchemeRegistry::instance(); + $registry->register('sftp', new SftpUriScheme()); + $uriDef = $config->getDefinition('URI', true, true); if ($uriDef instanceof HTMLPurifier_URIDefinition) { $this->configureUriDefinition($uriDef); @@ -83,17 +90,18 @@ protected function setConfig(HTMLPurifier_Config $config, string $cachePath): vo $config->set('Attr.EnableID', true); $config->set('Attr.ID.HTML5', true); $config->set('Output.FixInnerHTML', false); + + $allowedSchemes = UrlFilter::getAllowedSchemes(); + $allowedSchemesSetting = []; + foreach ($allowedSchemes as $scheme) { + $allowedSchemesSetting[$scheme] = true; + } + $defaultScheme = str_starts_with(url('/'), 'http:') ? 'http' : 'https'; $config->set('URI.SafeIframeRegexp', '%^(http://|https://|//)%'); - $config->set('URI.AllowedSchemes', [ - 'http' => true, - 'https' => true, - 'mailto' => true, - 'ftp' => true, - 'nntp' => true, - 'news' => true, - 'tel' => true, - 'file' => true, - ]); + $config->set('URI.AllowedSchemes', $allowedSchemesSetting); + $config->set('URI.MakeAbsolute', false); // We register our own MakeAbsolute filter below + $config->set('URI.DefaultScheme', $defaultScheme); + $config->set('URI.Base', url('/')); // $config->set('Cache.DefinitionImpl', null); // Disable cache during testing } @@ -156,11 +164,18 @@ protected function configureHtmlDefinition(HTMLPurifier_HTMLDefinition $definiti // Allow mention-ids on links $definition->addAttribute('a', 'data-mention-user-id', 'Number'); + + // Set up custom handler for srcset + // To remove once added upstream: https://github.com/xemlock/htmlpurifier-html5/pull/91 + $definition->addAttribute('img', 'srcset', new SrcsetAttrDef()); + $definition->addAttribute('source', 'srcset', new SrcsetAttrDef()); } protected function configureUriDefinition(HTMLPurifier_URIDefinition $definition): void { $definition->registerFilter(new UriLimitFileProtocolToAnchors()); + $definition->registerFilter(new UriEnsureScheme()); + $definition->registerFilter(new UriMakeAbsolute()); } public function purify(string $html): string diff --git a/app/Util/HtmlPurifier/Filters/UriEnsureScheme.php b/app/Util/HtmlPurifier/Filters/UriEnsureScheme.php new file mode 100644 index 00000000000..74380701a8e --- /dev/null +++ b/app/Util/HtmlPurifier/Filters/UriEnsureScheme.php @@ -0,0 +1,41 @@ +getDefinition('URI'); + $defaultScheme = $def->defaultScheme ?? ''; + + if (empty($uri->scheme) && $defaultScheme) { + if (!str_starts_with($uri->toString(), '#')) { + $uri->scheme = $defaultScheme; + } + } + + return true; + } +} diff --git a/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php b/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php index 19ca9cc82b9..bf259e08398 100644 --- a/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php +++ b/app/Util/HtmlPurifier/Filters/UriLimitFileProtocolToAnchors.php @@ -51,5 +51,3 @@ public function filter(&$uri, $config, $context) return false; } } - -// vim: et sw=4 sts=4 diff --git a/app/Util/HtmlPurifier/Filters/UriMakeAbsolute.php b/app/Util/HtmlPurifier/Filters/UriMakeAbsolute.php new file mode 100644 index 00000000000..fc005a7408a --- /dev/null +++ b/app/Util/HtmlPurifier/Filters/UriMakeAbsolute.php @@ -0,0 +1,39 @@ +toString(), '#')) { + return true; + } + + return parent::filter($uri, $config, $context); + } +} diff --git a/app/Util/HtmlPurifier/SftpUriScheme.php b/app/Util/HtmlPurifier/SftpUriScheme.php new file mode 100644 index 00000000000..889cb7f38cc --- /dev/null +++ b/app/Util/HtmlPurifier/SftpUriScheme.php @@ -0,0 +1,10 @@ +parseImageSources($string); + if (empty($sources)) { + return false; + } + + $uriFilter = new HTMLPurifier_AttrDef_URI(true); + + $filtered = array(); + foreach ($sources as $source) { + $uri = $source['uri']; + $descriptor = $source['descriptor']; + $validatedUri = $uriFilter->validate($uri, $config, $context); + if (is_string($validatedUri)) { + if ($descriptor) { + $filtered[] = $validatedUri . ' ' . $source['descriptor']; + } else { + $filtered[] = $validatedUri; + } + } + } + + if (empty($filtered)) { + return false; + } + + return implode(', ', $filtered); + } + + /** + * Parse the image source from srcset attribute text. + * Returns false if it's found to be invalid, otherwise + * returns an array of uri and descriptor combinations. + * + * This aims to follow the WHATWG parsing spec as per: + * https://html.spec.whatwg.org/multipage/images.html#parsing-a-srcset-attribute + * + * @param string $string + * @return array{uri: string, descriptor: string}[]|false + */ + private function parseImageSources($string) + { + $imageSources = array(); + $asciiWhitespace = " \n\r\t\f"; + $asciiWhiteSpaceComma = $asciiWhitespace . ','; + $input = trim($string, $asciiWhiteSpaceComma); + + if ($input === "") { + return false; + } + + $position = 0; + while ($position < strlen($input)) { + $position += strspn($input, $asciiWhitespace, $position); + $urlEnd = $position + strcspn($input, $asciiWhitespace, $position); + $url = substr($input, $position, $urlEnd - $position); + $position = $urlEnd; + $descriptors = array(); + + if (strpos($url, ',') === strlen($url) - 1) { + $url = rtrim($url, ','); + } else { + $position += strspn($input, $asciiWhitespace, $position); + $currentDescriptor = ''; + $state = 'in_descriptor'; + while (true) { + if ($position < strlen($input)) { + $c = $input[$position]; + } else { + $c = null; + } + + if ($state === 'in_descriptor') { + if ($c !== null && str_contains($asciiWhitespace, $c)) { + if ($currentDescriptor !== '') { + $descriptors[] = $currentDescriptor; + } + $state = 'after_descriptor'; + } else if ($c === ',') { + $position++; + if ($currentDescriptor !== '') { + $descriptors[] = $currentDescriptor; + } + break; + } else if ($c === '(') { + $currentDescriptor .= $c; + $state = 'in_parens'; + } else if ($c === null) { + if ($currentDescriptor !== '') { + $descriptors[] = $currentDescriptor; + } + break; + } else { + $currentDescriptor .= $c; + } + } else if ($state === 'in_parens') { + if ($c === ')') { + $currentDescriptor .= $c; + $state = 'in_descriptor'; + } else if ($c === null) { + $descriptors[] = $currentDescriptor; + break; + } else { + $currentDescriptor .= $c; + } + } else { + if ($c !== null && str_contains($asciiWhitespace, $c)) { + // Stay in this state + } else if ($c === null) { + break; + } else { + $state = 'in_descriptor'; + $position--; + } + } + + $position++; + } + } + + $descriptor = $this->formatDescriptor($descriptors); + + if ($url && $descriptor !== false) { + $imageSources[] = array( + 'uri' => $url, + 'descriptor' => $descriptor, + ); + } + } + + return $imageSources; + } + + /** + * Parse and format a single descriptor from an array of potential + * descriptor strings. Returns empty if valid but no descriptor. + * Returns false if invalid. + * @param string[] $descriptors + * @return false|string + */ + private function formatDescriptor(array $descriptors) + { + $error = false; + $width = ''; + $density = ''; + $futureCompatH = ''; + + foreach ($descriptors as $descriptor) { + $descriptor = trim($descriptor); + if ($descriptor === '') { + continue; + } + + $unit = $descriptor[strlen($descriptor) - 1]; + $number = trim(substr($descriptor, 0, -1)); + + if ($unit === 'w' && filter_var($number, FILTER_VALIDATE_INT) && intval($number) >= 0) { + if (!empty($width) || !empty($density) || intval($number) === 0) { + $error = true; + } + $width = $number; + } else if ($unit === 'x' && filter_var($number, FILTER_VALIDATE_FLOAT)) { + if (!empty($width) || !empty($density) || !empty($futureCompatH) || floatval($number) < 0) { + $error = true; + } + $density = $number; + } else if ($unit === 'h' && filter_var($number, FILTER_VALIDATE_INT) && intval($number) >= 0) { + if (!empty($futureCompatH) || !empty($density)) { + $error = true; + } + $futureCompatH = $number; + } else { + $error = true; + } + } + + if (!empty($futureCompatH) && empty($width)) { + $error = true; + } + + if ($error) { + return false; + } + + if ($width) { + return $width . 'w'; + } + + if ($density) { + return $density . 'x'; + } + + return ''; + } +} diff --git a/app/Util/UrlComparison.php b/app/Util/UrlComparison.php new file mode 100644 index 00000000000..9fda2d6142b --- /dev/null +++ b/app/Util/UrlComparison.php @@ -0,0 +1,36 @@ +a); + $bParts = parse_url($this->b); + + return ($aParts['host'] ?? '') === ($bParts['host'] ?? '') + && ($aParts['scheme'] ?? '') === ($bParts['scheme'] ?? '') + && ($aParts['port'] ?? '') === ($bParts['port'] ?? ''); + } + + /** + * Check if there's some overlap between the two URLs' paths. + */ + public function pathsOverlap(): bool + { + $aPath = parse_url($this->a, PHP_URL_PATH) ?? ''; + $bPath = parse_url($this->b, PHP_URL_PATH) ?? ''; + + return str_starts_with($aPath, $bPath) || str_starts_with($bPath, $aPath); + } +} diff --git a/app/Util/UrlFilter.php b/app/Util/UrlFilter.php new file mode 100644 index 00000000000..9380597c092 --- /dev/null +++ b/app/Util/UrlFilter.php @@ -0,0 +1,103 @@ +url = trim($url); + } + + /** + * Check if the URL is allowed to be generally used as a link + * in the application. This does not ensure the original URL string + * provided is safe as-is. Ensure you use the clean method to produce + * a URL that is considered safe to use. + */ + public function isAllowed(): bool + { + $urlParts = parse_url($this->url); + if (!$urlParts) { + return false; + } + + // Extra check to help avoid scenarios where non-standard characters are used in the scheme + // to work around parse_url handling with URLs which may be interpreted by the browser differently. + if (str_contains($this->url, ':') && !preg_match('/^[a-z]+:/i', $this->url)) { + return false; + } + + if (isset($urlParts['scheme'])) { + return in_array(strtolower($urlParts['scheme']), self::$allowedSchemes); + } + + return true; + } + + /** + * Clean the URL to ensure it's valid and only uses the allowed schemes. + * If the URL is not allowed, return a placeholder. + */ + public function clean(): string + { + if (!$this->isAllowed()) { + return '#badlink'; + } + + $urlParts = parse_url($this->url); + if (!$urlParts) { + return '#badlink'; + } + + $url = ''; + + if (isset($urlParts['scheme']) || isset($urlParts['host'])) { + $scheme = strtolower($urlParts['scheme'] ?? 'https'); + $url = $scheme . ':' . (isset($urlParts['host']) ? '//' : ''); + } + + if (isset($urlParts['user']) || isset($urlParts['pass'])) { + $url .= $urlParts['user'] ?? ''; + if (isset($urlParts['pass'])) { + $url .= ':' . $urlParts['pass']; + } + $url .= '@'; + } + + if (isset($urlParts['host'])) { + $url .= $urlParts['host']; + } + if (isset($urlParts['port'])) { + $url .= ':' . $urlParts['port']; + } + if (isset($urlParts['path'])) { + $url .= $urlParts['path']; + } + if (isset($urlParts['query'])) { + $url .= '?' . $urlParts['query']; + } + if (isset($urlParts['fragment'])) { + $url .= '#' . $urlParts['fragment']; + } + + return $url; + } + + /** + * Get schemes that are allowed to be used in content links. + */ + public static function getAllowedSchemes(): array + { + return self::$allowedSchemes; + } +} diff --git a/app/View/LayoutController.php b/app/View/LayoutController.php new file mode 100644 index 00000000000..ff6a6f030f7 --- /dev/null +++ b/app/View/LayoutController.php @@ -0,0 +1,71 @@ +middleware(function (Request $request, Closure $next) { + $this->preventGuestAccess(); + return $next($request); + }); + } + + /** + * Start editing the layout for a specific location. + */ + public function edit(string $location) + { + $namedLocations = $this->viewBlocks->getNamedLocations(); + $locationName = $namedLocations[$location] ?? $location; + $blocks = $this->viewBlocks->getForLocationForCurrentUser($location); + + $this->setPageTitle(trans('preferences.layout_edit')); + + return view('settings.layouts.edit', [ + 'location' => $location, + 'locationName' => $locationName, + 'namedLocations' => $namedLocations, + 'blocks' => $blocks, + ]); + } + + /** + * Update the layout for a specific location for the current user. + */ + public function update(string $location, Request $request) + { + $data = $this->validate($request, [ + 'layout' => ['required', 'string', 'json'], + ]); + + $layoutData = json_decode($data['layout'], true, 5); + if (is_array($layoutData)) { + $this->viewBlocks->updatePreferencesFromIdPositionMap($location, $layoutData); + } + + $this->showSuccessNotification(trans('preferences.layout_update_success')); + + return redirect("/layouts/{$location}"); + } + + /** + * Reset the layout for a specific location, for the current user, + * back to system defaults by removing any user-specific preferences. + */ + public function reset(string $location) + { + $this->viewBlockPreferences->clearForLocation($location); + + $this->showSuccessNotification(trans('preferences.layout_reset_success')); + + return redirect("/layouts/{$location}"); + } +} diff --git a/app/View/ViewBlock.php b/app/View/ViewBlock.php new file mode 100644 index 00000000000..f4ec62be45d --- /dev/null +++ b/app/View/ViewBlock.php @@ -0,0 +1,34 @@ +[]>> + */ + protected static array $defaults = [ + 'home-default' => [ + 'left' => [ + ViewBlocks\HomeRecentDrafts::class, + ViewBlocks\HomeRecentlyViewedOrRecentBooks::class, + ], + 'center' => [ + ViewBlocks\HomeTopFavourites::class, + ViewBlocks\HomeRecentlyUpdatedPages::class, + ], + 'right' => [ + ViewBlocks\HomeRecentActivity::class, + ], + ], + 'home-non-default' => [ + 'left' => [ + ViewBlocks\HomeRecentDrafts::class, + ViewBlocks\HomeTopFavourites::class, + ViewBlocks\HomeRecentlyViewedOrRecentBooks::class, + ViewBlocks\HomeRecentlyUpdatedPages::class, + ViewBlocks\HomeRecentActivity::class, + ], + 'right' => [ + ViewBlocks\HomeActions::class, + ], + ], + 'shelves-index' => [ + 'left' => [ + ViewBlocks\ShelvesIndexRecents::class, + ViewBlocks\ShelvesIndexPopular::class, + ViewBlocks\ShelvesIndexNew::class, + ], + 'right' => [ + ViewBlocks\ShelvesIndexActions::class, + ], + ], + 'shelves-show' => [ + 'left' => [ + ViewBlocks\ShelvesShowTags::class, + ViewBlocks\ShelvesShowDetails::class, + ViewBlocks\ShelvesShowActivity::class, + ], + 'right' => [ + ViewBlocks\ShelvesShowActions::class, + ], + ], + 'books-index' => [ + 'left' => [ + ViewBlocks\BooksIndexRecents::class, + ViewBlocks\BooksIndexPopular::class, + ViewBlocks\BooksIndexNew::class, + ], + 'right' => [ + ViewBlocks\BooksIndexActions::class, + ], + ], + 'books-show' => [ + 'left' => [ + ViewBlocks\BooksShowSearchForm::class, + ViewBlocks\BooksShowTags::class, + ViewBlocks\BooksShowShelves::class, + ViewBlocks\BooksShowActivity::class, + ], + 'right' => [ + ViewBlocks\BooksShowDetails::class, + ViewBlocks\BooksShowActions::class, + ], + ], + 'chapters-show' => [ + 'left' => [ + ViewBlocks\ChaptersShowSearchForm::class, + ViewBlocks\ChaptersShowTags::class, + ViewBlocks\ChaptersShowBookTree::class, + ], + 'right' => [ + ViewBlocks\ChaptersShowDetails::class, + ViewBlocks\ChaptersShowActions::class, + ], + ], + 'pages-show' => [ + 'left' => [ + ViewBlocks\PagesShowTags::class, + ViewBlocks\PagesShowAttachments::class, + ViewBlocks\PagesShowPageNav::class, + ViewBlocks\PagesShowBookTree::class, + ], + 'right' => [ + ViewBlocks\PagesShowDetails::class, + ViewBlocks\PagesShowActions::class, + ], + ], + ]; + + /** + * Get the default view blocks for the given location. + */ + public static function getForLocation(string $location): array + { + return self::$defaults[$location] ?? []; + } + + /** + * Get the locations for all default blocks. + * @return string[] + */ + public static function getLocations(): array + { + return array_keys(self::$defaults); + } + + public static function getLocationLabels(): array + { + return [ + 'home-default' => trans('common.homepage'), + 'home-non-default' => trans('common.homepage'), + 'shelves-index' => trans('entities.shelves'), + 'shelves-show' => trans('entities.shelf'), + 'books-index' => trans('entities.books'), + 'books-show' => trans('entities.book'), + 'chapters-show' => trans('entities.chapter'), + 'pages-show' => trans('entities.page'), + ]; + } +} diff --git a/app/View/ViewBlockInterface.php b/app/View/ViewBlockInterface.php new file mode 100644 index 00000000000..3d05d34d8b0 --- /dev/null +++ b/app/View/ViewBlockInterface.php @@ -0,0 +1,29 @@ + + */ + public function withData(array $viewData): array; +} diff --git a/app/View/ViewBlockManager.php b/app/View/ViewBlockManager.php new file mode 100644 index 00000000000..e18b68715f8 --- /dev/null +++ b/app/View/ViewBlockManager.php @@ -0,0 +1,214 @@ +[]>> + */ + protected array $blocksByLocationAndPosition = []; + + /** + * @var array[]>> + */ + protected array $locationBlockCache = []; + + /** + * Register a block type to be displayed at the given location and position. + * @param class-string $blockClass + */ + public function register(string $location, string $defaultPosition, string $blockClass): void + { + if (!isset($this->blocksByLocationAndPosition[$location])) { + $this->blocksByLocationAndPosition[$location] = []; + } + + if (!isset($this->blocksByLocationAndPosition[$location][$defaultPosition])) { + $this->blocksByLocationAndPosition[$location][$defaultPosition] = []; + } + + // @phpstan-ignore-next-line + if (!is_a($blockClass, ViewBlockInterface::class, true)) { + throw new \InvalidArgumentException('When registering a view block, the block class must implement ViewBlockInterface'); + } + + $this->blocksByLocationAndPosition[$location][$defaultPosition][] = $blockClass; + } + + /** + * Get all blocks registered for a given location and position, considering the + * preferences for the current user. + * @return ViewBlockInterface[] + * @throws BindingResolutionException + */ + public function getInstancesForLocationAndPositionForCurrentUser(string $location, string $position): array + { + $key = $location; + if (isset($this->locationBlockCache[$key])) { + $blocks = $this->locationBlockCache[$key][$position] ?? []; + return $this->blocksToInstances($blocks); + } + + $forLocation = $this->getForLocationForCurrentUser($location); + $this->locationBlockCache[$key] = $forLocation; + + $blocks = $forLocation[$position] ?? []; + return $this->blocksToInstances($blocks); + } + + /** + * Create instances of the given block classes. + * @param class-string[] $blocks + * @return ViewBlockInterface[] + * @throws BindingResolutionException + */ + protected function blocksToInstances(array $blocks): array + { + return array_map(fn (string $blockClass) => app()->make($blockClass), $blocks); + } + + /** + * Get all blocks registered for a given location, as sets of arrays + * keyed by position. + * @return array[]> + */ + protected function getForLocation(string $location): array + { + $defaults = ViewBlockDefaults::getForLocation($location); + $registered = $this->blocksByLocationAndPosition[$location] ?? []; + return array_merge_recursive($defaults, $registered); + } + + /** + * Get all blocks registered for a given location, as sets of arrays + * keyed by position, for the current user. + * Same as above but with user-specific preferences applied. + * @return array>> + * @throws BindingResolutionException + */ + public function getForLocationForCurrentUser(string $location): array + { + $forLocation = $this->getForLocation($location); + $userBlocksByPosition = $this->preferences->getIdByPositionMap($location); + if (empty($userBlocksByPosition)) { + return $forLocation; + } + + $results = []; + $blocksById = $this->blocksByPositionToIdMap($forLocation); + $idPositionMap = $this->blocksByPositionToIdPositionMap($forLocation); + $locations = array_keys($forLocation); + $locations[] = 'unused'; + + // Add based on user preferences + foreach ($locations as $position) { + $userBlockIds = $userBlocksByPosition[$position] ?? []; + $results[$position] = []; + foreach ($userBlockIds as $blockId) { + $block = $blocksById[$blockId] ?? null; + if ($block) { + $results[$position][] = $block; + unset($blocksById[$blockId]); + } + } + } + + // Add remaining blocks based on their default locations + foreach ($blocksById as $block) { + $position = $idPositionMap[$block::getId()] ?? 'unused'; + $results[$position][] = $block; + } + + return $results; + } + + /** + * Get the names of all locations where blocks are registered. + * Returns an array where the keys are location strings, and the + * values are translated labels for that location. + * @return array + */ + public function getNamedLocations(): array + { + $labels = ViewBlockDefaults::getLocationLabels(); + $defaults = ViewBlockDefaults::getLocations(); + $registered = array_keys($this->blocksByLocationAndPosition); + $merged = array_unique(array_merge($defaults, $registered)); + + $results = []; + foreach ($merged as $location) { + $results[$location] = $labels[$location] ?? $location; + } + + $usingDefaultHome = setting('app-homepage-type') === 'default'; + $toIgnore = $usingDefaultHome ? 'home-non-default' : 'home-default'; + unset($results[$toIgnore]); + + return $results; + } + + + /** + * Update user preferences for a given location to match the given layout data map. + * @param array $layoutData + * @throws BindingResolutionException + */ + public function updatePreferencesFromIdPositionMap(string $location, array $layoutData): void + { + $this->preferences->storeByIdPositionMap( + $location, + $layoutData, + $this->getForLocation($location), + ); + } + + /** + * Clear the local user-specific cache of blocks. + * The cache only needs to exist for the current request time since its purpose is to + * avoid duplicate loading across views. + */ + public function clearLocalCache(): void + { + $this->locationBlockCache = []; + } + + /** + * Convert a blocksByPosition array into a map of block IDs to blocks. + * @param array[]> $blocksByPosition + * @return array> + */ + protected function blocksByPositionToIdMap(array $blocksByPosition): array + { + $map = []; + foreach ($blocksByPosition as $position => $blocks) { + foreach ($blocks as $block) { + $map[$block::getId()] = $block; + } + } + return $map; + } + + /** + * Convert a blocksByPosition array into a map of block IDs to their positions. + * @param array[]> $blocksByPosition + * @return array + */ + protected function blocksByPositionToIdPositionMap(array $blocksByPosition): array + { + $map = []; + foreach ($blocksByPosition as $position => $blocks) { + foreach ($blocks as $block) { + $map[$block::getId()] = $position; + } + } + return $map; + } +} diff --git a/app/View/ViewBlockPreferences.php b/app/View/ViewBlockPreferences.php new file mode 100644 index 00000000000..247c3b16c52 --- /dev/null +++ b/app/View/ViewBlockPreferences.php @@ -0,0 +1,95 @@ + ['block-id-1', 'block-id-2'], + * 'position-2' => ['block-id-3'], + * ] + * @param array $layoutData + * @param array[]> $validBlocksByPosition + * @throws BindingResolutionException + */ + public function storeByIdPositionMap( + string $location, + array $layoutData, + array $validBlocksByPosition, + ): void { + $validIds = $this->extractValidBlockIds($validBlocksByPosition); + $validPositions = array_keys($validBlocksByPosition); + $validPositions[] = 'unused'; + + // Ignore updates for invalid/unknown locations + if (empty($validBlocksByPosition)) { + return; + } + + /** @var array $validatedLayoutData */ + $validatedLayoutData = []; + + foreach ($layoutData as $position => $blockIds) { + // @phpstan-ignore-next-line + if (!in_array($position, $validPositions) || !is_array($blockIds)) { + continue; + } + + $validatedLayoutData[$position] = array_filter($blockIds, function ($id) use ($validIds) { + return is_string($id) && in_array($id, $validIds); + }); + } + + $settingKey = $this->getSettingKey($location); + setting()->putForCurrentUser($settingKey, json_encode($validatedLayoutData)); + } + + /** + * Clear the view block preferences for a given location for the current user. + */ + public function clearForLocation(string $location): void + { + $settingKey = $this->getSettingKey($location); + setting()->removeForCurrentUser($settingKey); + } + + /** + * Get the layout data for a given location. + * Provides arrays of block ids keyed by position. + * @return array + */ + public function getIdByPositionMap(string $location): array + { + $settingKey = $this->getSettingKey($location); + $layoutData = setting()->getForCurrentUser($settingKey, '{}'); + return json_decode($layoutData, true) ?? []; + } + + protected function getSettingKey(string $location): string + { + return 'view-layout#' . $location; + } + + /** + * @param array[]> $blocksByPosition + * @return string[] + */ + protected function extractValidBlockIds(array $blocksByPosition): array + { + $ids = []; + + foreach ($blocksByPosition as $blocks) { + foreach ($blocks as $block) { + $ids[] = $block::getId(); + } + } + + return array_unique($ids); + } +} diff --git a/app/View/ViewBlocks/BooksIndexActions.php b/app/View/ViewBlocks/BooksIndexActions.php new file mode 100644 index 00000000000..83327d63dbd --- /dev/null +++ b/app/View/ViewBlocks/BooksIndexActions.php @@ -0,0 +1,20 @@ + $viewData['view'], + ]; + } +} diff --git a/app/View/ViewBlocks/BooksIndexNew.php b/app/View/ViewBlocks/BooksIndexNew.php new file mode 100644 index 00000000000..daab45a4633 --- /dev/null +++ b/app/View/ViewBlocks/BooksIndexNew.php @@ -0,0 +1,31 @@ +queries->visibleForList() + ->orderBy('created_at', 'desc') + ->take(4) + ->get(); + + return [ + 'new' => $new, + ]; + } +} diff --git a/app/View/ViewBlocks/BooksIndexPopular.php b/app/View/ViewBlocks/BooksIndexPopular.php new file mode 100644 index 00000000000..bfaee41e8dd --- /dev/null +++ b/app/View/ViewBlocks/BooksIndexPopular.php @@ -0,0 +1,26 @@ + $this->queries->popularForList()->take(4)->get(), + ]; + } +} diff --git a/app/View/ViewBlocks/BooksIndexRecents.php b/app/View/ViewBlocks/BooksIndexRecents.php new file mode 100644 index 00000000000..48abeb48653 --- /dev/null +++ b/app/View/ViewBlocks/BooksIndexRecents.php @@ -0,0 +1,31 @@ +isGuest()) { + $recents = $this->queries->recentlyViewedForCurrentUser()->take(4)->get(); + } + + return [ + 'recents' => $recents, + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowActions.php b/app/View/ViewBlocks/BooksShowActions.php new file mode 100644 index 00000000000..c7312f2851f --- /dev/null +++ b/app/View/ViewBlocks/BooksShowActions.php @@ -0,0 +1,26 @@ + $book, + 'watchOptions' => new UserEntityWatchOptions(user(), $book), + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowActivity.php b/app/View/ViewBlocks/BooksShowActivity.php new file mode 100644 index 00000000000..14943bb8d25 --- /dev/null +++ b/app/View/ViewBlocks/BooksShowActivity.php @@ -0,0 +1,30 @@ + $this->activityQueries->entityActivity($book, 20, 1), + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowDetails.php b/app/View/ViewBlocks/BooksShowDetails.php new file mode 100644 index 00000000000..582aec8b922 --- /dev/null +++ b/app/View/ViewBlocks/BooksShowDetails.php @@ -0,0 +1,34 @@ +referenceFetcher->getReferenceCountToEntity($book); + + return [ + 'book' => $book, + 'watchOptions' => new UserEntityWatchOptions(user(), $book), + 'referenceCount' => $referenceCount, + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowSearchForm.php b/app/View/ViewBlocks/BooksShowSearchForm.php new file mode 100644 index 00000000000..2fe099047d0 --- /dev/null +++ b/app/View/ViewBlocks/BooksShowSearchForm.php @@ -0,0 +1,20 @@ + trans('entities.books_search_this'), + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowShelves.php b/app/View/ViewBlocks/BooksShowShelves.php new file mode 100644 index 00000000000..590311c7f20 --- /dev/null +++ b/app/View/ViewBlocks/BooksShowShelves.php @@ -0,0 +1,25 @@ +shelves()->scopes('visible')->get(); + + return [ + 'shelves' => $shelves, + ]; + } +} diff --git a/app/View/ViewBlocks/BooksShowTags.php b/app/View/ViewBlocks/BooksShowTags.php new file mode 100644 index 00000000000..0448cd970d6 --- /dev/null +++ b/app/View/ViewBlocks/BooksShowTags.php @@ -0,0 +1,20 @@ + $viewData['book'], + ]; + } +} diff --git a/app/View/ViewBlocks/ChaptersShowActions.php b/app/View/ViewBlocks/ChaptersShowActions.php new file mode 100644 index 00000000000..1beb9c591a4 --- /dev/null +++ b/app/View/ViewBlocks/ChaptersShowActions.php @@ -0,0 +1,26 @@ + $chapter, + 'watchOptions' => new UserEntityWatchOptions(user(), $chapter), + ]; + } +} diff --git a/app/View/ViewBlocks/ChaptersShowBookTree.php b/app/View/ViewBlocks/ChaptersShowBookTree.php new file mode 100644 index 00000000000..03f36862d59 --- /dev/null +++ b/app/View/ViewBlocks/ChaptersShowBookTree.php @@ -0,0 +1,25 @@ + $book, + 'bookTree' => $viewData['bookTree'], + ]; + } +} diff --git a/app/View/ViewBlocks/ChaptersShowDetails.php b/app/View/ViewBlocks/ChaptersShowDetails.php new file mode 100644 index 00000000000..700b848f4b0 --- /dev/null +++ b/app/View/ViewBlocks/ChaptersShowDetails.php @@ -0,0 +1,39 @@ +referenceFetcher->getReferenceCountToEntity($chapter); + + return [ + 'chapter' => $chapter, + 'book' => $book, + 'watchOptions' => new UserEntityWatchOptions(user(), $chapter), + 'referenceCount' => $referenceCount, + ]; + } +} diff --git a/app/View/ViewBlocks/ChaptersShowSearchForm.php b/app/View/ViewBlocks/ChaptersShowSearchForm.php new file mode 100644 index 00000000000..0014e30eac8 --- /dev/null +++ b/app/View/ViewBlocks/ChaptersShowSearchForm.php @@ -0,0 +1,20 @@ + trans('entities.chapters_search_this'), + ]; + } +} diff --git a/app/View/ViewBlocks/ChaptersShowTags.php b/app/View/ViewBlocks/ChaptersShowTags.php new file mode 100644 index 00000000000..e513ee66bd6 --- /dev/null +++ b/app/View/ViewBlocks/ChaptersShowTags.php @@ -0,0 +1,24 @@ + $chapter, + ]; + } +} diff --git a/app/View/ViewBlocks/HomeActions.php b/app/View/ViewBlocks/HomeActions.php new file mode 100644 index 00000000000..211a60df5d0 --- /dev/null +++ b/app/View/ViewBlocks/HomeActions.php @@ -0,0 +1,20 @@ + $viewData['view'] ?? '', + 'homeView' => $viewData['homeView'] ?? 'default', + ]; + } +} diff --git a/app/View/ViewBlocks/HomeRecentActivity.php b/app/View/ViewBlocks/HomeRecentActivity.php new file mode 100644 index 00000000000..06869990f0a --- /dev/null +++ b/app/View/ViewBlocks/HomeRecentActivity.php @@ -0,0 +1,42 @@ +activityQueries->latest(10); + return [ + 'activity' => $activity, + ]; + } +} diff --git a/app/View/ViewBlocks/HomeRecentDrafts.php b/app/View/ViewBlocks/HomeRecentDrafts.php new file mode 100644 index 00000000000..dc88925131a --- /dev/null +++ b/app/View/ViewBlocks/HomeRecentDrafts.php @@ -0,0 +1,50 @@ +isGuest()) { + $draftPages = $this->pageQueries->currentUserDraftsForList() + ->orderBy('updated_at', 'desc') + ->with('book') + ->take(6) + ->get(); + } + + return [ + 'draftPages' => $draftPages, + ]; + } +} diff --git a/app/View/ViewBlocks/HomeRecentlyUpdatedPages.php b/app/View/ViewBlocks/HomeRecentlyUpdatedPages.php new file mode 100644 index 00000000000..99bec79281c --- /dev/null +++ b/app/View/ViewBlocks/HomeRecentlyUpdatedPages.php @@ -0,0 +1,47 @@ +queries->visibleForList() + ->where('draft', false) + ->orderBy('updated_at', 'desc') + ->take(8) + ->get(); + + return [ + 'recentlyUpdatedPages' => $recentlyUpdatedPages, + ]; + } +} diff --git a/app/View/ViewBlocks/HomeRecentlyViewedOrRecentBooks.php b/app/View/ViewBlocks/HomeRecentlyViewedOrRecentBooks.php new file mode 100644 index 00000000000..64cb897ef51 --- /dev/null +++ b/app/View/ViewBlocks/HomeRecentlyViewedOrRecentBooks.php @@ -0,0 +1,54 @@ +isGuest() ? 'books_recent' : 'my_recently_viewed'; + return trans("entities.{$key}"); + } + + public function getView(array $viewData): string + { + if ($viewData['homeView'] === 'default') { + return 'home.parts.default-card-recently-viewed-or-recent-books'; + } + + return 'home.parts.configured-section-recently-viewed-or-recent-books'; + } + + public function withData(array $viewData): array + { + if (user()->isGuest()) { + $recents = $this->queries->books->visibleForList() + ->orderBy('created_at', 'desc') + ->take(10) + ->get(); + } else { + $recents = $this->recentlyViewed->run(10, 1); + } + + return [ + 'recents' => $recents + ]; + } +} diff --git a/app/View/ViewBlocks/HomeTopFavourites.php b/app/View/ViewBlocks/HomeTopFavourites.php new file mode 100644 index 00000000000..28eeed15d69 --- /dev/null +++ b/app/View/ViewBlocks/HomeTopFavourites.php @@ -0,0 +1,42 @@ +topFavourites->run(6); + return [ + 'favourites' => $favourites + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowActions.php b/app/View/ViewBlocks/PagesShowActions.php new file mode 100644 index 00000000000..06a184fa397 --- /dev/null +++ b/app/View/ViewBlocks/PagesShowActions.php @@ -0,0 +1,26 @@ + $page, + 'watchOptions' => new UserEntityWatchOptions(user(), $page), + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowAttachments.php b/app/View/ViewBlocks/PagesShowAttachments.php new file mode 100644 index 00000000000..d29e9e9a6bb --- /dev/null +++ b/app/View/ViewBlocks/PagesShowAttachments.php @@ -0,0 +1,24 @@ + $page, + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowBookTree.php b/app/View/ViewBlocks/PagesShowBookTree.php new file mode 100644 index 00000000000..4f5b6af6ca7 --- /dev/null +++ b/app/View/ViewBlocks/PagesShowBookTree.php @@ -0,0 +1,25 @@ + $book, + 'bookTree' => $viewData['bookTree'], + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowDetails.php b/app/View/ViewBlocks/PagesShowDetails.php new file mode 100644 index 00000000000..6bbe9a8b2c0 --- /dev/null +++ b/app/View/ViewBlocks/PagesShowDetails.php @@ -0,0 +1,39 @@ +referenceFetcher->getReferenceCountToEntity($page); + + return [ + 'page' => $page, + 'book' => $book, + 'watchOptions' => new UserEntityWatchOptions(user(), $page), + 'referenceCount' => $referenceCount, + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowPageNav.php b/app/View/ViewBlocks/PagesShowPageNav.php new file mode 100644 index 00000000000..2a013ec2f84 --- /dev/null +++ b/app/View/ViewBlocks/PagesShowPageNav.php @@ -0,0 +1,28 @@ +getNavigation($page->html); + + return [ + 'pageNav' => $pageNav, + ]; + } +} diff --git a/app/View/ViewBlocks/PagesShowTags.php b/app/View/ViewBlocks/PagesShowTags.php new file mode 100644 index 00000000000..3edbb3288e1 --- /dev/null +++ b/app/View/ViewBlocks/PagesShowTags.php @@ -0,0 +1,24 @@ + $page, + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesIndexActions.php b/app/View/ViewBlocks/ShelvesIndexActions.php new file mode 100644 index 00000000000..8cd0f47d3bd --- /dev/null +++ b/app/View/ViewBlocks/ShelvesIndexActions.php @@ -0,0 +1,20 @@ + $viewData['view'], + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesIndexNew.php b/app/View/ViewBlocks/ShelvesIndexNew.php new file mode 100644 index 00000000000..21f21a4cff0 --- /dev/null +++ b/app/View/ViewBlocks/ShelvesIndexNew.php @@ -0,0 +1,31 @@ +queries->visibleForList() + ->orderBy('created_at', 'desc') + ->take(4) + ->get(); + + return [ + 'new' => $new, + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesIndexPopular.php b/app/View/ViewBlocks/ShelvesIndexPopular.php new file mode 100644 index 00000000000..df62f7c59c8 --- /dev/null +++ b/app/View/ViewBlocks/ShelvesIndexPopular.php @@ -0,0 +1,26 @@ + $this->queries->popularForList()->take(4)->get(), + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesIndexRecents.php b/app/View/ViewBlocks/ShelvesIndexRecents.php new file mode 100644 index 00000000000..5f42d395c6f --- /dev/null +++ b/app/View/ViewBlocks/ShelvesIndexRecents.php @@ -0,0 +1,31 @@ +isGuest()) { + $recents = $this->queries->recentlyViewedForCurrentUser()->take(4)->get(); + } + + return [ + 'recents' => $recents, + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesShowActions.php b/app/View/ViewBlocks/ShelvesShowActions.php new file mode 100644 index 00000000000..08a3cdee17c --- /dev/null +++ b/app/View/ViewBlocks/ShelvesShowActions.php @@ -0,0 +1,24 @@ + $shelf, + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesShowActivity.php b/app/View/ViewBlocks/ShelvesShowActivity.php new file mode 100644 index 00000000000..a14dbed5fc8 --- /dev/null +++ b/app/View/ViewBlocks/ShelvesShowActivity.php @@ -0,0 +1,30 @@ + $this->activityQueries->entityActivity($shelf, 20, 1), + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesShowDetails.php b/app/View/ViewBlocks/ShelvesShowDetails.php new file mode 100644 index 00000000000..1ceb21b6fe8 --- /dev/null +++ b/app/View/ViewBlocks/ShelvesShowDetails.php @@ -0,0 +1,32 @@ +referenceFetcher->getReferenceCountToEntity($shelf); + + return [ + 'shelf' => $shelf, + 'referenceCount' => $referenceCount, + ]; + } +} diff --git a/app/View/ViewBlocks/ShelvesShowTags.php b/app/View/ViewBlocks/ShelvesShowTags.php new file mode 100644 index 00000000000..b7af4a7384f --- /dev/null +++ b/app/View/ViewBlocks/ShelvesShowTags.php @@ -0,0 +1,20 @@ + $viewData['shelf'], + ]; + } +} diff --git a/composer.lock b/composer.lock index e0726839914..67b265547ec 100644 --- a/composer.lock +++ b/composer.lock @@ -62,16 +62,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.384.5", + "version": "3.393.4", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c7d34f2d60515bd0c307e462268f75877842da4a" + "reference": "a5880510e500aa0fe13a6410183cd222b3263890" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c7d34f2d60515bd0c307e462268f75877842da4a", - "reference": "c7d34f2d60515bd0c307e462268f75877842da4a", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a5880510e500aa0fe13a6410183cd222b3263890", + "reference": "a5880510e500aa0fe13a6410183cd222b3263890", "shasum": "" }, "require": { @@ -79,10 +79,10 @@ "ext-json": "*", "ext-pcre": "*", "ext-simplexml": "*", - "guzzlehttp/guzzle": "^7.4.5", - "guzzlehttp/promises": "^2.0", - "guzzlehttp/psr7": "^2.4.5", - "mtdowling/jmespath.php": "^2.8.0", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.6.3 || ^3.0", + "mtdowling/jmespath.php": "^2.9.1", "php": ">=8.1", "psr/http-message": "^1.0 || ^2.0", "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0" @@ -153,9 +153,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.384.5" + "source": "https://github.com/aws/aws-sdk-php/tree/3.393.4" }, - "time": "2026-06-08T18:25:02+00:00" + "time": "2026-08-21T18:24:57+00:00" }, { "name": "bacon/bacon-qr-code", @@ -635,16 +635,16 @@ }, { "name": "dompdf/dompdf", - "version": "v3.1.5", + "version": "v3.1.6", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496" + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", - "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005", "shasum": "" }, "require": { @@ -693,9 +693,9 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/v3.1.5" + "source": "https://github.com/dompdf/dompdf/tree/v3.1.6" }, - "time": "2026-03-03T13:54:37+00:00" + "time": "2026-07-20T12:29:38+00:00" }, { "name": "dompdf/php-font-lib", @@ -982,16 +982,16 @@ }, { "name": "firebase/php-jwt", - "version": "v7.0.5", + "version": "v7.1.0", "source": { "type": "git", "url": "https://github.com/googleapis/php-jwt.git", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380" + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/47ad26bab5e7c70ae8a6f08ed25ff83631121380", - "reference": "47ad26bab5e7c70ae8a6f08ed25ff83631121380", + "url": "https://api.github.com/repos/googleapis/php-jwt/zipball/b374a5d1a4f1f67fadc2165cdb284645945e2fc0", + "reference": "b374a5d1a4f1f67fadc2165cdb284645945e2fc0", "shasum": "" }, "require": { @@ -1000,6 +1000,7 @@ "require-dev": { "guzzlehttp/guzzle": "^7.4", "phpfastcache/phpfastcache": "^9.2", + "phpseclib/phpseclib": "~3.0", "phpspec/prophecy-phpunit": "^2.0", "phpunit/phpunit": "^9.5", "psr/cache": "^2.0||^3.0", @@ -1008,7 +1009,8 @@ }, "suggest": { "ext-sodium": "Support EdDSA (Ed25519) signatures", - "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present", + "phpseclib/phpseclib": "Support PS256 (RSASSA-PSS) signatures" }, "type": "library", "autoload": { @@ -1033,16 +1035,16 @@ } ], "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", - "homepage": "https://github.com/firebase/php-jwt", + "homepage": "https://github.com/googleapis/php-jwt", "keywords": [ "jwt", "php" ], "support": { "issues": "https://github.com/googleapis/php-jwt/issues", - "source": "https://github.com/googleapis/php-jwt/tree/v7.0.5" + "source": "https://github.com/googleapis/php-jwt/tree/v7.1.0" }, - "time": "2026-04-01T20:38:03+00:00" + "time": "2026-06-11T17:54:14+00:00" }, { "name": "fruitcake/php-cors", @@ -1117,24 +1119,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.4", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5" + "phpoption/phpoption": "^1.10" }, "require-dev": { - "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" }, "type": "library", "autoload": { @@ -1163,7 +1165,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" }, "funding": [ { @@ -1175,30 +1177,30 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:43:20+00:00" + "time": "2026-08-24T09:06:52+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.11.1", + "version": "7.15.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c" + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/5af96f374e0ab4ebd747b8310888c99d3adb0a8c", - "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5", - "guzzlehttp/psr7": "^2.11", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-client-implementation": "1.0" @@ -1206,8 +1208,8 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", - "guzzle/client-integration-tests": "3.0.2", - "guzzlehttp/test-server": "^0.5", + "guzzle/client-integration-tests": "3.0.3", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -1287,7 +1289,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.11.1" + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" }, "funding": [ { @@ -1303,20 +1305,20 @@ "type": "tidelift" } ], - "time": "2026-06-07T22:54:06+00:00" + "time": "2026-08-24T09:21:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.0", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", - "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", "shasum": "" }, "require": { @@ -1371,7 +1373,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.0" + "source": "https://github.com/guzzle/promises/tree/2.5.3" }, "funding": [ { @@ -1387,20 +1389,20 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:23:43+00:00" + "time": "2026-08-24T09:11:28+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.11.0", + "version": "2.13.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f" + "reference": "95e7828100de18b4e269fb1703be530082d5166d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f", - "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", "shasum": "" }, "require": { @@ -1409,7 +1411,7 @@ "psr/http-message": "^1.1 || ^2.0", "ralouphie/getallheaders": "^3.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "provide": { "psr/http-factory-implementation": "1.0", @@ -1490,7 +1492,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.11.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.1" }, "funding": [ { @@ -1506,25 +1508,25 @@ "type": "tidelift" } ], - "time": "2026-06-02T12:30:48+00:00" + "time": "2026-08-24T09:13:11+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.6", + "version": "v1.0.11", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "eef7f87bab6f204eba3c39224d8075c70c637946" + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946", - "reference": "eef7f87bab6f204eba3c39224d8075c70c637946", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d0058dccf4299d70c3d9da3378b8908b32780368", + "reference": "d0058dccf4299d70c3d9da3378b8908b32780368", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "symfony/polyfill-php80": "^1.24" + "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", @@ -1576,7 +1578,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.6" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.11" }, "funding": [ { @@ -1592,7 +1594,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T22:00:21+00:00" + "time": "2026-08-24T09:15:32+00:00" }, { "name": "intervention/gif", @@ -1740,16 +1742,16 @@ }, { "name": "knplabs/knp-snappy", - "version": "v1.7.2", + "version": "v1.7.3", "source": { "type": "git", "url": "https://github.com/KnpLabs/snappy.git", - "reference": "1461239a8b265fcc5457b7bdeb842c75b0f066eb" + "reference": "f749a7d2e0f1260f4a0f96925323e34ede898f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/1461239a8b265fcc5457b7bdeb842c75b0f066eb", - "reference": "1461239a8b265fcc5457b7bdeb842c75b0f066eb", + "url": "https://api.github.com/repos/KnpLabs/snappy/zipball/f749a7d2e0f1260f4a0f96925323e34ede898f1f", + "reference": "f749a7d2e0f1260f4a0f96925323e34ede898f1f", "shasum": "" }, "require": { @@ -1801,22 +1803,22 @@ ], "support": { "issues": "https://github.com/KnpLabs/snappy/issues", - "source": "https://github.com/KnpLabs/snappy/tree/v1.7.2" + "source": "https://github.com/KnpLabs/snappy/tree/v1.7.3" }, - "time": "2026-05-15T15:04:49+00:00" + "time": "2026-07-29T11:03:07+00:00" }, { "name": "laravel/framework", - "version": "v12.61.1", + "version": "v12.67.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "e8472ca9774452fe50841d9bdced060679f4d58d" + "reference": "fe2cdaba052cbb9f350761ccefc7ac221cbdf0b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/e8472ca9774452fe50841d9bdced060679f4d58d", - "reference": "e8472ca9774452fe50841d9bdced060679f4d58d", + "url": "https://api.github.com/repos/laravel/framework/zipball/fe2cdaba052cbb9f350761ccefc7ac221cbdf0b5", + "reference": "fe2cdaba052cbb9f350761ccefc7ac221cbdf0b5", "shasum": "" }, "require": { @@ -2025,20 +2027,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-06-04T14:22:52+00:00" + "time": "2026-08-18T13:37:47+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.18", + "version": "v0.3.23", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", - "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", + "url": "https://api.github.com/repos/laravel/prompts/zipball/b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", + "reference": "b7b4c35e5bc47450f6b6238c6cc9c47ba19b2221", "shasum": "" }, "require": { @@ -2082,22 +2084,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.18" + "source": "https://github.com/laravel/prompts/tree/v0.3.23" }, - "time": "2026-05-19T00:47:18+00:00" + "time": "2026-08-11T18:58:24+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.13", + "version": "v2.0.15", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { @@ -2145,20 +2147,20 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-16T14:03:50+00:00" + "time": "2026-07-21T16:49:22+00:00" }, { "name": "laravel/socialite", - "version": "v5.27.0", + "version": "v5.30.0", "source": { "type": "git", "url": "https://github.com/laravel/socialite.git", - "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e" + "reference": "caf714f55d51ab0d914b40033d8b0f489d6219cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/socialite/zipball/40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", - "reference": "40e0757a75637c7b2dff05d3286b0d8fc25e5c0e", + "url": "https://api.github.com/repos/laravel/socialite/zipball/caf714f55d51ab0d914b40033d8b0f489d6219cc", + "reference": "caf714f55d51ab0d914b40033d8b0f489d6219cc", "shasum": "" }, "require": { @@ -2217,7 +2219,7 @@ "issues": "https://github.com/laravel/socialite/issues", "source": "https://github.com/laravel/socialite" }, - "time": "2026-04-24T14:05:47+00:00" + "time": "2026-08-13T23:01:33+00:00" }, { "name": "laravel/tinker", @@ -2287,16 +2289,16 @@ }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d2d1aa8b35e072966c89bc0c66cf926e56767dc4", + "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4", "shasum": "" }, "require": { @@ -2318,8 +2320,8 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "^1.4 || ^2.0", "nyholm/psr7": "^1.5", - "phpstan/phpstan": "^1.8.2", - "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.0.0", "scrutinizer/ocular": "^1.8.1", "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", @@ -2333,7 +2335,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.11-dev" } }, "autoload": { @@ -2390,7 +2392,7 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-08-11T16:06:25+00:00" }, { "name": "league/config", @@ -2476,16 +2478,16 @@ }, { "name": "league/flysystem", - "version": "3.34.0", + "version": "3.35.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e" + "reference": "5fc8404762179ae514678487b23494fd69b2309c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", - "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/5fc8404762179ae514678487b23494fd69b2309c", + "reference": "5fc8404762179ae514678487b23494fd69b2309c", "shasum": "" }, "require": { @@ -2553,22 +2555,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.34.0" + "source": "https://github.com/thephpleague/flysystem/tree/3.35.3" }, - "time": "2026-05-14T10:28:08+00:00" + "time": "2026-08-22T12:55:54+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "3.34.0", + "version": "3.35.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "0c62fdac907791d8649ad3c61cb7a77628344fb8" + "reference": "b03780cb97585ee7e48977f40ed599b33b751634" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/0c62fdac907791d8649ad3c61cb7a77628344fb8", - "reference": "0c62fdac907791d8649ad3c61cb7a77628344fb8", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/b03780cb97585ee7e48977f40ed599b33b751634", + "reference": "b03780cb97585ee7e48977f40ed599b33b751634", "shasum": "" }, "require": { @@ -2608,22 +2610,22 @@ "storage" ], "support": { - "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.34.0" + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.35.3" }, - "time": "2026-05-04T08:24:00+00:00" + "time": "2026-08-08T16:19:23+00:00" }, { "name": "league/flysystem-local", - "version": "3.31.0", + "version": "3.35.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", "shasum": "" }, "require": { @@ -2657,9 +2659,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" }, - "time": "2026-01-23T15:30:45+00:00" + "time": "2026-08-12T13:29:21+00:00" }, { "name": "league/html-to-markdown", @@ -2752,16 +2754,16 @@ }, { "name": "league/mime-type-detection", - "version": "1.16.0", + "version": "1.17.0", "source": { "type": "git", "url": "https://github.com/thephpleague/mime-type-detection.git", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", - "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76", + "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76", "shasum": "" }, "require": { @@ -2771,7 +2773,7 @@ "require-dev": { "friendsofphp/php-cs-fixer": "^3.2", "phpstan/phpstan": "^0.12.68", - "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0" }, "type": "library", "autoload": { @@ -2792,7 +2794,7 @@ "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.16.0" + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0" }, "funding": [ { @@ -2804,7 +2806,7 @@ "type": "tidelift" } ], - "time": "2024-09-21T08:32:55+00:00" + "time": "2026-07-09T11:49:27+00:00" }, { "name": "league/oauth1-client", @@ -3131,24 +3133,24 @@ }, { "name": "masterminds/html5", - "version": "2.10.0", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/Masterminds/html5-php.git", - "reference": "fcf91eb64359852f00d921887b219479b4f21251" + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", - "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", + "reference": "a1e7a2f88ee13635d86fc61cfbdf2306a76ddfc7", "shasum": "" }, "require": { "ext-dom": "*", - "php": ">=5.3.0" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + "phpunit/phpunit": "^6 || ^7 || ^8 || ^9 || ^10" }, "type": "library", "extra": { @@ -3192,9 +3194,9 @@ ], "support": { "issues": "https://github.com/Masterminds/html5-php/issues", - "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + "source": "https://github.com/Masterminds/html5-php/tree/2.11.0" }, - "time": "2025-07-25T09:04:22+00:00" + "time": "2026-08-18T06:18:41+00:00" }, { "name": "monolog/monolog", @@ -3301,16 +3303,16 @@ }, { "name": "mtdowling/jmespath.php", - "version": "2.8.0", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/jmespath/jmespath.php.git", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + "reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", - "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/2157c5e50e813ec6a96c1eed3be7f64a20fb32a8", + "reference": "2157c5e50e813ec6a96c1eed3be7f64a20fb32a8", "shasum": "" }, "require": { @@ -3319,7 +3321,7 @@ }, "require-dev": { "composer/xdebug-handler": "^3.0.3", - "phpunit/phpunit": "^8.5.33" + "phpunit/phpunit": "^8.5.52" }, "bin": [ "bin/jp.php" @@ -3327,7 +3329,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "2.9-dev" } }, "autoload": { @@ -3361,22 +3363,22 @@ ], "support": { "issues": "https://github.com/jmespath/jmespath.php/issues", - "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + "source": "https://github.com/jmespath/jmespath.php/tree/2.9.2" }, - "time": "2024-09-04T18:46:31+00:00" + "time": "2026-07-06T18:56:19+00:00" }, { "name": "nesbot/carbon", - "version": "3.11.4", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60" + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60", - "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/a1c54919f5fff9800cd03c32bd01defd5a4061cb", + "reference": "a1c54919f5fff9800cd03c32bd01defd5a4061cb", "shasum": "" }, "require": { @@ -3468,20 +3470,20 @@ "type": "tidelift" } ], - "time": "2026-04-07T09:57:54+00:00" + "time": "2026-08-08T11:40:35+00:00" }, { "name": "nette/schema", - "version": "v1.3.5", + "version": "v1.3.6", "source": { "type": "git", "url": "https://github.com/nette/schema.git", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + "reference": "c54350438cd6914616f790a49cb424605f421562" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", - "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "url": "https://api.github.com/repos/nette/schema/zipball/c54350438cd6914616f790a49cb424605f421562", + "reference": "c54350438cd6914616f790a49cb424605f421562", "shasum": "" }, "require": { @@ -3533,22 +3535,22 @@ ], "support": { "issues": "https://github.com/nette/schema/issues", - "source": "https://github.com/nette/schema/tree/v1.3.5" + "source": "https://github.com/nette/schema/tree/v1.3.6" }, - "time": "2026-02-23T03:47:12+00:00" + "time": "2026-08-16T21:58:41+00:00" }, { "name": "nette/utils", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", "shasum": "" }, "require": { @@ -3568,7 +3570,7 @@ }, "suggest": { "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-iconv": "to use Strings::chr(), ord() and reverse()", "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", "ext-json": "to use Nette\\Utils\\Json", "ext-mbstring": "to use Strings::lower() etc...", @@ -3624,26 +3626,25 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.4" + "source": "https://github.com/nette/utils/tree/v4.1.5" }, - "time": "2026-05-11T20:49:54+00:00" + "time": "2026-07-17T23:02:45+00:00" }, { "name": "nikic/php-parser", - "version": "v5.7.0", + "version": "v5.8.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", - "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", "shasum": "" }, "require": { - "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" @@ -3682,9 +3683,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" }, - "time": "2025-12-06T11:56:16+00:00" + "time": "2026-07-04T14:30:18+00:00" }, { "name": "nunomaduro/termwind", @@ -3958,16 +3959,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.5", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", "shasum": "" }, "require": { @@ -3975,7 +3976,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" }, "type": "library", "extra": { @@ -4017,7 +4018,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" }, "funding": [ { @@ -4029,20 +4030,20 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:41:33+00:00" + "time": "2026-08-24T00:54:40+00:00" }, { "name": "phpseclib/phpseclib", - "version": "3.0.52", + "version": "3.0.56", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + "reference": "7adbbe38cde25e2df2116dbf2673c407e24fa305" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", - "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/7adbbe38cde25e2df2116dbf2673c407e24fa305", + "reference": "7adbbe38cde25e2df2116dbf2673c407e24fa305", "shasum": "" }, "require": { @@ -4123,7 +4124,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.56" }, "funding": [ { @@ -4139,20 +4140,20 @@ "type": "tidelift" } ], - "time": "2026-04-27T07:02:15+00:00" + "time": "2026-08-03T04:36:50+00:00" }, { "name": "pragmarx/google2fa", - "version": "v9.0.0", + "version": "v9.1.0", "source": { "type": "git", "url": "https://github.com/antonioribeiro/google2fa.git", - "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf" + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/e6bc62dd6ae83acc475f57912e27466019a1f2cf", - "reference": "e6bc62dd6ae83acc475f57912e27466019a1f2cf", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/f00bc788c555adfb6765c437ff3538e59cd88af1", + "reference": "f00bc788c555adfb6765c437ff3538e59cd88af1", "shasum": "" }, "require": { @@ -4160,8 +4161,11 @@ "php": "^7.1|^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.9", - "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + "phpstan/phpstan": "^1.0|^2.0", + "phpstan/phpstan-phpunit": "^1.0|^2.0", + "phpunit/phpunit": "~9|~10|~11|~12|~13", + "psalm/plugin-phpunit": "^0.19|^0.20", + "vimeo/psalm": "^5.26|^6.13" }, "type": "library", "autoload": { @@ -4184,27 +4188,36 @@ "keywords": [ "2fa", "Authentication", + "MFA", "Two Factor Authentication", - "google2fa" + "google-authenticator", + "google2fa", + "hotp", + "otp", + "rfc4226", + "rfc6238", + "totp" ], "support": { + "docs": "https://github.com/antonioribeiro/google2fa#readme", "issues": "https://github.com/antonioribeiro/google2fa/issues", - "source": "https://github.com/antonioribeiro/google2fa/tree/v9.0.0" + "security": "https://github.com/antonioribeiro/google2fa/security/policy", + "source": "https://github.com/antonioribeiro/google2fa" }, - "time": "2025-09-19T22:51:08+00:00" + "time": "2026-08-15T13:22:01+00:00" }, { "name": "predis/predis", - "version": "v3.5.0", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/predis/predis.git", - "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1" + "reference": "2ff20c08bb63697245ffee3f198d1673086c302d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/predis/predis/zipball/8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", - "reference": "8cc4319c06924c8ff0c5c7eec4243a19e3be32f1", + "url": "https://api.github.com/repos/predis/predis/zipball/2ff20c08bb63697245ffee3f198d1673086c302d", + "reference": "2ff20c08bb63697245ffee3f198d1673086c302d", "shasum": "" }, "require": { @@ -4246,7 +4259,7 @@ ], "support": { "issues": "https://github.com/predis/predis/issues", - "source": "https://github.com/predis/predis/tree/v3.5.0" + "source": "https://github.com/predis/predis/tree/v3.6.0" }, "funding": [ { @@ -4254,7 +4267,7 @@ "type": "github" } ], - "time": "2026-06-02T19:25:56+00:00" + "time": "2026-08-14T23:07:56+00:00" }, { "name": "psr/clock", @@ -4670,16 +4683,16 @@ }, { "name": "psy/psysh", - "version": "v0.12.23", + "version": "v0.12.24", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", - "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", + "reference": "ca0fdcf8a7617afa3adfdf1b5fef573dffb69ca1", "shasum": "" }, "require": { @@ -4743,9 +4756,9 @@ ], "support": { "issues": "https://github.com/bobthecow/psysh/issues", - "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + "source": "https://github.com/bobthecow/psysh/tree/v0.12.24" }, - "time": "2026-05-23T13:41:31+00:00" + "time": "2026-06-29T15:41:09+00:00" }, { "name": "ralouphie/getallheaders", @@ -4869,20 +4882,20 @@ }, { "name": "ramsey/uuid", - "version": "4.9.2", + "version": "4.9.3", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "8429c78ca35a09f27565311b98101e2826affde0" + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", - "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", "shasum": "" }, "require": { - "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "brick/math": ">=0.8.16 <=0.18", "php": "^8.0", "ramsey/collection": "^1.2 || ^2.0" }, @@ -4941,9 +4954,9 @@ ], "support": { "issues": "https://github.com/ramsey/uuid/issues", - "source": "https://github.com/ramsey/uuid/tree/4.9.2" + "source": "https://github.com/ramsey/uuid/tree/4.9.3" }, - "time": "2025-12-14T04:43:48+00:00" + "time": "2026-06-18T03:57:49+00:00" }, { "name": "robrichards/xmlseclibs", @@ -4989,16 +5002,16 @@ }, { "name": "sabberworm/php-css-parser", - "version": "v9.3.0", + "version": "v9.4.0", "source": { "type": "git", "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", - "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949" + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", - "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", "shasum": "" }, "require": { @@ -5009,15 +5022,15 @@ "require-dev": { "php-parallel-lint/php-parallel-lint": "1.4.0", "phpstan/extension-installer": "1.4.3", - "phpstan/phpstan": "1.12.32 || 2.1.32", - "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8", - "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", "phpunit/phpunit": "8.5.52", "rawr/phpunit-data-provider": "3.3.1", - "rector/rector": "1.2.10 || 2.2.8", - "rector/type-perfect": "1.0.0 || 2.1.0", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", "squizlabs/php_codesniffer": "4.0.1", - "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1" + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" }, "suggest": { "ext-mbstring": "for parsing UTF-8 CSS" @@ -5025,7 +5038,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.4.x-dev" + "dev-main": "9.5.x-dev" } }, "autoload": { @@ -5063,9 +5076,9 @@ ], "support": { "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", - "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0" + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" }, - "time": "2026-03-03T17:31:43+00:00" + "time": "2026-06-18T15:10:53+00:00" }, { "name": "socialiteproviders/discord", @@ -5234,20 +5247,19 @@ }, { "name": "socialiteproviders/microsoft-azure", - "version": "5.2.0", + "version": "5.2.1", "source": { "type": "git", "url": "https://github.com/SocialiteProviders/Microsoft-Azure.git", - "reference": "453d62c9d7e3b3b76e94c913fb46e68a33347b16" + "reference": "d70ea3c60b09cc113a14350761a0618815bfa11c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/453d62c9d7e3b3b76e94c913fb46e68a33347b16", - "reference": "453d62c9d7e3b3b76e94c913fb46e68a33347b16", + "url": "https://api.github.com/repos/SocialiteProviders/Microsoft-Azure/zipball/d70ea3c60b09cc113a14350761a0618815bfa11c", + "reference": "d70ea3c60b09cc113a14350761a0618815bfa11c", "shasum": "" }, "require": { - "ext-json": "*", "php": "^8.0", "socialiteproviders/manager": "^4.4" }, @@ -5281,7 +5293,7 @@ "issues": "https://github.com/socialiteproviders/providers/issues", "source": "https://github.com/socialiteproviders/providers" }, - "time": "2024-03-15T03:02:10+00:00" + "time": "2026-08-23T02:40:58+00:00" }, { "name": "socialiteproviders/okta", @@ -5515,16 +5527,16 @@ }, { "name": "symfony/console", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", - "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", + "url": "https://api.github.com/repos/symfony/console/zipball/962e18f09ebe68a49039b4c82fc0ea4871824fca", + "reference": "962e18f09ebe68a49039b4c82fc0ea4871824fca", "shasum": "" }, "require": { @@ -5589,7 +5601,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.13" + "source": "https://github.com/symfony/console/tree/v7.4.17" }, "funding": [ { @@ -5609,20 +5621,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T08:56:14+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/css-selector", - "version": "v7.4.9", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "b75663ed96cf4756e28e3105476f220f92886cc4" + "reference": "e3822e1cb7013a0a99b3c578130458927ec4eb89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/b75663ed96cf4756e28e3105476f220f92886cc4", - "reference": "b75663ed96cf4756e28e3105476f220f92886cc4", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/e3822e1cb7013a0a99b3c578130458927ec4eb89", + "reference": "e3822e1cb7013a0a99b3c578130458927ec4eb89", "shasum": "" }, "require": { @@ -5658,7 +5670,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v7.4.9" + "source": "https://github.com/symfony/css-selector/tree/v7.4.17" }, "funding": [ { @@ -5678,20 +5690,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", - "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", "shasum": "" }, "require": { @@ -5729,7 +5741,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" }, "funding": [ { @@ -5749,20 +5761,20 @@ "type": "tidelift" } ], - "time": "2026-04-13T15:52:40+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/error-handler", - "version": "v7.4.8", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa" + "reference": "8373921e231e190a88e2ad526951bbaa791576fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", - "reference": "8dd79d8af777ee6cba2fd4d98da6ffb839f3c0fa", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8373921e231e190a88e2ad526951bbaa791576fa", + "reference": "8373921e231e190a88e2ad526951bbaa791576fa", "shasum": "" }, "require": { @@ -5811,7 +5823,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v7.4.8" + "source": "https://github.com/symfony/error-handler/tree/v7.4.17" }, "funding": [ { @@ -5831,20 +5843,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v7.4.9", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101" + "reference": "d269974ee93c61d03620ffee358355bfdb471d66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e4a2e29753c7801f7a8340e066cfa788f3bc8101", - "reference": "e4a2e29753c7801f7a8340e066cfa788f3bc8101", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d269974ee93c61d03620ffee358355bfdb471d66", + "reference": "d269974ee93c61d03620ffee358355bfdb471d66", "shasum": "" }, "require": { @@ -5896,7 +5908,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.9" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.17" }, "funding": [ { @@ -5916,20 +5928,20 @@ "type": "tidelift" } ], - "time": "2026-04-18T13:18:21+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", - "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c7de7a00ffb67842132da02ea92988a39ccd9f4e", + "reference": "c7de7a00ffb67842132da02ea92988a39ccd9f4e", "shasum": "" }, "require": { @@ -5976,7 +5988,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.1" }, "funding": [ { @@ -5996,20 +6008,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.11", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", - "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", + "reference": "ee7bc7bca4c7079b88e57d5000aeeb20df570c8d", "shasum": "" }, "require": { @@ -6046,7 +6058,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + "source": "https://github.com/symfony/filesystem/tree/v7.4.17" }, "funding": [ { @@ -6066,20 +6078,20 @@ "type": "tidelift" } ], - "time": "2026-05-11T16:38:44+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/finder", - "version": "v7.4.8", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e0be088d22278583a82da281886e8c3592fbf149" + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149", - "reference": "e0be088d22278583a82da281886e8c3592fbf149", + "url": "https://api.github.com/repos/symfony/finder/zipball/5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", + "reference": "5ce28827081f6d1f0c32eaf3882750f19cb5bbe6", "shasum": "" }, "require": { @@ -6114,7 +6126,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.4.8" + "source": "https://github.com/symfony/finder/tree/v7.4.17" }, "funding": [ { @@ -6134,20 +6146,20 @@ "type": "tidelift" } ], - "time": "2026-03-24T13:12:05+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "symfony/http-foundation", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "bc354f47c62301e990b7874fa662326368508e2c" + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bc354f47c62301e990b7874fa662326368508e2c", - "reference": "bc354f47c62301e990b7874fa662326368508e2c", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/2ebe78c083501dfb9509b31a7aedcae4d60a391f", + "reference": "2ebe78c083501dfb9509b31a7aedcae4d60a391f", "shasum": "" }, "require": { @@ -6196,7 +6208,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.4.13" + "source": "https://github.com/symfony/http-foundation/tree/v7.4.17" }, "funding": [ { @@ -6216,20 +6228,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T11:20:33+00:00" + "time": "2026-08-20T09:55:18+00:00" }, { "name": "symfony/http-kernel", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "9df847980c436451f4f51d1284491bb4356dd989" + "reference": "aa160388d444210e3d01bbb4c5c53af4cd763df4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/9df847980c436451f4f51d1284491bb4356dd989", - "reference": "9df847980c436451f4f51d1284491bb4356dd989", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/aa160388d444210e3d01bbb4c5c53af4cd763df4", + "reference": "aa160388d444210e3d01bbb4c5c53af4cd763df4", "shasum": "" }, "require": { @@ -6287,7 +6299,7 @@ "symfony/validator": "^6.4|^7.0|^8.0", "symfony/var-dumper": "^6.4|^7.0|^8.0", "symfony/var-exporter": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "type": "library", "autoload": { @@ -6315,7 +6327,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v7.4.13" + "source": "https://github.com/symfony/http-kernel/tree/v7.4.17" }, "funding": [ { @@ -6335,20 +6347,20 @@ "type": "tidelift" } ], - "time": "2026-05-27T08:31:43+00:00" + "time": "2026-08-22T13:41:33+00:00" }, { "name": "symfony/mailer", - "version": "v7.4.12", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff" + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/5cefb712a25f320579615ba9e1942abaeade7dff", - "reference": "5cefb712a25f320579615ba9e1942abaeade7dff", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b17c9bf3a551d5f635638a3b6c05f06c4dc87584", + "reference": "b17c9bf3a551d5f635638a3b6c05f06c4dc87584", "shasum": "" }, "require": { @@ -6399,7 +6411,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v7.4.12" + "source": "https://github.com/symfony/mailer/tree/v7.4.17" }, "funding": [ { @@ -6419,20 +6431,20 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/mime", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770" + "reference": "bf328d82105831db3e409195db0540ff57f27c80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/a845722765c4f6b2ce88beaf4f4479975b186770", - "reference": "a845722765c4f6b2ce88beaf4f4479975b186770", + "url": "https://api.github.com/repos/symfony/mime/zipball/bf328d82105831db3e409195db0540ff57f27c80", + "reference": "bf328d82105831db3e409195db0540ff57f27c80", "shasum": "" }, "require": { @@ -6456,7 +6468,7 @@ "symfony/process": "^6.4|^7.0|^8.0", "symfony/property-access": "^6.4|^7.0|^8.0", "symfony/property-info": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + "symfony/serializer": "^6.4.44|^7.4.17|^8.1.5" }, "type": "library", "autoload": { @@ -6488,7 +6500,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.4.13" + "source": "https://github.com/symfony/mime/tree/v7.4.17" }, "funding": [ { @@ -6508,7 +6520,7 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:22:37+00:00" + "time": "2026-08-22T09:04:42+00:00" }, { "name": "symfony/polyfill-ctype", @@ -6595,16 +6607,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -6653,7 +6665,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -6673,7 +6685,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -7018,16 +7030,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.38.2", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8" + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/796a26abb75ce49f3a84433cd81bf1009d73d5f8", - "reference": "796a26abb75ce49f3a84433cd81bf1009d73d5f8", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/5ea99087fb99c273a9b9236ed4c31e78b16103c6", + "reference": "5ea99087fb99c273a9b9236ed4c31e78b16103c6", "shasum": "" }, "require": { @@ -7074,7 +7086,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.38.2" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.41.0" }, "funding": [ { @@ -7094,7 +7106,7 @@ "type": "tidelift" } ], - "time": "2026-05-27T06:51:48+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-php84", @@ -7178,16 +7190,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -7234,7 +7246,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -7254,7 +7266,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-uuid", @@ -7341,16 +7353,16 @@ }, { "name": "symfony/process", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f5804be144caceb570f6747519999636b664f24c" + "reference": "058d17fc284cce14efb2385783b55014a461b176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f5804be144caceb570f6747519999636b664f24c", - "reference": "f5804be144caceb570f6747519999636b664f24c", + "url": "https://api.github.com/repos/symfony/process/zipball/058d17fc284cce14efb2385783b55014a461b176", + "reference": "058d17fc284cce14efb2385783b55014a461b176", "shasum": "" }, "require": { @@ -7382,7 +7394,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.4.13" + "source": "https://github.com/symfony/process/tree/v7.4.17" }, "funding": [ { @@ -7402,20 +7414,20 @@ "type": "tidelift" } ], - "time": "2026-05-23T16:05:06+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/routing", - "version": "v7.4.13", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d" + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/3a162171bb008e5e0f15dce6581373a4c0e8390d", - "reference": "3a162171bb008e5e0f15dce6581373a4c0e8390d", + "url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3", + "reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3", "shasum": "" }, "require": { @@ -7467,7 +7479,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v7.4.13" + "source": "https://github.com/symfony/routing/tree/v7.4.17" }, "funding": [ { @@ -7487,20 +7499,20 @@ "type": "tidelift" } ], - "time": "2026-05-24T11:20:33+00:00" + "time": "2026-08-17T13:12:36+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", - "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", "shasum": "" }, "require": { @@ -7554,7 +7566,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" }, "funding": [ { @@ -7574,20 +7586,20 @@ "type": "tidelift" } ], - "time": "2026-03-28T09:44:51+00:00" + "time": "2026-06-16T09:55:08+00:00" }, { "name": "symfony/string", - "version": "v7.4.13", + "version": "v7.4.15", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" + "reference": "e394af32256bf9e7bf80849d95e589167c10097b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", - "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "url": "https://api.github.com/repos/symfony/string/zipball/e394af32256bf9e7bf80849d95e589167c10097b", + "reference": "e394af32256bf9e7bf80849d95e589167c10097b", "shasum": "" }, "require": { @@ -7645,7 +7657,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.13" + "source": "https://github.com/symfony/string/tree/v7.4.15" }, "funding": [ { @@ -7665,20 +7677,20 @@ "type": "tidelift" } ], - "time": "2026-05-23T15:23:29+00:00" + "time": "2026-07-28T07:33:02+00:00" }, { "name": "symfony/translation", - "version": "v7.4.10", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde" + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/ada7578c30dd5feaa8259cff3e885069ea81ddde", - "reference": "ada7578c30dd5feaa8259cff3e885069ea81ddde", + "url": "https://api.github.com/repos/symfony/translation/zipball/2ee1e4a3b32a528a642babe041ff7c440b213b4b", + "reference": "2ee1e4a3b32a528a642babe041ff7c440b213b4b", "shasum": "" }, "require": { @@ -7745,7 +7757,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v7.4.10" + "source": "https://github.com/symfony/translation/tree/v7.4.17" }, "funding": [ { @@ -7765,20 +7777,20 @@ "type": "tidelift" } ], - "time": "2026-05-06T11:19:24+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "symfony/translation-contracts", - "version": "v3.7.0", + "version": "v3.7.1", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", - "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/ccb206b98faccc511ebae8e5fad50f2dc0b30621", + "reference": "ccb206b98faccc511ebae8e5fad50f2dc0b30621", "shasum": "" }, "require": { @@ -7827,7 +7839,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.1" }, "funding": [ { @@ -7847,20 +7859,20 @@ "type": "tidelift" } ], - "time": "2026-01-05T13:30:16+00:00" + "time": "2026-06-05T06:23:12+00:00" }, { "name": "symfony/uid", - "version": "v7.4.9", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/uid.git", - "reference": "2676b524340abcfe4d6151ec698463cebafee439" + "reference": "69d732355a139c6f8881337d28515aa01f12b8be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/uid/zipball/2676b524340abcfe4d6151ec698463cebafee439", - "reference": "2676b524340abcfe4d6151ec698463cebafee439", + "url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be", + "reference": "69d732355a139c6f8881337d28515aa01f12b8be", "shasum": "" }, "require": { @@ -7905,7 +7917,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/uid/tree/v7.4.9" + "source": "https://github.com/symfony/uid/tree/v7.4.17" }, "funding": [ { @@ -7925,20 +7937,20 @@ "type": "tidelift" } ], - "time": "2026-04-30T15:19:22+00:00" + "time": "2026-08-11T07:38:58+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.4.8", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd" + "reference": "53712df8727da1744490202eeb9cb50d4b95419d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9510c3966f749a1d1ff0059e1eabef6cc621e7fd", - "reference": "9510c3966f749a1d1ff0059e1eabef6cc621e7fd", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/53712df8727da1744490202eeb9cb50d4b95419d", + "reference": "53712df8727da1744490202eeb9cb50d4b95419d", "shasum": "" }, "require": { @@ -7954,7 +7966,7 @@ "symfony/http-kernel": "^6.4|^7.0|^8.0", "symfony/process": "^6.4|^7.0|^8.0", "symfony/uid": "^6.4|^7.0|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -7992,7 +8004,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.4.8" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.17" }, "funding": [ { @@ -8012,7 +8024,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T13:44:50+00:00" + "time": "2026-08-21T12:09:28+00:00" }, { "name": "thecodingmachine/safe", @@ -8214,16 +8226,16 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.3", + "version": "v5.6.4", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "955e7815d677a3eaa7075231212f2110983adecc" + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", - "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", + "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", "shasum": "" }, "require": { @@ -8282,7 +8294,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" }, "funding": [ { @@ -8294,7 +8306,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:49:13+00:00" + "time": "2026-07-06T19:11:50+00:00" }, { "name": "voku/portable-ascii", @@ -8568,19 +8580,21 @@ }, { "name": "hamcrest/hamcrest-php", - "version": "v2.1.1", + "version": "v3.0.0", "source": { "type": "git", "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", - "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b61cd040da1a4925bc90a51c074f5297e7c0fa52", + "reference": "b61cd040da1a4925bc90a51c074f5297e7c0fa52", "shasum": "" }, "require": { + "ext-ctype": "*", + "ext-dom": "*", "php": "^7.4|^8.0" }, "replace": { @@ -8589,13 +8603,15 @@ "kodova/hamcrest-php": "*" }, "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -8613,9 +8629,9 @@ ], "support": { "issues": "https://github.com/hamcrest/hamcrest-php/issues", - "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + "source": "https://github.com/hamcrest/hamcrest-php/tree/v3.0.0" }, - "time": "2025-04-30T06:54:44+00:00" + "time": "2026-03-17T11:56:53+00:00" }, { "name": "iamcal/sql-parser", @@ -8826,29 +8842,28 @@ }, { "name": "mockery/mockery", - "version": "1.6.12", + "version": "1.6.15", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", - "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee", "shasum": "" }, "require": { - "hamcrest/hamcrest-php": "^2.0.1", - "lib-pcre": ">=7.0", + "hamcrest/hamcrest-php": "^2.0 || ^3.0", "php": ">=7.3" }, "conflict": { "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "^8.5 || ^9.6.17", - "symplify/easy-coding-standard": "^12.1.14" + "phpunit/phpunit": "^9.6.36", + "symplify/easy-coding-standard": "^13.2.17" }, "type": "library", "autoload": { @@ -8905,24 +8920,24 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2024-05-16T03:13:13+00:00" + "time": "2026-08-19T19:37:52+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.13.4", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -8957,35 +8972,35 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "nunomaduro/collision", - "version": "v8.9.4", + "version": "v8.9.5", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.8" + "symfony/console": "^7.4.14 || ^8.1.1" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -8993,12 +9008,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.6", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", - "laravel/pint": "^1.29.1", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", - "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" }, "type": "library", "extra": { @@ -9061,7 +9076,7 @@ "type": "patreon" } ], - "time": "2026-04-21T14:04:20+00:00" + "time": "2026-07-15T19:09:14+00:00" }, { "name": "phar-io/manifest", @@ -9183,11 +9198,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.2", + "version": "2.2.9", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6", - "reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/13d6b4f347bad222da436580c8304fa6f83e6bd0", + "reference": "13d6b4f347bad222da436580c8304fa6f83e6bd0", "shasum": "" }, "require": { @@ -9243,7 +9258,7 @@ "type": "github" } ], - "time": "2026-06-05T09:00:01+00:00" + "time": "2026-08-22T07:38:16+00:00" }, { "name": "phpunit/php-code-coverage", @@ -9594,24 +9609,24 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.55", + "version": "11.5.56", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", - "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f83edffa6967c3db468d48a695ec7bcb02e9256", + "reference": "5f83edffa6967c3db468d48a695ec7bcb02e9256", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", @@ -9676,31 +9691,15 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.56" }, "funding": [ { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://liberapay.com/sebastianbergmann", - "type": "liberapay" - }, - { - "url": "https://thanks.dev/u/gh/sebastianbergmann", - "type": "thanks_dev" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" + "url": "https://phpunit.de/sponsoring.html", + "type": "other" } ], - "time": "2026-02-18T12:37:06+00:00" + "time": "2026-07-06T14:52:39+00:00" }, { "name": "sebastian/cli-parser", @@ -10690,19 +10689,20 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "4.0.1", + "version": "4.0.4", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0525c73950de35ded110cffafb9892946d7771b5" + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0525c73950de35ded110cffafb9892946d7771b5", - "reference": "0525c73950de35ded110cffafb9892946d7771b5", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/bbdc3d0532623e21838b7041a4364383a8126f96", + "reference": "bbdc3d0532623e21838b7041a4364383a8126f96", "shasum": "" }, "require": { + "ext-libxml": "*", "ext-simplexml": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", @@ -10711,6 +10711,10 @@ "require-dev": { "phpunit/phpunit": "^8.4.0 || ^9.3.4 || ^10.5.32 || 11.3.3 - 11.5.28 || ^11.5.31" }, + "suggest": { + "ext-iconv": "For accurate character length calculation when the checked files contain multi-byte characters.", + "ext-pcntl": "For parallel processing support via the --parallel CLI option." + }, "bin": [ "bin/phpcbf", "bin/phpcs" @@ -10765,7 +10769,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-10T16:43:36+00:00" + "time": "2026-08-06T02:45:27+00:00" }, { "name": "ssddanbrown/asserthtml", @@ -10866,16 +10870,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v7.4.12", + "version": "v7.4.17", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "b59b59122690976550fd142c23fab62c84738db6" + "reference": "6b037d62595666cf65de867073c9a1f00184be0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/b59b59122690976550fd142c23fab62c84738db6", - "reference": "b59b59122690976550fd142c23fab62c84738db6", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/6b037d62595666cf65de867073c9a1f00184be0c", + "reference": "6b037d62595666cf65de867073c9a1f00184be0c", "shasum": "" }, "require": { @@ -10914,7 +10918,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v7.4.12" + "source": "https://github.com/symfony/dom-crawler/tree/v7.4.17" }, "funding": [ { @@ -10934,7 +10938,7 @@ "type": "tidelift" } ], - "time": "2026-05-20T07:20:23+00:00" + "time": "2026-08-21T17:40:08+00:00" }, { "name": "theseer/tokenizer", diff --git a/database/migrations/2026_07_27_201402_update_users_external_auth_id_collation.php b/database/migrations/2026_07_27_201402_update_users_external_auth_id_collation.php new file mode 100644 index 00000000000..9f9267c8800 --- /dev/null +++ b/database/migrations/2026_07_27_201402_update_users_external_auth_id_collation.php @@ -0,0 +1,31 @@ +string('external_auth_id') + ->collation('utf8mb4_bin') + ->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->string('external_auth_id') + ->change(); + }); + } +}; diff --git a/dev/docs/logical-theme-system.md b/dev/docs/logical-theme-system.md index b6342ce5465..7093bc00bb7 100644 --- a/dev/docs/logical-theme-system.md +++ b/dev/docs/logical-theme-system.md @@ -193,4 +193,105 @@ Theme::addSocialDriver('reddit', [ $driver->with(['prompt' => 'select_account']); $driver->scopes(['open_id']); }); -``` \ No newline at end of file +``` + +## Custom View Layout Block Example + +By listening to the `ThemeEvents::VIEW_BLOCKS_REGISTER` event, it's possible to register +custom view blocks which can be shown in a layout in the application. View blocks are typically the sections shown in the sidebar, +or the cards shown on the home view. + +As an example of using this system, we'll define a custom view block which will show on the default home view to state how many +books there are in the system. +To start, we'll need to create a custom class to represent our view block. This must implement `\BookStack\View\ViewBlockInterface`: + +```php +use BookStack\Entities\Queries\BookQueries; +use BookStack\View\ViewBlockInterface; +use BookStack\View\ViewBlockManager; + +class BookTotalBlock implements ViewBlockInterface { + + public function __construct( + protected BookQueries $bookQueries + ) { + } + + public static function getId(): string + { + return 'custom_book_total_block'; + } + + public static function getLabel(): string + { + return 'Total books displays'; + } + + public function getView(array $viewData): string + { + return 'blocks.total-blocks'; + } + + public function withData(array $viewData): array + { + $totalBooks = $this->bookQueries->visibleForList()->count(); + return [ + 'totalBooks' => $totalBooks, + ]; + } +} +``` + +The interface has a few required methods: + +- The `getId` method must provide a unique per-block-type string ID. +- The `getLabel` method must provide a general string label for the block. +- The `getView` method provides a string path to a view file (Can be one custom registered). + - This is provided the available view data at time of render, so it can be dynamic based on context. +- The `withData` method is called when block is being rendered, and it should return an array of data which will be merged with existing view data. + - This is also provided available view data, for use as context. + +In this example, we're also making use of the `BookQueries` internal BookStack class. +You're able to inject any other dependent classes/services via the constructor like this, and BookStack will attempt to auto-resolve them. + +We can then register this block class using the logical theme system like so: + +```php +use BookStack\Facades\Theme; +use BookStack\Theming\ThemeEvents; +use BookStack\View\ViewBlockManager; + +Theme::listen(ThemeEvents::VIEW_BLOCKS_REGISTER, function (ViewBlockManager $manager) { + $manager->register( + 'home-default', // The location/layout where this block will be displayed + 'right', // The default position for the block within the location + BookTotalBlock::class // The block class to register + ); +}); +``` + +The above registration code would typically be within your `functions.php` theme file. +You could also define the above block class in the same file or separate it into its own file +and include it from the `functions.php` via a `require_once()` call. + +Lastly, we'll need to create the view for the registered block. +In our example we return `blocks.total-blocks` from the `getView` method, so our view needs to be located at +`blocks/total-blocks.blade.php` from a view providing directory. +In a theme folder, we can just create it at `blocks/total-blocks.blade.php` within the folder path. +If we were building in a module, we'd need to create this at `views/blocks/total-blocks.blade.php` within our module folder. +For our example, we'll use this content to make use of the data we're passing to the view: + +```html +
+

Total Books

+
+

+ There are currently {{ $totalBooks }} books in the system! +

+
+
+``` + +This will then show up on the default home grid view, in the right column. +The exact position within that location may change depending on user preferences, but the block will be limited +to the location provided during registration unless it has also been registered for other locations. diff --git a/dev/licensing/php-library-licenses.txt b/dev/licensing/php-library-licenses.txt index 2259c44c301..892842c4a9e 100644 --- a/dev/licensing/php-library-licenses.txt +++ b/dev/licensing/php-library-licenses.txt @@ -110,7 +110,7 @@ License: BSD-3-Clause License File: vendor/firebase/php-jwt/LICENSE Copyright: Copyright (c) 2011, Neuman Vong Source: https://github.com/googleapis/php-jwt.git -Link: https://github.com/firebase/php-jwt +Link: https://github.com/googleapis/php-jwt ----------- fruitcake/php-cors License: MIT @@ -123,7 +123,7 @@ Link: https://github.com/fruitcake/php-cors graham-campbell/result-type License: MIT License File: vendor/graham-campbell/result-type/LICENSE -Copyright: Copyright (c) 2020-2024 Graham Campbell <*****@**********.**.**> +Copyright: Copyright (c) 2020-2026 Graham Campbell <*****@**********.**.**> Source: https://github.com/GrahamCampbell/Result-Type.git Link: https://github.com/GrahamCampbell/Result-Type.git ----------- diff --git a/lang/ar/settings.php b/lang/ar/settings.php index aa361e04b9a..af02a556411 100644 --- a/lang/ar/settings.php +++ b/lang/ar/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/bg/activities.php b/lang/bg/activities.php index 344423e0d6c..6195e0ae8fb 100644 --- a/lang/bg/activities.php +++ b/lang/bg/activities.php @@ -111,7 +111,7 @@ 'api_token_delete_notification' => 'API token successfully deleted', // Roles - 'role_create' => 'created role', + 'role_create' => 'създадена роля', 'role_create_notification' => 'Успешна създадена роля', 'role_update' => 'updated role', 'role_update_notification' => 'Успешно обновена роля', diff --git a/lang/bg/auth.php b/lang/bg/auth.php index 91c7495a108..a0e171b42cb 100644 --- a/lang/bg/auth.php +++ b/lang/bg/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Въведените данни не съвпадат с информацията в системата.', 'throttle' => 'Твърде много опити за влизане. Опитайте пак след :seconds секунди.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Твърде много опити за мултифакторна верификация. Опитайте пак след :seconds секунди.', // Login & Register 'sign_up' => 'Регистриране', diff --git a/lang/bg/components.php b/lang/bg/components.php index f88c69fc8be..03a9ff755b9 100644 --- a/lang/bg/components.php +++ b/lang/bg/components.php @@ -6,7 +6,7 @@ // Image Manager 'image_select' => 'Избор на изображение', - 'image_list' => '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.', diff --git a/lang/bg/editor.php b/lang/bg/editor.php index 16c951a71a4..a78d9eff94f 100644 --- a/lang/bg/editor.php +++ b/lang/bg/editor.php @@ -48,7 +48,7 @@ 'superscript' => 'Горен индекс', 'subscript' => 'Долен индекс', 'text_color' => 'Цвят на текста', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Цвят на подчертаване', 'custom_color' => 'Цвят по избор', 'remove_color' => 'Премахване на цвят', 'background_color' => 'Фонов цвят', diff --git a/lang/bg/notifications.php b/lang/bg/notifications.php index 563ac24e84d..ecee032d6c8 100644 --- a/lang/bg/notifications.php +++ b/lang/bg/notifications.php @@ -15,7 +15,7 @@ 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', 'detail_page_name' => 'Page Name:', - 'detail_page_path' => 'Page Path:', + 'detail_page_path' => 'Път към страница:', 'detail_commenter' => 'Commenter:', 'detail_comment' => 'Comment:', 'detail_created_by' => 'Created By:', diff --git a/lang/bg/preferences.php b/lang/bg/preferences.php index 8f5aaa07e90..6bff8fcfa29 100644 --- a/lang/bg/preferences.php +++ b/lang/bg/preferences.php @@ -37,7 +37,7 @@ 'profile' => 'Profile Details', 'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.', - 'profile_view_public' => 'View Public Profile', + 'profile_view_public' => 'Виж публичния профил', 'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.', 'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.', 'profile_email_no_permission' => 'Unfortunately you don\'t have permission to change your email address. If you want to change this, you\'d need to ask an administrator to change this for you.', diff --git a/lang/bg/settings.php b/lang/bg/settings.php index 0af97414045..2b04c073a11 100644 --- a/lang/bg/settings.php +++ b/lang/bg/settings.php @@ -51,7 +51,7 @@ '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', + 'app_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.', 'bookshelf_color' => 'Цвят на рафта', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/bg/validation.php b/lang/bg/validation.php index e2f9bdaa9c1..0c45ee28cd7 100644 --- a/lang/bg/validation.php +++ b/lang/bg/validation.php @@ -105,11 +105,11 @@ 'url' => 'Форматът на :attribute не е валиден.', 'uploaded' => 'Файлът не можа да бъде качен. Сървърът може да не приема файлове с такъв размер.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => ':attribute трябва да реферира към файл в ZIP архива.', + 'zip_file_size' => 'Файла :attribute не трябва да надвишава :size MB.', + 'zip_file_mime' => ':attribute трябва да реферира към файл от тип :validTypes, а е намерен :foundType.', + 'zip_model_expected' => 'Очаква се обект с данни, но е открит ":type".', + 'zip_unique' => ':attribute трябва да бъде уникален за типа на обекта в ZIP архива.', // Custom validation lines 'custom' => [ diff --git a/lang/bn/settings.php b/lang/bn/settings.php index ab7fe951271..5bceb85f8a4 100644 --- a/lang/bn/settings.php +++ b/lang/bn/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/bs/settings.php b/lang/bs/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/bs/settings.php +++ b/lang/bs/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ca/settings.php b/lang/ca/settings.php index ac60ce8e2cb..d5a70545977 100644 --- a/lang/ca/settings.php +++ b/lang/ca/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/cs/settings.php b/lang/cs/settings.php index 85c479dbddb..92425b5e4e0 100644 --- a/lang/cs/settings.php +++ b/lang/cs/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/cy/settings.php b/lang/cy/settings.php index 816a4b89ffb..8ac7bbc50b9 100644 --- a/lang/cy/settings.php +++ b/lang/cy/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/da/settings.php b/lang/da/settings.php index 9d83f5004db..c28c9fef0b8 100644 --- a/lang/da/settings.php +++ b/lang/da/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'Thailandsk', 'tr' => 'Türkçe', diff --git a/lang/de/settings.php b/lang/de/settings.php index 29e857098d6..a202898cf4e 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Russisch', 'sk' => 'Slowenisch', 'sl' => 'Slowenisch', + 'sr' => 'Српски', 'sv' => 'Schwedisch', 'th' => 'ภาษาไทย', 'tr' => 'Türkisch', diff --git a/lang/de_informal/settings.php b/lang/de_informal/settings.php index 7c1796afcc6..f02f6e02559 100644 --- a/lang/de_informal/settings.php +++ b/lang/de_informal/settings.php @@ -367,6 +367,7 @@ 'ru' => 'Russisch', 'sk' => 'Slowenisch', 'sl' => 'Slowenisch', + 'sr' => 'Српски', 'sv' => 'Schwedisch', 'th' => 'ภาษาไทย', 'tr' => 'Türkisch', diff --git a/lang/el/settings.php b/lang/el/settings.php index 42422a8e7da..01379e71bba 100644 --- a/lang/el/settings.php +++ b/lang/el/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/en/common.php b/lang/en/common.php index 06a9e855ce3..46b2a187c25 100644 --- a/lang/en/common.php +++ b/lang/en/common.php @@ -84,6 +84,8 @@ 'status_inactive' => 'Inactive', 'never' => 'Never', 'none' => 'None', + 'move_left' => 'Move Left', + 'move_right' => 'Move Right', // Header 'homepage' => 'Homepage', diff --git a/lang/en/preferences.php b/lang/en/preferences.php index f4459d738e4..1b874efa2f1 100644 --- a/lang/en/preferences.php +++ b/lang/en/preferences.php @@ -19,6 +19,27 @@ 'shortcuts_update_success' => 'Shortcut preferences have been updated!', 'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.', + 'interface' => 'Interface Preferences', + 'interface_desc' => 'Here you can find options to customize the appearance of the application user interface.', + 'interface_display_mode' => 'Display Mode', + 'interface_display_mode_desc' => 'Choose whether the application should show in dark or light mode. This can also be toggled from the home view, or via the profile dropdown in the header bar.', + 'layouts' => 'UI Layout Preferences', + 'layouts_desc' => 'Customize the layout of sections shown in the user interface for a range of views.', + 'layout_edit' => 'Edit Layout', + 'layout_edit_desc' => 'Drag and drop sections, or use the action menu found on each, to reconfigure which sections show within this layout in the interface, and where they are displayed.', + 'layout_edit_column_hint' => 'When viewed on smaller screen sizes, right column sections will be stacked on top of left column sections.', + 'layout_edit_save' => 'Save Layout', + 'layout_edit_layouts' => 'Layouts', + 'layout_edit_back_to_preferences' => 'Back to Preferences', + 'layout_edit_left' => 'Left', + 'layout_edit_right' => 'Right', + 'layout_edit_center' => 'Center', + 'layout_edit_unused' => 'Unused', + 'layout_edit_empty' => 'No sections to display', + 'layout_edit_reset_to_defaults' => 'Reset to Default', + 'layout_update_success' => 'Layout preferences have been updated!', + 'layout_reset_success' => 'Layout preferences have been reset!', + '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', diff --git a/lang/en/settings.php b/lang/en/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/en/validation.php b/lang/en/validation.php index ff028525df3..532d861d244 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -16,6 +16,7 @@ 'alpha_num' => 'The :attribute may only contain letters and numbers.', 'array' => 'The :attribute must be an array.', 'backup_codes' => 'The provided code is not valid or has already been used.', + 'base64_uri_mime' => 'The :attribute must be a valid base64 URI containing data of :mime mime type.', 'before' => 'The :attribute must be a date before :date.', 'between' => [ 'numeric' => 'The :attribute must be between :min and :max.', diff --git a/lang/es/activities.php b/lang/es/activities.php index ecfffeb5194..e256a88c9a3 100644 --- a/lang/es/activities.php +++ b/lang/es/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Usuario actualizado correctamente', 'user_delete' => 'usuario eliminado', 'user_delete_notification' => 'Usuario eliminado correctamente', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'reiniciar autenticación en dos pasos para el usuario', + 'user_mfa_reset_notification' => 'Métodos de autenticación en dos pasos restablecidos', // API Tokens 'api_token_create' => 'token de API creado', diff --git a/lang/es/auth.php b/lang/es/auth.php index 467508001e9..0a3cba2894a 100644 --- a/lang/es/auth.php +++ b/lang/es/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Estas credenciales no coinciden con nuestros registros.', 'throttle' => 'Demasiados intentos de inicio de sesión. Por favor, inténtalo de nuevo en :seconds segundos.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Demasiados intentos de verificación de autenticación en dos pasos. Por favor, inténtalo de nuevo en :seconds segundos.', // Login & Register 'sign_up' => 'Registrarse', diff --git a/lang/es/entities.php b/lang/es/entities.php index 7814161222b..e5ee473a034 100644 --- a/lang/es/entities.php +++ b/lang/es/entities.php @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Mostrar/ocultar barra lateral', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Contenido de Página', + 'page_contents_none' => 'No se encontraron encabezados en el contenido de la página.', + 'page_contents_info' => 'El menú de los contenidos de la página se genera a partir de cualquier formato de encabezado utilizado en la página.', 'page_tags' => 'Etiquetas de Página', 'chapter_tags' => 'Etiquetas de Capítulo', 'book_tags' => 'Etiquetas de Libro', diff --git a/lang/es/settings.php b/lang/es/settings.php index 2fe672f83db..c776cae32f0 100644 --- a/lang/es/settings.php +++ b/lang/es/settings.php @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'La autenticación en dos pasos añade una capa de seguridad adicional a tu cuenta.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar métodos', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Restablecer métodos de autenticación en dos pasos', + 'users_mfa_reset_desc' => 'Esto restablecerá y borrará todos los métodos de autenticación en dos pasos configurados para este usuario. Si la autenticación en dos pasos es requerida por cualquiera de sus roles, se les pedirá que configuren nuevos métodos en su próximo inicio de sesión.', + 'users_mfa_reset_confirm' => '¿Estás seguro de que deseas eliminar la autenticación en dos pasos para este usuario?', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -366,6 +366,7 @@ 'ru' => 'Ruso', 'sk' => 'Eslovaco', 'sl' => 'Esloveno', + 'sr' => 'Српски', 'sv' => 'Sueco', 'th' => 'ภาษาไทย', 'tr' => 'Turco', diff --git a/lang/es_AR/activities.php b/lang/es_AR/activities.php index b4ba5690e0e..ececde5badd 100644 --- a/lang/es_AR/activities.php +++ b/lang/es_AR/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Usuario actualizado con éxito', 'user_delete' => 'usuario eliminado', 'user_delete_notification' => 'El usuario fue eliminado correctamente', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'reiniciar autenticación en dos pasos para el usuario', + 'user_mfa_reset_notification' => 'Métodos de autenticación en dos pasos restablecidos', // API Tokens 'api_token_create' => 'token de API creado', diff --git a/lang/es_AR/auth.php b/lang/es_AR/auth.php index ad69e651ea0..82bd1a01497 100644 --- a/lang/es_AR/auth.php +++ b/lang/es_AR/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Estas credenciales no concuerdan con nuestros registros.', 'throttle' => 'Demasiados intentos fallidos de inicio de sesión. Por favor intente nuevamente en :seconds segundos.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Demasiados intentos de verificación de autenticación en dos pasos. Por favor, inténtalo de nuevo en :seconds segundos.', // Login & Register 'sign_up' => 'Registrarse', diff --git a/lang/es_AR/entities.php b/lang/es_AR/entities.php index aac46b4477a..72aeb2d9e8d 100644 --- a/lang/es_AR/entities.php +++ b/lang/es_AR/entities.php @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Mostrar/ocultar barra lateral', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Contenido de Página', + 'page_contents_none' => 'No se encontraron encabezados en el contenido de la página.', + 'page_contents_info' => 'El menú de los contenidos de la página se genera a partir de cualquier formato de encabezado utilizado en la página.', 'page_tags' => 'Etiquetas de página', 'chapter_tags' => 'Etiquetas de capítulo', 'book_tags' => 'Etiquetas de libro', diff --git a/lang/es_AR/settings.php b/lang/es_AR/settings.php index 5545b91f3bc..1baf726d1d5 100644 --- a/lang/es_AR/settings.php +++ b/lang/es_AR/settings.php @@ -265,9 +265,9 @@ 'users_mfa_desc' => 'Configure la autenticación de múltiples factores como una capa extra de seguridad para su cuenta de usuario.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Restablecer métodos de autenticación en dos pasos', + 'users_mfa_reset_desc' => 'Esto restablecerá y borrará todos los métodos de autenticación en dos pasos configurados para este usuario. Si la autenticación en dos pasos es requerida por cualquiera de sus roles, se les pedirá que configuren nuevos métodos en su próximo inicio de sesión.', + 'users_mfa_reset_confirm' => '¿Estás seguro de que deseas eliminar la autenticación en dos pasos para este usuario?', // API Tokens 'user_api_token_create' => 'Crear token API', @@ -367,6 +367,7 @@ 'ru' => 'Ruso', 'sk' => 'Eslovaco', 'sl' => 'Esloveno', + 'sr' => 'Српски', 'sv' => 'Sueco', 'th' => 'ภาษาไทย', 'tr' => 'Turco', diff --git a/lang/et/activities.php b/lang/et/activities.php index e42dd7fda36..87d474880b3 100644 --- a/lang/et/activities.php +++ b/lang/et/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Kasutaja on muudetud', 'user_delete' => 'kustutas kasutaja', 'user_delete_notification' => 'Kasutaja on kustutatud', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'lähtestas kasutaja MFA', + 'user_mfa_reset_notification' => 'Mitmeastmelise autentimise meetodite lähestamine', // API Tokens 'api_token_create' => 'lisas API tunnuse', diff --git a/lang/et/auth.php b/lang/et/auth.php index afbd2870bc7..56458ea20b1 100644 --- a/lang/et/auth.php +++ b/lang/et/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Kasutajanimi ja parool ei klapi.', 'throttle' => 'Liiga palju sisselogimiskatseid. Proovi uuesti :seconds sekundi pärast.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Liiga palju mitmeastmelise kinnituse katseid. Proovi uuesti :seconds sekundi pärast.', // Login & Register 'sign_up' => 'Registreeru', diff --git a/lang/et/entities.php b/lang/et/entities.php index 11c2f77aeb2..ec2fc183361 100644 --- a/lang/et/entities.php +++ b/lang/et/entities.php @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Kuva/peida külgriba', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Lehe sisu', + 'page_contents_none' => 'Lehe sisus ei leitud ühtegi pealkirja.', + 'page_contents_info' => 'Sisukord genereeritakse automaatselt lehel kasutatud pealkirjavormingutest.', 'page_tags' => 'Lehe sildid', 'chapter_tags' => 'Peatüki sildid', 'book_tags' => 'Raamatu sildid', diff --git a/lang/et/settings.php b/lang/et/settings.php index 3e28eae95f5..58800f5e39e 100644 --- a/lang/et/settings.php +++ b/lang/et/settings.php @@ -207,7 +207,7 @@ 'role_all' => 'Kõik', 'role_own' => 'Enda omad', 'role_controlled_by_asset' => 'Õigused määratud seotud objekti kaudu', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Määratud lehe kustutamise õiguste kaudu', 'role_save' => 'Salvesta roll', 'role_users' => 'Selle rolliga kasutajad', 'role_users_none' => 'Seda rolli ei ole hetkel ühelgi kasutajal', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Seadista mitmeastmeline autentimine, et oma kasutajakonto turvalisust tõsta.', 'users_mfa_x_methods' => ':count meetod seadistatud|:count meetodit seadistatud', 'users_mfa_configure' => 'Seadista meetodid', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Lähtesta mitmeastmelise autentimise meetodid', + 'users_mfa_reset_desc' => 'See lähestab kõik selle kasutaja seadistatud mitmeastmelise autentimise meetodid. Kui mõni tema rollidest nõuab mitmeastmelist autentimist, peab ta järgmisel sisselogimisel uued meetodid seadistama.', + 'users_mfa_reset_confirm' => 'Kas oled kindel, et soovid selle kasutaja mitmeastmelise autentimise meetodid lähtestada?', // API Tokens 'user_api_token_create' => 'Lisa API tunnus', @@ -366,6 +366,7 @@ 'ru' => 'Русский (vene keel)', 'sk' => 'Slovensky', 'sl' => 'Sloveenia', + 'sr' => 'Српски', 'sv' => 'Rootsi', 'th' => 'ภาษาไทย', 'tr' => 'Türgi', diff --git a/lang/eu/settings.php b/lang/eu/settings.php index ffa2c182e81..563df987108 100644 --- a/lang/eu/settings.php +++ b/lang/eu/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/fa/activities.php b/lang/fa/activities.php index 876c545a67f..85a2d72cbb6 100644 --- a/lang/fa/activities.php +++ b/lang/fa/activities.php @@ -36,8 +36,8 @@ 'book_update_notification' => 'کتاب با موفقیت به روزرسانی شد', 'book_delete' => 'حذف کتاب', 'book_delete_notification' => 'کتاب با موفقیت حذف شد', - 'book_sort' => 'مرتب سازی کتاب', - 'book_sort_notification' => 'کتاب با موفقیت مرتب سازی شد', + 'book_sort' => 'کتاب‌های مرتب‌شده', + 'book_sort_notification' => 'کتاب با موفقیت مجددا مرتب شد', // Bookshelves 'bookshelf_create' => 'ایجاد قفسه', @@ -99,8 +99,8 @@ 'user_update_notification' => 'کاربر با موفقیت به روز شد', 'user_delete' => 'کاربر حذف شده', 'user_delete_notification' => 'کاربر با موفقیت حذف شد', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'بازنشانی احراز هویت چندمرحله ای برای کاربر', + 'user_mfa_reset_notification' => 'بازنشانی روش‌های احراز هویت چندمرحله‌ای', // API Tokens 'api_token_create' => 'ایجاد توکن API', diff --git a/lang/fa/auth.php b/lang/fa/auth.php index 1896663996c..2ac7ddd4734 100644 --- a/lang/fa/auth.php +++ b/lang/fa/auth.php @@ -8,7 +8,7 @@ 'failed' => 'مشخصات وارد شده با اطلاعات ما سازگار نیست.', 'throttle' => 'دفعات تلاش شما برای ورود بیش از حد مجاز است. لطفا پس از :seconds ثانیه مجددا تلاش فرمایید.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'تلاش‌های تأیید هویت چندمرحله‌ای بیش از حد مجاز است. لطفاً پس از :seconds ثانیه دوباره تلاش کنید.', // Login & Register 'sign_up' => 'ثبت نام', diff --git a/lang/fa/entities.php b/lang/fa/entities.php index afeeca0e045..7533edd43f8 100644 --- a/lang/fa/entities.php +++ b/lang/fa/entities.php @@ -173,7 +173,7 @@ 'books_sort_desc' => 'برای سامان‌دهی محتوای یک کتاب، می‌توانید فصل‌ها و صفحات آن را جابه‌جا کنید. همچنین می‌توانید کتاب‌های دیگری بیفزایید تا جابه‌جایی فصل‌ها و صفحات میان کتاب‌ها آسان شود. در صورت تمایل، می‌توانید قاعده‌ای برای مرتب‌سازی خودکار تعیین کنید تا محتوای کتاب در صورت ایجاد تغییرات، به طور خودکار مرتب شود.', 'books_sort_auto_sort' => 'گزینه مرتب‌سازی خودکار', 'books_sort_auto_sort_active' => 'مرتب‌سازی خودکار با قاعده: :sortName فعال است', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'قوانین گزینه‌ی مرتب‌سازی خودکار را می‌توان توسط کاربری که دسترسی‌های لازم را دارد، در بخش تنظیمات "فهرست‌ها و مرتب‌سازی" ایجاد کرد.', 'books_sort_named' => 'مرتب‌سازی کتاب:bookName', 'books_sort_name' => 'مرتب‌سازی بر اساس نام', 'books_sort_created' => 'مرتب‌سازی بر اساس تاریخ ایجاد', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'نمایش/پنهان‌سازی نوار کناری', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'محتوای صفحه', + 'page_contents_none' => 'هیچ سرتیتری در محتوای صفحه یافت نشد.', + 'page_contents_info' => 'منوی محتوا از هر سرتیتری که در صفحه استفاده شده باشد تولید می شود.', 'page_tags' => 'برچسب‌های صفحه', 'chapter_tags' => 'برچسب‌های فصل', 'book_tags' => 'برچسب های کتاب', diff --git a/lang/fa/errors.php b/lang/fa/errors.php index 777d6e69293..94c268793a7 100644 --- a/lang/fa/errors.php +++ b/lang/fa/errors.php @@ -109,7 +109,7 @@ 'import_zip_cant_read' => 'امکان ایجاد کاربر وجود ندارد؛ زیرا ارسال ایمیل دعوت با خطا مواجه شد.', 'import_zip_cant_decode_data' => 'محتوای data.json در فایل ZIP پیدا یا رمزگشایی نشد.', 'import_zip_no_data' => 'داده‌های فایل ZIP فاقد محتوای کتاب، فصل یا صفحه مورد انتظار است.', - 'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.', + 'import_zip_data_too_large' => 'محتوای فایل ZIP data.json از حداکثر حجم آپلود مجازِ تعیین‌شده در برنامه بیشتر است.', 'import_validation_failed' => 'اعتبارسنجی فایل ZIP واردشده با خطا مواجه شد:', 'import_zip_failed_notification' => ' فایل ZIP وارد نشد.', 'import_perms_books' => 'شما مجوز لازم برای ایجاد کتاب را ندارید.', @@ -125,7 +125,7 @@ 'api_incorrect_token_secret' => 'راز ارائه شده برای کد API استفاده شده نادرست است', 'api_user_no_api_permission' => 'مالک نشانه API استفاده شده اجازه برقراری تماس های API را ندارد', 'api_user_token_expired' => 'رمز مجوز استفاده شده منقضی شده است', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => 'برای استفاده از API با احراز هویت مبتنی بر cookie فقط درخواست GET مجاز است', // Settings & Maintenance 'maintenance_test_email_failure' => 'خطا در هنگام ارسال ایمیل آزمایشی:', diff --git a/lang/fa/notifications.php b/lang/fa/notifications.php index d216b04fec6..b602095860c 100644 --- a/lang/fa/notifications.php +++ b/lang/fa/notifications.php @@ -11,11 +11,11 @@ 'updated_page_subject' => 'صفحه جدید: :pageName', 'updated_page_intro' => 'یک صفحه جدید ایجاد شده است در :appName:', 'updated_page_debounce' => 'برای جلوگیری از انبوه اعلان‌ها، برای مدتی اعلان‌ ویرایش‌هایی که توسط همان ویرایشگر در این صفحه انجام می‌شود، ارسال نخواهد شد.', - 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'comment_mention_subject' => 'شما در نظرات صفحه :pageName تگ شده‌اید', + 'comment_mention_intro' => 'شما در نظری در :appName: تگ شده اید:', 'detail_page_name' => 'نام صفحه:', - 'detail_page_path' => 'نام میسر صفحه:', + 'detail_page_path' => 'نام مسیر صفحه:', 'detail_commenter' => 'نظر دهنده:', 'detail_comment' => 'نظر:', 'detail_created_by' => 'ایجاد شده توسط:', diff --git a/lang/fa/preferences.php b/lang/fa/preferences.php index 00d277bdfd5..b24225c3deb 100644 --- a/lang/fa/preferences.php +++ b/lang/fa/preferences.php @@ -9,7 +9,7 @@ 'shortcuts' => 'میانبرها', 'shortcuts_interface' => 'تنظیمات کلید‌های میانبر رابط کاربری', - 'shortcuts_toggle_desc' => 'در اینجا می توانید میانبرهای سیستم را که برای پیمایش و ... استفاده می شود، فعال یا غیرفعال کنید.', + 'shortcuts_toggle_desc' => 'در اینجا می توانید میانبرهای سیستم را که برای پیمایش و... استفاده می شود، فعال یا غیرفعال کنید.', 'shortcuts_customize_desc' => 'می توانید هر یک از میانبرهای زیر را سفارشی کنید. کافی است پس از انتخاب ورودی برای میانبر، کلید ترکیبی مورد نظر خود را فشار دهید.', 'shortcuts_toggle_label' => 'میانبرهای صفحه کلید فعال شد', 'shortcuts_section_navigation' => 'ناوبری و پیمایش', @@ -23,7 +23,7 @@ 'notifications_desc' => 'تنظیمات اطلاعیه‌های ایمیلی هنگام انجام فعالیت‌های خاص در سیستم.', 'notifications_opt_own_page_changes' => 'در صورت تغییرات در صفحاتی که متعلق به من است، اطلاع بده', 'notifications_opt_own_page_comments' => 'در صورت ثبت نظر در صفحاتی که متعلق به من است، اطلاع بده', - 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', + 'notifications_opt_comment_mentions' => 'وقتی که من در یک نظر تگ شده ام اطلاع بده', 'notifications_opt_comment_replies' => 'پس از درج پاسخ به روی نظراتی که من ثبت کرده‌ام، اطلاع بده', 'notifications_save' => 'ذخیره تنظیمات', 'notifications_update_success' => 'تنظیمات اعلان‌ها به روز شده است!', @@ -36,7 +36,7 @@ 'auth_change_password_success' => 'رمز عبور به روز شد!', 'profile' => 'جزئیات پروفایل', - 'profile_desc' => 'علاوه بر جزئیاتی که برای ارتباط و شخصی‌سازی سیستم استفاده می‌شود، جزییات حساب خود را که نشان دهنده شما برای سایر کاربران است، مدیریت کنید.', + 'profile_desc' => 'علاوه بر جزئیاتی که برای ارتباط و شخصی‌سازی سیستم استفاده می‌شود، جزئیات حساب خود را که نشان دهنده شما برای سایر کاربران است، مدیریت کنید.', 'profile_view_public' => 'مشاهده پروفایل عمومی', 'profile_name_desc' => 'نام نمایشی خود را تنظیم کنید. این نام بسته به فعالیت شما و محتوای متعلق به شما، برای سایر کاربران سیستم قابل مشاهده است.', 'profile_email_desc' => 'این ایمیل برای اعلان‌ها و دسترسی به سیستم استفاده خواهد شد.', diff --git a/lang/fa/settings.php b/lang/fa/settings.php index bb1b1ca7e2d..4ba9a1fcd83 100644 --- a/lang/fa/settings.php +++ b/lang/fa/settings.php @@ -75,8 +75,8 @@ 'reg_confirm_restrict_domain_placeholder' => 'بدون محدودیت', // Sorting Settings - 'sorting' => 'Lists & Sorting', - 'sorting_book_default' => 'Default Book Sort Rule', + 'sorting' => 'فهرست‌ها و مرتب‌سازی', + 'sorting_book_default' => 'قانون پیش‌فرض مرتب‌سازی کتاب', 'sorting_book_default_desc' => 'قانون پیش‌فرض مرتب‌سازی را برای کتاب‌های جدید انتخاب کنید. تغییر قانون بر ترتیب کتاب‌های موجود تأثیری ندارد و می‌تواند برای هر کتاب به‌صورت جداگانه تغییر یابد.', 'sorting_rules' => 'قوانین مرتب‌سازی', 'sorting_rules_desc' => 'این‌ها عملیات مرتب‌سازی از پیش تعریف‌شده‌ای هستند که می‌توانید آن‌ها را بر محتوای سیستم اعمال کنید.', @@ -103,8 +103,8 @@ 'sort_rule_op_updated_date' => 'تاریخ به‌روزرسانی', 'sort_rule_op_chapters_first' => 'ابتدا فصل‌ها', 'sort_rule_op_chapters_last' => 'فصل‌ها در آخر', - 'sorting_page_limits' => 'Per-Page Display Limits', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting_page_limits' => 'محدودیت نمایش در هر صفحه', + 'sorting_page_limits_desc' => 'تعیین کنید که چه تعداد آیتم در فهرست‌های مختلف سیستم در هر صفحه نمایش داده شود. معمولاً مقدار کمتر عملکرد بهتری خواهد داشت، در حالی که مقدار بیشتر نیاز به کلیک کردن در صفحات متعدد را از بین می‌برد. استفاده از ضریبی از ۶ توصیه می‌شود.', // Maintenance settings 'maint' => 'نگهداری', @@ -197,17 +197,17 @@ 'role_import_content' => 'وارد کردن محتوا', 'role_editor_change' => 'تغییر ویرایشگر صفحه', 'role_notifications' => 'دریافت و مدیریت اعلان‌ها', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', + 'role_permission_note_users_and_roles' => 'این دسترسی ها از نظر فنی امکان مشاهده و جستجوی کاربران و نقش ها را در سیستم فراهم می کنند.', 'role_asset' => 'مجوزهای دارایی', 'roles_system_warning' => 'توجه داشته باشید که دسترسی به هر یک از سه مجوز فوق می‌تواند به کاربر اجازه دهد تا امتیازات خود یا امتیازات دیگران را در سیستم تغییر دهد. فقط نقش هایی را با این مجوزها به کاربران مورد اعتماد اختصاص دهید.', 'role_asset_desc' => 'این مجوزها دسترسی پیش‌فرض به دارایی‌های درون سیستم را کنترل می‌کنند. مجوزهای مربوط به کتاب‌ها، فصل‌ها و صفحات این مجوزها را لغو می‌کنند.', 'role_asset_admins' => 'به ادمین‌ها به‌طور خودکار به همه محتوا دسترسی داده می‌شود، اما این گزینه‌ها ممکن است گزینه‌های UI را نشان داده یا پنهان کنند.', 'role_asset_image_view_note' => 'این مربوط به مرئی بودن در بخش مدیر تصاویر است. دسترسی عملی به تصاویر آپلود شده بستگی به گزینه ذخیره‌سازی تصویر سیستم دارد.', - 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', + 'role_asset_users_note' => 'این دسترسی ها از نظر فنی امکان مشاهده و جستجوی کاربران و نقش ها را در سیستم فراهم می کنند.', 'role_all' => 'همه', 'role_own' => 'صاحب', 'role_controlled_by_asset' => 'توسط دارایی که در آن آپلود می شود کنترل می شود', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'کنترل شده با دسترسی حذف صفحه', 'role_save' => 'ذخیره نقش', 'role_users' => 'کاربران در این نقش', 'role_users_none' => 'در حال حاضر هیچ کاربری به این نقش اختصاص داده نشده است', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'تنظیم احراز هویت چند مرحله ای یک لایه امنیتی دیگر به حساب شما اضافه میکند.', 'users_mfa_x_methods' => ':count روش پیکربندی شده است|:count روش های پیکربندی شده', 'users_mfa_configure' => 'روش پیکربندی', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'بازنشانی روش های احراز هویت چندمرحله ای', + 'users_mfa_reset_desc' => 'این کار تمام روش‌های احراز هویت چندمرحله‌ای پیکربندی‌شده برای این کاربر را بازنشانی و پاک می‌کند. اگر احراز هویت چندمرحله‌ای توسط هر یک از نقش‌های آن‌ها الزامی شده باشد، در ورود بعدی از آن‌ها خواسته می‌شود که روش‌های جدیدی را پیکربندی کنند.', + 'users_mfa_reset_confirm' => 'آیا شما از بازنشانی احراز هویت چندمرحله ای برای این کاربر اطمینان دارید؟', // API Tokens 'user_api_token_create' => 'ایجاد توکن API', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/fa/validation.php b/lang/fa/validation.php index 93c7dcb6669..c4ccabfca19 100644 --- a/lang/fa/validation.php +++ b/lang/fa/validation.php @@ -106,7 +106,7 @@ 'uploaded' => 'بارگذاری فایل :attribute موفقیت آمیز نبود.', 'zip_file' => 'ویژگی :attribute باید به یک فایل درون پرونده فشرده شده اشاره کند.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', + 'zip_file_size' => 'حجم فایل :attribute نباید بیشتر از :size مگابایت باشد.', 'zip_file_mime' => 'ویژگی :attribute باید به فایلی با نوع :validTypes اشاره کند، اما نوع یافت‌شده :foundType است.', 'zip_model_expected' => 'سیستم در این بخش انتظار دریافت یک شیء داده‌ای را داشت، اما «:type» دریافت گردید', 'zip_unique' => 'برای هر نوع شیء در فایل ZIP، مقدار ویژگی :attribute باید یکتا و بدون تکرار باشد.', diff --git a/lang/fi/settings.php b/lang/fi/settings.php index aa8ac3e592b..3f77d1f154f 100644 --- a/lang/fi/settings.php +++ b/lang/fi/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/fr/activities.php b/lang/fr/activities.php index fcacc90898e..e0c51962ae8 100644 --- a/lang/fr/activities.php +++ b/lang/fr/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Utilisateur mis à jour avec succès', 'user_delete' => 'utilisateur supprimé', 'user_delete_notification' => 'Utilisateur supprimé avec succès', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'réinitialiser l\'authentification multifacteur pour l\'utilisateur', + 'user_mfa_reset_notification' => 'Les méthodes d\'authentification multifacteurs sont réinitialisées', // API Tokens 'api_token_create' => 'a créé un jeton API', diff --git a/lang/fr/auth.php b/lang/fr/auth.php index a7fe59d0ccb..e2f94904877 100644 --- a/lang/fr/auth.php +++ b/lang/fr/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Ces informations ne correspondent à aucun compte.', 'throttle' => 'Trop d\'essais, veuillez réessayer dans :seconds secondes.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Trop de tentatives de vérification multifactorielle. Veuillez réessayer dans :secondes secondes.', // Login & Register 'sign_up' => 'S\'inscrire', diff --git a/lang/fr/entities.php b/lang/fr/entities.php index fa0808912a7..3479ab57221 100644 --- a/lang/fr/entities.php +++ b/lang/fr/entities.php @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Afficher/masquer la barre latérale', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Contenu de la page', + 'page_contents_none' => 'Aucun titre n\'a été trouvé dans le contenu de la page.', + 'page_contents_info' => 'Le menu de contenu est généré à partir de tous les formats de titres utilisés dans la page.', 'page_tags' => 'Étiquettes de la page', 'chapter_tags' => 'Étiquettes du chapitre', 'book_tags' => 'Étiquettes du livre', diff --git a/lang/fr/preferences.php b/lang/fr/preferences.php index dbae975f31d..b16d5acf2df 100644 --- a/lang/fr/preferences.php +++ b/lang/fr/preferences.php @@ -15,7 +15,7 @@ 'shortcuts_section_navigation' => 'Navigation', 'shortcuts_section_actions' => 'Actions communes', 'shortcuts_save' => 'Sauvegarder les raccourcis', - 'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur "?" qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\'écran.', + 'shortcuts_overlay_desc' => 'Note : Lorsque les raccourcis sont activés, assistant est disponible en appuyant sur «?» qui mettra en surbrillance les raccourcis disponibles pour les actions actuellement visibles à l\'écran.', 'shortcuts_update_success' => 'Les préférences de raccourci ont été mises à jour !', 'shortcuts_overview_desc' => 'Gérer les raccourcis clavier que vous pouvez utiliser pour naviguer dans l\'interface utilisateur du système.', diff --git a/lang/fr/settings.php b/lang/fr/settings.php index 8ff81ba6bb3..cd2368db18c 100644 --- a/lang/fr/settings.php +++ b/lang/fr/settings.php @@ -39,7 +39,7 @@ 'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.', 'app_homepage_select' => 'Choisissez une page', 'app_footer_links' => 'Liens de pied de page', - 'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécesittent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".', + 'app_footer_links_desc' => 'Ajoutez des liens dans le pied de page du site. Ils seront affichés en bas de la plupart des pages, incluant celles qui ne nécessitent pas de connexion. Vous pouvez utiliser l\'étiquette "trans::" pour utiliser les traductions définies par le système. Par exemple, utiliser "trans::common.privacy_policy" fournira la traduction de "Politique de Confidentalité" et "trans::common.terms_of_service" fournira la traduction de "Conditions d\'utilisation".', 'app_footer_links_label' => 'Libellé du lien', 'app_footer_links_url' => 'URL du lien', 'app_footer_links_add' => 'Ajouter un lien en pied de page', @@ -61,17 +61,17 @@ 'page_draft_color' => 'Couleur des brouillons', // Registration Settings - 'reg_settings' => 'Préférence pour l\'inscription', + 'reg_settings' => 'Paramètres d\'inscription', 'reg_enable' => 'Activer l\'inscription', 'reg_enable_toggle' => 'Activer l\'inscription', - 'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur pourra s\'enregistrer en tant qu\'utilisateur de l\'application. Lors de l\'inscription, ils se voient attribuer un rôle par défaut.', - 'reg_default_role' => 'Rôle par défaut lors de l\'inscription', + 'reg_enable_desc' => 'Lorsque l\'inscription est activée, l\'utilisateur peut s\'inscrire lui-même en tant qu\'utilisateur de l\'application. Lors de son inscription, il se voit attribuer un rôle unique par défaut.', + 'reg_default_role' => 'Rôle de l\'utilisateur par défaut après l\'inscription', 'reg_enable_external_warning' => 'L\'option ci-dessus est ignorée lorsque l\'authentification externe LDAP ou SAML est activée. Les comptes utilisateur pour les membres non existants seront créés automatiquement si l\'authentification, par rapport au système externe utilisé, est réussie.', 'reg_email_confirmation' => 'Confirmation de l\'e-mail', 'reg_email_confirmation_toggle' => 'Obliger la confirmation par e-mail ?', 'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.', - 'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine', - 'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse.
Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.', + 'reg_confirm_restrict_domain' => 'Restriction de domaine', + 'reg_confirm_restrict_domain_desc' => 'Indiquez, séparés par des virgules, les domaines de messagerie autorisés pour l\'inscription. Les utilisateurs recevront un e-mail pour confirmer leur adresse avant de pouvoir utiliser l\'application.
Notez qu\'ils pourront modifier leur adresse e-mail après leur inscription.', 'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place', // Sorting Settings @@ -207,7 +207,7 @@ 'role_all' => 'Tous', 'role_own' => 'Propres', 'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Contrôlé par les autorisations de suppression de page', 'role_save' => 'Enregistrer le rôle', 'role_users' => 'Utilisateurs ayant ce rôle', 'role_users_none' => 'Aucun utilisateur avec ce rôle actuellement', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Configurer l\'authentification multi-facteurs ajoute une couche supplémentaire de sécurité à votre compte utilisateur.', 'users_mfa_x_methods' => ':count méthode configurée|:count méthodes configurées', 'users_mfa_configure' => 'Méthode de configuration', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Réinitialiser les méthodes d\'authentification multifacteurs', + 'users_mfa_reset_desc' => 'Cette action réinitialisera et supprimera toutes les méthodes d\'authentification multifacteurs configurées pour cet utilisateur. Si l\'authentification multifacteurs est requise par l\'un de ses rôles, il sera invité à configurer de nouvelles méthodes lors de sa prochaine connexion.', + 'users_mfa_reset_confirm' => 'Êtes-vous sûr de vouloir réinitialiser l\'authentification multifacteurs pour cet utilisateur ?', // API Tokens 'user_api_token_create' => 'Créer un nouveau jeton API', @@ -366,6 +366,7 @@ 'ru' => 'Russe', 'sk' => 'Slovaque', 'sl' => 'Slovène', + 'sr' => 'Српски', 'sv' => 'Suédois', 'th' => 'ภาษาไทย', 'tr' => 'Turc', diff --git a/lang/he/settings.php b/lang/he/settings.php index e816766813b..4c833fae659 100644 --- a/lang/he/settings.php +++ b/lang/he/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/hr/settings.php b/lang/hr/settings.php index a595b5b1605..7d2315c85fc 100644 --- a/lang/hr/settings.php +++ b/lang/hr/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/hu/settings.php b/lang/hu/settings.php index 3aaa91c35b7..0587574a8e6 100644 --- a/lang/hu/settings.php +++ b/lang/hu/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'Thai', 'tr' => 'Türkçe', diff --git a/lang/id/activities.php b/lang/id/activities.php index db32fcf644a..edb23657ec0 100644 --- a/lang/id/activities.php +++ b/lang/id/activities.php @@ -99,7 +99,7 @@ 'user_update_notification' => 'Pengguna berhasil diperbarui', 'user_delete' => 'pengguna yang dihapus', 'user_delete_notification' => 'Pengguna berhasil dihapus', - 'user_mfa_reset' => 'reset MFA for user', + 'user_mfa_reset' => 'atur ulang MFA untuk pengguna', 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', // API Tokens diff --git a/lang/id/editor.php b/lang/id/editor.php index 04999368930..5eadf80666b 100644 --- a/lang/id/editor.php +++ b/lang/id/editor.php @@ -8,8 +8,8 @@ return [ // General editor terms 'general' => 'Umum', - 'advanced' => 'Lanjutan', - 'none' => 'Tidak Ada', + 'advanced' => 'Tingkat lanjut', + 'none' => 'Tidak Satupun', 'cancel' => 'Batal', 'save' => 'Simpan', 'close' => 'Tutup', diff --git a/lang/id/settings.php b/lang/id/settings.php index 5d785314701..73fa631ee09 100644 --- a/lang/id/settings.php +++ b/lang/id/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/is/settings.php b/lang/is/settings.php index cabe31917ef..27400e812c3 100644 --- a/lang/is/settings.php +++ b/lang/is/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/it/entities.php b/lang/it/entities.php index 89e1cd98831..fefb9d34b08 100644 --- a/lang/it/entities.php +++ b/lang/it/entities.php @@ -173,7 +173,7 @@ 'books_sort_desc' => 'Spostare i capitoli e le pagine di un libro per riorganizzarne il contenuto. Possono essere aggiunti altri libri che permettono di spostare facilmente capitoli e pagine tra i libri. Opzionalmente una regola di ordinamento automatico può essere impostata per ordinare automaticamente i contenuti di questo libro in caso di modifiche.', 'books_sort_auto_sort' => 'Opzione Ordinamento Automatico', 'books_sort_auto_sort_active' => 'Ordinamento Automatico Attivo: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'Le regole delle opzioni di ordinamento automatico possono essere create nell\'area delle impostazioni "Elenchi e ordinamento" da un utente con le relative autorizzazioni.', 'books_sort_named' => 'Ordina il libro :bookName', 'books_sort_name' => 'Ordina per Nome', 'books_sort_created' => 'Ordina per Data di creazione', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Attiva/disattiva barra laterale', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Contenuto della pagina', + 'page_contents_none' => 'Nessun titolo trovato nel contenuto della pagina.', + 'page_contents_info' => 'Il sommario viene generato sulla base dei formati di intestazione utilizzati nella pagina.', 'page_tags' => 'Tag pagina', 'chapter_tags' => 'Tag capitolo', 'book_tags' => 'Tag libro', diff --git a/lang/it/settings.php b/lang/it/settings.php index 3b8681840f2..2686440b8bf 100644 --- a/lang/it/settings.php +++ b/lang/it/settings.php @@ -366,8 +366,9 @@ 'ru' => 'Russo', 'sk' => 'Sloveno', 'sl' => 'Sloveno', + 'sr' => 'Српски', 'sv' => 'Svedese', - 'th' => 'ภาษาไทย', + 'th' => 'Thailandese', 'tr' => 'Turco', 'uk' => 'Ucraino', 'uz' => 'O‘zbekcha', diff --git a/lang/ja/activities.php b/lang/ja/activities.php index 25ded1d49b7..93a3597f83a 100644 --- a/lang/ja/activities.php +++ b/lang/ja/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'ユーザーを更新しました', 'user_delete' => 'がユーザを削除', 'user_delete_notification' => 'ユーザーを削除しました', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'ユーザーの多要素認証をリセット', + 'user_mfa_reset_notification' => '多要素認証方法をリセット', // API Tokens 'api_token_create' => 'がAPIトークンを作成', diff --git a/lang/ja/auth.php b/lang/ja/auth.php index b4ee5f4acf9..6a68de99c1a 100644 --- a/lang/ja/auth.php +++ b/lang/ja/auth.php @@ -8,7 +8,7 @@ 'failed' => 'この資格情報は登録されていません。', 'throttle' => 'ログイン試行回数が制限を超えました。:seconds秒後に再試行してください。', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => '多要素認証の試行回数が制限を越えました。:seconds 秒後に再試行してください。', // Login & Register 'sign_up' => '新規登録', diff --git a/lang/ja/entities.php b/lang/ja/entities.php index 66019fa1aca..164004451b5 100644 --- a/lang/ja/entities.php +++ b/lang/ja/entities.php @@ -332,9 +332,9 @@ // Editor Sidebar 'toggle_sidebar' => 'サイドバーの切り替え', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'ページの内容', + 'page_contents_none' => 'ページ内に見出しが見つかりませんでした。', + 'page_contents_info' => '目次メニューは、ページ内で使用されている見出し書式から生成されます。', 'page_tags' => 'タグ', 'chapter_tags' => 'チャプターのタグ', 'book_tags' => 'ブックのタグ', diff --git a/lang/ja/settings.php b/lang/ja/settings.php index bca5ed0049f..023ce2780db 100644 --- a/lang/ja/settings.php +++ b/lang/ja/settings.php @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'アカウントのセキュリティを強化するために、多要素認証を設定してください。', 'users_mfa_x_methods' => ':count個の手段が設定されています|:count個の手段が設定されています', 'users_mfa_configure' => '手段を設定', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => '多要素認証方法をリセット', + 'users_mfa_reset_desc' => 'このユーザーに設定されたすべての多要素認証方法をリセットしてクリアします。 いずれかのロールで多要素認証が必須になっている場合、次回のログイン時に新しい認証方法を設定するよう求められます。', + 'users_mfa_reset_confirm' => 'このユーザーの多要素認証をリセットしてもよろしいですか?', // API Tokens 'user_api_token_create' => 'APIトークンの作成', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ka/settings.php b/lang/ka/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/ka/settings.php +++ b/lang/ka/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ko/settings.php b/lang/ko/settings.php index 90d501a7cb0..4e6ec78b406 100644 --- a/lang/ko/settings.php +++ b/lang/ko/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ku/settings.php b/lang/ku/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/ku/settings.php +++ b/lang/ku/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/lt/settings.php b/lang/lt/settings.php index 96ee2bebada..ef8053a55f3 100644 --- a/lang/lt/settings.php +++ b/lang/lt/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/lv/settings.php b/lang/lv/settings.php index 886e0ef0a0c..16c0f30ff1e 100644 --- a/lang/lv/settings.php +++ b/lang/lv/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/nb/settings.php b/lang/nb/settings.php index 1cc5d8e02ca..d859f98f4a6 100644 --- a/lang/nb/settings.php +++ b/lang/nb/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ne/settings.php b/lang/ne/settings.php index 549a4dc8b73..3a1b5b98d53 100644 --- a/lang/ne/settings.php +++ b/lang/ne/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/nl/activities.php b/lang/nl/activities.php index 55356966ddc..23519f9c9d7 100644 --- a/lang/nl/activities.php +++ b/lang/nl/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Gebruiker succesvol bijgewerkt', 'user_delete' => 'verwijderde gebruiker', 'user_delete_notification' => 'Gebruiker succesvol verwijderd', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'herstel meervoudige verificatie voor gebruiker', + 'user_mfa_reset_notification' => 'Meervoudige verificatie methodes hersteld', // API Tokens 'api_token_create' => 'API-token aangemaakt', diff --git a/lang/nl/auth.php b/lang/nl/auth.php index 49d04dd4fbf..3f67f8a6233 100644 --- a/lang/nl/auth.php +++ b/lang/nl/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Deze inloggegevens zijn niet bij ons bekend.', 'throttle' => 'Te veel inlogpogingen! Probeer het opnieuw na :seconds seconden.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Te veel pogingen om te verifiëren met meervoudige verificatie. Probeer het opnieuw na :seconds seconden.', // Login & Register 'sign_up' => 'Registreer', diff --git a/lang/nl/entities.php b/lang/nl/entities.php index 3695277a71e..6f5f7a37b7a 100644 --- a/lang/nl/entities.php +++ b/lang/nl/entities.php @@ -173,7 +173,7 @@ 'books_sort_desc' => 'Verplaats hoofdstukken en pagina\'s door het boek om ze te organiseren. Andere boeken kunnen worden toegevoegd zodat hoofdstukken en pagina\'s gemakkelijk tussen boeken kunnen worden verplaatst. Het is mogelijk om een automatische sorteerregel in te stellen die de inhoud zal sorteren bij wijzigingen.', 'books_sort_auto_sort' => 'Automatisch Sorteren', 'books_sort_auto_sort_active' => 'Automatisch Sorteren Actief: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'Regels voor automatisch sorteren kunnen worden aangemaakt door een bevoegde gebruiker in het gedeelte "Lijsten & Sorteren" van de instellingen.', 'books_sort_named' => 'Sorteer boek :bookName', 'books_sort_name' => 'Sorteren op naam', 'books_sort_created' => 'Sorteren op datum van aanmaken', @@ -332,8 +332,8 @@ // Editor Sidebar 'toggle_sidebar' => 'Zijbalk Tonen/Verbergen', 'page_contents' => 'Pagina Inhoud', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents_none' => 'Geen koppen gevonden binnen de inhoud van deze pagina.', + 'page_contents_info' => 'Het inhoudsmenu wordt gemaakt van alle koppen op een pagina.', 'page_tags' => 'Pagina Labels', 'chapter_tags' => 'Hoofdstuk Labels', 'book_tags' => 'Boek Labels', diff --git a/lang/nl/errors.php b/lang/nl/errors.php index 0e478fd5ac6..8dc197efe3e 100644 --- a/lang/nl/errors.php +++ b/lang/nl/errors.php @@ -125,7 +125,7 @@ 'api_incorrect_token_secret' => 'Het opgegeven geheim voor de API-token is onjuist', 'api_user_no_api_permission' => 'De eigenaar van de gebruikte API-token heeft geen machtiging om API calls te maken', 'api_user_token_expired' => 'De gebruikte autorisatie token is verlopen', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => 'Alleen GET verzoeken zijn toegestaan wanneer de API wordt gebruikt met cookie-gebaseerde authenticatie', // Settings & Maintenance 'maintenance_test_email_failure' => 'Fout opgetreden bij het verzenden van een test email:', diff --git a/lang/nl/settings.php b/lang/nl/settings.php index 9b0fe5ad154..0d0ddfa674a 100644 --- a/lang/nl/settings.php +++ b/lang/nl/settings.php @@ -104,7 +104,7 @@ 'sort_rule_op_chapters_first' => 'Hoofdstukken Eerst', 'sort_rule_op_chapters_last' => 'Hoofdstukken Laatst', 'sorting_page_limits' => 'Weergavelimiet Per Pagina', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting_page_limits_desc' => 'Stel in hoeveel items er op een pagina worden laten zien in de verschillende lijstweergaves. Een lager aantal verbeterd de snelheid, een hoger aantal verminderd het doorklikken door pagina\'s. Het wordt aanbevolen om een meervoud van 6 te gebruiken.', // Maintenance settings 'maint' => 'Onderhoud', @@ -207,7 +207,7 @@ 'role_all' => 'Alles', 'role_own' => 'Eigen', 'role_controlled_by_asset' => 'Gecontroleerd door de asset waar deze is geüpload', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Ingesteld volgens pagina verwijder machtigingen', 'role_save' => 'Rol Opslaan', 'role_users' => 'Gebruikers in deze rol', 'role_users_none' => 'Geen enkele gebruiker heeft deze rol', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Stel meervoudige verificatie in als extra beveiligingslaag voor je gebruikersaccount.', 'users_mfa_x_methods' => ':count methode geconfigureerd|:count methoden geconfigureerd', 'users_mfa_configure' => 'Configureer methoden', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Herstel Meervoudige Verificatie Methodes', + 'users_mfa_reset_desc' => 'Dit zal alle methodes voor meervoudige verificatie van deze gebruiker wissen. Als meervoudige verificatie vereist is vanwege een van hun rollen, worden ze bij hun volgende inlogpoging gevraagd om nieuwe methodes te configureren.', + 'users_mfa_reset_confirm' => 'Weet je zeker dat je de meervoudige verificatie van deze gebruiker wilt herstellen?', // API Tokens 'user_api_token_create' => 'API-token aanmaken', @@ -366,6 +366,7 @@ 'ru' => 'Русский (Russisch)', 'sk' => 'Slovensky (Slowaaks)', 'sl' => 'Slovenščina (Sloveens)', + 'sr' => 'Српски', 'sv' => 'Svenska (Zweeds)', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe (Turks)', diff --git a/lang/nn/settings.php b/lang/nn/settings.php index 08709833273..74f4c5ef330 100644 --- a/lang/nn/settings.php +++ b/lang/nn/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/pl/activities.php b/lang/pl/activities.php index 00687187d43..45920dcde34 100644 --- a/lang/pl/activities.php +++ b/lang/pl/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Użytkownik zaktualizowany pomyślnie', 'user_delete' => 'usunięto użytkownika', 'user_delete_notification' => 'Użytkownik pomyślnie usunięty', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'zresetuj MFA dla użytkownika', + 'user_mfa_reset_notification' => 'Przywracanie metod uwierzytelniania wieloskładnikowego', // API Tokens 'api_token_create' => 'utworzono token API', diff --git a/lang/pl/auth.php b/lang/pl/auth.php index 7b4997a6cb0..62b684bd56c 100644 --- a/lang/pl/auth.php +++ b/lang/pl/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Wprowadzone poświadczenia są nieprawidłowe.', 'throttle' => 'Zbyt wiele prób logowania. Spróbuj ponownie za :seconds s.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Zbyt wiele prób weryfikacji wieloskładnikowej. Spróbuj ponownie za :seconds sekund.', // Login & Register 'sign_up' => 'Zarejestruj się', diff --git a/lang/pl/entities.php b/lang/pl/entities.php index ea0ac73670e..6cc7966e28f 100644 --- a/lang/pl/entities.php +++ b/lang/pl/entities.php @@ -170,10 +170,10 @@ 'books_search_this' => 'Wyszukaj w tej książce', 'books_navigation' => 'Nawigacja po książce', 'books_sort' => 'Sortuj zawartość książki', - 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', + 'books_sort_desc' => 'Przenieś rozdziały i strony w książce w celu reorganizacji jej treści. Można dodać inne książki, które umożliwiają łatwe przenoszenie rozdziałów i stron między książkami. Opcjonalnie reguła automatycznego sortowania może być ustawiona, aby automatycznie sortować zawartość tej książki po jej zmianach.', 'books_sort_auto_sort' => 'Opcja automatycznego sortowania', 'books_sort_auto_sort_active' => 'Automatyczne sortowanie aktywne: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'Reguły opcji automatycznego sortowania mogą być tworzone w obszarze ustawień "Listy i sortowanie" przez użytkownika z odpowiednimi uprawnieniami.', 'books_sort_named' => 'Sortuj książkę :bookName', 'books_sort_name' => 'Sortuj według nazwy', 'books_sort_created' => 'Sortuj według daty utworzenia', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Przełącz pasek boczny', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Zawartość strony', + 'page_contents_none' => 'Nie znaleziono nagłówków w treści strony.', + 'page_contents_info' => 'Menu zawartości jest generowane z dowolnych formatów nagłówków używanych na stronie.', 'page_tags' => 'Tagi strony', 'chapter_tags' => 'Tagi rozdziału', 'book_tags' => 'Tagi książki', diff --git a/lang/pl/errors.php b/lang/pl/errors.php index 244913cac42..3cba1bfb24e 100644 --- a/lang/pl/errors.php +++ b/lang/pl/errors.php @@ -125,7 +125,7 @@ 'api_incorrect_token_secret' => 'Podany sekret dla tego API jest nieprawidłowy', 'api_user_no_api_permission' => 'Właściciel używanego tokenu API nie ma uprawnień do wykonywania zapytań do API', 'api_user_token_expired' => 'Token uwierzytelniania wygasł', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => 'Tylko żądania GET są dozwolone podczas korzystania z API z uwierzytelniania opartego na plikach cookie', // Settings & Maintenance 'maintenance_test_email_failure' => 'Błąd podczas wysyłania testowej wiadomości e-mail:', diff --git a/lang/pl/settings.php b/lang/pl/settings.php index bfa6339d551..3658ba8fdcb 100644 --- a/lang/pl/settings.php +++ b/lang/pl/settings.php @@ -104,7 +104,7 @@ 'sort_rule_op_chapters_first' => 'Rozdziały na początku', 'sort_rule_op_chapters_last' => 'Rozdziały na końcu', 'sorting_page_limits' => 'Limity wyświetlania per strona', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting_page_limits_desc' => 'Ustaw ile elementów pokazywać na stronie w różnych listach w systemie. Zazwyczaj mniejsza ilość będzie bardziej wydajna, podczas gdy większa ilość unika konieczności kliknięcia na wiele stron. Zaleca się stosowanie wielokrotności 6 razy.', // Maintenance settings 'maint' => 'Konserwacja', @@ -207,7 +207,7 @@ 'role_all' => 'Wszyscy', 'role_own' => 'Własne', 'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Kontrolowane przez uprawnienia do usuwania stron', 'role_save' => 'Zapisz rolę', 'role_users' => 'Użytkownicy w tej roli', 'role_users_none' => 'Brak użytkowników zapisanych do tej roli', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Skonfiguruj uwierzytelnianie wieloskładnikowe jako dodatkową warstwę bezpieczeństwa dla swojego konta użytkownika.', 'users_mfa_x_methods' => ':count metoda skonfigurowana|:count metody skonfigurowane', 'users_mfa_configure' => 'Konfiguruj metody', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Resetuj metody uwierzytelniania wieloetapowego', + 'users_mfa_reset_desc' => 'Spowoduje to zresetowanie i wyczyszczenie wszystkich skonfigurowanych metod uwierzytelniania wieloetapowego dla tego użytkownika. Jeśli uwierzytelnianie wieloetapowe jest wymagane przez dowolną z ich ról, zostaną poproszone o skonfigurowanie nowych metod przy następnym logowaniu.', + 'users_mfa_reset_confirm' => 'Czy na pewno chcesz zresetować uwierzytelnianie wieloskładnikowe dla tego użytkownika?', // API Tokens 'user_api_token_create' => 'Utwórz klucz API', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/pt/activities.php b/lang/pt/activities.php index e2ed42c7111..b7ca3d5b474 100644 --- a/lang/pt/activities.php +++ b/lang/pt/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Utilizador atualizado com sucesso', 'user_delete' => 'utilizador eliminado', 'user_delete_notification' => 'Utilizador removido com sucesso', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'reiniciar a MFA para o utilizador', + 'user_mfa_reset_notification' => 'Reinicialização dos métodos de autenticação multifatorial', // API Tokens 'api_token_create' => 'token API criado', diff --git a/lang/pt/auth.php b/lang/pt/auth.php index 453b201689d..cf662b42af8 100644 --- a/lang/pt/auth.php +++ b/lang/pt/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Estas credenciais não coincidem com os nossos registos.', 'throttle' => 'Demasiadas tentativas de acesso. Tente novamente em :seconds segundos.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Foram efetuadas demasiadas tentativas de verificação multifatorial. Por favor, tente novamente daqui a :seconds segundos.', // Login & Register 'sign_up' => 'Registar', diff --git a/lang/pt/editor.php b/lang/pt/editor.php index e3069909c67..2a5a0f5c6e3 100644 --- a/lang/pt/editor.php +++ b/lang/pt/editor.php @@ -48,7 +48,7 @@ 'superscript' => 'Superior à linha', 'subscript' => 'Inferior à linha', 'text_color' => 'Cor do texto', - 'highlight_color' => 'Highlight color', + 'highlight_color' => 'Cor de destaque', 'custom_color' => 'Cor personalizada', 'remove_color' => 'Remover cor', 'background_color' => 'Cor de fundo', @@ -83,9 +83,9 @@ 'table_properties' => 'Propriedades da tabela', 'table_properties_title' => 'Propriedades da Tabela', 'delete_table' => 'Eliminar tabela', - 'table_clear_formatting' => 'Clear table formatting', - 'resize_to_contents' => 'Resize to contents', - 'row_header' => 'Row header', + 'table_clear_formatting' => 'Limpar formatação de tabela', + 'resize_to_contents' => 'Redimensionar para o conteúdo', + 'row_header' => 'Cabeçalho da linha', 'insert_row_before' => 'Inserir linha antes', 'insert_row_after' => 'Inserir linha depois', 'delete_row' => 'Eliminar linha', @@ -149,7 +149,7 @@ 'url' => 'URL', 'text_to_display' => 'Texto a ser exibido', 'title' => 'Título', - 'browse_links' => 'Browse links', + 'browse_links' => 'Procurar ligações', 'open_link' => 'Abrir ligação', 'open_link_in' => 'Abrir ligação em...', 'open_link_current' => 'Janela atual', @@ -167,7 +167,7 @@ 'about_title' => 'Sobre o Editor WYSIWYG', 'editor_license' => 'Editor da licença de direitos autorais', 'editor_lexical_license' => 'Este editor é criado como um fork do :lexicaLink que é distribuído sob a licença MIT.', - 'editor_lexical_license_link' => 'Full license details can be found here.', + 'editor_lexical_license_link' => 'Detalhes da licença completa podem ser encontrados aqui.', 'editor_tiny_license' => 'Este editor foi criado com :tinyLink que é fornecido sob a licença MIT.', 'editor_tiny_license_link' => 'Os dados relativos aos direitos de autor e à licença do TinyMCE podem ser encontrados aqui.', 'save_continue' => 'Salvar página e continuar', diff --git a/lang/pt/entities.php b/lang/pt/entities.php index c278796a43f..5038eebcc7a 100644 --- a/lang/pt/entities.php +++ b/lang/pt/entities.php @@ -46,27 +46,27 @@ 'import' => 'Importar', 'import_validate' => 'Validar Importação', 'import_desc' => 'Importar livros, capítulos e páginas usando uma exportação ZIP portátil da mesma ou uma instância diferente. Selecione um arquivo ZIP para prosseguir. Após o carregamento e validação do arquivo, conseguirá configurar e confirmar a importação na próxima visualização.', - 'import_zip_select' => 'Select ZIP file to upload', - 'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:', - 'import_pending' => 'Pending Imports', - 'import_pending_none' => 'No imports have been started.', + 'import_zip_select' => 'Selecione o ficheiro ZIP para enviar', + 'import_zip_validation_errors' => 'Foram detetados erros ao validar o ficheiro ZIP fornecido:', + 'import_pending' => 'Aguardando importação', + 'import_pending_none' => 'Nenhuma importação foi iniciada.', 'import_continue' => 'Continuar importação', - 'import_continue_desc' => 'Continuar importação', - 'import_details' => 'Import Details', - 'import_run' => 'Run Import', - 'import_size' => ':size Import ZIP Size', - 'import_uploaded_at' => 'Uploaded :relativeTime', - 'import_uploaded_by' => 'Uploaded by', - 'import_location' => 'Import Location', - 'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.', - 'import_delete_confirm' => 'Are you sure you want to delete this import?', + 'import_continue_desc' => 'Verifique o conteúdo a importar a partir do ficheiro ZIP carregado. Quando estiver pronto, execute a importação para adicionar o seu conteúdo a este sistema. O ficheiro ZIP de importação carregado será automaticamente removido após a importação bem-sucedida.', + 'import_details' => 'Detalhes da importação', + 'import_run' => 'Executar Importação', + 'import_size' => ':size Tamanho do ZIP importado', + 'import_uploaded_at' => 'Carregado :relativeTime', + 'import_uploaded_by' => 'Carregado por', + 'import_location' => 'Local de Importação', + 'import_location_desc' => 'Selecione um local de destino para o seu conteúdo importado. Terá de dispor das permissões necessárias para criar conteúdo no local que escolher.', + 'import_delete_confirm' => 'Tem a certeza que pretende eliminar a importação?', 'import_delete_desc' => 'Isto irá eliminar o arquivo ZIP de importação enviado e não pode ser desfeito.', - 'import_errors' => 'Import Errors', - 'import_errors_desc' => 'The follow errors occurred during the import attempt:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'import_errors' => 'Erros de Importação', + 'import_errors_desc' => 'Ocorreram os seguintes erros durante a tentativa de importação:', + 'breadcrumb_siblings_for_page' => 'Navegar itens do mesmo nível por página', + 'breadcrumb_siblings_for_chapter' => 'Navegar itens do mesmo nível por capítulo', + 'breadcrumb_siblings_for_book' => 'Navegar itens do mesmo nível por livro', + 'breadcrumb_siblings_for_bookshelf' => 'Navegar itens do mesmo nível por estante', // Permissions and restrictions 'permissions' => 'Permissões', @@ -172,8 +172,8 @@ 'books_sort' => 'Ordenar Conteúdos do Livro', 'books_sort_desc' => 'Mova capítulos e páginas de um livro para reorganizar o seu conteúdo. É possível acrescentar outros livros, o que permite uma movimentação fácil de capítulos e páginas entre livros. Opcionalmente, uma regra de organização automática pode ser definida para classificar automaticamente o conteúdo deste livro após alterações.', 'books_sort_auto_sort' => '', - 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_active' => 'Ordenação automática ativada: :sortName', + 'books_sort_auto_sort_creation_hint' => 'As regras da opção de ordenação automática podem ser criadas na área de configurações "Listas e Ordenação" por um utilizador com as permissões necessárias.', 'books_sort_named' => 'Ordenar Livro :bookName', 'books_sort_name' => 'Ordenar por Nome', 'books_sort_created' => 'Ordenar por Data de Criação', @@ -235,7 +235,7 @@ 'pages_delete_draft' => 'Eliminar Rascunho de Página', 'pages_delete_success' => 'Página eliminada', 'pages_delete_draft_success' => 'Rascunho de página eliminado', - 'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.', + 'pages_delete_warning_template' => 'Esta página é atualmente utilizada como modelo de página predefinido para livros ou capítulos. Após a eliminação desta página, estes livros ou capítulos deixarão de ter um modelo de página predefinido atribuído.', 'pages_delete_confirm' => 'Tem certeza que deseja eliminar a página?', 'pages_delete_draft_confirm' => 'Tem certeza que deseja eliminar o rascunho de página?', 'pages_editing_named' => 'A Editar a Página :pageName', @@ -252,8 +252,8 @@ 'pages_edit_switch_to_markdown_clean' => '(Conteúdo Limitado)', 'pages_edit_switch_to_markdown_stable' => '(Conteúdo Estável)', 'pages_edit_switch_to_wysiwyg' => 'Alternar para o editor WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', + 'pages_edit_switch_to_new_wysiwyg' => 'Mudar para o novo WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(Em fase de testes beta)', 'pages_edit_set_changelog' => 'Relatar Alterações', 'pages_edit_enter_changelog_desc' => 'Digite uma breve descrição das alterações efetuadas por si', 'pages_edit_enter_changelog' => 'Inserir Alterações', @@ -273,7 +273,7 @@ 'pages_md_insert_drawing' => 'Inserir Desenho', 'pages_md_show_preview' => 'Mostrar pré-visualização', 'pages_md_sync_scroll' => 'Sincronizar pré-visualização', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_plain_editor' => 'Editor de texto simples', 'pages_drawing_unsaved' => 'Encontrado um rascunho não guardado', 'pages_drawing_unsaved_confirm' => 'Dados de um rascunho não guardado foi encontrado de um tentativa anteriormente falhada. Deseja restaurar e continuar a edição desse rascunho?', 'pages_not_in_chapter' => 'A página não está dentro de um capítulo', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Alternar barra lateral', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Conteúdo da Página', + 'page_contents_none' => 'Não foram encontrados títulos no conteúdo da página.', + 'page_contents_info' => 'O índice é gerado a partir dos formatos de título utilizados na página.', 'page_tags' => 'Etiquetas de Página', 'chapter_tags' => 'Etiquetas do Capítulo', 'book_tags' => 'Etiquetas do Livro', @@ -401,11 +401,11 @@ 'comment' => 'Comentário', 'comments' => 'Comentários', 'comment_add' => 'Adicionar Comentário', - 'comment_none' => 'No comments to display', + 'comment_none' => 'Não há comentários para apresentar', 'comment_placeholder' => 'Digite aqui os seus comentários', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', + 'comment_thread_count' => ':count Tópico de comentários|:count Tópicos de comentários', + 'comment_archived_count' => ':count Arquivado', + 'comment_archived_threads' => 'Tópicos Arquivados', 'comment_save' => 'Guardar comentário', 'comment_new' => 'Comentário Novo', 'comment_created' => 'comentado :createDiff', @@ -414,14 +414,14 @@ 'comment_deleted_success' => 'Comentário removido', 'comment_created_success' => 'Comentário adicionado', 'comment_updated_success' => 'Comentário editado', - 'comment_archive_success' => 'Comment archived', - 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', + 'comment_archive_success' => 'Comentário arquivado', + 'comment_unarchive_success' => 'Comentário não arquivado', + 'comment_view' => 'Ver comentário', + 'comment_jump_to_thread' => 'Ir para o tópico', 'comment_delete_confirm' => 'Tem a certeza de que deseja eliminar este comentário?', 'comment_in_reply_to' => 'Em resposta à :commentId', - 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', + 'comment_reference' => 'Referência', + 'comment_reference_outdated' => '(Desatualizado)', '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 @@ -452,7 +452,7 @@ // References 'references' => 'Referências', 'references_none' => 'Não há referências registadas para este item.', - 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.', + 'references_to_desc' => 'A seguir, encontra-se uma lista de todo o conteúdo conhecido no sistema associado a este item.', // Watch Options 'watch' => 'Ver', @@ -470,11 +470,11 @@ 'watch_desc_comments_page' => 'Notificar sobre alterações na página e novos comentários.', 'watch_change_default' => 'Alterar preferências padrão de notificação', 'watch_detail_ignore' => 'Ignorar notificações', - '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_new' => 'A observar novas páginas', + 'watch_detail_updates' => 'A observar novas páginas e atualizações', + 'watch_detail_comments' => 'A observar novas páginas, atualizações e comentários', + 'watch_detail_parent_book' => 'A observar via livro pai', 'watch_detail_parent_book_ignore' => 'A ignorar através do livro pai', - 'watch_detail_parent_chapter' => 'Watching via parent chapter', - 'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter', + 'watch_detail_parent_chapter' => 'A observar via capítulo pai', + 'watch_detail_parent_chapter_ignore' => 'A ignorar via capítulo pai', ]; diff --git a/lang/pt/errors.php b/lang/pt/errors.php index e32257e6240..d6e92290cd4 100644 --- a/lang/pt/errors.php +++ b/lang/pt/errors.php @@ -10,7 +10,7 @@ // Auth 'error_user_exists_different_creds' => 'Um utilizador com o endereço de e-mail :email já existe mas com credenciais diferentes.', - 'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details', + 'auth_pre_register_theme_prevention' => 'Não foi possível registar a conta de utilizador com os detalhes fornecidos', 'email_already_confirmed' => 'E-mail já foi confirmado. Tente iniciar sessão.', 'email_confirmation_invalid' => 'Este token de confirmação não é válido ou já foi utilizado. Por favor, tente registar-se novamente.', 'email_confirmation_expired' => 'O token de confirmação já expirou. Um novo e-mail foi enviado.', @@ -37,7 +37,7 @@ 'social_driver_not_found' => 'Social driver não encontrado', 'social_driver_not_configured' => 'Os seus parâmetros sociais de :socialAccount não estão corretamente configurados.', 'invite_token_expired' => 'Este link de convite expirou. Alternativamente, pode tentar redefinir a senha da sua conta.', - 'login_user_not_found' => 'A user for this action could not be found.', + 'login_user_not_found' => 'Não foi possível encontrar um utilizador para esta ação.', // System 'path_not_writable' => 'O caminho do arquivo :filePath não pôde ser carregado. Certifique-se de que tem permissões de escrita no servidor.', @@ -51,9 +51,9 @@ 'image_upload_error' => 'Ocorreu um erro no carregamento da imagem', 'image_upload_type_error' => 'O tipo de imagem enviada é inválida', 'image_upload_replace_type' => 'A imagem de substituição deverá ser do mesmo tipo que a anterior', - 'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.', - 'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.', - 'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.', + 'image_upload_memory_limit' => 'Não foi possível processar o carregamento da imagem e/ou criar miniaturas devido a limites de recursos do sistema.', + 'image_thumbnail_memory_limit' => 'Não foi possível criar variações de tamanho de imagem devido a limites de recursos do sistema.', + 'image_gallery_thumbnail_memory_limit' => 'Não foi possível criar miniaturas da galeria devido a limites de recursos do sistema.', 'drawing_data_not_found' => 'Dados de desenho não puderam ser carregados. Talvez o arquivo de desenho não exista mais ou não tenha permissão para aceder-lhe.', // Attachments @@ -107,16 +107,16 @@ // Import 'import_zip_cant_read' => 'Não foi possível ler o ficheiro ZIP.', - 'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.', - 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', - 'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.', - 'import_validation_failed' => 'Import ZIP failed to validate with errors:', - 'import_zip_failed_notification' => 'Failed to import ZIP file.', - 'import_perms_books' => 'You are lacking the required permissions to create books.', - 'import_perms_chapters' => 'You are lacking the required permissions to create chapters.', - 'import_perms_pages' => 'You are lacking the required permissions to create pages.', - 'import_perms_images' => 'You are lacking the required permissions to create images.', - 'import_perms_attachments' => 'You are lacking the required permission to create attachments.', + 'import_zip_cant_decode_data' => 'Não foi possível encontrar nem descodificar o conteúdo do ficheiro ZIP data.json.', + 'import_zip_no_data' => 'Os dados do ficheiro ZIP não contêm o conteúdo esperado de livro, capítulo ou página.', + 'import_zip_data_too_large' => 'O conteúdo do ficheiro ZIP data.json excede o tamanho máximo de upload definido para a aplicação.', + 'import_validation_failed' => 'A importação do ficheiro ZIP não foi validada devido a erros:', + 'import_zip_failed_notification' => 'Não foi possível importar o ficheiro ZIP.', + 'import_perms_books' => 'Não dispõe das permissões necessárias para criar livros.', + 'import_perms_chapters' => 'Não dispõe das permissões necessárias para criar capítulos.', + 'import_perms_pages' => 'Não dispõe das permissões necessárias para criar páginas.', + 'import_perms_images' => 'Não dispõe das permissões necessárias para criar imagens.', + 'import_perms_attachments' => 'Não dispõe das permissões necessárias para criar anexos.', // API errors 'api_no_authorization_found' => 'Nenhum token de autorização encontrado na requisição', @@ -125,11 +125,11 @@ 'api_incorrect_token_secret' => 'O segredo fornecido para o token de API usado está incorreto', 'api_user_no_api_permission' => 'O proprietário do token de API utilizado não tem permissão para fazer requisições de API', 'api_user_token_expired' => 'O token de autenticação expirou', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => 'Ao utilizar a API com autenticação baseada em “cookies”, apenas são permitidos pedidos GET', // Settings & Maintenance 'maintenance_test_email_failure' => 'Erro lançado ao enviar um e-mail de teste:', // HTTP errors - 'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts', + 'http_ssr_url_no_match' => 'O URL não corresponde aos "hosts" SSR permitidos configurados', ]; diff --git a/lang/pt/notifications.php b/lang/pt/notifications.php index cbe3a511c88..14beff712eb 100644 --- a/lang/pt/notifications.php +++ b/lang/pt/notifications.php @@ -11,11 +11,11 @@ 'updated_page_subject' => 'Página atualizada: :pageName', 'updated_page_intro' => 'Uma página foi atualizada em :appName:', 'updated_page_debounce' => 'Para evitar um grande volume de notificações, durante algum tempo não serão enviadas notificações de edições futuras para esta página através do mesmo editor.', - 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'comment_mention_subject' => 'Foi mencionado num comentário na página: :pageName', + 'comment_mention_intro' => 'Foi mencionado num comentário no :appName:', 'detail_page_name' => 'Nome da Página:', - 'detail_page_path' => 'Page Path:', + 'detail_page_path' => 'Caminho da página:', 'detail_commenter' => 'Comentador:', 'detail_comment' => 'Comentário:', 'detail_created_by' => 'Criado Por:', diff --git a/lang/pt/preferences.php b/lang/pt/preferences.php index b7308aaf910..b98cfa3691a 100644 --- a/lang/pt/preferences.php +++ b/lang/pt/preferences.php @@ -23,7 +23,7 @@ 'notifications_desc' => 'Controlar as notificações via correio eletrónico quando certas atividades são executadas pelo sistema.', 'notifications_opt_own_page_changes' => 'Notificar quando páginas que possuo sofrem alterações', 'notifications_opt_own_page_comments' => 'Notificar quando comentam páginas que possuo', - 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', + 'notifications_opt_comment_mentions' => 'Notificar-me quando for mencionado num comentário', 'notifications_opt_comment_replies' => 'Notificar respostas aos meus comentários', 'notifications_save' => 'Guardar preferências', 'notifications_update_success' => 'Preferências de notificação foram atualizadas!', @@ -43,7 +43,7 @@ 'profile_email_no_permission' => 'Infelizmente você não tem permissão para alterar seu correio eletrônico. Se você quiser mudar isso, você precisa pedir a um administrador para alterar por você.', 'profile_avatar_desc' => 'Selecione uma imagem que será usada para lhe representar aos outros usuários do sistema. Idealmente, esta imagem deve ser quadrada e sobre 256px em largura e altura.', 'profile_admin_options' => 'Opções de administrador', - 'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the "Settings > Users" area of the application.', + 'profile_admin_options_desc' => 'Poderá encontrar opções adicionais de nível de administrador, como as destinadas a gerir a atribuição de funções, na sua conta de utilizador, na secção "Definições > Utilizadores" da aplicação.', 'delete_account' => 'Excluir Conta', 'delete_my_account' => 'Excluir a Minha Conta', diff --git a/lang/pt/settings.php b/lang/pt/settings.php index 1488db87a73..7ec587d51e1 100644 --- a/lang/pt/settings.php +++ b/lang/pt/settings.php @@ -75,36 +75,36 @@ 'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição definida', // Sorting Settings - 'sorting' => 'Lists & Sorting', - 'sorting_book_default' => 'Default Book Sort Rule', - 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', - 'sorting_rules' => 'Sort Rules', - 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', - 'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books', - 'sort_rule_create' => 'Create Sort Rule', - 'sort_rule_edit' => 'Edit Sort Rule', - 'sort_rule_delete' => 'Delete Sort Rule', - 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', - 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?', - 'sort_rule_details' => 'Sort Rule Details', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', - 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', - 'sort_rule_available_operations' => 'Available Operations', - 'sort_rule_available_operations_empty' => 'No operations remaining', - 'sort_rule_configured_operations' => 'Configured Operations', - 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', + 'sorting' => 'Listas e ordenação', + 'sorting_book_default' => 'Regra de ordenação padrão dos livros', + 'sorting_book_default_desc' => 'Selecione a regra de ordenação predefinida a aplicar aos novos livros. Isto não afetará os livros existentes e pode ser substituído individualmente para cada livro.', + 'sorting_rules' => 'Regras de Ordenação', + 'sorting_rules_desc' => 'Trata-se de operações de ordenação predefinidas que podem ser aplicadas ao conteúdo do sistema.', + 'sort_rule_assigned_to_x_books' => 'Atribuído a: :count Livro|Atribuído a: :count Livros', + 'sort_rule_create' => 'Criar Regra de Ordenação', + 'sort_rule_edit' => 'Editar Regra de Ordenação', + 'sort_rule_delete' => 'Eliminar Regra de Ordenação', + 'sort_rule_delete_desc' => 'Remova esta regra de ordenação do sistema. Os livros que utilizam esta ordenação voltarão a ser ordenados manualmente.', + 'sort_rule_delete_warn_books' => 'Esta regra de ordenação é atualmente utilizada em :count livro(s). Tem a certeza de que deseja eliminar isto?', + 'sort_rule_delete_warn_default' => 'Esta regra de ordenação é atualmente utilizada em livros. Tem a certeza de que deseja eliminar isto?', + 'sort_rule_details' => 'Detalhes de Regras de Ordenação', + 'sort_rule_details_desc' => 'Defina um nome para esta regra de ordenação, que aparecerá nas listas quando os utilizadores selecionarem uma opção de ordenação.', + 'sort_rule_operations' => 'Operações de Ordenação', + 'sort_rule_operations_desc' => 'Configure as ações de ordenação a executar, selecionando-as na lista de operações disponíveis. Quando utilizadas, as operações serão aplicadas por ordem, de cima para baixo. Quaisquer alterações efetuadas aqui serão aplicadas a todos os livros atribuídos quando se guardar.', + 'sort_rule_available_operations' => 'Operações Disponíveis', + 'sort_rule_available_operations_empty' => 'Não há operações pendentes', + 'sort_rule_configured_operations' => 'Operações Configuradas', + 'sort_rule_configured_operations_empty' => 'Operações de arrastar/adicionar a partir da lista "Operações Disponíveis"', 'sort_rule_op_asc' => '(Asc)', 'sort_rule_op_desc' => '(Desc)', - 'sort_rule_op_name' => 'Name - Alphabetical', - 'sort_rule_op_name_numeric' => 'Name - Numeric', - 'sort_rule_op_created_date' => 'Created Date', - 'sort_rule_op_updated_date' => 'Updated Date', - 'sort_rule_op_chapters_first' => 'Chapters First', - 'sort_rule_op_chapters_last' => 'Chapters Last', - 'sorting_page_limits' => 'Per-Page Display Limits', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sort_rule_op_name' => 'Nome - Alfabético', + 'sort_rule_op_name_numeric' => 'Nome - Numérico', + 'sort_rule_op_created_date' => 'Data de criação', + 'sort_rule_op_updated_date' => 'Data de atualização', + 'sort_rule_op_chapters_first' => 'Capítulos: Primeiro', + 'sort_rule_op_chapters_last' => 'Capítulos: Últimos', + 'sorting_page_limits' => 'Limites de Exibição por Página', + 'sorting_page_limits_desc' => 'Defina o número de itens a apresentar por página nas várias listas do sistema. Normalmente, um número mais baixo proporciona melhor desempenho, enquanto um número mais elevado evita a necessidade de percorrer várias páginas. É recomendado utilizar um múltiplo de 6.', // Maintenance settings 'maint' => 'Manutenção', @@ -141,7 +141,7 @@ 'recycle_bin_contents_empty' => 'A reciclagem está atualmente vazia', 'recycle_bin_empty' => 'Esvaziar Reciclagem', 'recycle_bin_empty_confirm' => 'Isto irá destruir permanentemente todos os itens na reciclagem inclusive o conteúdo de cada item. Tem certeza de que a deseja esvaziar?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', + 'recycle_bin_destroy_confirm' => 'Esta ação irá eliminar definitivamente este item do sistema, com quaisquer elementos secundários listados abaixo, e não será possível recuperar este conteúdo. Tem a certeza de que deseja eliminar definitivamente este item?', 'recycle_bin_destroy_list' => 'Itens a serem Destruídos', 'recycle_bin_restore_list' => 'Itens a serem Restaurados', 'recycle_bin_restore_confirm' => 'Esta ação irá restaurar o item excluído, inclusive quaisquer elementos filhos, para o seu local original. Se a localização original tiver, entretanto, sido eliminada e estiver agora na reciclagem, o item pai também precisará de ser restaurado.', @@ -194,20 +194,20 @@ 'role_access_api' => 'Aceder à API do sistema', 'role_manage_settings' => 'Gerir as configurações da aplicação', 'role_export_content' => 'Exportar conteúdo', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Importar conteúdo', 'role_editor_change' => 'Alterar editor de página', - 'role_notifications' => 'Receive & manage notifications', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', + 'role_notifications' => 'Receber e gerir notificações', + 'role_permission_note_users_and_roles' => 'Tecnicamente, estas permissões também permitirão visualizar e pesquisar utilizadores e papéis no sistema.', 'role_asset' => 'Permissões de Ativos', 'roles_system_warning' => 'Esteja ciente de que o acesso a qualquer uma das três permissões acima pode permitir que um utilizador altere os seus próprios privilégios ou privilégios de outros no sistema. Apenas atribua cargos com essas permissões a utilizadores de confiança.', 'role_asset_desc' => 'Estas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por estas permissões.', 'role_asset_admins' => 'Os administradores recebem automaticamente acesso a todo o conteúdo, mas estas opções podem mostrar ou ocultar as opções da Interface de Usuário.', 'role_asset_image_view_note' => 'Isto está relacionado com a visibilidade do gerenciador de imagens. O acesso real dos arquivos de imagem enviados dependerá da opção de armazenamento de imagens do sistema.', - 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', + 'role_asset_users_note' => 'Tecnicamente, estas permissões também permitirão visualizar e pesquisar utilizadores no sistema.', 'role_all' => 'Todos', 'role_own' => 'Próprio', 'role_controlled_by_asset' => 'Controlado pelo ativo para o qual eles são enviados', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Controlado pelas permissões de eliminação de páginas', 'role_save' => 'Guardar Cargo', 'role_users' => 'Utilizadores com este cargo', 'role_users_none' => 'Nenhum utilizador está atualmente vinculado a este cargo', @@ -229,8 +229,8 @@ 'users_send_invite_text' => 'Pode escolher enviar a este utilizador um convite por e-mail que o possibilitará definir a sua própria palavra-passe, ou defina você mesmo uma.', 'users_send_invite_option' => 'Enviar convite por e-mail', 'users_external_auth_id' => 'ID de Autenticação Externa', - 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', - 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', + 'users_external_auth_id_desc' => 'Quando se utiliza um sistema de autenticação externo (como SAML2, OIDC ou LDAP), este é o ID que associa este utilizador do BookStack à conta do sistema de autenticação. Pode ignorar este campo se estiver a utilizar a autenticação padrão baseada no e-mail.', + 'users_password_warning' => 'Preencha os campos abaixo apenas se pretender alterar a palavra-passe deste utilizador.', 'users_system_public' => 'Este utilizador representa quaisquer convidados que visitam a aplicação. Não pode ser utilizado para efetuar autenticação, mas é automaticamente atribuído.', 'users_delete' => 'Eliminar Utilizador', 'users_delete_named' => 'Eliminar :userName', @@ -246,7 +246,7 @@ 'users_preferred_language' => 'Linguagem de Preferência', 'users_preferred_language_desc' => 'Esta opção irá alterar o idioma utilizado para a interface de utilizador da aplicação. Isto não afetará nenhum conteúdo criado por utilizadores.', 'users_social_accounts' => 'Contas Sociais', - 'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.', + 'users_social_accounts_desc' => 'Ver o estado das contas sociais associadas a este utilizador. As contas sociais podem ser utilizadas em complemento ao sistema de autenticação principal para aceder ao sistema.', 'users_social_accounts_info' => 'Aqui pode ligar outras contas para acesso mais rápido. Desligar uma conta não retira a possibilidade de acesso usando-a. Para revogar o acesso ao perfil através da conta social, você deverá fazê-lo na sua conta social.', 'users_social_connect' => 'Contas Associadas', 'users_social_disconnect' => 'Dissociar Conta', @@ -255,7 +255,7 @@ 'users_social_connected' => 'A conta:socialAccount foi associada com sucesso ao seu perfil.', 'users_social_disconnected' => 'A conta:socialAccount foi dissociada com sucesso de seu perfil.', 'users_api_tokens' => 'Tokens de API', - 'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.', + 'users_api_tokens_desc' => 'Crie e faça a gestão dos tokens de acesso utilizados para autenticação na API REST do BookStack. As permissões para a API são geridas através do utilizador a quem o token pertence.', 'users_api_tokens_none' => 'Nenhum token de API foi criado para este utilizador', 'users_api_tokens_create' => 'Criar Token', 'users_api_tokens_expires' => 'Expira', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Configure a autenticação multi-fatores como uma camada extra de segurança para sua conta de utilizador.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Redefinir métodos de autenticação multifator', + 'users_mfa_reset_desc' => 'Isto irá reiniciar e eliminar todos os métodos de autenticação multifator configurados para este utilizador. Se a autenticação multifator for exigida por alguma das suas funções, ser-lhe-á solicitado que configure novos métodos no seu próximo início de sessão.', + 'users_mfa_reset_confirm' => 'Tem a certeza de que deseja repor a autenticação multifator para este utilizador?', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -316,13 +316,13 @@ 'webhooks_last_error_message' => 'Última mensagem de erro:', // Licensing - 'licenses' => 'Licenses', - 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', - 'licenses_bookstack' => 'BookStack License', - 'licenses_php' => 'PHP Library Licenses', - 'licenses_js' => 'JavaScript Library Licenses', - 'licenses_other' => 'Other Licenses', - 'license_details' => 'License Details', + 'licenses' => 'Licenças', + 'licenses_desc' => 'Esta página apresenta informações sobre as licenças do BookStack, bem como sobre os projetos e bibliotecas utilizados no BookStack. Muitos dos projetos aqui listados só podem ser utilizados num contexto de desenvolvimento.', + 'licenses_bookstack' => 'Licença de BookStack', + 'licenses_php' => 'Licenças de Bibliotecas PHP', + 'licenses_js' => 'Licenças de Bibliotecas de JavaScript', + 'licenses_other' => 'Outras Licenças', + 'license_details' => 'Detalhes de Licença', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/pt/validation.php b/lang/pt/validation.php index 17d891cdb16..4d80f85c422 100644 --- a/lang/pt/validation.php +++ b/lang/pt/validation.php @@ -105,11 +105,11 @@ 'url' => 'O formato da URL :attribute é inválido.', 'uploaded' => 'O arquivo não pôde ser carregado. O servidor pode não aceitar arquivos deste tamanho.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => 'O :attribute deve referenciar um ficheiro dentro do ZIP.', + 'zip_file_size' => 'O ficheiro :attribute não deve exceder :size MB.', + 'zip_file_mime' => 'O :attribute deve referenciar um ficheiro do tipo :validTypes, encontrado em :foundType.', + 'zip_model_expected' => 'Era esperado um objeto de dados, mas foi encontrado “:type”.', + 'zip_unique' => 'O :attribute deve ser único para o tipo de objeto dentro do ficheiro ZIP.', // Custom validation lines 'custom' => [ diff --git a/lang/pt_BR/activities.php b/lang/pt_BR/activities.php index e9069564f50..dbadcf1cc22 100644 --- a/lang/pt_BR/activities.php +++ b/lang/pt_BR/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Usuário atualizado com sucesso', 'user_delete' => 'usuário excluído', 'user_delete_notification' => 'Usuário removido com sucesso', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'redefinir MFA para usuário', + 'user_mfa_reset_notification' => 'Redefinir métodos de autenticação de múltiplos fatores', // API Tokens 'api_token_create' => 'token de API criado', diff --git a/lang/pt_BR/auth.php b/lang/pt_BR/auth.php index dfcc47da6f8..e3455e144f2 100644 --- a/lang/pt_BR/auth.php +++ b/lang/pt_BR/auth.php @@ -8,7 +8,7 @@ 'failed' => 'As credenciais fornecidas não puderam ser validadas em nossos registros.', 'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Muitas tentativas de verificação de multifatores. Por favor tente novamente em :seconds segundos.', // Login & Register 'sign_up' => 'Criar Conta', diff --git a/lang/pt_BR/entities.php b/lang/pt_BR/entities.php index 7fa0b50c1d1..fafdb80d5ff 100644 --- a/lang/pt_BR/entities.php +++ b/lang/pt_BR/entities.php @@ -219,7 +219,7 @@ 'chapters_permissions_active' => 'Permissões de Capítulo Ativas', 'chapters_permissions_success' => 'Permissões de Capítulo Atualizadas', 'chapters_search_this' => 'Pesquisar neste Capítulo', - 'chapter_sort_book' => 'Classificar livro', + 'chapter_sort_book' => 'Ordenar livro', // Pages 'page' => 'Página', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => '', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Conteúdos da página', + 'page_contents_none' => 'Nenhum título foi encontrado no conteúdo da página.', + 'page_contents_info' => 'O menu de conteúdo é gerado a partir de qualquer formato de cabeçalho usado na página.', 'page_tags' => 'Marcadores de Página', 'chapter_tags' => 'Marcadores de Capítulo', 'book_tags' => 'Marcadores de Livro', diff --git a/lang/pt_BR/settings.php b/lang/pt_BR/settings.php index 8129637c8dc..09832d8656b 100644 --- a/lang/pt_BR/settings.php +++ b/lang/pt_BR/settings.php @@ -183,7 +183,7 @@ 'role_details' => 'Detalhes do Perfil', 'role_name' => 'Nome do Perfil', 'role_desc' => 'Breve Descrição do Perfil', - 'role_mfa_enforced' => 'Requer Autenticação Multi-fator', + 'role_mfa_enforced' => 'Requer Autenticação Multifator', 'role_external_auth_id' => 'IDs de Autenticação Externa', 'role_system' => 'Permissões do Sistema', 'role_manage_users' => 'Gerenciar usuários', @@ -260,13 +260,13 @@ 'users_api_tokens_create' => 'Criar Token', 'users_api_tokens_expires' => 'Expira', 'users_api_tokens_docs' => 'Documentação da API', - 'users_mfa' => 'Autenticação de Múltiplos Fatores', - 'users_mfa_desc' => 'A autenticação multi-fator adiciona outra camada de segurança à sua conta.', + 'users_mfa' => 'Autenticação Multifator', + 'users_mfa_desc' => 'A autenticação multifator adiciona uma camada extra de segurança à sua conta.', 'users_mfa_x_methods' => ':count método configurado|:count métodos configurados', 'users_mfa_configure' => 'Configurar Métodos', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Redefiner Métodos de Autenticação Multifator', + 'users_mfa_reset_desc' => 'Isto irá redefinir e limpar todos os métodos de autenticação multifator configurados para este usuário. Se a autenticação multifator for exigida por qualquer uma de suas funções, você será solicitado a configurar novos métodos em seu próximo login.', + 'users_mfa_reset_confirm' => 'Você tem certeza que deseja remover o método de autenticação multifator?', // API Tokens 'user_api_token_create' => 'Criar Token de API', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ro/settings.php b/lang/ro/settings.php index bbbaa34d959..7423c9822b9 100644 --- a/lang/ro/settings.php +++ b/lang/ro/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/ru/settings.php b/lang/ru/settings.php index 4fdd5784ab2..f3a6529ccc2 100644 --- a/lang/ru/settings.php +++ b/lang/ru/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/sk/settings.php b/lang/sk/settings.php index e76fefb98e8..52547235326 100644 --- a/lang/sk/settings.php +++ b/lang/sk/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/sl/settings.php b/lang/sl/settings.php index 9bd63a0e88e..71f280ce226 100644 --- a/lang/sl/settings.php +++ b/lang/sl/settings.php @@ -367,6 +367,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/sq/settings.php b/lang/sq/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/sq/settings.php +++ b/lang/sq/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/sr/activities.php b/lang/sr/activities.php index 555d01adca1..bf6573e9718 100644 --- a/lang/sr/activities.php +++ b/lang/sr/activities.php @@ -77,20 +77,20 @@ 'maintenance_action_run' => 'покренуо акцију одржавања', // Webhooks - 'webhook_create' => 'креиран вебхоок', - 'webhook_create_notification' => 'Вебхоок је успешно креиран', - 'webhook_update' => 'ажуриран вебхоок', - 'webhook_update_notification' => 'Вебхоок је успешно ажуриран', - 'webhook_delete' => 'обрисан вебхоок', - 'webhook_delete_notification' => 'Вебхоок је успешно обрисан', + 'webhook_create' => 'креирана веб закачка', + 'webhook_create_notification' => 'Веб закачка је успешно креирана', + 'webhook_update' => 'ажурирана веб закачка', + 'webhook_update_notification' => 'Веб закачка је успешно ажурирана', + 'webhook_delete' => 'обрисана веб закачка', + 'webhook_delete_notification' => 'Веб закачка је успешно обрисана', // Imports 'import_create' => 'креиран увоз', - 'import_create_notification' => 'Import successfully uploaded', + 'import_create_notification' => 'Увоз је успешно отпремљен', 'import_run' => 'ажуриран увоз', - 'import_run_notification' => 'Content successfully imported', - 'import_delete' => 'deleted import', - 'import_delete_notification' => 'Import successfully deleted', + 'import_run_notification' => 'Садржај је успешно увезен', + 'import_delete' => 'обрисан увоз', + 'import_delete_notification' => 'Увоз је успешно обрисан', // Users 'user_create' => 'креирао корисника', @@ -99,8 +99,8 @@ 'user_update_notification' => 'Корисник је успешно ажуриран', 'user_delete' => 'избрисан корисника', 'user_delete_notification' => 'Корисник је успешно уклоњен', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'поништи МФА за корисника', + 'user_mfa_reset_notification' => 'Поништавања начина мултифакторске аутентификације', // API Tokens 'api_token_create' => 'креирао апи токен', @@ -130,12 +130,12 @@ 'comment_delete' => 'обрисан коментар', // Sort Rules - 'sort_rule_create' => 'created sort rule', - 'sort_rule_create_notification' => 'Sort rule successfully created', - 'sort_rule_update' => 'updated sort rule', - 'sort_rule_update_notification' => 'Sort rule successfully updated', - 'sort_rule_delete' => 'deleted sort rule', - 'sort_rule_delete_notification' => 'Sort rule successfully deleted', + 'sort_rule_create' => 'направљено је правило слагања', + 'sort_rule_create_notification' => 'Правило слагања је успешно направљено', + 'sort_rule_update' => 'ажурирано је правило слагања', + 'sort_rule_update_notification' => 'Правило слагања је успешно ажурирано', + 'sort_rule_delete' => 'избрисано је правило слагања', + 'sort_rule_delete_notification' => 'Правило слагања је успешно избрисано', // Other 'permissions_update' => 'ажуриране дозволе', diff --git a/lang/sr/auth.php b/lang/sr/auth.php index 96169fe2bab..78b3c76e2de 100644 --- a/lang/sr/auth.php +++ b/lang/sr/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Ови акредитиви се не поклапају са нашом евиденцијом.', 'throttle' => 'Превише покушаја пријаве. Покушајте поново за :seconds секунди.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Превише покушаја потврђивања вишефакторском аутентификацијом. Молим вас покушајте поново за :seconds секунди.', // Login & Register 'sign_up' => 'Региструј се', @@ -89,13 +89,13 @@ 'mfa_setup_remove_confirmation' => 'Да ли сте сигурни да желите да уклоните овај метод вишефакторске аутентификације?', 'mfa_setup_action' => 'Подешавање', 'mfa_backup_codes_usage_limit_warning' => 'Преостало вам је мање од 5 резервних кодова. Генеришите и сачувајте нови сет пре него што вам понестане кодова како бисте спречили да останете без налога.', - 'mfa_option_totp_title' => 'Aplikacije za mobilne uređaje', + 'mfa_option_totp_title' => 'Апликација за мобилне уређаје', 'mfa_option_totp_desc' => 'Да бисте користили вишефакторску аутентификацију, биће вам потребна мобилна апликација која подржава ТОТП, као што јеGoogle Authenticator, Authy или Microsoft Authenticator.', 'mfa_option_backup_codes_title' => 'Резервни кодови', 'mfa_option_backup_codes_desc' => 'Генерише скуп резервних кодова за једнократну употребу које ћете унети приликом пријављивања да бисте потврдили свој идентитет. Обавезно их чувајте на безбедном и безбедном месту.', 'mfa_gen_confirm_and_enable' => 'Потврдите и омогућите', 'mfa_gen_backup_codes_title' => 'Подешавање резервних кодова', - 'mfa_gen_backup_codes_desc' => 'Чувајте доњу листу кодова на безбедном месту. Када приступате систему, моћи ћете да користите један од кодова као други механизам за аутентификацију.', + 'mfa_gen_backup_codes_desc' => 'Сачувајте списак кодова испод на безбедном месту. Када приступате систему, моћи ћете да користите један од кодова као други механизам за аутентификацију.', 'mfa_gen_backup_codes_download' => 'Преузми кодове', 'mfa_gen_backup_codes_usage_warning' => 'Сваки код се може искористити једном', 'mfa_gen_totp_title' => 'Подешавање мобилне апликације', diff --git a/lang/sr/common.php b/lang/sr/common.php index c5c62db6588..a1efbb7af6c 100644 --- a/lang/sr/common.php +++ b/lang/sr/common.php @@ -12,7 +12,7 @@ 'save' => 'Сачувај', 'continue' => 'Настави', 'select' => 'Изабери', - 'toggle_all' => 'Сакриј/Прикажи све', + 'toggle_all' => 'Укључи све/ништа', 'more' => 'Више', // Form Labels @@ -20,18 +20,18 @@ 'description' => 'Опис', 'role' => 'Улога', 'cover_image' => 'Насловна слика', - 'cover_image_description' => 'Ова слика би требало да буде приближно 440к250px иако ће бити флексибилно скалирана и исечена како би одговарала корисничком интерфејсу у различитим сценаријима по потреби, тако да ће се стварне димензије приказа разликовати.', + 'cover_image_description' => 'Ова слика би требало да буде приближно 440х250px иако ће бити флексибилно скалирана и исечена како би одговарала корисничком интерфејсу у различитим сценаријима по потреби, тако да ће се стварне димензије приказа разликовати.', // Actions 'actions' => 'Радње', - 'view' => 'Преглед', + 'view' => 'Прегледај', 'view_all' => 'Прикажи све', 'new' => 'Ново', 'create' => 'Креирај', - 'update' => 'Ажурирање', + 'update' => 'Ажурирај', 'edit' => 'Уреди', 'archive' => 'Архивирај', - 'unarchive' => 'Un-Archive', + 'unarchive' => 'Деархивирај', 'sort' => 'Разврстај', 'move' => 'Премести', 'copy' => 'Умножи', @@ -40,7 +40,7 @@ 'delete_confirm' => 'Потврди брисање', 'search' => 'Претражи', 'search_clear' => 'Обриши претрагу', - 'reset' => 'Ресетуј', + 'reset' => 'Поништи', 'remove' => 'Уклони', 'add' => 'Додај', 'configure' => 'Конфигуриши', @@ -81,7 +81,7 @@ 'breadcrumb' => 'Навигација', 'status' => 'Стање', 'status_active' => 'Активан', - 'status_inactive' => 'Неактивно', + 'status_inactive' => 'Неактиван', 'never' => 'Никад', 'none' => 'Ништа', @@ -89,7 +89,7 @@ 'homepage' => 'Почетна страна', 'header_menu_expand' => 'Проширите мени заглавља', 'profile_menu' => 'Мени профила', - 'view_profile' => 'Погледај Профил', + 'view_profile' => 'Погледај профил', 'edit_profile' => 'Измени профил', 'dark_mode' => 'Тамни режим', 'light_mode' => 'Светли режим', @@ -111,5 +111,5 @@ 'terms_of_service' => 'Услови коришћења', // OpenSearch - 'opensearch_description' => 'Search :appName', + 'opensearch_description' => 'Претражи :appName', ]; diff --git a/lang/sr/editor.php b/lang/sr/editor.php index 2756775a9f2..0f64e1272ef 100644 --- a/lang/sr/editor.php +++ b/lang/sr/editor.php @@ -48,8 +48,8 @@ 'superscript' => 'Надскрипт', 'subscript' => 'Субкрипт', 'text_color' => 'Боја текста', - 'highlight_color' => 'Highlight color', - 'custom_color' => 'Боја текста', + 'highlight_color' => 'Боја наглашавања', + 'custom_color' => 'Прилагођена боја', 'remove_color' => 'Уклоните боју', 'background_color' => 'Боја позадине', 'align_left' => 'Поравнај лево', @@ -61,7 +61,7 @@ 'list_task' => 'Листа задатака', 'indent_increase' => 'Повећај увлачење', 'indent_decrease' => 'Умањи увлачење', - 'table' => 'Tabela', + 'table' => 'Табела', 'insert_image' => 'Уметни слику', 'insert_image_title' => 'Убаци/уреди слику', 'insert_link' => 'Убаци/измени везу', @@ -149,11 +149,11 @@ 'url' => 'УРЛ', 'text_to_display' => 'Текст за приказ', 'title' => 'Наслов', - 'browse_links' => 'Browse links', + 'browse_links' => 'Потражи везе', 'open_link' => 'Отвори везу', 'open_link_in' => 'Отвори везу у...', 'open_link_current' => 'Тренутни прозор', - 'open_link_new' => 'Нови Прозор', + 'open_link_new' => 'Нови прозор', 'remove_link' => 'Уклони везу', 'insert_collapsible' => 'Уредите склопиви блок', 'collapsible_unwrap' => 'Одмотати', @@ -166,8 +166,8 @@ 'about' => 'О уређивачу', 'about_title' => 'О уређивачу WYSIWYG', 'editor_license' => 'Уредничка лиценца и ауторска права', - 'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.', - 'editor_lexical_license_link' => 'Full license details can be found here.', + 'editor_lexical_license' => 'Уређивач је изграђен као копија :lexicalLink који се дистрибуира под MIT лиценцом.', + 'editor_lexical_license_link' => 'Комплетни детаљи лиценце се могу пронаћи овде.', 'editor_tiny_license' => 'Овај уређивач је направљен помоћу :tinyLink који је обезбеђен под МИТ лиценцом.', 'editor_tiny_license_link' => 'Детаље о ауторским правима и лиценци за ТиниМЦЕ можете пронаћи овде.', 'save_continue' => 'Сачувај страницу и настави', diff --git a/lang/sr/entities.php b/lang/sr/entities.php index cd4e732f412..1c872f546fc 100644 --- a/lang/sr/entities.php +++ b/lang/sr/entities.php @@ -24,7 +24,7 @@ 'meta_updated_name' => 'Ажурирано :timeLength од :user', 'meta_owned_name' => 'Власништво :user', 'meta_reference_count' => 'Референтна од :count item|Референтна од :count items', - 'entity_select' => 'Избор ентитета', + 'entity_select' => 'Избор ставке', 'entity_select_lack_permission' => 'Немате потребне дозволе да изаберете ову ставку', 'images' => 'Слике', 'my_recent_drafts' => 'Моји недавни нацрти', @@ -38,39 +38,39 @@ 'export_html' => 'Садржана веб датотека', 'export_pdf' => 'PDF датотека', 'export_text' => 'Датотеке чистог текста', - 'export_md' => 'Markdown File', - 'export_zip' => 'Portable ZIP', + 'export_md' => 'Markdown датотека', + 'export_zip' => 'Портабилан ZIP', 'default_template' => 'Подразумевани шаблон странице', 'default_template_explain' => 'Доделите шаблон странице који ће се користити као подразумевани садржај за све странице креиране у оквиру ове ставке. Имајте на уму да ће се ово користити само ако креатор странице има приступ за преглед изабране странице шаблона.', 'default_template_select' => 'Изаберите страницу са шаблоном', - 'import' => 'Import', - 'import_validate' => 'Validate Import', - 'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.', - 'import_zip_select' => 'Select ZIP file to upload', - 'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:', - 'import_pending' => 'Pending Imports', - 'import_pending_none' => 'No imports have been started.', + 'import' => 'Увоз', + 'import_validate' => 'Потврди увоз', + 'import_desc' => 'Увезите књиге, поглавља и стране користећи портабилан zip извоз из исте или друге инстанце. Изаберите ZIP датотеку за наставак. Након што је датотека постављена и потврђена од ваше стране, моћи ћете да подесите и потврдите увоз у следећем приказу.', + 'import_zip_select' => 'Изаберите ZIP датотеку за постављање', + 'import_zip_validation_errors' => 'Откривене су грешке током провере достављене ZIP датотеке:', + 'import_pending' => 'Увози на чекању', + 'import_pending_none' => 'Ниједан увоз није започет.', 'import_continue' => 'Настави увоз', - 'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.', - 'import_details' => 'Import Details', - 'import_run' => 'Run Import', - 'import_size' => ':size Import ZIP Size', - 'import_uploaded_at' => 'Uploaded :relativeTime', - 'import_uploaded_by' => 'Uploaded by', - 'import_location' => 'Import Location', - 'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.', - 'import_delete_confirm' => 'Are you sure you want to delete this import?', - 'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.', - 'import_errors' => 'Import Errors', - 'import_errors_desc' => 'The follow errors occurred during the import attempt:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'import_continue_desc' => 'Проверите садржај који ће бити увезен из отпремљене ZIP датотеке. Када сте спремни, покрените увоз да би сте додали садржај у систем. Отпремљена ZIP датотека ће аутоматски бити уклоњена по успешном увозу.', + 'import_details' => 'Детаљи увоза', + 'import_run' => 'Покрени увоз', + 'import_size' => ':size величина увозног ZIP-а', + 'import_uploaded_at' => 'Отпремљено :relativeTime', + 'import_uploaded_by' => 'Отпремио', + 'import_location' => 'Локација увоза', + 'import_location_desc' => 'Изаберите одредишну локацију за ваш увезени садржај. Биће вам потребне одговарајуће дозволе за прављење на локацији коју изаберете.', + 'import_delete_confirm' => 'Да ли заиста желите да обришете овај увоз?', + 'import_delete_desc' => 'Ово ће обрисати отпремљену ZIP датотеку, и није могућ опозив.', + 'import_errors' => 'Грешке увоза', + 'import_errors_desc' => 'Следеће грешке су се појавиле током покушаја увоза:', + 'breadcrumb_siblings_for_page' => 'Истражи сроднике стране', + 'breadcrumb_siblings_for_chapter' => 'Истражи сроднике поглавља', + 'breadcrumb_siblings_for_book' => 'Истражи сроднике књиге', + 'breadcrumb_siblings_for_bookshelf' => 'Истражи сроднике полице', // Permissions and restrictions 'permissions' => 'Дозволе', - 'permissions_desc' => 'Подесите дозволе овде да бисте заменили подразумеване дозволе које дају корисничке улоге.', + 'permissions_desc' => 'Овде подесите дозволе да бисте заменили подразумеване дозволе које дају корисничке улоге.', 'permissions_book_cascade' => 'Дозволе постављене за књиге ће се аутоматски пребацивати на подређена поглавља и странице, осим ако немају дефинисане сопствене дозволе.', 'permissions_chapter_cascade' => 'Дозволе постављене на поглављима ће се аутоматски каскадно пребацивати на подређене странице, осим ако немају дефинисане сопствене дозволе.', 'permissions_save' => 'Сачувај дозволе', @@ -84,397 +84,397 @@ 'search_results' => 'Резултати претраге', 'search_total_results_found' => ':count пронађених резултата|:count укупно пронађених резултата', 'search_clear' => 'Обриши претрагу', - 'search_no_pages' => 'No pages matched this search', - 'search_for_term' => 'Search for :term', - 'search_more' => 'More Results', - 'search_advanced' => 'Advanced Search', - 'search_terms' => 'Search Terms', - 'search_content_type' => 'Content Type', - 'search_exact_matches' => 'Exact Matches', - 'search_tags' => 'Tag Searches', - 'search_options' => 'Options', - 'search_viewed_by_me' => 'Viewed by me', - 'search_not_viewed_by_me' => 'Not viewed by me', - 'search_permissions_set' => 'Permissions set', - 'search_created_by_me' => 'Created by me', - 'search_updated_by_me' => 'Updated by me', - 'search_owned_by_me' => 'Owned by me', - 'search_date_options' => 'Date Options', - 'search_updated_before' => 'Updated before', - 'search_updated_after' => 'Updated after', - 'search_created_before' => 'Created before', - 'search_created_after' => 'Created after', - 'search_set_date' => 'Set Date', - 'search_update' => 'Update Search', + 'search_no_pages' => 'Ниједна страна се не поклапа са претрагом', + 'search_for_term' => 'Претражи :term', + 'search_more' => 'Више резултата', + 'search_advanced' => 'Напредна претрага', + 'search_terms' => 'Термини претраге', + 'search_content_type' => 'Тип садржаја', + 'search_exact_matches' => 'Тачно поклапање', + 'search_tags' => 'Претрага ознака', + 'search_options' => 'Опције', + 'search_viewed_by_me' => 'Гледао сам', + 'search_not_viewed_by_me' => 'Нисам гледао', + 'search_permissions_set' => 'Скуп дозвола', + 'search_created_by_me' => 'Направио сам', + 'search_updated_by_me' => 'Ажурирао сам', + 'search_owned_by_me' => 'Ја сам власник', + 'search_date_options' => 'Опције датума', + 'search_updated_before' => 'Ажурирано пре', + 'search_updated_after' => 'Ажурирано после', + 'search_created_before' => 'Направљено пре', + 'search_created_after' => 'Направљено после', + 'search_set_date' => 'Подеси датум', + 'search_update' => 'Ажурирај претрагу', // Shelves - 'shelf' => 'Shelf', + 'shelf' => 'Полица', 'shelves' => 'Полице', - 'x_shelves' => ':count Shelf|:count Shelves', - 'shelves_empty' => 'No shelves have been created', - 'shelves_create' => 'Create New Shelf', - 'shelves_popular' => 'Popular Shelves', - 'shelves_new' => 'New Shelves', - 'shelves_new_action' => 'New Shelf', - 'shelves_popular_empty' => 'The most popular shelves will appear here.', - 'shelves_new_empty' => 'The most recently created shelves will appear here.', - 'shelves_save' => 'Save Shelf', - 'shelves_books' => 'Books on this shelf', - 'shelves_add_books' => 'Add books to this shelf', - 'shelves_drag_books' => 'Drag books below to add them to this shelf', - 'shelves_empty_contents' => 'This shelf has no books assigned to it', - 'shelves_edit_and_assign' => 'Edit shelf to assign books', - 'shelves_edit_named' => 'Edit Shelf :name', - 'shelves_edit' => 'Edit Shelf', - 'shelves_delete' => 'Delete Shelf', - 'shelves_delete_named' => 'Delete Shelf :name', - 'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.", - 'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?', - 'shelves_permissions' => 'Shelf Permissions', - 'shelves_permissions_updated' => 'Shelf Permissions Updated', - 'shelves_permissions_active' => 'Shelf Permissions Active', - 'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.', - 'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.', - 'shelves_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.', - 'shelves_copy_permission_success' => 'Shelf permissions copied to :count books', + 'x_shelves' => ':count полица|:count полице', + 'shelves_empty' => 'Није направљена ниједна полица', + 'shelves_create' => 'Направи нову полицу', + 'shelves_popular' => 'Популарне полице', + 'shelves_new' => 'Нове полице', + 'shelves_new_action' => 'Нова полица', + 'shelves_popular_empty' => 'Најпопуларније полице ће се појавити овде.', + 'shelves_new_empty' => 'Најскорије направљене полице ће се појавити овде.', + 'shelves_save' => 'Сачувај полицу', + 'shelves_books' => 'Књиге на овој полици', + 'shelves_add_books' => 'Додајте књиге на ову полицу', + 'shelves_drag_books' => 'Превуците књиге испод да их додате на ову полицу', + 'shelves_empty_contents' => 'Ова полица нема додељених књига', + 'shelves_edit_and_assign' => 'Уредите полицу да би сте доделили књиге', + 'shelves_edit_named' => 'Измени полицу :name', + 'shelves_edit' => 'Измени полицу', + 'shelves_delete' => 'Обриши полицу', + 'shelves_delete_named' => 'Обриши полицу :name', + 'shelves_delete_explain' => "Ово ће обрисати полицу са називом ':name'. Садржане књиге неће бити обрисане.", + 'shelves_delete_confirmation' => 'Да ли заиста желите да обришете ову полицу?', + 'shelves_permissions' => 'Дозволе полице', + 'shelves_permissions_updated' => 'Дозволе полице су ажуриране', + 'shelves_permissions_active' => 'Дозволе полице су активне', + 'shelves_permissions_cascade_warning' => 'Дозволе на полицама се не преносе аутоматски на садржане књиге. Ово је зато што књига може да постоји на вишеструко полица. Дозволе се међутим могу умножити на књиге наследнице користећи опцију испод.', + 'shelves_permissions_create' => 'Дозволе за прављење полица се користе само за дозволе умножавања на књиге наследнице користећи радњу испод. Оне не контролишу могућност прављења књига.', + 'shelves_copy_permissions_to_books' => 'Умножи дозволе на књиге', + 'shelves_copy_permissions' => 'Умножи дозволе', + 'shelves_copy_permissions_explain' => 'Ово ће применити тренутне поставке дозвола ове полице на све књиге садржане на њој. Пре активирања, потврдите да су сачуване све измене над дозволама ове полице.', + 'shelves_copy_permission_success' => 'Дозволе полице су умножене на :count књиге', // Books - 'book' => 'Book', - 'books' => 'Books', - 'x_books' => ':count Book|:count Books', - 'books_empty' => 'No books have been created', - 'books_popular' => 'Popular Books', - 'books_recent' => 'Recent Books', - 'books_new' => 'New Books', - 'books_new_action' => 'New Book', - 'books_popular_empty' => 'The most popular books will appear here.', - 'books_new_empty' => 'The most recently created books will appear here.', - 'books_create' => 'Create New Book', - 'books_delete' => 'Delete Book', - 'books_delete_named' => 'Delete Book :bookName', - 'books_delete_explain' => 'This will delete the book with the name \':bookName\'. All pages and chapters will be removed.', - 'books_delete_confirmation' => 'Are you sure you want to delete this book?', - 'books_edit' => 'Edit Book', - 'books_edit_named' => 'Edit Book :bookName', - 'books_form_book_name' => 'Book Name', - 'books_save' => 'Save Book', - 'books_permissions' => 'Book Permissions', - 'books_permissions_updated' => 'Book Permissions Updated', - 'books_empty_contents' => 'No pages or chapters have been created for this book.', - 'books_empty_create_page' => 'Create a new page', - 'books_empty_sort_current_book' => 'Sort the current book', - 'books_empty_add_chapter' => 'Add a chapter', - 'books_permissions_active' => 'Book Permissions Active', - 'books_search_this' => 'Search this book', - 'books_navigation' => 'Book Navigation', - 'books_sort' => 'Sort Book Contents', - 'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.', - 'books_sort_auto_sort' => 'Auto Sort Option', - 'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', - 'books_sort_named' => 'Sort Book :bookName', - 'books_sort_name' => 'Sort by Name', - 'books_sort_created' => 'Sort by Created Date', - 'books_sort_updated' => 'Sort by Updated Date', - 'books_sort_chapters_first' => 'Chapters First', - 'books_sort_chapters_last' => 'Chapters Last', - 'books_sort_show_other' => 'Show Other Books', - 'books_sort_save' => 'Save New Order', - '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', + 'book' => 'Књига', + 'books' => 'Књиге', + 'x_books' => ':count књига|:count књиге', + 'books_empty' => 'Није направљена ниједна књига', + 'books_popular' => 'Популарне књиге', + 'books_recent' => 'Недавне књиге', + 'books_new' => 'Нове књиге', + 'books_new_action' => 'Нова књига', + 'books_popular_empty' => 'Најпопуларније књиге ће се појавити овде.', + 'books_new_empty' => 'Најскорије направљене књиге ће се појавити овде.', + 'books_create' => 'Направи нову књигу', + 'books_delete' => 'Обриши књигу', + 'books_delete_named' => 'Обриши књигу :bookName', + 'books_delete_explain' => 'Ово ће обрисати књигу под називом \':bookName\'. Све стране и поглавља ће такође бити уклоњени.', + 'books_delete_confirmation' => 'Да ли заиста желите да обришете ову књигу?', + 'books_edit' => 'Измени књигу', + 'books_edit_named' => 'Измени књигу :bookName', + 'books_form_book_name' => 'Назив књиге', + 'books_save' => 'Сачувај књигу', + 'books_permissions' => 'Дозволе књиге', + 'books_permissions_updated' => 'Ажуриране су дозволе књиге', + 'books_empty_contents' => 'Нису направљене стране нити поглавља за ову књигу.', + 'books_empty_create_page' => 'Направи нову страну', + 'books_empty_sort_current_book' => 'Разврстај тренутну књигу', + 'books_empty_add_chapter' => 'Додај поглавље', + 'books_permissions_active' => 'Дозволе књиге су активне', + 'books_search_this' => 'Претражи ову књигу', + 'books_navigation' => 'Навигација књиге', + 'books_sort' => 'Разврстај садржај књиге', + 'books_sort_desc' => 'Преместите поглавља и стране унутар књиге да би сте реорганизовали њен садржај. Друге књиге се могу додати што омогућава лако премештање поглавља и страна међу књигама. Опционо се може подесити правило за аутоматско разврставање садржаја ове књиге након измена.', + 'books_sort_auto_sort' => 'Опције аутоматског разврставања', + 'books_sort_auto_sort_active' => 'Активно је аутоматско разврставање: :sortName', + 'books_sort_auto_sort_creation_hint' => 'Правила опција аутоматског разврставања могу бити направљена под поставкама "Спискови и разврставање" од стране корисника са одговарајућим дозволама.', + 'books_sort_named' => 'Разврстај књигу :bookName', + 'books_sort_name' => 'Разврстај по називу', + 'books_sort_created' => 'Разврстај по датуму прављења', + 'books_sort_updated' => 'Разврстај по датуму ажурирања', + 'books_sort_chapters_first' => 'Прво поглавља', + 'books_sort_chapters_last' => 'Последње поглавља', + 'books_sort_show_other' => 'Прикажи друге књиге', + 'books_sort_save' => 'Сачувај нови редослед', + 'books_sort_show_other_desc' => 'Овде додајте друге књиге да би сте их уврстили у операцију разврставања и омогућили лаку реорганизацију међу књигама.', + 'books_sort_move_up' => 'Помери горе', + 'books_sort_move_down' => 'Помери доле', + 'books_sort_move_prev_book' => 'Помери до претходне књиге', + 'books_sort_move_next_book' => 'Помери до следеће књиге', + 'books_sort_move_prev_chapter' => 'Помери у претходно поглавље', + 'books_sort_move_next_chapter' => 'Помери у следеће поглавље', + 'books_sort_move_book_start' => 'Помери на почетак књиге', + 'books_sort_move_book_end' => 'Помери на крај књиге', + 'books_sort_move_before_chapter' => 'Помери пре поглавља', + 'books_sort_move_after_chapter' => 'Помери после поглавља', + 'books_copy' => 'Умножи књигу', + 'books_copy_success' => 'Књига је успешно умножена', // Chapters - 'chapter' => 'Chapter', - 'chapters' => 'Chapters', - 'x_chapters' => ':count Chapter|:count Chapters', - 'chapters_popular' => 'Popular Chapters', - 'chapters_new' => 'New Chapter', - 'chapters_create' => 'Create New Chapter', - 'chapters_delete' => 'Delete Chapter', - 'chapters_delete_named' => 'Delete Chapter :chapterName', - 'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.', - 'chapters_delete_confirm' => 'Are you sure you want to delete this chapter?', - 'chapters_edit' => 'Edit Chapter', - 'chapters_edit_named' => 'Edit Chapter :chapterName', - 'chapters_save' => 'Save Chapter', - 'chapters_move' => 'Move Chapter', - 'chapters_move_named' => 'Move Chapter :chapterName', - 'chapters_copy' => 'Copy Chapter', - 'chapters_copy_success' => 'Chapter successfully copied', - 'chapters_permissions' => 'Chapter Permissions', - 'chapters_empty' => 'No pages are currently in this chapter.', - 'chapters_permissions_active' => 'Chapter Permissions Active', - 'chapters_permissions_success' => 'Chapter Permissions Updated', - 'chapters_search_this' => 'Search this chapter', - 'chapter_sort_book' => 'Sort Book', + 'chapter' => 'Поглавље', + 'chapters' => 'Поглавља', + 'x_chapters' => ':count поглавље|:count поглавља', + 'chapters_popular' => 'Популарна поглавља', + 'chapters_new' => 'Ново поглавље', + 'chapters_create' => 'Направи ново поглавље', + 'chapters_delete' => 'Обриши поглавље', + 'chapters_delete_named' => 'Обриши поглавље :chapterName', + 'chapters_delete_explain' => 'Ово ће обрисати поглавље са називом \':chapterName\'. Све стране које постоје унутар овог поглавља ће такође бити обрисане.', + 'chapters_delete_confirm' => 'Да ли заиста желите да обришете ово поглавље?', + 'chapters_edit' => 'Измени поглавље', + 'chapters_edit_named' => 'Измени поглавље :chapterName', + 'chapters_save' => 'Сачувај поглавље', + 'chapters_move' => 'Премести поглавље', + 'chapters_move_named' => 'Премести поглавље :chapterName', + 'chapters_copy' => 'Умножи поглавље', + 'chapters_copy_success' => 'Поглавље успешно умножено', + 'chapters_permissions' => 'Дозволе поглавља', + 'chapters_empty' => 'Тренутно нема страница у овом поглављу.', + 'chapters_permissions_active' => 'Активне су дозволе поглавља', + 'chapters_permissions_success' => 'Дозволе поглавља су ажуриране', + 'chapters_search_this' => 'Претражи ово поглавље', + 'chapter_sort_book' => 'Разврстај књигу', // Pages - 'page' => 'Page', - 'pages' => 'Pages', - 'x_pages' => ':count Page|:count Pages', - 'pages_popular' => 'Popular Pages', - 'pages_new' => 'New Page', - 'pages_attachments' => 'Attachments', - 'pages_navigation' => 'Page Navigation', - 'pages_delete' => 'Delete Page', - 'pages_delete_named' => 'Delete Page :pageName', - 'pages_delete_draft_named' => 'Delete Draft Page :pageName', - 'pages_delete_draft' => 'Delete Draft Page', - 'pages_delete_success' => 'Page deleted', - 'pages_delete_draft_success' => 'Draft page deleted', - 'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.', - 'pages_delete_confirm' => 'Are you sure you want to delete this page?', - 'pages_delete_draft_confirm' => 'Are you sure you want to delete this draft page?', - 'pages_editing_named' => 'Editing Page :pageName', - 'pages_edit_draft_options' => 'Draft Options', - 'pages_edit_save_draft' => 'Save Draft', - 'pages_edit_draft' => 'Edit Page Draft', - 'pages_editing_draft' => 'Editing Draft', - 'pages_editing_page' => 'Editing Page', - 'pages_edit_draft_save_at' => 'Draft saved at ', - 'pages_edit_delete_draft' => 'Delete Draft', - 'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.', - 'pages_edit_discard_draft' => 'Discard Draft', - 'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor', - 'pages_edit_switch_to_markdown_clean' => '(Clean Content)', - 'pages_edit_switch_to_markdown_stable' => '(Stable Content)', - 'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor', - 'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', - 'pages_edit_set_changelog' => 'Set Changelog', - 'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made', - 'pages_edit_enter_changelog' => 'Enter Changelog', - 'pages_editor_switch_title' => 'Switch Editor', - 'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?', - 'pages_editor_switch_consider_following' => 'Consider the following when changing editors:', - 'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.', - 'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.', - 'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.', - 'pages_save' => 'Save Page', - 'pages_title' => 'Page Title', - 'pages_name' => 'Page Name', - 'pages_md_editor' => 'Editor', - 'pages_md_preview' => 'Preview', - 'pages_md_insert_image' => 'Insert Image', - 'pages_md_insert_link' => 'Insert Entity Link', - 'pages_md_insert_drawing' => 'Insert Drawing', - 'pages_md_show_preview' => 'Show preview', - 'pages_md_sync_scroll' => 'Sync preview scroll', - 'pages_md_plain_editor' => 'Plaintext editor', - 'pages_drawing_unsaved' => 'Unsaved Drawing Found', - 'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?', - 'pages_not_in_chapter' => 'Page is not in a chapter', - 'pages_move' => 'Move Page', - 'pages_copy' => 'Copy Page', - 'pages_copy_desination' => 'Copy Destination', - 'pages_copy_success' => 'Page successfully copied', - 'pages_permissions' => 'Page Permissions', - 'pages_permissions_success' => 'Page permissions updated', - 'pages_revision' => 'Revision', - 'pages_revisions' => 'Page Revisions', - 'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.', - 'pages_revisions_named' => 'Page Revisions for :pageName', - 'pages_revision_named' => 'Page Revision for :pageName', - 'pages_revision_restored_from' => 'Restored from #:id; :summary', - 'pages_revisions_created_by' => 'Created By', - 'pages_revisions_date' => 'Revision Date', + 'page' => 'Страна', + 'pages' => 'Стране', + 'x_pages' => ':count страна|:count стране', + 'pages_popular' => 'Популарне стране', + 'pages_new' => 'Нова страна', + 'pages_attachments' => 'Прилози', + 'pages_navigation' => 'Навигација стране', + 'pages_delete' => 'Обриши страну', + 'pages_delete_named' => 'Обриши страну :pageName', + 'pages_delete_draft_named' => 'Обриши нацрт стране :pageName', + 'pages_delete_draft' => 'Обриши нацрт стране', + 'pages_delete_success' => 'Страна је обрисана', + 'pages_delete_draft_success' => 'Нацрт стране је обрисан', + 'pages_delete_warning_template' => 'Ова страна је у активној употреби као подразумевани шаблон књиге или поглавља. Ове књиге или поглавља више неће имати додељен подразумевани шаблон стране након обришете ову страну.', + 'pages_delete_confirm' => 'Да ли заиста желите да обришете ову страну?', + 'pages_delete_draft_confirm' => 'Да ли заиста желите да обришете овај нацрт?', + 'pages_editing_named' => 'Уређивање стране :pageName', + 'pages_edit_draft_options' => 'Опције нацрта', + 'pages_edit_save_draft' => 'Сачувај нацрт', + 'pages_edit_draft' => 'Измени нацрт стране', + 'pages_editing_draft' => 'Уређивање нацрта', + 'pages_editing_page' => 'Уређивање стране', + 'pages_edit_draft_save_at' => 'Нацрт сачуван у ', + 'pages_edit_delete_draft' => 'Обриши нацрт', + 'pages_edit_delete_draft_confirm' => 'Да ли заиста желите да обришете ваш нацрт стране? Све ваше измене, од последњег пуног снимања ће бити изгубљене и уређивач ће бити ажуриран на последње сачувано стање стране без нацрта.', + 'pages_edit_discard_draft' => 'Одбаци нацрт', + 'pages_edit_switch_to_markdown' => 'Пребаци се на Маркдаун уређивач', + 'pages_edit_switch_to_markdown_clean' => '(чист садржај)', + 'pages_edit_switch_to_markdown_stable' => '(стабилан садржај)', + 'pages_edit_switch_to_wysiwyg' => 'Пребаци на WYSIWYG уређивач', + 'pages_edit_switch_to_new_wysiwyg' => 'Пребаци на нови WYSIWYG', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(у бета тестирању)', + 'pages_edit_set_changelog' => 'Напомена о изменама', + 'pages_edit_enter_changelog_desc' => 'Унесите кратак опис измена које сте извршили', + 'pages_edit_enter_changelog' => 'Упишите запис о променама', + 'pages_editor_switch_title' => 'Промени уређивач', + 'pages_editor_switch_are_you_sure' => 'Да ли заиста желите да промените уређивач за ову страну?', + 'pages_editor_switch_consider_following' => 'Размотрите следеће када мењате уређиваче:', + 'pages_editor_switch_consideration_a' => 'Једном сачуване, опције новог уређивача ће се користити за све будуће уреднике, укључујући оне који немају могућност да сами мењају тип уређивача.', + 'pages_editor_switch_consideration_b' => 'Ово потенцијално може довести до губитка детаља и синтаксе у одређеним случајевима.', + 'pages_editor_switch_consideration_c' => 'Ознаке или записи промена, начињени након последњег снимања, неће се задржати преко ове измене.', + 'pages_save' => 'Сачувај страну', + 'pages_title' => 'Наслов стране', + 'pages_name' => 'Назив стране', + 'pages_md_editor' => 'Уређивач', + 'pages_md_preview' => 'Преглед', + 'pages_md_insert_image' => 'Уметни слику', + 'pages_md_insert_link' => 'Уметни везу до ентитета', + 'pages_md_insert_drawing' => 'Уметни цртеж', + 'pages_md_show_preview' => 'Прикажи преглед', + 'pages_md_sync_scroll' => 'Синхронизуј положај прегледа', + 'pages_md_plain_editor' => 'Уређивач чистог текста', + 'pages_drawing_unsaved' => 'Пронађен је несачуван цртеж', + 'pages_drawing_unsaved_confirm' => 'Пронађени су подаци о несачуваном цртежу од претходног неуспелог покушаја снимања. Да ли желите да га повратите и наставите са уређивањем овог несачуваног цртежа?', + 'pages_not_in_chapter' => 'Страна није у поглављу', + 'pages_move' => 'Премести страну', + 'pages_copy' => 'Умножи страну', + 'pages_copy_desination' => 'Умножи одредиште', + 'pages_copy_success' => 'Страна је успешно умножена', + 'pages_permissions' => 'Дозволе стране', + 'pages_permissions_success' => 'Ажуриране су дозволе стране', + 'pages_revision' => 'Ревизија', + 'pages_revisions' => 'Ревизије стране', + 'pages_revisions_desc' => 'спод су наведене све ревизије ове стране. Можете их погледати, упоредити и вратити старе верзије стране ако имате дозвола за то. Пуна историја стране се можда не може сагледати овде с обзиром да, у зависности од подешавања система, старе ревизије су можда аутоматски обрисане.', + 'pages_revisions_named' => 'Ревизије стране за :pageName', + 'pages_revision_named' => 'Ревизија стране за :pageName', + 'pages_revision_restored_from' => 'Враћено из #:id; :summary', + 'pages_revisions_created_by' => 'Направио', + 'pages_revisions_date' => 'Датум ревизије', 'pages_revisions_number' => '#', - 'pages_revisions_sort_number' => 'Revision Number', - 'pages_revisions_numbered' => 'Revision #:id', - 'pages_revisions_numbered_changes' => 'Revision #:id Changes', - 'pages_revisions_editor' => 'Editor Type', - 'pages_revisions_changelog' => 'Changelog', - 'pages_revisions_changes' => 'Changes', - 'pages_revisions_current' => 'Current Version', - 'pages_revisions_preview' => 'Preview', - 'pages_revisions_restore' => 'Restore', - 'pages_revisions_none' => 'This page has no revisions', - 'pages_copy_link' => 'Copy Link', - 'pages_edit_content_link' => 'Jump to section in editor', - 'pages_pointer_enter_mode' => 'Enter section select mode', - 'pages_pointer_label' => 'Page Section Options', - 'pages_pointer_permalink' => 'Page Section Permalink', - 'pages_pointer_include_tag' => 'Page Section Include Tag', - 'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag', - 'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink', - 'pages_permissions_active' => 'Page Permissions Active', - 'pages_initial_revision' => 'Initial publish', - 'pages_references_update_revision' => 'System auto-update of internal links', - 'pages_initial_name' => 'New Page', - 'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.', - 'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.', - '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_revisions_sort_number' => 'Број ревизије', + 'pages_revisions_numbered' => 'Ревизија #:id', + 'pages_revisions_numbered_changes' => 'Промене ревизије #:id', + 'pages_revisions_editor' => 'Тип уређивача', + 'pages_revisions_changelog' => 'Запис промене', + 'pages_revisions_changes' => 'Промене', + 'pages_revisions_current' => 'Тренутна верзија', + 'pages_revisions_preview' => 'Преглед', + 'pages_revisions_restore' => 'Враћање', + 'pages_revisions_none' => 'Ова страна нема ревизија', + 'pages_copy_link' => 'Умножи везу', + 'pages_edit_content_link' => 'Скочи на секцију у уређивачу', + 'pages_pointer_enter_mode' => 'Уђите у режим избора секције', + 'pages_pointer_label' => 'Опције секције стране', + 'pages_pointer_permalink' => 'Стална веза секције стране', + 'pages_pointer_include_tag' => 'Секција стране садржи ознаку', + 'pages_pointer_toggle_link' => 'Режим сталне везе. Притисните за приказ садржане ознаке', + 'pages_pointer_toggle_include' => 'Режим садржане ознаке. Притисните за приказ сталне везе', + 'pages_permissions_active' => 'Активне су дозволе стране', + 'pages_initial_revision' => 'Прва објава', + 'pages_references_update_revision' => 'Системско аутоматско ажурирање интерних веза', + 'pages_initial_name' => 'Нова страна', + 'pages_editing_draft_notification' => 'Тренутно уређујете нацрт који је сачуван :timeDiff.', + 'pages_draft_edited_notification' => 'Ова страна је ажурирана од тада. Препоручује се да одбаците овај нацрт.', + 'pages_draft_page_changed_since_creation' => 'Ова страна је ажурирана након прављења овог нацрта. Препоручује се да обаците овај нацрт или да се потрудите да не препишете било какве измене на страни.', 'pages_draft_edit_active' => [ - 'start_a' => ':count users have started editing this page', - 'start_b' => ':userName has started editing this page', - 'time_a' => 'since the page was last updated', - 'time_b' => 'in the last :minCount minutes', - 'message' => ':start :time. Take care not to overwrite each other\'s updates!', + 'start_a' => ':count корисника је започело уређивање ове стране', + 'start_b' => ':userName је започео уређивање ове стране', + 'time_a' => 'од када је страна последи пут ажурирана', + 'time_b' => 'у последњих:minCount минута', + 'message' => ':start :time. Водите рачуна да једни другима не препишете измене!', ], - '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_specific' => 'Specific Page', - 'pages_is_template' => 'Page Template', + 'pages_draft_discarded' => 'Нацрт је одбачен! Уређивач је ажуриран са тренутним садржајем стране', + 'pages_draft_deleted' => 'Нацрт је обисан! Уређивач је ажуриран са тренутним садржајем стране', + 'pages_specific' => 'Одређена страна', + 'pages_is_template' => 'Шаблон стране', // Editor Sidebar - 'toggle_sidebar' => 'Toggle Sidebar', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', - 'page_tags' => 'Page Tags', - 'chapter_tags' => 'Chapter Tags', - 'book_tags' => 'Book Tags', - 'shelf_tags' => 'Shelf Tags', - 'tag' => 'Tag', - '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', - 'tag_value' => 'Tag Value (Optional)', - '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_add' => 'Add another tag', - 'tags_remove' => 'Remove this tag', - '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.', - 'attachments' => 'Attachments', - 'attachments_explain' => 'Upload some files or attach some links to display on your page. These are visible in the page sidebar.', - 'attachments_explain_instant_save' => 'Changes here are saved instantly.', - 'attachments_upload' => 'Upload File', - 'attachments_link' => 'Attach Link', - 'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.', - 'attachments_set_link' => 'Set Link', - 'attachments_delete' => 'Are you sure you want to delete this attachment?', - 'attachments_dropzone' => 'Drop files here to upload', - 'attachments_no_files' => 'No files have been uploaded', - 'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.', - 'attachments_link_name' => 'Link Name', - 'attachment_link' => 'Attachment link', - 'attachments_link_url' => 'Link to file', - 'attachments_link_url_hint' => 'Url of site or file', - 'attach' => 'Attach', - 'attachments_insert_link' => 'Add Attachment Link to Page', - 'attachments_edit_file' => 'Edit File', - 'attachments_edit_file_name' => 'File Name', - 'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite', - 'attachments_order_updated' => 'Attachment order updated', - 'attachments_updated_success' => 'Attachment details updated', - 'attachments_deleted' => 'Attachment deleted', - 'attachments_file_uploaded' => 'File successfully uploaded', - 'attachments_file_updated' => 'File successfully updated', - 'attachments_link_attached' => 'Link successfully attached to page', - 'templates' => 'Templates', - 'templates_set_as_template' => 'Page is a template', - 'templates_explain_set_as_template' => 'You can set this page as a template so its contents be utilized when creating other pages. Other users will be able to use this template if they have view permissions for this page.', - 'templates_replace_content' => 'Replace page content', - 'templates_append_content' => 'Append to page content', - 'templates_prepend_content' => 'Prepend to page content', + 'toggle_sidebar' => 'Приказ помоћне траке', + 'page_contents' => 'Садржај стране', + 'page_contents_none' => 'Није пронађено ниједно заглавље у садржају стране.', + 'page_contents_info' => 'Мени садржаја се генерише према форматима заглавља коришћеним на овој страни.', + 'page_tags' => 'Ознаке стране', + 'chapter_tags' => 'Ознаке поглавља', + 'book_tags' => 'Ознаке књиге', + 'shelf_tags' => 'Ознаке полице', + 'tag' => 'Ознака', + 'tags' => 'Ознаке', + 'tags_index_desc' => 'Ознаке се могу придодати садржају унутар система како би се применио флексибилан облик категоризације. Ознаке могу имати кључ и вредност, док је вредност опциона. Када су примењене, садржај се може претраживати коришћењем назива ознаке и вредности.', + 'tag_name' => 'Назив ознаке', + 'tag_value' => 'Вредност ознаке (опционо)', + 'tags_explain' => "Додај неке ознаке за бољу категоризацију вашег садржаја. \n Можете доделити вредност ознаци за још прецизнију организацију.", + 'tags_add' => 'Додај још једну ознаку', + 'tags_remove' => 'Уклони ову ознаку', + 'tags_usages' => 'Укупна употреба ознака', + 'tags_assigned_pages' => 'Додељено странама', + 'tags_assigned_chapters' => 'Додељено поглављима', + 'tags_assigned_books' => 'Додељено књигама', + 'tags_assigned_shelves' => 'Додељено полицама', + 'tags_x_unique_values' => ':count јединствених вредности', + 'tags_all_values' => 'Све вредности', + 'tags_view_tags' => 'Преглед ознака', + 'tags_view_existing_tags' => 'Погледај постојеће ознаке', + 'tags_list_empty_hint' => 'Ознаке се могу доделити путем траке са стране у уређивачу стране док се уређују детаљи о књизи, поглавља или полице.', + 'attachments' => 'Прилози', + 'attachments_explain' => 'Отпремите неке датотеке или прикачите неке везе за приказ на вашој страни. Оне су видљиве на помоћној траци стране.', + 'attachments_explain_instant_save' => 'Измене овде су моментално сачуване.', + 'attachments_upload' => 'Постави датотеку', + 'attachments_link' => 'Закачи везу', + 'attachments_upload_drop' => 'Алтернативно можете превући и отпустити датотеку овде да би сте је отпремили као прилог.', + 'attachments_set_link' => 'Подеси везу', + 'attachments_delete' => 'Да ли заиста желите да обришете овај прилог?', + 'attachments_dropzone' => 'Отпустите датотеке овде да их отпремите', + 'attachments_no_files' => 'Ниједна датотека није постављена', + 'attachments_explain_link' => 'Можете закачити везу ако не желите да отпремате датотеку. Ово може бити веза ка другој страни или датотека у облаку.', + 'attachments_link_name' => 'Назив везе', + 'attachment_link' => 'Веза прилога', + 'attachments_link_url' => 'Веза до датотеке', + 'attachments_link_url_hint' => 'Адреса сајта или датотеке', + 'attach' => 'Закачи', + 'attachments_insert_link' => 'Додај везу прилога на страну', + 'attachments_edit_file' => 'Измени датотеку', + 'attachments_edit_file_name' => 'Назив датотеке', + 'attachments_edit_drop_upload' => 'Отпустите датотеке или кликните овде да отпремите и препишете', + 'attachments_order_updated' => 'Ажуриран је редослед прилога', + 'attachments_updated_success' => 'Ажурирани су детаљи прилога', + 'attachments_deleted' => 'Прилог је обрисан', + 'attachments_file_uploaded' => 'Датотека је успешно отпремљена', + 'attachments_file_updated' => 'Датотека је успешно ажурирана', + 'attachments_link_attached' => 'Веза је успешно закачена на страну', + 'templates' => 'Шаблони', + 'templates_set_as_template' => 'Страна је шаблон', + 'templates_explain_set_as_template' => 'Можете подесити ову страну као шаблон како би њен садржај био искоришћен при прављењу других страна. Други корисници ће моћи да користе овај шаблон ако имају дозволу да прегледају ову страну.', + 'templates_replace_content' => 'Замени садржај стране', + 'templates_append_content' => 'Придодај садржају стране', + 'templates_prepend_content' => 'Уметни пре садржаја стране', // Profile View - 'profile_user_for_x' => 'User for :time', - 'profile_created_content' => 'Created Content', - 'profile_not_created_pages' => ':userName has not created any pages', - 'profile_not_created_chapters' => ':userName has not created any chapters', - 'profile_not_created_books' => ':userName has not created any books', - 'profile_not_created_shelves' => ':userName has not created any shelves', + 'profile_user_for_x' => 'Корисник последњих :time', + 'profile_created_content' => 'Направљени садржај', + 'profile_not_created_pages' => ':userName није направио ниједну страницу', + 'profile_not_created_chapters' => ':userName није направио ниједно поглавље', + 'profile_not_created_books' => ':userName није направио ниједну књигу', + 'profile_not_created_shelves' => ':userName није направио ниједну полицу', // Comments - 'comment' => 'Comment', - 'comments' => 'Comments', - 'comment_add' => 'Add Comment', - 'comment_none' => 'No comments to display', - 'comment_placeholder' => 'Leave a comment here', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', - 'comment_save' => 'Save Comment', - 'comment_new' => 'New Comment', - 'comment_created' => 'commented :createDiff', - 'comment_updated' => 'Updated :updateDiff by :username', - 'comment_updated_indicator' => 'Updated', - 'comment_deleted_success' => 'Comment deleted', - 'comment_created_success' => 'Comment added', - 'comment_updated_success' => 'Comment updated', - 'comment_archive_success' => 'Comment archived', - 'comment_unarchive_success' => 'Comment un-archived', - 'comment_view' => 'View comment', - 'comment_jump_to_thread' => 'Jump to thread', - 'comment_delete_confirm' => 'Are you sure you want to delete this comment?', - 'comment_in_reply_to' => 'In reply to :commentId', - 'comment_reference' => 'Reference', - 'comment_reference_outdated' => '(Outdated)', - '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' => 'Коментар', + 'comments' => 'Коментари', + 'comment_add' => 'Додај коментар', + 'comment_none' => 'Нема коментара за приказ', + 'comment_placeholder' => 'Оставите коментар овде', + 'comment_thread_count' => ':count коментар у разговору|:count коментара у разговору', + 'comment_archived_count' => ':count архивирано', + 'comment_archived_threads' => 'Архивиране приче', + 'comment_save' => 'Сачувај коментар', + 'comment_new' => 'Нови коментар', + 'comment_created' => 'коментарисао :createDiff', + 'comment_updated' => 'Измењено :updateDiff од стране :username', + 'comment_updated_indicator' => 'Ажурирано', + 'comment_deleted_success' => 'Коментар обрисан', + 'comment_created_success' => 'Коментар додат', + 'comment_updated_success' => 'Коментар измењен', + 'comment_archive_success' => 'Коментар архиивиран', + 'comment_unarchive_success' => 'Коментар деархивиран', + 'comment_view' => 'Погледај коментар', + 'comment_jump_to_thread' => 'Пређи на разговор', + 'comment_delete_confirm' => 'Да ли заиста желите да обришете овај коментар?', + 'comment_in_reply_to' => 'Као одговор на :commentId', + 'comment_reference' => 'Референца', + 'comment_reference_outdated' => '(застарело)', + 'comment_editor_explain' => 'Ево коментара који су остављени на овој страни. Коментари се могу додавати и може им се управљати при прегледу сачуване стране.', // Revision - 'revision_delete_confirm' => 'Are you sure you want to delete this revision?', - 'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.', - 'revision_cannot_delete_latest' => 'Cannot delete the latest revision.', + 'revision_delete_confirm' => 'Да ли заиста желите да обришете ову ревизију?', + 'revision_restore_confirm' => 'Да ли заиста желите да вратите ову ревизију? Садржај тренутне стране ће бити замењен.', + 'revision_cannot_delete_latest' => 'Није могуће обрисати последњу ревизију.', // Copy view - 'copy_consider' => 'Please consider the below when copying content.', - 'copy_consider_permissions' => 'Custom permission settings will not be copied.', - 'copy_consider_owner' => 'You will become the owner of all copied content.', - 'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.', - 'copy_consider_attachments' => 'Page attachments will not be copied.', - 'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.', + 'copy_consider' => 'Узмите у обзир следеће када умножавате садржај.', + 'copy_consider_permissions' => 'Поставке прилагођених дозвола неће бити умножене.', + 'copy_consider_owner' => 'Постаћете власник свег умноженог садржаја.', + 'copy_consider_images' => 'Датотеке слика стране неће бити умножене и оригиналне слике ће задржати однос према страни на коју су оригинално отпремљене.', + 'copy_consider_attachments' => 'Прилози стране неће бити умножени.', + 'copy_consider_access' => 'Промена локације, власника или дозвола може проузроковати да садржај постане доступан онима који раније нису имали приступ.', // Conversions - 'convert_to_shelf' => 'Convert to Shelf', - 'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.', - 'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.', - 'convert_book' => 'Convert Book', - 'convert_book_confirm' => 'Are you sure you want to convert this book?', - 'convert_undo_warning' => 'This cannot be as easily undone.', - 'convert_to_book' => 'Convert to Book', - 'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.', - 'convert_chapter' => 'Convert Chapter', - 'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?', + 'convert_to_shelf' => 'Претвори у полицу', + 'convert_to_shelf_contents_desc' => 'Можете претворити ову књигу у нову полицу са истим садржајем. Поглавља садржана унутар ове књиге ће бити претворена у нове књиге. Ако ова књига садржи икакве стране, које нису у поглављу, ова књига ће бити преименована и садржаће те стане и та књига ће постати део нове полице.', + 'convert_to_shelf_permissions_desc' => 'Све дозволе подешене над овом књигом ће бити умножене на нову полицу и на све нове књиге наследнике које немају приморане сопствене дозволе. Напомена да се дозволе на полицама на преносе на њихов садржај, као што је то случај са књигама.', + 'convert_book' => 'Претвори књигу', + 'convert_book_confirm' => 'Да ли заиста желите да претворите ову књигу?', + 'convert_undo_warning' => 'Ово не може лако бити повраћено.', + 'convert_to_book' => 'Претвори у књигу', + 'convert_to_book_desc' => 'Можете да претворите ово поглавље у нову књигу са истим садржајем. Све дозволе подешене над овим поглављем ће бити умножене на нову књигу али све наслеђене дозволе, од књиге родитеља, неће бити умножене што ће довести д промене у контрол приступа.', + 'convert_chapter' => 'Претвори поглавље', + 'convert_chapter_confirm' => 'Да ли заиста желите да претворите ово поглавље?', // References - 'references' => 'References', - 'references_none' => 'There are no tracked references to this item.', - 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.', + 'references' => 'Референце', + 'references_none' => 'Нема праћених референци ка овој ставки.', + 'references_to_desc' => 'Испод је наведен сав познати садржај у систему који је повезан са овом ставком.', // Watch Options - 'watch' => 'Watch', - 'watch_title_default' => 'Default Preferences', - 'watch_desc_default' => 'Revert watching to just your default notification preferences.', - 'watch_title_ignore' => 'Ignore', - 'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.', - 'watch_title_new' => 'New Pages', - 'watch_desc_new' => 'Notify when any new page is created within this item.', - 'watch_title_updates' => 'All Page Updates', - 'watch_desc_updates' => 'Notify upon all new pages and page changes.', - 'watch_desc_updates_page' => 'Notify upon all page changes.', - 'watch_title_comments' => 'All Page Updates & Comments', - 'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.', - 'watch_desc_comments_page' => 'Notify upon page changes and new comments.', - 'watch_change_default' => 'Change default notification preferences', - 'watch_detail_ignore' => 'Ignoring notifications', - 'watch_detail_new' => 'Watching for new pages', - 'watch_detail_updates' => 'Watching new pages and updates', - 'watch_detail_comments' => 'Watching new pages, updates & comments', - 'watch_detail_parent_book' => 'Watching via parent book', - 'watch_detail_parent_book_ignore' => 'Ignoring via parent book', - 'watch_detail_parent_chapter' => 'Watching via parent chapter', - 'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter', + 'watch' => 'Прати', + 'watch_title_default' => 'Подразумевана подешавања', + 'watch_desc_default' => 'Вратите праћење на ваша подразумевана подешавања обавештавања.', + 'watch_title_ignore' => 'Игнориши', + 'watch_desc_ignore' => 'Игнориши сва обавештења, укључујући она из подешавања на корисничком нивоу.', + 'watch_title_new' => 'Нове стране', + 'watch_desc_new' => 'Обавести када се направи икаква нова страна унутар ове ставке.', + 'watch_title_updates' => 'Све измене странице', + 'watch_desc_updates' => 'Обавести при прављењу свих нових страна и измена страна.', + 'watch_desc_updates_page' => 'Обавести при свим изменама на страни.', + 'watch_title_comments' => 'Све измене странице и коментари', + 'watch_desc_comments' => 'Обавести за све нове стране, измене страна и новим коментарима.', + 'watch_desc_comments_page' => 'Обавести при измени стране и новим коментарима.', + 'watch_change_default' => 'Измени подразумевана подешавања обавештавања', + 'watch_detail_ignore' => 'Игнорисање обавештења', + 'watch_detail_new' => 'Праћење нових страна', + 'watch_detail_updates' => 'Праћење нових страна и измена', + 'watch_detail_comments' => 'Праћење нових страна, измена и коментара', + 'watch_detail_parent_book' => 'Праћење кроз родитељску књигу', + 'watch_detail_parent_book_ignore' => 'Игнорисање кроз родитељску књигу', + 'watch_detail_parent_chapter' => 'Праћење кроз родитељско поглавље', + 'watch_detail_parent_chapter_ignore' => 'Игнорисање кроз родитељско поглавље', ]; diff --git a/lang/sr/errors.php b/lang/sr/errors.php index 55ba90a5c7a..38928ccc6e9 100644 --- a/lang/sr/errors.php +++ b/lang/sr/errors.php @@ -9,127 +9,127 @@ 'permissionJson' => 'Немате овлашћење да извршите ову акцију.', // Auth - 'error_user_exists_different_creds' => 'Корисник са е-мејл адресом :email већ постоји са другим приступним подацима.', - 'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details', - 'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', - 'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', - 'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', - 'email_confirmation_awaiting' => 'The email address for the account in use needs to be confirmed', - 'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', - 'ldap_fail_authed' => 'LDAP access failed using given dn & password details', - 'ldap_extension_not_installed' => 'LDAP PHP extension not installed', - 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', - 'saml_already_logged_in' => 'Already logged in', - 'saml_no_email_address' => 'Could not find an email address, for this user, in the data provided by the external authentication system', - 'saml_invalid_response_id' => 'The request from the external authentication system is not recognised by a process started by this application. Navigating back after a login could cause this issue.', - 'saml_fail_authed' => 'Login using :system failed, system did not provide successful authorization', - 'oidc_already_logged_in' => 'Already logged in', - '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', - 'social_no_action_defined' => 'No action defined', - 'social_login_bad_response' => "Error received during :socialAccount login: \n:error", - 'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.', - 'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.', - 'social_account_existing' => 'This :socialAccount is already attached to your profile.', - 'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.', - 'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ', - 'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.', - 'social_driver_not_found' => 'Social driver not found', - 'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', - 'invite_token_expired' => 'This invitation link has expired. You can instead try to reset your account password.', - 'login_user_not_found' => 'A user for this action could not be found.', + 'error_user_exists_different_creds' => 'Корисник са адресом е-поште :email већ постоји са другим приступним подацима.', + 'auth_pre_register_theme_prevention' => 'Кориснички налог није могао бити регистрован са достављеним подацима', + 'email_already_confirmed' => 'Е-пошта је већ потврђена. Покушајте да се пријавите.', + 'email_confirmation_invalid' => 'Овај потврдни токен није исправан или је већ искоришћен. Покушајте да се поново региструјете.', + 'email_confirmation_expired' => 'Потврдни токен је истекао. Послата је нова е-порука за потврду.', + 'email_confirmation_awaiting' => 'Адреса е-поште за налог у употреби мора бити потврђен', + 'ldap_fail_anonymous' => 'LDAP приступ није успео користећи анонимно спајање', + 'ldap_fail_authed' => 'LDAP приступ није успео са наведеним dn и лозинка подацима', + 'ldap_extension_not_installed' => 'LDAP PHP проширење није инсталирано', + 'ldap_cannot_connect' => 'Није могуће повезати се на ldap сервер. Иницијално повезивање није успело', + 'saml_already_logged_in' => 'Већ пријављен', + 'saml_no_email_address' => 'Нисмо могли да пронађемо адресу е-поште за овог корисника у достављеним подацима од стране екстерног система за аутентификацију', + 'saml_invalid_response_id' => 'Захтев од екстерног система за аутентификацију није препознат од стране процеса започетког овом апликацијом. Повратак након пријаве може бити узрок овог проблема.', + 'saml_fail_authed' => 'Пријава користећи :system није успела, систем није доставио успешну ауторизацију', + 'oidc_already_logged_in' => 'Већ пријављен', + 'oidc_no_email_address' => 'Нисмо могли да пронађемо адресу е-поште за овог корисника у достављеним подацима од стране екстерног система за аутентификацију', + 'oidc_fail_authed' => 'Пријава користећи :system није успела, систем није доставио успешну ауторизацију', + 'social_no_action_defined' => 'Није дефинисана радња', + 'social_login_bad_response' => "Добијена је грешка током :socialAccount пријаве: \n:error", + 'social_account_in_use' => 'Овај :socialAccount налог је већ у употреби. Покушајте пријаву са :socialAccount опцијом.', + 'social_account_email_in_use' => 'Ова е-пошта :email је већ у употреби. Ако већ имате налог можете повезати ваш :socialAccount налог у поставкама вашег профила.', + 'social_account_existing' => 'Овај :socialAccount је већ повезан са вашим профилом.', + 'social_account_already_used_existing' => 'Овај :socialAccount налог је већ у употреби од стране другог корисника.', + 'social_account_not_used' => 'Овај :socialAccount налог није повезан ни са једним корисником. Молим вас повежите га у поставкама вашег профила. ', + 'social_account_register_instructions' => 'Ако већ немате налог, можете регистровати налог користећи :socialAccount опцију.', + 'social_driver_not_found' => 'Друштвени прикључак није пронађен', + 'social_driver_not_configured' => 'Ваше поставке за :socialAccount нису исправно подешене.', + 'invite_token_expired' => 'Ова веза позивнице је истекла. Можете покушати да поништите лозинку вашег налога.', + 'login_user_not_found' => 'Корисник за ову радњу није могао бити пронађен.', // System - 'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.', - 'cannot_get_image_from_url' => 'Cannot get image from :url', - 'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.', - 'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.', - 'server_post_limit' => 'The server cannot receive the provided amount of data. Try again with less data or a smaller file.', - 'uploaded' => 'The server does not allow uploads of this size. Please try a smaller file size.', + 'path_not_writable' => 'На путању :filePath није се могло отпремити. Потврдите да је уписива на серверу.', + 'cannot_get_image_from_url' => 'Није могуће добити слику из :url', + 'cannot_create_thumbs' => 'Сервер не може да прави сличице. Молим вас проверите да је GD PHP проширење инсталирано.', + 'server_upload_limit' => 'Сервер не дозвољава отпремање ове величине. Молим вас покушајте са мањом датотеком.', + 'server_post_limit' => 'Сервер не може да прими достављену количину података. Покушајте поново са мање података или са мањом датотеком.', + 'uploaded' => 'Сервер не дозвољава отпремање ове величине. Молим вас покушајте са мањом датотеком.', // Drawing & Images - 'image_upload_error' => 'An error occurred uploading the image', - 'image_upload_type_error' => 'The image type being uploaded is invalid', - 'image_upload_replace_type' => 'Image file replacements must be of the same type', - 'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.', - 'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.', - 'image_gallery_thumbnail_memory_limit' => 'Failed to create gallery thumbnails due to system resource limits.', - '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_error' => 'Појавила се грешка током отпремања датотеке', + 'image_upload_type_error' => 'Тип датотеке слике која се отпрема није исправна', + 'image_upload_replace_type' => 'Датотека заменске слике мора бити истог типа', + 'image_upload_memory_limit' => 'Неуспело завршавање отпремања слика и/или прављења сличица због ограничења системских ресурса.', + 'image_thumbnail_memory_limit' => 'Није успело прављење варијација величина слике због ограничења системских ресурса.', + 'image_gallery_thumbnail_memory_limit' => 'Није успело прављење сличица галерије због ограничења системских ресурса.', + 'drawing_data_not_found' => 'Цртеж није могао бити учитан. Датотека цртежа можда више не постоји или ви можда немате дозволе да јој приступите.', // Attachments - 'attachment_not_found' => 'Attachment not found', - 'attachment_upload_error' => 'An error occurred uploading the attachment file', + 'attachment_not_found' => 'Прилог није пронађен', + 'attachment_upload_error' => 'Појавила се грешка током отпремања датотеке прилога', // Pages - 'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page', - 'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content', - 'page_custom_home_deletion' => 'Cannot delete a page while it is set as a homepage', + 'page_draft_autosave_fail' => 'Није успело чување нацрта. Потврдите да имате везу са интернетом пре снимања ове стране', + 'page_draft_delete_fail' => 'Није успело брисање нацрта стране и добављања сачуваног садржаја', + 'page_custom_home_deletion' => 'Није могуће брисање стране док је она подешена као почетна', // Entities - 'entity_not_found' => 'Entity not found', - 'bookshelf_not_found' => 'Shelf not found', + 'entity_not_found' => 'Ентитет није пронађен', + 'bookshelf_not_found' => 'Полица није пронађена', 'book_not_found' => 'Књига није пронађена', 'page_not_found' => 'Страница није пронађена', 'chapter_not_found' => 'Поглавље није пронађено', 'selected_book_not_found' => 'Одабрана књига није пронађена', - 'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found', + 'selected_book_chapter_not_found' => 'Одабрана књига или поглавље није пронађено', 'guests_cannot_save_drafts' => 'Гости не могу сачувати нацрте', // Users 'users_cannot_delete_only_admin' => 'Не можете обрисати јединог администратора', 'users_cannot_delete_guest' => 'Не можете обрисати госта', - 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', + 'users_could_not_send_invite' => 'Корисник није могао бити направљен јер позивница е-поруком није послата', // Roles 'role_cannot_be_edited' => 'Ова улога се не може мењати', 'role_system_cannot_be_deleted' => 'Ово је системска улога и не може се мењати', - 'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role', - 'role_cannot_remove_only_admin' => 'This user is the only user assigned to the administrator role. Assign the administrator role to another user before attempting to remove it here.', + 'role_registration_default_cannot_delete' => 'Ова улога се не може обрисати док је подешена као подразумевана улога за регистрацију', + 'role_cannot_remove_only_admin' => 'Овај корисник је једини коме је додељена улога администратора. Доделите ову улогу другом кориснику пре покушаја да га уклоните овде.', // Comments - 'comment_list' => 'An error occurred while fetching the comments.', - 'cannot_add_comment_to_draft' => 'You cannot add comments to a draft.', - 'comment_add' => 'An error occurred while adding / updating the comment.', - 'comment_delete' => 'An error occurred while deleting the comment.', - 'empty_comment' => 'Cannot add an empty comment.', + 'comment_list' => 'Појавила се грешка током добављања коментара.', + 'cannot_add_comment_to_draft' => 'Не можете додати коментаре нацрту.', + 'comment_add' => 'Појавила се грешка током додавања / измене коментара.', + 'comment_delete' => 'Појавила се грешка током брисања коментара.', + 'empty_comment' => 'Није могуће додати празан коментар.', // Error pages - '404_page_not_found' => 'Page Not Found', - 'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.', - 'sorry_page_not_found_permission_warning' => 'If you expected this page to exist, you might not have permission to view it.', - '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.', - 'return_home' => 'Return to home', + '404_page_not_found' => 'Страна није пронађена', + 'sorry_page_not_found' => 'Извините, страна коју сте тражили није могла бити пронађена.', + 'sorry_page_not_found_permission_warning' => 'Ако сте очекивали да ова страна постоји, можда немате дозволу да је прегледате.', + 'image_not_found' => 'Слика није пронађена', + 'image_not_found_subtitle' => 'Извините, слика коју сте тражили није могла бити пронађена.', + 'image_not_found_details' => 'Ако сте очекивали да ова слика постоји, можда је обрисана.', + 'return_home' => 'Повратак на почетну', 'error_occurred' => 'Догодила се грешка', - 'app_down' => ':appName is down right now', - 'back_soon' => 'It will be back up soon.', + 'app_down' => ':appName тренутно није дотупно', + 'back_soon' => 'Вратиће се ускоро.', // Import - 'import_zip_cant_read' => 'Could not read ZIP file.', - 'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.', - 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', - 'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.', - 'import_validation_failed' => 'Import ZIP failed to validate with errors:', - 'import_zip_failed_notification' => 'Failed to import ZIP file.', - 'import_perms_books' => 'You are lacking the required permissions to create books.', - 'import_perms_chapters' => 'You are lacking the required permissions to create chapters.', - 'import_perms_pages' => 'You are lacking the required permissions to create pages.', - 'import_perms_images' => 'You are lacking the required permissions to create images.', - 'import_perms_attachments' => 'You are lacking the required permission to create attachments.', + 'import_zip_cant_read' => 'Није се могла прочитати ZIP датотека.', + 'import_zip_cant_decode_data' => 'НИје се могла пронаћи и декодовати ZIP data.json садржај.', + 'import_zip_no_data' => 'Подаци у ZIP датотеци немају очекивани садржај књиге, поглавља или стране.', + 'import_zip_data_too_large' => 'ZIP data.json садржај превазилази максималну величину за отпремање подешену у апликацији.', + 'import_validation_failed' => 'Увоз ZIP-а није прошао потврду са овим грешкама:', + 'import_zip_failed_notification' => 'Неуспео увоз ZIP датотеке.', + 'import_perms_books' => 'Недостају вам неопходне дозволе да правите књиге.', + 'import_perms_chapters' => 'Недостају вам неопходне дозволе да правите поглавља.', + 'import_perms_pages' => 'Недостају вам неопходне дозволе да правите стране.', + 'import_perms_images' => 'Недостају вам неопходне дозволе да правите слике.', + 'import_perms_attachments' => 'Недостају вам неопходне дозволе да правите прилоге.', // API errors - 'api_no_authorization_found' => 'No authorization token found on the request', - 'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect', - 'api_user_token_not_found' => 'No matching API token was found for the provided authorization token', - 'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect', - 'api_user_no_api_permission' => 'The owner of the used API token does not have permission to make API calls', - 'api_user_token_expired' => 'The authorization token used has expired', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_no_authorization_found' => 'Није пронађен токен за ауторизацију у захтеву', + 'api_bad_authorization_format' => 'Токен за ауторизацију је пронађен у захтеву али његов формат делује неисправан', + 'api_user_token_not_found' => 'Није пронађен одговарајући API токен за достављени токен ауторизације', + 'api_incorrect_token_secret' => 'Достављена тајна за пружени коришћени API токен није исправна', + 'api_user_no_api_permission' => 'Власник коришћеног API токена нема дозволе да упућује API позиве', + 'api_user_token_expired' => 'Коришћени токен за ауторизацију је истекао', + 'api_cookie_auth_only_get' => 'Дозвољени су само GET захтеви када се користи API са аутентификацијом заснованом на колачићима', // Settings & Maintenance - 'maintenance_test_email_failure' => 'Error thrown when sending a test email:', + 'maintenance_test_email_failure' => 'Враћена је грешка током слања пробне е-поруке:', // HTTP errors - 'http_ssr_url_no_match' => 'The URL does not match the configured allowed SSR hosts', + 'http_ssr_url_no_match' => 'Адреса се не подудара са подешеном за дозвољене SSR домаћине', ]; diff --git a/lang/sr/notifications.php b/lang/sr/notifications.php index 4cc499fdd40..f25c7038d9d 100644 --- a/lang/sr/notifications.php +++ b/lang/sr/notifications.php @@ -10,13 +10,13 @@ 'new_page_intro' => 'Нова страница је креирана у :appName:', 'updated_page_subject' => 'Ажурирана страница: :pageName', 'updated_page_intro' => 'Страница је ажурирана у :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.', - 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'updated_page_debounce' => 'Да би смо спречили масовна обавештења, неко време вам неће бити слата обавештења за измене ове стране од стране истог уредника.', + 'comment_mention_subject' => 'Поменути сте у коментару на страни: :pageName', + 'comment_mention_intro' => 'Поменути сте у коментару на :appName:', 'detail_page_name' => 'Назив странице:', 'detail_page_path' => 'Путања странице:', - 'detail_commenter' => 'Commenter:', + 'detail_commenter' => 'Коментатор:', 'detail_comment' => 'Коментар:', 'detail_created_by' => 'Креирао/ла:', 'detail_updated_by' => 'Отпремио/ла:', @@ -24,6 +24,6 @@ 'action_view_comment' => 'Погледај коментар', 'action_view_page' => 'Погледај страницу', - 'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.', - 'footer_reason_link' => 'your notification preferences', + 'footer_reason' => 'Ово обавештење вам је послато зато што :link покрива ове типове активности за ову ставку.', + 'footer_reason_link' => 'ваше преференце за обавештавања', ]; diff --git a/lang/sr/pagination.php b/lang/sr/pagination.php index 85bd12fc319..ce1a32bcf08 100644 --- a/lang/sr/pagination.php +++ b/lang/sr/pagination.php @@ -6,7 +6,7 @@ */ return [ - 'previous' => '« Previous', - 'next' => 'Next »', + 'previous' => '« Претходна', + 'next' => 'Следећа »', ]; diff --git a/lang/sr/passwords.php b/lang/sr/passwords.php index b408f3c2fda..1c961f6973c 100644 --- a/lang/sr/passwords.php +++ b/lang/sr/passwords.php @@ -6,10 +6,10 @@ */ return [ - 'password' => 'Passwords must be at least eight characters and match the confirmation.', - 'user' => "We can't find a user with that e-mail address.", - 'token' => 'The password reset token is invalid for this email address.', - 'sent' => 'We have e-mailed your password reset link!', - 'reset' => 'Your password has been reset!', + 'password' => 'Лозинке морају имати најмање осам карактера и да се поклапају са потврдом.', + 'user' => "Не можемо да пронађемо корисника са том адресом е-поште.", + 'token' => 'Токен за поништавање лозинке је неисправан за ову адресу е-поште.', + 'sent' => 'Послали смо вам везу за поништавање лозинке е-поштом!', + 'reset' => 'Ваша лозинка је поништена!', ]; diff --git a/lang/sr/preferences.php b/lang/sr/preferences.php index f4459d738e4..a5b885b19b4 100644 --- a/lang/sr/preferences.php +++ b/lang/sr/preferences.php @@ -5,48 +5,48 @@ */ return [ - 'my_account' => 'My Account', + 'my_account' => 'Мој налог', - 'shortcuts' => 'Shortcuts', - 'shortcuts_interface' => 'UI Shortcut Preferences', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', - 'shortcuts_save' => 'Save Shortcuts', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', - 'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.', + 'shortcuts' => 'Пречице', + 'shortcuts_interface' => 'Подешавања пречица интерфејса', + 'shortcuts_toggle_desc' => 'Овде можете да омогућите или онемогућите пречице тастатуре интерфејса система, које се користе за навигацију и радње.', + 'shortcuts_customize_desc' => 'Можете да прилагодите сваку пречицу испод. Само притисните жељену комбинацију тастера након одабира уноса за пречицу.', + 'shortcuts_toggle_label' => 'Пречице тастатуре се омогућене', + 'shortcuts_section_navigation' => 'Навигација', + 'shortcuts_section_actions' => 'Уобичајене радње', + 'shortcuts_save' => 'Сачувај пречице', + 'shortcuts_overlay_desc' => 'Напомена: Када су пречице омогућене помоћни приказ је доступан притиском на "?" што ће нагласити доступне пречице за радње које су тренутно видљиве на екрану.', + 'shortcuts_update_success' => 'Преференце пречица су ажуриране!', + 'shortcuts_overview_desc' => 'Управљајте пречицама тастатуре које можете да користите за навигацију корисничким интерфејсом.', - 'notifications' => 'Notification Preferences', - 'notifications_desc' => 'Control the email notifications you receive when certain activity is performed within the system.', - 'notifications_opt_own_page_changes' => 'Notify upon changes to pages I own', - 'notifications_opt_own_page_comments' => 'Notify upon comments on pages I own', - 'notifications_opt_comment_mentions' => 'Notify when I\'m mentioned in a comment', - 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', - 'notifications_save' => 'Save Preferences', - 'notifications_update_success' => 'Notification preferences have been updated!', - 'notifications_watched' => 'Watched & Ignored Items', - 'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.', + 'notifications' => 'Подешавања обавештавања', + 'notifications_desc' => 'Контролишите обавештења е-поштом која добијате када се изврши одређена активност у оквиру система.', + 'notifications_opt_own_page_changes' => 'Обавести ме о изменама страница чији сам власник', + 'notifications_opt_own_page_comments' => 'Обавести ме о коментарима на странама чији сам власник', + 'notifications_opt_comment_mentions' => 'Обавести ме када сам поменут у коментару', + 'notifications_opt_comment_replies' => 'Обавести ме о одговорима на моје коментаре', + 'notifications_save' => 'Сачувај подешавања', + 'notifications_update_success' => 'Преференце обавештења су ажуриране!', + 'notifications_watched' => 'Праћене и игнорисане ставке', + 'notifications_watched_desc' => 'Испод су ставке над којима су примењене прилагођене преференце праћења. Да би сте ажурирали ваше преференце за ове, погледајте ставку па затим пронађите опције праћења у траци са стране.', - 'auth' => 'Access & Security', - 'auth_change_password' => 'Change Password', - 'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.', - 'auth_change_password_success' => 'Password has been updated!', + 'auth' => 'Приступ и безбедност', + 'auth_change_password' => 'Промени лозинку', + 'auth_change_password_desc' => 'Промените лозинку коју користите за пријављивање у апликацију. Она мора бити дугачка најмање 8 карактера.', + 'auth_change_password_success' => 'Лозинка је ажурирана!', - 'profile' => 'Profile Details', - 'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.', - 'profile_view_public' => 'View Public Profile', - 'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.', - 'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.', - 'profile_email_no_permission' => 'Unfortunately you don\'t have permission to change your email address. If you want to change this, you\'d need to ask an administrator to change this for you.', - 'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.', - 'profile_admin_options' => 'Administrator Options', - 'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the "Settings > Users" area of the application.', + 'profile' => 'Детаљи о профилу', + 'profile_desc' => 'Управљајте детаљима вашег налога чиме се представљате другим корисницима, поред детаља који се користе за комункацију и персонализацију система.', + 'profile_view_public' => 'Погледај јавни профил', + 'profile_name_desc' => 'Подесите ваше име за приказ које је видљво другим корисницима у систему кроз активности које извршавате и садржај који је у вашем власништу.', + 'profile_email_desc' => 'Ова е-пошта ће се користити за обавештења и, у зависности од система аутентификације, прступ систему.', + 'profile_email_no_permission' => 'На жалост немате дозволу да мењате адресу ваше е-поште. Ако желите да је промените, мораћете да замолите администратора да то уради уместо вас.', + 'profile_avatar_desc' => 'Изаберите слику која ће се користити да се представљате другима у систему. Идеално би требала бити четвртаста и око 256px у ширини и висини.', + 'profile_admin_options' => 'Администраторске опције', + 'profile_admin_options_desc' => 'Додатне опције администраторског нивоа, попут оних за управљање доделама улога, се могу пронаћи у корисничком налогу под опцијом "Поставке > Корисници".', - 'delete_account' => 'Delete Account', - 'delete_my_account' => 'Delete My Account', - 'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\'ve created, such as created pages and uploaded images, will remain.', - 'delete_my_account_warning' => 'Are you sure you want to delete your account?', + 'delete_account' => 'Обриши налог', + 'delete_my_account' => 'Обриши мој налог', + 'delete_my_account_desc' => 'Ово ће у потпуности обисати ваш кориснички налог из система. Нећете моћи да повратите овај налог или да поништите ову радњу. Садржај који сте направили, као што су странице и отпремљене слике, ће остати.', + 'delete_my_account_warning' => 'Да ли заиста желите да обришете ваш налог?', ]; diff --git a/lang/sr/settings.php b/lang/sr/settings.php index 143fdaef19e..d962a358d2d 100644 --- a/lang/sr/settings.php +++ b/lang/sr/settings.php @@ -7,24 +7,24 @@ return [ // Common Messages - 'settings' => 'Подешавања', - 'settings_save' => 'Сачувај подешавања', + 'settings' => 'Поставке', + 'settings_save' => 'Сачувај поставке', 'system_version' => 'Верзија система', 'categories' => 'Категорије', // App Settings - 'app_customization' => 'Прилгођавање', + 'app_customization' => 'Прилагођавање', 'app_features_security' => 'Својства и сигурност', 'app_name' => 'Назив апликације', 'app_name_desc' => 'Ово име се приказује у заглављу и у свим системским порукама е-поште.', 'app_name_header' => 'Прикажи назив у заглављу', - 'app_public_access' => 'Javni pristup', - 'app_public_access_desc' => 'Омогућавање ове опције ће омогућити посетиоцима, који нису пријављени, да приступе садржају у вашој Боокстак инстанци.', + 'app_public_access' => 'Јавни приступ', + 'app_public_access_desc' => 'Омогућавање ове опције ће омогућити посетиоцима, који нису пријављени, да приступе садржају у вашој Букстек инстанци.', 'app_public_access_desc_guest' => 'Приступ за јавне посетиоце може се контролисати преко корисника „Гост“.', 'app_public_access_toggle' => 'Дозволи јавни приступ', 'app_public_viewing' => 'Дозволити јавно гледање?', - 'app_secure_images' => 'Веће безбедност отпремања слика', - 'app_secure_images_toggle' => 'Омогућите већу безбедност отпремања слика', + 'app_secure_images' => 'Већа безбедност при отпремању слика', + 'app_secure_images_toggle' => 'Омогући већу безбедност при отпремању слика', 'app_secure_images_desc' => 'Из разлога перформанси, све слике су јавне. Ова опција додаје насумичан низ који је тешко погодити испред Урл-ова слике. Уверите се да индекси директоријума нису омогућени да бисте спречили лак приступ.', 'app_default_editor' => 'Подразумевани уређивач страница', 'app_default_editor_desc' => 'Изаберите који уређивач ће се подразумевано користити приликом уређивања нових страница. Ово се може заменити на нивоу странице где дозволе дозвољавају.', @@ -75,36 +75,36 @@ 'reg_confirm_restrict_domain_placeholder' => 'Нема постављених ограничења', // Sorting Settings - 'sorting' => 'Lists & Sorting', - 'sorting_book_default' => 'Default Book Sort Rule', - 'sorting_book_default_desc' => 'Select the default sort rule to apply to new books. This won\'t affect existing books, and can be overridden per-book.', - 'sorting_rules' => 'Sort Rules', - 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', - 'sort_rule_assigned_to_x_books' => 'Assigned to :count Book|Assigned to :count Books', - 'sort_rule_create' => 'Create Sort Rule', - 'sort_rule_edit' => 'Edit Sort Rule', - 'sort_rule_delete' => 'Delete Sort Rule', - 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', - 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?', - 'sort_rule_details' => 'Sort Rule Details', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', - 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', - 'sort_rule_available_operations' => 'Available Operations', - 'sort_rule_available_operations_empty' => 'No operations remaining', - 'sort_rule_configured_operations' => 'Configured Operations', - 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', - 'sort_rule_op_asc' => '(Asc)', - 'sort_rule_op_desc' => '(Desc)', - 'sort_rule_op_name' => 'Name - Alphabetical', - 'sort_rule_op_name_numeric' => 'Name - Numeric', - 'sort_rule_op_created_date' => 'Created Date', - 'sort_rule_op_updated_date' => 'Updated Date', - 'sort_rule_op_chapters_first' => 'Chapters First', - 'sort_rule_op_chapters_last' => 'Chapters Last', - 'sorting_page_limits' => 'Per-Page Display Limits', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting' => 'Спискови и разврставање', + 'sorting_book_default' => 'Подразумевано правило разврставања', + 'sorting_book_default_desc' => 'Одаберите подразумевано правило разврставање за нове књиге. Ово неће утицати на постојеће књиге и може бити прерађено за сваку књигу.', + 'sorting_rules' => 'Правила разврставања', + 'sorting_rules_desc' => 'Ово су предефинисане операције разврставања које се могу применити на садржај у систему.', + 'sort_rule_assigned_to_x_books' => 'Додељено :count књизи|Додељено :count књигама', + 'sort_rule_create' => 'Направи правило разврставања', + 'sort_rule_edit' => 'Измени правило разврставања', + 'sort_rule_delete' => 'Обриши правило разврставања', + 'sort_rule_delete_desc' => 'Уклоните ово правило разврставања из система. Књиге које користе ово разврставање ће се вратити на ручно разврставање.', + 'sort_rule_delete_warn_books' => 'Ово правило разврставања тренутно користе :count књиге. Да ли заиста желите да обришете ово?', + 'sort_rule_delete_warn_default' => 'Ово правило разврставања се тренутно користи као подразумевано за књиге. Да ли заиста желите да обришете ово?', + 'sort_rule_details' => 'Детаљи правила разврставања', + 'sort_rule_details_desc' => 'Подесите назив за ово правило, које ће се појавити у списковима када корисници бирају разврставање.', + 'sort_rule_operations' => 'Операције разврставања', + 'sort_rule_operations_desc' => 'Конфигуришите радње разврставања за извршавање њиховим премештањем из списка доступних операција. Након употребе, операције ће се применити редом, од врха ка дну. Све промене извршене овде ће се применити на свим додељеним књигама након снимања.', + 'sort_rule_available_operations' => 'Доступне операције', + 'sort_rule_available_operations_empty' => 'Нема преосталих операција', + 'sort_rule_configured_operations' => 'Подешене операције', + 'sort_rule_configured_operations_empty' => 'Превуците/додајте операције са списка "Доступне операције"', + 'sort_rule_op_asc' => '(раст)', + 'sort_rule_op_desc' => '(опад)', + 'sort_rule_op_name' => 'Назив - азбучно', + 'sort_rule_op_name_numeric' => 'Назив - нумеричко', + 'sort_rule_op_created_date' => 'Датум прављења', + 'sort_rule_op_updated_date' => 'Датум ажурирања', + 'sort_rule_op_chapters_first' => 'Прво поглавља', + 'sort_rule_op_chapters_last' => 'Последње поглавља', + 'sorting_page_limits' => 'Ограничења приказа по страници', + 'sorting_page_limits_desc' => 'Подесите колико ставки се приказује по страни на разним списковима у систему. Типично мањи број ће пружити боље перформансе, док ће већи избећи потребу листање вишеструко страна. Препоручује се коришћење броја дељивог са 6.', // Maintenance settings 'maint' => 'Одржавање', @@ -113,68 +113,68 @@ 'maint_delete_images_only_in_revisions' => 'Такође избришите слике које постоје само у старим ревизијама странице', 'maint_image_cleanup_run' => 'Покрени чишћење', 'maint_image_cleanup_warning' => ':count пронађене су потенцијално неискоришћене слике. Да ли сте сигурни да желите да избришете ове слике?', - 'maint_image_cleanup_success' => ':count potentially unused images found and deleted!', - 'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!', - 'maint_send_test_email' => 'Send a Test Email', - 'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.', - 'maint_send_test_email_run' => 'Send test email', - 'maint_send_test_email_success' => 'Email sent to :address', - 'maint_send_test_email_mail_subject' => 'Test Email', - 'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!', - 'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.', - 'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.', - 'maint_recycle_bin_open' => 'Open 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_image_cleanup_success' => ':count потенцијално некоришћених слика је пронађено и обрисано!', + 'maint_image_cleanup_nothing_found' => 'Нису пронађене некоришћене слике. Ништа није обрисано!', + 'maint_send_test_email' => 'Пошаљи пробну е-поруку', + 'maint_send_test_email_desc' => 'Ово шаље пробну е-поруку на вашу адресу е-поште наведену у вашем профилу.', + 'maint_send_test_email_run' => 'Пошаљи пробну е-поруку', + 'maint_send_test_email_success' => 'Е-порука послата на :address', + 'maint_send_test_email_mail_subject' => 'Пробна порука', + 'maint_send_test_email_mail_greeting' => 'Чини се да достава е-порука функционише!', + 'maint_send_test_email_mail_text' => 'Честитамо! С обзиром да сте добили ово обавештење е-поруком, чини се да су ваше поставке исправно подешене.', + 'maint_recycle_bin_desc' => 'Обрисане полице, књиге, поглављи и стране се шаљу у канту за отпатке да би се могле повратити или трајно обрисати. Старије ставке у канти могу се аутоматски уклонити након неког времена у зависности од подешавања система.', + 'maint_recycle_bin_open' => 'Отвори канту', + 'maint_regen_references' => 'Регенериши референце', + 'maint_regen_references_desc' => 'Ова радња ће поново изградити индекс референце међу ставкама унутар базе података. Ово се обчно решава аутоматски али ова радња може бити корисна за индексацију старог садржаја или садржаја додатог кроз незваничне начине.', + 'maint_regen_references_success' => 'Индекс референци је регенерисан!', + 'maint_timeout_command_note' => 'Напомена: Овој радњи треба времена да се изврши, што може довести до проблема са истеком времена чекања у неким веб окружењима. Као алтернатива, ова радња се може извршити користећи команду у терминалу.', // Recycle Bin - 'recycle_bin' => 'Recycle Bin', - 'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'recycle_bin_deleted_item' => 'Deleted Item', - 'recycle_bin_deleted_parent' => 'Parent', - 'recycle_bin_deleted_by' => 'Deleted By', - 'recycle_bin_deleted_at' => 'Deletion Time', - 'recycle_bin_permanently_delete' => 'Permanently Delete', - 'recycle_bin_restore' => 'Restore', - 'recycle_bin_contents_empty' => 'The recycle bin is currently empty', - 'recycle_bin_empty' => 'Empty Recycle Bin', - 'recycle_bin_empty_confirm' => 'This will permanently destroy all items in the recycle bin including content contained within each item. Are you sure you want to empty the recycle bin?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', - 'recycle_bin_destroy_list' => 'Items to be Destroyed', - 'recycle_bin_restore_list' => 'Items to be Restored', - 'recycle_bin_restore_confirm' => 'This action will restore the deleted item, including any child elements, to their original location. If the original location has since been deleted, and is now in the recycle bin, the parent item will also need to be restored.', - 'recycle_bin_restore_deleted_parent' => 'The parent of this item has also been deleted. These will remain deleted until that parent is also restored.', - 'recycle_bin_restore_parent' => 'Restore Parent', - 'recycle_bin_destroy_notification' => 'Deleted :count total items from the recycle bin.', - 'recycle_bin_restore_notification' => 'Restored :count total items from the recycle bin.', + 'recycle_bin' => 'Канта за отпатке', + 'recycle_bin_desc' => 'Одавде можете да вратите ставке које су обрисане или изабрати да их трајно уклоните из система. Овај списак није филтриран за разлику од сличних слискова активности у систему где су примењени филтери дозвола.', + 'recycle_bin_deleted_item' => 'Обрисана ставка', + 'recycle_bin_deleted_parent' => 'Родитељ', + 'recycle_bin_deleted_by' => 'Избрисао', + 'recycle_bin_deleted_at' => 'Време брисања', + 'recycle_bin_permanently_delete' => 'Обриши трајно', + 'recycle_bin_restore' => 'Поврати', + 'recycle_bin_contents_empty' => 'Канта је тренутно празна', + 'recycle_bin_empty' => 'Испразни канту', + 'recycle_bin_empty_confirm' => 'Ово ће трајно уништити све ставке у канти укључујући садржај унутар сваке ставке. Да ли заиста желите да испразните канту за отпатке?', + 'recycle_bin_destroy_confirm' => 'Ова радња ће трајно обрисати ову ставку из система, заједно са наследним елементима наведеним испод, и нећете моћи да вратите садржај. Да ли заиста желите да трајно обришете ову ставку?', + 'recycle_bin_destroy_list' => 'Ставке за уништавање', + 'recycle_bin_restore_list' => 'Ставке за опоравак', + 'recycle_bin_restore_confirm' => 'Ова радња ће вратити обрисану ставку, укључујући наследне елементе , на њихову оригиналну локацију. Ако је оригинална локација од тада обрисана, и сада се налази у канти за отпатке, родитељска ставка се такође мора вратити.', + 'recycle_bin_restore_deleted_parent' => 'Раодитељ ове ставке је такође обрисан. Ово ће остати обрисано док се не врати тај родитељ..', + 'recycle_bin_restore_parent' => 'Врати родитеља', + 'recycle_bin_destroy_notification' => 'Обрисано :count ставки укупно из канте за отпатке.', + 'recycle_bin_restore_notification' => 'Враћено :count ставки укупно из канте за отпатке.', // Audit Log - 'audit' => 'Audit Log', - 'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.', - 'audit_event_filter' => 'Event Filter', - 'audit_event_filter_no_filter' => 'No Filter', + 'audit' => 'Запис за ревизију', + 'audit_desc' => 'Овај запис за ревизију приказује списак активности које се прате усистему. Овај списак није филтриран за разлику од сличних слискова активности у систему где су примењени филтери дозвола.', + 'audit_event_filter' => 'Филтер догађаја', + 'audit_event_filter_no_filter' => 'Без филтера', 'audit_deleted_item' => 'Избрисана ставка', - 'audit_deleted_item_name' => 'Name: :name', + 'audit_deleted_item_name' => 'Назив: :name', 'audit_table_user' => 'Корисник', 'audit_table_event' => 'Догађај', - 'audit_table_related' => 'Related Item or Detail', + 'audit_table_related' => 'Повезана ставка или детаљ', 'audit_table_ip' => 'ИП адреса', 'audit_table_date' => 'Датум активности', - 'audit_date_from' => 'Date Range From', - 'audit_date_to' => 'Date Range To', + 'audit_date_from' => 'Опсег датума од', + 'audit_date_to' => 'Опсег датума до', // Role Settings 'roles' => 'Улоге', - 'role_user_roles' => 'User Roles', - 'roles_index_desc' => 'Roles are used to group users & provide system permission to their members. When a user is a member of multiple roles the privileges granted will stack and the user will inherit all abilities.', - 'roles_x_users_assigned' => ':count user assigned|:count users assigned', - 'roles_x_permissions_provided' => ':count permission|:count permissions', - 'roles_assigned_users' => 'Assigned Users', - 'roles_permissions_provided' => 'Provided Permissions', - 'role_create' => 'Create New Role', - 'role_delete' => 'Delete Role', + 'role_user_roles' => 'Корисничке улоге', + 'roles_index_desc' => 'Улоге се користе да би се груписали корисници и доделиле дозволе за систем њиховим члановима. Када је корисник члан вишеструко група привилегије додељене ће се објединити и корисник ће наследити све способности.', + 'roles_x_users_assigned' => ':count корисник додељен|:count корисника додељено', + 'roles_x_permissions_provided' => ':count дозвола|:count дозвола', + 'roles_assigned_users' => 'Додељени корисници', + 'roles_permissions_provided' => 'Пружене дозволе', + 'role_create' => 'Направи нову улогу', + 'role_delete' => 'Обриши улогу', 'role_delete_confirm' => 'Ово ће избрисати улогу са именом \':roleName\'.', 'role_delete_users_assigned' => 'Ова улога има :userCount корисника који су јој додељени. Ако желите да мигрирате кориснике са ове улоге, изаберите нову улогу испод.', 'role_delete_no_migration' => "Немојте мигрирати кориснике", @@ -184,145 +184,145 @@ 'role_name' => 'Назив улоге', 'role_desc' => 'Кратак опис улоге', 'role_mfa_enforced' => 'Захтева вишефакторску аутентификацију', - 'role_external_auth_id' => 'External Authentication IDs', - 'role_system' => 'System Permissions', - 'role_manage_users' => 'Manage users', - 'role_manage_roles' => 'Manage roles & role permissions', - 'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions', - 'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages', - 'role_manage_page_templates' => 'Manage page templates', - 'role_access_api' => 'Access system API', - 'role_manage_settings' => 'Manage app settings', - 'role_export_content' => 'Export content', - 'role_import_content' => 'Import content', - 'role_editor_change' => 'Change page editor', - 'role_notifications' => 'Receive & manage notifications', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', - '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.', - 'role_asset_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.', - '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_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', - 'role_all' => 'All', - 'role_own' => 'Own', - 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', - 'role_save' => 'Save Role', - 'role_users' => 'Users in this role', - 'role_users_none' => 'No users are currently assigned to this role', + 'role_external_auth_id' => 'ID-јеви екстерне аутентификације', + 'role_system' => 'Системске дозволе', + 'role_manage_users' => 'Управља корисницима', + 'role_manage_roles' => 'Управља улогама и дозволама улога', + 'role_manage_entity_permissions' => 'Управља дозволама над свим елементима', + 'role_manage_own_entity_permissions' => 'Управља дозволама над сопственим елементима', + 'role_manage_page_templates' => 'Управља шаблонима страна', + 'role_access_api' => 'Приступа системском API-ју', + 'role_manage_settings' => 'Управља поставкама апликације', + 'role_export_content' => 'Извози садржај', + 'role_import_content' => 'Увози садржај', + 'role_editor_change' => 'Мења уређивач стране', + 'role_notifications' => 'Прима и управља обавештењима', + 'role_permission_note_users_and_roles' => 'Ове дозволе ће технички такође омогућити видљивост и претрагу корисника и улога у систему.', + 'role_asset' => 'Дозволе над имовином', + 'roles_system_warning' => 'Имајте на уму да приступ било којој од дозвола изнад може да дозволи кориснику да измени сопствене привилегије или привилегије других у систему. Доделите улоге са овим дозволама само корисницима од поверења.', + 'role_asset_desc' => 'Ове дозволе конторлишу подразумевани приступ имовини унутар система. Дозволе над књигама, поглављима и странама ће прерадити ове дозволе.', + 'role_asset_admins' => 'Администраторима је аутоматски дат приступ целом садржају али ове опције могу да прикажу или сакрију опције на интерфејсу.', + 'role_asset_image_view_note' => 'Ово се односи на видљивост унутар менаџера слика. Сам приступ отпремљеним сликама зависиће од системских опције складиштења слика.', + 'role_asset_users_note' => 'Ове дозволе ће технички такође омогућити видљивост и претрагу корисника у систему.', + 'role_all' => 'Све', + 'role_own' => 'Власник', + 'role_controlled_by_asset' => 'Контролисано по имовини у којој су постављене', + 'role_controlled_by_page_delete' => 'Контролисано дозволама брисања од странице', + 'role_save' => 'Сачувај улогу', + 'role_users' => 'Корисници са овом улогом', + 'role_users_none' => 'Тренутно ниједном кориснику није додељена ова улога', // Users - 'users' => 'Users', - 'users_index_desc' => 'Create & manage individual user accounts within the system. User accounts are used for login and attribution of content & activity. Access permissions are primarily role-based but user content ownership, among other factors, may also affect permissions & access.', - 'user_profile' => 'User Profile', - 'users_add_new' => 'Add New User', - 'users_search' => 'Search Users', - 'users_latest_activity' => 'Latest Activity', - 'users_details' => 'User Details', - 'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.', - 'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.', - 'users_role' => 'User Roles', - 'users_role_desc' => 'Select which roles this user will be assigned to. If a user is assigned to multiple roles the permissions from those roles will stack and they will receive all abilities of the assigned roles.', - 'users_password' => 'User Password', - 'users_password_desc' => 'Set a password used to log-in to the application. This must be at least 8 characters long.', - 'users_send_invite_text' => 'You can choose to send this user an invitation email which allows them to set their own password otherwise you can set their password yourself.', - 'users_send_invite_option' => 'Send user invite email', - 'users_external_auth_id' => 'External Authentication ID', - 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', - 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', - 'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', - 'users_delete' => 'Delete User', - 'users_delete_named' => 'Delete user :userName', - 'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', - 'users_delete_confirm' => 'Are you sure you want to delete this user?', - 'users_migrate_ownership' => 'Migrate Ownership', - 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.', - 'users_none_selected' => 'No user selected', - 'users_edit' => 'Edit User', - 'users_edit_profile' => 'Edit Profile', - 'users_avatar' => 'User Avatar', - 'users_avatar_desc' => 'Select an image to represent this user. This should be approx 256px square.', - 'users_preferred_language' => 'Preferred Language', - 'users_preferred_language_desc' => 'This option will change the language used for the user-interface of the application. This will not affect any user-created content.', - 'users_social_accounts' => 'Social Accounts', - 'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.', - 'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not revoke previously authorized access. Revoke access from your profile settings on the connected social account.', - 'users_social_connect' => 'Connect Account', - 'users_social_disconnect' => 'Disconnect Account', - 'users_social_status_connected' => 'Connected', - 'users_social_status_disconnected' => 'Disconnected', - 'users_social_connected' => ':socialAccount account was successfully attached to your profile.', - 'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', - 'users_api_tokens' => 'API Tokens', - 'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.', - 'users_api_tokens_none' => 'No API tokens have been created for this user', - 'users_api_tokens_create' => 'Create Token', - 'users_api_tokens_expires' => 'Expires', - 'users_api_tokens_docs' => 'API Documentation', - '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_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users' => 'Корисници', + 'users_index_desc' => 'Правите и управљајте појединачним налозима корисника унутар система. Кориснички налози се користе за пријављивање и приписивање садржаја и активности. Дозволе за приступ су примарно базиране на улогама али власништо над корисничким садржајем, између осталог, такође може да утиче на дозволе и приступ.', + 'user_profile' => 'Кориснички профил', + 'users_add_new' => 'Додај новог корисника', + 'users_search' => 'Претражи кориснике', + 'users_latest_activity' => 'Последња активност', + 'users_details' => 'Детаљи о кориснику', + 'users_details_desc' => 'Подесите име за приказ и адресу е-поште за овог корисника. Ова адреса е-поште ће се користити за пријављивање у апликацију.', + 'users_details_desc_no_email' => 'Подесите име за приказ за овог корисника како би други могли да га препознају.', + 'users_role' => 'Корисничке улоге', + 'users_role_desc' => 'Одаберите које улоге ће бити додељене кориснику. Ако је кориснику додељено више улога, дозволе од тих улога ће се сакупити и они ће добити све дозволе додељених улога.', + 'users_password' => 'Корисничка лозинка', + 'users_password_desc' => 'Подесите лозинку која се користи за пријављивање у апликацију. Она мора имати најмање 8 карактера.', + 'users_send_invite_text' => 'Можете изабрати да овом кориснику пошаљете позивницу е-поруком што ће им омогућити да самостално подесе своју лозинку. У супротном можете је сами подесити.', + 'users_send_invite_option' => 'Пошаљи кориснику позивницу е-поруком', + 'users_external_auth_id' => 'ID екстерне аутентификације', + 'users_external_auth_id_desc' => 'Када се користи екстерни систем за аутентификацију (попут SAML2, OIDC или LDAP) ово је ID који повезује овог BookStack корисника са налогом система за аутентфикацију. Можете игнорисати ово поље ако користите подразумевану аутентификацију засновану на е-пошти.', + 'users_password_warning' => 'Попуните поља испод само ако желите да промените лозинку овом кориснику.', + 'users_system_public' => 'Овај корисник представља сваког госта који посећује вашу инстанцу. Не може се користити за пријављивање али је аутоматски додељен.', + 'users_delete' => 'Обриши корисника', + 'users_delete_named' => 'Обриши корисника :userName', + 'users_delete_warning' => 'Ово ће у потпуности да обрише овог корисника са именом \':userName\' из система.', + 'users_delete_confirm' => 'Да ли заиста желите да обришете овог корисника?', + 'users_migrate_ownership' => 'Миграција власништва', + 'users_migrate_ownership_desc' => 'Овде одаберите корисника ако желите да други корисник постане власник свих ставки за које је власник овај корисник.', + 'users_none_selected' => 'Корисник није изабран', + 'users_edit' => 'Измени корисника', + 'users_edit_profile' => 'Измена профила', + 'users_avatar' => 'Аватар корисника', + 'users_avatar_desc' => 'Изаберите слику која ће да представља овог корисника. Требала би бити квадрат приближно 256px.', + 'users_preferred_language' => 'Преферирани језик', + 'users_preferred_language_desc' => 'Ова опција ће променити језик који се користи за кориснички интерфејс апликације. Ово неће утицати на било какав садржај направљен од стране корисника.', + 'users_social_accounts' => 'Налози друштвених мрежа', + 'users_social_accounts_desc' => 'Погледајте статус повезаних налога друштвених мрежа за овог корисника. Налози друштвених мрежа се могу користити за приступ систему поред примарног система за аутентификацију.', + 'users_social_accounts_info' => 'Овде можете да повежете ваше друге налоге за бржу и лакшу пријаву. Развезивање налога овде не укида претходно ауторизован приступ. Повуците приступ из поставки вашег профила на повезаном налогу друштвене мреже.', + 'users_social_connect' => 'Повежи налог', + 'users_social_disconnect' => 'Развежи налог', + 'users_social_status_connected' => 'Повезан', + 'users_social_status_disconnected' => 'Развезан', + 'users_social_connected' => ':socialAccount налог је успешно закачен на ваш профил.', + 'users_social_disconnected' => ':socialAccount налог је успешно развезан са вашег профила.', + 'users_api_tokens' => 'API токени', + 'users_api_tokens_desc' => 'Направљај и управљај токенима за приступ који се користе за аутентификацију са BookStack REST API-јем. Дозволама за API се управља кроз корисника којем припада токен.', + 'users_api_tokens_none' => 'Још ниједан API токен није направљен за овог корисника', + 'users_api_tokens_create' => 'Направи токен', + 'users_api_tokens_expires' => 'Истиче', + 'users_api_tokens_docs' => 'API документација', + 'users_mfa' => 'Вишефакторска аутентификација', + 'users_mfa_desc' => 'Подесите вишефакторску аутентификацију ка додатни слој безбедности за ваш кориснички налог.', + 'users_mfa_x_methods' => ':count начин подешен|:count начина су подешена', + 'users_mfa_configure' => 'Подеси начине', + 'users_mfa_reset' => 'Поништи начине вишефакторске аутентификације', + 'users_mfa_reset_desc' => 'Ово ће понитити и почистити све подешене начине вишефакторске аутентификације за овог корисника. Ако је вишефакторска аутентификација неопходна за било коју од његових улога, од њих ће бити затражено да подесе нови начин приликом наредне пријаве.', + 'users_mfa_reset_confirm' => 'Да ли заиста желите да поништите вишефакторску аутентификацију за овог корисника?', // API Tokens - 'user_api_token_create' => 'Create API Token', - 'user_api_token_name' => 'Name', - 'user_api_token_name_desc' => 'Give your token a readable name as a future reminder of its intended purpose.', - 'user_api_token_expiry' => 'Expiry Date', - 'user_api_token_expiry_desc' => 'Set a date at which this token expires. After this date, requests made using this token will no longer work. Leaving this field blank will set an expiry 100 years into the future.', - 'user_api_token_create_secret_message' => 'Immediately after creating this token a "Token ID" & "Token Secret" will be generated and displayed. The secret will only be shown a single time so be sure to copy the value to somewhere safe and secure before proceeding.', - 'user_api_token' => 'API Token', - 'user_api_token_id' => 'Token ID', - 'user_api_token_id_desc' => 'This is a non-editable system generated identifier for this token which will need to be provided in API requests.', - 'user_api_token_secret' => 'Token Secret', - 'user_api_token_secret_desc' => 'This is a system generated secret for this token which will need to be provided in API requests. This will only be displayed this one time so copy this value to somewhere safe and secure.', - 'user_api_token_created' => 'Token created :timeAgo', - 'user_api_token_updated' => 'Token updated :timeAgo', - 'user_api_token_delete' => 'Delete Token', - 'user_api_token_delete_warning' => 'This will fully delete this API token with the name \':tokenName\' from the system.', - 'user_api_token_delete_confirm' => 'Are you sure you want to delete this API token?', + 'user_api_token_create' => 'Направи API токен', + 'user_api_token_name' => 'Назив', + 'user_api_token_name_desc' => 'Дајте вашем токену читљив назив као будући подсетник његове сврхе.', + 'user_api_token_expiry' => 'Датум истека', + 'user_api_token_expiry_desc' => 'Подесите датум када истиче ваш токен. Након овог датума, захтеви послати користећи овај токен више неће функционисати. Остављањем овог поља празним ће подесити истек 100 година у будућности.', + 'user_api_token_create_secret_message' => 'Моментално након прављења овог токена "ID токена" и "Тајна токена" ће бити генерисани и приказани. Тајна ће бити приказана само једном зато се потрудите да ископирате вредност на неко сигурно и безбедно место пре настављања.', + 'user_api_token' => 'API токен', + 'user_api_token_id' => 'ID токена', + 'user_api_token_id_desc' => 'Ово је неизменљиви идентификатор који је генерисао систем за овај токен којег треба доставити у API захтевима.', + 'user_api_token_secret' => 'Тајна токена', + 'user_api_token_secret_desc' => 'Ово је тајна коју је генерисао систем за овај токен коју треба доставити у API захтевима. Ово ће бити приказано само једном зато ископирајте ову вредност на неко сигурно и безбедно место.', + 'user_api_token_created' => 'Токен је направљен :timeAgo', + 'user_api_token_updated' => 'Токен је ажуриран :timeAgo', + 'user_api_token_delete' => 'Обриши токен', + 'user_api_token_delete_warning' => 'Ово ће у потпуности обрисати овај API токен са називом \':tokenName\' из система.', + 'user_api_token_delete_confirm' => 'Да ли заиста желите да обришете овај API токен?', // Webhooks - 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => ':count trigger event|:count trigger events', - 'webhooks_create' => 'Create New Webhook', - 'webhooks_none_created' => 'No webhooks have yet been created.', - 'webhooks_edit' => 'Edit Webhook', - 'webhooks_save' => 'Save Webhook', - 'webhooks_details' => 'Webhook Details', - 'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.', - 'webhooks_events' => 'Webhook Events', - 'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.', - 'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.', - 'webhooks_events_all' => 'All system events', - 'webhooks_name' => 'Webhook Name', - 'webhooks_timeout' => 'Webhook Request Timeout (Seconds)', - 'webhooks_endpoint' => 'Webhook Endpoint', - 'webhooks_active' => 'Webhook Active', - 'webhook_events_table_header' => 'Events', - 'webhooks_delete' => 'Delete Webhook', - 'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.', - 'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?', - 'webhooks_format_example' => 'Webhook Format Example', - 'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.', - 'webhooks_status' => 'Webhook Status', - 'webhooks_last_called' => 'Last Called:', - 'webhooks_last_errored' => 'Last Errored:', - 'webhooks_last_error_message' => 'Last Error Message:', + 'webhooks' => 'Веб закачке', + 'webhooks_index_desc' => 'Веб закачке су начин да се пошаљу подаци на екстерне УРЛ адресе када се одређене радње и догађаји одвију унутар система који дозвољава интеграцију засновану на догађајима са екстерним платформама као што су размена порука или системи за обавештавање.', + 'webhooks_x_trigger_events' => ':count окидач догађаја|:count окидача догађаја', + 'webhooks_create' => 'Направи нову веб закачку', + 'webhooks_none_created' => 'Још нису направљене веб закачке.', + 'webhooks_edit' => 'Измени веб закачку', + 'webhooks_save' => 'Сачувај веб закачку', + 'webhooks_details' => 'Детаљи веб закачке', + 'webhooks_details_desc' => 'Пружите одговарајући назив и POST крајњу тачку којој слати податке ове веб закачке.', + 'webhooks_events' => 'Догађаји веб закачке', + 'webhooks_events_desc' => 'Изаберите све догађаје који требају да окину позив за ову веб закачку.', + 'webhooks_events_warning' => 'Имајте на уму да ће ови догађаји бити окинути за све изабране догађаје, чак и када су примењене прилагођене дозволе. Обезбедите да коришћење ове веб закачке неће изложити поверљив садржај.', + 'webhooks_events_all' => 'Сви догађаји система', + 'webhooks_name' => 'Назив веб закачке', + 'webhooks_timeout' => 'Време чекања на веб закачку (секунде)', + 'webhooks_endpoint' => 'Крајња тачка веб закачке', + 'webhooks_active' => 'Веб закачка је активна', + 'webhook_events_table_header' => 'Догађаји', + 'webhooks_delete' => 'Обриши веб закачку', + 'webhooks_delete_warning' => 'Ово ће у потпуности обрисати ову веб закачку са називом \':webhookName\' из система.', + 'webhooks_delete_confirm' => 'Да ли заиста желите да обришете ову веб закачку?', + 'webhooks_format_example' => 'Пример формата веб закачке', + 'webhooks_format_example_desc' => 'Податак веб закачке се шаље као POST захтев подешеној крајњој тачки као JSON пратећи формат испод. "related_item" и "url" својства су опциона и зависиће од типе окинутог догађаја.', + 'webhooks_status' => 'Статус веб закачке', + 'webhooks_last_called' => 'Последњи пут позвана:', + 'webhooks_last_errored' => 'Последња грешка:', + 'webhooks_last_error_message' => 'Порука последње грешке:', // Licensing - 'licenses' => 'Licenses', - 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', - 'licenses_bookstack' => 'BookStack License', - 'licenses_php' => 'PHP Library Licenses', - 'licenses_js' => 'JavaScript Library Licenses', - 'licenses_other' => 'Other Licenses', - 'license_details' => 'License Details', + 'licenses' => 'Лиценце', + 'licenses_desc' => 'Ова страна приказује детаљне информације о лиценци за BookStack поред самих пројеката и библиотека које се користе унутар BookStack-а. Многи пројекти наведени могу се користити само у току развоја.', + 'licenses_bookstack' => 'BookStack лиценца', + 'licenses_php' => 'Лиценца PHP библиотеке', + 'licenses_js' => 'Лиценце JavaScript библиотеке', + 'licenses_other' => 'Друге лиценце', + 'license_details' => 'Детаљи о лиценци', //! If editing translations files directly please ignore this in all //! languages apart from en. Content will be auto-copied from en. @@ -366,8 +366,9 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', - 'th' => 'ภาษาไทย', + 'th' => 'Тајландски', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/sr/validation.php b/lang/sr/validation.php index 6770c5a8015..35e6c706f93 100644 --- a/lang/sr/validation.php +++ b/lang/sr/validation.php @@ -8,113 +8,113 @@ return [ // Standard laravel validation lines - 'accepted' => 'The :attribute must be accepted.', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'accepted' => ':attribute мора бити прихваћен.', + 'active_url' => ':attribute није исправна URL адреса.', + 'after' => ':attribute мора бити датум после :date.', + 'alpha' => ':attribute може да садржи само слова.', + 'alpha_dash' => ':attribute може да садржи само слова, бројеве, црте и подцрте.', + 'alpha_num' => ':attribute може да садржи само слова и бројеве.', 'array' => ':attribute мора бити низ.', - 'backup_codes' => 'The provided code is not valid or has already been used.', - 'before' => 'The :attribute must be a date before :date.', + 'backup_codes' => 'Достављени код није исправан или је већ искоришћен.', + 'before' => ':attribute мора бити датум пре :date.', 'between' => [ - 'numeric' => 'The :attribute must be between :min and :max.', - 'file' => 'The :attribute must be between :min and :max kilobytes.', - 'string' => 'The :attribute must be between :min and :max characters.', - 'array' => 'The :attribute must have between :min and :max items.', + 'numeric' => ':attribute мора бити између :min и :max.', + 'file' => ':attribute мора бити између :min и :max килобајта.', + 'string' => ':attribute мора бити између :min и :max карактера.', + 'array' => ':attribute мора бити између :min и :max ставки.', ], - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'email' => 'The :attribute must be a valid email address.', - 'ends_with' => 'The :attribute must end with one of the following: :values', - 'file' => 'The :attribute must be provided as a valid file.', - 'filled' => 'The :attribute field is required.', + 'boolean' => 'Поље :attribute мора бити тачно или нетачно.', + 'confirmed' => ':attribute потврда се не подудара.', + 'date' => ':attribute није исправан датум.', + 'date_format' => ':attribute се не подудара са форматом :format.', + 'different' => ':attribute и :other се морају разликовати.', + 'digits' => ':attribute мора бити :digits цифри.', + 'digits_between' => ':attribute мора бити између :min и :max цифара.', + 'email' => ':attribute морабити исправна адреса е-поште.', + 'ends_with' => ':attribute се мора завршити са једним од следећих: :values', + 'file' => ':attribute мора бити исправна достављена датотека.', + 'filled' => ':attribute поље је неопходно.', 'gt' => [ - 'numeric' => 'The :attribute must be greater than :value.', - 'file' => 'The :attribute must be greater than :value kilobytes.', - 'string' => 'The :attribute must be greater than :value characters.', - 'array' => 'The :attribute must have more than :value items.', + 'numeric' => ':attribute мора бити веће од :value.', + 'file' => ':attribute мора бити веће од :value килобајта.', + 'string' => ':attribute мора бити веће од :value карактера.', + 'array' => ':attribute мора садржати више од :value ставки.', ], 'gte' => [ - 'numeric' => 'The :attribute must be greater than or equal :value.', - 'file' => 'The :attribute must be greater than or equal :value kilobytes.', - 'string' => 'The :attribute must be greater than or equal :value characters.', - 'array' => 'The :attribute must have :value items or more.', + 'numeric' => ':attribute мора бити веће од или једнако :value.', + 'file' => ':attribute мора бити веће од или једнако :value килобајта.', + 'string' => ':attribute мора бити веће од или једнако :value карактера.', + 'array' => ':attribute мора да садржи :value или више ставки.', ], - 'exists' => 'The selected :attribute is invalid.', - 'image' => 'The :attribute must be an image.', - 'image_extension' => 'The :attribute must have a valid & supported image extension.', - 'in' => 'The selected :attribute is invalid.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'ipv4' => 'The :attribute must be a valid IPv4 address.', - 'ipv6' => 'The :attribute must be a valid IPv6 address.', - 'json' => 'The :attribute must be a valid JSON string.', + 'exists' => 'Изабрани :attribute је неисправан.', + 'image' => ':attribute мора бити слика.', + 'image_extension' => ':attribute мора да има исправну и подржану екстензију слике.', + 'in' => 'Изабрани :attribute је неисправан.', + 'integer' => ':attribute мора бити цели број.', + 'ip' => ':attribute мора бити исправна ИП адреса.', + 'ipv4' => ':attribute мора бити исправна IPv4 адреса.', + 'ipv6' => ':attribute мора бити исправна IPv6 адреса.', + 'json' => ':attribute мора бити исправна JSON ниска.', 'lt' => [ - 'numeric' => 'The :attribute must be less than :value.', - 'file' => 'The :attribute must be less than :value kilobytes.', - 'string' => 'The :attribute must be less than :value characters.', - 'array' => 'The :attribute must have less than :value items.', + 'numeric' => ':attribute мора бити мање од :value.', + 'file' => ':attribute мора бити мање од :value килобајта.', + 'string' => ':attribute мора бити мање од :value карактера.', + 'array' => ':attribute мора садржати мање од :value ставки.', ], 'lte' => [ - 'numeric' => 'The :attribute must be less than or equal :value.', - 'file' => 'The :attribute must be less than or equal :value kilobytes.', - 'string' => 'The :attribute must be less than or equal :value characters.', - 'array' => 'The :attribute must not have more than :value items.', + 'numeric' => ':attribute мора бити мање од или једнако :value.', + 'file' => ':attribute мора бити мање од или једнако :value килобајта.', + 'string' => ':attribute мора бити мање од или једнако :value карактера.', + 'array' => ':attribute не сме садржати више од :value ставки.', ], 'max' => [ - 'numeric' => 'The :attribute may not be greater than :max.', - 'file' => 'The :attribute may not be greater than :max kilobytes.', - 'string' => 'The :attribute may not be greater than :max characters.', - 'array' => 'The :attribute may not have more than :max items.', + 'numeric' => ':attribute не може бити већи од :max.', + 'file' => ':attribute не може бити већи од :max килобајта.', + 'string' => ':attribute не може бити већи од :max знакова.', + 'array' => ':attribute не може садржати више од :max ставки.', ], - 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimes' => ':attribute мора бити датотека типа: :values.', 'min' => [ - 'numeric' => 'The :attribute must be at least :min.', - 'file' => 'The :attribute must be at least :min kilobytes.', - 'string' => 'The :attribute must be at least :min characters.', - 'array' => 'The :attribute must have at least :min items.', + 'numeric' => ':attribute мора бити најмање :min.', + 'file' => ':attribute мора бити најмање :min килобајта.', + 'string' => ':attribute мора бити најмање :min карактера.', + 'array' => ':attribute мора садржати најмање :min ставки.', ], - 'not_in' => 'The selected :attribute is invalid.', - 'not_regex' => 'The :attribute format is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', - 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'safe_url' => 'The provided link may not be safe.', + 'not_in' => 'Изабрани :attribute је неисправан.', + 'not_regex' => ':attribute формат је неисправан.', + 'numeric' => ':attribute мора бити број.', + 'regex' => ':attribute формат је неисправан.', + 'required' => ':attribute поље је неопходно.', + 'required_if' => ':attribute поље је неопходно када :other је :value.', + 'required_with' => 'Поље :attribute је обавезно када је :values присутно.', + 'required_with_all' => 'Поље :attribute је обавезно када је :values присутно.', + 'required_without' => 'Поље :attribute је обавезно када :values није присутно.', + 'required_without_all' => 'Поље :attribute је обавезно када ниједно од :values није присутно.', + 'same' => ':attribute и :other се морају поклапати.', + 'safe_url' => 'Достављена веза можда није безбедна.', 'size' => [ - 'numeric' => 'The :attribute must be :size.', - 'file' => 'The :attribute must be :size kilobytes.', - 'string' => 'The :attribute must be :size characters.', - 'array' => 'The :attribute must contain :size items.', + 'numeric' => ':attribute мора бити :size.', + 'file' => ':attribute мора бити :size килобајта.', + 'string' => ':attribute мора бити :size карактера.', + 'array' => ':attribute мора да садржи :size ставки.', ], - 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid zone.', - 'totp' => 'The provided code is not valid or has expired.', - 'unique' => 'The :attribute has already been taken.', - 'url' => 'The :attribute format is invalid.', - 'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.', + 'string' => ':attribute мора бити текст.', + 'timezone' => ':attribute мора бити исправна зона.', + 'totp' => 'Достављени код није исправан или је истекао.', + 'unique' => ':attribute је већ заузет.', + 'url' => ':attribute формат је неисправан.', + 'uploaded' => 'Датотека није могла бити отпремљена. Сервер можда не прихвата датотеке ове величине.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => ':attribute мора бити референца за датотеку унутар ZIP-а.', + 'zip_file_size' => ':attribute датотеке не сме бити већи од :size MB.', + 'zip_file_mime' => ':attribute мора бити референца за датотеку типа :validTypes, пронађено :foundType.', + 'zip_model_expected' => 'Очекиван је објекат податка али је ":type" пронађен.', + 'zip_unique' => ':attribute мора бити јединствено за тип објекта унутар ZIP-а.', // Custom validation lines 'custom' => [ 'password-confirm' => [ - 'required_with' => 'Password confirmation required', + 'required_with' => 'Неопходна је потврда лозинке', ], ], diff --git a/lang/sv/activities.php b/lang/sv/activities.php index c501c675273..c7afc0bace2 100644 --- a/lang/sv/activities.php +++ b/lang/sv/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => 'Användaren har uppdaterats', 'user_delete' => 'raderad användare', 'user_delete_notification' => 'Användaren har tagits bort', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => 'återställ MFA för användare', + 'user_mfa_reset_notification' => 'Metoder för multifaktorautentisering återställda', // API Tokens 'api_token_create' => 'skapade API-token', diff --git a/lang/sv/auth.php b/lang/sv/auth.php index 6c94f21f9e0..44275e787d8 100644 --- a/lang/sv/auth.php +++ b/lang/sv/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Uppgifterna stämmer inte överens med våra register.', 'throttle' => 'För många inloggningsförsök. Prova igen om :seconds sekunder.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'För många försök till multifaktorverifiering. Försök igen om :seconds sekunder.', // Login & Register 'sign_up' => 'Skapa konto', diff --git a/lang/sv/entities.php b/lang/sv/entities.php index 1df81684913..6a0e2f0995b 100644 --- a/lang/sv/entities.php +++ b/lang/sv/entities.php @@ -63,10 +63,10 @@ 'import_delete_desc' => 'Detta kommer att ta bort den uppladdade ZIP-baserade importfilen och kan inte ångras.', 'import_errors' => 'Importfel', 'import_errors_desc' => 'Följande fel inträffade under importförsöket:', - 'breadcrumb_siblings_for_page' => 'Navigate siblings for page', - 'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter', - 'breadcrumb_siblings_for_book' => 'Navigate siblings for book', - 'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf', + 'breadcrumb_siblings_for_page' => 'Navigera mellan syskon för sida', + 'breadcrumb_siblings_for_chapter' => 'Navigera mellan syskon för kapitel', + 'breadcrumb_siblings_for_book' => 'Navigera mellan syskon för bok', + 'breadcrumb_siblings_for_bookshelf' => 'Navigera mellan syskon för hylla', // Permissions and restrictions 'permissions' => 'Rättigheter', @@ -173,7 +173,7 @@ 'books_sort_desc' => 'Flytta kapitel och sidor inom en bok för att omorganisera dess innehåll. Andra böcker kan läggas till, vilket gör det enkelt att flytta kapitel och sidor mellan böcker. Du kan även ställa in en regel som automatiskt sorterar bokens innehåll vid ändringar.', 'books_sort_auto_sort' => 'Automatiskt sorteringsalternativ', 'books_sort_auto_sort_active' => 'Aktiv automatisk sorteringsregel: :sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => 'Regler för automatisk sortering kan skapas i inställningsområdet "Listor och sortering" av en användare med relevanta behörigheter.', 'books_sort_named' => 'Sortera boken :bookName', 'books_sort_name' => 'Sortera utifrån namn', 'books_sort_created' => 'Sortera utifrån skapelse', @@ -253,7 +253,7 @@ 'pages_edit_switch_to_markdown_stable' => '(Stabilt innehåll)', 'pages_edit_switch_to_wysiwyg' => 'Växla till WYSIWYG-redigerare', 'pages_edit_switch_to_new_wysiwyg' => 'Växla till ny WYSIWYG', - 'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)', + 'pages_edit_switch_to_new_wysiwyg_desc' => '(I betatestning)', 'pages_edit_set_changelog' => 'Beskriv dina ändringar', 'pages_edit_enter_changelog_desc' => 'Ange en kort beskrivning av de ändringar du har gjort', 'pages_edit_enter_changelog' => 'Ändringslogg', @@ -272,8 +272,8 @@ 'pages_md_insert_link' => 'Infoga länk', 'pages_md_insert_drawing' => 'Infoga teckning', 'pages_md_show_preview' => 'Visa förhandsgranskning', - 'pages_md_sync_scroll' => 'Sync preview scroll', - 'pages_md_plain_editor' => 'Plaintext editor', + 'pages_md_sync_scroll' => 'Synkronisera förhandsgranskningsrullning', + 'pages_md_plain_editor' => 'Textredigerare (plaintext)', 'pages_drawing_unsaved' => 'Osparad ritning hittades', 'pages_drawing_unsaved_confirm' => 'Osparade ritningsdata hittades från ett tidigare misslyckat sparförsök. Vill du återställa och fortsätta redigera den osparade ritningen?', 'pages_not_in_chapter' => 'Sidan ligger inte i något kapitel', @@ -306,10 +306,10 @@ 'pages_edit_content_link' => 'Hoppa till sektionen i redigeraren', 'pages_pointer_enter_mode' => 'Ange markeringsläge för sektion', 'pages_pointer_label' => 'Alternativ för sidsektion', - 'pages_pointer_permalink' => 'Page Section Permalink', - 'pages_pointer_include_tag' => 'Page Section Include Tag', - 'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag', - 'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink', + 'pages_pointer_permalink' => 'Permalänk för sidavsnitt', + 'pages_pointer_include_tag' => 'Include-tagg för sidavsnitt', + 'pages_pointer_toggle_link' => 'Permalänksläge, tryck för att visa include-tagg', + 'pages_pointer_toggle_include' => 'Include-taggläge, tryck för att visa permalänk', 'pages_permissions_active' => 'Anpassade rättigheter är i bruk', 'pages_initial_revision' => 'Första publicering', 'pages_references_update_revision' => 'Automatisk uppdatering av interna länkar', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => 'Visa/Dölj sidopanel', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => 'Sidans innehåll', + 'page_contents_none' => 'Inga rubriker hittades i sidans innehåll.', + 'page_contents_info' => 'Innehållsmenyn genereras utifrån de rubrikformat som används på sidan.', 'page_tags' => 'Sidtaggar', 'chapter_tags' => 'Kapiteltaggar', 'book_tags' => 'Boktaggar', @@ -361,7 +361,7 @@ 'attachments_explain_instant_save' => 'Ändringar här sparas omgående.', 'attachments_upload' => 'Ladda upp fil', 'attachments_link' => 'Bifoga länk', - 'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.', + 'attachments_upload_drop' => 'Alternativt kan du dra och släppa en fil här för att ladda upp den som en bilaga.', 'attachments_set_link' => 'Ange länk', 'attachments_delete' => 'Är du säker på att du vill ta bort bilagan?', 'attachments_dropzone' => 'Släpp filer här för uppladdning', @@ -403,9 +403,9 @@ 'comment_add' => 'Lägg till kommentar', 'comment_none' => 'Inga kommentarer att visa', 'comment_placeholder' => 'Lämna en kommentar här', - 'comment_thread_count' => ':count Comment Thread|:count Comment Threads', - 'comment_archived_count' => ':count Archived', - 'comment_archived_threads' => 'Archived Threads', + 'comment_thread_count' => ':count kommentarstråd|:count kommentarstrådar', + 'comment_archived_count' => ':count arkiverad(e)', + 'comment_archived_threads' => 'Arkiverade trådar', 'comment_save' => 'Spara kommentar', 'comment_new' => 'Ny kommentar', 'comment_created' => 'kommenterade :createDiff', @@ -415,7 +415,7 @@ 'comment_created_success' => 'Kommentaren har sparats', 'comment_updated_success' => 'Kommentaren har uppdaterats', 'comment_archive_success' => 'Arkivera kommentar', - 'comment_unarchive_success' => 'Comment un-archived', + 'comment_unarchive_success' => 'Kommentar avarkiverad', 'comment_view' => 'Visa kommentar', 'comment_jump_to_thread' => 'Hoppa till tråd', 'comment_delete_confirm' => 'Är du säker på att du vill ta bort den här kommentaren?', @@ -452,12 +452,12 @@ // References 'references' => 'Referenser', 'references_none' => 'Det finns inga referenser kopplade till detta objekt.', - 'references_to_desc' => 'Listed below is all the known content in the system that links to this item.', + 'references_to_desc' => 'Nedan listas allt känt innehåll i systemet som länkar till detta objekt.', // Watch Options 'watch' => 'Följ', 'watch_title_default' => 'Standardinställningar', - 'watch_desc_default' => 'Revert watching to just your default notification preferences.', + 'watch_desc_default' => 'Återställ bevakning till enbart dina standardaviseringsinställningar.', 'watch_title_ignore' => 'Ignorera', 'watch_desc_ignore' => 'Ignorera samtliga meddelanden, även sådana som styrs av användarens egna inställningar.', 'watch_title_new' => 'Nya sidor', @@ -465,16 +465,16 @@ 'watch_title_updates' => 'Alla siduppdateringar', 'watch_desc_updates' => 'Meddela vid alla nya sidor och siduppdateringar.', 'watch_desc_updates_page' => 'Meddela alla siduppdateringar.', - 'watch_title_comments' => 'All Page Updates & Comments', + 'watch_title_comments' => 'Alla sidupdateringar och kommentarer', 'watch_desc_comments' => 'Meddela vid alla nya sidor, siduppdateringar och nya kommentarer.', 'watch_desc_comments_page' => 'Meddela vid siduppdateringar och nya kommentarer.', 'watch_change_default' => 'Ändra standardinställningar för meddelanden', 'watch_detail_ignore' => 'Ignorera meddelanden', - 'watch_detail_new' => 'Watching for new pages', - 'watch_detail_updates' => 'Watching new pages and updates', - 'watch_detail_comments' => 'Watching new pages, updates & comments', - 'watch_detail_parent_book' => 'Watching via parent book', - 'watch_detail_parent_book_ignore' => 'Ignoring via parent book', - 'watch_detail_parent_chapter' => 'Watching via parent chapter', - 'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter', + 'watch_detail_new' => 'Bevakar nya sidor', + 'watch_detail_updates' => 'Bevakar nya sidor och uppdateringar', + 'watch_detail_comments' => 'Bevakar nya sidor, uppdateringar och kommentarer', + 'watch_detail_parent_book' => 'Bevakar via överordnad bok', + 'watch_detail_parent_book_ignore' => 'Ignorerar via överordnad bok', + 'watch_detail_parent_chapter' => 'Bevakar via överordnat kapitel', + 'watch_detail_parent_chapter_ignore' => 'Ignorerar via överordnat kapitel', ]; diff --git a/lang/sv/errors.php b/lang/sv/errors.php index 0548f96fd25..7cbe9f6c561 100644 --- a/lang/sv/errors.php +++ b/lang/sv/errors.php @@ -10,7 +10,7 @@ // Auth 'error_user_exists_different_creds' => 'En användare med adressen :email finns redan.', - 'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details', + 'auth_pre_register_theme_prevention' => 'Användarkontot kunde inte registreras med de angivna uppgifterna', 'email_already_confirmed' => 'E-posten har redan bekräftats, prova att logga in.', 'email_confirmation_invalid' => 'Denna bekräftelsekod är inte giltig eller har redan använts. Vänligen prova att registrera dig på nytt.', 'email_confirmation_expired' => 'Denna bekräftelsekod har gått ut. Vi har skickat dig en ny.', @@ -51,18 +51,18 @@ 'image_upload_error' => 'Ett fel inträffade vid uppladdningen', 'image_upload_type_error' => 'Filtypen du försöker ladda upp är ogiltig', 'image_upload_replace_type' => 'Bilder som skall ersättas måste vara av samma filtyp', - 'image_upload_memory_limit' => 'Failed to handle image upload and/or create thumbnails due to system resource limits.', - 'image_thumbnail_memory_limit' => 'Failed to create image size variations due to system resource limits.', + 'image_upload_memory_limit' => 'Det gick inte att hantera bilduppladdningen och/eller skapa miniatyrbilder på grund av begränsade systemresurser.', + 'image_thumbnail_memory_limit' => 'Det gick inte att skapa bildstorleksvarianter på grund av begränsade systemresurser.', 'image_gallery_thumbnail_memory_limit' => 'Misslyckades att skapa galleriminiatyrer på grund av otillräckliga systemresurser.', - 'drawing_data_not_found' => 'Drawing data could not be loaded. The drawing file might no longer exist or you may not have permission to access it.', + 'drawing_data_not_found' => 'Ritningsdata kunde inte laddas. Ritningen kanske inte längre finns, eller så har du inte behörighet att komma åt den.', // Attachments 'attachment_not_found' => 'Bilagan hittades ej', - 'attachment_upload_error' => 'An error occurred uploading the attachment file', + 'attachment_upload_error' => 'Ett fel uppstod vid uppladdning av bilagan', // Pages 'page_draft_autosave_fail' => 'Kunde inte spara utkastet. Kontrollera att du är ansluten till internet.', - 'page_draft_delete_fail' => 'Failed to delete page draft and fetch current page saved content', + 'page_draft_delete_fail' => 'Det gick inte att radera utkastet och hämta det sparade innehållet för aktuell sida', 'page_custom_home_deletion' => 'Det går inte att ta bort sidan medan den används som startsida', // Entities @@ -78,7 +78,7 @@ // Users 'users_cannot_delete_only_admin' => 'Du kan inte ta bort den enda admin-användaren', 'users_cannot_delete_guest' => 'Du kan inte ta bort gästanvändaren', - 'users_could_not_send_invite' => 'Could not create user since invite email failed to send', + 'users_could_not_send_invite' => 'Kunde inte skapa användare eftersom inbjudningsmejlet inte kunde skickas', // Roles 'role_cannot_be_edited' => 'Den här rollen kan inte redigeras', @@ -108,8 +108,8 @@ // Import 'import_zip_cant_read' => 'Kunde inte läsa ZIP-filen.', 'import_zip_cant_decode_data' => 'Kunde inte hitta och avkoda ZIP data.json innehåll.', - 'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.', - 'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.', + 'import_zip_no_data' => 'ZIP-filens data innehåller inte förväntat bok-, kapitel- eller sidinnehåll.', + 'import_zip_data_too_large' => 'Innehållet i ZIP-filens data.json överskrider den konfigurerade maxgränsen för uppladdning i applikationen.', 'import_validation_failed' => 'ZIP-filen kunde inte valideras med fel:', 'import_zip_failed_notification' => 'Det gick inte att importera ZIP-fil.', 'import_perms_books' => 'Du saknar behörighet att skapa böcker.', @@ -125,7 +125,7 @@ 'api_incorrect_token_secret' => 'Hemligheten för den angivna API-token är felaktig', 'api_user_no_api_permission' => 'Ägaren av den använda API-token har inte behörighet att göra API-anrop', 'api_user_token_expired' => 'Den använda auktoriseringstoken har löpt ut', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => 'Endast GET-förfrågningar är tillåtna vid API-användning med cookiebaserad autentisering', // Settings & Maintenance 'maintenance_test_email_failure' => 'Ett fel uppstod när ett test mail skulle skickas:', diff --git a/lang/sv/notifications.php b/lang/sv/notifications.php index 19933c049cb..cf1a87cf656 100644 --- a/lang/sv/notifications.php +++ b/lang/sv/notifications.php @@ -11,8 +11,8 @@ 'updated_page_subject' => 'Uppdaterad sida: :pageName', 'updated_page_intro' => 'En sida har blivit uppdaterad i :appName:', 'updated_page_debounce' => 'För att förhindra en massa notiser, så kommer det inte skickas nya notiser på ett tag för ytterligare ändringar till denna sida av samma skribent.', - 'comment_mention_subject' => 'You have been mentioned in a comment on page: :pageName', - 'comment_mention_intro' => 'You were mentioned in a comment on :appName:', + 'comment_mention_subject' => 'Du har blivit nämnd i en kommentar på sidan: :pageName', + 'comment_mention_intro' => 'Du har blivit nämnd i en kommentar på :appName:', 'detail_page_name' => 'Sidonamn:', 'detail_page_path' => 'Sidosökväg:', diff --git a/lang/sv/preferences.php b/lang/sv/preferences.php index 7ebf2681350..80dc907184e 100644 --- a/lang/sv/preferences.php +++ b/lang/sv/preferences.php @@ -8,45 +8,45 @@ 'my_account' => 'Mitt Konto', 'shortcuts' => 'Genvägar', - 'shortcuts_interface' => 'UI Shortcut Preferences', - 'shortcuts_toggle_desc' => 'Here you can enable or disable keyboard system interface shortcuts, used for navigation and actions.', - 'shortcuts_customize_desc' => 'You can customize each of the shortcuts below. Just press your desired key combination after selecting the input for a shortcut.', - 'shortcuts_toggle_label' => 'Keyboard shortcuts enabled', - 'shortcuts_section_navigation' => 'Navigation', - 'shortcuts_section_actions' => 'Common Actions', + 'shortcuts_interface' => 'Inställningar för UI-genvägar', + 'shortcuts_toggle_desc' => 'Här kan du aktivera eller inaktivera tangentbordsgenvägar för systemgränssnittet, som används för navigering och åtgärder.', + 'shortcuts_customize_desc' => 'Du kan anpassa varje genväg nedan. Tryck bara på önskad tangentkombination efter att ha valt inmatningsfältet för en genväg.', + 'shortcuts_toggle_label' => 'Tangentbordsgenvägar aktiverade', + 'shortcuts_section_navigation' => 'Navigering', + 'shortcuts_section_actions' => 'Vanliga åtgärder', 'shortcuts_save' => 'Spara genvägar', - 'shortcuts_overlay_desc' => 'Note: When shortcuts are enabled a helper overlay is available via pressing "?" which will highlight the available shortcuts for actions currently visible on the screen.', - 'shortcuts_update_success' => 'Shortcut preferences have been updated!', - 'shortcuts_overview_desc' => 'Manage keyboard shortcuts you can use to navigate the system user interface.', + 'shortcuts_overlay_desc' => 'Obs: När genvägar är aktiverade finns en hjälpöverlagring tillgänglig genom att trycka på "?", vilken markerar de tillgängliga genvägarna för åtgärder som för närvarande syns på skärmen.', + 'shortcuts_update_success' => 'Genvägsinställningarna har uppdaterats!', + 'shortcuts_overview_desc' => 'Hantera tangentbordsgenvägar som du kan använda för att navigera i systemets användargränssnitt.', - '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_mentions' => 'Notify when I\'m mentioned in a comment', - 'notifications_opt_comment_replies' => 'Notify upon replies to my comments', - 'notifications_save' => 'Save Preferences', - 'notifications_update_success' => 'Notification preferences have been updated!', - 'notifications_watched' => 'Watched & Ignored Items', - 'notifications_watched_desc' => 'Below are the items that have custom watch preferences applied. To update your preferences for these, view the item then find the watch options in the sidebar.', + 'notifications' => 'Aviseringsinställningar', + 'notifications_desc' => 'Kontrollera de e-postaviseringar du får när viss aktivitet utförs i systemet.', + 'notifications_opt_own_page_changes' => 'Meddela vid ändringar på sidor jag äger', + 'notifications_opt_own_page_comments' => 'Meddela vid kommentarer på sidor jag äger', + 'notifications_opt_comment_mentions' => 'Meddela när jag blir nämnd i en kommentar', + 'notifications_opt_comment_replies' => 'Meddela vid svar på mina kommentarer', + 'notifications_save' => 'Spara inställningar', + 'notifications_update_success' => 'Aviseringsinställningarna har uppdaterats!', + 'notifications_watched' => 'Bevakade och ignorerade objekt', + 'notifications_watched_desc' => 'Nedan visas de objekt som har anpassade bevakningsinställningar. För att uppdatera dina inställningar för dessa, öppna objektet och hitta bevakningsalternativen i sidopanelen.', - 'auth' => 'Access & Security', - 'auth_change_password' => 'Change Password', - 'auth_change_password_desc' => 'Change the password you use to log-in to the application. This must be at least 8 characters long.', + 'auth' => 'Åtkomst och säkerhet', + 'auth_change_password' => 'Ändra lösenord', + 'auth_change_password_desc' => 'Ändra lösenordet du använder för att logga in i applikationen. Det måste vara minst 8 tecken långt.', 'auth_change_password_success' => 'Lösenordet har uppdaterats!', 'profile' => 'Profildetaljer', - 'profile_desc' => 'Manage the details of your account which represents you to other users, in addition to details that are used for communication and system personalisation.', + 'profile_desc' => 'Hantera detaljerna för ditt konto som representerar dig för andra användare, utöver de uppgifter som används för kommunikation och systempersonalisering.', 'profile_view_public' => 'Visa publik profil', - 'profile_name_desc' => 'Configure your display name which will be visible to other users in the system through the activity you perform, and content you own.', - 'profile_email_desc' => 'This email will be used for notifications and, depending on active system authentication, system access.', - 'profile_email_no_permission' => 'Unfortunately you don\'t have permission to change your email address. If you want to change this, you\'d need to ask an administrator to change this for you.', - 'profile_avatar_desc' => 'Select an image which will be used to represent yourself to others in the system. Ideally this image should be square and about 256px in width and height.', - 'profile_admin_options' => 'Administrator Options', - 'profile_admin_options_desc' => 'Additional administrator-level options, like those to manage role assignments, can be found for your user account in the "Settings > Users" area of the application.', + 'profile_name_desc' => 'Konfigurera ditt visningsnamn som kommer att vara synligt för andra användare i systemet genom den aktivitet du utför och det innehåll du äger.', + 'profile_email_desc' => 'Denna e-postadress kommer att användas för aviseringar och beroende på aktiv systemautentisering, för systemåtkomst.', + 'profile_email_no_permission' => 'Tyvärr har du inte behörighet att ändra din e-postadress. Om du vill ändra detta behöver du be en administratör att göra det åt dig.', + 'profile_avatar_desc' => 'Välj en bild som kommer att representera dig för andra i systemet. Helst bör bilden vara kvadratisk och cirka 256px bred och hög.', + 'profile_admin_options' => 'Administratörsalternativ', + 'profile_admin_options_desc' => 'Ytterligare inställningar på administratörsnivå, till exempel för att hantera rolltilldelningar, hittar du för ditt användarkonto under "Inställningar > Användare" i applikationen.', 'delete_account' => 'Radera konto', 'delete_my_account' => 'Radera mitt konto', - 'delete_my_account_desc' => 'This will fully delete your user account from the system. You will not be able to recover this account or revert this action. Content you\'ve created, such as created pages and uploaded images, will remain.', - 'delete_my_account_warning' => 'Are you sure you want to delete your account?', + 'delete_my_account_desc' => 'Detta kommer permanent radera ditt användarkonto från systemet. Du kommer inte kunna återställa detta konto eller ångra denna åtgärd. Innehåll du har skapat, till exempel skapade sidor och uppladdade bilder, kommer att finnas kvar.', + 'delete_my_account_warning' => 'Är du säker på att du vill radera ditt konto?', ]; diff --git a/lang/sv/settings.php b/lang/sv/settings.php index e900a86a3ba..2968183dd8f 100644 --- a/lang/sv/settings.php +++ b/lang/sv/settings.php @@ -16,8 +16,8 @@ 'app_customization' => 'Sidanpassning', 'app_features_security' => 'Funktioner och säkerhet', 'app_name' => 'Applikationsnamn', - 'app_name_desc' => 'Namnet visas i sidhuvdet och i eventuella mail.', - 'app_name_header' => 'Visa applikationsnamn i sidhuvudet?', + 'app_name_desc' => 'Namnet visas i sidhuvudet och i eventuella mejl.', + 'app_name_header' => 'Visa applikationsnamn i sidhuvudet', 'app_public_access' => 'Offentlig åtkomst', 'app_public_access_desc' => 'Om du aktiverar detta alternativ låter du icke inloggade besökare komma åt innehåll på din sida', 'app_public_access_desc_guest' => 'Åtkomst för icke inloggade besökare kan styras via användaren "Guest".', @@ -75,36 +75,36 @@ 'reg_confirm_restrict_domain_placeholder' => 'Ingen begränsning inställd', // Sorting Settings - 'sorting' => 'Lists & Sorting', - 'sorting_book_default' => 'Default Book Sort Rule', + 'sorting' => 'Listor och sortering', + 'sorting_book_default' => 'Standardsorteringsregel för böcker', 'sorting_book_default_desc' => 'Välj standard sorteringsregel som skall tillämpas på nya böcker. Detta påverkar inte befintliga böcker och kan åsidosättas per bok.', 'sorting_rules' => 'Sorteringsregler', - 'sorting_rules_desc' => 'These are predefined sorting operations which can be applied to content in the system.', + 'sorting_rules_desc' => 'Detta är fördefinierade sorteringsåtgärder som kan tillämpas på innehåll i systemet.', 'sort_rule_assigned_to_x_books' => 'Tilldelad till :count bok|Tilldelad till :count böcker', 'sort_rule_create' => 'Skapa sorteringsregel', 'sort_rule_edit' => 'Redigera sorteringsregel', 'sort_rule_delete' => 'Ta bort sorteringsregel', - 'sort_rule_delete_desc' => 'Remove this sort rule from the system. Books using this sort will revert to manual sorting.', - 'sort_rule_delete_warn_books' => 'This sort rule is currently used on :count book(s). Are you sure you want to delete this?', - 'sort_rule_delete_warn_default' => 'This sort rule is currently used as the default for books. Are you sure you want to delete this?', + 'sort_rule_delete_desc' => 'Ta bort denna sorteringsregel från systemet. Böcker som använder denna sortering kommer att återgå till manuell sortering.', + 'sort_rule_delete_warn_books' => 'Denna sorteringsregel används för närvarande på :count bok/böcker. Är du säker på att du vill radera den?', + 'sort_rule_delete_warn_default' => 'Denna sorteringsregel används för närvarande som standard för böcker. Är du säker på att du vill radera den?', 'sort_rule_details' => 'Detaljer för sorteringsregler', - 'sort_rule_details_desc' => 'Set a name for this sort rule, which will appear in lists when users are selecting a sort.', - 'sort_rule_operations' => 'Sort Operations', - 'sort_rule_operations_desc' => 'Configure the sort actions to be performed by moving them from the list of available operations. Upon use, the operations will be applied in order, from top to bottom. Any changes made here will be applied to all assigned books upon save.', + 'sort_rule_details_desc' => 'Ange ett namn för denna sorteringsregel, vilket kommer att visas i listor när användare väljer en sortering.', + 'sort_rule_operations' => 'Sorteringsåtgärder', + 'sort_rule_operations_desc' => 'Konfigurera sorteringsåtgärderna som ska utföras genom att flytta dem från listan över tillgängliga åtgärder. Vid användning kommer åtgärderna att tillämpas i ordning, uppifrån och ner. Alla ändringar som görs här kommer att tillämpas på alla tilldelade böcker vid sparande.', 'sort_rule_available_operations' => 'Tillgängliga åtgärder', - 'sort_rule_available_operations_empty' => 'No operations remaining', - 'sort_rule_configured_operations' => 'Configured Operations', - 'sort_rule_configured_operations_empty' => 'Drag/add operations from the "Available Operations" list', - 'sort_rule_op_asc' => '(Asc)', - 'sort_rule_op_desc' => '(Desc)', + 'sort_rule_available_operations_empty' => 'Inga åtgärder kvar', + 'sort_rule_configured_operations' => 'Konfigurerade åtgärder', + 'sort_rule_configured_operations_empty' => 'Dra/lägg till åtgärder från listan "Tillgängliga åtgärder"', + 'sort_rule_op_asc' => '(Stigande)', + 'sort_rule_op_desc' => '(Fallande)', 'sort_rule_op_name' => 'Namn - Alfabetisk ordning', 'sort_rule_op_name_numeric' => 'Namn - Numerisk ordning', 'sort_rule_op_created_date' => 'Datum skapat', 'sort_rule_op_updated_date' => 'Datum uppdaterat', - 'sort_rule_op_chapters_first' => 'Chapters First', - 'sort_rule_op_chapters_last' => 'Chapters Last', - 'sorting_page_limits' => 'Per-Page Display Limits', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sort_rule_op_chapters_first' => 'Kapitel först', + 'sort_rule_op_chapters_last' => 'Kapitel sist', + 'sorting_page_limits' => 'Gränser för antal objekt per sida', + 'sorting_page_limits_desc' => 'Ställ in hur många objekt som ska visas per sida i olika listor i systemet. Vanligtvis är ett lägre antal mer prestandaeffektivt, medan ett högre antal minskar behovet av att klicka sig igenom flera sidor. Det rekommenderas att använda en multipel av 6.', // Maintenance settings 'maint' => 'Underhåll', @@ -141,7 +141,7 @@ 'recycle_bin_contents_empty' => 'Papperskorgen är för närvarande tom', 'recycle_bin_empty' => 'Töm papperskorgen', 'recycle_bin_empty_confirm' => 'Detta kommer permanent att förstöra alla objekt i papperskorgen inklusive innehåll som finns i varje objekt. Är du säker du vill tömma papperskorgen?', - 'recycle_bin_destroy_confirm' => 'This action will permanently delete this item from the system, along with any child elements listed below, and you will not be able to restore this content. Are you sure you want to permanently delete this item?', + 'recycle_bin_destroy_confirm' => 'Denna åtgärd kommer att permanent radera detta objekt från systemet, tillsammans med eventuella underliggande element som listas nedan, och du kommer inte att kunna återställa detta innehåll. Är du säker på att du vill permanent radera detta objekt?', 'recycle_bin_destroy_list' => 'Objekt som ska förstöras', 'recycle_bin_restore_list' => 'Objekt som ska återställas', 'recycle_bin_restore_confirm' => 'Denna åtgärd kommer att återställa det raderade objektet, inklusive alla underordnade element, till deras ursprungliga plats. Om den ursprungliga platsen har tagits bort sedan dess, och är nu i papperskorgen, kommer det överordnade objektet också att behöva återställas.', @@ -168,11 +168,11 @@ // Role Settings 'roles' => 'Roller', 'role_user_roles' => 'Användarroller', - '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' => 'Roller används för att gruppera användare och ge deras medlemmar systembehörigheter. När en användare är medlem i flera roller läggs behörigheterna samman och användaren ärver alla rättigheter.', + 'roles_x_users_assigned' => ':count användare tilldelad|:count användare tilldelade', + 'roles_x_permissions_provided' => ':count behörighet|:count behörigheter', + 'roles_assigned_users' => 'Tilldelade användare', + 'roles_permissions_provided' => 'Tillhandahållna behörigheter', 'role_create' => 'Skapa ny roll', 'role_delete' => 'Ta bort roll', 'role_delete_confirm' => 'Rollen med namn \':roleName\' kommer att tas bort.', @@ -194,27 +194,27 @@ 'role_access_api' => 'Åtkomst till systemets API', 'role_manage_settings' => 'Hantera appinställningar', 'role_export_content' => 'Exportera innehåll', - 'role_import_content' => 'Import content', + 'role_import_content' => 'Importera innehåll', 'role_editor_change' => 'Ändra sidredigerare', - 'role_notifications' => 'Receive & manage notifications', - 'role_permission_note_users_and_roles' => 'These permissions will technically also provide visibility & searching of users & roles in the system.', + 'role_notifications' => 'Ta emot och hantera aviseringar', + 'role_permission_note_users_and_roles' => 'Dessa behörigheter kommer i praktiken även att ge synlighet och sökmöjlighet för användare och roller i systemet.', 'role_asset' => 'Tillgång till innehåll', 'roles_system_warning' => 'Var medveten om att åtkomst till någon av ovanstående tre behörigheter kan tillåta en användare att ändra sina egna rättigheter eller andras rättigheter i systemet. Tilldela endast roller med dessa behörigheter till betrodda användare.', 'role_asset_desc' => 'Det här är standardinställningarna för allt innehåll i systemet. Eventuella anpassade rättigheter på böcker, kapitel och sidor skriver över dessa inställningar.', 'role_asset_admins' => 'Administratörer har automatisk tillgång till allt innehåll men dessa alternativ kan visa och dölja vissa gränssnittselement', 'role_asset_image_view_note' => 'Detta avser synlighet inom bildhanteraren. Faktisk åtkomst för uppladdade bildfiler kommer att bero på alternativ för bildlagring.', - 'role_asset_users_note' => 'These permissions will technically also provide visibility & searching of users in the system.', + 'role_asset_users_note' => 'Dessa behörigheter kommer i praktiken även att ge synlighet och sökmöjlighet för användare i systemet.', 'role_all' => 'Alla', 'role_own' => 'Egna', 'role_controlled_by_asset' => 'Kontrolleras av den sida de laddas upp till', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => 'Styrs av behörigheter för radering av sidor', 'role_save' => 'Spara roll', 'role_users' => 'Användare med denna roll', 'role_users_none' => 'Inga användare tillhör den här rollen', // Users 'users' => 'Användare', - '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' => 'Skapa och hantera enskilda användarkonton i systemet. Användarkonton används för inloggning och för att tillskriva innehåll och aktivitet en användare. Åtkomstbehörigheter är i huvudsak rollbaserade, men användarens innehållsägarskap, bland andra faktorer, kan också påverka behörigheter och åtkomst.', 'user_profile' => 'Användarprofil', 'users_add_new' => 'Lägg till användare', 'users_search' => 'Sök användare', @@ -229,8 +229,8 @@ 'users_send_invite_text' => 'Du kan välja att skicka denna användare ett e-postmeddelande som tillåter dem att ställa in sitt eget lösenord, eller så kan du ställa in deras lösenord själv.', 'users_send_invite_option' => 'Skicka e-post med inbjudan', 'users_external_auth_id' => 'Externt ID för autentisering', - 'users_external_auth_id_desc' => 'When an external authentication system is in use (such as SAML2, OIDC or LDAP) this is the ID which links this BookStack user to the authentication system account. You can ignore this field if using the default email-based authentication.', - 'users_password_warning' => 'Only fill the below if you would like to change the password for this user.', + 'users_external_auth_id_desc' => 'När ett externt autentiseringssystem används (såsom SAML2, OIDC eller LDAP) är detta det ID som länkar detta BookStack-konto till kontot i autentiseringssystemet. Du kan bortse från detta fält om standardautentisering via e-post används.', + 'users_password_warning' => 'Dessa behörigheter kommer i praktiken även att ge synlighet och sökmöjlighet för användare i systemet.', 'users_system_public' => 'Den här användaren representerar eventuella gäster som använder systemet. Den kan inte användas för att logga in utan tilldeles automatiskt.', 'users_delete' => 'Ta bort användare', 'users_delete_named' => 'Ta bort användaren :userName', @@ -246,7 +246,7 @@ 'users_preferred_language' => 'Föredraget språk', 'users_preferred_language_desc' => 'Det här alternativet kommer att ändra det språk som används i användargränssnittet. Detta påverkar inget användarskapat innehåll.', 'users_social_accounts' => 'Anslutna konton', - 'users_social_accounts_desc' => 'View the status of the connected social accounts for this user. Social accounts can be used in addition to the primary authentication system for system access.', + 'users_social_accounts_desc' => 'Dessa behörigheter kommer i praktiken även att ge synlighet och sökmöjlighet för användare och roller i systemet.', 'users_social_accounts_info' => 'Här kan du ansluta dina andra konton för snabbare och smidigare inloggning. Om du kopplar från en tjänst här kommer de behörigheter som tidigare givits inte att tas bort - ta bort behörigheter genom att logga in på ditt konto på tjänsten i fråga.', 'users_social_connect' => 'Anslut konto', 'users_social_disconnect' => 'Koppla från konto', @@ -255,7 +255,7 @@ 'users_social_connected' => ':socialAccount har kopplats till ditt konto.', 'users_social_disconnected' => ':socialAccount har kopplats bort från ditt konto.', 'users_api_tokens' => 'API-nyckel', - 'users_api_tokens_desc' => 'Create and manage the access tokens used to authenticate with the BookStack REST API. Permissions for the API are managed via the user that the token belongs to.', + 'users_api_tokens_desc' => 'Ta emot och hantera aviseringar', 'users_api_tokens_none' => 'Inga API-tokens har skapats för den här användaren', 'users_api_tokens_create' => 'Skapa token', 'users_api_tokens_expires' => 'Förfaller', @@ -264,9 +264,9 @@ 'users_mfa_desc' => 'Konfigurera multifaktorsautentisering som ett extra skydd för ditt konto.', 'users_mfa_x_methods' => ':count metod konfigurerad|:count metoder konfigurerade', 'users_mfa_configure' => 'Konfigurera metoder', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => 'Återställ metoder för multifaktorautentisering', + 'users_mfa_reset_desc' => 'Detta kommer att återställa och rensa alla konfigurerade metoder för multifaktorautentisering för denna användare. Om multifaktorautentisering krävs av någon av deras roller kommer de att uppmanas att konfigurera nya metoder vid nästa inloggning.', + 'users_mfa_reset_confirm' => 'Är du säker på att du vill återställa multifaktorautentisering för denna användare?', // API Tokens 'user_api_token_create' => 'Skapa API-nyckel', @@ -288,8 +288,8 @@ // Webhooks 'webhooks' => 'Webhooks', - 'webhooks_index_desc' => 'Webhooks are a way to send data to external URLs when certain actions and events occur within the system which allows event-based integration with external platforms such as messaging or notification systems.', - 'webhooks_x_trigger_events' => ':count trigger event|:count trigger events', + 'webhooks_index_desc' => 'Webhooks är ett sätt att skicka data till externa URLer när vissa åtgärder och händelser inträffar i systemet, vilket möjliggör händelsebaserad integration med externa plattformar såsom meddelande- eller aviseringssystem.', + 'webhooks_x_trigger_events' => ':count utlösande händelse|:count utlösande händelser', 'webhooks_create' => 'Skapa ny webhook', 'webhooks_none_created' => 'Inga webhooks har skapats än.', 'webhooks_edit' => 'Redigera webhook', @@ -317,7 +317,7 @@ // Licensing 'licenses' => 'Licenser', - 'licenses_desc' => 'This page details license information for BookStack in addition to the projects & libraries that are used within BookStack. Many projects listed may only be used in a development context.', + 'licenses_desc' => 'Denna sida beskriver licensinformation för BookStack samt de projekt och bibliotek som används inom BookStack. Många av de listade projekten används eventuellt bara i ett utvecklingssammanhang.', 'licenses_bookstack' => 'BookStack licens', 'licenses_php' => 'Licenser för PHP-bibliotek', 'licenses_js' => 'Licenser för JavaScript-bibliotek', @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenska', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/sv/validation.php b/lang/sv/validation.php index b0c1f7a1bb9..4f9298ba33d 100644 --- a/lang/sv/validation.php +++ b/lang/sv/validation.php @@ -105,11 +105,11 @@ 'url' => 'Formatet på :attribute är ogiltigt.', 'uploaded' => 'Filen kunde inte laddas upp. Servern kanske inte tillåter filer med denna storlek.', - 'zip_file' => 'The :attribute needs to reference a file within the ZIP.', - 'zip_file_size' => 'The file :attribute must not exceed :size MB.', - 'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.', - 'zip_model_expected' => 'Data object expected but ":type" found.', - 'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.', + 'zip_file' => ':attribute måste referera till en fil inom ZIP-filen.', + 'zip_file_size' => 'Filen :attribute får inte överstiga :size MB.', + 'zip_file_mime' => ':attribute måste referera till en fil av typen :validTypes, hittade :foundType.', + 'zip_model_expected' => 'Dataobjekt förväntades men ":type" hittades.', + 'zip_unique' => ':attribute måste referera till en fil inom ZIP-filen.', // Custom validation lines 'custom' => [ diff --git a/lang/th/settings.php b/lang/th/settings.php index 558d2da0f6e..af85e37c109 100644 --- a/lang/th/settings.php +++ b/lang/th/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/tk/settings.php b/lang/tk/settings.php index d03024a89d6..0e5ce84cf21 100644 --- a/lang/tk/settings.php +++ b/lang/tk/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/tr/auth.php b/lang/tr/auth.php index f8942a4dfc3..907487baff9 100644 --- a/lang/tr/auth.php +++ b/lang/tr/auth.php @@ -8,7 +8,7 @@ 'failed' => 'Girdiğiniz bilgiler kayıtlarımızla uyuşmuyor.', 'throttle' => 'Çok fazla giriş yapmaya çalıştınız. Lütfen :seconds saniye içinde tekrar deneyin.', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => 'Çok fazla çok faktörlü doğrulama denemesi yapıldı. Lütfen :seconds içinde tekrar deneyin.', // Login & Register 'sign_up' => 'Kaydol', @@ -92,7 +92,7 @@ 'mfa_option_totp_title' => 'Mobil Uygulama', 'mfa_option_totp_desc' => 'Çok aşamalı kimlik doğrulamayı kullanabilmek için Google Authenticator, Authy veya Microsoft Authenticator gibi TOTP destekleyen bir mobil uygulamaya ihtiyacınız olacaktır.', 'mfa_option_backup_codes_title' => 'Yedekleme Kodları', - 'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.', + 'mfa_option_backup_codes_desc' => 'Kimliğinizi doğrulamak için giriş yaparken kullanacağınız tek kullanımlık yedek kodlar oluşturur. Bunları güvenli ve sağlam bir yerde sakladığınızdan emin olun.', 'mfa_gen_confirm_and_enable' => 'Onayla ve aktive et', 'mfa_gen_backup_codes_title' => 'Yedekleme Kodları Kurulumu', 'mfa_gen_backup_codes_desc' => 'Aşağıdaki kod listesini güvenli bir yerde sakla. Sisteme giriş yaparken kodlardan birini ikinci bir kimlik doğrulama mekanizması olarak kullanabileceksin.', diff --git a/lang/tr/common.php b/lang/tr/common.php index 5b5e753873a..14baba8b2b1 100644 --- a/lang/tr/common.php +++ b/lang/tr/common.php @@ -20,7 +20,7 @@ 'description' => 'Açıklama', 'role' => 'Rol', 'cover_image' => 'Kapak resmi', - 'cover_image_description' => 'This image should be approximately 440x250px although it will be flexibly scaled & cropped to fit the user interface in different scenarios as required, so actual dimensions for display will differ.', + 'cover_image_description' => 'Bu görüntü yaklaşık 440x250px olmalıdır, ancak kullanıcı arayüzüne farklı senaryolarda uyacak şekilde esnek ölçeklendirilip kırpılacak, bu yüzden gerçek ekran boyutları farklı olacaktır.', // Actions 'actions' => 'İşlemler', diff --git a/lang/tr/editor.php b/lang/tr/editor.php index c020c82fddf..79a82fdf61b 100644 --- a/lang/tr/editor.php +++ b/lang/tr/editor.php @@ -13,7 +13,7 @@ 'cancel' => 'İptal', 'save' => 'Kaydet', 'close' => 'Kapat', - 'apply' => 'Apply', + 'apply' => 'Uygula', 'undo' => 'Geri al', 'redo' => 'Yeniden yap', 'left' => 'Sol', diff --git a/lang/tr/settings.php b/lang/tr/settings.php index 39ae23f2845..ac7ba9747ae 100644 --- a/lang/tr/settings.php +++ b/lang/tr/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovence', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/uk/settings.php b/lang/uk/settings.php index 521dc9578fd..0eae5a364c7 100644 --- a/lang/uk/settings.php +++ b/lang/uk/settings.php @@ -366,8 +366,9 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', - 'th' => 'ภาษาไทย', + 'th' => 'Тайська', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/uz/settings.php b/lang/uz/settings.php index c1d3b3d46e1..ed3421c914c 100644 --- a/lang/uz/settings.php +++ b/lang/uz/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/vi/settings.php b/lang/vi/settings.php index d029fe7f329..a3d3d549155 100644 --- a/lang/vi/settings.php +++ b/lang/vi/settings.php @@ -366,6 +366,7 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', 'th' => 'ภาษาไทย', 'tr' => 'Türkçe', diff --git a/lang/zh_CN/auth.php b/lang/zh_CN/auth.php index 4c97b46ce02..6cb68aad8ee 100644 --- a/lang/zh_CN/auth.php +++ b/lang/zh_CN/auth.php @@ -8,7 +8,7 @@ 'failed' => '用户名或密码错误。', 'throttle' => '您的登录次数过多,请在:seconds秒后重试。', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => '多因子认证尝试次数过多,请在 :seconds 秒后重试。', // Login & Register 'sign_up' => '注册', diff --git a/lang/zh_CN/entities.php b/lang/zh_CN/entities.php index 9daf816da04..bede7fb1c0d 100644 --- a/lang/zh_CN/entities.php +++ b/lang/zh_CN/entities.php @@ -173,7 +173,7 @@ 'books_sort_desc' => '在书籍内部移动章节与页面以重组内容;支持添加其他书籍,实现跨书籍便捷移动章节与页面;还可设置自动排序规则,在内容发生变更时自动对本书内容进行排序。', 'books_sort_auto_sort' => '自动排序选项', 'books_sort_auto_sort_active' => '自动排序已激活:::sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => '具备相关权限的用户可在 “列表与排序” 设置区域中创建自动排序选项规则。', 'books_sort_named' => '排序书籍「:bookName」', 'books_sort_name' => '按名称排序', 'books_sort_created' => '创建时间排序', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => '切换侧边栏', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => '页面内容', + 'page_contents_none' => '未在页面内容中找到任何标题。', + 'page_contents_info' => '目录是根据页面中使用的所有标题格式生成的。', 'page_tags' => '页面标签', 'chapter_tags' => '章节标签', 'book_tags' => '书籍标签', diff --git a/lang/zh_CN/settings.php b/lang/zh_CN/settings.php index eef14b1687d..c1fae2dce47 100644 --- a/lang/zh_CN/settings.php +++ b/lang/zh_CN/settings.php @@ -104,7 +104,7 @@ 'sort_rule_op_chapters_first' => '章节正序', 'sort_rule_op_chapters_last' => '章节倒序', 'sorting_page_limits' => '每页显示限制', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting_page_limits_desc' => '设置系统内各列表的单页显示数量。通常较少的数据量性能更佳,较多的数量则能减少用户的翻页操作。建议设置为 6 的倍数。', // Maintenance settings 'maint' => '维护', @@ -207,7 +207,7 @@ 'role_all' => '全部的', 'role_own' => '拥有的', 'role_controlled_by_asset' => '由其所在的资源来控制', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => '受页面删除权限限制', 'role_save' => '保存角色', 'role_users' => '此角色的用户', 'role_users_none' => '目前没有用户被分配到这个角色', @@ -366,8 +366,9 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', - 'th' => 'ภาษาไทย', + 'th' => '泰语', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/lang/zh_TW/activities.php b/lang/zh_TW/activities.php index 791fa26b752..ac1524ec433 100644 --- a/lang/zh_TW/activities.php +++ b/lang/zh_TW/activities.php @@ -99,8 +99,8 @@ 'user_update_notification' => '使用者已成功更新。', 'user_delete' => '已刪除使用者', 'user_delete_notification' => '使用者移除成功', - 'user_mfa_reset' => 'reset MFA for user', - 'user_mfa_reset_notification' => 'Multi-factor authentication methods reset', + 'user_mfa_reset' => '重設使用者的多要素驗證', + 'user_mfa_reset_notification' => '重設多要素驗證方法', // API Tokens 'api_token_create' => '建立 API 權杖', diff --git a/lang/zh_TW/auth.php b/lang/zh_TW/auth.php index 47e8ed950b2..7ac341e16b0 100644 --- a/lang/zh_TW/auth.php +++ b/lang/zh_TW/auth.php @@ -8,7 +8,7 @@ 'failed' => '使用者名稱或密碼錯誤。', 'throttle' => '您的登入次數過多,請在 :seconds 秒後重試。', - 'mfa_throttle' => 'Too many multi-factor verification attempts. Please try again in :seconds seconds.', + 'mfa_throttle' => '多要素驗證嘗試次數過多。請於 :seconds 秒後再試。', // Login & Register 'sign_up' => '註冊', diff --git a/lang/zh_TW/entities.php b/lang/zh_TW/entities.php index ba53d885dd4..4c5cfed1de2 100644 --- a/lang/zh_TW/entities.php +++ b/lang/zh_TW/entities.php @@ -173,7 +173,7 @@ 'books_sort_desc' => '在書籍中移動章節和頁面,重新安排其內容。可加入其他書籍,方便在書籍之間移動章節與頁面。可選擇設定自動排序規則,以便在變更時自動排序此書籍的內容。', 'books_sort_auto_sort' => '自動排序選項', 'books_sort_auto_sort_active' => '自動排序啟動::sortName', - 'books_sort_auto_sort_creation_hint' => 'Auto sort option rules can be created in the "Lists & Sorting" settings area by a user with the relevant permissions.', + 'books_sort_auto_sort_creation_hint' => '具備相關權限的使用者可在「清單與排序」設定區域中建立自動排序選項規則。', 'books_sort_named' => '排序書本 :bookName', 'books_sort_name' => '按名稱排序', 'books_sort_created' => '按建立時間排序', @@ -331,9 +331,9 @@ // Editor Sidebar 'toggle_sidebar' => '切換側邊欄', - 'page_contents' => 'Page Contents', - 'page_contents_none' => 'No headings were found in the page content.', - 'page_contents_info' => 'The contents menu is generated from any heading formats used in the page.', + 'page_contents' => '頁面內容', + 'page_contents_none' => '在頁面內容中未找到任何標題。', + 'page_contents_info' => '內容選單是根據頁面中使用的任何標題格式所產生的。', 'page_tags' => '頁面標籤', 'chapter_tags' => '章節標籤', 'book_tags' => '書本標籤', diff --git a/lang/zh_TW/errors.php b/lang/zh_TW/errors.php index e5c08ca141b..989ab0ad04f 100644 --- a/lang/zh_TW/errors.php +++ b/lang/zh_TW/errors.php @@ -125,7 +125,7 @@ 'api_incorrect_token_secret' => '給定使用的 API 權杖的密碼錯誤', 'api_user_no_api_permission' => '使用的 API 權杖擁有者無權呼叫 API', 'api_user_token_expired' => '使用的授權權杖已過期', - 'api_cookie_auth_only_get' => 'Only GET requests are allowed when using the API with cookie-based authentication', + 'api_cookie_auth_only_get' => '使用以 cookie 為基礎的驗證來呼叫 API 時,僅允許 GET 請求', // Settings & Maintenance 'maintenance_test_email_failure' => '寄送測試電子郵件時發生錯誤:', diff --git a/lang/zh_TW/settings.php b/lang/zh_TW/settings.php index 95108545019..e12f2caf5c6 100644 --- a/lang/zh_TW/settings.php +++ b/lang/zh_TW/settings.php @@ -104,7 +104,7 @@ 'sort_rule_op_chapters_first' => '第一章', 'sort_rule_op_chapters_last' => '最後一章', 'sorting_page_limits' => '每頁顯示限制', - 'sorting_page_limits_desc' => 'Set how many items to show per-page in various lists within the system. Typically a lower amount will be more performant, while a higher amount avoids the need to click through multiple pages. Using a multiple of 6 is recommended.', + 'sorting_page_limits_desc' => '設定系統內各清單每頁顯示的項目數量。通常項目數量較少時效能較佳,而數量較多則可避免使用者需點擊多頁瀏覽。建議採用 6 的倍數。', // Maintenance settings 'maint' => '維護', @@ -208,7 +208,7 @@ 'role_all' => '全部', 'role_own' => '擁有', 'role_controlled_by_asset' => '依據隸屬的資源來決定', - 'role_controlled_by_page_delete' => 'Controlled by page delete permissions', + 'role_controlled_by_page_delete' => '受頁面刪除權限控制', 'role_save' => '儲存角色', 'role_users' => '屬於此角色的使用者', 'role_users_none' => '目前沒有使用者被分配到此角色', @@ -265,9 +265,9 @@ 'users_mfa_desc' => '設定多重身份驗證為您的帳戶多增加了一道防線', 'users_mfa_x_methods' => ':count 個措施已配置|:count 個措施已配置', 'users_mfa_configure' => '方式設置', - 'users_mfa_reset' => 'Reset Multi-Factor Authentication Methods', - 'users_mfa_reset_desc' => 'This will reset and clear all configured multi-factor authentication methods for this user. If multi-factor authentication is required by any of their roles, they\'ll be prompted to configure new methods on their next login.', - 'users_mfa_reset_confirm' => 'Are you sure you want to reset multi-factor authentication for this user?', + 'users_mfa_reset' => '重設多要素驗證方式', + 'users_mfa_reset_desc' => '此操作將重設並清除該使用者所有已設定的多要素驗證方法。若其任何角色要求使用多要素驗證,系統將在下次登入時提示其設定新的驗證方法。', + 'users_mfa_reset_confirm' => '您確定要重設此使用者的多要素驗證嗎?', // API Tokens 'user_api_token_create' => '建立 API 權杖', @@ -367,8 +367,9 @@ 'ru' => 'Русский', 'sk' => 'Slovensky', 'sl' => 'Slovenščina', + 'sr' => 'Српски', 'sv' => 'Svenska', - 'th' => 'ภาษาไทย', + 'th' => '泰語', 'tr' => 'Türkçe', 'uk' => 'Українська', 'uz' => 'O‘zbekcha', diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bab28ea0eb3..54403adfefc 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -7,7 +7,7 @@ parameters: - app # The level 8 is the highest level - level: 4 + level: 5 phpVersion: min: 80200 diff --git a/resources/icons/chevron-left.svg b/resources/icons/chevron-left.svg new file mode 100644 index 00000000000..e64210047b5 --- /dev/null +++ b/resources/icons/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/interface.svg b/resources/icons/interface.svg new file mode 100644 index 00000000000..af1c5fc4975 --- /dev/null +++ b/resources/icons/interface.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/js/components/back-to-top.js b/resources/js/components/back-to-top.js deleted file mode 100644 index 046e640d10a..00000000000 --- a/resources/js/components/back-to-top.js +++ /dev/null @@ -1,58 +0,0 @@ -import {Component} from './component'; - -export class BackToTop extends Component { - - setup() { - this.button = this.$el; - this.targetElem = document.getElementById('header'); - this.showing = false; - this.breakPoint = 1200; - - if (document.body.classList.contains('flexbox')) { - this.button.style.display = 'none'; - return; - } - - this.button.addEventListener('click', this.scrollToTop.bind(this)); - window.addEventListener('scroll', this.onPageScroll.bind(this)); - } - - onPageScroll() { - const scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; - if (!this.showing && scrollTopPos > this.breakPoint) { - this.button.style.display = 'block'; - this.showing = true; - setTimeout(() => { - this.button.style.opacity = 0.4; - }, 1); - } else if (this.showing && scrollTopPos < this.breakPoint) { - this.button.style.opacity = 0; - this.showing = false; - setTimeout(() => { - this.button.style.display = 'none'; - }, 500); - } - } - - scrollToTop() { - const targetTop = this.targetElem.getBoundingClientRect().top; - const scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body; - const duration = 300; - const start = Date.now(); - const scrollStart = this.targetElem.getBoundingClientRect().top; - - function setPos() { - const percentComplete = (1 - ((Date.now() - start) / duration)); - const target = Math.abs(percentComplete * scrollStart); - if (percentComplete > 0) { - scrollElem.scrollTop = target; - requestAnimationFrame(setPos.bind(this)); - } else { - scrollElem.scrollTop = targetTop; - } - } - - requestAnimationFrame(setPos.bind(this)); - } - -} diff --git a/resources/js/components/back-to-top.ts b/resources/js/components/back-to-top.ts new file mode 100644 index 00000000000..e47a0478acc --- /dev/null +++ b/resources/js/components/back-to-top.ts @@ -0,0 +1,132 @@ +import {Component} from './component'; + +export class BackToTop extends Component { + + private container!: HTMLElement; + private button!: HTMLElement; + private progress!: HTMLElement; + private progressPath!: SVGPathElement; + private targetElem!: HTMLElement; + + private showing: boolean = false; + private breakPoint: number = 1200; + private isAnimating: boolean = false; + + setup(): void { + this.container = this.$el; + this.button = this.$refs.button; + this.progress = this.$refs.progress; + this.progressPath = this.$refs.progressPath as unknown as SVGPathElement; + + this.targetElem = document.getElementById('header') as HTMLElement; + + if (document.body.classList.contains('flexbox')) { + this.container.style.display = 'none'; + return; + } + + this.button.addEventListener('click', this.scrollToTop.bind(this)); + window.addEventListener('scroll', this.onPageScroll.bind(this)); + + this.setupProgressBar(); + } + + private setupProgressBar(): void { + this.button.addEventListener('transitionstart', event => { + if (event.target !== this.button) return; + this.isAnimating = true; + this.renderProgressPath(); + }); + const stopAnimating = (event: TransitionEvent) => { + if (event.target !== this.button) return; + this.isAnimating = false; + }; + this.button.addEventListener('transitionend', stopAnimating); + this.button.addEventListener('transitioncancel', stopAnimating); + this.renderProgressPath(); + } + + private onPageScroll(): void { + const scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; + if (!this.showing && scrollTopPos > this.breakPoint) { + this.container.style.display = 'block'; + this.showing = true; + setTimeout(() => { + this.container.style.opacity = '0.4'; + }, 1); + } else if (this.showing && scrollTopPos < this.breakPoint) { + this.container.style.opacity = '0'; + this.showing = false; + setTimeout(() => { + this.container.style.display = 'none'; + }, 500); + } + + if (this.showing) { + const maxScrollTop = document.documentElement.scrollHeight - window.innerHeight; + const scrollTopPercent = (scrollTopPos / maxScrollTop) * 100; + this.updateProgress(scrollTopPercent); + } + } + + private scrollToTop(): void { + const targetTop = this.targetElem.getBoundingClientRect().top; + const scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body; + const duration = 300; + const start = Date.now(); + const scrollStart = this.targetElem.getBoundingClientRect().top; + + const setPos = () => { + const percentComplete = (1 - ((Date.now() - start) / duration)); + const target = Math.abs(percentComplete * scrollStart); + if (percentComplete > 0) { + scrollElem.scrollTop = target; + requestAnimationFrame(setPos); + } else { + scrollElem.scrollTop = targetTop; + } + }; + + requestAnimationFrame(setPos); + } + + private renderProgressPath(): void { + const bounds = this.button.getBoundingClientRect(); + const progressInset = window.getComputedStyle(this.progress).insetInlineStart; + const offset = Math.abs(Number(progressInset.replace('px', '') || 0)); + const path = this.roundedRectPath(bounds.width, bounds.height, bounds.height / 2, offset); + this.progressPath.setAttribute('d', path); + if (this.isAnimating) { + window.requestAnimationFrame(this.renderProgressPath.bind(this)); + } + } + + private updateProgress(percentComplete: number): void { + if (percentComplete < 5) { + percentComplete = 5; + } + this.progressPath.setAttribute('stroke-dasharray', `${Math.ceil(percentComplete)} 100`); + } + + private roundedRectPath(w: number, h: number, r: number, offset: number = 1.5): string { + // Expand dimensions outward by the offset + const W = w + 2 * offset; + const H = h + 2 * offset; + + // The corner radius also grows by the offset + const R = Math.min(r + offset, W / 2, H / 2); + + return [ + `M ${W / 2} 0`, // start at top center + `H ${W - R}`, // line to top-right (before arc) + `A ${R} ${R} 0 0 1 ${W} ${R}`, // arc: top-right corner + `V ${H - R}`, // line down right edge + `A ${R} ${R} 0 0 1 ${W - R} ${H}`, // arc: bottom-right corner + `H ${R}`, // line across bottom edge + `A ${R} ${R} 0 0 1 ${0} ${H - R}`, // arc: bottom-left corner + `V ${R}`, // line up left edge + `A ${R} ${R} 0 0 1 ${R} ${0}`, // arc: top-left corner + `Z` // close path back to start + ].join(' '); + } +} diff --git a/resources/js/components/dropdown.js b/resources/js/components/dropdown.js index d2b044ee1ca..4da08f23189 100644 --- a/resources/js/components/dropdown.js +++ b/resources/js/components/dropdown.js @@ -12,7 +12,7 @@ export class Dropdown extends Component { this.container = this.$el; this.menu = this.$refs.menu; this.toggle = this.$refs.toggle; - this.moveMenu = this.$opts.moveMenu; + this.fixedPositionMenu = this.$opts.fixedPositionMenu === 'true'; this.bubbleEscapes = this.$opts.bubbleEscapes === 'true'; this.direction = (document.dir === 'rtl') ? 'right' : 'left'; @@ -33,13 +33,13 @@ export class Dropdown extends Component { const menuOriginalRect = this.menu.getBoundingClientRect(); let heightOffset = 0; const toggleHeight = this.toggle.getBoundingClientRect().height; - const containerBounds = findClosestScrollContainer(this.menu).getBoundingClientRect(); + const containerEl = this.fixedPositionMenu ? this.body : findClosestScrollContainer(this.menu); + const containerBounds = containerEl.getBoundingClientRect(); const dropUpwards = menuOriginalRect.bottom > containerBounds.bottom; const containerRect = this.container.getBoundingClientRect(); // If enabled, Move to body to prevent being trapped within scrollable sections - if (this.moveMenu) { - this.body.appendChild(this.menu); + if (this.fixedPositionMenu) { this.menu.style.position = 'fixed'; this.menu.style.width = `${menuOriginalRect.width}px`; this.menu.style.left = `${menuOriginalRect.left}px`; @@ -99,12 +99,11 @@ export class Dropdown extends Component { this.menu.style.bottom = ''; this.menu.style.maxHeight = ''; - if (this.moveMenu) { + if (this.fixedPositionMenu) { this.menu.style.position = ''; this.menu.style[this.direction] = ''; this.menu.style.width = ''; this.menu.style.left = ''; - this.container.appendChild(this.menu); } this.showing = false; @@ -125,10 +124,6 @@ export class Dropdown extends Component { this.hide(); }); - if (this.moveMenu) { - keyboardNavHandler.shareHandlingToEl(this.menu); - } - // Hide menu on option click this.container.addEventListener('click', event => { const possibleChildren = Array.from(this.menu.querySelectorAll('a')); diff --git a/resources/js/components/index.ts b/resources/js/components/index.ts index 688877227f2..e124046b9fc 100644 --- a/resources/js/components/index.ts +++ b/resources/js/components/index.ts @@ -30,6 +30,7 @@ export {GlobalSearch} from './global-search'; export {HeaderMobileToggle} from './header-mobile-toggle'; export {ImageManager} from './image-manager'; export {ImagePicker} from './image-picker'; +export {LayoutEditor} from './layout-editor'; export {ListSortControl} from './list-sort-control'; export {LoadingButton} from './loading-button'; export {MarkdownEditor} from './markdown-editor'; diff --git a/resources/js/components/layout-editor.ts b/resources/js/components/layout-editor.ts new file mode 100644 index 00000000000..babacc2765c --- /dev/null +++ b/resources/js/components/layout-editor.ts @@ -0,0 +1,82 @@ +import Sortable from "sortablejs"; +import {Component} from "./component"; +import {buildListActions, sortActionClickListener} from "../services/multi-lists"; + + +export class LayoutEditor extends Component { + protected input!: HTMLInputElement; + protected columns!: HTMLElement[]; + protected actionMenus!: HTMLElement[]; + + setup(): void { + this.input = this.$refs.input as HTMLInputElement; + this.columns = this.$manyRefs.column || []; + this.actionMenus = this.$manyRefs.actionMenu || []; + + this.initSortable(); + + const unusedColumn = this.columns.find(column => column.dataset.column === 'unused') as HTMLElement; + const listActions = buildListActions(...this.columns); + listActions.add = this.addBlockToLeastPopulated.bind(this); + listActions.remove = function (item: HTMLElement) { + unusedColumn.appendChild(item); + }; + const sortActionListener = sortActionClickListener(listActions, this.onChange.bind(this)); + for (const actionMenu of this.actionMenus) { + actionMenu.addEventListener('click', sortActionListener); + } + } + + protected initSortable(): void { + const sortAction = this.onChange.bind(this); + + for (const column of this.columns) { + new Sortable(column, { + group: 'layout-editor-blocks', + ghostClass: 'primary-background-light', + handle: '.handle', + animation: 150, + onSort: sortAction, + }); + } + } + + protected onChange(): void { + const configured: Record = {}; + for (const column of this.columns) { + const location = column.dataset.column || ''; + if (!location) continue; + configured[location] = []; + + const blockNodes = column.children; + for (let i = 0; i < blockNodes.length; i++) { + const blockNode = blockNodes[i] as HTMLElement; + const blockId = blockNode.dataset.blockId || ''; + if (!blockId) continue; + configured[location].push(blockId); + } + } + + this.input.value = JSON.stringify(configured, null, 2); + } + + protected addBlockToLeastPopulated(item: HTMLElement): void { + let populationCount = 100; + let leastPopulated: null|HTMLElement = null + for (const column of this.columns) { + const blockCount = column.children.length; + if (column.dataset.column === 'unused') { + continue; + } + + if (blockCount < populationCount) { + leastPopulated = column; + populationCount = blockCount; + } + } + + if (leastPopulated) { + leastPopulated.appendChild(item); + } + } +} \ No newline at end of file diff --git a/resources/js/components/shelf-sort.js b/resources/js/components/shelf-sort.js index b56b01980a1..593bf03a6a9 100644 --- a/resources/js/components/shelf-sort.js +++ b/resources/js/components/shelf-sort.js @@ -1,6 +1,6 @@ import Sortable from 'sortablejs'; import {Component} from './component'; -import {buildListActions, sortActionClickListener} from '../services/dual-lists.ts'; +import {buildListActions, sortActionClickListener} from '../services/multi-lists.ts'; export class ShelfSort extends Component { diff --git a/resources/js/components/sort-rule-manager.ts b/resources/js/components/sort-rule-manager.ts index ff08f4ab878..70d885815b0 100644 --- a/resources/js/components/sort-rule-manager.ts +++ b/resources/js/components/sort-rule-manager.ts @@ -1,6 +1,6 @@ import {Component} from "./component.js"; import Sortable from "sortablejs"; -import {buildListActions, sortActionClickListener} from "../services/dual-lists"; +import {buildListActions, sortActionClickListener} from "../services/multi-lists"; export class SortRuleManager extends Component { diff --git a/resources/js/services/dom.ts b/resources/js/services/dom.ts index 8696fe81639..16942d5033b 100644 --- a/resources/js/services/dom.ts +++ b/resources/js/services/dom.ts @@ -259,7 +259,7 @@ export function hashElement(element: HTMLElement): string { } /** - * Find the closest scroll container parent for the given element + * Find the closest scroll container parent for the given element, * otherwise will default to the body element. */ export function findClosestScrollContainer(start: HTMLElement): HTMLElement { diff --git a/resources/js/services/events.ts b/resources/js/services/events.ts index 6045d51f823..1b39e413fbb 100644 --- a/resources/js/services/events.ts +++ b/resources/js/services/events.ts @@ -67,8 +67,10 @@ export class EventManager { * Notify of standard server-provided validation errors. */ showValidationErrors(responseErr: HttpError): void { - if (responseErr.status === 422 && responseErr.data) { - const message = Object.values(responseErr.data).flat().join('\n'); + if (responseErr.status === 422 && responseErr.data instanceof Object) { + const data = responseErr.data; + const errorValues = Object.values(data.errors ?? data); + const message = errorValues.flat().join('\n'); this.error(message); } } diff --git a/resources/js/services/http.ts b/resources/js/services/http.ts index f9eaafc3912..e8787ad8de9 100644 --- a/resources/js/services/http.ts +++ b/resources/js/services/http.ts @@ -192,9 +192,8 @@ export class HttpManager { } /** - * Parse the response text for an error response to a user - * presentable string. Handles a range of errors responses including - * validation responses & server response text. + * Parse the response text for an error response to a user-presentable string. + * Handles a range of error responses, including validation responses and server response text. */ protected formatErrorResponseText(text: string): string { const data = text.startsWith('{') ? JSON.parse(text) : {message: text}; @@ -206,13 +205,13 @@ export class HttpManager { return data.message || data.error; } - const values = Object.values(data); - const isValidation = values.every(val => { + const errorValues = Object.values(data.errors || {}); + const isValidation = errorValues.length > 0 && errorValues.every(val => { return Array.isArray(val) && val.every(x => typeof x === 'string'); }); if (isValidation) { - return values.flat().join(' '); + return errorValues.flat().join(' '); } return text; diff --git a/resources/js/services/keyboard-navigation.ts b/resources/js/services/keyboard-navigation.ts index 13fbdfecc9d..70f99330d38 100644 --- a/resources/js/services/keyboard-navigation.ts +++ b/resources/js/services/keyboard-navigation.ts @@ -87,7 +87,9 @@ export class KeyboardNavigationHandler { const focusable: HTMLElement[] = []; const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])'; for (const container of this.containers) { - const toAdd = [...container.querySelectorAll(selector)].filter(e => isHTMLElement(e)); + const toAdd = [...container.querySelectorAll(selector)].filter(e => { + return isHTMLElement(e) && e.checkVisibility(); + }) as HTMLElement[]; focusable.push(...toAdd); } diff --git a/resources/js/services/dual-lists.ts b/resources/js/services/multi-lists.ts similarity index 61% rename from resources/js/services/dual-lists.ts rename to resources/js/services/multi-lists.ts index 98f2af92daf..29c88d23837 100644 --- a/resources/js/services/dual-lists.ts +++ b/resources/js/services/multi-lists.ts @@ -1,13 +1,12 @@ /** * Service for helping manage common dual-list scenarios. - * (Shelf book manager, sort set manager). + * (Shelf book manager, sort set manager, layout-editor). */ type ListActionsSet = Record void)>; export function buildListActions( - availableList: HTMLElement, - configuredList: HTMLElement, + ...lists: HTMLElement[] ): ListActionsSet { return { move_up(item) { @@ -22,11 +21,29 @@ export function buildListActions( const newIndex = Math.min(index + 2, list.children.length); list.insertBefore(item, list.children[newIndex] || null); }, + move_right(item) { + const list = item.parentNode as HTMLElement; + const listIndex = lists.indexOf(list); + const targetListIndex = Math.min(listIndex + 1, lists.length - 1); + lists[targetListIndex].appendChild(item); + }, + move_left(item) { + const list = item.parentNode as HTMLElement; + const listIndex = lists.indexOf(list); + const targetListIndex = Math.max(listIndex - 1, 0); + lists[targetListIndex].appendChild(item); + }, remove(item) { - availableList.appendChild(item); + const otherList = lists.find(list => list !== item.parentNode); + if (otherList) { + otherList.appendChild(item); + } }, add(item) { - configuredList.appendChild(item); + const otherList = lists.find(list => list !== item.parentNode); + if (otherList) { + otherList.appendChild(item); + } }, }; } diff --git a/resources/sass/_components.scss b/resources/sass/_components.scss index 8608427d8e8..ba53bfb97f2 100644 --- a/resources/sass/_components.scss +++ b/resources/sass/_components.scss @@ -1098,16 +1098,24 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group { // Back to top link $btt-size: 40px; -.back-to-top { - background-color: var(--color-primary); +.back-to-top-container { position: fixed; bottom: vars.$m; right: vars.$l; + opacity: 0; + z-index: 999; + transition: opacity ease-in-out 180ms; + &:hover { + opacity: 1 !important; + } +} +.back-to-top { + background-color: var(--color-primary); padding: 5px 7px; cursor: pointer; color: #FFF; fill: #FFF; - svg { + .inner svg { width: math.div($btt-size, 1.5); height: math.div($btt-size, 1.5); margin-inline-end: 4px; @@ -1116,8 +1124,6 @@ $btt-size: 40px; height: $btt-size; border-radius: $btt-size; transition: all ease-in-out 180ms; - opacity: 0; - z-index: 999; overflow: hidden; &:hover { width: $btt-size*3.4; @@ -1125,6 +1131,7 @@ $btt-size: 40px; } .inner { width: $btt-size*3.4; + text-align: start; } span { position: relative; @@ -1133,6 +1140,23 @@ $btt-size: 40px; } } +.back-to-top-progress { + position: absolute; + top: -3px; + inset-inline-start: -3px; + width: 100%; + height: 100%; + pointer-events: none; + overflow: visible; + path { + fill: none; + stroke: var(--color-primary); + stroke-width: 2px; + stroke-linecap: butt; + transition: stroke-dasharray ease-in-out 120ms; + } +} + // Sortable scroll boxes .scroll-box { list-style: none; @@ -1222,11 +1246,19 @@ input.scroll-box-search, .scroll-box-header-item { display: none; } -.scroll-box > li.empty-state { +.scroll-box.layout-editor-column-left [data-action="move_left"], +.scroll-box.layout-editor-column-right [data-action="move_right"], +.scroll-box.layout-editor-column-unused [data-action="move_right"], +.scroll-box.layout-editor-column-unused [data-action="move_left"], +.scroll-box.layout-editor-column-unused [data-action="remove"], +.scroll-box.layout-editor-column-left [data-action="add"], +.scroll-box.layout-editor-column-right [data-action="add"], +.scroll-box.layout-editor-column-center [data-action="add"], +{ display: none; } -.scroll-box > li.empty-state:last-child { - display: list-item; +.scroll-box:has(> li:not(.empty-state)) .empty-state { + display: none; } details.section-expander summary { diff --git a/resources/views/books/index.blade.php b/resources/views/books/index.blade.php index 660c008dfb1..6e311a88bae 100644 --- a/resources/views/books/index.blade.php +++ b/resources/views/books/index.blade.php @@ -5,11 +5,9 @@ @stop @section('left') - @include('books.parts.index-sidebar-section-recents', ['recents' => $recents]) - @include('books.parts.index-sidebar-section-popular', ['popular' => $popular]) - @include('books.parts.index-sidebar-section-new', ['new' => $new]) + @include('common.view-blocks', ['location' => 'books-index', 'position' => 'left']) @stop @section('right') - @include('books.parts.index-sidebar-section-actions', ['view' => $view]) + @include('common.view-blocks', ['location' => 'books-index', 'position' => 'right']) @stop diff --git a/resources/views/books/parts/show-sidebar-section-details.blade.php b/resources/views/books/parts/show-sidebar-section-details.blade.php index 709d0ffd9a1..2c3d6d141dd 100644 --- a/resources/views/books/parts/show-sidebar-section-details.blade.php +++ b/resources/views/books/parts/show-sidebar-section-details.blade.php @@ -1,7 +1,7 @@
{{ trans('common.details') }}
+ +
+ + + +@stop diff --git a/resources/views/settings/layouts/parts/block-column.blade.php b/resources/views/settings/layouts/parts/block-column.blade.php new file mode 100644 index 00000000000..20da8dff72f --- /dev/null +++ b/resources/views/settings/layouts/parts/block-column.blade.php @@ -0,0 +1,17 @@ +{{-- +$columnBlocks - array - Blocks to list +$label - string - Section title +$id - string - identifier for location/column +--}} +
+ +
    +
  • {{ trans('preferences.layout_edit_empty') }}
  • + @foreach($columnBlocks as $block) + @include('settings.layouts.parts.block', ['block' => $block]) + @endforeach +
+
\ No newline at end of file diff --git a/resources/views/settings/layouts/parts/block.blade.php b/resources/views/settings/layouts/parts/block.blade.php new file mode 100644 index 00000000000..1885c4254cf --- /dev/null +++ b/resources/views/settings/layouts/parts/block.blade.php @@ -0,0 +1,23 @@ +@php /** @var $block class-string<\BookStack\View\ViewBlockInterface> */ @endphp +
  • +
    @icon('grip')
    +
    {{ $block::getLabel() }}
    + +
  • \ No newline at end of file diff --git a/resources/views/shelves/index.blade.php b/resources/views/shelves/index.blade.php index 70357068d7e..87c6392983f 100644 --- a/resources/views/shelves/index.blade.php +++ b/resources/views/shelves/index.blade.php @@ -4,12 +4,10 @@ @include('shelves.parts.list', ['shelves' => $shelves, 'view' => $view, 'listOptions' => $listOptions]) @stop -@section('right') - @include('shelves.parts.index-sidebar-section-actions', ['view' => $view]) +@section('left') + @include('common.view-blocks', ['location' => 'shelves-index', 'position' => 'left']) @stop -@section('left') - @include('shelves.parts.index-sidebar-section-recents', ['recents' => $recents]) - @include('shelves.parts.index-sidebar-section-popular', ['popular' => $popular]) - @include('shelves.parts.index-sidebar-section-new', ['new' => $new]) -@stop \ No newline at end of file +@section('right') + @include('common.view-blocks', ['location' => 'shelves-index', 'position' => 'right']) +@stop diff --git a/resources/views/shelves/show.blade.php b/resources/views/shelves/show.blade.php index 9d07e5da018..b942f35ff7a 100644 --- a/resources/views/shelves/show.blade.php +++ b/resources/views/shelves/show.blade.php @@ -69,15 +69,9 @@ @stop @section('left') - @include('shelves.parts.show-sidebar-section-tags', ['shelf' => $shelf]) - @include('shelves.parts.show-sidebar-section-details', ['shelf' => $shelf]) - @include('shelves.parts.show-sidebar-section-activity', ['activity' => $activity]) + @include('common.view-blocks', ['location' => 'shelves-show', 'position' => 'left']) @stop @section('right') - @include('shelves.parts.show-sidebar-section-actions', ['shelf' => $shelf, 'view' => $view]) + @include('common.view-blocks', ['location' => 'shelves-show', 'position' => 'right']) @stop - - - - diff --git a/resources/views/users/account/interface.blade.php b/resources/views/users/account/interface.blade.php new file mode 100644 index 00000000000..7d78f08c81b --- /dev/null +++ b/resources/views/users/account/interface.blade.php @@ -0,0 +1,54 @@ +@extends('users.account.layout') + +@section('main') +
    +
    + {{ method_field('put') }} + {{ csrf_field() }} + +

    {{ trans('preferences.interface') }}

    +

    {{ trans('preferences.interface_desc') }}

    + +
    + @include('users.parts.language-option-row', ['value' => old('language') ?? user()->getLocale()->appLocale()]) + @include('users.account.parts.display-mode-option-row') +
    + +
    + +
    + +
    +
    + +
    +

    {{ trans('preferences.layouts') }}

    +

    {{ trans('preferences.layouts_desc') }}

    + +
    + @foreach($namedLocations as $locationKey => $locationName) + + @endforeach +
    + +
    + +
    +
    +
    +

    {{ trans('preferences.shortcuts_interface') }}

    +

    {{ trans('preferences.shortcuts_overview_desc') }}

    +
    + +
    +
    +@stop diff --git a/resources/views/users/account/layout.blade.php b/resources/views/users/account/layout.blade.php index df8ebc2d904..94f1f328de0 100644 --- a/resources/views/users/account/layout.blade.php +++ b/resources/views/users/account/layout.blade.php @@ -11,6 +11,7 @@