diff --git a/.gitignore b/.gitignore index 5f41a864e..e7e053505 100644 --- a/.gitignore +++ b/.gitignore @@ -8,16 +8,15 @@ Homestead.yaml /public/css /public/js /public/bower +/public/build/ /storage/images _ide_helper.php /storage/debugbar .phpstorm.meta.php yarn.lock /bin +nbproject .buildpath - .project - .settings/org.eclipse.wst.common.project.facet.core.xml - .settings/org.eclipse.php.core.prefs diff --git a/.travis.yml b/.travis.yml index 909e3e1f4..839d3be3f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,4 +25,4 @@ after_failure: - cat storage/logs/laravel.log script: - - phpunit \ No newline at end of file + - phpunit diff --git a/app/Comment.php b/app/Comment.php new file mode 100644 index 000000000..de01b6212 --- /dev/null +++ b/app/Comment.php @@ -0,0 +1,96 @@ +morphTo('entity'); + } + + /** + * Get the page that this comment is in. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function page() + { + return $this->belongsTo(Page::class); + } + + /** + * Get the owner of this comment. + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /* + * Not being used, but left here because might be used in the future for performance reasons. + */ + public function getPageComments($pageId) { + $query = static::newQuery(); + $query->join('users AS u', 'comments.created_by', '=', 'u.id'); + $query->leftJoin('users AS u1', 'comments.updated_by', '=', 'u1.id'); + $query->leftJoin('images AS i', 'i.id', '=', 'u.image_id'); + $query->selectRaw('comments.id, text, html, comments.created_by, comments.updated_by, ' + . 'comments.created_at, comments.updated_at, comments.parent_id, ' + . 'u.name AS created_by_name, u1.name AS updated_by_name, ' + . 'i.url AS avatar '); + $query->whereRaw('page_id = ?', [$pageId]); + $query->orderBy('created_at'); + return $query->get(); + } + + public function getAllPageComments($pageId) { + return self::where('page_id', '=', $pageId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->get(); + } + + public function getCommentById($commentId) { + return self::where('id', '=', $commentId)->with(['createdBy' => function($query) { + $query->select('id', 'name', 'image_id'); + }, 'updatedBy' => function($query) { + $query->select('id', 'name'); + }, 'createdBy.avatar' => function ($query) { + $query->select('id', 'path', 'url'); + }])->first(); + } + + public function getCreatedAttribute() { + $created = [ + 'day_time_str' => $this->created_at->toDayDateTimeString(), + 'diff' => $this->created_at->diffForHumans() + ]; + return $created; + } + + public function getUpdatedAttribute() { + if (empty($this->updated_at)) { + return null; + } + $updated = [ + 'day_time_str' => $this->updated_at->toDayDateTimeString(), + 'diff' => $this->updated_at->diffForHumans() + ]; + return $updated; + } + + public function getSubCommentsAttribute() { + return $this->sub_comments; + } +} diff --git a/app/Console/Commands/UpgradeDatabaseEncoding.php b/app/Console/Commands/UpgradeDatabaseEncoding.php new file mode 100644 index 000000000..a17fc9523 --- /dev/null +++ b/app/Console/Commands/UpgradeDatabaseEncoding.php @@ -0,0 +1,57 @@ +option('database') !== null) { + DB::setDefaultConnection($this->option('database')); + } + + $database = DB::getDatabaseName(); + $tables = DB::select('SHOW TABLES'); + $this->line('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + $this->line('USE `'.$database.'`;'); + $key = 'Tables_in_' . $database; + foreach ($tables as $table) { + $tableName = $table->$key; + $this->line('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + } + + DB::setDefaultConnection($connection); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 4fa0b3c80..af9f5fd46 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -15,7 +15,8 @@ class Kernel extends ConsoleKernel Commands\ClearActivity::class, Commands\ClearRevisions::class, Commands\RegeneratePermissions::class, - Commands\RegenerateSearch::class + Commands\RegenerateSearch::class, + Commands\UpgradeDatabaseEncoding::class ]; /** diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index 8b0ef309a..9a23fe2a1 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -8,6 +8,7 @@ use BookStack\Exceptions\UserRegistrationException; use BookStack\Repos\UserRepo; use BookStack\Services\EmailConfirmationService; use BookStack\Services\SocialAuthService; +use BookStack\SocialAccount; use BookStack\User; use Exception; use Illuminate\Http\Request; @@ -103,7 +104,7 @@ class RegisterController extends Controller * @param Request|\Illuminate\Http\Request $request * @return Response * @throws UserRegistrationException - * @throws \Illuminate\Foundation\Validation\ValidationException + * @throws \Illuminate\Validation\ValidationException */ public function postRegister(Request $request) { @@ -255,16 +256,13 @@ class RegisterController extends Controller */ public function socialCallback($socialDriver) { - if (session()->has('social-callback')) { - $action = session()->pull('social-callback'); - if ($action == 'login') { - return $this->socialAuthService->handleLoginCallback($socialDriver); - } elseif ($action == 'register') { - return $this->socialRegisterCallback($socialDriver); - } - } else { + if (!session()->has('social-callback')) { throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login'); } + + $action = session()->pull('social-callback'); + if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver); + if ($action == 'register') return $this->socialRegisterCallback($socialDriver); return redirect()->back(); } diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php new file mode 100644 index 000000000..e8d5eab30 --- /dev/null +++ b/app/Http/Controllers/CommentController.php @@ -0,0 +1,99 @@ +entityRepo = $entityRepo; + $this->commentRepo = $commentRepo; + $this->comment = $comment; + parent::__construct(); + } + + public function save(Request $request, $pageId, $commentId = null) + { + $this->validate($request, [ + 'text' => 'required|string', + 'html' => 'required|string', + ]); + + try { + $page = $this->entityRepo->getById('page', $pageId, true); + } catch (ModelNotFoundException $e) { + return response('Not found', 404); + } + + if($page->draft) { + // cannot add comments to drafts. + return response()->json([ + 'status' => 'error', + 'message' => trans('errors.cannot_add_comment_to_draft'), + ], 400); + } + + $this->checkOwnablePermission('page-view', $page); + if (empty($commentId)) { + // create a new comment. + $this->checkPermission('comment-create-all'); + $comment = $this->commentRepo->create($page, $request->only(['text', 'html', 'parent_id'])); + $respMsg = trans('entities.comment_created'); + } else { + // update existing comment + // get comment by ID and check if this user has permission to update. + $comment = $this->comment->findOrFail($commentId); + $this->checkOwnablePermission('comment-update', $comment); + $this->commentRepo->update($comment, $request->all()); + $respMsg = trans('entities.comment_updated'); + } + + $comment = $this->commentRepo->getCommentById($comment->id); + + return response()->json([ + 'status' => 'success', + 'message' => $respMsg, + 'comment' => $comment + ]); + + } + + public function destroy($id) { + $comment = $this->comment->findOrFail($id); + $this->checkOwnablePermission('comment-delete', $comment); + $this->commentRepo->delete($comment); + $updatedComment = $this->commentRepo->getCommentById($comment->id); + + return response()->json([ + 'status' => 'success', + 'message' => trans('entities.comment_deleted'), + 'comment' => $updatedComment + ]); + } + + + public function getPageComments($pageId) { + try { + $page = $this->entityRepo->getById('page', $pageId, true); + } catch (ModelNotFoundException $e) { + return response('Not found', 404); + } + + $this->checkOwnablePermission('page-view', $page); + + $comments = $this->commentRepo->getPageComments($pageId); + return response()->json(['status' => 'success', 'comments'=> $comments['comments'], + 'total' => $comments['total'], 'permissions' => [ + 'comment_create' => $this->currentUser->can('comment-create-all'), + 'comment_update_own' => $this->currentUser->can('comment-update-own'), + 'comment_update_all' => $this->currentUser->can('comment-update-all'), + 'comment_delete_all' => $this->currentUser->can('comment-delete-all'), + 'comment_delete_own' => $this->currentUser->can('comment-delete-own'), + ], 'user_id' => $this->currentUser->id]); + } +} diff --git a/app/Http/Controllers/PageController.php b/app/Http/Controllers/PageController.php index c97597bc4..9a8525c23 100644 --- a/app/Http/Controllers/PageController.php +++ b/app/Http/Controllers/PageController.php @@ -161,7 +161,7 @@ class PageController extends Controller $pageContent = $this->entityRepo->renderPage($page); $sidebarTree = $this->entityRepo->getBookChildren($page->book); $pageNav = $this->entityRepo->getPageNav($pageContent); - + Views::add($page); $this->setPageTitle($page->getShortName()); return view('pages/show', [ @@ -376,7 +376,7 @@ class PageController extends Controller $page->fill($revision->toArray()); $this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()])); - + return view('pages/revision', [ 'page' => $page, 'book' => $page->book, diff --git a/app/Page.php b/app/Page.php index c9823e7e4..d722e4e54 100644 --- a/app/Page.php +++ b/app/Page.php @@ -66,6 +66,10 @@ class Page extends Entity return $this->hasMany(Attachment::class, 'uploaded_to')->orderBy('order', 'asc'); } + public function comments() { + return $this->hasMany(Comment::class, 'page_id')->orderBy('created_on', 'asc'); + } + /** * Get the url for this page. * @param string|bool $path diff --git a/app/Repos/CommentRepo.php b/app/Repos/CommentRepo.php new file mode 100644 index 000000000..ce71b9234 --- /dev/null +++ b/app/Repos/CommentRepo.php @@ -0,0 +1,105 @@ +comment = $comment; + } + + public function create (Page $page, $data = []) { + $userId = user()->id; + $comment = $this->comment->newInstance(); + $comment->fill($data); + // new comment + $comment->page_id = $page->id; + $comment->created_by = $userId; + $comment->updated_at = null; + $comment->save(); + return $comment; + } + + public function update($comment, $input, $activeOnly = true) { + $userId = user()->id; + $comment->updated_by = $userId; + $comment->fill($input); + + // only update active comments by default. + $whereClause = ['active' => 1]; + if (!$activeOnly) { + $whereClause = []; + } + $comment->update($whereClause); + return $comment; + } + + public function delete($comment) { + $comment->text = trans('entities.comment_deleted'); + $comment->html = trans('entities.comment_deleted'); + $comment->active = false; + $userId = user()->id; + $comment->updated_by = $userId; + $comment->save(); + return $comment; + } + + public function getPageComments($pageId) { + $comments = $this->comment->getAllPageComments($pageId); + $index = []; + $totalComments = count($comments); + $finalCommentList = []; + + // normalizing the response. + for ($i = 0; $i < count($comments); ++$i) { + $comment = $this->normalizeComment($comments[$i]); + $parentId = $comment->parent_id; + if (empty($parentId)) { + $finalCommentList[] = $comment; + $index[$comment->id] = $comment; + continue; + } + + if (empty($index[$parentId])) { + // weird condition should not happen. + continue; + } + if (empty($index[$parentId]->sub_comments)) { + $index[$parentId]->sub_comments = []; + } + array_push($index[$parentId]->sub_comments, $comment); + $index[$comment->id] = $comment; + } + return [ + 'comments' => $finalCommentList, + 'total' => $totalComments + ]; + } + + public function getCommentById($commentId) { + return $this->normalizeComment($this->comment->getCommentById($commentId)); + } + + private function normalizeComment($comment) { + if (empty($comment)) { + return; + } + $comment->createdBy->avatar_url = $comment->createdBy->getAvatar(50); + $comment->createdBy->profile_url = $comment->createdBy->getProfileUrl(); + if (!empty($comment->updatedBy)) { + $comment->updatedBy->profile_url = $comment->updatedBy->getProfileUrl(); + } + return $comment; + } +} \ No newline at end of file diff --git a/app/Repos/EntityRepo.php b/app/Repos/EntityRepo.php index 7bc5fc4fc..d87c40f9b 100644 --- a/app/Repos/EntityRepo.php +++ b/app/Repos/EntityRepo.php @@ -571,7 +571,7 @@ class EntityRepo $draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id); $draftPage->html = $this->formatHtml($input['html']); - $draftPage->text = strip_tags($draftPage->html); + $draftPage->text = $this->pageToPlainText($draftPage); $draftPage->draft = false; $draftPage->revision_count = 1; @@ -713,6 +713,17 @@ class EntityRepo return $content; } + /** + * Get the plain text version of a page's content. + * @param Page $page + * @return string + */ + public function pageToPlainText(Page $page) + { + $html = $this->renderPage($page); + return strip_tags($html); + } + /** * Get a new draft page instance. * @param Book $book @@ -816,7 +827,7 @@ class EntityRepo $userId = user()->id; $page->fill($input); $page->html = $this->formatHtml($input['html']); - $page->text = strip_tags($page->html); + $page->text = $this->pageToPlainText($page); if (setting('app-editor') !== 'markdown') $page->markdown = ''; $page->updated_by = $userId; $page->revision_count++; @@ -933,7 +944,7 @@ class EntityRepo $revision = $page->revisions()->where('id', '=', $revisionId)->first(); $page->fill($revision->toArray()); $page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id); - $page->text = strip_tags($page->html); + $page->text = $this->pageToPlainText($page); $page->updated_by = user()->id; $page->save(); $this->searchService->indexEntity($page); @@ -953,7 +964,7 @@ class EntityRepo if ($page->draft) { $page->fill($data); if (isset($data['html'])) { - $page->text = strip_tags($data['html']); + $page->text = $this->pageToPlainText($page); } $page->save(); return $page; diff --git a/app/Services/PermissionService.php b/app/Services/PermissionService.php index c6c981337..93787a3e5 100644 --- a/app/Services/PermissionService.php +++ b/app/Services/PermissionService.php @@ -468,7 +468,7 @@ class PermissionService $action = end($explodedPermission); $this->currentAction = $action; - $nonJointPermissions = ['restrictions', 'image', 'attachment']; + $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; // Handle non entity specific jointPermissions if (in_array($explodedPermission[0], $nonJointPermissions)) { diff --git a/config/app.php b/config/app.php index 48348f837..a390eaf83 100644 --- a/config/app.php +++ b/config/app.php @@ -58,7 +58,7 @@ return [ */ 'locale' => env('APP_LANG', 'en'), - 'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja'], + 'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk', 'ja', 'pl'], /* |-------------------------------------------------------------------------- diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index ebf78d1fa..b03e34b9b 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -70,4 +70,14 @@ $factory->define(BookStack\Image::class, function ($faker) { 'type' => 'gallery', 'uploaded_to' => 0 ]; +}); + +$factory->define(BookStack\Comment::class, function($faker) { + $text = $faker->paragraph(3); + $html = '
' . $text. '
'; + return [ + 'html' => $html, + 'text' => $text, + 'active' => 1 + ]; }); \ No newline at end of file diff --git a/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php b/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php index 550c95826..5681013ad 100644 --- a/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php +++ b/database/migrations/2017_07_02_152834_update_db_encoding_to_ut8mb4.php @@ -1,7 +1,5 @@ setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); - $pdo->exec('ALTER DATABASE `'.$database.'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); - $key = 'Tables_in_' . $database; - foreach ($tables as $table) { - $tableName = $table->$key; - $pdo->exec('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci'); - } + // Migration removed due to issues during live migration. + // Instead you can run the command `artisan bookstack:db-utf8mb4` + // which will generate out the SQL request to upgrade your DB to utf8mb4. } /** @@ -32,15 +23,6 @@ class UpdateDbEncodingToUt8mb4 extends Migration */ public function down() { - $database = DB::getDatabaseName(); - $tables = DB::select('SHOW TABLES'); - $pdo = DB::getPdo(); - $pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); - $pdo->exec('ALTER DATABASE `'.$database.'` CHARACTER SET utf8 COLLATE utf8_unicode_ci'); - $key = 'Tables_in_' . $database; - foreach ($tables as $table) { - $tableName = $table->$key; - $pdo->exec('ALTER TABLE `'.$tableName.'` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci'); - } + // } } diff --git a/database/migrations/2017_08_01_130541_create_comments_table.php b/database/migrations/2017_08_01_130541_create_comments_table.php new file mode 100644 index 000000000..bfb7eecbf --- /dev/null +++ b/database/migrations/2017_08_01_130541_create_comments_table.php @@ -0,0 +1,66 @@ +increments('id')->unsigned(); + $table->integer('page_id')->unsigned(); + $table->longText('text')->nullable(); + $table->longText('html')->nullable(); + $table->integer('parent_id')->unsigned()->nullable(); + $table->integer('created_by')->unsigned(); + $table->integer('updated_by')->unsigned()->nullable(); + $table->boolean('active')->default(true); + + $table->index(['page_id']); + $table->timestamps(); + + // Assign new comment permissions to admin role + $adminRoleId = DB::table('roles')->where('system_name', '=', 'admin')->first()->id; + // Create & attach new entity permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permissionId = DB::table('role_permissions')->insertGetId([ + 'name' => strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)), + 'display_name' => $op . ' ' . $entity . 's', + 'created_at' => \Carbon\Carbon::now()->toDateTimeString(), + 'updated_at' => \Carbon\Carbon::now()->toDateTimeString() + ]); + DB::table('permission_role')->insert([ + 'role_id' => $adminRoleId, + 'permission_id' => $permissionId + ]); + } + + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('comments'); + // Delete comment role permissions + $ops = ['Create All', 'Create Own', 'Update All', 'Update Own', 'Delete All', 'Delete Own']; + $entity = 'Comment'; + foreach ($ops as $op) { + $permName = strtolower($entity) . '-' . strtolower(str_replace(' ', '-', $op)); + DB::table('role_permissions')->where('name', '=', $permName)->delete(); + } + } +} diff --git a/database/seeds/DummyContentSeeder.php b/database/seeds/DummyContentSeeder.php index 3d92efab1..996cd178d 100644 --- a/database/seeds/DummyContentSeeder.php +++ b/database/seeds/DummyContentSeeder.php @@ -20,7 +20,10 @@ class DummyContentSeeder extends Seeder ->each(function($book) use ($user) { $chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id]) ->each(function($chapter) use ($user, $book){ - $pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]); + $pages = factory(\BookStack\Page::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id])->each(function($page) use ($user) { + $comments = factory(\BookStack\Comment::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'page_id' => $page->id]); + $page->comments()->saveMany($comments); + }); $chapter->pages()->saveMany($pages); }); $pages = factory(\BookStack\Page::class, 3)->make(['created_by' => $user->id, 'updated_by' => $user->id]); diff --git a/gulpfile.js b/gulpfile.js index b72bb366d..f851dd7d6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,3 +1,5 @@ +'use strict'; + const argv = require('yargs').argv; const gulp = require('gulp'), plumber = require('gulp-plumber'); @@ -12,8 +14,10 @@ const babelify = require("babelify"); const watchify = require("watchify"); const envify = require("envify"); const gutil = require("gulp-util"); +const liveReload = require('gulp-livereload'); if (argv.production) process.env.NODE_ENV = 'production'; +let isProduction = argv.production || process.env.NODE_ENV === 'production'; gulp.task('styles', () => { let chain = gulp.src(['resources/assets/sass/**/*.scss']) @@ -24,31 +28,40 @@ gulp.task('styles', () => { }})) .pipe(sass()) .pipe(autoprefixer('last 2 versions')); - if (argv.production) chain = chain.pipe(minifycss()); - return chain.pipe(gulp.dest('public/css/')); + if (isProduction) chain = chain.pipe(minifycss()); + return chain.pipe(gulp.dest('public/css/')).pipe(liveReload()); }); -function scriptTask(watch=false) { +function scriptTask(watch = false) { let props = { basedir: 'resources/assets/js', debug: true, - entries: ['global.js'] + entries: ['global.js'], + fast: !isProduction, + cache: {}, + packageCache: {}, }; let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props); - bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']}); + + if (isProduction) { + bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']}); + } + function rebundle() { let stream = bundler.bundle(); stream = stream.pipe(source('common.js')); - if (argv.production) stream = stream.pipe(buffer()).pipe(uglify()); - return stream.pipe(gulp.dest('public/js/')); + if (isProduction) stream = stream.pipe(buffer()).pipe(uglify()); + return stream.pipe(gulp.dest('public/js/')).pipe(liveReload()); } + bundler.on('update', function() { rebundle(); - gutil.log('Rebundle...'); + gutil.log('Rebundling assets...'); }); + bundler.on('log', gutil.log); return rebundle(); } @@ -57,6 +70,7 @@ gulp.task('scripts', () => {scriptTask(false)}); gulp.task('scripts-watch', () => {scriptTask(true)}); gulp.task('default', ['styles', 'scripts-watch'], () => { + liveReload.listen(); gulp.watch("resources/assets/sass/**/*.scss", ['styles']); }); diff --git a/package.json b/package.json index 93f62bf1f..f447ec786 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "build": "gulp build", "production": "gulp build --production", "dev": "gulp", - "watch": "gulp" + "watch": "gulp", + "permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads" }, "devDependencies": { "babelify": "^7.3.0", @@ -13,6 +14,7 @@ "gulp": "3.9.1", "gulp-autoprefixer": "3.1.1", "gulp-clean-css": "^3.0.4", + "gulp-livereload": "^3.8.1", "gulp-minify-css": "1.2.4", "gulp-plumber": "1.1.0", "gulp-sass": "3.1.0", @@ -29,15 +31,17 @@ "angular-sanitize": "^1.5.5", "angular-ui-sortable": "^0.17.0", "axios": "^0.16.1", + "babel-polyfill": "^6.23.0", "babel-preset-es2015": "^6.24.1", - "clipboard": "^1.5.16", + "clipboard": "^1.7.1", "codemirror": "^5.26.0", "dropzone": "^4.0.1", "gulp-util": "^3.0.8", "markdown-it": "^8.3.1", "markdown-it-task-lists": "^2.0.0", "moment": "^2.12.0", - "vue": "^2.2.6" + "vue": "^2.2.6", + "vuedraggable": "^2.14.1" }, "browser": { "vue": "vue/dist/vue.common.js" diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff b/public/fonts/roboto-mono-v4-latin-regular.woff deleted file mode 100644 index 8cb9e6fd8..000000000 Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff and /dev/null differ diff --git a/public/fonts/roboto-mono-v4-latin-regular.woff2 b/public/fonts/roboto-mono-v4-latin-regular.woff2 deleted file mode 100644 index 1f6598111..000000000 Binary files a/public/fonts/roboto-mono-v4-latin-regular.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff b/public/fonts/roboto-v15-cyrillic_latin-100.woff deleted file mode 100644 index 4eb2be6a1..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100.woff2 deleted file mode 100644 index 007b90e85..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff deleted file mode 100644 index fa7e51bc8..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 deleted file mode 100644 index f27a169cb..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-100italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff b/public/fonts/roboto-v15-cyrillic_latin-300.woff deleted file mode 100644 index ace052941..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300.woff2 deleted file mode 100644 index 0c093b91c..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff deleted file mode 100644 index 7984971e7..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 deleted file mode 100644 index 46ed6c7cc..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-300italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff b/public/fonts/roboto-v15-cyrillic_latin-500.woff deleted file mode 100644 index 8ae98f2de..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500.woff2 deleted file mode 100644 index fba67842e..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff deleted file mode 100644 index 560968d16..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 deleted file mode 100644 index cc41bf873..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-500italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff b/public/fonts/roboto-v15-cyrillic_latin-700.woff deleted file mode 100644 index 7d19e332d..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700.woff2 deleted file mode 100644 index e2274a4fb..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff deleted file mode 100644 index 1604c8763..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 deleted file mode 100644 index f950ca2aa..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-700italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff b/public/fonts/roboto-v15-cyrillic_latin-italic.woff deleted file mode 100644 index d76d13d6a..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 b/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 deleted file mode 100644 index a80f41528..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-italic.woff2 and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff b/public/fonts/roboto-v15-cyrillic_latin-regular.woff deleted file mode 100644 index a2ada2f46..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff and /dev/null differ diff --git a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 b/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 deleted file mode 100644 index a3b35e686..000000000 Binary files a/public/fonts/roboto-v15-cyrillic_latin-regular.woff2 and /dev/null differ diff --git a/public/logo.png b/public/logo.png index 1803feebf..585f8895b 100644 Binary files a/public/logo.png and b/public/logo.png differ diff --git a/readme.md b/readme.md index 4f025e3c2..5d099ad5f 100644 --- a/readme.md +++ b/readme.md @@ -22,9 +22,12 @@ All development on BookStack is currently done on the master branch. When it's t SASS is used to help the CSS development and the JavaScript is run through browserify/babel to allow for writing ES6 code. Both of these are done using gulp. To run the build task you can use the following commands: ``` bash -# Build and minify for production +# Build assets for development npm run-script build +# Build and minify assets for production +npm run-script production + # Build for dev (With sourcemaps) and watch for changes npm run-script dev ``` @@ -76,7 +79,7 @@ These are the great open-source projects used to help build BookStack: * [jQuery Sortable](https://johnny.github.io/jquery-sortable/) * [Material Design Iconic Font](http://zavoloklom.github.io/material-design-iconic-font/icons.html) * [Dropzone.js](http://www.dropzonejs.com/) -* [ZeroClipboard](http://zeroclipboard.org/) +* [clipboard.js](https://clipboardjs.com/) * [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html) * [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists) * [Moment.js](http://momentjs.com/) diff --git a/resources/assets/js/components/back-top-top.js b/resources/assets/js/components/back-top-top.js new file mode 100644 index 000000000..5fa9b3436 --- /dev/null +++ b/resources/assets/js/components/back-top-top.js @@ -0,0 +1,53 @@ + +class BackToTop { + + constructor(elem) { + this.elem = elem; + this.targetElem = document.getElementById('header'); + this.showing = false; + this.breakPoint = 1200; + this.elem.addEventListener('click', this.scrollToTop.bind(this)); + window.addEventListener('scroll', this.onPageScroll.bind(this)); + } + + onPageScroll() { + let scrollTopPos = document.documentElement.scrollTop || document.body.scrollTop || 0; + if (!this.showing && scrollTopPos > this.breakPoint) { + this.elem.style.display = 'block'; + this.showing = true; + setTimeout(() => { + this.elem.style.opacity = 0.4; + }, 1); + } else if (this.showing && scrollTopPos < this.breakPoint) { + this.elem.style.opacity = 0; + this.showing = false; + setTimeout(() => { + this.elem.style.display = 'none'; + }, 500); + } + } + + scrollToTop() { + let targetTop = this.targetElem.getBoundingClientRect().top; + let scrollElem = document.documentElement.scrollTop ? document.documentElement : document.body; + let duration = 300; + let start = Date.now(); + let scrollStart = this.targetElem.getBoundingClientRect().top; + + function setPos() { + let percentComplete = (1-((Date.now() - start) / duration)); + let target = Math.abs(percentComplete * scrollStart); + if (percentComplete > 0) { + scrollElem.scrollTop = target; + requestAnimationFrame(setPos.bind(this)); + } else { + scrollElem.scrollTop = targetTop; + } + } + + requestAnimationFrame(setPos.bind(this)); + } + +} + +module.exports = BackToTop; \ No newline at end of file diff --git a/resources/assets/js/components/chapter-toggle.js b/resources/assets/js/components/chapter-toggle.js new file mode 100644 index 000000000..ad373a668 --- /dev/null +++ b/resources/assets/js/components/chapter-toggle.js @@ -0,0 +1,67 @@ + +class ChapterToggle { + + constructor(elem) { + this.elem = elem; + this.isOpen = elem.classList.contains('open'); + elem.addEventListener('click', this.click.bind(this)); + } + + open() { + let list = this.elem.parentNode.querySelector('.inset-list'); + + this.elem.classList.add('open'); + list.style.display = 'block'; + list.style.height = ''; + let height = list.getBoundingClientRect().height; + list.style.height = '0px'; + list.style.overflow = 'hidden'; + list.style.transition = 'height ease-in-out 240ms'; + + let transitionEndBound = onTransitionEnd.bind(this); + function onTransitionEnd() { + list.style.overflow = ''; + list.style.height = ''; + list.style.transition = ''; + list.removeEventListener('transitionend', transitionEndBound); + } + + setTimeout(() => { + list.style.height = `${height}px`; + list.addEventListener('transitionend', transitionEndBound) + }, 1); + } + + close() { + let list = this.elem.parentNode.querySelector('.inset-list'); + + this.elem.classList.remove('open'); + list.style.display = 'block'; + list.style.height = list.getBoundingClientRect().height + 'px'; + list.style.overflow = 'hidden'; + list.style.transition = 'height ease-in-out 240ms'; + + let transitionEndBound = onTransitionEnd.bind(this); + function onTransitionEnd() { + list.style.overflow = ''; + list.style.height = ''; + list.style.transition = ''; + list.style.display = 'none'; + list.removeEventListener('transitionend', transitionEndBound); + } + + setTimeout(() => { + list.style.height = `0px`; + list.addEventListener('transitionend', transitionEndBound) + }, 1); + } + + click(event) { + event.preventDefault(); + this.isOpen ? this.close() : this.open(); + this.isOpen = !this.isOpen; + } + +} + +module.exports = ChapterToggle; \ No newline at end of file diff --git a/resources/assets/js/components/dropdown.js b/resources/assets/js/components/dropdown.js new file mode 100644 index 000000000..0401efce0 --- /dev/null +++ b/resources/assets/js/components/dropdown.js @@ -0,0 +1,48 @@ +/** + * Dropdown + * Provides some simple logic to create simple dropdown menus. + */ +class DropDown { + + constructor(elem) { + this.container = elem; + this.menu = elem.querySelector('ul'); + this.toggle = elem.querySelector('[dropdown-toggle]'); + this.setupListeners(); + } + + show() { + this.menu.style.display = 'block'; + this.menu.classList.add('anim', 'menuIn'); + this.container.addEventListener('mouseleave', this.hide.bind(this)); + + // Focus on first input if existing + let input = this.menu.querySelector('input'); + if (input !== null) input.focus(); + } + + hide() { + this.menu.style.display = 'none'; + this.menu.classList.remove('anim', 'menuIn'); + } + + setupListeners() { + // Hide menu on option click + this.container.addEventListener('click', event => { + let possibleChildren = Array.from(this.menu.querySelectorAll('a')); + if (possibleChildren.indexOf(event.target) !== -1) this.hide(); + }); + // Show dropdown on toggle click + this.toggle.addEventListener('click', this.show.bind(this)); + // Hide menu on enter press + this.container.addEventListener('keypress', event => { + if (event.keyCode !== 13) return true; + event.preventDefault(); + this.hide(); + return false; + }); + } + +} + +module.exports = DropDown; \ No newline at end of file diff --git a/resources/assets/js/components/expand-toggle.js b/resources/assets/js/components/expand-toggle.js new file mode 100644 index 000000000..61d9f54b7 --- /dev/null +++ b/resources/assets/js/components/expand-toggle.js @@ -0,0 +1,65 @@ + +class ExpandToggle { + + constructor(elem) { + this.elem = elem; + this.isOpen = false; + this.selector = elem.getAttribute('expand-toggle'); + elem.addEventListener('click', this.click.bind(this)); + } + + open(elemToToggle) { + elemToToggle.style.display = 'block'; + elemToToggle.style.height = ''; + let height = elemToToggle.getBoundingClientRect().height; + elemToToggle.style.height = '0px'; + elemToToggle.style.overflow = 'hidden'; + elemToToggle.style.transition = 'height ease-in-out 240ms'; + + let transitionEndBound = onTransitionEnd.bind(this); + function onTransitionEnd() { + elemToToggle.style.overflow = ''; + elemToToggle.style.height = ''; + elemToToggle.style.transition = ''; + elemToToggle.removeEventListener('transitionend', transitionEndBound); + } + + setTimeout(() => { + elemToToggle.style.height = `${height}px`; + elemToToggle.addEventListener('transitionend', transitionEndBound) + }, 1); + } + + close(elemToToggle) { + elemToToggle.style.display = 'block'; + elemToToggle.style.height = elemToToggle.getBoundingClientRect().height + 'px'; + elemToToggle.style.overflow = 'hidden'; + elemToToggle.style.transition = 'all ease-in-out 240ms'; + + let transitionEndBound = onTransitionEnd.bind(this); + function onTransitionEnd() { + elemToToggle.style.overflow = ''; + elemToToggle.style.height = ''; + elemToToggle.style.transition = ''; + elemToToggle.style.display = 'none'; + elemToToggle.removeEventListener('transitionend', transitionEndBound); + } + + setTimeout(() => { + elemToToggle.style.height = `0px`; + elemToToggle.addEventListener('transitionend', transitionEndBound) + }, 1); + } + + click(event) { + event.preventDefault(); + let matchingElems = document.querySelectorAll(this.selector); + for (let i = 0, len = matchingElems.length; i < len; i++) { + this.isOpen ? this.close(matchingElems[i]) : this.open(matchingElems[i]); + } + this.isOpen = !this.isOpen; + } + +} + +module.exports = ExpandToggle; \ No newline at end of file diff --git a/resources/assets/js/components/index.js b/resources/assets/js/components/index.js new file mode 100644 index 000000000..43466a0d9 --- /dev/null +++ b/resources/assets/js/components/index.js @@ -0,0 +1,28 @@ + +let componentMapping = { + 'dropdown': require('./dropdown'), + 'overlay': require('./overlay'), + 'back-to-top': require('./back-top-top'), + 'notification': require('./notification'), + 'chapter-toggle': require('./chapter-toggle'), + 'expand-toggle': require('./expand-toggle'), +}; + +window.components = {}; + +let componentNames = Object.keys(componentMapping); + +for (let i = 0, len = componentNames.length; i < len; i++) { + let name = componentNames[i]; + let elems = document.querySelectorAll(`[${name}]`); + if (elems.length === 0) continue; + + let component = componentMapping[name]; + if (typeof window.components[name] === "undefined") window.components[name] = []; + for (let j = 0, jLen = elems.length; j < jLen; j++) { + let instance = new component(elems[j]); + if (typeof elems[j].components === 'undefined') elems[j].components = {}; + elems[j].components[name] = instance; + window.components[name].push(instance); + } +} \ No newline at end of file diff --git a/resources/assets/js/components/notification.js b/resources/assets/js/components/notification.js new file mode 100644 index 000000000..1a9819702 --- /dev/null +++ b/resources/assets/js/components/notification.js @@ -0,0 +1,41 @@ + +class Notification { + + constructor(elem) { + this.elem = elem; + this.type = elem.getAttribute('notification'); + this.textElem = elem.querySelector('span'); + this.autohide = this.elem.hasAttribute('data-autohide'); + window.Events.listen(this.type, text => { + this.show(text); + }); + elem.addEventListener('click', this.hide.bind(this)); + if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent); + + this.hideCleanup = this.hideCleanup.bind(this); + } + + show(textToShow = '') { + this.elem.removeEventListener('transitionend', this.hideCleanup); + this.textElem.textContent = textToShow; + this.elem.style.display = 'block'; + setTimeout(() => { + this.elem.classList.add('showing'); + }, 1); + + if (this.autohide) setTimeout(this.hide.bind(this), 2000); + } + + hide() { + this.elem.classList.remove('showing'); + this.elem.addEventListener('transitionend', this.hideCleanup); + } + + hideCleanup() { + this.elem.style.display = 'none'; + this.elem.removeEventListener('transitionend', this.hideCleanup); + } + +} + +module.exports = Notification; \ No newline at end of file diff --git a/resources/assets/js/components/overlay.js b/resources/assets/js/components/overlay.js new file mode 100644 index 000000000..6e7a598ac --- /dev/null +++ b/resources/assets/js/components/overlay.js @@ -0,0 +1,39 @@ + +class Overlay { + + constructor(elem) { + this.container = elem; + elem.addEventListener('click', event => { + if (event.target === elem) return this.hide(); + }); + let closeButtons = elem.querySelectorAll('.overlay-close'); + for (let i=0; i < closeButtons.length; i++) { + closeButtons[i].addEventListener('click', this.hide.bind(this)); + } + } + + toggle(show = true) { + let start = Date.now(); + let duration = 240; + + function setOpacity() { + let elapsedTime = (Date.now() - start); + let targetOpacity = show ? (elapsedTime / duration) : 1-(elapsedTime / duration); + this.container.style.opacity = targetOpacity; + if (elapsedTime > duration) { + this.container.style.display = show ? 'flex' : 'none'; + this.container.style.opacity = ''; + } else { + requestAnimationFrame(setOpacity.bind(this)); + } + } + + requestAnimationFrame(setOpacity.bind(this)); + } + + hide() { this.toggle(false); } + show() { this.toggle(true); } + +} + +module.exports = Overlay; \ No newline at end of file diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js index ac1c3487c..8b37379fa 100644 --- a/resources/assets/js/controllers.js +++ b/resources/assets/js/controllers.js @@ -8,256 +8,6 @@ moment.locale('en-gb'); module.exports = function (ngApp, events) { - ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService', - function ($scope, $attrs, $http, $timeout, imageManagerService) { - - $scope.images = []; - $scope.imageType = $attrs.imageType; - $scope.selectedImage = false; - $scope.dependantPages = false; - $scope.showing = false; - $scope.hasMore = false; - $scope.imageUpdateSuccess = false; - $scope.imageDeleteSuccess = false; - $scope.uploadedTo = $attrs.uploadedTo; - $scope.view = 'all'; - - $scope.searching = false; - $scope.searchTerm = ''; - - let page = 0; - let previousClickTime = 0; - let previousClickImage = 0; - let dataLoaded = false; - let callback = false; - - let preSearchImages = []; - let preSearchHasMore = false; - - /** - * Used by dropzone to get the endpoint to upload to. - * @returns {string} - */ - $scope.getUploadUrl = function () { - return window.baseUrl('/images/' + $scope.imageType + '/upload'); - }; - - /** - * Cancel the current search operation. - */ - function cancelSearch() { - $scope.searching = false; - $scope.searchTerm = ''; - $scope.images = preSearchImages; - $scope.hasMore = preSearchHasMore; - } - $scope.cancelSearch = cancelSearch; - - - /** - * Runs on image upload, Adds an image to local list of images - * and shows a success message to the user. - * @param file - * @param data - */ - $scope.uploadSuccess = function (file, data) { - $scope.$apply(() => { - $scope.images.unshift(data); - }); - events.emit('success', trans('components.image_upload_success')); - }; - - /** - * Runs the callback and hides the image manager. - * @param returnData - */ - function callbackAndHide(returnData) { - if (callback) callback(returnData); - $scope.hide(); - } - - /** - * Image select action. Checks if a double-click was fired. - * @param image - */ - $scope.imageSelect = function (image) { - let dblClickTime = 300; - let currentTime = Date.now(); - let timeDiff = currentTime - previousClickTime; - - if (timeDiff < dblClickTime && image.id === previousClickImage) { - // If double click - callbackAndHide(image); - } else { - // If single - $scope.selectedImage = image; - $scope.dependantPages = false; - } - previousClickTime = currentTime; - previousClickImage = image.id; - }; - - /** - * Action that runs when the 'Select image' button is clicked. - * Runs the callback and hides the image manager. - */ - $scope.selectButtonClick = function () { - callbackAndHide($scope.selectedImage); - }; - - /** - * Show the image manager. - * Takes a callback to execute later on. - * @param doneCallback - */ - function show(doneCallback) { - callback = doneCallback; - $scope.showing = true; - $('#image-manager').find('.overlay').css('display', 'flex').hide().fadeIn(240); - // Get initial images if they have not yet been loaded in. - if (!dataLoaded) { - fetchData(); - dataLoaded = true; - } - } - - // Connects up the image manger so it can be used externally - // such as from TinyMCE. - imageManagerService.show = show; - imageManagerService.showExternal = function (doneCallback) { - $scope.$apply(() => { - show(doneCallback); - }); - }; - window.ImageManager = imageManagerService; - - /** - * Hide the image manager - */ - $scope.hide = function () { - $scope.showing = false; - $('#image-manager').find('.overlay').fadeOut(240); - }; - - let baseUrl = window.baseUrl('/images/' + $scope.imageType + '/all/'); - - /** - * Fetch the list image data from the server. - */ - function fetchData() { - let url = baseUrl + page + '?'; - let components = {}; - if ($scope.uploadedTo) components['page_id'] = $scope.uploadedTo; - if ($scope.searching) components['term'] = $scope.searchTerm; - - - url += Object.keys(components).map((key) => { - return key + '=' + encodeURIComponent(components[key]); - }).join('&'); - - $http.get(url).then((response) => { - $scope.images = $scope.images.concat(response.data.images); - $scope.hasMore = response.data.hasMore; - page++; - }); - } - $scope.fetchData = fetchData; - - /** - * Start a search operation - */ - $scope.searchImages = function() { - - if ($scope.searchTerm === '') { - cancelSearch(); - return; - } - - if (!$scope.searching) { - preSearchImages = $scope.images; - preSearchHasMore = $scope.hasMore; - } - - $scope.searching = true; - $scope.images = []; - $scope.hasMore = false; - page = 0; - baseUrl = window.baseUrl('/images/' + $scope.imageType + '/search/'); - fetchData(); - }; - - /** - * Set the current image listing view. - * @param viewName - */ - $scope.setView = function(viewName) { - cancelSearch(); - $scope.images = []; - $scope.hasMore = false; - page = 0; - $scope.view = viewName; - baseUrl = window.baseUrl('/images/' + $scope.imageType + '/' + viewName + '/'); - fetchData(); - }; - - /** - * Save the details of an image. - * @param event - */ - $scope.saveImageDetails = function (event) { - event.preventDefault(); - let url = window.baseUrl('/images/update/' + $scope.selectedImage.id); - $http.put(url, this.selectedImage).then(response => { - events.emit('success', trans('components.image_update_success')); - }, (response) => { - if (response.status === 422) { - let errors = response.data; - let message = ''; - Object.keys(errors).forEach((key) => { - message += errors[key].join('\n'); - }); - events.emit('error', message); - } else if (response.status === 403) { - events.emit('error', response.data.error); - } - }); - }; - - /** - * Delete an image from system and notify of success. - * Checks if it should force delete when an image - * has dependant pages. - * @param event - */ - $scope.deleteImage = function (event) { - event.preventDefault(); - let force = $scope.dependantPages !== false; - let url = window.baseUrl('/images/' + $scope.selectedImage.id); - if (force) url += '?force=true'; - $http.delete(url).then((response) => { - $scope.images.splice($scope.images.indexOf($scope.selectedImage), 1); - $scope.selectedImage = false; - events.emit('success', trans('components.image_delete_success')); - }, (response) => { - // Pages failure - if (response.status === 400) { - $scope.dependantPages = response.data; - } else if (response.status === 403) { - events.emit('error', response.data.error); - } - }); - }; - - /** - * Simple date creator used to properly format dates. - * @param stringDate - * @returns {Date} - */ - $scope.getDate = function (stringDate) { - return new Date(stringDate); - }; - - }]); ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce', function ($scope, $http, $attrs, $interval, $timeout, $sce) { @@ -379,7 +129,7 @@ module.exports = function (ngApp, events) { */ $scope.discardDraft = function () { let url = window.baseUrl('/ajax/page/' + pageId); - $http.get(url).then((responseData) => { + $http.get(url).then(responseData => { if (autoSave) $interval.cancel(autoSave); $scope.draftText = trans('entities.pages_editing_page'); $scope.isUpdateDraft = false; @@ -395,284 +145,225 @@ module.exports = function (ngApp, events) { }]); - ngApp.controller('PageTagController', ['$scope', '$http', '$attrs', - function ($scope, $http, $attrs) { + // Controller used to reply to and add new comments + ngApp.controller('CommentReplyController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + const MarkdownIt = require("markdown-it"); + const md = new MarkdownIt({html: true}); + let vm = this; - const pageId = Number($attrs.pageId); - $scope.tags = []; - - $scope.sortOptions = { - handle: '.handle', - items: '> tr', - containment: "parent", - axis: "y" + vm.saveComment = function () { + let pageId = $scope.comment.pageId || $scope.pageId; + let comment = $scope.comment.text; + if (!comment) { + return events.emit('warning', trans('errors.empty_comment')); + } + let commentHTML = md.render($scope.comment.text); + let serviceUrl = `/ajax/page/${pageId}/comment/`; + let httpMethod = 'post'; + let reqObj = { + text: comment, + html: commentHTML }; - /** - * Push an empty tag to the end of the scope tags. - */ - function addEmptyTag() { - $scope.tags.push({ - name: '', - value: '' - }); + if ($scope.isEdit === true) { + // this will be set when editing the comment. + serviceUrl = `/ajax/page/${pageId}/comment/${$scope.comment.id}`; + httpMethod = 'put'; + } else if ($scope.isReply === true) { + // if its reply, get the parent comment id + reqObj.parent_id = $scope.parentId; } - $scope.addEmptyTag = addEmptyTag; - - /** - * Get all tags for the current book and add into scope. - */ - function getTags() { - let url = window.baseUrl(`/ajax/tags/get/page/${pageId}`); - $http.get(url).then((responseData) => { - $scope.tags = responseData.data; - addEmptyTag(); - }); - } - getTags(); - - /** - * Set the order property on all tags. - */ - function setTagOrder() { - for (let i = 0; i < $scope.tags.length; i++) { - $scope.tags[i].order = i; + $http[httpMethod](window.baseUrl(serviceUrl), reqObj).then(resp => { + if (!isCommentOpSuccess(resp)) { + return; } - } - - /** - * When an tag changes check if another empty editable - * field needs to be added onto the end. - * @param tag - */ - $scope.tagChange = function(tag) { - let cPos = $scope.tags.indexOf(tag); - if (cPos !== $scope.tags.length-1) return; - - if (tag.name !== '' || tag.value !== '') { - addEmptyTag(); - } - }; - - /** - * When an tag field loses focus check the tag to see if its - * empty and therefore could be removed from the list. - * @param tag - */ - $scope.tagBlur = function(tag) { - let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag); - if (tag.name === '' && tag.value === '' && !isLast) { - let cPos = $scope.tags.indexOf(tag); - $scope.tags.splice(cPos, 1); - } - }; - - /** - * Remove a tag from the current list. - * @param tag - */ - $scope.removeTag = function(tag) { - let cIndex = $scope.tags.indexOf(tag); - $scope.tags.splice(cIndex, 1); - }; - - }]); - - - ngApp.controller('PageAttachmentController', ['$scope', '$http', '$attrs', - function ($scope, $http, $attrs) { - - const pageId = $scope.uploadedTo = $attrs.pageId; - let currentOrder = ''; - $scope.files = []; - $scope.editFile = false; - $scope.file = getCleanFile(); - $scope.errors = { - link: {}, - edit: {} - }; - - function getCleanFile() { - return { - page_id: pageId - }; - } - - // Angular-UI-Sort options - $scope.sortOptions = { - handle: '.handle', - items: '> tr', - containment: "parent", - axis: "y", - stop: sortUpdate, - }; - - /** - * Event listener for sort changes. - * Updates the file ordering on the server. - * @param event - * @param ui - */ - function sortUpdate(event, ui) { - let newOrder = $scope.files.map(file => {return file.id}).join(':'); - if (newOrder === currentOrder) return; - - currentOrder = newOrder; - $http.put(window.baseUrl(`/attachments/sort/page/${pageId}`), {files: $scope.files}).then(resp => { - events.emit('success', resp.data.message); - }, checkError('sort')); - } - - /** - * Used by dropzone to get the endpoint to upload to. - * @returns {string} - */ - $scope.getUploadUrl = function (file) { - let suffix = (typeof file !== 'undefined') ? `/${file.id}` : ''; - return window.baseUrl(`/attachments/upload${suffix}`); - }; - - /** - * Get files for the current page from the server. - */ - function getFiles() { - let url = window.baseUrl(`/attachments/get/page/${pageId}`); - $http.get(url).then(resp => { - $scope.files = resp.data; - currentOrder = resp.data.map(file => {return file.id}).join(':'); - }, checkError('get')); - } - getFiles(); - - /** - * Runs on file upload, Adds an file to local file list - * and shows a success message to the user. - * @param file - * @param data - */ - $scope.uploadSuccess = function (file, data) { - $scope.$apply(() => { - $scope.files.push(data); - }); - events.emit('success', trans('entities.attachments_file_uploaded')); - }; - - /** - * Upload and overwrite an existing file. - * @param file - * @param data - */ - $scope.uploadSuccessUpdate = function (file, data) { - $scope.$apply(() => { - let search = filesIndexOf(data); - if (search !== -1) $scope.files[search] = data; - - if ($scope.editFile) { - $scope.editFile = angular.copy(data); - data.link = ''; + // hide the comments first, and then retrigger the refresh + if ($scope.isEdit) { + updateComment($scope.comment, resp.data); + $scope.$emit('evt.comment-success', $scope.comment.id); + } else { + $scope.comment.text = ''; + if ($scope.isReply === true && $scope.parent.sub_comments) { + $scope.parent.sub_comments.push(resp.data.comment); + } else { + $scope.$emit('evt.new-comment', resp.data.comment); } + $scope.$emit('evt.comment-success', null, true); + } + $scope.comment.is_hidden = true; + $timeout(function() { + $scope.comment.is_hidden = false; }); - events.emit('success', trans('entities.attachments_file_updated')); - }; - /** - * Delete a file from the server and, on success, the local listing. - * @param file - */ - $scope.deleteFile = function(file) { - if (!file.deleting) { - file.deleting = true; + events.emit('success', trans(resp.data.message)); + + }, checkError); + + }; + + function checkError(response) { + let msg = null; + if (isCommentOpSuccess(response)) { + // all good + return; + } else if (response.data) { + msg = response.data.message; + } else { + msg = trans('errors.comment_add'); + } + if (msg) { + events.emit('success', msg); + } + } + }]); + + // Controller used to delete comments + ngApp.controller('CommentDeleteController', ['$scope', '$http', '$timeout', function ($scope, $http, $timeout) { + let vm = this; + + vm.delete = function(comment) { + $http.delete(window.baseUrl(`/ajax/comment/${comment.id}`)).then(resp => { + if (!isCommentOpSuccess(resp)) { return; } - $http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => { - events.emit('success', resp.data.message); - $scope.files.splice($scope.files.indexOf(file), 1); - }, checkError('delete')); - }; - - /** - * Attach a link to a page. - * @param file - */ - $scope.attachLinkSubmit = function(file) { - file.uploaded_to = pageId; - $http.post(window.baseUrl('/attachments/link'), file).then(resp => { - $scope.files.push(resp.data); - events.emit('success', trans('entities.attachments_link_attached')); - $scope.file = getCleanFile(); - }, checkError('link')); - }; - - /** - * Start the edit mode for a file. - * @param file - */ - $scope.startEdit = function(file) { - $scope.editFile = angular.copy(file); - $scope.editFile.link = (file.external) ? file.path : ''; - }; - - /** - * Cancel edit mode - */ - $scope.cancelEdit = function() { - $scope.editFile = false; - }; - - /** - * Update the name and link of a file. - * @param file - */ - $scope.updateFile = function(file) { - $http.put(window.baseUrl(`/attachments/${file.id}`), file).then(resp => { - let search = filesIndexOf(resp.data); - if (search !== -1) $scope.files[search] = resp.data; - - if ($scope.editFile && !file.external) { - $scope.editFile.link = ''; - } - $scope.editFile = false; - events.emit('success', trans('entities.attachments_updated_success')); - }, checkError('edit')); - }; - - /** - * Get the url of a file. - */ - $scope.getFileUrl = function(file) { - return window.baseUrl('/attachments/' + file.id); - }; - - /** - * Search the local files via another file object. - * Used to search via object copies. - * @param file - * @returns int - */ - function filesIndexOf(file) { - for (let i = 0; i < $scope.files.length; i++) { - if ($scope.files[i].id == file.id) return i; + updateComment(comment, resp.data, $timeout, true); + }, function (resp) { + if (isCommentOpSuccess(resp)) { + events.emit('success', trans('entities.comment_deleted')); + } else { + events.emit('error', trans('error.comment_delete')); } - return -1; + }); + }; + }]); + + // Controller used to fetch all comments for a page + ngApp.controller('CommentListController', ['$scope', '$http', '$timeout', '$location', function ($scope, $http, $timeout, $location) { + let vm = this; + $scope.errors = {}; + // keep track of comment levels + $scope.level = 1; + vm.totalCommentsStr = trans('entities.comments_loading'); + vm.permissions = {}; + vm.trans = window.trans; + + $scope.$on('evt.new-comment', function (event, comment) { + // add the comment to the comment list. + vm.comments.push(comment); + ++vm.totalComments; + setTotalCommentMsg(); + event.stopPropagation(); + event.preventDefault(); + }); + + vm.canEditDelete = function (comment, prop) { + if (!comment.active) { + return false; + } + let propAll = prop + '_all'; + let propOwn = prop + '_own'; + + if (vm.permissions[propAll]) { + return true; } - /** - * Check for an error response in a ajax request. - * @param errorGroupName - */ - function checkError(errorGroupName) { - $scope.errors[errorGroupName] = {}; - return function(response) { - if (typeof response.data !== 'undefined' && typeof response.data.error !== 'undefined') { - events.emit('error', response.data.error); - } - if (typeof response.data !== 'undefined' && typeof response.data.validation !== 'undefined') { - $scope.errors[errorGroupName] = response.data.validation; - console.log($scope.errors[errorGroupName]) - } - } + if (vm.permissions[propOwn] && comment.created_by.id === vm.current_user_id) { + return true; } - }]); + return false; + }; + vm.canComment = function () { + return vm.permissions.comment_create; + }; + + // check if there are is any direct linking + let linkedCommentId = $location.search().cm; + + $timeout(function() { + $http.get(window.baseUrl(`/ajax/page/${$scope.pageId}/comments/`)).then(resp => { + if (!isCommentOpSuccess(resp)) { + // just show that no comments are available. + vm.totalComments = 0; + setTotalCommentMsg(); + return; + } + vm.comments = resp.data.comments; + vm.totalComments = +resp.data.total; + vm.permissions = resp.data.permissions; + vm.current_user_id = resp.data.user_id; + setTotalCommentMsg(); + if (!linkedCommentId) { + return; + } + $timeout(function() { + // wait for the UI to render. + focusLinkedComment(linkedCommentId); + }); + }, checkError); + }); + + function setTotalCommentMsg () { + if (vm.totalComments === 0) { + vm.totalCommentsStr = trans('entities.no_comments'); + } else if (vm.totalComments === 1) { + vm.totalCommentsStr = trans('entities.one_comment'); + } else { + vm.totalCommentsStr = trans('entities.x_comments', { + numComments: vm.totalComments + }); + } + } + + function focusLinkedComment(linkedCommentId) { + let comment = angular.element('#' + linkedCommentId); + if (comment.length === 0) { + return; + } + + window.setupPageShow.goToText(linkedCommentId); + } + + function checkError(response) { + let msg = null; + if (isCommentOpSuccess(response)) { + // all good + return; + } else if (response.data) { + msg = response.data.message; + } else { + msg = trans('errors.comment_list'); + } + if (msg) { + events.emit('success', msg); + } + } + }]); + + function updateComment(comment, resp, $timeout, isDelete) { + comment.text = resp.comment.text; + comment.updated = resp.comment.updated; + comment.updated_by = resp.comment.updated_by; + comment.active = resp.comment.active; + if (isDelete && !resp.comment.active) { + comment.html = trans('entities.comment_deleted'); + } else { + comment.html = resp.comment.html; + } + if (!$timeout) { + return; + } + comment.is_hidden = true; + $timeout(function() { + comment.is_hidden = false; + }); + } + + function isCommentOpSuccess(resp) { + if (resp && resp.data && resp.data.status === 'success') { + return true; + } + return false; + } }; diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js index 51f1b7579..fc92121ff 100644 --- a/resources/assets/js/directives.js +++ b/resources/assets/js/directives.js @@ -1,152 +1,10 @@ "use strict"; -const DropZone = require("dropzone"); const MarkdownIt = require("markdown-it"); const mdTasksLists = require('markdown-it-task-lists'); const code = require('./code'); module.exports = function (ngApp, events) { - /** - * Common tab controls using simple jQuery functions. - */ - ngApp.directive('tabContainer', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - const $content = element.find('[tab-content]'); - const $buttons = element.find('[tab-button]'); - - if (attrs.tabContainer) { - let initial = attrs.tabContainer; - $buttons.filter(`[tab-button="${initial}"]`).addClass('selected'); - $content.hide().filter(`[tab-content="${initial}"]`).show(); - } else { - $content.hide().first().show(); - $buttons.first().addClass('selected'); - } - - $buttons.click(function() { - let clickedTab = $(this); - $buttons.removeClass('selected'); - $content.hide(); - let name = clickedTab.addClass('selected').attr('tab-button'); - $content.filter(`[tab-content="${name}"]`).show(); - }); - } - }; - }); - - /** - * Sub form component to allow inner-form sections to act like their own forms. - */ - ngApp.directive('subForm', function() { - return { - restrict: 'A', - link: function (scope, element, attrs) { - element.on('keypress', e => { - if (e.keyCode === 13) { - submitEvent(e); - } - }); - - element.find('button[type="submit"]').click(submitEvent); - - function submitEvent(e) { - e.preventDefault(); - if (attrs.subForm) scope.$eval(attrs.subForm); - } - } - }; - }); - - /** - * DropZone - * Used for uploading images - */ - ngApp.directive('dropZone', [function () { - return { - restrict: 'E', - template: ` -', '');};
cm.setOption('extraKeys', extraKeys);
// Update data on content change
@@ -303,6 +152,73 @@ module.exports = function (ngApp, events) {
cm.setSelections(cursor);
}
+ // Helper to replace the start of the line
+ function replaceLineStart(newStart) {
+ let cursor = cm.getCursor();
+ let lineContent = cm.getLine(cursor.line);
+ let lineLen = lineContent.length;
+ let lineStart = lineContent.split(' ')[0];
+
+ // Remove symbol if already set
+ if (lineStart === newStart) {
+ lineContent = lineContent.replace(`${newStart} `, '');
+ cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+ cm.setCursor({line: cursor.line, ch: cursor.ch - (newStart.length + 1)});
+ return;
+ }
+
+ let alreadySymbol = /^[#>`]/.test(lineStart);
+ let posDif = 0;
+ if (alreadySymbol) {
+ posDif = newStart.length - lineStart.length;
+ lineContent = lineContent.replace(lineStart, newStart).trim();
+ } else if (newStart !== '') {
+ posDif = newStart.length + 1;
+ lineContent = newStart + ' ' + lineContent;
+ }
+ cm.replaceRange(lineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+ cm.setCursor({line: cursor.line, ch: cursor.ch + posDif});
+ }
+
+ function wrapLine(start, end) {
+ let cursor = cm.getCursor();
+ let lineContent = cm.getLine(cursor.line);
+ let lineLen = lineContent.length;
+ let newLineContent = lineContent;
+
+ if (lineContent.indexOf(start) === 0 && lineContent.slice(-end.length) === end) {
+ newLineContent = lineContent.slice(start.length, lineContent.length - end.length);
+ } else {
+ newLineContent = `${start}${lineContent}${end}`;
+ }
+
+ cm.replaceRange(newLineContent, {line: cursor.line, ch: 0}, {line: cursor.line, ch: lineLen});
+ cm.setCursor({line: cursor.line, ch: cursor.ch + (newLineContent.length - lineLen)});
+ }
+
+ function wrapSelection(start, end) {
+ let selection = cm.getSelection();
+ if (selection === '') return wrapLine(start, end);
+ let newSelection = selection;
+ let frontDiff = 0;
+ let endDiff = 0;
+
+ if (selection.indexOf(start) === 0 && selection.slice(-end.length) === end) {
+ newSelection = selection.slice(start.length, selection.length - end.length);
+ endDiff = -(end.length + start.length);
+ } else {
+ newSelection = `${start}${selection}${end}`;
+ endDiff = start.length + end.length;
+ }
+
+ let selections = cm.listSelections()[0];
+ cm.replaceSelection(newSelection);
+ let headFirst = selections.head.ch <= selections.anchor.ch;
+ selections.head.ch += headFirst ? frontDiff : endDiff;
+ selections.anchor.ch += headFirst ? endDiff : frontDiff;
+ cm.setSelections([selections]);
+ }
+
// Handle image upload and add image into markdown content
function uploadImage(file) {
if (file === null || file.type.indexOf('image') !== 0) return;
@@ -345,10 +261,20 @@ module.exports = function (ngApp, events) {
});
}
+ function insertLink() {
+ let cursorPos = cm.getCursor('from');
+ let selectedText = cm.getSelection() || '';
+ let newText = `[${selectedText}]()`;
+ cm.focus();
+ cm.replaceSelection(newText);
+ let cursorPosDiff = (selectedText === '') ? -3 : -1;
+ cm.setCursor(cursorPos.line, cursorPos.ch + newText.length+cursorPosDiff);
+ }
+
// Show the image manager and handle image insertion
function showImageManager() {
let cursorPos = cm.getCursor('from');
- window.ImageManager.showExternal(image => {
+ window.ImageManager.show(image => {
let selectedText = cm.getSelection();
let newText = "![" + (selectedText || image.name) + "](" + image.thumbs.display + ")";
cm.focus();
@@ -461,188 +387,6 @@ module.exports = function (ngApp, events) {
}
}]);
- /**
- * Tag Autosuggestions
- * Listens to child inputs and provides autosuggestions depending on field type
- * and input. Suggestions provided by server.
- */
- ngApp.directive('tagAutosuggestions', ['$http', function ($http) {
- return {
- restrict: 'A',
- link: function (scope, elem, attrs) {
-
- // Local storage for quick caching.
- const localCache = {};
-
- // Create suggestion element
- const suggestionBox = document.createElement('ul');
- suggestionBox.className = 'suggestion-box';
- suggestionBox.style.position = 'absolute';
- suggestionBox.style.display = 'none';
- const $suggestionBox = $(suggestionBox);
-
- // General state tracking
- let isShowing = false;
- let currentInput = false;
- let active = 0;
-
- // Listen to input events on autosuggest fields
- elem.on('input focus', '[autosuggest]', function (event) {
- let $input = $(this);
- let val = $input.val();
- let url = $input.attr('autosuggest');
- let type = $input.attr('autosuggest-type');
-
- // Add name param to request if for a value
- if (type.toLowerCase() === 'value') {
- let $nameInput = $input.closest('tr').find('[autosuggest-type="name"]').first();
- let nameVal = $nameInput.val();
- if (nameVal !== '') {
- url += '?name=' + encodeURIComponent(nameVal);
- }
- }
-
- let suggestionPromise = getSuggestions(val.slice(0, 3), url);
- suggestionPromise.then(suggestions => {
- if (val.length === 0) {
- displaySuggestions($input, suggestions.slice(0, 6));
- } else {
- suggestions = suggestions.filter(item => {
- return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
- }).slice(0, 4);
- displaySuggestions($input, suggestions);
- }
- });
- });
-
- // Hide autosuggestions when input loses focus.
- // Slight delay to allow clicks.
- let lastFocusTime = 0;
- elem.on('blur', '[autosuggest]', function (event) {
- let startTime = Date.now();
- setTimeout(() => {
- if (lastFocusTime < startTime) {
- $suggestionBox.hide();
- isShowing = false;
- }
- }, 200)
- });
- elem.on('focus', '[autosuggest]', function (event) {
- lastFocusTime = Date.now();
- });
-
- elem.on('keydown', '[autosuggest]', function (event) {
- if (!isShowing) return;
-
- let suggestionElems = suggestionBox.childNodes;
- let suggestCount = suggestionElems.length;
-
- // Down arrow
- if (event.keyCode === 40) {
- let newActive = (active === suggestCount - 1) ? 0 : active + 1;
- changeActiveTo(newActive, suggestionElems);
- }
- // Up arrow
- else if (event.keyCode === 38) {
- let newActive = (active === 0) ? suggestCount - 1 : active - 1;
- changeActiveTo(newActive, suggestionElems);
- }
- // Enter or tab key
- else if ((event.keyCode === 13 || event.keyCode === 9) && !event.shiftKey) {
- currentInput[0].value = suggestionElems[active].textContent;
- currentInput.focus();
- $suggestionBox.hide();
- isShowing = false;
- if (event.keyCode === 13) {
- event.preventDefault();
- return false;
- }
- }
- });
-
- // Change the active suggestion to the given index
- function changeActiveTo(index, suggestionElems) {
- suggestionElems[active].className = '';
- active = index;
- suggestionElems[active].className = 'active';
- }
-
- // Display suggestions on a field
- let prevSuggestions = [];
-
- function displaySuggestions($input, suggestions) {
-
- // Hide if no suggestions
- if (suggestions.length === 0) {
- $suggestionBox.hide();
- isShowing = false;
- prevSuggestions = suggestions;
- return;
- }
-
- // Otherwise show and attach to input
- if (!isShowing) {
- $suggestionBox.show();
- isShowing = true;
- }
- if ($input !== currentInput) {
- $suggestionBox.detach();
- $input.after($suggestionBox);
- currentInput = $input;
- }
-
- // Return if no change
- if (prevSuggestions.join() === suggestions.join()) {
- prevSuggestions = suggestions;
- return;
- }
-
- // Build suggestions
- $suggestionBox[0].innerHTML = '';
- for (let i = 0; i < suggestions.length; i++) {
- let suggestion = document.createElement('li');
- suggestion.textContent = suggestions[i];
- suggestion.onclick = suggestionClick;
- if (i === 0) {
- suggestion.className = 'active';
- active = 0;
- }
- $suggestionBox[0].appendChild(suggestion);
- }
-
- prevSuggestions = suggestions;
- }
-
- // Suggestion click event
- function suggestionClick(event) {
- currentInput[0].value = this.textContent;
- currentInput.focus();
- $suggestionBox.hide();
- isShowing = false;
- }
-
- // Get suggestions & cache
- function getSuggestions(input, url) {
- let hasQuery = url.indexOf('?') !== -1;
- let searchUrl = url + (hasQuery ? '&' : '?') + 'search=' + encodeURIComponent(input);
-
- // Get from local cache if exists
- if (typeof localCache[searchUrl] !== 'undefined') {
- return new Promise((resolve, reject) => {
- resolve(localCache[searchUrl]);
- });
- }
-
- return $http.get(searchUrl).then(response => {
- localCache[searchUrl] = response.data;
- return response.data;
- });
- }
-
- }
- }
- }]);
-
ngApp.directive('entityLinkSelector', [function($http) {
return {
restrict: 'A',
@@ -678,6 +422,7 @@ module.exports = function (ngApp, events) {
function hide() {
element.fadeOut(240);
}
+ scope.hide = hide;
// Listen to confirmation of entity selections (doubleclick)
events.listen('entity-select-confirm', entity => {
@@ -789,4 +534,128 @@ module.exports = function (ngApp, events) {
}
};
}]);
+
+ ngApp.directive('commentReply', [function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'comment-reply.html',
+ scope: {
+ pageId: '=',
+ parentId: '=',
+ parent: '='
+ },
+ link: function (scope, element) {
+ scope.isReply = true;
+ element.find('textarea').focus();
+ scope.$on('evt.comment-success', function (event) {
+ // no need for the event to do anything more.
+ event.stopPropagation();
+ event.preventDefault();
+ scope.closeBox();
+ });
+
+ scope.closeBox = function () {
+ element.remove();
+ scope.$destroy();
+ };
+ }
+ };
+ }]);
+
+ ngApp.directive('commentEdit', [function () {
+ return {
+ restrict: 'E',
+ templateUrl: 'comment-reply.html',
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element) {
+ scope.isEdit = true;
+ element.find('textarea').focus();
+ scope.$on('evt.comment-success', function (event, commentId) {
+ // no need for the event to do anything more.
+ event.stopPropagation();
+ event.preventDefault();
+ if (commentId === scope.comment.id && !scope.isNew) {
+ scope.closeBox();
+ }
+ });
+
+ scope.closeBox = function () {
+ element.remove();
+ scope.$destroy();
+ };
+ }
+ };
+ }]);
+
+
+ ngApp.directive('commentReplyLink', ['$document', '$compile', function ($document, $compile) {
+ return {
+ scope: {
+ comment: '='
+ },
+ link: function (scope, element, attr) {
+ element.on('$destroy', function () {
+ element.off('click');
+ scope.$destroy();
+ });
+
+ element.on('click', function (e) {
+ e.preventDefault();
+ var $container = element.parents('.comment-actions').first();
+ if (!$container.length) {
+ console.error('commentReplyLink directive should be placed inside a container with class comment-box!');
+ return;
+ }
+ if (attr.noCommentReplyDupe) {
+ removeDupe();
+ }
+
+ compileHtml($container, scope, attr.isReply === 'true');
+ });
+ }
+ };
+
+ function compileHtml($container, scope, isReply) {
+ let lnkFunc = null;
+ if (isReply) {
+ lnkFunc = $compile(' {!! $book->searchSnippet !!} {{ $book->getExcerpt() }} {!! $book->searchSnippet !!} {{ $book->getExcerpt() }}
+
+
Größere Bilder werden verkleinert.',
- 'app_primary_color' => 'Primäre Anwendungsfarbe',
- 'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein.
Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.',
+ 'app_logo_desc' => "Dieses Bild sollte 43px hoch sein.\nGrößere Bilder werden verkleinert.",
+ 'app_primary_color' => 'Primäre Anwendungsfarbe',
+ 'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zurückgesetzt.",
/**
* Registration settings
@@ -39,11 +39,11 @@ return [
'reg_settings' => 'Registrierungseinstellungen',
'reg_allow' => 'Registrierung erlauben?',
'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
- 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
- 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
- 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
- 'reg_confirm_restrict_domain_desc' => 'Fügen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.
Hinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.',
- 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
+ 'reg_confirm_email' => 'Bestätigung per E-Mail erforderlich?',
+ 'reg_confirm_email_desc' => 'Falls die Einschränkung für Domains genutzt wird, ist die Bestätigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
+ 'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschränken',
+ 'reg_confirm_restrict_domain_desc' => "Fügen sie eine durch Komma getrennte Liste von Domains hinzu, auf die die Registrierung eingeschränkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu bestätigen, bevor sie diese Anwendung nutzen können.\nHinweis: Benutzer können ihre E-Mail Adresse nach erfolgreicher Registrierung ändern.",
+ 'reg_confirm_restrict_domain_placeholder' => 'Keine Einschränkung gesetzt',
/**
* Role settings
@@ -53,31 +53,31 @@ return [
'role_user_roles' => 'Benutzer-Rollen',
'role_create' => 'Neue Rolle anlegen',
'role_create_success' => 'Rolle erfolgreich angelegt',
- 'role_delete' => 'Rolle löschen',
- 'role_delete_confirm' => 'Sie möchten die Rolle \':roleName\' löschen.',
- 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.',
+ 'role_delete' => 'Rolle löschen',
+ 'role_delete_confirm' => 'Sie möchten die Rolle ":roleName" löschen.',
+ 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie können unten eine neue Rolle auswählen, die Sie diesen Benutzern zuordnen möchten.',
'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen",
- 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?',
- 'role_delete_success' => 'Rolle erfolgreich gelöscht',
+ 'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle löschen möchten?',
+ 'role_delete_success' => 'Rolle erfolgreich gelöscht',
'role_edit' => 'Rolle bearbeiten',
- 'role_details' => 'Rollen-Details',
+ 'role_details' => 'Rollendetails',
'role_name' => 'Rollenname',
'role_desc' => 'Kurzbeschreibung der Rolle',
'role_system' => 'System-Berechtigungen',
'role_manage_users' => 'Benutzer verwalten',
- 'role_manage_roles' => 'Rollen & Rollen-Berechtigungen verwalten',
- 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel und Seiten-Berechtigungen verwalten',
- 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',
- 'role_manage_settings' => 'Globaleinstellungen verwalrten',
+ 'role_manage_roles' => 'Rollen und Rollen-Berechtigungen verwalten',
+ 'role_manage_entity_permissions' => 'Alle Buch-, Kapitel- und Seiten-Berechtigungen verwalten',
+ 'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener Bücher, Kapitel und Seiten verwalten',
+ 'role_manage_settings' => 'Globaleinstellungen verwalten',
'role_asset' => 'Berechtigungen',
- 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
+ 'role_asset_desc' => 'Diese Berechtigungen gelten für den Standard-Zugriff innerhalb des Systems. Berechtigungen für Bücher, Kapitel und Seiten überschreiben diese Berechtigungenen.',
'role_all' => 'Alle',
'role_own' => 'Eigene',
- 'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
+ 'role_controlled_by_asset' => 'Berechtigungen werden vom Uploadziel bestimmt',
'role_save' => 'Rolle speichern',
'role_update_success' => 'Rolle erfolgreich gespeichert',
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
- 'role_users_none' => 'Bisher sind dieser Rolle keiner Benutzer zugeordnet,',
+ 'role_users_none' => 'Bisher sind dieser Rolle keine Benutzer zugeordnet',
/**
* Users
@@ -85,28 +85,28 @@ return [
'users' => 'Benutzer',
'user_profile' => 'Benutzerprofil',
- 'users_add_new' => 'Benutzer hinzufügen',
+ 'users_add_new' => 'Benutzer hinzufügen',
'users_search' => 'Benutzer suchen',
'users_role' => 'Benutzerrollen',
'users_external_auth_id' => 'Externe Authentifizierungs-ID',
- 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:',
- 'users_system_public' => 'Dieser Benutzer repräsentiert alle Gast-Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
+ 'users_password_warning' => 'Füllen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort ändern möchten:',
+ 'users_system_public' => 'Dieser Benutzer repräsentiert alle unangemeldeten Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
+ 'users_delete' => 'Benutzer löschen',
+ 'users_delete_named' => 'Benutzer ":userName" löschen',
+ 'users_delete_warning' => 'Der Benutzer ":userName" wird aus dem System gelöscht.',
+ 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
+ 'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
'users_books_display_type' => 'Bevorzugtes Display-Layout für Bücher',
- 'users_delete' => 'Benutzer löschen',
- 'users_delete_named' => 'Benutzer :userName löschen',
- 'users_delete_warning' => 'Sie möchten den Benutzer \':userName\' gänzlich aus dem System löschen.',
- 'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer löschen möchten?',
- 'users_delete_success' => 'Benutzer erfolgreich gelöscht.',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
'users_avatar' => 'Benutzer-Bild',
- 'users_avatar_desc' => 'Dieses Bild sollte einen Durchmesser von ca. 256px haben.',
+ 'users_avatar_desc' => 'Das Bild sollte eine Auflösung von 256x256px haben.',
'users_preferred_language' => 'Bevorzugte Sprache',
'users_social_accounts' => 'Social-Media Konten',
- 'users_social_accounts_info' => 'Hier können Sie andere Social-Media Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto hier lösen, bleibt der Zugriff erhalteb. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media Kontos.',
- 'users_social_connect' => 'Social-Media Konto verknüpfen',
- 'users_social_disconnect' => 'Social-Media Kontoverknüpfung lösen',
- 'users_social_connected' => ':socialAccount Konto wurde erfolgreich mit dem Profil verknüpft.',
- 'users_social_disconnected' => ':socialAccount Konto wurde erfolgreich vom Profil gelöst.',
+ 'users_social_accounts_info' => 'Hier können Sie andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Sie ein Social-Media Konto lösen, bleibt der Zugriff erhalten. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verknüpften Social-Media-Kontos.',
+ 'users_social_connect' => 'Social-Media-Konto verknüpfen',
+ 'users_social_disconnect' => 'Social-Media-Konto lösen',
+ 'users_social_connected' => ':socialAccount-Konto wurde erfolgreich mit dem Profil verknüpft.',
+ 'users_social_disconnected' => ':socialAccount-Konto wurde erfolgreich vom Profil gelöst.',
];
diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php
index 3a6a1bc15..5ac4b1b27 100644
--- a/resources/lang/de/validation.php
+++ b/resources/lang/de/validation.php
@@ -19,54 +19,54 @@ return [
'alpha' => ':attribute kann nur Buchstaben enthalten.',
'alpha_dash' => ':attribute kann nur Buchstaben, Zahlen und Bindestriche enthalten.',
'alpha_num' => ':attribute kann nur Buchstaben und Zahlen enthalten.',
- 'array' => ':attribute muss eine Array sein.',
+ 'array' => ':attribute muss ein Array sein.',
'before' => ':attribute muss ein Datum vor :date sein.',
'between' => [
'numeric' => ':attribute muss zwischen :min und :max liegen.',
- 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.',
+ 'file' => ':attribute muss zwischen :min und :max Kilobytes groß sein.',
'string' => ':attribute muss zwischen :min und :max Zeichen lang sein.',
'array' => ':attribute muss zwischen :min und :max Elemente enthalten.',
],
'boolean' => ':attribute Feld muss wahr oder falsch sein.',
- 'confirmed' => ':attribute Bestätigung stimmt nicht überein.',
+ 'confirmed' => ':attribute stimmt nicht überein.',
'date' => ':attribute ist kein valides Datum.',
'date_format' => ':attribute entspricht nicht dem Format :format.',
- 'different' => ':attribute und :other müssen unterschiedlich sein.',
+ 'different' => ':attribute und :other müssen unterschiedlich sein.',
'digits' => ':attribute muss :digits Stellen haben.',
'digits_between' => ':attribute muss zwischen :min und :max Stellen haben.',
- 'email' => ':attribute muss eine valide E-Mail Adresse sein.',
- 'filled' => ':attribute Feld ist erforderlich.',
- 'exists' => 'Markiertes :attribute ist ungültig.',
+ 'email' => ':attribute muss eine valide E-Mail-Adresse sein.',
+ 'filled' => ':attribute ist erforderlich.',
+ 'exists' => ':attribute ist ungültig.',
'image' => ':attribute muss ein Bild sein.',
- 'in' => 'Markiertes :attribute ist ungültig.',
+ 'in' => ':attribute ist ungültig.',
'integer' => ':attribute muss eine Zahl sein.',
'ip' => ':attribute muss eine valide IP-Adresse sein.',
'max' => [
- 'numeric' => ':attribute darf nicht größer als :max sein.',
- 'file' => ':attribute darf nicht größer als :max Kilobyte sein.',
- 'string' => ':attribute darf nicht länger als :max Zeichen sein.',
+ 'numeric' => ':attribute darf nicht größer als :max sein.',
+ 'file' => ':attribute darf nicht größer als :max Kilobyte sein.',
+ 'string' => ':attribute darf nicht länger als :max Zeichen sein.',
'array' => ':attribute darf nicht mehr als :max Elemente enthalten.',
],
'mimes' => ':attribute muss eine Datei vom Typ: :values sein.',
'min' => [
- 'numeric' => ':attribute muss mindestens :min. sein',
- 'file' => ':attribute muss mindestens :min Kilobyte groß sein.',
+ 'numeric' => ':attribute muss mindestens :min sein',
+ 'file' => ':attribute muss mindestens :min Kilobyte groß sein.',
'string' => ':attribute muss mindestens :min Zeichen lang sein.',
'array' => ':attribute muss mindesten :min Elemente enthalten.',
],
- 'not_in' => 'Markiertes :attribute ist ungültig.',
+ 'not_in' => ':attribute ist ungültig.',
'numeric' => ':attribute muss eine Zahl sein.',
- 'regex' => ':attribute Format ist ungültig.',
- 'required' => ':attribute Feld ist erforderlich.',
- 'required_if' => ':attribute Feld ist erforderlich, wenn :other :value ist.',
- 'required_with' => ':attribute Feld ist erforderlich, wenn :values vorhanden ist.',
- 'required_with_all' => ':attribute Feld ist erforderlich, wenn :values vorhanden sind.',
- 'required_without' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden ist.',
- 'required_without_all' => ':attribute Feld ist erforderlich, wenn :values nicht vorhanden sind.',
- 'same' => ':attribute und :other muss übereinstimmen.',
+ 'regex' => ':attribute ist in einem ungültigen Format.',
+ 'required' => ':attribute ist erforderlich.',
+ 'required_if' => ':attribute ist erforderlich, wenn :other :value ist.',
+ 'required_with' => ':attribute ist erforderlich, wenn :values vorhanden ist.',
+ 'required_with_all' => ':attribute ist erforderlich, wenn :values vorhanden sind.',
+ 'required_without' => ':attribute ist erforderlich, wenn :values nicht vorhanden ist.',
+ 'required_without_all' => ':attribute ist erforderlich, wenn :values nicht vorhanden sind.',
+ 'same' => ':attribute und :other müssen übereinstimmen.',
'size' => [
'numeric' => ':attribute muss :size sein.',
- 'file' => ':attribute muss :size Kilobytes groß sein.',
+ 'file' => ':attribute muss :size Kilobytes groß sein.',
'string' => ':attribute muss :size Zeichen lang sein.',
'array' => ':attribute muss :size Elemente enthalten.',
],
diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php
index 450f4ce48..43053df10 100644
--- a/resources/lang/en/entities.php
+++ b/resources/lang/en/entities.php
@@ -234,4 +234,27 @@ return [
'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',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Comment',
+ 'comments' => 'Comments',
+ 'comment_placeholder' => 'Enter your comments here, markdown supported...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Comments',
+ 'one_comment' => '1 Comment',
+ 'comments_loading' => 'Loading...',
+ 'comment_save' => 'Save Comment',
+ 'comment_reply' => 'Reply',
+ 'comment_edit' => 'Edit',
+ 'comment_delete' => 'Delete',
+ 'comment_cancel' => 'Cancel',
+ 'comment_created' => 'Comment added',
+ 'comment_updated' => 'Comment updated',
+ 'comment_deleted' => 'Comment deleted',
+ 'comment_updated_text' => 'Updated :updateDiff by',
+ 'comment_delete_confirm' => 'This will remove the contents of the comment. Are you sure you want to delete this comment?',
+ 'comment_create' => 'Created'
+
];
\ No newline at end of file
diff --git a/resources/lang/en/errors.php b/resources/lang/en/errors.php
index c4578a37a..71bcd1f9a 100644
--- a/resources/lang/en/errors.php
+++ b/resources/lang/en/errors.php
@@ -60,6 +60,13 @@ return [
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role',
+ // 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 the comment.',
+ 'comment_delete' => 'An error occurred while deleting the comment.',
+ 'empty_comment' => 'Cannot add an 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.',
diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php
index d35857903..0a7547a58 100644
--- a/resources/lang/en/settings.php
+++ b/resources/lang/en/settings.php
@@ -123,6 +123,7 @@ return [
'pt_BR' => 'Português do Brasil',
'sk' => 'Slovensky',
'ja' => '日本語',
+ 'pl' => 'Polski',
]
///////////////////////////////////
];
diff --git a/resources/lang/es/entities.php b/resources/lang/es/entities.php
index d6b2810bc..2ca55a786 100644
--- a/resources/lang/es/entities.php
+++ b/resources/lang/es/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName no ha creado ningún libro',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Comentario',
+ 'comments' => 'Comentarios',
+ 'comment_placeholder' => 'Introduzca sus comentarios aquí, markdown supported ...',
+ 'no_comments' => 'No hay comentarios',
+ 'x_comments' => ':numComments Comentarios',
+ 'one_comment' => '1 Comentario',
+ 'comments_loading' => 'Cargando ...',
+ 'comment_save' => 'Guardar comentario',
+ 'comment_reply' => 'Responder',
+ 'comment_edit' => 'Editar',
+ 'comment_delete' => 'Eliminar',
+ 'comment_cancel' => 'Cancelar',
+ 'comment_created' => 'Comentario añadido',
+ 'comment_updated' => 'Comentario actualizado',
+ 'comment_deleted' => 'Comentario eliminado',
+ 'comment_updated_text' => 'Actualizado hace :updateDiff por',
+ 'comment_delete_confirm' => 'Esto eliminará el contenido del comentario. ¿Estás seguro de que quieres eliminar este comentario?',
+ 'comment_create' => 'Creado'
];
diff --git a/resources/lang/es/errors.php b/resources/lang/es/errors.php
index 1e39a3cb8..e488b6a1b 100644
--- a/resources/lang/es/errors.php
+++ b/resources/lang/es/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'Volverá a estar operativa en corto tiempo.',
+
+ // Comments
+ 'comment_list' => 'Se ha producido un error al buscar los comentarios.',
+ 'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.',
+ 'comment_add' => 'Se ha producido un error al añadir el comentario.',
+ 'comment_delete' => 'Se ha producido un error al eliminar el comentario.',
+ 'empty_comment' => 'No se puede agregar un comentario vacío.',
];
diff --git a/resources/lang/fr/auth.php b/resources/lang/fr/auth.php
index 41d051c5f..015bfdff0 100644
--- a/resources/lang/fr/auth.php
+++ b/resources/lang/fr/auth.php
@@ -10,7 +10,7 @@ return [
| these language lines according to your application's requirements.
|
*/
- 'failed' => 'Ces informations ne correspondent a aucun compte.',
+ 'failed' => 'Ces informations ne correspondent à aucun compte.',
'throttle' => "Trop d'essais, veuillez réessayer dans :seconds secondes.",
/**
@@ -26,7 +26,7 @@ return [
'password' => 'Mot de passe',
'password_confirm' => 'Confirmez le mot de passe',
'password_hint' => 'Doit faire plus de 5 caractères',
- 'forgot_password' => 'Mot de passe oublié?',
+ 'forgot_password' => 'Mot de passe oublié ?',
'remember_me' => 'Se souvenir de moi',
'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
'create_account' => 'Créer un compte',
@@ -35,9 +35,9 @@ return [
'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
'register_thanks' => 'Merci pour votre enregistrement',
- 'register_confirm' => 'Vérifiez vos e-mails et cliquer sur le lien de confirmation pour rejoindre :appName.',
+ 'register_confirm' => 'Vérifiez vos e-mails et cliquez sur le lien de confirmation pour rejoindre :appName.',
'registrations_disabled' => "L'inscription est désactivée pour le moment",
- 'registration_email_domain_invalid' => 'Cette adresse e-mail ne peux pas adcéder à l\'application',
+ 'registration_email_domain_invalid' => 'Cette adresse e-mail ne peut pas accéder à l\'application',
'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
@@ -51,7 +51,7 @@ return [
'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
- 'email_reset_text' => 'Vous recevez cet e-mail parceque nous avons reçu une demande de réinitialisation pour votre compte',
+ 'email_reset_text' => 'Vous recevez cet e-mail parce que nous avons reçu une demande de réinitialisation pour votre compte',
'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
@@ -59,11 +59,11 @@ return [
* Email Confirmation
*/
'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
- 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName!',
- 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous:',
+ 'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName !',
+ 'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous :',
'email_confirm_action' => 'Confirmez votre adresse e-mail',
'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
- 'email_confirm_success' => 'Votre adresse e-mail a été confirmée!',
+ 'email_confirm_success' => 'Votre adresse e-mail a été confirmée !',
'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
'email_not_confirmed' => 'Adresse e-mail non confirmée',
diff --git a/resources/lang/fr/common.php b/resources/lang/fr/common.php
index 129cf082e..74f174dca 100644
--- a/resources/lang/fr/common.php
+++ b/resources/lang/fr/common.php
@@ -9,7 +9,7 @@ return [
'back' => 'Retour',
'save' => 'Enregistrer',
'continue' => 'Continuer',
- 'select' => 'Selectionner',
+ 'select' => 'Sélectionner',
/**
* Form Labels
@@ -55,6 +55,6 @@ return [
/**
* Email Content
*/
- 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur:',
+ 'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
'email_rights' => 'Tous droits réservés',
];
diff --git a/resources/lang/fr/entities.php b/resources/lang/fr/entities.php
index 5562fb0fd..0d89993e9 100644
--- a/resources/lang/fr/entities.php
+++ b/resources/lang/fr/entities.php
@@ -12,7 +12,7 @@ return [
'recently_update' => 'Mis à jour récemment',
'recently_viewed' => 'Vus récemment',
'recent_activity' => 'Activité récente',
- 'create_now' => 'En créer un récemment',
+ 'create_now' => 'En créer un maintenant',
'revisions' => 'Révisions',
'meta_created' => 'Créé :timeLength',
'meta_created_name' => 'Créé :timeLength par :user',
@@ -59,8 +59,8 @@ return [
'books_create' => 'Créer un nouveau livre',
'books_delete' => 'Supprimer un livre',
'books_delete_named' => 'Supprimer le livre :bookName',
- 'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', Tous les chapitres et pages seront supprimés.',
- 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre?',
+ 'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', tous les chapitres et pages seront supprimés.',
+ 'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre ?',
'books_edit' => 'Modifier le livre',
'books_edit_named' => 'Modifier le livre :bookName',
'books_form_book_name' => 'Nom du livre',
@@ -90,18 +90,18 @@ return [
'chapters_create' => 'Créer un nouveau chapitre',
'chapters_delete' => 'Supprimer le chapitre',
'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
- 'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', Toutes les pages seront déplacée dans le livre parent.',
- 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre?',
+ 'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', toutes les pages seront déplacées dans le livre parent.',
+ 'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre ?',
'chapters_edit' => 'Modifier le chapitre',
'chapters_edit_named' => 'Modifier le chapitre :chapterName',
'chapters_save' => 'Enregistrer le chapitre',
- 'chapters_move' => 'Déplace le chapitre',
+ 'chapters_move' => 'Déplacer le chapitre',
'chapters_move_named' => 'Déplacer le chapitre :chapterName',
'chapter_move_success' => 'Chapitre déplacé dans :bookName',
'chapters_permissions' => 'Permissions du chapitre',
- 'chapters_empty' => 'Il n\'y a pas de pages dans ce chapitre actuellement.',
+ 'chapters_empty' => 'Il n\'y a pas de page dans ce chapitre actuellement.',
'chapters_permissions_active' => 'Permissions du chapitre activées',
- 'chapters_permissions_success' => 'Permissions du chapitres mises à jour',
+ 'chapters_permissions_success' => 'Permissions du chapitre mises à jour',
/**
* Pages
@@ -118,8 +118,8 @@ return [
'pages_delete_draft' => 'Supprimer le brouillon',
'pages_delete_success' => 'Page supprimée',
'pages_delete_draft_success' => 'Brouillon supprimé',
- 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page?',
- 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon?',
+ 'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page ?',
+ 'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon ?',
'pages_editing_named' => 'Modification de la page :pageName',
'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
'pages_edit_save_draft' => 'Enregistrer le brouillon',
@@ -131,7 +131,7 @@ return [
'pages_edit_discard_draft' => 'Ecarter le brouillon',
'pages_edit_set_changelog' => 'Remplir le journal des changements',
'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués',
- 'pages_edit_enter_changelog' => 'Entrez dans le journal des changements',
+ 'pages_edit_enter_changelog' => 'Entrer dans le journal des changements',
'pages_save' => 'Enregistrez la page',
'pages_title' => 'Titre de la page',
'pages_name' => 'Nom de la page',
@@ -139,7 +139,7 @@ return [
'pages_md_preview' => 'Prévisualisation',
'pages_md_insert_image' => 'Insérer une image',
'pages_md_insert_link' => 'Insérer un lien',
- 'pages_not_in_chapter' => 'La page n\'est pas dans un chanpitre',
+ 'pages_not_in_chapter' => 'La page n\'est pas dans un chapitre',
'pages_move' => 'Déplacer la page',
'pages_move_success' => 'Page déplacée à ":parentName"',
'pages_permissions' => 'Permissions de la page',
@@ -160,15 +160,15 @@ return [
'pages_initial_revision' => 'Publication initiale',
'pages_initial_name' => 'Nouvelle page',
'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.',
- 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visit. Vous devriez écarter ce brouillon.',
+ 'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visite. Vous devriez écarter ce brouillon.',
'pages_draft_edit_active' => [
- 'start_a' => ':count utilisateurs ont commencé a éditer cette page',
+ 'start_a' => ':count utilisateurs ont commencé à éditer cette page',
'start_b' => ':userName a commencé à éditer cette page',
'time_a' => 'depuis la dernière sauvegarde',
'time_b' => 'dans les :minCount dernières minutes',
- 'message' => ':start :time. Attention a ne pas écraser les mises à jour de quelqu\'un d\'autre!',
+ 'message' => ':start :time. Attention à ne pas écraser les mises à jour de quelqu\'un d\'autre !',
],
- 'pages_draft_discarded' => 'Brouuillon écarté, la page est dans sa version actuelle.',
+ 'pages_draft_discarded' => 'Brouillon écarté, la page est dans sa version actuelle.',
/**
* Editor sidebar
@@ -210,7 +210,29 @@ return [
*/
'profile_user_for_x' => 'Utilisateur depuis :time',
'profile_created_content' => 'Contenu créé',
- 'profile_not_created_pages' => ':userName n\'a pas créé de pages',
- 'profile_not_created_chapters' => ':userName n\'a pas créé de chapitres',
- 'profile_not_created_books' => ':userName n\'a pas créé de livres',
+ 'profile_not_created_pages' => ':userName n\'a pas créé de page',
+ 'profile_not_created_chapters' => ':userName n\'a pas créé de chapitre',
+ 'profile_not_created_books' => ':userName n\'a pas créé de livre',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Commentaire',
+ 'comments' => 'Commentaires',
+ 'comment_placeholder' => 'Entrez vos commentaires ici, merci supporté ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Commentaires',
+ 'one_comment' => '1 Commentaire',
+ 'comments_loading' => 'Loading ...',
+ 'comment_save' => 'Enregistrer le commentaire',
+ 'comment_reply' => 'Répondre',
+ 'comment_edit' => 'Modifier',
+ 'comment_delete' => 'Supprimer',
+ 'comment_cancel' => 'Annuler',
+ 'comment_created' => 'Commentaire ajouté',
+ 'comment_updated' => 'Commentaire mis à jour',
+ 'comment_deleted' => 'Commentaire supprimé',
+ 'comment_updated_text' => 'Mis à jour il y a :updateDiff par',
+ 'comment_delete_confirm' => 'Cela supprime le contenu du commentaire. Êtes-vous sûr de vouloir supprimer ce commentaire?',
+ 'comment_create' => 'Créé'
];
diff --git a/resources/lang/fr/errors.php b/resources/lang/fr/errors.php
index 72af89f7f..9e20147b6 100644
--- a/resources/lang/fr/errors.php
+++ b/resources/lang/fr/errors.php
@@ -18,21 +18,21 @@ return [
'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti',
'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe',
'ldap_extension_not_installed' => 'L\'extention LDAP PHP n\'est pas installée',
- 'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
- 'social_no_action_defined' => 'No action defined',
- 'social_account_in_use' => 'Cet compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
- 'social_account_email_in_use' => 'L\'email :email Est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
+ 'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
+ 'social_no_action_defined' => 'Pas d\'action définie',
+ 'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
+ 'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.',
'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ',
'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
- 'social_driver_not_found' => 'Social driver not found',
- 'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
+ 'social_driver_not_found' => 'Pilote de compte social absent',
+ 'social_driver_not_configured' => 'Vos préférences pour le compte :socialAccount sont incorrectes.',
// System
- 'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
+ 'path_not_writable' => 'Impossible d\'écrire dans :filePath. Assurez-vous d\'avoir les droits d\'écriture sur le serveur',
'cannot_get_image_from_url' => 'Impossible de récupérer l\'image depuis :url',
- 'cannot_create_thumbs' => 'Le serveur ne peux pas créer de miniatures, vérifier que l\extensions GD PHP est installée.',
+ 'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\'extension PHP GD est installée.',
'server_upload_limit' => 'La taille du fichier est trop grande.',
'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
@@ -57,7 +57,7 @@ return [
// Roles
'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
- 'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et on ne peut pas le supprimer',
+ 'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et ne peut pas être supprimé',
'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
// Error pages
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Une erreur est survenue',
'app_down' => ':appName n\'est pas en service pour le moment',
'back_soon' => 'Nous serons bientôt de retour.',
+
+ // comments
+ 'comment_list' => 'Une erreur s\'est produite lors de la récupération des commentaires.',
+ 'cannot_add_comment_to_draft' => 'Vous ne pouvez pas ajouter de commentaires à un projet.',
+ 'comment_add' => 'Une erreur s\'est produite lors de l\'ajout du commentaire.',
+ 'comment_delete' => 'Une erreur s\'est produite lors de la suppression du commentaire.',
+ 'empty_comment' => 'Impossible d\'ajouter un commentaire vide.',
];
diff --git a/resources/lang/fr/passwords.php b/resources/lang/fr/passwords.php
index 7be81da23..484b4b20c 100644
--- a/resources/lang/fr/passwords.php
+++ b/resources/lang/fr/passwords.php
@@ -16,7 +16,7 @@ return [
'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
'token' => 'Le jeton de réinitialisation est invalide.',
- 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe!',
- 'reset' => 'Votre mot de passe a été réinitialisé!',
+ 'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe !',
+ 'reset' => 'Votre mot de passe a été réinitialisé !',
];
diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php
index 5d9c966e7..5516e66a4 100644
--- a/resources/lang/fr/settings.php
+++ b/resources/lang/fr/settings.php
@@ -19,27 +19,27 @@ return [
'app_settings' => 'Préférences de l\'application',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
- 'app_name_header' => 'Afficher le nom dans l\'en-tête?',
- 'app_public_viewing' => 'Accepter le visionnage public des pages?',
- 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé?',
+ 'app_name_header' => 'Afficher le nom dans l\'en-tête ?',
+ 'app_public_viewing' => 'Accepter le visionnage public des pages ?',
+ 'app_secure_images' => 'Activer l\'ajout d\'image sécurisé ?',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_editor' => 'Editeur des pages',
'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.',
'app_custom_html' => 'HTML personnalisé dans l\'en-tête',
- 'app_custom_html_desc' => 'Le contenu inséré ici sera jouté en bas de la balise de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
+ 'app_custom_html_desc' => 'Le contenu inséré ici sera ajouté en bas de la balise de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
'app_logo' => 'Logo de l\'Application',
'app_logo_desc' => 'Cette image doit faire 43px de hauteur.
Les images plus larges seront réduites.',
'app_primary_color' => 'Couleur principale de l\'application',
- 'app_primary_color_desc' => 'This should be a hex value.
Leave empty to reset to the default color.',
+ 'app_primary_color_desc' => 'Cela devrait être une valeur hexadécimale.
Laisser vide pour rétablir la couleur par défaut.',
/**
* Registration settings
*/
'reg_settings' => 'Préférence pour l\'inscription',
- 'reg_allow' => 'Accepter l\'inscription?',
+ 'reg_allow' => 'Accepter l\'inscription ?',
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
- 'reg_confirm_email' => 'Obliger la confirmation par e-mail?',
+ 'reg_confirm_email' => '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 utilisateur recevront un e-mail de confirmation à cette adresse.
Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
@@ -57,17 +57,17 @@ return [
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
- 'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle?',
+ 'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle ?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
- 'role_name' => 'Nom du Rôle',
+ 'role_name' => 'Nom du rôle',
'role_desc' => 'Courte description du rôle',
'role_system' => 'Permissions système',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
- 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres chapitres et pages',
+ 'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
@@ -95,7 +95,7 @@ return [
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
- 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur?',
+ 'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur ?',
'users_delete_success' => 'Utilisateurs supprimés avec succès',
'users_edit' => 'Modifier l\'utilisateur',
'users_edit_profile' => 'Modifier le profil',
@@ -107,7 +107,7 @@ return [
'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.',
'users_social_connect' => 'Connecter le compte',
'users_social_disconnect' => 'Déconnecter le compte',
- 'users_social_connected' => 'Votre compte :socialAccount a élté ajouté avec succès.',
+ 'users_social_connected' => 'Votre compte :socialAccount a été ajouté avec succès.',
'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès',
];
diff --git a/resources/lang/nl/entities.php b/resources/lang/nl/entities.php
index d6975e130..6df9e5dd9 100644
--- a/resources/lang/nl/entities.php
+++ b/resources/lang/nl/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName heeft geen pagina\'s gemaakt',
'profile_not_created_chapters' => ':userName heeft geen hoofdstukken gemaakt',
'profile_not_created_books' => ':userName heeft geen boeken gemaakt',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Commentaar',
+ 'comments' => 'Commentaren',
+ 'comment_placeholder' => 'Vul hier uw reacties in, markdown ondersteund ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Opmerkingen',
+ 'one_comment' => '1 commentaar',
+ 'comments_loading' => 'Loading ...',
+ 'comment_save' => 'Opslaan opslaan',
+ 'comment_reply' => 'Antwoord',
+ 'comment_edit' => 'Bewerken',
+ 'comment_delete' => 'Verwijderen',
+ 'comment_cancel' => 'Annuleren',
+ 'comment_created' => 'Opmerking toegevoegd',
+ 'comment_updated' => 'Opmerking bijgewerkt',
+ 'comment_deleted' => 'Opmerking verwijderd',
+ 'comment_updated_text' => 'Bijgewerkt :updateDiff geleden door',
+ 'comment_delete_confirm' => 'Hiermee verwijdert u de inhoud van de reactie. Weet u zeker dat u deze reactie wilt verwijderen?',
+ 'comment_create' => 'Gemaakt'
];
\ No newline at end of file
diff --git a/resources/lang/nl/errors.php b/resources/lang/nl/errors.php
index f8b635bce..b8fab59fd 100644
--- a/resources/lang/nl/errors.php
+++ b/resources/lang/nl/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Er Ging Iets Fout',
'app_down' => ':appName is nu niet beschikbaar',
'back_soon' => 'Komt snel weer online.',
+
+ // Comments
+ 'comment_list' => 'Er is een fout opgetreden tijdens het ophalen van de reacties.',
+ 'cannot_add_comment_to_draft' => 'U kunt geen reacties toevoegen aan een ontwerp.',
+ 'comment_add' => 'Er is een fout opgetreden tijdens het toevoegen van de reactie.',
+ 'comment_delete' => 'Er is een fout opgetreden tijdens het verwijderen van de reactie.',
+ 'empty_comment' => 'Kan geen lege reactie toevoegen.',
];
\ No newline at end of file
diff --git a/resources/lang/pl/activities.php b/resources/lang/pl/activities.php
new file mode 100644
index 000000000..5ef5acab0
--- /dev/null
+++ b/resources/lang/pl/activities.php
@@ -0,0 +1,40 @@
+ 'utworzono stronę',
+ 'page_create_notification' => 'Strona utworzona pomyślnie',
+ 'page_update' => 'zaktualizowano stronę',
+ 'page_update_notification' => 'Strona zaktualizowana pomyślnie',
+ 'page_delete' => 'usunięto stronę',
+ 'page_delete_notification' => 'Strona usunięta pomyślnie',
+ 'page_restore' => 'przywrócono stronę',
+ 'page_restore_notification' => 'Stronga przywrócona pomyślnie',
+ 'page_move' => 'przeniesiono stronę',
+
+ // Chapters
+ 'chapter_create' => 'utworzono rozdział',
+ 'chapter_create_notification' => 'Rozdział utworzony pomyślnie',
+ 'chapter_update' => 'zaktualizowano rozdział',
+ 'chapter_update_notification' => 'Rozdział zaktualizowany pomyślnie',
+ 'chapter_delete' => 'usunięto rozdział',
+ 'chapter_delete_notification' => 'Rozdział usunięty pomyślnie',
+ 'chapter_move' => 'przeniesiono rozdział',
+
+ // Books
+ 'book_create' => 'utworzono księgę',
+ 'book_create_notification' => 'Księga utworzona pomyślnie',
+ 'book_update' => 'zaktualizowano księgę',
+ 'book_update_notification' => 'Księga zaktualizowana pomyślnie',
+ 'book_delete' => 'usunięto księgę',
+ 'book_delete_notification' => 'Księga usunięta pomyślnie',
+ 'book_sort' => 'posortowano księgę',
+ 'book_sort_notification' => 'Księga posortowana pomyślnie',
+
+];
diff --git a/resources/lang/pl/auth.php b/resources/lang/pl/auth.php
new file mode 100644
index 000000000..740e067ca
--- /dev/null
+++ b/resources/lang/pl/auth.php
@@ -0,0 +1,76 @@
+ 'These credentials do not match our records.',
+ 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+
+ /**
+ * Login & Register
+ */
+ 'sign_up' => 'Zarejestruj się',
+ 'log_in' => 'Zaloguj się',
+ 'log_in_with' => 'Zaloguj się za pomocą :socialDriver',
+ 'sign_up_with' => 'Zarejestruj się za pomocą :socialDriver',
+ 'logout' => 'Wyloguj',
+
+ 'name' => 'Imię',
+ 'username' => 'Nazwa użytkownika',
+ 'email' => 'Email',
+ 'password' => 'Hasło',
+ 'password_confirm' => 'Potwierdzenie hasła',
+ 'password_hint' => 'Musi mieć więcej niż 5 znaków',
+ 'forgot_password' => 'Przypomnij hasło',
+ 'remember_me' => 'Zapamiętaj mnie',
+ 'ldap_email_hint' => 'Wprowadź adres email dla tego konta.',
+ 'create_account' => 'Utwórz konto',
+ 'social_login' => 'Logowanie za pomocą konta społecznościowego',
+ 'social_registration' => 'Rejestracja za pomocą konta społecznościowego',
+ 'social_registration_text' => 'Zarejestruj się za pomocą innej usługi.',
+
+ 'register_thanks' => 'Dziękujemy za rejestrację!',
+ 'register_confirm' => 'Sprawdź podany adres e-mail i kliknij w link, by uzyskać dostęp do :appName.',
+ 'registrations_disabled' => 'Rejestracja jest obecnie zablokowana.',
+ 'registration_email_domain_invalid' => 'Adresy e-mail z tej domeny nie mają dostępu do tej aplikacji',
+ 'register_success' => 'Dziękujemy za rejestrację! Zalogowano Cię automatycznie.',
+
+
+ /**
+ * Password Reset
+ */
+ 'reset_password' => 'Resetowanie hasła',
+ 'reset_password_send_instructions' => 'Wprowadź adres e-mail powiązany z Twoim kontem, by otrzymać link do resetowania hasła.',
+ 'reset_password_send_button' => 'Wyślij link do resetowania hasła',
+ 'reset_password_sent_success' => 'Wysłano link do resetowania hasła na adres :email.',
+ 'reset_password_success' => 'Hasło zostało zresetowane pomyślnie.',
+
+ 'email_reset_subject' => 'Resetowanie hasła do :appName',
+ 'email_reset_text' => 'Otrzymujesz tę wiadomość ponieważ ktoś zażądał zresetowania hasła do Twojego konta.',
+ 'email_reset_not_requested' => 'Jeśli to nie Ty złożyłeś żądanie zresetowania hasła, zignoruj tę wiadomość.',
+
+
+ /**
+ * Email Confirmation
+ */
+ 'email_confirm_subject' => 'Potwierdź swój adres email w :appName',
+ 'email_confirm_greeting' => 'Dziękujemy za dołączenie do :appName!',
+ 'email_confirm_text' => 'Prosimy byś potwierdził swoje hasło klikając przycisk poniżej:',
+ 'email_confirm_action' => 'Potwierdź email',
+ 'email_confirm_send_error' => 'Wymagane jest potwierdzenie hasła, lecz wiadomość nie mogła zostać wysłana. Skontaktuj się z administratorem w celu upewnienia się, że skrzynka została skonfigurowana prawidłowo.',
+ 'email_confirm_success' => 'Adres email został potwierdzony!',
+ 'email_confirm_resent' => 'Wiadomość potwierdzająca została wysłana, sprawdź swoją skrzynkę.',
+
+ 'email_not_confirmed' => 'Adres email niepotwierdzony',
+ 'email_not_confirmed_text' => 'Twój adres email nie został jeszcze potwierdzony.',
+ 'email_not_confirmed_click_link' => 'Aby potwierdzić swoje konto kliknij w link wysłany w wiadomości po rejestracji.',
+ 'email_not_confirmed_resend' => 'Jeśli wiadomość do Ciebie nie dotarła możesz wysłać ją ponownie wypełniając formularz poniżej.',
+ 'email_not_confirmed_resend_button' => 'Wyślij ponownie wiadomość z potwierdzeniem',
+];
\ No newline at end of file
diff --git a/resources/lang/pl/common.php b/resources/lang/pl/common.php
new file mode 100644
index 000000000..1c8963653
--- /dev/null
+++ b/resources/lang/pl/common.php
@@ -0,0 +1,59 @@
+ 'Anuluj',
+ 'confirm' => 'Zatwierdź',
+ 'back' => 'Wstecz',
+ 'save' => 'Zapisz',
+ 'continue' => 'Kontynuuj',
+ 'select' => 'Wybierz',
+
+ /**
+ * Form Labels
+ */
+ 'name' => 'Nazwa',
+ 'description' => 'Opis',
+ 'role' => 'Rola',
+
+ /**
+ * Actions
+ */
+ 'actions' => 'Akcje',
+ 'view' => 'Widok',
+ 'create' => 'Utwórz',
+ 'update' => 'Zaktualizuj',
+ 'edit' => 'Edytuj',
+ 'sort' => 'Sortuj',
+ 'move' => 'Przenieś',
+ 'delete' => 'Usuń',
+ 'search' => 'Szukaj',
+ 'search_clear' => 'Wyczyść wyszukiwanie',
+ 'reset' => 'Resetuj',
+ 'remove' => 'Usuń',
+ 'add' => 'Dodaj',
+
+
+ /**
+ * Misc
+ */
+ 'deleted_user' => 'Użytkownik usunięty',
+ 'no_activity' => 'Brak aktywności do pokazania',
+ 'no_items' => 'Brak elementów do wyświetlenia',
+ 'back_to_top' => 'Powrót na górę',
+ 'toggle_details' => 'Włącz/wyłącz szczegóły',
+
+ /**
+ * Header
+ */
+ 'view_profile' => 'Zobacz profil',
+ 'edit_profile' => 'Edytuj profil',
+
+ /**
+ * Email Content
+ */
+ 'email_action_help' => 'Jeśli masz problem z kliknięciem przycisku ":actionText", skopiuj i wklej poniższy adres URL w nowej karcie swojej przeglądarki:',
+ 'email_rights' => 'Wszelkie prawa zastrzeżone',
+];
\ No newline at end of file
diff --git a/resources/lang/pl/components.php b/resources/lang/pl/components.php
new file mode 100644
index 000000000..c1dbcd44b
--- /dev/null
+++ b/resources/lang/pl/components.php
@@ -0,0 +1,32 @@
+ 'Wybór obrazka',
+ 'image_all' => 'Wszystkie',
+ 'image_all_title' => 'Zobacz wszystkie obrazki',
+ 'image_book_title' => 'Zobacz obrazki zapisane w tej księdze',
+ 'image_page_title' => 'Zobacz obrazki zapisane na tej stronie',
+ 'image_search_hint' => 'Szukaj po nazwie obrazka',
+ 'image_uploaded' => 'Udostępniono :uploadedDate',
+ 'image_load_more' => 'Wczytaj więcej',
+ 'image_image_name' => 'Nazwa obrazka',
+ 'image_delete_confirm' => 'Ten obrazek jest używany na stronach poniżej, kliknij ponownie Usuń by potwierdzić usunięcie obrazka.',
+ 'image_select_image' => 'Wybierz obrazek',
+ 'image_dropzone' => 'Upuść obrazki tutaj lub kliknij by wybrać obrazki do udostępnienia',
+ 'images_deleted' => 'Usunięte obrazki',
+ 'image_preview' => 'Podgląd obrazka',
+ 'image_upload_success' => 'Obrazek wysłany pomyślnie',
+ 'image_update_success' => 'Szczegóły obrazka zaktualizowane pomyślnie',
+ 'image_delete_success' => 'Obrazek usunięty pomyślnie',
+
+ /**
+ * Code editor
+ */
+ 'code_editor' => 'Edytuj kod',
+ 'code_language' => 'Język kodu',
+ 'code_content' => 'Zawartość kodu',
+ 'code_save' => 'Zapisz kod',
+];
\ No newline at end of file
diff --git a/resources/lang/pl/entities.php b/resources/lang/pl/entities.php
new file mode 100644
index 000000000..30e853bce
--- /dev/null
+++ b/resources/lang/pl/entities.php
@@ -0,0 +1,237 @@
+ 'Ostatnio utworzone',
+ 'recently_created_pages' => 'Ostatnio utworzone strony',
+ 'recently_updated_pages' => 'Ostatnio zaktualizowane strony',
+ 'recently_created_chapters' => 'Ostatnio utworzone rozdziały',
+ 'recently_created_books' => 'Ostatnio utworzone księgi',
+ 'recently_update' => 'Ostatnio zaktualizowane',
+ 'recently_viewed' => 'Ostatnio wyświetlane',
+ 'recent_activity' => 'Ostatnia aktywność',
+ 'create_now' => 'Utwórz teraz',
+ 'revisions' => 'Rewizje',
+ 'meta_revision' => 'Rewizja #:revisionCount',
+ 'meta_created' => 'Utworzono :timeLength',
+ 'meta_created_name' => 'Utworzono :timeLength przez :user',
+ 'meta_updated' => 'Zaktualizowano :timeLength',
+ 'meta_updated_name' => 'Zaktualizowano :timeLength przez :user',
+ 'x_pages' => ':count stron',
+ 'entity_select' => 'Wybór encji',
+ 'images' => 'Obrazki',
+ 'my_recent_drafts' => 'Moje ostatnie szkice',
+ 'my_recently_viewed' => 'Moje ostatnio wyświetlane',
+ 'no_pages_viewed' => 'Nie wyświetlano żadnych stron',
+ 'no_pages_recently_created' => 'Nie utworzono ostatnio żadnych stron',
+ 'no_pages_recently_updated' => 'Nie zaktualizowano ostatnio żadnych stron',
+ 'export' => 'Eksportuj',
+ 'export_html' => 'Plik HTML',
+ 'export_pdf' => 'Plik PDF',
+ 'export_text' => 'Plik tekstowy',
+
+ /**
+ * Permissions and restrictions
+ */
+ 'permissions' => 'Uprawnienia',
+ 'permissions_intro' => 'Jeśli odblokowane, te uprawnienia będą miały priorytet względem pozostałych ustawionych uprawnień ról.',
+ 'permissions_enable' => 'Odblokuj własne uprawnienia',
+ 'permissions_save' => 'Zapisz uprawnienia',
+
+ /**
+ * Search
+ */
+ 'search_results' => 'Wyniki wyszukiwania',
+ 'search_total_results_found' => ':count znalezionych wyników|:count ogółem znalezionych wyników',
+ 'search_clear' => 'Wyczyść wyszukiwanie',
+ 'search_no_pages' => 'Brak stron spełniających zadane kryterium',
+ 'search_for_term' => 'Szukaj :term',
+ 'search_more' => 'Więcej wyników',
+ 'search_filters' => 'Filtry wyszukiwania',
+ 'search_content_type' => 'Rodziaj treści',
+ 'search_exact_matches' => 'Dokładne frazy',
+ 'search_tags' => 'Tagi wyszukiwania',
+ 'search_viewed_by_me' => 'Wyświetlone przeze mnie',
+ 'search_not_viewed_by_me' => 'Niewyświetlone przeze mnie',
+ 'search_permissions_set' => 'Zbiór uprawnień',
+ 'search_created_by_me' => 'Utworzone przeze mnie',
+ 'search_updated_by_me' => 'Zaktualizowane przeze mnie',
+ 'search_updated_before' => 'Zaktualizowane przed',
+ 'search_updated_after' => 'Zaktualizowane po',
+ 'search_created_before' => 'Utworzone przed',
+ 'search_created_after' => 'Utworzone po',
+ 'search_set_date' => 'Ustaw datę',
+ 'search_update' => 'Zaktualizuj wyszukiwanie',
+
+ /**
+ * Books
+ */
+ 'book' => 'Księga',
+ 'books' => 'Księgi',
+ 'books_empty' => 'Brak utworzonych ksiąg',
+ 'books_popular' => 'Popularne księgi',
+ 'books_recent' => 'Ostatnie księgi',
+ 'books_popular_empty' => 'Najbardziej popularne księgi zostaną wyświetlone w tym miejscu.',
+ 'books_create' => 'Utwórz księgę',
+ 'books_delete' => 'Usuń księgę',
+ 'books_delete_named' => 'Usuń księgę :bookName',
+ 'books_delete_explain' => 'To spowoduje usunięcie księgi \':bookName\', Wszystkie strony i rozdziały zostaną usunięte.',
+ 'books_delete_confirmation' => 'Czy na pewno chcesz usunąc tę księgę?',
+ 'books_edit' => 'Edytuj księgę',
+ 'books_edit_named' => 'Edytuj księgę :bookName',
+ 'books_form_book_name' => 'Nazwa księgi',
+ 'books_save' => 'Zapisz księgę',
+ 'books_permissions' => 'Uprawnienia księgi',
+ 'books_permissions_updated' => 'Zaktualizowano uprawnienia księgi',
+ 'books_empty_contents' => 'Brak stron lub rozdziałów w tej księdze.',
+ 'books_empty_create_page' => 'Utwórz nową stronę',
+ 'books_empty_or' => 'lub',
+ 'books_empty_sort_current_book' => 'posortuj bieżącą księgę',
+ 'books_empty_add_chapter' => 'Dodaj rozdział',
+ 'books_permissions_active' => 'Uprawnienia księgi aktywne',
+ 'books_search_this' => 'Wyszukaj w tej księdze',
+ 'books_navigation' => 'Nawigacja po księdze',
+ 'books_sort' => 'Sortuj zawartość Księgi',
+ 'books_sort_named' => 'Sortuj księgę :bookName',
+ 'books_sort_show_other' => 'Pokaż inne księgi',
+ 'books_sort_save' => 'Zapisz nowy porządek',
+
+ /**
+ * Chapters
+ */
+ 'chapter' => 'Rozdział',
+ 'chapters' => 'Rozdziały',
+ 'chapters_popular' => 'Popularne rozdziały',
+ 'chapters_new' => 'Nowy rozdział',
+ 'chapters_create' => 'Utwórz nowy rozdział',
+ 'chapters_delete' => 'Usuń rozdział',
+ 'chapters_delete_named' => 'Usuń rozdział :chapterName',
+ 'chapters_delete_explain' => 'To spowoduje usunięcie rozdziału \':chapterName\', Wszystkie strony zostaną usunięte
+ i dodane bezpośrednio do księgi macierzystej.',
+ 'chapters_delete_confirm' => 'Czy na pewno chcesz usunąć ten rozdział?',
+ 'chapters_edit' => 'Edytuj rozdział',
+ 'chapters_edit_named' => 'Edytuj rozdział :chapterName',
+ 'chapters_save' => 'Zapisz rozdział',
+ 'chapters_move' => 'Przenieś rozdział',
+ 'chapters_move_named' => 'Przenieś rozdział :chapterName',
+ 'chapter_move_success' => 'Rozdział przeniesiony do :bookName',
+ 'chapters_permissions' => 'Uprawienia rozdziału',
+ 'chapters_empty' => 'Brak stron w tym rozdziale.',
+ 'chapters_permissions_active' => 'Uprawnienia rozdziału aktywne',
+ 'chapters_permissions_success' => 'Zaktualizowano uprawnienia rozdziału',
+ 'chapters_search_this' => 'Przeszukaj ten rozdział',
+
+ /**
+ * Pages
+ */
+ 'page' => 'Strona',
+ 'pages' => 'Strony',
+ 'pages_popular' => 'Popularne strony',
+ 'pages_new' => 'Nowa strona',
+ 'pages_attachments' => 'Załączniki',
+ 'pages_navigation' => 'Nawigacja po stronie',
+ 'pages_delete' => 'Usuń stronę',
+ 'pages_delete_named' => 'Usuń stronę :pageName',
+ 'pages_delete_draft_named' => 'Usuń szkic strony :pageName',
+ 'pages_delete_draft' => 'Usuń szkic strony',
+ 'pages_delete_success' => 'Strona usunięta pomyślnie',
+ 'pages_delete_draft_success' => 'Szkic strony usunięty pomyślnie',
+ 'pages_delete_confirm' => 'Czy na pewno chcesz usunąć tę stron?',
+ 'pages_delete_draft_confirm' => 'Czy na pewno chcesz usunąć szkic strony?',
+ 'pages_editing_named' => 'Edytowanie strony :pageName',
+ 'pages_edit_toggle_header' => 'Włącz/wyłącz nagłówek',
+ 'pages_edit_save_draft' => 'Zapisz szkic',
+ 'pages_edit_draft' => 'Edytuj szkic strony',
+ 'pages_editing_draft' => 'Edytowanie szkicu strony',
+ 'pages_editing_page' => 'Edytowanie strony',
+ 'pages_edit_draft_save_at' => 'Szkic zapisany ',
+ 'pages_edit_delete_draft' => 'Usuń szkic',
+ 'pages_edit_discard_draft' => 'Porzuć szkic',
+ 'pages_edit_set_changelog' => 'Ustaw log zmian',
+ 'pages_edit_enter_changelog_desc' => 'Opisz zmiany, które zostały wprowadzone',
+ 'pages_edit_enter_changelog' => 'Wyświetl log zmian',
+ 'pages_save' => 'Zapisz stronę',
+ 'pages_title' => 'Tytuł strony',
+ 'pages_name' => 'Nazwa strony',
+ 'pages_md_editor' => 'Edytor',
+ 'pages_md_preview' => 'Podgląd',
+ 'pages_md_insert_image' => 'Wstaw obrazek',
+ 'pages_md_insert_link' => 'Wstaw łącze do encji',
+ 'pages_not_in_chapter' => 'Strona nie została umieszczona w rozdziale',
+ 'pages_move' => 'Przenieś stronę',
+ 'pages_move_success' => 'Strona przeniesiona do ":parentName"',
+ 'pages_permissions' => 'Uprawnienia strony',
+ 'pages_permissions_success' => 'Zaktualizowano uprawnienia strony',
+ 'pages_revisions' => 'Rewizje strony',
+ 'pages_revisions_named' => 'Rewizje strony :pageName',
+ 'pages_revision_named' => 'Rewizja stroony :pageName',
+ 'pages_revisions_created_by' => 'Utworzona przez',
+ 'pages_revisions_date' => 'Data rewizji',
+ 'pages_revisions_number' => '#',
+ 'pages_revisions_changelog' => 'Log zmian',
+ 'pages_revisions_changes' => 'Zmiany',
+ 'pages_revisions_current' => 'Obecna wersja',
+ 'pages_revisions_preview' => 'Podgląd',
+ 'pages_revisions_restore' => 'Przywróć',
+ 'pages_revisions_none' => 'Ta strona nie posiada żadnych rewizji',
+ 'pages_copy_link' => 'Kopiuj link',
+ 'pages_permissions_active' => 'Uprawnienia strony aktywne',
+ 'pages_initial_revision' => 'Wydanie pierwotne',
+ 'pages_initial_name' => 'Nowa strona',
+ 'pages_editing_draft_notification' => 'Edytujesz obecnie szkic, który był ostatnio zapisany :timeDiff.',
+ 'pages_draft_edited_notification' => 'Od tego czasu ta strona była zmieniana. Zalecane jest odrzucenie tego szkicu.',
+ 'pages_draft_edit_active' => [
+ 'start_a' => ':count użytkowników rozpoczęło edytowanie tej strony',
+ 'start_b' => ':userName edytuje stronę',
+ 'time_a' => ' od czasu ostatniej edycji',
+ 'time_b' => 'w ciągu ostatnich :minCount minut',
+ 'message' => ':start :time. Pamiętaj by nie nadpisywać czyichś zmian!',
+ ],
+ 'pages_draft_discarded' => 'Szkic odrzucony, edytor został uzupełniony najnowszą wersją strony',
+
+ /**
+ * Editor sidebar
+ */
+ 'page_tags' => 'Tagi strony',
+ 'tag' => 'Tag',
+ 'tags' => '',
+ 'tag_value' => 'Wartość tagu (opcjonalnie)',
+ 'tags_explain' => "Dodaj tagi by skategoryzować zawartość. \n W celu dokładniejszej organizacji zawartości możesz dodać wartości do tagów.",
+ 'tags_add' => 'Dodaj kolejny tag',
+ 'attachments' => 'Załączniki',
+ 'attachments_explain' => 'Udostępnij kilka plików lub załącz link. Będą one widoczne na marginesie strony.',
+ 'attachments_explain_instant_save' => 'Zmiany są zapisywane natychmiastowo.',
+ 'attachments_items' => 'Załączniki',
+ 'attachments_upload' => 'Dodaj plik',
+ 'attachments_link' => 'Dodaj link',
+ 'attachments_set_link' => 'Ustaw link',
+ 'attachments_delete_confirm' => 'Kliknij ponownie Usuń by potwierdzić usunięcie załącznika.',
+ 'attachments_dropzone' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki',
+ 'attachments_no_files' => 'Nie udostępniono plików',
+ 'attachments_explain_link' => 'Możesz załączyć link jeśli nie chcesz udostępniać pliku. Może być to link do innej strony lub link do pliku w chmurze.',
+ 'attachments_link_name' => 'Nazwa linku',
+ 'attachment_link' => 'Link do załącznika',
+ 'attachments_link_url' => 'Link do pliku',
+ 'attachments_link_url_hint' => 'Strona lub plik',
+ 'attach' => 'Załącz',
+ 'attachments_edit_file' => 'Edytuj plik',
+ 'attachments_edit_file_name' => 'Nazwa pliku',
+ 'attachments_edit_drop_upload' => 'Upuść pliki lub kliknij tutaj by udostępnić pliki i nadpisać istniejące',
+ 'attachments_order_updated' => 'Kolejność załączników zaktualizowana',
+ 'attachments_updated_success' => 'Szczegóły załączników zaktualizowane',
+ 'attachments_deleted' => 'Załączniki usunięte',
+ 'attachments_file_uploaded' => 'Plik załączony pomyślnie',
+ 'attachments_file_updated' => 'Plik zaktualizowany pomyślnie',
+ 'attachments_link_attached' => 'Link pomyślnie dodany do strony',
+
+ /**
+ * Profile View
+ */
+ 'profile_user_for_x' => 'Użytkownik od :time',
+ 'profile_created_content' => 'Utworzona zawartość',
+ 'profile_not_created_pages' => ':userName nie utworzył żadnych stron',
+ 'profile_not_created_chapters' => ':userName nie utworzył żadnych rozdziałów',
+ 'profile_not_created_books' => ':userName nie utworzył żadnych ksiąg',
+];
\ No newline at end of file
diff --git a/resources/lang/pl/errors.php b/resources/lang/pl/errors.php
new file mode 100644
index 000000000..633bf7a2d
--- /dev/null
+++ b/resources/lang/pl/errors.php
@@ -0,0 +1,70 @@
+ 'Nie masz uprawnień do wyświetlenia tej strony.',
+ 'permissionJson' => 'Nie masz uprawnień do wykonania tej akcji.',
+
+ // Auth
+ 'error_user_exists_different_creds' => 'Użytkownik o adresie :email już istnieje.',
+ 'email_already_confirmed' => 'Email został potwierdzony, spróbuj się zalogować.',
+ 'email_confirmation_invalid' => 'Ten token jest nieprawidłowy lub został już wykorzystany. Spróbuj zarejestrować się ponownie.',
+ 'email_confirmation_expired' => 'Ten token potwierdzający wygasł. Wysłaliśmy Ci kolejny.',
+ 'ldap_fail_anonymous' => 'Dostęp LDAP przy użyciu anonimowego powiązania nie powiódł się',
+ 'ldap_fail_authed' => 'Dostęp LDAP przy użyciu tego dn i hasła nie powiódł się',
+ 'ldap_extension_not_installed' => 'Rozszerzenie LDAP PHP nie zostało zainstalowane',
+ 'ldap_cannot_connect' => 'Nie można połączyć z serwerem LDAP, połączenie nie zostało ustanowione',
+ 'social_no_action_defined' => 'Brak zdefiniowanej akcji',
+ 'social_account_in_use' => 'To konto :socialAccount jest już w użyciu, spróbuj zalogować się za pomocą opcji :socialAccount.',
+ 'social_account_email_in_use' => 'Email :email jest już w użyciu. Jeśli masz już konto, połącz konto :socialAccount z poziomu ustawień profilu.',
+ 'social_account_existing' => 'Konto :socialAccount jest już połączone z Twoim profilem',
+ 'social_account_already_used_existing' => 'Konto :socialAccount jest już używane przez innego użytkownika.',
+ 'social_account_not_used' => 'To konto :socialAccount nie jest połączone z żadnym użytkownikiem. Połącz je ze swoim kontem w ustawieniach profilu. ',
+ 'social_account_register_instructions' => 'Jeśli nie masz jeszcze konta, możesz zarejestrować je używając opcji :socialAccount.',
+ 'social_driver_not_found' => 'Funkcja społecznościowa nie została odnaleziona',
+ 'social_driver_not_configured' => 'Ustawienia konta :socialAccount nie są poprawne.',
+
+ // System
+ 'path_not_writable' => 'Zapis do ścieżki :filePath jest niemożliwy. Upewnij się że aplikacja ma prawa do zapisu w niej.',
+ 'cannot_get_image_from_url' => 'Nie można pobrać obrazka z :url',
+ 'cannot_create_thumbs' => 'Serwer nie może utworzyć miniaturek. Upewnij się że rozszerzenie GD PHP zostało zainstalowane.',
+ 'server_upload_limit' => 'Serwer nie pozwala na przyjęcie pliku o tym rozmiarze. Spróbuj udostępnić coś o mniejszym rozmiarze.',
+ 'image_upload_error' => 'Wystąpił błąd podczas udostępniania obrazka',
+
+ // Attachments
+ 'attachment_page_mismatch' => 'Niezgodność stron podczas aktualizacji załącznika',
+
+ // Pages
+ 'page_draft_autosave_fail' => 'Zapis szkicu nie powiódł się. Upewnij się że posiadasz połączenie z internetem.',
+
+ // Entities
+ 'entity_not_found' => 'Encja nie została odnaleziona',
+ 'book_not_found' => 'Księga nie została odnaleziona',
+ 'page_not_found' => 'Strona nie została odnaleziona',
+ 'chapter_not_found' => 'Rozdział nie został odnaleziony',
+ 'selected_book_not_found' => 'Wybrana księga nie została odnaleziona',
+ 'selected_book_chapter_not_found' => 'Wybrana księga lub rozdział nie zostały odnalezione',
+ 'guests_cannot_save_drafts' => 'Goście nie mogą zapisywać szkiców',
+
+ // Users
+ 'users_cannot_delete_only_admin' => 'Nie możesz usunąć jedynego administratora',
+ 'users_cannot_delete_guest' => 'Nie możesz usunąć użytkownika-gościa',
+
+ // Roles
+ 'role_cannot_be_edited' => 'Ta rola nie może być edytowana',
+ 'role_system_cannot_be_deleted' => 'Ta rola jest rolą systemową i nie może zostać usunięta',
+ 'role_registration_default_cannot_delete' => 'Ta rola nie może zostać usunięta jeśli jest ustawiona jako domyślna rola użytkownika',
+
+ // Error pages
+ '404_page_not_found' => 'Strona nie została odnaleziona',
+ 'sorry_page_not_found' => 'Przepraszamy, ale strona której szukasz nie została odnaleziona.',
+ 'return_home' => 'Powrót do strony głównej',
+ 'error_occurred' => 'Wystąpił błąd',
+ 'app_down' => ':appName jest aktualnie wyłączona',
+ 'back_soon' => 'Niedługo zostanie uruchomiona ponownie.',
+];
\ No newline at end of file
diff --git a/resources/lang/pl/pagination.php b/resources/lang/pl/pagination.php
new file mode 100644
index 000000000..564694190
--- /dev/null
+++ b/resources/lang/pl/pagination.php
@@ -0,0 +1,19 @@
+ '« Poprzednia',
+ 'next' => 'Następna »',
+
+];
diff --git a/resources/lang/pl/passwords.php b/resources/lang/pl/passwords.php
new file mode 100644
index 000000000..a9e669f4d
--- /dev/null
+++ b/resources/lang/pl/passwords.php
@@ -0,0 +1,22 @@
+ 'Hasło musi zawierać co najmniej 6 znaków i być zgodne z powtórzeniem.',
+ 'user' => "Nie znaleziono użytkownika o takim adresie email.",
+ 'token' => 'Ten token resetowania hasła jest nieprawidłowy.',
+ 'sent' => 'Wysłaliśmy Ci link do resetowania hasła!',
+ 'reset' => 'Twoje hasło zostało zresetowane!',
+
+];
diff --git a/resources/lang/pl/settings.php b/resources/lang/pl/settings.php
new file mode 100644
index 000000000..381e5517a
--- /dev/null
+++ b/resources/lang/pl/settings.php
@@ -0,0 +1,111 @@
+ 'Ustawienia',
+ 'settings_save' => 'Zapisz ustawienia',
+ 'settings_save_success' => 'Ustawienia zapisane',
+
+ /**
+ * App settings
+ */
+
+ 'app_settings' => 'Ustawienia aplikacji',
+ 'app_name' => 'Nazwa aplikacji',
+ 'app_name_desc' => 'Ta nazwa jest wyświetlana w nagłówku i emailach.',
+ 'app_name_header' => 'Pokazać nazwę aplikacji w nagłówku?',
+ 'app_public_viewing' => 'Zezwolić na publiczne przeglądanie?',
+ 'app_secure_images' => 'Odblokować wyższe bezpieczeństwo obrazków?',
+ 'app_secure_images_desc' => 'Ze względów wydajnościowych wszystkie obrazki są publiczne. Ta opcja dodaje dodatkowy, trudny do zgadnienia losowy ciąg na początku nazwy obrazka. Upewnij się że indeksowanie ścieżek jest zablokowane, by uniknąć problemów z dostępem do obrazka.',
+ 'app_editor' => 'Edytor strony',
+ 'app_editor_desc' => 'Wybierz edytor używany przez użytkowników do edycji zawartości.',
+ 'app_custom_html' => 'Własna zawartość tagu ',
+ 'app_custom_html_desc' => 'Zawartość dodana tutaj zostanie dołączona do sekcji każdej strony. Przydatne przy nadpisywaniu styli lub dodawaniu analityki.',
+ 'app_logo' => 'Logo aplikacji',
+ 'app_logo_desc' => 'Ten obrazek powinien mieć nie więcej niż 43px w pionie.
Większe obrazki będą skalowane w dół.',
+ 'app_primary_color' => 'Podstawowy kolor aplikacji',
+ 'app_primary_color_desc' => 'To powinna być wartość HEX.
Zostaw to pole puste, by powrócić do podstawowego koloru.',
+
+ /**
+ * Registration settings
+ */
+
+ 'reg_settings' => 'Ustawienia rejestracji',
+ 'reg_allow' => 'Zezwolić na rejestrację?',
+ 'reg_default_role' => 'Domyślna rola użytkownika po rejestracji',
+ 'reg_confirm_email' => 'Wymagać potwierdzenia adresu email?',
+ 'reg_confirm_email_desc' => 'Jeśli restrykcje domenowe zostały uzupełnione potwierdzenie adresu stanie się konieczne, a poniższa wartośc zostanie zignorowana.',
+ 'reg_confirm_restrict_domain' => 'Restrykcje domenowe dot. adresu email',
+ 'reg_confirm_restrict_domain_desc' => 'Wprowadź listę domen adresów email rozdzieloną przecinkami, którym chciałbyś zezwolić na rejestrację. Wymusi to konieczność potwierdzenia adresu email przez użytkownika przed uzyskaniem dostępu do aplikacji.
Pamiętaj, że użytkownicy będą mogli zmienić adres email po rejestracji.',
+ 'reg_confirm_restrict_domain_placeholder' => 'Brak restrykcji',
+
+ /**
+ * Role settings
+ */
+
+ 'roles' => 'Role',
+ 'role_user_roles' => 'Role użytkownika',
+ 'role_create' => 'Utwórz nową rolę',
+ 'role_create_success' => 'Rola utworzona pomyślnie',
+ 'role_delete' => 'Usuń rolę',
+ 'role_delete_confirm' => 'To spowoduje usunięcie roli \':roleName\'.',
+ 'role_delete_users_assigned' => 'Tę rolę ma przypisanych :userCount użytkowników. Jeśli chcesz zmigrować użytkowników z tej roli, wybierz nową poniżej.',
+ 'role_delete_no_migration' => "Nie migruj użytkowników",
+ 'role_delete_sure' => 'Czy na pewno chcesz usunąć tę rolę?',
+ 'role_delete_success' => 'Rola usunięta pomyślnie',
+ 'role_edit' => 'Edytuj rolę',
+ 'role_details' => 'Szczegóły roli',
+ 'role_name' => 'Nazwa roli',
+ 'role_desc' => 'Krótki opis roli',
+ 'role_system' => 'Uprawnienia systemowe',
+ 'role_manage_users' => 'Zarządzanie użytkownikami',
+ 'role_manage_roles' => 'Zarządzanie rolami i uprawnieniami ról',
+ 'role_manage_entity_permissions' => 'Zarządzanie uprawnieniami ksiąg, rozdziałów i stron',
+ 'role_manage_own_entity_permissions' => 'Zarządzanie uprawnieniami własnych ksiąg, rozdziałów i stron',
+ 'role_manage_settings' => 'Zarządzanie ustawieniami aplikacji',
+ 'role_asset' => 'Zarządzanie zasobami',
+ 'role_asset_desc' => 'Te ustawienia kontrolują zarządzanie zasobami systemu. Uprawnienia ksiąg, rozdziałów i stron nadpisują te ustawienia.',
+ 'role_all' => 'Wszyscy',
+ 'role_own' => 'Własne',
+ 'role_controlled_by_asset' => 'Kontrolowane przez zasób, do którego zostały udostępnione',
+ 'role_save' => 'Zapisz rolę',
+ 'role_update_success' => 'Rola zapisana pomyślnie',
+ 'role_users' => 'Użytkownicy w tej roli',
+ 'role_users_none' => 'Brak użytkowników zapisanych do tej roli',
+
+ /**
+ * Users
+ */
+
+ 'users' => 'Użytkownicy',
+ 'user_profile' => 'Profil użytkownika',
+ 'users_add_new' => 'Dodaj użytkownika',
+ 'users_search' => 'Wyszukaj użytkownika',
+ 'users_role' => 'Role użytkownika',
+ 'users_external_auth_id' => 'Zewnętrzne ID autentykacji',
+ 'users_password_warning' => 'Wypełnij poniżej tylko jeśli chcesz zmienić swoje hasło:',
+ 'users_system_public' => 'Ten użytkownik reprezentuje każdego gościa odwiedzającego tę aplikację. Nie można się na niego zalogować, lecz jest przyznawany automatycznie.',
+ 'users_delete' => 'Usuń użytkownika',
+ 'users_delete_named' => 'Usuń :userName',
+ 'users_delete_warning' => 'To usunie użytkownika \':userName\' z systemu.',
+ 'users_delete_confirm' => 'Czy na pewno chcesz usunąć tego użytkownika?',
+ 'users_delete_success' => 'Użytkownik usunięty pomyślnie',
+ 'users_edit' => 'Edytuj użytkownika',
+ 'users_edit_profile' => 'Edytuj profil',
+ 'users_edit_success' => 'Użytkownik zaktualizowany pomyśłnie',
+ 'users_avatar' => 'Avatar użytkownika',
+ 'users_avatar_desc' => 'Ten obrazek powinien mieć 25px x 256px.',
+ 'users_preferred_language' => 'Preferowany język',
+ 'users_social_accounts' => 'Konta społecznościowe',
+ 'users_social_accounts_info' => 'Tutaj możesz połączyć kilka kont społecznościowych w celu łatwiejszego i szybszego logowania.',
+ 'users_social_connect' => 'Podłącz konto',
+ 'users_social_disconnect' => 'Odłącz konto',
+ 'users_social_connected' => ':socialAccount zostało dodane do Twojego profilu.',
+ 'users_social_disconnected' => ':socialAccount zostało odłączone od Twojego profilu.',
+];
diff --git a/resources/lang/pl/validation.php b/resources/lang/pl/validation.php
new file mode 100644
index 000000000..6a7c13e80
--- /dev/null
+++ b/resources/lang/pl/validation.php
@@ -0,0 +1,108 @@
+ ':attribute musi zostać zaakceptowany.',
+ 'active_url' => ':attribute nie jest prawidłowym adresem URL.',
+ 'after' => ':attribute musi być datą następującą po :date.',
+ 'alpha' => ':attribute może zawierać wyłącznie litery.',
+ 'alpha_dash' => ':attribute może zawierać wyłącznie litery, cyfry i myślniki.',
+ 'alpha_num' => ':attribute może zawierać wyłącznie litery i cyfry.',
+ 'array' => ':attribute musi być tablicą.',
+ 'before' => ':attribute musi być datą poprzedzającą :date.',
+ 'between' => [
+ 'numeric' => ':attribute musi zawierać się w przedziale od :min do :max.',
+ 'file' => 'Waga :attribute musi zawierać się pomiędzy :min i :max kilobajtów.',
+ 'string' => 'Długość :attribute musi zawierać się pomiędzy :min i :max.',
+ 'array' => ':attribute musi mieć od :min do :max elementów.',
+ ],
+ 'boolean' => ':attribute musi być wartością prawda/fałsz.',
+ 'confirmed' => ':attribute i potwierdzenie muszą być zgodne.',
+ 'date' => ':attribute nie jest prawidłową datą.',
+ 'date_format' => ':attribute musi mieć format :format.',
+ 'different' => ':attribute i :other muszą się różnić.',
+ 'digits' => ':attribute musi mieć :digits cyfr.',
+ 'digits_between' => ':attribute musi mieć od :min do :max cyfr.',
+ 'email' => ':attribute musi być prawidłowym adresem e-mail.',
+ 'filled' => ':attribute jest wymagany.',
+ 'exists' => 'Wybrana wartość :attribute jest nieprawidłowa.',
+ 'image' => ':attribute musi być obrazkiem.',
+ 'in' => 'Wybrana wartość :attribute jest nieprawidłowa.',
+ 'integer' => ':attribute musi być liczbą całkowitą.',
+ 'ip' => ':attribute musi być prawidłowym adresem IP.',
+ 'max' => [
+ 'numeric' => 'Wartość :attribute nie może być większa niż :max.',
+ 'file' => 'Wielkość :attribute nie może być większa niż :max kilobajtów.',
+ 'string' => 'Długość :attribute nie może być większa niż :max znaków.',
+ 'array' => 'Rozmiar :attribute nie może być większy niż :max elementów.',
+ ],
+ 'mimes' => ':attribute musi być plikiem typu: :values.',
+ 'min' => [
+ 'numeric' => 'Wartość :attribute nie może być mniejsza od :min.',
+ 'file' => 'Wielkość :attribute nie może być mniejsza niż :min kilobajtów.',
+ 'string' => 'Długość :attribute nie może być mniejsza niż :min znaków.',
+ 'array' => 'Rozmiar :attribute musi posiadać co najmniej :min elementy.',
+ ],
+ 'not_in' => 'Wartość :attribute jest nieprawidłowa.',
+ 'numeric' => ':attribute musi być liczbą.',
+ 'regex' => 'Format :attribute jest nieprawidłowy.',
+ 'required' => 'Pole :attribute jest wymagane.',
+ 'required_if' => 'Pole :attribute jest wymagane jeśli :other ma wartość :value.',
+ 'required_with' => 'Pole :attribute jest wymagane jeśli :values zostało wprowadzone.',
+ 'required_with_all' => 'Pole :attribute jest wymagane jeśli :values są obecne.',
+ 'required_without' => 'Pole :attribute jest wymagane jeśli :values nie zostało wprowadzone.',
+ 'required_without_all' => 'Pole :attribute jest wymagane jeśli żadna z wartości :values nie została podana.',
+ 'same' => 'Pole :attribute i :other muszą być takie same.',
+ 'size' => [
+ 'numeric' => ':attribute musi mieć długość :size.',
+ 'file' => ':attribute musi mieć :size kilobajtów.',
+ 'string' => ':attribute mmusi mieć długość :size znaków.',
+ 'array' => ':attribute musi posiadać :size elementów.',
+ ],
+ 'string' => ':attribute musi być ciągiem znaków.',
+ 'timezone' => ':attribute musi być prawidłową strefą czasową.',
+ 'unique' => ':attribute zostało już zajęte.',
+ 'url' => 'Format :attribute jest nieprawidłowy.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify custom validation messages for attributes using the
+ | convention "attribute.rule" to name the lines. This makes it quick to
+ | specify a specific custom language line for a given attribute rule.
+ |
+ */
+
+ 'custom' => [
+ 'password-confirm' => [
+ 'required_with' => 'Potwierdzenie hasła jest wymagane.',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Attributes
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used to swap attribute place-holders
+ | with something more reader friendly such as E-Mail Address instead
+ | of "email". This simply helps us make messages a little cleaner.
+ |
+ */
+
+ 'attributes' => [],
+
+];
diff --git a/resources/lang/pt_BR/entities.php b/resources/lang/pt_BR/entities.php
index 5a965fe62..e6b900fdd 100644
--- a/resources/lang/pt_BR/entities.php
+++ b/resources/lang/pt_BR/entities.php
@@ -214,4 +214,26 @@ return [
'profile_not_created_pages' => ':userName não criou páginas',
'profile_not_created_chapters' => ':userName não criou capítulos',
'profile_not_created_books' => ':userName não criou livros',
+
+ /**
+ * Comments
+ */
+ 'comentário' => 'Comentário',
+ 'comentários' => 'Comentários',
+ 'comment_placeholder' => 'Digite seus comentários aqui, markdown suportado ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments Comentários',
+ 'one_comment' => '1 comentário',
+ 'comments_loading' => 'Carregando ....',
+ 'comment_save' => 'Salvar comentário',
+ 'comment_reply' => 'Responder',
+ 'comment_edit' => 'Editar',
+ 'comment_delete' => 'Excluir',
+ 'comment_cancel' => 'Cancelar',
+ 'comment_created' => 'Comentário adicionado',
+ 'comment_updated' => 'Comentário atualizado',
+ 'comment_deleted' => 'Comentário eliminado',
+ 'comment_updated_text' => 'Atualizado :updatedDiff atrás por',
+ 'comment_delete_confirm' => 'Isso removerá o conteúdo do comentário. Tem certeza de que deseja excluir esse comentário?',
+ 'comment_create' => 'Criada'
];
\ No newline at end of file
diff --git a/resources/lang/pt_BR/errors.php b/resources/lang/pt_BR/errors.php
index 91b85e3ef..16fc78ff5 100644
--- a/resources/lang/pt_BR/errors.php
+++ b/resources/lang/pt_BR/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Um erro ocorreu',
'app_down' => ':appName está fora do ar no momento',
'back_soon' => 'Voltaremos em seguida.',
+
+ // comments
+ 'comment_list' => 'Ocorreu um erro ao buscar os comentários.',
+ 'cannot_add_comment_to_draft' => 'Você não pode adicionar comentários a um rascunho.',
+ 'comment_add' => 'Ocorreu um erro ao adicionar o comentário.',
+ 'comment_delete' => 'Ocorreu um erro ao excluir o comentário.',
+ 'empty_comment' => 'Não é possível adicionar um comentário vazio.',
];
\ No newline at end of file
diff --git a/resources/lang/sk/entities.php b/resources/lang/sk/entities.php
index e70864753..7c8f34368 100644
--- a/resources/lang/sk/entities.php
+++ b/resources/lang/sk/entities.php
@@ -223,4 +223,26 @@ return [
'profile_not_created_pages' => ':userName nevytvoril žiadne stránky',
'profile_not_created_chapters' => ':userName nevytvoril žiadne kapitoly',
'profile_not_created_books' => ':userName nevytvoril žiadne knihy',
+
+ /**
+ * Comments
+ */
+ 'comment' => 'Komentár',
+ 'comments' => 'Komentáre',
+ 'comment_placeholder' => 'Tu zadajte svoje pripomienky, podporované označenie ...',
+ 'no_comments' => 'No Comments',
+ 'x_comments' => ':numComments komentárov',
+ 'one_comment' => '1 komentár',
+ 'comments_loading' => 'Loading ..',
+ 'comment_save' => 'Uložiť komentár',
+ 'comment_reply' => 'Odpovedať',
+ 'comment_edit' => 'Upraviť',
+ 'comment_delete' => 'Odstrániť',
+ 'comment_cancel' => 'Zrušiť',
+ 'comment_created' => 'Pridaný komentár',
+ 'comment_updated' => 'Komentár aktualizovaný',
+ 'comment_deleted' => 'Komentár bol odstránený',
+ 'comment_updated_text' => 'Aktualizované pred :updateDiff',
+ 'comment_delete_confirm' => 'Tým sa odstráni obsah komentára. Naozaj chcete odstrániť tento komentár?',
+ 'comment_create' => 'Vytvorené'
];
diff --git a/resources/lang/sk/errors.php b/resources/lang/sk/errors.php
index e3420852a..d4c7b7a3a 100644
--- a/resources/lang/sk/errors.php
+++ b/resources/lang/sk/errors.php
@@ -67,4 +67,11 @@ return [
'error_occurred' => 'Nastala chyba',
'app_down' => ':appName je momentálne nedostupná',
'back_soon' => 'Čoskoro bude opäť dostupná.',
+
+ // comments
+ 'comment_list' => 'Pri načítaní komentárov sa vyskytla chyba',
+ 'cannot_add_comment_to_draft' => 'Do konceptu nemôžete pridávať komentáre.',
+ 'comment_add' => 'Počas pridávania komentára sa vyskytla chyba',
+ 'comment_delete' => 'Pri odstraňovaní komentára došlo k chybe',
+ 'empty_comment' => 'Nelze pridať prázdny komentár.',
];
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index 95a9d72b0..1c972e4fb 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -36,7 +36,7 @@
{{$book->name}}
- @if(isset($book->searchSnippet))
-