From c3f7b39a0f3daa64a2d838190ae8efaedea4633c Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Fri, 7 Jan 2022 13:04:49 +0000
Subject: [PATCH 1/8] Addressed phpstan cases

---
 app/Entities/Tools/BookSortMap.php  | 2 +-
 app/Http/Controllers/Controller.php | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/Entities/Tools/BookSortMap.php b/app/Entities/Tools/BookSortMap.php
index 43b25817e..ff1ec767f 100644
--- a/app/Entities/Tools/BookSortMap.php
+++ b/app/Entities/Tools/BookSortMap.php
@@ -24,7 +24,7 @@ class BookSortMap
 
     public static function fromJson(string $json): self
     {
-        $map = new static();
+        $map = new BookSortMap();
         $mapData = json_decode($json);
 
         foreach ($mapData as $mapDataItem) {
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
index f836f18ed..2c4c2df1e 100644
--- a/app/Http/Controllers/Controller.php
+++ b/app/Http/Controllers/Controller.php
@@ -48,6 +48,8 @@ abstract class Controller extends BaseController
     /**
      * On a permission error redirect to home and display.
      * the error as a notification.
+     *
+     * @return never
      */
     protected function showPermissionError()
     {

From 2d0abc41644896f4f0405103679a8832df9425b8 Mon Sep 17 00:00:00 2001
From: Aitor Matxinea <46600792+AitorMatxi@users.noreply.github.com>
Date: Mon, 10 Jan 2022 11:45:48 +0100
Subject: [PATCH 2/8] Update auth.php

Fix misspelled word "As" to "Has".
---
 resources/lang/es/auth.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/lang/es/auth.php b/resources/lang/es/auth.php
index f35432138..4980229ec 100644
--- a/resources/lang/es/auth.php
+++ b/resources/lang/es/auth.php
@@ -64,7 +64,7 @@ return [
     'email_not_confirmed_resend_button' => 'Reenviar Correo Electrónico de confirmación',
 
     // User Invite
-    'user_invite_email_subject' => 'As sido invitado a unirte a :appName!',
+    'user_invite_email_subject' => 'Has sido invitado a unirte a :appName!',
     'user_invite_email_greeting' => 'Se ha creado una cuenta para usted en :appName.',
     'user_invite_email_text' => 'Clica en el botón a continuación para ajustar una contraseña y poder acceder:',
     'user_invite_email_action' => 'Ajustar la Contraseña de la Cuenta',

From 8d91f4369b418aa922e0246fdc57e4755936077d Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 17:04:01 +0000
Subject: [PATCH 3/8] Improved custom homepage check on item deletion

Custom homepage usage will now be checked before any actioning
of deletion rather than potentially causing an exception acting
during the deletion.

Previously a deletion could still be created, within the recycle bin,
for the parent which may lead to the page being deleted anyway.

For #3150
---
 app/Entities/Models/Entity.php  |  1 +
 app/Entities/Tools/TrashCan.php | 46 ++++++++++++++++++++++++++++-----
 tests/HomepageTest.php          | 18 +++++++++++++
 3 files changed, 58 insertions(+), 7 deletions(-)

diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php
index b55334295..7ad78f1d1 100644
--- a/app/Entities/Models/Entity.php
+++ b/app/Entities/Models/Entity.php
@@ -36,6 +36,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
  * @property string     $slug
  * @property Carbon     $created_at
  * @property Carbon     $updated_at
+ * @property Carbon     $deleted_at
  * @property int        $created_by
  * @property int        $updated_by
  * @property bool       $restricted
diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php
index ab62165af..015610e71 100644
--- a/app/Entities/Tools/TrashCan.php
+++ b/app/Entities/Tools/TrashCan.php
@@ -22,9 +22,11 @@ class TrashCan
 {
     /**
      * Send a shelf to the recycle bin.
+     * @throws NotifyException
      */
     public function softDestroyShelf(Bookshelf $shelf)
     {
+        $this->ensureDeletable($shelf);
         Deletion::createForEntity($shelf);
         $shelf->delete();
     }
@@ -36,6 +38,7 @@ class TrashCan
      */
     public function softDestroyBook(Book $book)
     {
+        $this->ensureDeletable($book);
         Deletion::createForEntity($book);
 
         foreach ($book->pages as $page) {
@@ -57,6 +60,7 @@ class TrashCan
     public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
     {
         if ($recordDelete) {
+            $this->ensureDeletable($chapter);
             Deletion::createForEntity($chapter);
         }
 
@@ -77,19 +81,47 @@ class TrashCan
     public function softDestroyPage(Page $page, bool $recordDelete = true)
     {
         if ($recordDelete) {
+            $this->ensureDeletable($page);
             Deletion::createForEntity($page);
         }
 
-        // Check if set as custom homepage & remove setting if not used or throw error if active
-        $customHome = setting('app-homepage', '0:');
-        if (intval($page->id) === intval(explode(':', $customHome)[0])) {
-            if (setting('app-homepage-type') === 'page') {
-                throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
+        $page->delete();
+    }
+
+    /**
+     * Ensure the given entity is deletable.
+     * Is not for permissions, but logical conditions within the application.
+     * Will throw if not deletable.
+     *
+     * @throws NotifyException
+     */
+    protected function ensureDeletable(Entity $entity): void
+    {
+        $customHomeId = intval(explode(':', setting('app-homepage', '0:'))[0]);
+        $customHomeActive = setting('app-homepage-type') === 'page';
+        $removeCustomHome = false;
+
+        // Check custom homepage usage for pages
+        if ($entity instanceof Page && $entity->id === $customHomeId) {
+            if ($customHomeActive) {
+                throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
             }
-            setting()->remove('app-homepage');
+            $removeCustomHome = true;
         }
 
-        $page->delete();
+        // Check custom homepage usage within chapters or books
+        if ($entity instanceof Chapter || $entity instanceof Book) {
+            if ($entity->pages()->where('id', '=', $customHomeId)->exists()) {
+                if ($customHomeActive) {
+                    throw new NotifyException(trans('errors.page_custom_home_deletion'), $entity->getUrl());
+                }
+                $removeCustomHome = true;
+            }
+        }
+
+        if ($removeCustomHome) {
+            setting()->remove('app-homepage');
+        }
     }
 
     /**
diff --git a/tests/HomepageTest.php b/tests/HomepageTest.php
index dc1b22779..900650a70 100644
--- a/tests/HomepageTest.php
+++ b/tests/HomepageTest.php
@@ -79,6 +79,24 @@ class HomepageTest extends TestCase
         $pageDeleteReq->assertSessionMissing('error');
     }
 
+    public function test_custom_homepage_cannot_be_deleted_from_parent_deletion()
+    {
+        /** @var Page $page */
+        $page = Page::query()->first();
+        $this->setSettings([
+            'app-homepage'      => $page->id,
+            'app-homepage-type' => 'page',
+        ]);
+
+        $this->asEditor()->delete($page->book->getUrl());
+        $this->assertSessionError('Cannot delete a page while it is set as a homepage');
+        $this->assertDatabaseMissing('deletions', ['deletable_id' => $page->book->id]);
+
+        $page->refresh();
+        $this->assertNull($page->deleted_at);
+        $this->assertNull($page->book->deleted_at);
+    }
+
     public function test_custom_homepage_renders_includes()
     {
         $this->asEditor();

From 4239d4c54d3da132ba1ff072437f1ee0cca2dc33 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 17:46:17 +0000
Subject: [PATCH 4/8] Fixed error on webhooks for recycle bin operations

Updated the getUrl method on deletions to not require any passed
params to align with usage in webhooks.
Probably better to have a proper interface but would require a wider
change.

Fixes #3154
---
 app/Entities/Models/Deletion.php     |  2 +-
 app/Entities/Models/PageRevision.php | 13 ++-----------
 2 files changed, 3 insertions(+), 12 deletions(-)

diff --git a/app/Entities/Models/Deletion.php b/app/Entities/Models/Deletion.php
index 97abb87ff..181c9c580 100644
--- a/app/Entities/Models/Deletion.php
+++ b/app/Entities/Models/Deletion.php
@@ -59,7 +59,7 @@ class Deletion extends Model implements Loggable
     /**
      * Get a URL for this specific deletion.
      */
-    public function getUrl($path): string
+    public function getUrl(string $path = 'restore'): string
     {
         return url("/settings/recycle-bin/{$this->id}/" . ltrim($path, '/'));
     }
diff --git a/app/Entities/Models/PageRevision.php b/app/Entities/Models/PageRevision.php
index 2bfa169f4..4daf50536 100644
--- a/app/Entities/Models/PageRevision.php
+++ b/app/Entities/Models/PageRevision.php
@@ -46,19 +46,10 @@ class PageRevision extends Model
 
     /**
      * Get the url for this revision.
-     *
-     * @param null|string $path
-     *
-     * @return string
      */
-    public function getUrl($path = null)
+    public function getUrl(string $path = ''): string
     {
-        $url = $this->page->getUrl() . '/revisions/' . $this->id;
-        if ($path) {
-            return $url . '/' . trim($path, '/');
-        }
-
-        return $url;
+        return $this->page->getUrl('/revisions/' . $this->id . '/' . ltrim($path, '/'));
     }
 
     /**

From 941217d9fb223e9c5d22d57b2214d48982594823 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 18:12:11 +0000
Subject: [PATCH 5/8] Improved loading for images with failed thumbnails

- A placeholder is now shown in the gallery.
- The page editors will use the original image url if the display
  thumbnail is missing.

For #3142
---
 public/loading_error.png                         | Bin 0 -> 2096 bytes
 resources/js/components/image-manager.js         |   4 ++++
 resources/js/components/markdown-editor.js       |   3 ++-
 resources/js/components/wysiwyg-editor.js        |   6 ++++--
 .../pages/parts/image-manager-form.blade.php     |   2 +-
 5 files changed, 11 insertions(+), 4 deletions(-)
 create mode 100644 public/loading_error.png

diff --git a/public/loading_error.png b/public/loading_error.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f588fbe1e8fb4cd09f5bf1138897a7ae9a1254c
GIT binary patch
literal 2096
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
SEB_>a96+2nQFLY_iv0(S!8H~D

literal 0
HcmV?d00001

diff --git a/resources/js/components/image-manager.js b/resources/js/components/image-manager.js
index c974ab1b0..6d05d3388 100644
--- a/resources/js/components/image-manager.js
+++ b/resources/js/components/image-manager.js
@@ -74,6 +74,10 @@ class ImageManager {
 
         this.listContainer.addEventListener('event-emit-select-image', this.onImageSelectEvent.bind(this));
 
+        this.listContainer.addEventListener('error', event => {
+            event.target.src = baseUrl('loading_error.png');
+        }, true);
+
         onSelect(this.selectButton, () => {
             if (this.callback) {
                 this.callback(this.lastSelected);
diff --git a/resources/js/components/markdown-editor.js b/resources/js/components/markdown-editor.js
index a90f74e27..def3db5af 100644
--- a/resources/js/components/markdown-editor.js
+++ b/resources/js/components/markdown-editor.js
@@ -395,8 +395,9 @@ class MarkdownEditor {
     actionInsertImage() {
         const cursorPos = this.cm.getCursor('from');
         window.ImageManager.show(image => {
+            const imageUrl = image.thumbs.display || image.url;
             let selectedText = this.cm.getSelection();
-            let newText = "[![" + (selectedText || image.name) + "](" + image.thumbs.display + ")](" + image.url + ")";
+            let newText = "[![" + (selectedText || image.name) + "](" + imageUrl + ")](" + image.url + ")";
             this.cm.focus();
             this.cm.replaceSelection(newText);
             this.cm.setCursor(cursorPos.line, cursorPos.ch + newText.length);
diff --git a/resources/js/components/wysiwyg-editor.js b/resources/js/components/wysiwyg-editor.js
index bde73f4bf..7a2b6ceba 100644
--- a/resources/js/components/wysiwyg-editor.js
+++ b/resources/js/components/wysiwyg-editor.js
@@ -563,8 +563,9 @@ class WysiwygEditor {
                         }
 
                         // Replace the actively selected content with the linked image
+                        const imageUrl = image.thumbs.display || image.url;
                         let html = `<a href="${image.url}" target="_blank">`;
-                        html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
+                        html += `<img src="${imageUrl}" alt="${image.name}">`;
                         html += '</a>';
                         win.tinyMCE.activeEditor.execCommand('mceInsertContent', false, html);
                     }, 'gallery');
@@ -723,8 +724,9 @@ class WysiwygEditor {
                     tooltip: 'Insert an image',
                     onclick: function () {
                         window.ImageManager.show(function (image) {
+                            const imageUrl = image.thumbs.display || image.url;
                             let html = `<a href="${image.url}" target="_blank">`;
-                            html += `<img src="${image.thumbs.display}" alt="${image.name}">`;
+                            html += `<img src="${imageUrl}" alt="${image.name}">`;
                             html += '</a>';
                             editor.execCommand('mceInsertContent', false, html);
                         }, 'gallery');
diff --git a/resources/views/pages/parts/image-manager-form.blade.php b/resources/views/pages/parts/image-manager-form.blade.php
index 6d6255226..81041fcac 100644
--- a/resources/views/pages/parts/image-manager-form.blade.php
+++ b/resources/views/pages/parts/image-manager-form.blade.php
@@ -8,7 +8,7 @@
 
         <div class="image-manager-viewer">
             <a href="{{ $image->url }}" target="_blank" rel="noopener" class="block">
-                <img src="{{ $image->thumbs['display'] }}"
+                <img src="{{ $image->thumbs['display'] ?? $image->url }}"
                      alt="{{ $image->name }}"
                      class="anim fadeIn"
                      title="{{ $image->name }}">

From d3eaaf64576658fa342bd319a2233b215c8f2631 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 18:17:28 +0000
Subject: [PATCH 6/8] New Crowdin updates (#3148)

* New translations common.php (Czech)

* New translations common.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations errors.php (Japanese)

* New translations entities.php (Japanese)

* New translations common.php (Japanese)

* New translations settings.php (Japanese)

* New translations entities.php (Japanese)

* New translations settings.php (Japanese)

* New translations auth.php (Japanese)

* New translations common.php (Portuguese, Brazilian)

* New translations activities.php (Portuguese, Brazilian)

* New translations activities.php (Portuguese, Brazilian)
---
 resources/lang/cs/common.php        |  2 +-
 resources/lang/ja/auth.php          |  6 ++--
 resources/lang/ja/common.php        |  8 ++---
 resources/lang/ja/entities.php      | 36 ++++++++++-----------
 resources/lang/ja/errors.php        |  2 +-
 resources/lang/ja/settings.php      | 32 +++++++++---------
 resources/lang/pt_BR/activities.php | 50 ++++++++++++++---------------
 resources/lang/pt_BR/common.php     | 18 +++++------
 resources/lang/zh_CN/common.php     |  2 +-
 resources/lang/zh_CN/settings.php   |  2 +-
 10 files changed, 79 insertions(+), 79 deletions(-)

diff --git a/resources/lang/cs/common.php b/resources/lang/cs/common.php
index 9acd965b3..13865f40e 100644
--- a/resources/lang/cs/common.php
+++ b/resources/lang/cs/common.php
@@ -74,7 +74,7 @@ return [
     'status' => 'Stav',
     'status_active' => 'Aktivní',
     'status_inactive' => 'Neaktivní',
-    'never' => 'Never',
+    'never' => 'Nikdy',
 
     // Header
     'header_menu_expand' => 'Rozbalit menu v záhlaví',
diff --git a/resources/lang/ja/auth.php b/resources/lang/ja/auth.php
index bbfb675c0..9c8d36669 100644
--- a/resources/lang/ja/auth.php
+++ b/resources/lang/ja/auth.php
@@ -21,7 +21,7 @@ return [
     'email' => 'メールアドレス',
     'password' => 'パスワード',
     'password_confirm' => 'パスワード (確認)',
-    'password_hint' => 'Must be at least 8 characters',
+    'password_hint' => '8文字以上で設定する必要があります',
     'forgot_password' => 'パスワードをお忘れですか?',
     'remember_me' => 'ログイン情報を保存する',
     'ldap_email_hint' => 'このアカウントで使用するEメールアドレスを入力してください。',
@@ -54,7 +54,7 @@ return [
     'email_confirm_text' => '以下のボタンを押し、メールアドレスを確認してください:',
     'email_confirm_action' => 'メールアドレスを確認',
     'email_confirm_send_error' => 'Eメールの確認が必要でしたが、システム上でEメールの送信ができませんでした。管理者に連絡し、Eメールが正しく設定されていることを確認してください。',
-    'email_confirm_success' => 'Your email has been confirmed! You should now be able to login using this email address.',
+    'email_confirm_success' => 'メールアドレスが確認されました!このメールアドレスでログインできるようになりました。',
     'email_confirm_resent' => '確認メールを再送信しました。受信トレイを確認してください。',
 
     'email_not_confirmed' => 'Eメールアドレスが確認できていません',
@@ -71,7 +71,7 @@ return [
     'user_invite_page_welcome' => ':appNameへようこそ!',
     'user_invite_page_text' => 'アカウントの設定を完了してアクセスするには、今後の訪問時に:appNameにログインするためのパスワードを設定する必要があります。',
     'user_invite_page_confirm_button' => 'パスワードを確定',
-    'user_invite_success_login' => 'Password set, you should now be able to login using your set password to access :appName!',
+    'user_invite_success_login' => 'パスワードが設定されました。設定したパスワードで:appNameにログインできるようになりました!',
 
     // Multi-factor Authentication
     'mfa_setup' => '多要素認証を設定',
diff --git a/resources/lang/ja/common.php b/resources/lang/ja/common.php
index 2c1d37c08..bf605525b 100644
--- a/resources/lang/ja/common.php
+++ b/resources/lang/ja/common.php
@@ -85,10 +85,10 @@ return [
     'light_mode' => 'ライトモード',
 
     // Layout tabs
-    'tab_info' => 'Info',
-    'tab_info_label' => 'Tab: Show Secondary Information',
-    'tab_content' => 'Content',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_info' => '情報',
+    'tab_info_label' => 'タブ: サブコンテンツを表示',
+    'tab_content' => '内容',
+    'tab_content_label' => 'タブ: メインコンテンツを表示',
 
     // Email Content
     'email_action_help' => '":actionText" をクリックできない場合、以下のURLをコピーしブラウザで開いてください:',
diff --git a/resources/lang/ja/entities.php b/resources/lang/ja/entities.php
index e6d1f426b..e5ba8c673 100644
--- a/resources/lang/ja/entities.php
+++ b/resources/lang/ja/entities.php
@@ -22,7 +22,7 @@ return [
     'meta_created_name' => '作成: :timeLength (:user)',
     'meta_updated' => '更新: :timeLength',
     'meta_updated_name' => '更新: :timeLength (:user)',
-    'meta_owned_name' => 'Owned by :user',
+    'meta_owned_name' => '所有者: :user',
     'entity_select' => 'エンティティ選択',
     'images' => '画像',
     'my_recent_drafts' => '最近の下書き',
@@ -36,14 +36,14 @@ return [
     'export_html' => 'Webページ',
     'export_pdf' => 'PDF',
     'export_text' => 'テキストファイル',
-    'export_md' => 'Markdown File',
+    'export_md' => 'Markdown',
 
     // Permissions and restrictions
     'permissions' => '権限',
     'permissions_intro' => 'この設定は各ユーザの役割よりも優先して適用されます。',
     'permissions_enable' => 'カスタム権限設定を有効にする',
     'permissions_save' => '権限を保存',
-    'permissions_owner' => 'Owner',
+    'permissions_owner' => '所有者',
 
     // Search
     'search_results' => '検索結果',
@@ -143,8 +143,8 @@ return [
     'books_sort_chapters_last' => 'チャプターを後に',
     'books_sort_show_other' => '他のブックを表示',
     'books_sort_save' => '並び順を保存',
-    'books_copy' => 'Copy Book',
-    'books_copy_success' => 'Book successfully copied',
+    'books_copy' => 'ブックをコピー',
+    'books_copy_success' => 'ブックが正常にコピーされました',
 
     // Chapters
     'chapter' => 'チャプター',
@@ -163,8 +163,8 @@ return [
     'chapters_move' => 'チャプターを移動',
     'chapters_move_named' => 'チャプター「:chapterName」を移動',
     'chapter_move_success' => 'チャプターを「:bookName」に移動しました',
-    'chapters_copy' => 'Copy Chapter',
-    'chapters_copy_success' => 'Chapter successfully copied',
+    'chapters_copy' => 'チャプターをコピー',
+    'chapters_copy_success' => 'チャプターが正常にコピーされました',
     'chapters_permissions' => 'チャプター権限',
     'chapters_empty' => 'まだチャプター内にページはありません。',
     'chapters_permissions_active' => 'チャプターの権限は有効です',
@@ -215,16 +215,16 @@ return [
     'pages_copy_success' => 'ページが正常にコピーされました',
     'pages_permissions' => 'ページの権限設定',
     'pages_permissions_success' => 'ページの権限を更新しました',
-    'pages_revision' => 'Revision',
+    'pages_revision' => '編集履歴',
     'pages_revisions' => '編集履歴',
     'pages_revisions_named' => ':pageName のリビジョン',
     'pages_revision_named' => ':pageName のリビジョン',
-    'pages_revision_restored_from' => 'Restored from #:id; :summary',
+    'pages_revision_restored_from' => '#:id :summary から復元',
     'pages_revisions_created_by' => '作成者',
     'pages_revisions_date' => '日付',
     'pages_revisions_number' => '#',
-    'pages_revisions_numbered' => 'Revision #:id',
-    'pages_revisions_numbered_changes' => 'Revision #:id Changes',
+    'pages_revisions_numbered' => 'リビジョン #:id',
+    'pages_revisions_numbered_changes' => 'リビジョン #:id の変更',
     'pages_revisions_changelog' => '説明',
     'pages_revisions_changes' => '変更点',
     'pages_revisions_current' => '現在のバージョン',
@@ -333,15 +333,15 @@ return [
 
     // Revision
     'revision_delete_confirm' => 'このリビジョンを削除しますか?',
-    'revision_restore_confirm' => 'Are you sure you want to restore this revision? The current page contents will be replaced.',
+    'revision_restore_confirm' => 'このリビジョンを復元してよろしいですか?現在のページの内容が置換されます。',
     'revision_delete_success' => 'リビジョンを削除しました',
     'revision_cannot_delete_latest' => '最新のリビジョンを削除できません。',
 
     // Copy view
-    'copy_consider' => 'Please consider the below when copying content.',
-    'copy_consider_permissions' => 'Custom permission settings will not be copied.',
-    'copy_consider_owner' => 'You will become the owner of all copied content.',
-    'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
-    'copy_consider_attachments' => 'Page attachments will not be copied.',
-    'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
+    'copy_consider' => 'コンテンツをコピーする場合は以下の点にご注意ください。',
+    'copy_consider_permissions' => 'カスタム権限設定はコピーされません。',
+    'copy_consider_owner' => 'あなたはコピーされた全てのコンテンツの所有者になります。',
+    'copy_consider_images' => 'ページの画像ファイルは複製されず、元の画像は最初にアップロードされたページとの関係を保持します。',
+    'copy_consider_attachments' => 'ページの添付ファイルはコピーされません。',
+    'copy_consider_access' => '場所、所有者または権限を変更すると、以前アクセスできなかったユーザーがこのコンテンツにアクセスできるようになる可能性があります。',
 ];
diff --git a/resources/lang/ja/errors.php b/resources/lang/ja/errors.php
index c30e3c50c..d63e9bf36 100644
--- a/resources/lang/ja/errors.php
+++ b/resources/lang/ja/errors.php
@@ -99,7 +99,7 @@ return [
     'api_no_authorization_found' => 'リクエストに認証トークンが見つかりません',
     'api_bad_authorization_format' => 'リクエストに認証トークンが見つかりましたが、形式が正しくないようです',
     'api_user_token_not_found' => '提供された認証トークンに一致するAPIトークンが見つかりませんでした',
-    'api_incorrect_token_secret' => 'The secret provided for the given used API token is incorrect',
+    'api_incorrect_token_secret' => '利用されたAPIトークンに対して提供されたシークレットが正しくありません',
     'api_user_no_api_permission' => '使用されているAPIトークンの所有者には、API呼び出しを行う権限がありません',
     'api_user_token_expired' => '認証トークンが期限切れです。',
 
diff --git a/resources/lang/ja/settings.php b/resources/lang/ja/settings.php
index bee756813..0fb4b113e 100644
--- a/resources/lang/ja/settings.php
+++ b/resources/lang/ja/settings.php
@@ -234,27 +234,27 @@ return [
     'user_api_token_delete_success' => 'APIトークンが正常に削除されました',
 
     // Webhooks
-    'webhooks' => 'Webhooks',
-    'webhooks_create' => 'Create New Webhook',
-    'webhooks_none_created' => 'No webhooks have yet been created.',
-    'webhooks_edit' => 'Edit Webhook',
-    'webhooks_save' => 'Save Webhook',
-    'webhooks_details' => 'Webhook Details',
-    'webhooks_details_desc' => 'Provide a user friendly name and a POST endpoint as a location for the webhook data to be sent to.',
-    'webhooks_events' => 'Webhook Events',
-    'webhooks_events_desc' => 'Select all the events that should trigger this webhook to be called.',
-    'webhooks_events_warning' => 'Keep in mind that these events will be triggered for all selected events, even if custom permissions are applied. Ensure that use of this webhook won\'t expose confidential content.',
-    'webhooks_events_all' => 'All system events',
-    'webhooks_name' => 'Webhook Name',
-    'webhooks_timeout' => 'Webhook Request Timeout (Seconds)',
-    'webhooks_endpoint' => 'Webhook Endpoint',
+    'webhooks' => 'Webhook',
+    'webhooks_create' => 'Webhookを作成',
+    'webhooks_none_created' => 'Webhookはまだ作成されていません。',
+    'webhooks_edit' => 'Webhookを編集',
+    'webhooks_save' => 'Webhookを保存',
+    'webhooks_details' => 'Webhookの詳細',
+    'webhooks_details_desc' => 'ユーザーフレンドリーな名前とWebhookデータの送信先にするPOSTエンドポイントを指定します。',
+    'webhooks_events' => 'Webhookのイベント',
+    'webhooks_events_desc' => 'このWebhookの呼び出しをトリガーするすべてのイベントを選択します。',
+    'webhooks_events_warning' => 'これらのイベントはカスタム権限が適用されている場合でも、選択したすべてのイベントに対してトリガーされることに注意してください。このWebhookの利用により機密コンテンツが公開されないことを確認してください。',
+    'webhooks_events_all' => '全てのシステムイベント',
+    'webhooks_name' => 'Webhook名',
+    'webhooks_timeout' => 'Webhookリクエストタイムアウト (秒)',
+    'webhooks_endpoint' => 'Webhookエンドポイント',
     'webhooks_active' => 'Webhook Active',
     'webhook_events_table_header' => 'Events',
     'webhooks_delete' => 'Delete Webhook',
     'webhooks_delete_warning' => 'This will fully delete this webhook, with the name \':webhookName\', from the system.',
     'webhooks_delete_confirm' => 'Are you sure you want to delete this webhook?',
-    'webhooks_format_example' => 'Webhook Format Example',
-    'webhooks_format_example_desc' => 'Webhook data is sent as a POST request to the configured endpoint as JSON following the format below. The "related_item" and "url" properties are optional and will depend on the type of event triggered.',
+    'webhooks_format_example' => 'Webhookのフォーマット例',
+    'webhooks_format_example_desc' => 'Webhookのデータは、設定されたエンドポイントにPOSTリクエストにより以下のフォーマットのJSONで送信されます。related_item と url プロパティはオプションであり、トリガーされるイベントの種類によって異なります。',
     'webhooks_status' => 'Webhook Status',
     'webhooks_last_called' => 'Last Called:',
     'webhooks_last_errored' => 'Last Errored:',
diff --git a/resources/lang/pt_BR/activities.php b/resources/lang/pt_BR/activities.php
index b24446622..f6fa8e415 100644
--- a/resources/lang/pt_BR/activities.php
+++ b/resources/lang/pt_BR/activities.php
@@ -7,57 +7,57 @@ return [
 
     // Pages
     'page_create'                 => 'criou a página',
-    'page_create_notification'    => 'Page successfully created',
+    'page_create_notification'    => 'Página criada com sucesso',
     'page_update'                 => 'atualizou a página',
-    'page_update_notification'    => 'Page successfully updated',
+    'page_update_notification'    => 'Página atualizada com sucesso',
     'page_delete'                 => 'excluiu a página',
-    'page_delete_notification'    => 'Page successfully deleted',
+    'page_delete_notification'    => 'Página excluída com sucesso',
     'page_restore'                => 'restaurou a página',
-    'page_restore_notification'   => 'Page successfully restored',
+    'page_restore_notification'   => 'Página restaurada com sucesso',
     'page_move'                   => 'moveu a página',
 
     // Chapters
     'chapter_create'              => 'criou o capítulo',
-    'chapter_create_notification' => 'Chapter successfully created',
+    'chapter_create_notification' => 'Capítulo criado com sucesso',
     'chapter_update'              => 'atualizou o capítulo',
-    'chapter_update_notification' => 'Chapter successfully updated',
+    'chapter_update_notification' => 'Capítulo atualizado com sucesso',
     'chapter_delete'              => 'excluiu o capítulo',
-    'chapter_delete_notification' => 'Chapter successfully deleted',
+    'chapter_delete_notification' => 'Capítulo excluída com sucesso',
     'chapter_move'                => 'moveu o capítulo',
 
     // Books
     'book_create'                 => 'criou o livro',
-    'book_create_notification'    => 'Book successfully created',
+    'book_create_notification'    => 'Livro criado com sucesso',
     'book_update'                 => 'atualizou o livro',
-    'book_update_notification'    => 'Book successfully updated',
+    'book_update_notification'    => 'Livro atualizado com sucesso',
     'book_delete'                 => 'excluiu o livro',
-    'book_delete_notification'    => 'Book successfully deleted',
+    'book_delete_notification'    => 'Livro excluído com sucesso',
     'book_sort'                   => 'ordenou o livro',
-    'book_sort_notification'      => 'Book successfully re-sorted',
+    'book_sort_notification'      => 'Livro reordenado com sucesso',
 
     // Bookshelves
-    'bookshelf_create'            => 'created bookshelf',
-    'bookshelf_create_notification'    => 'Bookshelf successfully created',
+    'bookshelf_create'            => 'prateleira criada',
+    'bookshelf_create_notification'    => 'Prateleira criada com sucesso',
     'bookshelf_update'                 => 'atualizou a prateleira',
-    'bookshelf_update_notification'    => 'Bookshelf successfully updated',
+    'bookshelf_update_notification'    => 'Prateleira atualizada com sucesso',
     'bookshelf_delete'                 => 'excluiu a prateleira',
-    'bookshelf_delete_notification'    => 'Bookshelf successfully deleted',
+    'bookshelf_delete_notification'    => 'Prateleira excluída com sucesso',
 
     // Favourites
-    'favourite_add_notification' => '":name" has been added to your favourites',
-    'favourite_remove_notification' => '":name" has been removed from your favourites',
+    'favourite_add_notification' => '":name" foi adicionada aos seus favoritos',
+    'favourite_remove_notification' => '":name" foi removida dos seus favoritos',
 
     // MFA
-    'mfa_setup_method_notification' => 'Multi-factor method successfully configured',
-    'mfa_remove_method_notification' => 'Multi-factor method successfully removed',
+    'mfa_setup_method_notification' => 'Método de multi-fatores configurado com sucesso',
+    'mfa_remove_method_notification' => 'Método de multi-fatores removido com sucesso',
 
     // Webhooks
-    'webhook_create' => 'created webhook',
-    'webhook_create_notification' => 'Webhook successfully created',
-    'webhook_update' => 'updated webhook',
-    'webhook_update_notification' => 'Webhook successfully updated',
-    'webhook_delete' => 'deleted webhook',
-    'webhook_delete_notification' => 'Webhook successfully deleted',
+    'webhook_create' => 'webhook criado',
+    'webhook_create_notification' => 'Webhook criado com sucesso',
+    'webhook_update' => 'webhook atualizado',
+    'webhook_update_notification' => 'Webhook atualizado com sucesso',
+    'webhook_delete' => 'webhook excluído',
+    'webhook_delete_notification' => 'Webhook excluido com sucesso',
 
     // Other
     'commented_on'                => 'comentou em',
diff --git a/resources/lang/pt_BR/common.php b/resources/lang/pt_BR/common.php
index 00d0ca844..fd2e4235f 100644
--- a/resources/lang/pt_BR/common.php
+++ b/resources/lang/pt_BR/common.php
@@ -39,14 +39,14 @@ return [
     'reset' => 'Redefinir',
     'remove' => 'Remover',
     'add' => 'Adicionar',
-    'configure' => 'Configure',
+    'configure' => 'Configurar',
     'fullscreen' => 'Tela cheia',
     'favourite' => 'Favoritos',
     'unfavourite' => 'Remover dos Favoritos',
     'next' => 'Seguinte',
     'previous' => 'Anterior',
-    'filter_active' => 'Active Filter:',
-    'filter_clear' => 'Clear Filter',
+    'filter_active' => 'Filtro Ativo:',
+    'filter_clear' => 'Limpar Filtro',
 
     // Sort Options
     'sort_options' => 'Opções de Ordenação',
@@ -72,12 +72,12 @@ return [
     'default' => 'Padrão',
     'breadcrumb' => 'Caminho',
     'status' => 'Status',
-    'status_active' => 'Active',
-    'status_inactive' => 'Inactive',
-    'never' => 'Never',
+    'status_active' => 'Ativo',
+    'status_inactive' => 'Inativo',
+    'never' => 'Nunca',
 
     // Header
-    'header_menu_expand' => 'Expand Header Menu',
+    'header_menu_expand' => 'Expandir Cabeçalho do Menu',
     'profile_menu' => 'Menu de Perfil',
     'view_profile' => 'Visualizar Perfil',
     'edit_profile' => 'Editar Perfil',
@@ -86,9 +86,9 @@ return [
 
     // Layout tabs
     'tab_info' => 'Informações',
-    'tab_info_label' => 'Tab: Show Secondary Information',
+    'tab_info_label' => 'Aba: Mostrar Informação Secundária',
     'tab_content' => 'Conteúdo',
-    'tab_content_label' => 'Tab: Show Primary Content',
+    'tab_content_label' => 'Aba: Mostrar Conteúdo Primário',
 
     // Email Content
     'email_action_help' => 'Se você estiver tendo problemas ao clicar o botão ":actionText", copie e cole a URL abaixo no seu navegador:',
diff --git a/resources/lang/zh_CN/common.php b/resources/lang/zh_CN/common.php
index 5edb74d25..412ecb062 100644
--- a/resources/lang/zh_CN/common.php
+++ b/resources/lang/zh_CN/common.php
@@ -74,7 +74,7 @@ return [
     'status' => '状态',
     'status_active' => '已激活',
     'status_inactive' => '未激活',
-    'never' => '永不',
+    'never' => '从未',
 
     // Header
     'header_menu_expand' => '展开标头菜单',
diff --git a/resources/lang/zh_CN/settings.php b/resources/lang/zh_CN/settings.php
index 85b7094b9..064b16650 100755
--- a/resources/lang/zh_CN/settings.php
+++ b/resources/lang/zh_CN/settings.php
@@ -236,7 +236,7 @@ return [
     // Webhooks
     'webhooks' => 'Webhooks',
     'webhooks_create' => '新建 Webhook',
-    'webhooks_none_created' => '不存在已创建的 webhooks',
+    'webhooks_none_created' => '尚未创建任何 webhook',
     'webhooks_edit' => '编辑 Webhook',
     'webhooks_save' => '保存 Webhook',
     'webhooks_details' => 'Webhook 详情',

From ade66dcf2f0a5df51b09e310928607e11f2942d4 Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 18:18:15 +0000
Subject: [PATCH 7/8] Applied latest styleci changes

---
 app/Entities/Tools/TrashCan.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/app/Entities/Tools/TrashCan.php b/app/Entities/Tools/TrashCan.php
index 015610e71..1e130c9e1 100644
--- a/app/Entities/Tools/TrashCan.php
+++ b/app/Entities/Tools/TrashCan.php
@@ -22,6 +22,7 @@ class TrashCan
 {
     /**
      * Send a shelf to the recycle bin.
+     *
      * @throws NotifyException
      */
     public function softDestroyShelf(Bookshelf $shelf)

From 2aace1670478b8f2c25cbd95bb4edba004e8ccbb Mon Sep 17 00:00:00 2001
From: Dan Brown <ssddanbrown@googlemail.com>
Date: Mon, 10 Jan 2022 18:22:43 +0000
Subject: [PATCH 8/8] Updated translator attribution before release v21.12.2

---
 .github/translators.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.github/translators.txt b/.github/translators.txt
index b7639ce85..0a4fbdc1b 100644
--- a/.github/translators.txt
+++ b/.github/translators.txt
@@ -210,3 +210,4 @@ Tomáš Batelka (Vofy) :: Czech
 Mundo Racional (ismael.mesquita) :: Portuguese, Brazilian
 Zarik (3apuk) :: Russian
 Ali Shaatani (a.shaatani) :: Arabic
+ChacMaster :: Portuguese, Brazilian