Просмотр исходного кода

Merge remote-tracking branch 'origin/support/apply-nextjs-2' into imprv/determine-color-scheme-2

Yuki Takei 3 лет назад
Родитель
Сommit
7ee0b542f1
100 измененных файлов с 2676 добавлено и 2172 удалено
  1. 1 1
      .github/workflows/reusable-app-prod.yml
  2. 22 1
      CHANGELOG.md
  3. 1 1
      lerna.json
  4. 1 1
      package.json
  5. 2 2
      packages/app/docker/README.md
  6. 7 7
      packages/app/package.json
  7. 280 0
      packages/app/public/static/locales/en_US/admin.json
  8. 1 279
      packages/app/public/static/locales/en_US/translation.json
  9. 1 0
      packages/app/public/static/locales/ja_JP/translation.json
  10. 1 0
      packages/app/public/static/locales/zh_CN/translation.json
  11. 2 2
      packages/app/src/components/Admin/App/AppSetting.jsx
  12. 3 3
      packages/app/src/components/Admin/App/AppSettingsPageContents.tsx
  13. 3 3
      packages/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx
  14. 2 2
      packages/app/src/components/Admin/AuditLogManagement.tsx
  15. 12 12
      packages/app/src/components/Admin/Common/AdminNavigation.jsx
  16. 14 14
      packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx
  17. 11 11
      packages/app/src/components/Admin/Notification/GlobalNotification.jsx
  18. 2 2
      packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx
  19. 8 8
      packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx
  20. 2 2
      packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx
  21. 8 8
      packages/app/src/components/Admin/Notification/NotificationSetting.jsx
  22. 1 1
      packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx
  23. 8 8
      packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx
  24. 9 9
      packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx
  25. 1 1
      packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx
  26. 19 19
      packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx
  27. 21 21
      packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx
  28. 47 47
      packages/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx
  29. 27 27
      packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx
  30. 48 48
      packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx
  31. 44 44
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  32. 4 4
      packages/app/src/components/Admin/Security/SecurityManagementContents.jsx
  33. 33 33
      packages/app/src/components/Admin/Security/SecuritySetting.jsx
  34. 5 5
      packages/app/src/components/Admin/Security/ShareLinkSetting.jsx
  35. 21 21
      packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx
  36. 1 1
      packages/app/src/components/Admin/UserManagement.jsx
  37. 1 1
      packages/app/src/components/Layout/AdminLayout.tsx
  38. 12 12
      packages/app/src/components/Layout/BasicLayout.tsx
  39. 3 16
      packages/app/src/components/Layout/RawLayout.tsx
  40. 1 1
      packages/app/src/components/LoginForm.jsx
  41. 1 1
      packages/app/src/components/Me/AssociateModal.tsx
  42. 1 1
      packages/app/src/components/Me/DisassociateModal.tsx
  43. 8 1
      packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  44. 9 9
      packages/app/src/components/PageComment/DeleteCommentModal.tsx
  45. 18 5
      packages/app/src/components/PageCreateModal.jsx
  46. 43 7
      packages/app/src/components/PageDeleteModal.tsx
  47. 39 17
      packages/app/src/components/PageDuplicateModal.tsx
  48. 2 2
      packages/app/src/components/PageEditor/Editor.tsx
  49. 55 35
      packages/app/src/components/PageRenameModal.tsx
  50. 39 8
      packages/app/src/components/PutbackPageModal.jsx
  51. 0 130
      packages/app/src/components/ShareLink/ShareLink.jsx
  52. 76 0
      packages/app/src/components/ShareLink/ShareLink.tsx
  53. 146 136
      packages/app/src/components/ShortcutsModal.tsx
  54. 2 2
      packages/app/src/components/Sidebar/RecentChanges.tsx
  55. 20 2
      packages/app/src/components/Theme/ThemeAntarctic.tsx
  56. 20 2
      packages/app/src/components/Theme/ThemeChristmas.tsx
  57. 189 185
      packages/app/src/components/Theme/ThemeDefault.module.scss
  58. 192 188
      packages/app/src/components/Theme/ThemeFireRed.module.scss
  59. 20 2
      packages/app/src/components/Theme/ThemeHalloween.tsx
  60. 257 253
      packages/app/src/components/Theme/ThemeHufflepuff.module.scss
  61. 20 2
      packages/app/src/components/Theme/ThemeHufflepuff.tsx
  62. 20 2
      packages/app/src/components/Theme/ThemeIsland.tsx
  63. 192 188
      packages/app/src/components/Theme/ThemeJadeGreen.module.scss
  64. 20 20
      packages/app/src/components/Theme/ThemeKibela.module.scss
  65. 188 184
      packages/app/src/components/Theme/ThemeMonoBlue.module.scss
  66. 20 2
      packages/app/src/components/Theme/ThemeSpring.tsx
  67. 20 2
      packages/app/src/components/Theme/ThemeWood.tsx
  68. 0 34
      packages/app/src/components/Theme/utils/ThemeImageProvider.tsx
  69. 1 1
      packages/app/src/components/Theme/utils/ThemeInjector.tsx
  70. 10 8
      packages/app/src/components/Theme/utils/ThemeProvider.tsx
  71. 3 3
      packages/app/src/components/TrashPageList.tsx
  72. 4 0
      packages/app/src/interfaces/share-link.ts
  73. 5 5
      packages/app/src/pages/[[...path]].page.tsx
  74. 16 16
      packages/app/src/pages/admin/[[...path]].page.tsx
  75. 102 0
      packages/app/src/pages/trash.page.tsx
  76. 1 1
      packages/app/src/server/routes/apiv3/security-setting.js
  77. 1 1
      packages/app/src/server/routes/index.js
  78. 5 5
      packages/app/src/server/service/acl.js
  79. 2 2
      packages/app/src/server/views/admin/app.html
  80. 2 2
      packages/app/src/server/views/admin/audit-log.html
  81. 1 1
      packages/app/src/server/views/admin/external-accounts.html
  82. 2 2
      packages/app/src/server/views/admin/global-notification-detail.html
  83. 2 2
      packages/app/src/server/views/admin/markdown.html
  84. 2 2
      packages/app/src/server/views/admin/notification.html
  85. 2 2
      packages/app/src/server/views/admin/search.html
  86. 2 2
      packages/app/src/server/views/admin/security.html
  87. 2 2
      packages/app/src/server/views/admin/slack-integration-legacy.html
  88. 2 2
      packages/app/src/server/views/admin/slack-integration.html
  89. 2 2
      packages/app/src/server/views/admin/user-group-detail.html
  90. 2 2
      packages/app/src/server/views/admin/user-groups.html
  91. 2 2
      packages/app/src/server/views/admin/users.html
  92. 1 1
      packages/app/src/server/views/widget/alert_siteurl_undefined.html
  93. 6 0
      packages/app/src/server/views/widget/headers/drawio.html
  94. 14 0
      packages/app/src/stores/share-link.tsx
  95. 167 0
      packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts
  96. 2 2
      packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts
  97. 1 1
      packages/codemirror-textlint/package.json
  98. 1 1
      packages/core/package.json
  99. 1 1
      packages/plugin-attachment-refs/package.json
  100. 3 3
      packages/plugin-lsx/package.json

+ 1 - 1
.github/workflows/reusable-app-prod.yml

@@ -193,7 +193,7 @@ jobs:
       fail-fast: false
       matrix:
         # List string expressions that is comma separated ids of tests in "test/cypress/integration"
-        spec-group: ['10', '20', '21', '30', '40', '60']
+        spec-group: ['10', '20', '21', '30', '40', '50', '60']
 
     services:
       mongodb:

+ 22 - 1
CHANGELOG.md

@@ -1,9 +1,30 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v5.1.2...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v5.1.3...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v5.1.3](https://github.com/weseek/growi/compare/v5.1.2...v5.1.3) - 2022-08-28
+
+### 💎 Features
+
+- feat(auditlog): Copy URL of the table (#6421) @miya
+
+### 🚀 Improvement
+
+- imprv(auditlog): Activity paging UI (#6444) @miya
+- imprv: Improvement behavior when click on drawio diagram. (#6486) @kaishuu0123
+
+### 🐛 Bug Fixes
+
+- fix: Label of alert when updating tags (#6478) @miya
+- fix: Uploading image using shortcut key(ctrl+v) shows toastError (#6474) @Yohei-Shiina
+- fix: Pager is not displayed (#6468) @miya
+
+### 🧰 Maintenance
+
+- support: Use vscode-stylelint (#6430) @yuki-takei
+
 ## [v5.1.2](https://github.com/weseek/growi/compare/v5.1.1...v5.1.2) - 2022-08-03
 
 ### 💎 Features

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
   "npmClient": "yarn",
   "useWorkspaces": true,
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "packages": [
     "packages/*"
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 2 - 2
packages/app/docker/README.md

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`5.1.2`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.2/docker/Dockerfile)
-* [`5.1.2-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.2/docker/Dockerfile)
+* [`5.1.3`, `5.1`, `5`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.3/docker/Dockerfile)
+* [`5.1.3-nocdn`, `5.1-nocdn`, `5-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.3/docker/Dockerfile)
 * [`5.0.11`, `5.0` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`5.0.11-nocdn`, `5.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v5.0.11/packages/app/docker/Dockerfile)
 * [`4.5.23`, `4.5`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.5.23/packages/app/docker/Dockerfile)

+ 7 - 7
packages/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",
@@ -63,11 +63,11 @@
     "@elastic/elasticsearch7": "npm:@elastic/elasticsearch@^7.17.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^5.1.3-RC.0",
-    "@growi/core": "^5.1.3-RC.0",
-    "@growi/plugin-attachment-refs": "^5.1.3-RC.0",
-    "@growi/plugin-lsx": "^5.1.3-RC.0",
-    "@growi/slack": "^5.1.3-RC.0",
+    "@growi/codemirror-textlint": "^5.1.4-RC.0",
+    "@growi/core": "^5.1.4-RC.0",
+    "@growi/plugin-attachment-refs": "^5.1.4-RC.0",
+    "@growi/plugin-lsx": "^5.1.4-RC.0",
+    "@growi/slack": "^5.1.4-RC.0",
     "@promster/express": "^7.0.2",
     "@promster/server": "^7.0.4",
     "@slack/events-api": "^3.0.0",
@@ -191,7 +191,7 @@
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.1.4",
-    "@growi/ui": "^5.1.3-RC.0",
+    "@growi/ui": "^5.1.4-RC.0",
     "@handsontable/react": "=2.1.0",
     "@icon/themify-icons": "1.0.1-alpha.3",
     "@next/bundle-analyzer": "^12.2.3",

+ 280 - 0
packages/app/public/static/locales/en_US/admin.json

@@ -1,4 +1,276 @@
 {
+  "wiki_management_home_page": "Wiki Management Home Page",
+  "app_settings": "App Settings",
+  "security_settings": {
+    "security_settings": "Security Settings",
+    "Guest Users Access": "Guest users access",
+    "always_hidden": "Always hidden",
+    "always_displayed": "Always displayed",
+    "displayed_or_hidden": "Displayed / Hidden",
+    "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
+    "Register limitation": "Register limitation",
+    "Register limitation desc": "Restriction of new users' registration",
+    "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
+    "users_without_account": "Users without account is not accessible",
+    "example": "Example",
+    "restrict_emails": "You can restrict email registration to your wiki by writing an email domain (beginning with @). ",
+    "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
+    "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
+    "insert_single": "Please insert single e-mail address per line.",
+    "page_list_and_search_results": "Page list / Search results",
+    "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
+    "page_listing_1_desc": "Show pages that are restricted by 'Only me' option when listing/searching",
+    "page_listing_2": "Page listing/searching<br>restricted by User group",
+    "page_listing_2_desc": "Show pages that are restricted by User group when listing/searching",
+    "page_access_rights": "Page access",
+    "page_delete_rights": "Delete rights",
+    "page_delete": "Page Delete",
+    "page_delete_completely": "Page Delete Completely",
+    "other_options": "Other options",
+    "deletion_explain": "Restricts users who can trash the selected single page.",
+    "complete_deletion_explain": "Restricts users who can completely delete  selected single page.",
+    "recursive_deletion_explain": "Restricts users who can trash pages including descendants.",
+    "recursive_complete_deletion_explain": "Restricts users who can completely delete pages including descendants.",
+    "inherit": "Inherit(Use the same setting as for a single page)",
+    "admin_only": "Admin only",
+    "admin_and_author": "Admin and author",
+    "anyone": "Anyone",
+    "session": "Session",
+    "max_age": "Max age (msec)",
+    "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
+    "max_age_caution": "Restarting the server is required after you modify this value.",
+    "forced_update_desc": "Settings have been forcibly changed. Previous setting: ",
+    "page_delete_rights_caution": "The \"Delete / Delete All\" permission (including descendant pages) is forced to be stronger than the \"Delete / Completely Delete\" permission. <br> <br> Admin only > Admin and autor > Anyone",
+    "Authentication mechanism settings": "Authentication Mechanism Settings",
+    "setup_is_not_yet_complete": "Setup is not yet complete",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
+    "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
+    "xss_prevent_setting_link": "Go to Markdown Settings",
+    "callback_URL": "Callback URL",
+    "providerName": "Provider Name",
+    "issuerHost": "Issuer Host",
+    "scope": "Scope",
+    "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
+    "authorization_endpoint": "Authorization Endpoint",
+    "token_endpoint": "Token Endpoint",
+    "revocation_endpoint": "Revocation Endpoint",
+    "introspection_endpoint": "Introspection Endpoint",
+    "userinfo_endpoint": "UserInfo Endpoint",
+    "end_session_endpoint": "EndSessioin Endpoint",
+    "registration_endpoint": "Registration Endpoint",
+    "jwks_uri": "JSON Web Key Set URL",
+    "clientID": "Client ID",
+    "client_secret": "Client Secret",
+    "updated_general_security_setting": "Succeeded to update security setting",
+    "setup_not_completed_yet": "Setup not completed yet",
+    "guest_mode": {
+      "deny": "Deny (Registered users only)",
+      "readonly": "Accept (Guests can read only)"
+    },
+    "registration_mode": {
+      "open": "Open (Anyone can register)",
+      "restricted": "Restricted (Requires approval by administrators)",
+      "closed": "Closed (Invitation Only)"
+    },
+    "share_link_rights": "Share link rights",
+    "enable_link_sharing": "Enable link sharing",
+    "all_share_links": "All share links",
+    "configuration": " Configuration",
+    "optional": "Optional",
+    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
+    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
+    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
+    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
+    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
+    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
+    "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
+    "Local": {
+      "name": "ID/Password",
+      "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+      "enable_local": "Enable ID/Password",
+      "password_reset_by_users": "Password reset by users",
+      "enable_password_reset_by_users": "Enable password reset by users",
+      "password_reset_desc": "when forgot password, users are able to reset it by themselves.",
+      "email_authentication": "Email authentication on user registration",
+      "enable_email_authentication": "Enable email authentication",
+      "enable_email_authentication_desc": "Email authentication is going to be performed for user registration.",
+      "please_enable_mailer": "Please setup mailer first.",
+      "need_complete_mail_setting_warning": "To use the following functions, please complete the mail settings."
+    },
+    "ldap": {
+      "enable_ldap": "Enable LDAP",
+      "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
+      "bind_mode": "Binding Mode",
+      "bind_manager": "Manager Bind",
+      "bind_user": "User Bind",
+      "bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
+      "bind_DN_user_detail1": "The query used to bind with the directory service.",
+      "bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "bind_DN_password": "Bind DN Password",
+      "bind_DN_password_manager_detail": "The password for the Bind DN account.",
+      "bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
+      "search_filter": "Search Filter",
+      "search_filter_detail1": "The query used to locate the authenticated user.",
+      "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
+      "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
+      "search_filter_example1": "Match with 'uid' or 'mail'",
+      "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "name_detail": "Specification of mappings for full name when creating new users",
+      "mail_detail": "Specification of mappings for mail address when creating new users",
+      "group_search_base_DN": "Group Search Base DN",
+      "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
+      "group_search_filter": "Group Search Filter",
+      "group_search_filter_detail1": "The query used to filter for groups.",
+      "group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
+      "group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
+      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
+      "group_search_user_DN_property": "User DN Property",
+      "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
+      "test_config": "Test Saved Configuration",
+      "updated_ldap": "Succeeded to update LDAP setting"
+    },
+    "SAML": {
+      "name": "SAML",
+      "enable_saml": "Enable SAML",
+      "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
+      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
+      "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
+      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
+      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
+      "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
+      "attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+      "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
+      "updated_saml": "Succeeded to update SAML setting"
+    },
+    "Basic": {
+      "enable_basic": "Enable Basic",
+      "name": "Basic Authentication",
+      "desc_1": "Login with <code>username</code> in Authorization header.",
+      "desc_2": "User will be automatically generated if not exist.",
+      "updated_basic": "Succeeded to update Basic setting"
+    },
+    "OAuth": {
+      "enable_oidc": "Enable OIDC",
+      "register": "Register for %s",
+      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
+      "Google": {
+        "enable_google": "Enable Google OAuth",
+        "name": "Google OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Create Project if no projects exist",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_google": "Succeeded to update Google OAuth setting"
+      },
+      "Facebook": {
+        "name": "Facebook OAuth"
+      },
+      "Twitter": {
+        "enable_twitter": "Enable Twitter OAuth",
+        "name": "Twitter OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Sign in Twitter",
+        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
+        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
+        "register_5": "Copy and paste your ClientID and Client Secret above",
+        "updated_twitter": "Succeeded to update Twitter OAuth setting"
+      },
+      "GitHub": {
+        "enable_github": "Enable GitHub OAuth",
+        "name": "GitHub OAuth",
+        "register_1": "Access {{link}}",
+        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_github": "Succeeded to update GitHub OAuth setting"
+      },
+      "OIDC": {
+        "name": "OpenID Connect",
+        "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
+        "username_detail": "Specification of mappings for <code>username</code> when creating new users",
+        "name_detail": "Specification of mappings for <code>name</code> when creating new users",
+        "mapping_detail": "Specification of mappings for %s when creating new users",
+        "register_1": "Contant to OIDC IdP Administrator",
+        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
+        "register_3": "Copy and paste your ClientID and Client Secret above",
+        "updated_oidc": "Succeeded to update OpenID Connect",
+        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
+      },
+      "how_to": {
+        "google": "How to configure Google OAuth?",
+        "github": "How to configure GitHub OAuth?",
+        "twitter": "How to configure Twitter OAuth?",
+        "oidc": "How to configure OIDC?"
+      }
+    },
+    "form_item_name": {
+      "entryPoint": "Entry point",
+      "issuer": "Issuer",
+      "cert": "Certificate",
+      "attrMapId": "ID",
+      "attrMapUsername": "Username",
+      "attrMapMail": "Mail Address",
+      "attrMapFirstName": "First Name",
+      "attrMapLastName": "Last Name",
+      "ABLCRule": "Rule"
+    }
+  },
+  "markdown_settings": "Markdown Settings",
+  "notification_settings": {
+    "notification_settings": "Notification Settings",
+    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
+    "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
+    "slack_app_configuration": "Slack app configuration",
+    "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
+    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
+    "how_to": {
+      "header": "How to configure Incoming Webhooks?",
+      "workspace": "(At Workspace) Add a hook",
+      "workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
+      "workspace_desc2": "Choose the default channel to post.",
+      "workspace_desc3": "Add.",
+      "at_growi": "(At GROWI admin page) Set Webhook URL",
+      "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
+    },
+    "user_trigger_notification_header": "Default notification settings for patterns",
+    "pattern": "Pattern",
+    "channel": "Channel",
+    "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
+    "channel_desc": "Slack channel name. Without <code>#</code>.",
+    "valid_page": "Enable/disable Notification",
+    "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
+    "just_me_notification_help": "<strong>The page that is restricted by 'Only Me'</strong> is notify when the page edited.",
+    "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
+    "notification_list": "List of notification settings",
+    "add_notification": "Add new",
+    "trigger_path": "Trigger path",
+    "trigger_path_help": "(expression with <code>*</code> is supported)",
+    "trigger_events": "Trigger events",
+    "notify_to": "Notify to",
+    "back_to_list": "Go back to list",
+    "notification_detail": "Notification Setting Details",
+    "event_pageCreate": "When new page is \"CREATED\"",
+    "event_pageEdit": "When page is \"EDITED\"",
+    "event_pageDelete": "When page is \"DELETED\"",
+    "event_pageMove": "When page is \"MOVED\" (renamed)",
+    "event_pageLike": "When someone \"LIKES\" page",
+    "event_comment": "When someone \"COMMENTS\" on page",
+    "email": {
+      "ifttt_link": "Create a new IFTTT applet with Email trigger"
+    },
+    "updated_slackApp": "Succeeded to update Slack App Configuration setting",
+    "add_notification_pattern": "Add user trigger notification patterns",
+    "delete_notification_pattern": "Delete notification pattern",
+    "delete_notification_pattern_desc1": "Delete Path: {{path}}",
+    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
+    "toggle_notification": "Updated setting of {{path}}"
+  },
+  "Customize": "Customize",
+  "full_text_search_management": "Full Text Search Management",
   "mailer_setup_required":"<a href='/admin/app'>Email settings</a> are required to send.",
   "admin_top": {
     "management_wiki": "Management Wiki",
@@ -305,12 +577,14 @@
     "delete": "Delete"
   },
   "external_notification": {
+    "external_notification": "External Notification",
     "enabled": "Enabled",
     "disabled": "Disabled",
     "header_status": "Slack Integration Status",
     "caution_enabled": "CAUTION: Currently, notifications that are configured in this page will send only to the Slack Workspace set as primary."
   },
   "slack_integration": {
+    "slack_integration": "Slack Integration",
     "selecting_bot_types": {
       "slack_bot": "Slack bot",
       "detailed_explanation": "Detailed explanation",
@@ -422,10 +696,13 @@
     }
   },
   "slack_integration_legacy": {
+    "slack_integration_legacy": "Legacy Slack Integration",
+    "alert_disabled": "This 'Slack Legacy Intenfation' has been currently disabled since <a href='/admin/slack-integration'>New settings</a> are enabled",
     "alert_Pd": "This 'Legacy Slack Integration' is currently disabled because <a href='/admin/slack-integration'>the new settings</a> is enabled.",
     "alert_deplicated": "This 'Legacy Slack Integration' is outdated and will be discontinued in the future. Use <a href='/admin/slack-integration'>the new settings</a> instead. "
   },
   "user_management": {
+    "user_management": "User Management",
     "invite_users": "Temporarily issue a new user",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "invite_modal": {
@@ -483,6 +760,7 @@
     "current_users": "Current users:"
   },
   "user_group_management": {
+    "user_group_management": "User Group Management",
     "create_group": "Create new group",
     "add_child_group": "Add child group",
     "remove_child_group": "Remove",
@@ -529,6 +807,8 @@
     }
   },
   "audit_log_management": {
+    "audit_log": "Audit Log",
+    "audit_log_settings": "Audit Log Settings",
     "user": "User",
     "username": "Username",
     "date": "Date",

+ 1 - 279
packages/app/public/static/locales/en_US/translation.json

@@ -114,26 +114,13 @@
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New page",
   "Create under": "Create page under below:",
-  "Wiki Management Home Page": "Wiki Management Home Page",
-  "App Settings": "App Settings",
   "V5 Page Migration": "Convert To V5 Compatibility",
   "GROWI.5.0_new_schema": "GROWI.5.0 new schema",
   "See_more_detail_on_new_schema": "See more detail on <a href='#'>{{url}}</a> <i class='icon-share-alt'></i> ",
   "Site URL settings": "Site URL settings",
-  "Markdown Settings": "Markdown Settings",
-  "Customize": "Customize",
-  "Notification Settings": "Notification Settings",
-  "slack_integration": "Slack Integration",
-  "External_Notification": "External Notification",
-  "Legacy_Slack_Integration": "Legacy Slack Integration",
-  "User_Management": "User Management",
   "external_account_management": "External Account Management",
   "UserGroup": "UserGroup",
   "ChildUserGroup": "ChildUserGroup",
-  "UserGroup Management": "UserGroup Management",
-  "AuditLog": "Audit Log",
-  "AuditLog Settings": "Audit Log Settings",
-  "Full Text Search Management": "Full Text Search Management",
   "Import Data": "Import Data",
   "Export Archive Data": "Export Archive Data",
   "Basic Settings": "Basic Settings",
@@ -148,9 +135,6 @@
   "page_list": "Page List",
   "scope_of_page_disclosure": "Scope of page disclosure",
   "set_point": "Set point",
-  "always_displayed": "Always displayed",
-  "always_hidden": "Always hidden",
-  "displayed_or_hidden": "Displayed / Hidden",
   "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -256,7 +240,6 @@
     "new_password_confirm": "Re-enter new password",
     "password_is_not_set": "Password is not set"
   },
-  "security_settings": "Security settings",
   "share_links": {
     "Shere this page link to public": "Shere this page link to public",
     "share_link_list": "Share link list",
@@ -547,6 +530,7 @@
     "create_failed": "Failed to create {{target}}",
     "update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_succeeded": "File upload succeeded.",
     "file_upload_failed": "File upload failed.",
     "initialize_successed": "Succeeded to initialize {{target}}",
     "give_user_admin": "Succeeded to give {{username}} admin",
@@ -689,268 +673,6 @@
       "error_duplicate_pages_found": "Multiple pages with the same path name were found. Please rename or delete and try again."
     }
   },
-  "security_setting": {
-    "Guest Users Access": "Guest users access",
-    "Fixed by env var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",
-    "Register limitation": "Register limitation",
-    "Register limitation desc": "Restriction of new users' registration",
-    "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-    "users_without_account": "Users without account is not accessible",
-    "example": "Example",
-    "restrict_emails": "You can restrict email registration to your wiki by writing an email domain (beginning with @). ",
-    "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
-    "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
-    "insert_single": "Please insert single e-mail address per line.",
-    "page_list_and_search_results": "Page list / Search results",
-    "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
-    "page_listing_1_desc": "Show pages that are restricted by 'Only me' option when listing/searching",
-    "page_listing_2": "Page listing/searching<br>restricted by User group",
-    "page_listing_2_desc": "Show pages that are restricted by User group when listing/searching",
-    "page_access_rights": "Page access",
-    "page_delete_rights": "Delete rights",
-    "page_delete": "Page Delete",
-    "page_delete_completely": "Page Delete Completely",
-    "other_options": "Other options",
-    "deletion_explain": "Restricts users who can trash the selected single page.",
-    "complete_deletion_explain": "Restricts users who can completely delete  selected single page.",
-    "recursive_deletion_explain": "Restricts users who can trash pages including descendants.",
-    "recursive_complete_deletion_explain": "Restricts users who can completely delete pages including descendants.",
-    "inherit": "Inherit(Use the same setting as for a single page)",
-    "admin_only": "Admin only",
-    "admin_and_author": "Admin and author",
-    "anyone": "Anyone",
-    "session": "Session",
-    "max_age": "Max age (msec)",
-    "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
-    "max_age_caution": "Restarting the server is required after you modify this value.",
-    "forced_update_desc": "Settings have been forcibly changed. Previous setting: ",
-    "page_delete_rights_caution": "The \"Delete / Delete All\" permission (including descendant pages) is forced to be stronger than the \"Delete / Completely Delete\" permission. <br> <br> Admin only > Admin and autor > Anyone",
-    "Authentication mechanism settings": "Authentication Mechanism Settings",
-    "setup_is_not_yet_complete": "Setup is not yet complete",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
-    "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
-    "xss_prevent_setting_link": "Go to Markdown Settings",
-    "callback_URL": "Callback URL",
-    "providerName": "Provider Name",
-    "issuerHost": "Issuer Host",
-    "scope": "Scope",
-    "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
-    "authorization_endpoint": "Authorization Endpoint",
-    "token_endpoint": "Token Endpoint",
-    "revocation_endpoint": "Revocation Endpoint",
-    "introspection_endpoint": "Introspection Endpoint",
-    "userinfo_endpoint": "UserInfo Endpoint",
-    "end_session_endpoint": "EndSessioin Endpoint",
-    "registration_endpoint": "Registration Endpoint",
-    "jwks_uri": "JSON Web Key Set URL",
-    "clientID": "Client ID",
-    "client_secret": "Client Secret",
-    "updated_general_security_setting": "Succeeded to update security setting",
-    "setup_not_completed_yet": "Setup not completed yet",
-    "guest_mode": {
-      "deny": "Deny (Registered users only)",
-      "readonly": "Accept (Guests can read only)"
-    },
-    "registration_mode": {
-      "open": "Open (Anyone can register)",
-      "restricted": "Restricted (Requires approval by administrators)",
-      "closed": "Closed (Invitation Only)"
-    },
-    "share_link_rights": "Share link rights",
-    "enable_link_sharing": "Enable link sharing",
-    "all_share_links": "All share links",
-    "configuration": " Configuration",
-    "optional": "Optional",
-    "Treat username matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>username</code> match",
-    "Treat username matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>username</code>.",
-    "Treat email matching as identical": "Automatically bind external accounts newly logged in to local accounts when <code>email</code> match",
-    "Treat email matching as identical_warn": "WARNING: Be aware of security because the system treats the same user as a match of <code>email</code>.",
-    "Use env var if empty": "Use env var <code>{{env}}</code> if empty",
-    "Use default if both are empty": "If both ​​are empty, the default value <code>{{target}}</code> is used.",
-    "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
-    "Local": {
-      "name": "ID/Password",
-      "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-      "enable_local": "Enable ID/Password",
-      "password_reset_by_users": "Password reset by users",
-      "enable_password_reset_by_users": "Enable password reset by users",
-      "password_reset_desc": "when forgot password, users are able to reset it by themselves.",
-      "email_authentication": "Email authentication on user registration",
-      "enable_email_authentication": "Enable email authentication",
-      "enable_email_authentication_desc": "Email authentication is going to be performed for user registration.",
-      "please_enable_mailer": "Please setup mailer first.",
-      "need_complete_mail_setting_warning": "To use the following functions, please complete the mail settings."
-    },
-    "ldap": {
-      "enable_ldap": "Enable LDAP",
-      "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
-      "bind_mode": "Binding Mode",
-      "bind_manager": "Manager Bind",
-      "bind_user": "User Bind",
-      "bind_DN_manager_detail": "The DN of the account that authenticates and queries the directory service",
-      "bind_DN_user_detail1": "The query used to bind with the directory service.",
-      "bind_DN_user_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-      "bind_DN_password": "Bind DN Password",
-      "bind_DN_password_manager_detail": "The password for the Bind DN account.",
-      "bind_DN_password_user_detail": "The password that is entered in the login page will be used to bind.",
-      "search_filter": "Search Filter",
-      "search_filter_detail1": "The query used to locate the authenticated user.",
-      "search_filter_detail2": "Use <code>&#123;&#123;username&#125;&#125;</code> to reference the username entered in the login page.",
-      "search_filter_detail3": "If empty, the filter <code>(uid=&#123;&#123;username&#125;&#125;)</code> is used.",
-      "search_filter_example1": "Match with 'uid' or 'mail'",
-      "search_filter_example2": "Match with 'sAMAccountName' for Active Directory",
-      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-      "name_detail": "Specification of mappings for full name when creating new users",
-      "mail_detail": "Specification of mappings for mail address when creating new users",
-      "group_search_base_DN": "Group Search Base DN",
-      "group_search_base_DN_detail": "The base DN from which to search for groups. If defined, also <code>Group Search Filter</code> must be defined for the search to work.",
-      "group_search_filter": "Group Search Filter",
-      "group_search_filter_detail1": "The query used to filter for groups.",
-      "group_search_filter_detail2": "Login via LDAP is accepted only when this query hits one or more groups.",
-      "group_search_filter_detail3": "Use <code>&#123;&#123;dn&#125;&#125;</code> to have it replaced of the found user object.",
-      "group_search_filter_detail4": "<code>(&(cn=group1)(memberUid=&#123;&#123;dn&#125;&#125;))</code> hits the groups which has <code>cn=group1</code> and <code>memberUid</code> includes the user's <code>uid</code>(when <code>Group DN Property</code> is not changed from the default value.)",
-      "group_search_user_DN_property": "User DN Property",
-      "group_search_user_DN_property_detail": "The property of user object to use in <code>&#123;&#123;dn&#125;&#125;</code> interpolation of <code>Group Search Filter</code>.",
-      "test_config": "Test Saved Configuration",
-      "updated_ldap": "Succeeded to update LDAP setting"
-    },
-    "SAML": {
-      "name": "SAML",
-      "enable_saml": "Enable SAML",
-      "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
-      "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-      "mapping_detail": "Specification of mappings for {{target}} when creating new users",
-      "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
-      "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
-      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
-      "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-      "attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
-      "attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
-      "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
-      "updated_saml": "Succeeded to update SAML setting"
-    },
-    "Basic": {
-      "enable_basic": "Enable Basic",
-      "name": "Basic Authentication",
-      "desc_1": "Login with <code>username</code> in Authorization header.",
-      "desc_2": "User will be automatically generated if not exist.",
-      "updated_basic": "Succeeded to update Basic setting"
-    },
-    "OAuth": {
-      "enable_oidc": "Enable OIDC",
-      "register": "Register for %s",
-      "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
-      "Google": {
-        "enable_google": "Enable Google OAuth",
-        "name": "Google OAuth",
-        "register_1": "Access {{link}}",
-        "register_2": "Create Project if no projects exist",
-        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above",
-        "updated_google": "Succeeded to update Google OAuth setting"
-      },
-      "Facebook": {
-        "name": "Facebook OAuth"
-      },
-      "Twitter": {
-        "enable_twitter": "Enable Twitter OAuth",
-        "name": "Twitter OAuth",
-        "register_1": "Access {{link}}",
-        "register_2": "Sign in Twitter",
-        "register_3": "Create Credentials &rightarrow; OAuth client ID &rightarrow; Select \"Web application\"",
-        "register_4": "Register your OAuth App with one of Authorized redirect URIs as <code>{{url}}</code>",
-        "register_5": "Copy and paste your ClientID and Client Secret above",
-        "updated_twitter": "Succeeded to update Twitter OAuth setting"
-      },
-      "GitHub": {
-        "enable_github": "Enable GitHub OAuth",
-        "name": "GitHub OAuth",
-        "register_1": "Access {{link}}",
-        "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
-        "register_3": "Copy and paste your ClientID and Client Secret above",
-        "updated_github": "Succeeded to update GitHub OAuth setting"
-      },
-      "OIDC": {
-        "name": "OpenID Connect",
-        "id_detail": "Specification of the name of attribute which can identify the user in OIDC claims",
-        "username_detail": "Specification of mappings for <code>username</code> when creating new users",
-        "name_detail": "Specification of mappings for <code>name</code> when creating new users",
-        "mapping_detail": "Specification of mappings for %s when creating new users",
-        "register_1": "Contant to OIDC IdP Administrator",
-        "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
-        "register_3": "Copy and paste your ClientID and Client Secret above",
-        "updated_oidc": "Succeeded to update OpenID Connect",
-        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
-      },
-      "how_to": {
-        "google": "How to configure Google OAuth?",
-        "github": "How to configure GitHub OAuth?",
-        "twitter": "How to configure Twitter OAuth?",
-        "oidc": "How to configure OIDC?"
-      }
-    },
-    "form_item_name": {
-      "entryPoint": "Entry point",
-      "issuer": "Issuer",
-      "cert": "Certificate",
-      "attrMapId": "ID",
-      "attrMapUsername": "Username",
-      "attrMapMail": "Mail Address",
-      "attrMapFirstName": "First Name",
-      "attrMapLastName": "Last Name",
-      "ABLCRule": "Rule"
-    }
-  },
-  "notification_setting": {
-    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
-    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
-    "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-    "slack_app_configuration": "Slack app configuration",
-    "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
-    "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
-    "how_to": {
-      "header": "How to configure Incoming Webhooks?",
-      "workspace": "(At Workspace) Add a hook",
-      "workspace_desc1": "Go to <a href='https://slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
-      "workspace_desc2": "Choose the default channel to post.",
-      "workspace_desc3": "Add.",
-      "at_growi": "(At GROWI admin page) Set Webhook URL",
-      "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
-    },
-    "user_trigger_notification_header": "Default notification settings for patterns",
-    "pattern": "Pattern",
-    "channel": "Channel",
-    "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
-    "channel_desc": "Slack channel name. Without <code>#</code>.",
-    "valid_page": "Enable/disable Notification",
-    "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
-    "just_me_notification_help": "<strong>The page that is restricted by 'Only Me'</strong> is notify when the page edited.",
-    "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
-    "notification_list": "List of notification settings",
-    "add_notification": "Add new",
-    "trigger_path": "Trigger path",
-    "trigger_path_help": "(expression with <code>*</code> is supported)",
-    "trigger_events": "Trigger events",
-    "notify_to": "Notify to",
-    "back_to_list": "Go back to list",
-    "notification_detail": "Notification Setting Details",
-    "event_pageCreate": "When new page is \"CREATED\"",
-    "event_pageEdit": "When page is \"EDITED\"",
-    "event_pageDelete": "When page is \"DELETED\"",
-    "event_pageMove": "When page is \"MOVED\" (renamed)",
-    "event_pageLike": "When someone \"LIKES\" page",
-    "event_comment": "When someone \"COMMENTS\" on page",
-    "email": {
-      "ifttt_link": "Create a new IFTTT applet with Email trigger"
-    },
-    "updated_slackApp": "Succeeded to update Slack App Configuration setting",
-    "add_notification_pattern": "Add user trigger notification patterns",
-    "delete_notification_pattern": "Delete notification pattern",
-    "delete_notification_pattern_desc1": "Delete Path: {{path}}",
-    "delete_notification_pattern_desc2": "Once deleted, it cannot be recovered",
-    "toggle_notification": "Updated setting of {{path}}"
-  },
   "full_text_search_management": {
     "elasticsearch_management": "Elasticsearch management",
     "connection_status": "Connection status",

+ 1 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -547,6 +547,7 @@
     "create_failed": "{{target}}の作成に失敗しました",
     "update_successed": "{{target}}を更新しました",
     "update_failed": "{{target}}の更新に失敗しました",
+    "file_upload_succeeded": "ファイルをアップロードしました",
     "file_upload_failed": "ファイルのアップロードに失敗しました",
     "initialize_successed": "{{target}}を初期化しました",
     "give_user_admin": "{{username}}を管理者に設定しました",

+ 1 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -525,6 +525,7 @@
     "create_failed": "Failed to create {{target}}",
 		"update_successed": "Succeeded to update {{target}}",
     "update_failed": "Failed to update {{target}}",
+    "file_upload_succeeded": "文件上传成功",
     "file_upload_failed": "文件上传失败",
     "initialize_successed": "Succeeded to initialize {{target}}",
 		"give_user_admin": "Succeeded to give {{username}} admin",

+ 2 - 2
packages/app/src/components/Admin/App/AppSetting.jsx

@@ -18,12 +18,12 @@ const logger = loggerFactory('growi:appSettings');
 
 const AppSetting = (props) => {
   const { adminAppContainer } = props;
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const submitHandler = useCallback(async() => {
     try {
       await adminAppContainer.updateAppSettingHandler();
-      toastSuccess(t('toaster.update_successed', { target: t('App Settings') }));
+      toastSuccess(t('toaster.update_successed', { target: t('app_settings') }));
     }
     catch (err) {
       toastError(err);

+ 3 - 3
packages/app/src/components/Admin/App/AppSettingsPageContents.tsx

@@ -25,7 +25,7 @@ type Props = {
 }
 
 const AppSettingsPageContents = (props: Props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { adminAppContainer } = props;
   const { isV5Compatible } = adminAppContainer.state;
 
@@ -78,7 +78,7 @@ const AppSettingsPageContents = (props: Props) => {
 
       <div className="row">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">{t('App Settings')}</h2>
+          <h2 className="admin-setting-header">{t('app_settings')}</h2>
           <AppSetting />
         </div>
       </div>
@@ -92,7 +92,7 @@ const AppSettingsPageContents = (props: Props) => {
 
       <div className="row mt-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header" id="mail-settings">{t('admin:app_setting.mail_settings')}</h2>
+          <h2 className="admin-setting-header" id="mail-settings">{t('app_setting.mail_settings')}</h2>
           <MailSetting />
         </div>
       </div>

+ 3 - 3
packages/app/src/components/Admin/AuditLog/AuditLogDisableMode.tsx

@@ -3,7 +3,7 @@ import React, { FC } from 'react';
 import { useTranslation } from 'react-i18next';
 
 export const AuditLogDisableMode: FC = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   return (
     <div id="content-main" className="content-main container-lg">
@@ -12,10 +12,10 @@ export const AuditLogDisableMode: FC = () => {
           <div className="col-md-6 mt-5">
             <div className="text-center">
               <h1><i className="icon-exclamation large"></i></h1>
-              <h1 className="text-center">{t('AuditLog')}</h1>
+              <h1 className="text-center">{t('audit_log_management.audit_log')}</h1>
               <h3
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:audit_log_management.disable_mode_explain') }}
+                dangerouslySetInnerHTML={{ __html: t('audit_log_management.disable_mode_explain') }}
               />
             </div>
           </div>

+ 2 - 2
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -30,7 +30,7 @@ const formatDate = (date: Date | null) => {
 const PAGING_LIMIT = 10;
 
 export const AuditLogManagement: FC = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const typeaheadRef = useRef<IClearable>(null);
 
@@ -159,7 +159,7 @@ export const AuditLogManagement: FC = () => {
 
       <h2 className="admin-setting-header mb-3">
         <span>
-          {isSettingPage ? t('AuditLog Settings') : t('AuditLog')}
+          {isSettingPage ? t('audit_log_management.audit_log_settings') : t('audit_log_management.audit_log')}
         </span>
         { !isSettingPage && (
           <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>

+ 12 - 12
packages/app/src/components/Admin/Common/AdminNavigation.jsx

@@ -15,7 +15,7 @@ import urljoin from 'url-join';
 // import { withUnstatedContainers } from '../../UnstatedUtils';
 
 const AdminNavigation = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   // const { appContainer } = props;
   const pathname = window.location.pathname;
 
@@ -25,22 +25,22 @@ const AdminNavigation = (props) => {
   // eslint-disable-next-line react/prop-types
   const MenuLabel = ({ menu }) => {
     switch (menu) {
-      case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('App Settings') }</>;
-      case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings') }</>;
-      case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('Markdown Settings') }</>;
+      case 'app':                      return <><i className="icon-fw icon-settings"></i>        { t('app_settings') }</>;
+      case 'security':                 return <><i className="icon-fw icon-shield"></i>          { t('security_settings.security_settings') }</>;
+      case 'markdown':                 return <><i className="icon-fw icon-note"></i>            { t('markdown_settings') }</>;
       case 'customize':                return <><i className="icon-fw icon-wrench"></i>          { t('Customize') }</>;
       case 'importer':                 return <><i className="icon-fw icon-cloud-upload"></i>    { t('Import Data') }</>;
       case 'export':                   return <><i className="icon-fw icon-cloud-download"></i>  { t('Export Archive Data') }</>;
-      case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('External_Notification')}</>;
-      case 'slack-integration':        return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration') }</>;
-      case 'slack-integration-legacy': return <><i className="icon-fw icon-shuffle"></i>         { t('Legacy_Slack_Integration')}</>;
-      case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('User_Management') }</>;
-      case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('UserGroup Management') }</>;
-      case 'search':                   return <><i className="icon-fw icon-magnifier"></i>       { t('Full Text Search Management') }</>;
+      case 'notification':             return <><i className="icon-fw icon-bell"></i>            { t('external_notification.external_notification')}</>;
+      case 'slack-integration':        return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration.slack_integration') }</>;
+      case 'slack-integration-legacy': return <><i className="icon-fw icon-shuffle"></i>         { t('slack_integration_legacy.slack_integration_legacy')}</>;
+      case 'users':                    return <><i className="icon-fw icon-user"></i>            { t('user_management.user_management') }</>;
+      case 'user-groups':              return <><i className="icon-fw icon-people"></i>          { t('user_group_management.user_group_management') }</>;
+      case 'search':                   return <><i className="icon-fw icon-magnifier"></i>       { t('full_text_search_management') }</>;
       // TODO: Consider where to place the "AuditLog"
-      case 'audit-log':                return <><i className="icon-fw icon-feed"></i>            { t('AuditLog')}</>;
+      case 'audit-log':                return <><i className="icon-fw icon-feed"></i>            { t('audit_log_management.audit_log')}</>;
       case 'cloud':                    return <><i className="icon-fw icon-share-alt"></i>       { t('to_cloud_settings')} </>;
-      default:                         return <><i className="icon-fw icon-home"></i>            { t('Wiki Management Home Page') }</>;
+      default:                         return <><i className="icon-fw icon-home"></i>            { t('wiki_management_home_page') }</>;
     }
   };
 

+ 14 - 14
packages/app/src/components/Admin/LegacySlackIntegration/SlackConfiguration.jsx

@@ -25,7 +25,7 @@ class SlackConfiguration extends React.Component {
 
     try {
       await adminSlackIntegrationLegacyContainer.updateSlackAppConfiguration();
-      toastSuccess(t('notification_setting.updated_slackApp'));
+      toastSuccess(t('notification_settings.updated_slackApp'));
     }
     catch (err) {
       toastError(err);
@@ -62,7 +62,7 @@ class SlackConfiguration extends React.Component {
         </div>
         {adminSlackIntegrationLegacyContainer.state.selectSlackOption === 'Incoming Webhooks' ? (
           <React.Fragment>
-            <h2 className="border-bottom mb-5">{t('notification_setting.slack_incoming_configuration')}</h2>
+            <h2 className="border-bottom mb-5">{t('admin:notification_settings.slack_incoming_configuration')}</h2>
 
             <div className="row mb-3">
               <label className="col-md-3 text-left text-md-right">Webhook URL</label>
@@ -87,11 +87,11 @@ class SlackConfiguration extends React.Component {
                     onChange={() => { adminSlackIntegrationLegacyContainer.switchIsIncomingWebhookPrioritized() }}
                   />
                   <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
-                    {t('notification_setting.prioritize_webhook')}
+                    {t('admin:notification_settings.prioritize_webhook')}
                   </label>
                 </div>
                 <p className="form-text text-muted">
-                  {t('notification_setting.prioritize_webhook_desc')}
+                  {t('admin:notification_settings.prioritize_webhook_desc')}
                 </p>
               </div>
             </div>
@@ -99,20 +99,20 @@ class SlackConfiguration extends React.Component {
         )
           : (
             <React.Fragment>
-              <h2 className="border-bottom mb-5">{t('notification_setting.slack_app_configuration')}</h2>
+              <h2 className="border-bottom mb-5">{t('notification_settings.slack_app_configuration')}</h2>
 
               <div className="well card">
                 <span className="text-danger"><i className="icon-fw icon-exclamation"></i>NOT RECOMMENDED</span>
                 <br />
                 {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.slack_app_configuration_desc') }} />
+                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.slack_app_configuration_desc') }} />
                 <br />
                 <a
                   href="#slack-incoming-webhooks"
                   data-toggle="tab"
                   onClick={() => adminSlackIntegrationLegacyContainer.switchSlackOption('Incoming Webhooks')}
                 >
-                  {t('notification_setting.use_instead')}
+                  {t('admin:notification_settings.use_instead')}
                 </a>
               </div>
 
@@ -141,24 +141,24 @@ class SlackConfiguration extends React.Component {
 
         <h3>
           <i className="icon-question" aria-hidden="true"></i>{' '}
-          <a href="#collapseHelpForIwh" data-toggle="collapse">{t('notification_setting.how_to.header')}</a>
+          <a href="#collapseHelpForIwh" data-toggle="collapse">{t('admin:notification_settings.how_to.header')}</a>
         </h3>
 
         <ol id="collapseHelpForIwh" className="collapse">
           <li>
-            {t('notification_setting.how_to.workspace')}
+            {t('notification_settings.how_to.workspace')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html:  t('notification_setting.how_to.workspace_desc1') }} />
-              <li>{t('notification_setting.how_to.workspace_desc2')}</li>
-              <li>{t('notification_setting.how_to.workspace_desc3')}</li>
+              <li dangerouslySetInnerHTML={{ __html:  t('notification_settings.how_to.workspace_desc1') }} />
+              <li>{t('notification_settings.how_to.workspace_desc2')}</li>
+              <li>{t('notification_settings.how_to.workspace_desc3')}</li>
             </ol>
           </li>
           <li>
-            {t('notification_setting.how_to.at_growi')}
+            {t('notification_settings.how_to.at_growi')}
             <ol>
               {/* eslint-disable-next-line react/no-danger */}
-              <li dangerouslySetInnerHTML={{ __html: t('notification_setting.how_to.at_growi_desc') }} />
+              <li dangerouslySetInnerHTML={{ __html: t('notification_settings.how_to.at_growi_desc') }} />
             </ol>
           </li>
         </ol>

+ 11 - 11
packages/app/src/components/Admin/Notification/GlobalNotification.jsx

@@ -26,7 +26,7 @@ class GlobalNotification extends React.Component {
 
     try {
       await adminNotificationContainer.updateGlobalNotificationForPages();
-      toastSuccess(t('toaster.update_successed', { target: t('External_Notification') }));
+      toastSuccess(t('toaster.update_successed', { target: t('external_notification.external_notification') }));
     }
     catch (err) {
       toastError(err);
@@ -40,11 +40,11 @@ class GlobalNotification extends React.Component {
     return (
       <React.Fragment>
 
-        <h2 className="border-bottom my-4">{t('notification_setting.valid_page')}</h2>
+        <h2 className="border-bottom my-4">{t('notification_settings.valid_page')}</h2>
 
         <p className="card well">
           {/* eslint-disable-next-line react/no-danger */}
-          <span dangerouslySetInnerHTML={{ __html: t('notification_setting.link_notification_help') }} />
+          <span dangerouslySetInnerHTML={{ __html: t('notification_settings.link_notification_help') }} />
         </p>
 
 
@@ -60,7 +60,7 @@ class GlobalNotification extends React.Component {
               />
               <label className="custom-control-label" htmlFor="isNotificationForOwnerPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.just_me_notification_help') }} />
+                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.just_me_notification_help') }} />
               </label>
             </div>
           </div>
@@ -79,7 +79,7 @@ class GlobalNotification extends React.Component {
               />
               <label className="custom-control-label" htmlFor="isNotificationForGroupPageEnabled">
                 {/* eslint-disable-next-line react/no-danger */}
-                <span dangerouslySetInnerHTML={{ __html: t('notification_setting.group_notification_help') }} />
+                <span dangerouslySetInnerHTML={{ __html: t('notification_settings.group_notification_help') }} />
               </label>
             </div>
           </div>
@@ -97,9 +97,9 @@ class GlobalNotification extends React.Component {
           </div>
         </div>
 
-        <h2 className="border-bottom mb-5">{t('notification_setting.notification_list')}
+        <h2 className="border-bottom mb-5">{t('notification_settings.notification_list')}
           <a href="/admin/global-notification/new">
-            <p className="btn btn-outline-secondary pull-right">{t('notification_setting.add_notification')}</p>
+            <p className="btn btn-outline-secondary pull-right">{t('notification_settings.add_notification')}</p>
           </a>
         </h2>
 
@@ -108,9 +108,9 @@ class GlobalNotification extends React.Component {
             <tr>
               <th>ON/OFF</th>
               {/* eslint-disable-next-line react/no-danger */}
-              <th>{t('notification_setting.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help') }} /></th>
-              <th>{t('notification_setting.trigger_events')}</th>
-              <th>{t('notification_setting.notify_to')}</th>
+              <th>{t('notification_settings.trigger_path')} <span dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help') }} /></th>
+              <th>{t('notification_settings.trigger_events')}</th>
+              <th>{t('notification_settings.notify_to')}</th>
               <th></th>
             </tr>
           </thead>
@@ -134,7 +134,7 @@ GlobalNotification.propTypes = {
 };
 
 const GlobalNotificationWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   return <GlobalNotification t={t} {...props} />;
 };

+ 2 - 2
packages/app/src/components/Admin/Notification/GlobalNotificationList.jsx

@@ -39,7 +39,7 @@ class GlobalNotificationList extends React.Component {
       await apiv3Put(`/notification-setting/global-notification/${notification._id}/enabled`, {
         isEnabled,
       });
-      toastSuccess(t('notification_setting.toggle_notification', { path: notification.triggerPath }));
+      toastSuccess(t('notification_settings.toggle_notification', { path: notification.triggerPath }));
       await this.props.adminNotificationContainer.retrieveNotificationData();
     }
     catch (err) {
@@ -61,7 +61,7 @@ class GlobalNotificationList extends React.Component {
 
     try {
       const deletedNotificaton = await adminNotificationContainer.deleteGlobalNotificationPattern(this.state.notificationForConfiguration._id);
-      toastSuccess(t('notification_setting.delete_notification_pattern', { path: deletedNotificaton.triggerPath }));
+      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.triggerPath }));
     }
     catch (err) {
       toastError(err);

+ 8 - 8
packages/app/src/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -109,20 +109,20 @@ class ManageGlobalNotification extends React.Component {
         <div className="my-3">
           <a href="/admin/notification#global-notification" className="btn btn-outline-secondary">
             <i className="icon-fw ti-arrow-left" aria-hidden="true"></i>
-            {t('notification_setting.back_to_list')}
+            {t('notification_settings.back_to_list')}
           </a>
         </div>
 
 
         <div className="row">
           <div className="form-box col-md-12">
-            <h2 className="border-bottom mb-5">{t('notification_setting.notification_detail')}</h2>
+            <h2 className="border-bottom mb-5">{t('notification_settings.notification_detail')}</h2>
           </div>
 
           <div className="col-sm-4">
-            <h3 htmlFor="triggerPath">{t('notification_setting.trigger_path')}
+            <h3 htmlFor="triggerPath">{t('notification_settings.trigger_path')}
               {/* eslint-disable-next-line react/no-danger */}
-              <small dangerouslySetInnerHTML={{ __html: t('notification_setting.trigger_path_help', '<code>*</code>') }} />
+              <small dangerouslySetInnerHTML={{ __html: t('notification_settings.trigger_path_help', '<code>*</code>') }} />
             </h3>
             <div className="form-group">
               <input
@@ -135,7 +135,7 @@ class ManageGlobalNotification extends React.Component {
               />
             </div>
 
-            <h3>{t('notification_setting.notify_to')}</h3>
+            <h3>{t('notification_settings.notify_to')}</h3>
             <div className="form-group form-inline">
               <div className="custom-control custom-radio">
                 <input
@@ -190,7 +190,7 @@ class ManageGlobalNotification extends React.Component {
                     {/* eslint-disable-next-line react/no-danger */}
                     {!isMailerSetup && <span className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('admin:mailer_setup_required') }} />}
                     <b>Hint: </b>
-                    <a href="https://ifttt.com/create" target="blank">{t('notification_setting.email.ifttt_link')}
+                    <a href="https://ifttt.com/create" target="blank">{t('notification_settings.email.ifttt_link')}
                       <i className="icon-share-alt" />
                     </a>
                   </p>
@@ -214,7 +214,7 @@ class ManageGlobalNotification extends React.Component {
                   </div>
                   <p className="p-2">
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
                   </p>
                 </>
               )}
@@ -222,7 +222,7 @@ class ManageGlobalNotification extends React.Component {
 
           <div className="offset-1 col-sm-5">
             <div className="form-group">
-              <h3>{t('notification_setting.trigger_events')}</h3>
+              <h3>{t('notification_settings.trigger_events')}</h3>
               <div className="my-1">
                 <TriggerEventCheckBox
                   checkbox="success"

+ 2 - 2
packages/app/src/components/Admin/Notification/NotificationDeleteModal.jsx

@@ -17,10 +17,10 @@ class NotificationDeleteModal extends React.PureComponent {
         </ModalHeader>
         <ModalBody>
           <p>
-            {t('notification_setting.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
+            {t('notification_settings.delete_notification_pattern_desc1', { path: notificationForConfiguration.triggerPath })}
           </p>
           <p className="text-danger">
-            {t('notification_setting.delete_notification_pattern_desc2')}
+            {t('notification_settings.delete_notification_pattern_desc2')}
           </p>
         </ModalBody>
         <ModalFooter>

+ 8 - 8
packages/app/src/components/Admin/Notification/NotificationSetting.jsx

@@ -28,11 +28,11 @@ let retrieveErrors = null;
 
 // eslint-disable-next-line react/prop-types
 const Badge = ({ isEnabled }) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   return isEnabled
-    ? <span className="badge badge-success">{t('admin:external_notification.enabled')}</span>
-    : <span className="badge badge-secondary">{t('admin:external_notification.disabled')}</span>;
+    ? <span className="badge badge-success">{t('external_notification.enabled')}</span>
+    : <span className="badge badge-secondary">{t('external_notification.disabled')}</span>;
 };
 
 const SkeltonListItem = () => (
@@ -54,12 +54,12 @@ const SlackIntegrationListItem = ({ isEnabled, currentBotType }) => {
     <li data-testid="slack-integration-list-item" className="list-group-item">
       <h4>
         <Badge isEnabled={isEnabled} />
-        <a href="/admin/slack-integration" className="ml-2">{t('slack_integration')}</a>
+        <a href="/admin/slack-integration" className="ml-2">{t('slack_integration.slack_integration')}</a>
       </h4>
       { isCautionVisible && (
         <ul className="mt-2 pl-4">
           {/* eslint-disable-next-line react/no-danger */}
-          <li dangerouslySetInnerHTML={{ __html: t('admin:external_notification.caution_enabled') }} />
+          <li dangerouslySetInnerHTML={{ __html: t('external_notification.caution_enabled') }} />
         </ul>
       ) }
     </li>
@@ -80,7 +80,7 @@ const LegacySlackIntegrationListItem = ({ isEnabled }) => {
         <ul className="mt-2 pl-4">
           <li>
             {/* eslint-disable-next-line react/no-danger */}
-            <span className="text-danger" dangerouslySetInnerHTML={{ __html: t('admin:slack_integration_legacy.alert_deplicated') }}></span>
+            <span className="text-danger" dangerouslySetInnerHTML={{ __html: t('slack_integration_legacy.alert_deplicated') }}></span>
           </li>
         </ul>
       ) }
@@ -142,7 +142,7 @@ function NotificationSetting(props) {
 
   return (
     <div data-testid="admin-notification">
-      <h2 className="admin-setting-header">{t('admin:external_notification.header_status')}</h2>
+      <h2 className="admin-setting-header">{t('external_notification.header_status')}</h2>
       <ul className="list-group">
         { !isMounted && <SkeltonListItem />}
         { isMounted && (
@@ -155,7 +155,7 @@ function NotificationSetting(props) {
       </ul>
 
 
-      <h2 className="admin-setting-header mt-5">{t('Notification Settings')}</h2>
+      <h2 className="admin-setting-header mt-5">{t('notification_settings.notification_settings')}</h2>
 
       <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} hideBorderBottom />
 

+ 1 - 1
packages/app/src/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -17,7 +17,7 @@ const TriggerEventCheckBox = (props) => {
       />
       <label className="custom-control-label" htmlFor={`trigger-event-${props.event}`}>
         {props.children}{' '}
-        {t(`notification_setting.event_${props.event}`)}
+        {t(`notification_settings.event_${props.event}`)}
       </label>
     </div>
   );

+ 8 - 8
packages/app/src/components/Admin/Notification/UserTriggerNotification.jsx

@@ -54,7 +54,7 @@ class UserTriggerNotification extends React.Component {
 
     try {
       await adminNotificationContainer.addNotificationPattern(this.state.pathPattern, this.state.channel);
-      toastSuccess(t('notification_setting.add_notification_pattern'));
+      toastSuccess(t('notification_settings.add_notification_pattern'));
       this.setState({ pathPattern: '', channel: '' });
     }
     catch (err) {
@@ -68,7 +68,7 @@ class UserTriggerNotification extends React.Component {
 
     try {
       const deletedNotificaton = await adminNotificationContainer.deleteUserTriggerNotificationPattern(notificationIdForDelete);
-      toastSuccess(t('notification_setting.delete_notification_pattern', { path: deletedNotificaton.pathPattern }));
+      toastSuccess(t('notification_settings.delete_notification_pattern', { path: deletedNotificaton.pathPattern }));
     }
     catch (err) {
       toastError(err);
@@ -82,13 +82,13 @@ class UserTriggerNotification extends React.Component {
 
     return (
       <React.Fragment>
-        <h2 className="border-bottom my-4">{t('notification_setting.user_trigger_notification_header')}</h2>
+        <h2 className="border-bottom my-4">{t('notification_settings.user_trigger_notification_header')}</h2>
 
         <table className="table table-bordered">
           <thead>
             <tr>
-              <th>{t('notification_setting.pattern')}</th>
-              <th>{t('notification_setting.channel')}</th>
+              <th>{t('notification_settings.pattern')}</th>
+              <th>{t('notification_settings.channel')}</th>
               <th />
             </tr>
           </thead>
@@ -105,7 +105,7 @@ class UserTriggerNotification extends React.Component {
                 />
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_setting.pattern_desc') }} />
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.pattern_desc') }} />
                 </p>
               </td>
 
@@ -125,7 +125,7 @@ class UserTriggerNotification extends React.Component {
                 </div>
                 <p className="p-2 mb-0">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <span dangerouslySetInnerHTML={{ __html: t('notification_setting.channel_desc') }} />
+                  <span dangerouslySetInnerHTML={{ __html: t('notification_settings.channel_desc') }} />
                 </p>
               </td>
               <td>
@@ -152,7 +152,7 @@ UserTriggerNotification.propTypes = {
 };
 
 const UserTriggerNotificationWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   return <UserTriggerNotification t={t} {...props} />;
 };

+ 9 - 9
packages/app/src/components/Admin/Security/BasicSecuritySettingContents.jsx

@@ -24,7 +24,7 @@ class BasicSecurityManagementContents extends React.Component {
     try {
       await adminBasicSecurityContainer.updateBasicSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.Basic.updated_basic'));
+      toastSuccess(t('security_settings.Basic.updated_basic'));
     }
     catch (err) {
       toastError(err);
@@ -39,7 +39,7 @@ class BasicSecurityManagementContents extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          { t('security_setting.Basic.name') }
+          { t('security_settings.Basic.name') }
         </h2>
 
         {adminBasicSecurityContainer.state.retrieveError != null && (
@@ -59,17 +59,17 @@ class BasicSecurityManagementContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsBasicEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isBasicEnabled">
-                { t('security_setting.Basic.enable_basic') }
+                { t('security_settings.Basic.enable_basic') }
               </label>
             </div>
             <p className="form-text text-muted">
               <small>
-                <span dangerouslySetInnerHTML={{ __html: t('security_setting.Basic.desc_1') }} /><br />
-                { t('security_setting.Basic.desc_2')}
+                <span dangerouslySetInnerHTML={{ __html: t('security_settings.Basic.desc_1') }} /><br />
+                { t('security_settings.Basic.desc_2')}
               </small>
             </p>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
-            && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+            && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -88,11 +88,11 @@ class BasicSecurityManagementContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByEmail-basic"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical', 'username') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical', 'username') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn', 'username') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn', 'username') }} />
                 </p>
               </div>
             </div>
@@ -126,7 +126,7 @@ BasicSecurityManagementContents.propTypes = {
 };
 
 const BasicSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   return <BasicSecurityManagementContents t={t} {...props} />;
 };

+ 1 - 1
packages/app/src/components/Admin/Security/FacebookSecuritySetting.jsx

@@ -15,7 +15,7 @@ class FacebookSecurityManagement extends React.Component {
     return (
       <>
         <h2 className="alert-anchor border-bottom">
-          Facebook OAuth { t('security_setting.configuration') }
+          Facebook OAuth { t('admin:security_settings.configuration') }
         </h2>
 
         <p className="well">(TBD)</p>

+ 19 - 19
packages/app/src/components/Admin/Security/GitHubSecuritySettingContents.jsx

@@ -28,7 +28,7 @@ class GitHubSecurityManagementContents extends React.Component {
     try {
       await adminGitHubSecurityContainer.updateGitHubSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.OAuth.GitHub.updated_github'));
+      toastSuccess(t('security_settings.OAuth.GitHub.updated_github'));
     }
     catch (err) {
       toastError(err);
@@ -47,7 +47,7 @@ class GitHubSecurityManagementContents extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.GitHub.name')}
+          {t('security_settings.OAuth.GitHub.name')}
         </h2>
 
         {adminGitHubSecurityContainer.state.retrieveError != null && (
@@ -67,16 +67,16 @@ class GitHubSecurityManagementContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsGitHubOAuthEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isGitHubEnabled">
-                {t('security_setting.OAuth.GitHub.enable_github')}
+                {t('security_settings.OAuth.GitHub.enable_github')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
         <div className="row mb-5">
-          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_settings.callback_URL')}</label>
           <div className="col-12 col-md-6">
             <input
               className="form-control"
@@ -84,13 +84,13 @@ class GitHubSecurityManagementContents extends React.Component {
               value={gitHubCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -101,10 +101,10 @@ class GitHubSecurityManagementContents extends React.Component {
         {isGitHubEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row mb-5">
-              <label htmlFor="githubClientId" className="col-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <label htmlFor="githubClientId" className="col-3 text-right py-2">{t('security_settings.clientID')}</label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -114,13 +114,13 @@ class GitHubSecurityManagementContents extends React.Component {
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientId(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_ID' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="githubClientSecret" className="col-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <label htmlFor="githubClientSecret" className="col-3 text-right py-2">{t('security_settings.client_secret')}</label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -130,7 +130,7 @@ class GitHubSecurityManagementContents extends React.Component {
                   onChange={e => adminGitHubSecurityContainer.changeGitHubClientSecret(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GITHUB_CLIENT_SECRET' }) }} />
                 </p>
               </div>
             </div>
@@ -148,11 +148,11 @@ class GitHubSecurityManagementContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByUserNameGitHub"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -173,13 +173,13 @@ class GitHubSecurityManagementContents extends React.Component {
         <div style={{ minHeight: '300px' }}>
           <h4>
             <i className="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForGitHubOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.github')}</a>
+            <a href="#collapseHelpForGitHubOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.github')}</a>
           </h4>
           <ol id="collapseHelpForGitHubOauth" className="collapse">
             {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.GitHub.register_3') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_1', { link: '<a href="https://github.com/settings/developers" target=_blank>GitHub Developer Settings</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_2', { url: gitHubCallbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.GitHub.register_3') }} />
           </ol>
         </div>
 
@@ -192,7 +192,7 @@ class GitHubSecurityManagementContents extends React.Component {
 }
 
 const GitHubSecurityManagementContentsFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: siteUrl } = useSiteUrl();
   return <GitHubSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };

+ 21 - 21
packages/app/src/components/Admin/Security/GoogleSecuritySettingContents.jsx

@@ -26,7 +26,7 @@ class GoogleSecurityManagementContents extends React.Component {
     try {
       await adminGoogleSecurityContainer.updateGoogleSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.OAuth.Google.updated_google'));
+      toastSuccess(t('security_settings.OAuth.Google.updated_google'));
     }
     catch (err) {
       toastError(err);
@@ -45,7 +45,7 @@ class GoogleSecurityManagementContents extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.Google.name')}
+          {t('security_settings.OAuth.Google.name')}
         </h2>
 
         {adminGoogleSecurityContainer.state.retrieveError != null && (
@@ -65,16 +65,16 @@ class GoogleSecurityManagementContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsGoogleOAuthEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isGoogleEnabled">
-                {t('security_setting.OAuth.Google.enable_google')}
+                {t('security_settings.OAuth.Google.enable_google')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
         <div className="row mb-5">
-          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <label className="col-12 col-md-3 text-left text-md-right py-2">{t('security_settings.callback_URL')}</label>
           <div className="col-12 col-md-6">
             <input
               className="form-control"
@@ -82,13 +82,13 @@ class GoogleSecurityManagementContents extends React.Component {
               value={googleCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -99,10 +99,10 @@ class GoogleSecurityManagementContents extends React.Component {
         {isGoogleEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row mb-5">
-              <label htmlFor="googleClientId" className="col-3 text-right py-2">{t('security_setting.clientID')}</label>
+              <label htmlFor="googleClientId" className="col-3 text-right py-2">{t('security_settings.clientID')}</label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -112,13 +112,13 @@ class GoogleSecurityManagementContents extends React.Component {
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientId(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_ID' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="googleClientSecret" className="col-3 text-right py-2">{t('security_setting.client_secret')}</label>
+              <label htmlFor="googleClientSecret" className="col-3 text-right py-2">{t('security_settings.client_secret')}</label>
               <div className="col-6">
                 <input
                   className="form-control"
@@ -128,7 +128,7 @@ class GoogleSecurityManagementContents extends React.Component {
                   onChange={e => adminGoogleSecurityContainer.changeGoogleClientSecret(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_GOOGLE_CLIENT_SECRET' }) }} />
                 </p>
               </div>
             </div>
@@ -146,11 +146,11 @@ class GoogleSecurityManagementContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByUserNameGoogle"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -176,15 +176,15 @@ class GoogleSecurityManagementContents extends React.Component {
         <div style={{ minHeight: '300px' }}>
           <h4>
             <i className="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForGoogleOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.google')}</a>
+            <a href="#collapseHelpForGoogleOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.google')}</a>
           </h4>
           <ol id="collapseHelpForGoogleOauth" className="collapse">
             {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_2') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_3') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Google.register_5') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_1', { link: '<a href="https://console.cloud.google.com/apis/credentials" target=_blank>Google Cloud Platform API Manager</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_2') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_3') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_4', { url: googleCallbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Google.register_5') }} />
           </ol>
         </div>
 
@@ -197,7 +197,7 @@ class GoogleSecurityManagementContents extends React.Component {
 }
 
 const GoogleSecurityManagementContentsFc = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: siteUrl } = useSiteUrl();
   return <GoogleSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };

+ 47 - 47
packages/app/src/components/Admin/Security/LdapSecuritySettingContents.jsx

@@ -32,7 +32,7 @@ class LdapSecuritySettingContents extends React.Component {
     try {
       await adminLdapSecurityContainer.updateLdapSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.ldap.updated_ldap'));
+      toastSuccess(t('security_settings.ldap.updated_ldap'));
     }
     catch (err) {
       toastError(err);
@@ -69,11 +69,11 @@ class LdapSecuritySettingContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsLdapEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isLdapEnabled">
-                {t('security_setting.ldap.enable_ldap')}
+                {t('security_settings.ldap.enable_ldap')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -81,7 +81,7 @@ class LdapSecuritySettingContents extends React.Component {
         {isLdapEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="form-group row">
               <label htmlFor="serverUrl" className="text-left text-md-right col-md-3 col-form-label">
@@ -99,16 +99,16 @@ class LdapSecuritySettingContents extends React.Component {
                   <p
                     className="form-text text-muted"
                     // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.server_url_detail') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.server_url_detail') }}
                   />
-                  {t('security_setting.example')}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
+                  {t('security_settings.example')}: <code>ldaps://ldap.company.com/ou=people,dc=company,dc=com</code>
                 </small>
               </div>
             </div>
 
             <div className="form-group row">
               <label className="text-left text-md-right col-md-3 col-form-label">
-                <strong>{t('security_setting.ldap.bind_mode')}</strong>
+                <strong>{t('security_settings.ldap.bind_mode')}</strong>
               </label>
               <div className="col-md-6">
                 <div className="dropdown">
@@ -121,15 +121,15 @@ class LdapSecuritySettingContents extends React.Component {
                     aria-expanded="true"
                   >
                     {adminLdapSecurityContainer.state.isUserBind
-                      ? <span className="pull-left">{t('security_setting.ldap.bind_user')}</span>
-                      : <span className="pull-left">{t('security_setting.ldap.bind_manager')}</span>}
+                      ? <span className="pull-left">{t('security_settings.ldap.bind_user')}</span>
+                      : <span className="pull-left">{t('security_settings.ldap.bind_manager')}</span>}
                   </button>
                   <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
                     <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(true) }}>
-                      {t('security_setting.ldap.bind_user')}
+                      {t('security_settings.ldap.bind_user')}
                     </button>
                     <button className="dropdown-item" type="button" onClick={() => { adminLdapSecurityContainer.changeLdapBindMode(false) }}>
-                      {t('security_setting.ldap.bind_manager')}
+                      {t('security_settings.ldap.bind_manager')}
                     </button>
                   </div>
                 </div>
@@ -151,20 +151,20 @@ class LdapSecuritySettingContents extends React.Component {
                 {(adminLdapSecurityContainer.state.isUserBind === true) ? (
                   <p className="form-text text-muted passport-ldap-userbind">
                     <small>
-                      {t('security_setting.ldap.bind_DN_user_detail1')}<br />
+                      {t('security_settings.ldap.bind_DN_user_detail1')}<br />
                       {/* eslint-disable-next-line react/no-danger */}
-                      <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.bind_DN_user_detail2') }} /><br />
-                      {t('security_setting.example')}1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
-                      {t('security_setting.example')}2: <code>{'{{ username }}'}@domain.com</code>
+                      <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.bind_DN_user_detail2') }} /><br />
+                      {t('security_settings.example')}1: <code>uid={'{{ username }}'},dc=domain,dc=com</code><br />
+                      {t('security_settings.example')}2: <code>{'{{ username }}'}@domain.com</code>
                     </small>
                   </p>
                 )
                   : (
                     <p className="form-text text-muted passport-ldap-managerbind">
                       <small>
-                        {t('security_setting.ldap.bind_DN_manager_detail')}<br />
-                        {t('security_setting.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
-                        {t('security_setting.example')}2: <code>admin@domain.com</code>
+                        {t('security_settings.ldap.bind_DN_manager_detail')}<br />
+                        {t('security_settings.example')}1: <code>uid=admin,dc=domain,dc=com</code><br />
+                        {t('security_settings.example')}2: <code>admin@domain.com</code>
                       </small>
                     </p>
                   )}
@@ -173,13 +173,13 @@ class LdapSecuritySettingContents extends React.Component {
 
             <div className="form-group row">
               <div htmlFor="bindDNPassword" className="text-left text-md-right col-md-3 col-form-label">
-                <strong>{t('security_setting.ldap.bind_DN_password')}</strong>
+                <strong>{t('security_settings.ldap.bind_DN_password')}</strong>
               </div>
               <div className="col-md-6">
                 {(adminLdapSecurityContainer.state.isUserBind) ? (
                   <p className="well card passport-ldap-userbind">
                     <small>
-                      {t('security_setting.ldap.bind_DN_password_user_detail')}
+                      {t('security_settings.ldap.bind_DN_password_user_detail')}
                     </small>
                   </p>
                 )
@@ -187,7 +187,7 @@ class LdapSecuritySettingContents extends React.Component {
                     <>
                       <p className="well card passport-ldap-managerbind">
                         <small>
-                          {t('security_setting.ldap.bind_DN_password_manager_detail')}
+                          {t('security_settings.ldap.bind_DN_password_manager_detail')}
                         </small>
                       </p>
                       <input
@@ -204,7 +204,7 @@ class LdapSecuritySettingContents extends React.Component {
 
             <div className="form-group row">
               <label className="text-left text-md-right col-md-3 col-form-label">
-                <strong>{t('security_setting.ldap.search_filter')}</strong>
+                <strong>{t('security_settings.ldap.search_filter')}</strong>
               </label>
               <div className="col-md-6">
                 <input
@@ -216,18 +216,18 @@ class LdapSecuritySettingContents extends React.Component {
                 />
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_setting.ldap.search_filter_detail1')}<br />
+                    {t('security_settings.ldap.search_filter_detail1')}<br />
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail2') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail2') }} /><br />
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.search_filter_detail3') }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.search_filter_detail3') }} />
                   </small>
                 </p>
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_setting.example')}1 - {t('security_setting.ldap.search_filter_example1')}:
+                    {t('security_settings.example')}1 - {t('security_settings.ldap.search_filter_example1')}:
                     <code>(|(uid={'{{username}}'})(mail={'{{username}}'}))</code><br />
-                    {t('security_setting.example')}2 - {t('security_setting.ldap.search_filter_example2')}:
+                    {t('security_settings.example')}2 - {t('security_settings.ldap.search_filter_example2')}:
                     <code>(sAMAccountName={'{{username}}'})</code>
                   </small>
                 </p>
@@ -235,7 +235,7 @@ class LdapSecuritySettingContents extends React.Component {
             </div>
 
             <h3 className="alert-anchor border-bottom">
-              Attribute Mapping ({t('security_setting.optional')})
+              Attribute Mapping ({t('security_settings.optional')})
             </h3>
 
             <div className="form-group row">
@@ -253,7 +253,7 @@ class LdapSecuritySettingContents extends React.Component {
                 />
                 <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.username_detail') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.username_detail') }} />
                 </p>
               </div>
             </div>
@@ -272,12 +272,12 @@ class LdapSecuritySettingContents extends React.Component {
                     className="custom-control-label"
                     htmlFor="isSameUsernameTreatedAsIdenticalUser"
                     // eslint-disable-next-line react/no-danger
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -297,7 +297,7 @@ class LdapSecuritySettingContents extends React.Component {
                 />
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_setting.ldap.mail_detail')}
+                    {t('security_settings.ldap.mail_detail')}
                   </small>
                 </p>
               </div>
@@ -317,7 +317,7 @@ class LdapSecuritySettingContents extends React.Component {
                 />
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_setting.ldap.name_detail')}
+                    {t('security_settings.ldap.name_detail')}
                   </small>
                 </p>
               </div>
@@ -325,12 +325,12 @@ class LdapSecuritySettingContents extends React.Component {
 
 
             <h3 className="alert-anchor border-bottom">
-              {t('security_setting.ldap.group_search_filter')} ({t('security_setting.optional')})
+              {t('security_settings.ldap.group_search_filter')} ({t('security_settings.optional')})
             </h3>
 
             <div className="form-group row">
               <label className="text-left text-md-right col-md-3 col-form-label">
-                <strong htmlFor="groupSearchBase">{t('security_setting.ldap.group_search_base_DN')}</strong>
+                <strong htmlFor="groupSearchBase">{t('security_settings.ldap.group_search_base_DN')}</strong>
               </label>
               <div className="col-md-6">
                 <input
@@ -343,8 +343,8 @@ class LdapSecuritySettingContents extends React.Component {
                 <p className="form-text text-muted">
                   <small>
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_base_DN_detail') }} /><br />
-                    {t('security_setting.example')}: <code>ou=groups,dc=domain,dc=com</code>
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_base_DN_detail') }} /><br />
+                    {t('security_settings.example')}: <code>ou=groups,dc=domain,dc=com</code>
                   </small>
                 </p>
               </div>
@@ -352,7 +352,7 @@ class LdapSecuritySettingContents extends React.Component {
 
             <div className="form-group row">
               <label className="text-left text-md-right col-md-3 col-form-label">
-                <strong htmlFor="groupSearchFilter">{t('security_setting.ldap.group_search_filter')}</strong>
+                <strong htmlFor="groupSearchFilter">{t('security_settings.ldap.group_search_filter')}</strong>
               </label>
               <div className="col-md-6">
                 <input
@@ -365,17 +365,17 @@ class LdapSecuritySettingContents extends React.Component {
                 <p className="form-text text-muted">
                   <small>
                     {/* eslint-disable react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail1') }} /><br />
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail2') }} /><br />
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail3') }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail1') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail2') }} /><br />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail3') }} />
                     {/* eslint-enable react/no-danger */}
                   </small>
                 </p>
                 <p className="form-text text-muted">
                   <small>
-                    {t('security_setting.example')}:
+                    {t('security_settings.example')}:
                     {/* eslint-disable-next-line react/no-danger */}
-                    <span dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_filter_detail4') }} />
+                    <span dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_filter_detail4') }} />
                   </small>
                 </p>
               </div>
@@ -383,7 +383,7 @@ class LdapSecuritySettingContents extends React.Component {
 
             <div className="form-group row">
               <label className="text-left text-md-right col-md-3 col-form-label">
-                <strong htmlFor="groupDnProperty">{t('security_setting.ldap.group_search_user_DN_property')}</strong>
+                <strong htmlFor="groupDnProperty">{t('security_settings.ldap.group_search_user_DN_property')}</strong>
               </label>
               <div className="col-md-6">
                 <input
@@ -396,7 +396,7 @@ class LdapSecuritySettingContents extends React.Component {
                 />
                 <p className="form-text text-muted">
                   {/* eslint-disable-next-line react/no-danger */}
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.ldap.group_search_user_DN_property_detail') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.ldap.group_search_user_DN_property_detail') }} />
                 </p>
               </div>
             </div>
@@ -414,7 +414,7 @@ class LdapSecuritySettingContents extends React.Component {
                   type="button"
                   className="btn btn-outline-secondary ml-2"
                   onClick={this.openLdapAuthTestModal}
-                >{t('security_setting.ldap.test_config')}
+                >{t('security_settings.ldap.test_config')}
                 </button>
               </div>
             </div>
@@ -438,7 +438,7 @@ LdapSecuritySettingContents.propTypes = {
 };
 
 const LdapSecuritySettingContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <LdapSecuritySettingContents t={t} {...props} />;
 };
 

+ 27 - 27
packages/app/src/components/Admin/Security/LocalSecuritySettingContents.jsx

@@ -24,7 +24,7 @@ class LocalSecuritySettingContents extends React.Component {
     try {
       await adminLocalSecurityContainer.updateLocalSecuritySetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.updated_general_security_setting'));
+      toastSuccess(t('security_settings.updated_general_security_setting'));
     }
     catch (err) {
       toastError(err);
@@ -50,13 +50,13 @@ class LocalSecuritySettingContents extends React.Component {
             </p>
           </div>
         )}
-        <h2 className="alert-anchor border-bottom">{t('security_setting.Local.name')}</h2>
+        <h2 className="alert-anchor border-bottom">{t('security_settings.Local.name')}</h2>
 
         {!isMailerSetup && (
           <div className="row">
             <div className="col-12">
               <div className="alert alert-danger">
-                <span>{t('security_setting.Local.need_complete_mail_setting_warning')}</span>
+                <span>{t('security_settings.Local.need_complete_mail_setting_warning')}</span>
                 <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('admin:app_setting.mail_settings')}</a>
               </div>
             </div>
@@ -68,7 +68,7 @@ class LocalSecuritySettingContents extends React.Component {
             className="alert alert-info"
             // eslint-disable-next-line max-len
             dangerouslySetInnerHTML={{
-              __html: t('security_setting.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }),
+              __html: t('security_settings.Local.note for the only env option', { env: 'LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }),
             }}
           />
         )}
@@ -85,18 +85,18 @@ class LocalSecuritySettingContents extends React.Component {
                 disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
               />
               <label className="custom-control-label" htmlFor="isLocalEnabled">
-                {t('security_setting.Local.enable_local')}
+                {t('security_settings.Local.enable_local')}
               </label>
             </div>
             {!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled && (
-              <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>
+              <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>
             )}
           </div>
         </div>
 
         {isLocalEnabled && (
           <>
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row">
               <div className="col-12 col-md-3 text-left text-md-right py-2">
@@ -112,9 +112,9 @@ class LocalSecuritySettingContents extends React.Component {
                     aria-haspopup="true"
                     aria-expanded="true"
                   >
-                    {registrationMode === 'Open' && t('security_setting.registration_mode.open')}
-                    {registrationMode === 'Restricted' && t('security_setting.registration_mode.restricted')}
-                    {registrationMode === 'Closed' && t('security_setting.registration_mode.closed')}
+                    {registrationMode === 'Open' && t('security_settings.registration_mode.open')}
+                    {registrationMode === 'Restricted' && t('security_settings.registration_mode.restricted')}
+                    {registrationMode === 'Closed' && t('security_settings.registration_mode.closed')}
                   </button>
                   <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
                     <button
@@ -124,7 +124,7 @@ class LocalSecuritySettingContents extends React.Component {
                         adminLocalSecurityContainer.changeRegistrationMode('Open');
                       }}
                     >
-                      {t('security_setting.registration_mode.open')}
+                      {t('security_settings.registration_mode.open')}
                     </button>
                     <button
                       className="dropdown-item"
@@ -133,7 +133,7 @@ class LocalSecuritySettingContents extends React.Component {
                         adminLocalSecurityContainer.changeRegistrationMode('Restricted');
                       }}
                     >
-                      {t('security_setting.registration_mode.restricted')}
+                      {t('security_settings.registration_mode.restricted')}
                     </button>
                     <button
                       className="dropdown-item"
@@ -142,12 +142,12 @@ class LocalSecuritySettingContents extends React.Component {
                         adminLocalSecurityContainer.changeRegistrationMode('Closed');
                       }}
                     >
-                      {t('security_setting.registration_mode.closed')}
+                      {t('security_settings.registration_mode.closed')}
                     </button>
                   </div>
                 </div>
 
-                <p className="form-text text-muted small">{t('security_setting.Register limitation desc')}</p>
+                <p className="form-text text-muted small">{t('security_settings.Register limitation desc')}</p>
               </div>
             </div>
             <div className="row">
@@ -163,19 +163,19 @@ class LocalSecuritySettingContents extends React.Component {
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
                 />
                 <p className="form-text text-muted small">
-                  {t('security_setting.restrict_emails')}
+                  {t('security_settings.restrict_emails')}
                   <br />
-                  {t('security_setting.for_example')}
+                  {t('security_settings.for_example')}
                   <code>@growi.org</code>
-                  {t('security_setting.in_this_case')}
+                  {t('security_settings.in_this_case')}
                   <br />
-                  {t('security_setting.insert_single')}
+                  {t('security_settings.insert_single')}
                 </p>
               </div>
             </div>
 
             <div className="row">
-              <label className="col-12 col-md-3 text-left text-md-right  col-form-label">{t('security_setting.Local.password_reset_by_users')}</label>
+              <label className="col-12 col-md-3 text-left text-md-right  col-form-label">{t('security_settings.Local.password_reset_by_users')}</label>
               <div className="col-12 col-md-6">
                 <div className="custom-control custom-switch custom-checkbox-success">
                   <input
@@ -186,17 +186,17 @@ class LocalSecuritySettingContents extends React.Component {
                     onChange={() => adminLocalSecurityContainer.switchIsPasswordResetEnabled()}
                   />
                   <label className="custom-control-label" htmlFor="isPasswordResetEnabled">
-                    {t('security_setting.Local.enable_password_reset_by_users')}
+                    {t('security_settings.Local.enable_password_reset_by_users')}
                   </label>
                 </div>
                 <p className="form-text text-muted small">
-                  {t('security_setting.Local.password_reset_desc')}
+                  {t('security_settings.Local.password_reset_desc')}
                 </p>
               </div>
             </div>
 
             <div className="row">
-              <label className="col-12 col-md-3 text-left text-md-right  col-form-label">{t('security_setting.Local.email_authentication')}</label>
+              <label className="col-12 col-md-3 text-left text-md-right  col-form-label">{t('security_settings.Local.email_authentication')}</label>
               <div className="col-12 col-md-6">
                 <div className="custom-control custom-switch custom-checkbox-success">
                   <input
@@ -207,17 +207,17 @@ class LocalSecuritySettingContents extends React.Component {
                     onChange={() => adminLocalSecurityContainer.switchIsEmailAuthenticationEnabled()}
                   />
                   <label className="custom-control-label" htmlFor="isEmailAuthenticationEnabled">
-                    {t('security_setting.Local.enable_email_authentication')}
+                    {t('security_settings.Local.enable_email_authentication')}
                   </label>
                 </div>
                 {!isMailerSetup && (
                   <div className="alert alert-warning p-1 my-1 small d-inline-block">
-                    <span>{t('security_setting.Local.please_enable_mailer')}</span>
-                    <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('admin:app_setting.mail_settings')}</a>
+                    <span>{t('security_settings.Local.please_enable_mailer')}</span>
+                    <a href="/admin/app#mail-settings"> <i className="fa fa-link"></i> {t('app_setting.mail_settings')}</a>
                   </div>
                 )}
                 <p className="form-text text-muted small">
-                  {t('security_setting.Local.enable_email_authentication_desc')}
+                  {t('security_settings.Local.enable_email_authentication_desc')}
                 </p>
               </div>
             </div>
@@ -249,7 +249,7 @@ LocalSecuritySettingContents.propTypes = {
 };
 
 const LocalSecuritySettingContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: isMailerSetup } = useIsMailerSetup();
   return <LocalSecuritySettingContents t={t} {...props} isMailerSetup={isMailerSetup ?? false} />;
 };

+ 48 - 48
packages/app/src/components/Admin/Security/OidcSecuritySettingContents.jsx

@@ -27,7 +27,7 @@ class OidcSecurityManagementContents extends React.Component {
     try {
       await adminOidcSecurityContainer.updateOidcSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.OAuth.OIDC.updated_oidc'));
+      toastSuccess(t('security_settings.OAuth.OIDC.updated_oidc'));
     }
     catch (err) {
       toastError(err);
@@ -45,7 +45,7 @@ class OidcSecurityManagementContents extends React.Component {
 
       <>
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.OIDC.name')}
+          {t('security_settings.OAuth.OIDC.name')}
         </h2>
 
         <div className="row mb-5 form-group">
@@ -59,16 +59,16 @@ class OidcSecurityManagementContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsOidcEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isOidcEnabled">
-                {t('security_setting.OAuth.enable_oidc')}
+                {t('security_settings.OAuth.enable_oidc')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
         <div className="row mb-5 form-group">
-          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
           <div className="col-md-6">
             <input
               className="form-control"
@@ -76,13 +76,13 @@ class OidcSecurityManagementContents extends React.Component {
               value={oidcCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -92,10 +92,10 @@ class OidcSecurityManagementContents extends React.Component {
         {isOidcEnabled && (
           <>
 
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcProviderName" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.providerName')}</label>
+              <label htmlFor="oidcProviderName" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.providerName')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -108,7 +108,7 @@ class OidcSecurityManagementContents extends React.Component {
             </div>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcIssuerHost" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.issuerHost')}</label>
+              <label htmlFor="oidcIssuerHost" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.issuerHost')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -118,13 +118,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcIssuerHost(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_ISSUER_HOST' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcClientId" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.clientID')}</label>
+              <label htmlFor="oidcClientId" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.clientID')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -134,13 +134,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcClientId(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_ID' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcClientSecret" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.client_secret')}</label>
+              <label htmlFor="oidcClientSecret" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.client_secret')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -150,14 +150,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcClientSecret(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_OIDC_CLIENT_SECRET' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcAuthorizationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.authorization_endpoint')}
+                {t('security_settings.authorization_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -168,13 +168,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcTokenEndpoint" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.token_endpoint')}</label>
+              <label htmlFor="oidcTokenEndpoint" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.token_endpoint')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -184,14 +184,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcRevocationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.revocation_endpoint')}
+                {t('security_settings.revocation_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -202,14 +202,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcIntrospectionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.introspection_endpoint')}
+                {t('security_settings.introspection_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -220,14 +220,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcUserInfoEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.userinfo_endpoint')}
+                {t('security_settings.userinfo_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -238,14 +238,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcEndSessionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.end_session_endpoint')}
+                {t('security_settings.end_session_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -256,14 +256,14 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
               <label htmlFor="oidcRegistrationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
-                {t('security_setting.registration_endpoint')}
+                {t('security_settings.registration_endpoint')}
               </label>
               <div className="col-md-6">
                 <input
@@ -274,13 +274,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
-              <label htmlFor="oidcJWKSUri" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.jwks_uri')}</label>
+              <label htmlFor="oidcJWKSUri" className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.jwks_uri')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -290,13 +290,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.Use discovered URL if empty') }} />
                 </p>
               </div>
             </div>
 
             <h3 className="alert-anchor border-bottom">
-              Attribute Mapping ({t('security_setting.optional')})
+              Attribute Mapping ({t('security_settings.optional')})
             </h3>
 
             <div className="row mb-5 form-group">
@@ -310,7 +310,7 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapId(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.id_detail') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.id_detail') }} />
                 </p>
               </div>
             </div>
@@ -326,7 +326,7 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapUserName(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.username_detail') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.username_detail') }} />
                 </p>
               </div>
             </div>
@@ -342,7 +342,7 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapName(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.name_detail') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.name_detail') }} />
                 </p>
               </div>
             </div>
@@ -358,13 +358,13 @@ class OidcSecurityManagementContents extends React.Component {
                   onChange={e => adminOidcSecurityContainer.changeOidcAttrMapEmail(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.OIDC.mapping_detail', { target: t('Email') }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5 form-group">
-              <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+              <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -372,13 +372,13 @@ class OidcSecurityManagementContents extends React.Component {
                   defaultValue={oidcCallbackUrl}
                   readOnly
                 />
-                <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+                <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
                 {(siteUrl == null || siteUrl === '') && (
                   <div className="alert alert-danger">
                     <i
                       className="icon-exclamation"
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                      dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                     />
                   </div>
                 )}
@@ -398,11 +398,11 @@ class OidcSecurityManagementContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByUserName-oidc"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -420,11 +420,11 @@ class OidcSecurityManagementContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByEmail-oidc"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -450,12 +450,12 @@ class OidcSecurityManagementContents extends React.Component {
         <div style={{ minHeight: '300px' }}>
           <h4>
             <i className="icon-question" aria-hidden="true" />
-            <a href="#collapseHelpForOidcOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.oidc')}</a>
+            <a href="#collapseHelpForOidcOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.oidc')}</a>
           </h4>
           <ol id="collapseHelpForOidcOauth" className="collapse">
-            <li>{t('security_setting.OAuth.OIDC.register_1')}</li>
-            <li>{t('security_setting.OAuth.OIDC.register_2')}</li>
-            <li>{t('security_setting.OAuth.OIDC.register_3')}</li>
+            <li>{t('security_settings.OAuth.OIDC.register_1')}</li>
+            <li>{t('security_settings.OAuth.OIDC.register_2')}</li>
+            <li>{t('security_settings.OAuth.OIDC.register_3')}</li>
           </ol>
         </div>
 
@@ -473,7 +473,7 @@ OidcSecurityManagementContents.propTypes = {
 };
 
 const OidcSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: siteUrl } = useSiteUrl();
   return <OidcSecurityManagementContents t={t} {...props} siteUrl={siteUrl} />;
 };

+ 44 - 44
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -34,7 +34,7 @@ class SamlSecurityManagementContents extends React.Component {
     try {
       await adminSamlSecurityContainer.updateSamlSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.SAML.updated_saml'));
+      toastSuccess(t('security_settings.SAML.updated_saml'));
     }
     catch (err) {
       toastError(err);
@@ -54,13 +54,13 @@ class SamlSecurityManagementContents extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.SAML.name')}
+          {t('security_settings.SAML.name')}
         </h2>
 
         {useOnlyEnvVars && (
           <p
             className="alert alert-info"
-            dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
+            dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.note for the only env option', { env: 'SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS' }) }}
           />
         )}
 
@@ -76,16 +76,16 @@ class SamlSecurityManagementContents extends React.Component {
                 disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
               />
               <label className="custom-control-label" htmlFor="isSamlEnabled">
-                {t('security_setting.SAML.enable_saml')}
+                {t('security_settings.SAML.enable_saml')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('saml') && isSamlEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
         <div className="row form-group mb-5">
-          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.callback_URL')}</label>
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.callback_URL')}</label>
           <div className="col-md-6">
             <input
               className="form-control"
@@ -93,13 +93,13 @@ class SamlSecurityManagementContents extends React.Component {
               defaultValue={samlCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
+            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'SAML Identity' })}</p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -111,7 +111,7 @@ class SamlSecurityManagementContents extends React.Component {
 
             {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
               <div className="alert alert-danger">
-                {t('security_setting.missing mandatory configs')}
+                {t('security_settings.missing mandatory configs')}
                 <ul>
                   {adminSamlSecurityContainer.state.missingMandatoryConfigKeys.map((configKey) => {
                     const key = configKey.replace('security:passport-saml:', '');
@@ -137,7 +137,7 @@ class SamlSecurityManagementContents extends React.Component {
               </thead>
               <tbody>
                 <tr>
-                  <th>{t('security_setting.form_item_name.entryPoint')}</th>
+                  <th>{t('security_settings.form_item_name.entryPoint')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -156,12 +156,12 @@ class SamlSecurityManagementContents extends React.Component {
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ENTRY_POINT' }) }} />
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.issuer')}</th>
+                  <th>{t('security_settings.form_item_name.issuer')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -180,12 +180,12 @@ class SamlSecurityManagementContents extends React.Component {
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ISSUER' }) }} />
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.cert')}</th>
+                  <th>{t('security_settings.form_item_name.cert')}</th>
                   <td>
                     <textarea
                       className="form-control form-control-sm"
@@ -198,7 +198,7 @@ class SamlSecurityManagementContents extends React.Component {
                     />
                     <p>
                       <small>
-                        {t('security_setting.SAML.cert_detail')}
+                        {t('security_settings.SAML.cert_detail')}
                       </small>
                     </p>
                     <div>
@@ -225,7 +225,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       value={adminSamlSecurityContainer.state.envCert || ''}
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_CERT' }) }} />
                     </p>
                   </td>
                 </tr>
@@ -247,7 +247,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               </thead>
               <tbody>
                 <tr>
-                  <th>{t('security_setting.form_item_name.attrMapId')}</th>
+                  <th>{t('security_settings.form_item_name.attrMapId')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -257,7 +257,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     />
                     <p className="form-text text-muted">
                       <small>
-                        {t('security_setting.SAML.id_detail')}
+                        {t('security_settings.SAML.id_detail')}
                       </small>
                     </p>
                   </td>
@@ -269,12 +269,12 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_ID' }) }} />
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.attrMapUsername')}</th>
+                  <th>{t('security_settings.form_item_name.attrMapUsername')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -283,7 +283,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.username_detail') }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.username_detail') }} />
                     </p>
                   </td>
                   <td>
@@ -294,12 +294,12 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_USERNAME' }) }} />
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.attrMapMail')}</th>
+                  <th>{t('security_settings.form_item_name.attrMapMail')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -308,7 +308,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: 'Email' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: 'Email' }) }} />
                     </p>
                   </td>
                   <td>
@@ -319,12 +319,12 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_MAIL' }) }} />
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.attrMapFirstName')}</th>
+                  <th>{t('security_settings.form_item_name.attrMapFirstName')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -334,7 +334,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     />
                     <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapFirstName') }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapFirstName') }) }} />
                     </p>
                   </td>
                   <td>
@@ -346,15 +346,15 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     />
                     <p className="form-text text-muted">
                       <small>
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_FIRST_NAME' }) }} />
                         <br />
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'firstName' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'firstName' }) }} />
                       </small>
                     </p>
                   </td>
                 </tr>
                 <tr>
-                  <th>{t('security_setting.form_item_name.attrMapLastName')}</th>
+                  <th>{t('security_settings.form_item_name.attrMapLastName')}</th>
                   <td>
                     <input
                       className="form-control"
@@ -364,7 +364,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     />
                     <p className="form-text text-muted">
                       {/* eslint-disable-next-line max-len */}
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.mapping_detail', { target: t('security_setting.form_item_name.attrMapLastName') }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.mapping_detail', { target: t('security_settings.form_item_name.attrMapLastName') }) }} />
                     </p>
                   </td>
                   <td>
@@ -376,9 +376,9 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     />
                     <p className="form-text text-muted">
                       <small>
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ATTR_MAPPING_LAST_NAME' }) }} />
                         <br />
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.Use default if both are empty', { target: 'lastName' }) }} />
+                        <span dangerouslySetInnerHTML={{ __html: t('security_settings.Use default if both are empty', { target: 'lastName' }) }} />
                       </small>
                     </p>
                   </td>
@@ -403,11 +403,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   <label
                     className="custom-control-label"
                     htmlFor="bindByUserName-SAML"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat username matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat username matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -425,11 +425,11 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                   <label
                     className="custom-control-label"
                     htmlFor="bindByEmail-SAML"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -439,7 +439,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
             </h3>
 
             <p className="form-text text-muted">
-              <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
+              <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_detail') }} />
             </p>
 
             <table className="table settings-table">
@@ -454,7 +454,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <tbody>
                 <tr>
                   <th>
-                    { t('security_setting.form_item_name.ABLCRule') }
+                    { t('security_settings.form_item_name.ABLCRule') }
                   </th>
                   <td>
                     <textarea
@@ -491,9 +491,9 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                           </div>
                           <Collapse isOpen={this.state.isHelpOpened}>
                             <div className="card-body">
-                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_help') }} />
-                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example1') }} />
-                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example2') }} />
+                              <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_help') }} />
+                              <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example1') }} />
+                              <p dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.attr_based_login_control_rule_example2') }} />
                             </div>
                           </Collapse>
                         </div>
@@ -508,7 +508,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                       readOnly
                     />
                     <p className="form-text text-muted">
-                      <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
+                      <small dangerouslySetInnerHTML={{ __html: t('security_settings.SAML.Use env var if empty', { env: 'SAML_ABLC_RULE' }) }} />
                     </p>
                   </td>
                 </tr>
@@ -546,7 +546,7 @@ SamlSecurityManagementContents.propTypes = {
 };
 
 const SamlSecurityManagementContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: siteUrl } = useSiteUrl();
   return <SamlSecurityManagementContents t={t} siteUrl={siteUrl} {...props} />;
 };

+ 4 - 4
packages/app/src/components/Admin/Security/SecurityManagementContents.jsx

@@ -18,7 +18,7 @@ import ShareLinkSetting from './ShareLinkSetting';
 import TwitterSecuritySetting from './TwitterSecuritySetting';
 
 const SecurityManagementContents = () => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
 
   const [activeTab, setActiveTab] = useState('passport_local');
   const [activeComponents, setActiveComponents] = useState(new Set(['passport_local']));
@@ -93,16 +93,16 @@ const SecurityManagementContents = () => {
 
       {/* XSS configuration link */}
       <div className="mb-5">
-        <h2 className="border-bottom">{t('security_setting.xss_prevent_setting')}</h2>
+        <h2 className="border-bottom">{t('security_settings.xss_prevent_setting')}</h2>
         <div className="text-center">
           <a style={{ fontSize: 'large' }} href="/admin/markdown/#preventXSS">
-            <i className="fa-fw icon-login"></i> {t('security_setting.xss_prevent_setting_link')}
+            <i className="fa-fw icon-login"></i> {t('security_settings.xss_prevent_setting_link')}
           </a>
         </div>
       </div>
 
       <div className="auth-mechanism-configurations">
-        <h2 className="border-bottom">{t('security_setting.Authentication mechanism settings')}</h2>
+        <h2 className="border-bottom">{t('security_settings.Authentication mechanism settings')}</h2>
         <CustomNav
           activeTab={activeTab}
           navTabMapping={navTabMapping}

+ 33 - 33
packages/app/src/components/Admin/Security/SecuritySetting.jsx

@@ -44,13 +44,13 @@ const getDeleteConfigValueForT = (DeleteConfigValue) => {
   switch (DeleteConfigValue) {
     case PageDeleteConfigValue.Anyone:
     case null:
-      return 'security_setting.anyone';
+      return 'security_settings.anyone';
     case PageDeleteConfigValue.Inherit:
-      return 'security_setting.inherit';
+      return 'security_settings.inherit';
     case PageDeleteConfigValue.AdminOnly:
-      return 'security_setting.admin_only';
+      return 'security_settings.admin_only';
     case PageDeleteConfigValue.AdminAndAuthor:
-      return 'security_setting.admin_and_author';
+      return 'security_settings.admin_and_author';
   }
 };
 
@@ -95,7 +95,7 @@ class SecuritySetting extends React.Component {
     const { t, adminGeneralSecurityContainer } = this.props;
     try {
       await adminGeneralSecurityContainer.updateGeneralSecuritySetting();
-      toastSuccess(t('security_setting.updated_general_security_setting'));
+      toastSuccess(t('security_settings.updated_general_security_setting'));
     }
     catch (err) {
       toastError(err);
@@ -209,7 +209,7 @@ class SecuritySetting extends React.Component {
                   type="button"
                   onClick={() => { this.setDeletionConfigState(PageDeleteConfigValue.Inherit, setState, deletionType) }}
                 >
-                  {t('security_setting.inherit')}
+                  {t('security_settings.inherit')}
                 </button>
               )
               : (
@@ -218,7 +218,7 @@ class SecuritySetting extends React.Component {
                   type="button"
                   onClick={() => { this.setDeletionConfigState(PageDeleteConfigValue.Anyone, setState, deletionType) }}
                 >
-                  {t('security_setting.anyone')}
+                  {t('security_settings.anyone')}
                 </button>
               )
           }
@@ -227,14 +227,14 @@ class SecuritySetting extends React.Component {
             type="button"
             onClick={() => { this.setDeletionConfigState(PageDeleteConfigValue.AdminAndAuthor, setState, deletionType) }}
           >
-            {t('security_setting.admin_and_author')}
+            {t('security_settings.admin_and_author')}
           </button>
           <button
             className="dropdown-item"
             type="button"
             onClick={() => { this.setDeletionConfigState(PageDeleteConfigValue.AdminOnly, setState, deletionType) }}
           >
-            {t('security_setting.admin_only')}
+            {t('security_settings.admin_only')}
           </button>
         </div>
         <p className="form-text text-muted small">
@@ -254,10 +254,10 @@ class SecuritySetting extends React.Component {
 
         <div className="col-md-3 text-md-right">
           {!isRecursiveDeletion(deletionType) && isTypeDeletion(deletionType) && (
-            <strong>{t('security_setting.page_delete')}</strong>
+            <strong>{t('security_settings.page_delete')}</strong>
           )}
           {!isRecursiveDeletion(deletionType) && !isTypeDeletion(deletionType) && (
-            <strong>{t('security_setting.page_delete_completely')}</strong>
+            <strong>{t('security_settings.page_delete_completely')}</strong>
           )}
         </div>
 
@@ -276,7 +276,7 @@ class SecuritySetting extends React.Component {
                     onClick={() => this.setExpantOtherDeleteOptionsState(deletionType, !expantDeleteOptionsState)}
                   >
                     <i className={`fa fa-fw fa-arrow-right ${expantDeleteOptionsState ? 'fa-rotate-90' : ''}`}></i>
-                    { t('security_setting.other_options') }
+                    { t('security_settings.other_options') }
                   </button>
                   <Collapse isOpen={expantDeleteOptionsState}>
                     <div className="pb-4">
@@ -284,13 +284,13 @@ class SecuritySetting extends React.Component {
                         <span className="text-warning">
                           <i className="icon-info"></i>
                           {/* eslint-disable-next-line react/no-danger */}
-                          <span dangerouslySetInnerHTML={{ __html: t('security_setting.page_delete_rights_caution') }} />
+                          <span dangerouslySetInnerHTML={{ __html: t('security_settings.page_delete_rights_caution') }} />
                         </span>
                       </p>
                       { this.previousPageRecursiveAuthorityState(deletionType) !== null && (
                         <div className="mb-3">
                           <strong>
-                            {t('security_setting.forced_update_desc')}
+                            {t('security_settings.forced_update_desc')}
                           </strong>
                           <code>
                             {t(getDeleteConfigValueForT(this.previousPageRecursiveAuthorityState(deletionType)))}
@@ -326,7 +326,7 @@ class SecuritySetting extends React.Component {
     return (
       <React.Fragment>
         <h2 className="alert-anchor border-bottom">
-          {t('security_settings')}
+          {t('security_settings.security_settings')}
         </h2>
 
         {adminGeneralSecurityContainer.retrieveError != null && (
@@ -335,7 +335,7 @@ class SecuritySetting extends React.Component {
           </div>
         )}
 
-        <h4 className="mt-4">{ t('security_setting.page_list_and_search_results') }</h4>
+        <h4 className="mt-4">{ t('security_settings.page_list_and_search_results') }</h4>
         <div className="row justify-content-md-center">
           <table className="table table-bordered col-lg-9 mb-5">
             <thead>
@@ -347,11 +347,11 @@ class SecuritySetting extends React.Component {
             <tbody>
               <tr>
                 <th scope="row">{ t('Public') }</th>
-                <td>{ t('always_displayed') }</td>
+                <td>{ t('security_settings.always_displayed') }</td>
               </tr>
               <tr>
                 <th scope="row">{ t('Anyone with the link') }</th>
-                <td>{ t('always_hidden') }</td>
+                <td>{ t('security_settings.always_hidden') }</td>
               </tr>
               <tr>
                 <th scope="row">{ t('Only me') }</th>
@@ -365,7 +365,7 @@ class SecuritySetting extends React.Component {
                       onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByOwner() }}
                     />
                     <label className="custom-control-label" htmlFor="isShowRestrictedByOwner">
-                      {t('displayed_or_hidden')}
+                      {t('security_settings.displayed_or_hidden')}
                     </label>
                   </div>
                 </td>
@@ -382,7 +382,7 @@ class SecuritySetting extends React.Component {
                       onChange={() => { adminGeneralSecurityContainer.switchIsShowRestrictedByGroup() }}
                     />
                     <label className="custom-control-label" htmlFor="isShowRestrictedByGroup">
-                      {t('displayed_or_hidden')}
+                      {t('security_settings.displayed_or_hidden')}
                     </label>
                   </div>
                 </td>
@@ -391,10 +391,10 @@ class SecuritySetting extends React.Component {
           </table>
         </div>
 
-        <h4>{t('security_setting.page_access_rights')}</h4>
+        <h4>{t('security_settings.page_access_rights')}</h4>
         <div className="row mb-4">
           <div className="col-md-3 text-md-right py-2">
-            <strong>{t('security_setting.Guest Users Access')}</strong>
+            <strong>{t('security_settings.Guest Users Access')}</strong>
           </div>
           <div className="col-md-9">
             <div className="dropdown">
@@ -408,16 +408,16 @@ class SecuritySetting extends React.Component {
                 aria-expanded="true"
               >
                 <span className="float-left">
-                  {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
-                  {currentRestrictGuestMode === 'Readonly' && t('security_setting.guest_mode.readonly')}
+                  {currentRestrictGuestMode === 'Deny' && t('security_settings.guest_mode.deny')}
+                  {currentRestrictGuestMode === 'Readonly' && t('security_settings.guest_mode.readonly')}
                 </span>
               </button>
               <div className="dropdown-menu" aria-labelledby="dropdownMenuButton">
                 <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Deny') }}>
-                  {t('security_setting.guest_mode.deny')}
+                  {t('security_settings.guest_mode.deny')}
                 </button>
                 <button className="dropdown-item" type="button" onClick={() => { adminGeneralSecurityContainer.changeRestrictGuestMode('Readonly') }}>
-                  {t('security_setting.guest_mode.readonly')}
+                  {t('security_settings.guest_mode.readonly')}
                 </button>
               </div>
             </div>
@@ -427,7 +427,7 @@ class SecuritySetting extends React.Component {
                 </i><b>FIXED</b><br />
                 <b
                   dangerouslySetInnerHTML={{
-                    __html: t('security_setting.Fixed by env var',
+                    __html: t('security_settings.Fixed by env var',
                       { key: 'FORCE_WIKI_MODE', value: adminGeneralSecurityContainer.state.wikiMode }),
                   }}
                 />
@@ -436,7 +436,7 @@ class SecuritySetting extends React.Component {
           </div>
         </div>
 
-        <h4>{t('security_setting.page_delete_rights')}</h4>
+        <h4>{t('security_settings.page_delete_rights')}</h4>
         {/* Render PageDeletePermission */}
         {
           [
@@ -453,9 +453,9 @@ class SecuritySetting extends React.Component {
           ].map(arr => this.renderPageDeletePermission(arr[0], arr[1], arr[2], arr[3]))
         }
 
-        <h4>{t('security_setting.session')}</h4>
+        <h4>{t('security_settings.session')}</h4>
         <div className="form-group row">
-          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.max_age')}</label>
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_settings.max_age')}</label>
           <div className="col-md-6">
             <input
               className="form-control col-md-3"
@@ -467,10 +467,10 @@ class SecuritySetting extends React.Component {
               placeholder="2592000000"
             />
             {/* eslint-disable-next-line react/no-danger */}
-            <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_setting.max_age_desc') }} />
+            <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_settings.max_age_desc') }} />
             <p className="card well">
               <span className="text-warning">
-                <i className="icon-info"></i> {t('security_setting.max_age_caution')}
+                <i className="icon-info"></i> {t('security_settings.max_age_caution')}
               </span>
             </p>
           </div>
@@ -495,7 +495,7 @@ SecuritySetting.propTypes = {
 };
 
 const SecuritySettingWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <SecuritySetting t={t} {...props} />;
 };
 

+ 5 - 5
packages/app/src/components/Admin/Security/ShareLinkSetting.jsx

@@ -137,7 +137,7 @@ class ShareLinkSetting extends React.Component {
           </button>
           <h2 className="alert-anchor border-bottom">{t('share_links.share_link_management')}</h2>
         </div>
-        <h4>{t('security_setting.share_link_rights')}</h4>
+        <h4>{t('security_settings.share_link_rights')}</h4>
         <div className="row mb-5">
           <div className="col-6 offset-3">
             <div className="custom-control custom-switch custom-checkbox-success">
@@ -149,15 +149,15 @@ class ShareLinkSetting extends React.Component {
                 onChange={() => this.switchDisableLinkSharing()}
               />
               <label className="custom-control-label" htmlFor="disableLinkSharing">
-                {t('security_setting.enable_link_sharing')}
+                {t('security_settings.enable_link_sharing')}
               </label>
             </div>
             {!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && disableLinkSharing && (
-              <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>
+              <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>
             )}
           </div>
         </div>
-        <h4>{t('security_setting.all_share_links')}</h4>
+        <h4>{t('security_settings.all_share_links')}</h4>
         <Pager
           links={shareLinks}
           activePage={shareLinksActivePage}
@@ -196,7 +196,7 @@ ShareLinkSetting.propTypes = {
 };
 
 const ShareLinkSettingWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   return <ShareLinkSetting t={t} {...props} />;
 };
 

+ 21 - 21
packages/app/src/components/Admin/Security/TwitterSecuritySettingContents.jsx

@@ -28,7 +28,7 @@ class TwitterSecuritySettingContents extends React.Component {
     try {
       await adminTwitterSecurityContainer.updateTwitterSetting();
       await adminGeneralSecurityContainer.retrieveSetupStratedies();
-      toastSuccess(t('security_setting.OAuth.Twitter.updated_twitter'));
+      toastSuccess(t('security_settings.OAuth.Twitter.updated_twitter'));
     }
     catch (err) {
       toastError(err);
@@ -47,7 +47,7 @@ class TwitterSecuritySettingContents extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.Twitter.name')}
+          {t('security_settings.OAuth.Twitter.name')}
         </h2>
 
         {adminTwitterSecurityContainer.state.retrieveError != null && (
@@ -67,16 +67,16 @@ class TwitterSecuritySettingContents extends React.Component {
                 onChange={() => { adminGeneralSecurityContainer.switchIsTwitterOAuthEnabled() }}
               />
               <label className="custom-control-label" htmlFor="isTwitterEnabled">
-                {t('security_setting.OAuth.Twitter.enable_twitter')}
+                {t('security_settings.OAuth.Twitter.enable_twitter')}
               </label>
             </div>
             {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
-              && <div className="badge badge-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
+              && <div className="badge badge-warning">{t('security_settings.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
         <div className="row mb-5">
-          <label className="col-md-3 text-md-right py-2">{t('security_setting.callback_URL')}</label>
+          <label className="col-md-3 text-md-right py-2">{t('security_settings.callback_URL')}</label>
           <div className="col-md-6">
             <input
               className="form-control"
@@ -84,13 +84,13 @@ class TwitterSecuritySettingContents extends React.Component {
               value={twitterCallbackUrl}
               readOnly
             />
-            <p className="form-text text-muted small">{t('security_setting.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
+            <p className="form-text text-muted small">{t('security_settings.desc_of_callback_URL', { AuthName: 'OAuth' })}</p>
             {(siteUrl == null || siteUrl === '') && (
               <div className="alert alert-danger">
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_settings.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('app_settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -101,10 +101,10 @@ class TwitterSecuritySettingContents extends React.Component {
         {isTwitterEnabled && (
           <React.Fragment>
 
-            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+            <h3 className="border-bottom">{t('security_settings.configuration')}</h3>
 
             <div className="row mb-5">
-              <label htmlFor="TwitterConsumerId" className="col-md-3 text-md-right py-2">{t('security_setting.clientID')}</label>
+              <label htmlFor="TwitterConsumerId" className="col-md-3 text-md-right py-2">{t('security_settings.clientID')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -114,13 +114,13 @@ class TwitterSecuritySettingContents extends React.Component {
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerKey(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_KEY' }) }} />
                 </p>
               </div>
             </div>
 
             <div className="row mb-5">
-              <label htmlFor="TwitterConsumerSecret" className="col-md-3 text-md-right py-2">{t('security_setting.client_secret')}</label>
+              <label htmlFor="TwitterConsumerSecret" className="col-md-3 text-md-right py-2">{t('security_settings.client_secret')}</label>
               <div className="col-md-6">
                 <input
                   className="form-control"
@@ -130,7 +130,7 @@ class TwitterSecuritySettingContents extends React.Component {
                   onChange={e => adminTwitterSecurityContainer.changeTwitterConsumerSecret(e.target.value)}
                 />
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Use env var if empty', { env: 'OAUTH_TWITTER_CONSUMER_SECRET' }) }} />
                 </p>
               </div>
             </div>
@@ -148,11 +148,11 @@ class TwitterSecuritySettingContents extends React.Component {
                   <label
                     className="custom-control-label"
                     htmlFor="bindByUserNameTwitter"
-                    dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical') }}
+                    dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical') }}
                   />
                 </div>
                 <p className="form-text text-muted">
-                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.Treat email matching as identical_warn') }} />
+                  <small dangerouslySetInnerHTML={{ __html: t('security_settings.Treat email matching as identical_warn') }} />
                 </p>
               </div>
             </div>
@@ -178,16 +178,16 @@ class TwitterSecuritySettingContents extends React.Component {
         <div style={{ minHeight: '300px' }}>
           <h4>
             <i className="icon-question" aria-hidden="true"></i>
-            <a href="#collapseHelpForTwitterOauth" data-toggle="collapse"> {t('security_setting.OAuth.how_to.twitter')}</a>
+            <a href="#collapseHelpForTwitterOauth" data-toggle="collapse"> {t('security_settings.OAuth.how_to.twitter')}</a>
           </h4>
           <ol id="collapseHelpForTwitterOauth" className="collapse">
             {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_1', { link: '<a href="https://apps.twitter.com/" target=_blank>Twitter Application Management</a>' }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_2') }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_3') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_1', { link: '<a href="https://apps.twitter.com/" target=_blank>Twitter Application Management</a>' }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_2') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_3') }} />
             {/* eslint-disable-next-line max-len */}
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_4', { url: adminTwitterSecurityContainer.state.callbackUrl }) }} />
-            <li dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.Twitter.register_5') }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_4', { url: adminTwitterSecurityContainer.state.callbackUrl }) }} />
+            <li dangerouslySetInnerHTML={{ __html: t('security_settings.OAuth.Twitter.register_5') }} />
           </ol>
         </div>
 
@@ -207,7 +207,7 @@ TwitterSecuritySettingContents.propTypes = {
 };
 
 const TwitterSecuritySettingContentsWrapperFC = (props) => {
-  const { t } = useTranslation();
+  const { t } = useTranslation('admin');
   const { data: siteUrl } = useSiteUrl();
   return <TwitterSecuritySettingContents t={t} siteUrl={siteUrl} {...props} />;
 };

+ 1 - 1
packages/app/src/components/Admin/UserManagement.jsx

@@ -157,7 +157,7 @@ class UserManagement extends React.Component {
           </a>
         </p>
 
-        <h2>{t('User_Management')}</h2>
+        <h2>{t('user_management.user_management')}</h2>
         <div className="border-top border-bottom">
 
           <div className="row d-flex justify-content-start align-items-center my-2">

+ 1 - 1
packages/app/src/components/Layout/AdminLayout.tsx

@@ -36,7 +36,7 @@ const AdminLayout = ({
         <GrowiNavbar />
 
         <header className="py-0 position-relative">
-          <h1 className="title">{title}</h1>
+          <h1 className="title px-3">{title}</h1>
         </header>
         <div id="main" className="main">
           <div className="container-fluid">

+ 12 - 12
packages/app/src/components/Layout/BasicLayout.tsx

@@ -7,6 +7,18 @@ import Sidebar from '../Sidebar';
 
 import { RawLayout } from './RawLayout';
 
+// const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
+// const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
+const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
+const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
+const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
+// Page modals
+const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
+const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
+const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
+const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
+const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
+
 
 type Props = {
   title: string
@@ -19,18 +31,6 @@ export const BasicLayout = ({
   children, title, className, expandContainer,
 }: Props): JSX.Element => {
 
-  // const HotkeysManager = dynamic(() => import('../client/js/components/Hotkeys/HotkeysManager'), { ssr: false });
-  // const PageCreateModal = dynamic(() => import('../client/js/components/PageCreateModal'), { ssr: false });
-  const GrowiNavbarBottom = dynamic(() => import('../Navbar/GrowiNavbarBottom').then(mod => mod.GrowiNavbarBottom), { ssr: false });
-  const ShortcutsModal = dynamic(() => import('../ShortcutsModal'), { ssr: false });
-  const SystemVersion = dynamic(() => import('../SystemVersion'), { ssr: false });
-  // Page modals
-  const PageCreateModal = dynamic(() => import('../PageCreateModal'), { ssr: false });
-  const PageDuplicateModal = dynamic(() => import('../PageDuplicateModal'), { ssr: false });
-  const PageDeleteModal = dynamic(() => import('../PageDeleteModal'), { ssr: false });
-  const PageRenameModal = dynamic(() => import('../PageRenameModal'), { ssr: false });
-  const PagePresentationModal = dynamic(() => import('../PagePresentationModal'), { ssr: false });
-
   const myClassName = `${className ?? ''} ${expandContainer ? 'growi-layout-fluid' : ''}`;
 
   return (

+ 3 - 16
packages/app/src/components/Layout/RawLayout.tsx

@@ -1,16 +1,14 @@
 import React, { ReactNode, useState } from 'react';
 
+import { isClient } from '@growi/core';
 import Head from 'next/head';
-import Image from 'next/image';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 import { useGrowiTheme } from '~/stores/context';
-import { ColorScheme, ResolvedThemes, useNextThemes } from '~/stores/use-next-themes';
+import { ColorScheme, useNextThemes } from '~/stores/use-next-themes';
 import loggerFactory from '~/utils/logger';
 
-import { getBackgroundImageSrc } from '../Theme/utils/ThemeImageProvider';
 import { ThemeProvider } from '../Theme/utils/ThemeProvider';
-import { isClient } from '@growi/core';
 
 
 const logger = loggerFactory('growi:cli:RawLayout');
@@ -23,7 +21,6 @@ type Props = {
 }
 
 export const RawLayout = ({ children, title, className }: Props): JSX.Element => {
-
   const classNames: string[] = ['wrapper'];
   if (className != null) {
     classNames.push(className);
@@ -34,19 +31,12 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
   const { resolvedTheme, resolvedThemeByAttributes } = useNextThemes();
 
   const [colorScheme, setColorScheme] = useState<ColorScheme|undefined>(undefined);
-  const [backgroundImageSrc, setBackgroundImageSrc] = useState<string | undefined>(undefined);
 
   // set colorScheme in CSR
   useIsomorphicLayoutEffect(() => {
     setColorScheme(resolvedTheme ?? resolvedThemeByAttributes);
   }, [resolvedTheme]);
 
-  // set background image
-  useIsomorphicLayoutEffect(() => {
-    const imgSrc = getBackgroundImageSrc(growiTheme, colorScheme);
-    setBackgroundImageSrc(imgSrc);
-  }, [growiTheme, colorScheme]);
-
   const scriptToRewriteDataColorScheme = isClient() ? `
     wrapper = document.getElementById('wrapper');
     wrapper.setAttribute('data-color-scheme', '${resolvedThemeByAttributes}');
@@ -61,11 +51,8 @@ export const RawLayout = ({ children, title, className }: Props): JSX.Element =>
         {/* set data-color-scheme immediately after load */}
         <script>{scriptToRewriteDataColorScheme}</script>
       </Head>
-      <ThemeProvider theme={growiTheme}>
+      <ThemeProvider theme={growiTheme} colorScheme={colorScheme}>
         <div id="wrapper" className={classNames.join(' ')} data-color-scheme={colorScheme}>
-          {backgroundImageSrc != null && <div className="grw-bg-image-wrapper">
-            <Image className='grw-bg-image' alt='background-image' src={backgroundImageSrc} layout='fill' quality="100" />
-          </div>}
           {children}
         </div>
       </ThemeProvider>

+ 1 - 1
packages/app/src/components/LoginForm.jsx

@@ -175,7 +175,7 @@ class LoginForm extends React.Component {
         )}
         { (!isMailerSetup && isEmailAuthenticationEnabled) && (
           <p className="alert alert-danger">
-            <span>{t('security_setting.Local.please_enable_mailer')}</span>
+            <span>{t('security_settings.Local.please_enable_mailer')}</span>
           </p>
         )}
 

+ 1 - 1
packages/app/src/components/Me/AssociateModal.tsx

@@ -45,7 +45,7 @@ const AssociateModal = (props: Props): JSX.Element => {
       mutatePersonalExternalAccounts();
 
       closeModalHandler();
-      toastSuccess(t('security_setting.updated_general_security_setting'));
+      toastSuccess(t('security_settings.updated_general_security_setting'));
     }
     catch (err) {
       toastError(err);

+ 1 - 1
packages/app/src/components/Me/DisassociateModal.tsx

@@ -32,7 +32,7 @@ const DisassociateModal = (props: Props): JSX.Element => {
     try {
       await disassociateLdapAccount({ providerType, accountId });
       props.onClose();
-      toastSuccess(t('security_setting.updated_general_security_setting'));
+      toastSuccess(t('security_settings.updated_general_security_setting'));
     }
     catch (err) {
       toastError(err);

+ 8 - 1
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
 
 
 import { isPopulated } from '@growi/core';
+import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import { useTranslation } from 'next-i18next';
 import dynamic from 'next/dynamic';
 import { DropdownItem } from 'reactstrap';
@@ -13,6 +14,8 @@ import {
   IPageToRenameWithMeta, IPageWithMeta, IPageInfoForEntity, IPageHasId,
 } from '~/interfaces/page';
 import { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
+import { IUser } from '~/interfaces/user';
+import { IResTagsUpdateApiv1 } from '~/interfaces/tag';
 import {
   useCurrentPageId,
   useCurrentPathname,
@@ -224,9 +227,13 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
     const { _id: pageId, revision: revisionId } = currentPage;
     try {
-      await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
+      const res: IResTagsUpdateApiv1 = await apiPost('/tags.update', { pageId, revisionId, tags: newTags });
       mutateCurrentPage();
 
+      // TODO: fix https://github.com/weseek/growi/pull/6478 without pageContainer
+      // const lastUpdateUser = res.savedPage?.lastUpdateUser as IUser;
+      // await pageContainer.setState({ lastUpdateUsername: lastUpdateUser.username });
+
       // revalidate SWRTagsInfo
       mutateSWRTagsInfo();
       mutatePageTagsForEditors(newTags);

+ 9 - 9
packages/app/src/components/PageComment/DeleteCommentModal.tsx

@@ -25,7 +25,7 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
     isShown, comment, errorMessage, cancelToDelete, confirmToDelete,
   } = props;
 
-  const HeaderContent = useMemo(() => {
+  const headerContent = () => {
     if (comment == null || isShown === false) {
       return <></>;
     }
@@ -35,9 +35,9 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
         Delete comment?
       </span>
     );
-  }, [comment, isShown]);
+  };
 
-  const BodyContent = useMemo(() => {
+  const bodyContent = () => {
     if (comment == null || isShown === false) {
       return <></>;
     }
@@ -59,9 +59,9 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
         <p className="card well comment-body mt-2 p-2">{commentBodyElement}</p>
       </>
     );
-  }, [comment, isShown]);
+  };
 
-  const FooterContent = useMemo(() => {
+  const footerContent = () => {
     if (comment == null || isShown === false) {
       return <></>;
     }
@@ -75,18 +75,18 @@ export const DeleteCommentModal = (props: DeleteCommentModalProps): JSX.Element
         </Button>
       </>
     );
-  }, [cancelToDelete, comment, confirmToDelete, errorMessage, isShown]);
+  };
 
   return (
     <Modal isOpen={isShown} toggle={cancelToDelete} className={`${styles['page-comment-delete-modal']}`}>
       <ModalHeader tag="h4" toggle={cancelToDelete} className="bg-danger text-light">
-        {HeaderContent}
+        {headerContent()}
       </ModalHeader>
       <ModalBody>
-        {BodyContent}
+        {bodyContent()}
       </ModalBody>
       <ModalFooter>
-        {FooterContent}
+        {footerContent()}
       </ModalFooter>
     </Modal>
   );

+ 18 - 5
packages/app/src/components/PageCreateModal.jsx

@@ -19,7 +19,7 @@ const {
   userPageRoot, isCreatablePage, generateEditorPath, isUsersHomePage,
 } = pagePathUtils;
 
-const PageCreateModal = (props) => {
+const PageCreateModal = () => {
   const { t } = useTranslation();
 
   const { data: currentUser } = useCurrentUser();
@@ -42,8 +42,10 @@ const PageCreateModal = (props) => {
 
   // ensure pageNameInput is synced with selectedPagePath || currentPagePath
   useEffect(() => {
-    setPageNameInput(isCreatable ? pathUtils.addTrailingSlash(pathname) : '/');
-  }, [pathname, isCreatable]);
+    if (isOpened) {
+      setPageNameInput(isCreatable ? pathUtils.addTrailingSlash(pathname) : '/');
+    }
+  }, [isOpened, pathname, isCreatable]);
 
   const checkIsUsersHomePageDebounce = useMemo(() => {
     const checkIsUsersHomePage = () => {
@@ -54,8 +56,10 @@ const PageCreateModal = (props) => {
   }, [pageNameInput]);
 
   useEffect(() => {
-    checkIsUsersHomePageDebounce(pageNameInput);
-  }, [checkIsUsersHomePageDebounce, pageNameInput]);
+    if (isOpened) {
+      checkIsUsersHomePageDebounce(pageNameInput);
+    }
+  }, [isOpened, checkIsUsersHomePageDebounce, pageNameInput]);
 
   function transitBySubmitEvent(e, transitHandler) {
     // prevent page transition by submit
@@ -132,6 +136,9 @@ const PageCreateModal = (props) => {
   }
 
   function renderCreateTodayForm() {
+    if (!isOpened) {
+      return <></>;
+    }
     return (
       <div className="row">
         <fieldset className="col-12 mb-4">
@@ -183,6 +190,9 @@ const PageCreateModal = (props) => {
   }
 
   function renderInputPageForm() {
+    if (!isOpened) {
+      return <></>;
+    }
     return (
       <div className="row" data-testid="row-create-page-under-below">
         <fieldset className="col-12 mb-4">
@@ -237,6 +247,9 @@ const PageCreateModal = (props) => {
   }
 
   function renderTemplatePageForm() {
+    if (!isOpened) {
+      return <></>;
+    }
     return (
       <div className="row">
         <fieldset className="col-12">

+ 43 - 7
packages/app/src/components/PageDeleteModal.tsx

@@ -228,13 +228,26 @@ const PageDeleteModal: FC = () => {
     return <></>;
   };
 
-  return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
-      <ModalHeader tag="h4" toggle={closeDeleteModal} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
+  const headerContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+
+    return (
+      <>
         <i className={`icon-fw icon-${deleteIconAndKey[deleteMode].icon}`}></i>
         { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
-      </ModalHeader>
-      <ModalBody>
+      </>
+    );
+  };
+
+  const bodyContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+
+    return (
+      <>
         <div className="form-group grw-scrollable-modal-body pb-1">
           <label>{ t('modal_delete.deleting_page') }:</label><br />
           {/* Todo: change the way to show path on modal when too many pages are selected */}
@@ -242,8 +255,17 @@ const PageDeleteModal: FC = () => {
         </div>
         { isDeletable && renderDeleteRecursivelyForm()}
         { isDeletable && !forceDeleteCompletelyMode && renderDeleteCompletelyForm() }
-      </ModalBody>
-      <ModalFooter>
+      </>
+    );
+  };
+
+  const footerContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+
+    return (
+      <>
         <ApiErrorMessageList errs={errs} />
         <button
           type="button"
@@ -254,6 +276,20 @@ const PageDeleteModal: FC = () => {
           <i className={`mr-1 icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
           { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
         </button>
+      </>
+    );
+  };
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeDeleteModal} data-testid="page-delete-modal" className="grw-create-page">
+      <ModalHeader tag="h4" toggle={closeDeleteModal} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
+        {headerContent()}
+      </ModalHeader>
+      <ModalBody>
+        {bodyContent()}
+      </ModalBody>
+      <ModalFooter>
+        {footerContent()}
       </ModalFooter>
     </Modal>
 

+ 39 - 17
packages/app/src/components/PageDuplicateModal.tsx

@@ -75,10 +75,10 @@ const PageDuplicateModal = (): JSX.Element => {
   }, [checkExistPaths]);
 
   useEffect(() => {
-    if (page != null && pageNameInput !== page.path) {
+    if (isOpened && page != null && pageNameInput !== page.path) {
       checkExistPathsDebounce(page.path, pageNameInput);
     }
-  }, [pageNameInput, subordinatedPages, checkExistPathsDebounce, page]);
+  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page]);
 
   /**
    * change pageNameInput for PagePathAutoComplete
@@ -150,22 +150,17 @@ const PageDuplicateModal = (): JSX.Element => {
 
   }, [isOpened]);
 
-  if (page == null) {
-    return <></>;
-  }
 
-  const { path } = page;
-  const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
+  const bodyContent = () => {
+    if (!isOpened || page == null) {
+      return <></>;
+    }
 
-  const submitButtonEnabled = existingPaths.length === 0
-    || (isDuplicateRecursively && isDuplicateRecursivelyWithoutExistPath);
+    const { path } = page;
+    const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
 
-  return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
-        { t('modal_duplicate.label.Duplicate page') }
-      </ModalHeader>
-      <ModalBody>
+    return (
+      <>
         <div className="form-group"><label>{t('modal_duplicate.label.Current page name')}</label><br />
           <code>{path}</code>
         </div>
@@ -239,9 +234,20 @@ const PageDuplicateModal = (): JSX.Element => {
             ) }
           </div>
         </div>
+      </>
+    );
+  };
 
-      </ModalBody>
-      <ModalFooter>
+  const footerContent = () => {
+    if (!isOpened || page == null) {
+      return <></>;
+    }
+
+    const submitButtonEnabled = existingPaths.length === 0
+    || (isDuplicateRecursively && isDuplicateRecursivelyWithoutExistPath);
+
+    return (
+      <>
         <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button
           type="button"
@@ -251,6 +257,22 @@ const PageDuplicateModal = (): JSX.Element => {
         >
           { t('modal_duplicate.label.Duplicate page') }
         </button>
+      </>
+    );
+  };
+
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeDuplicateModal} data-testid="page-duplicate-modal" className="grw-duplicate-page" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={closeDuplicateModal} className="bg-primary text-light">
+        { t('modal_duplicate.label.Duplicate page') }
+      </ModalHeader>
+      <ModalBody>
+        {bodyContent()}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
+        {footerContent()}
       </ModalFooter>
     </Modal>
   );

+ 2 - 2
packages/app/src/components/PageEditor/Editor.tsx

@@ -8,7 +8,7 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-import { toastError } from '~/client/util/apiNotification';
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { useDefaultIndentSize } from '~/stores/context';
 import { useEditorSettings } from '~/stores/editor';
 import { useIsMobile } from '~/stores/ui';
@@ -138,7 +138,7 @@ const Editor = React.forwardRef((props: EditorPropsType, ref): JSX.Element => {
   const pasteFilesHandler = useCallback((event) => {
     const items = event.clipboardData.items || event.clipboardData.files || [];
 
-    toastError(t('toaster.file_upload_failed'));
+    toastSuccess(t('toaster.file_upload_succeeded'));
 
     // abort if length is not 1
     if (items.length < 1) {

+ 55 - 35
packages/app/src/components/PageRenameModal.tsx

@@ -151,15 +151,17 @@ const PageRenameModal = (): JSX.Element => {
   }, [isUsersHomePage, pageNameInput]);
 
   useEffect(() => {
-    if (page != null && pageNameInput !== page.data.path) {
+    if (isOpened && page != null && pageNameInput !== page.data.path) {
       checkExistPathsDebounce(page.data.path, pageNameInput);
       checkIsUsersHomePageDebounce(pageNameInput);
     }
-  }, [pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomePageDebounce]);
+  }, [isOpened, pageNameInput, subordinatedPages, checkExistPathsDebounce, page, checkIsUsersHomePageDebounce]);
 
   useEffect(() => {
-    setCanRename(false);
-  }, [pageNameInput]);
+    if (isOpened && page != null) {
+      setCanRename(false);
+    }
+  }, [isOpened, page, pageNameInput]);
 
 
   function ppacInputChangeHandler(value) {
@@ -177,7 +179,7 @@ const PageRenameModal = (): JSX.Element => {
   }
 
   useEffect(() => {
-    if (isOpened) {
+    if (isOpened || page == null) {
       return;
     }
 
@@ -193,37 +195,18 @@ const PageRenameModal = (): JSX.Element => {
       setExpandOtherOptions(false);
     }, 1000);
 
-  }, [isOpened]);
-
-  if (page == null) {
-    return <></>;
-  }
-
-  const { path } = page.data;
-  const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
-
-  let submitButtonDisabled = false;
+  }, [isOpened, page]);
 
-  if (isMatchedWithUserHomePagePath) {
-    submitButtonDisabled = true;
-  }
-  else if (!canRename) {
-    submitButtonDisabled = true;
-  }
-  else if (isV5Compatible(page.meta)) {
-    submitButtonDisabled = existingPaths.length !== 0; // v5 data
-  }
-  else {
-    submitButtonDisabled = !isRenameRecursively; // v4 data
-  }
+  const bodyContent = () => {
+    if (!isOpened || page == null) {
+      return <></>;
+    }
 
+    const { path } = page.data;
+    const isTargetPageDuplicate = existingPaths.includes(pageNameInput);
 
-  return (
-    <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
-      <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
-        { t('modal_rename.label.Move/Rename page') }
-      </ModalHeader>
-      <ModalBody>
+    return (
+      <>
         <div className="form-group">
           <label>{ t('modal_rename.label.Current page name') }</label><br />
           <code>{ path }</code>
@@ -338,9 +321,31 @@ const PageRenameModal = (): JSX.Element => {
           </div>
           <div> {subordinatedError} </div>
         </Collapse>
+      </>
+    );
+  };
 
-      </ModalBody>
-      <ModalFooter>
+  const footerContent = () => {
+    if (!isOpened || page == null) {
+      return <></>;
+    }
+
+    let submitButtonDisabled = false;
+
+    if (isMatchedWithUserHomePagePath) {
+      submitButtonDisabled = true;
+    }
+    else if (!canRename) {
+      submitButtonDisabled = true;
+    }
+    else if (isV5Compatible(page.meta)) {
+      submitButtonDisabled = existingPaths.length !== 0; // v5 data
+    }
+    else {
+      submitButtonDisabled = !isRenameRecursively; // v4 data
+    }
+    return (
+      <>
         <ApiErrorMessageList errs={errs} targetPath={pageNameInput} />
         <button
           type="button"
@@ -349,6 +354,21 @@ const PageRenameModal = (): JSX.Element => {
           disabled={submitButtonDisabled}
         >Rename
         </button>
+      </>
+    );
+  };
+
+
+  return (
+    <Modal size="lg" isOpen={isOpened} toggle={closeRenameModal} data-testid="page-rename-modal" autoFocus={false}>
+      <ModalHeader tag="h4" toggle={closeRenameModal} className="bg-primary text-light">
+        { t('modal_rename.label.Move/Rename page') }
+      </ModalHeader>
+      <ModalBody>
+        {bodyContent()}
+      </ModalBody>
+      <ModalFooter>
+        {footerContent()}
       </ModalFooter>
     </Modal>
   );

+ 39 - 8
packages/app/src/components/PutbackPageModal.jsx

@@ -53,13 +53,22 @@ const PutBackPageModal = () => {
     }
   }
 
-
-  return (
-    <Modal isOpen={isOpened} toggle={closePutBackPageModal} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={closePutBackPageModal} className="bg-info text-light">
+  const HeaderContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+    return (
+      <>
         <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('modal_putback.label.Put Back Page') }
-      </ModalHeader>
-      <ModalBody>
+      </>
+    );
+  };
+  const BodyContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+    return (
+      <>
         <div className="form-group">
           <label>{t('modal_putback.label.Put Back Page')}:</label><br />
           <code>{path}</code>
@@ -79,12 +88,34 @@ const PutBackPageModal = () => {
             <code>{ path }</code>{ t('modal_putback.help.recursively') }
           </p>
         </div>
-      </ModalBody>
-      <ModalFooter>
+      </>
+    );
+
+  };
+  const FooterContent = () => {
+    if (!isOpened) {
+      return <></>;
+    }
+    return (
+      <>
         <ApiErrorMessageList errs={errs} targetPath={targetPath} />
         <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
           <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
         </button>
+      </>
+    );
+  };
+
+  return (
+    <Modal isOpen={isOpened} toggle={closePutBackPageModal} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={closePutBackPageModal} className="bg-info text-light">
+        <HeaderContent/>
+      </ModalHeader>
+      <ModalBody>
+        <BodyContent/>
+      </ModalBody>
+      <ModalFooter>
+        <FooterContent/>
       </ModalFooter>
     </Modal>
   );

+ 0 - 130
packages/app/src/components/ShareLink/ShareLink.jsx

@@ -1,130 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-import { useTranslation } from 'next-i18next';
-
-
-import PageContainer from '~/client/services/PageContainer';
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Delete, apiv3Get } from '~/client/util/apiv3-client';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import ShareLinkForm from './ShareLinkForm';
-import ShareLinkList from './ShareLinkList';
-
-class ShareLink extends React.Component {
-
-  constructor() {
-    super();
-    this.state = {
-      shareLinks: [],
-      isOpenShareLinkForm: false,
-    };
-
-    this.toggleShareLinkFormHandler = this.toggleShareLinkFormHandler.bind(this);
-    this.deleteAllLinksButtonHandler = this.deleteAllLinksButtonHandler.bind(this);
-    this.deleteLinkById = this.deleteLinkById.bind(this);
-  }
-
-  componentDidMount() {
-    this.retrieveShareLinks();
-  }
-
-  async retrieveShareLinks() {
-    const { pageContainer } = this.props;
-    const { pageId } = pageContainer.state;
-
-    try {
-      const res = await apiv3Get('/share-links/', { relatedPage: pageId });
-      const { shareLinksResult } = res.data;
-      this.setState({ shareLinks: shareLinksResult });
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-  }
-
-  toggleShareLinkFormHandler() {
-    this.setState({ isOpenShareLinkForm: !this.state.isOpenShareLinkForm });
-    this.retrieveShareLinks();
-  }
-
-  async deleteAllLinksButtonHandler() {
-    const { t, pageContainer } = this.props;
-    const { pageId } = pageContainer.state;
-
-    try {
-      const res = await apiv3Delete('/share-links/', { relatedPage: pageId });
-      const count = res.data.n;
-      toastSuccess(t('toaster.remove_share_link', { count }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-    this.retrieveShareLinks();
-  }
-
-  async deleteLinkById(shareLinkId) {
-    const { t } = this.props;
-
-    try {
-      const res = await apiv3Delete(`/share-links/${shareLinkId}`);
-      const { deletedShareLink } = res.data;
-      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
-    }
-    catch (err) {
-      toastError(err);
-    }
-
-    this.retrieveShareLinks();
-  }
-
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="container p-0" data-testid="share-link-management">
-        <h3 className="grw-modal-head d-flex pb-2">
-          { t('share_links.share_link_list') }
-          <button className="btn btn-danger ml-auto " type="button" onClick={this.deleteAllLinksButtonHandler}>{t('delete_all')}</button>
-        </h3>
-
-        <div>
-          <ShareLinkList
-            shareLinks={this.state.shareLinks}
-            onClickDeleteButton={this.deleteLinkById}
-          />
-          <button
-            className="btn btn-outline-secondary d-block mx-auto px-5"
-            type="button"
-            onClick={this.toggleShareLinkFormHandler}
-          >
-            {this.state.isOpenShareLinkForm ? t('Close') : t('New')}
-          </button>
-          {this.state.isOpenShareLinkForm && <ShareLinkForm onCloseForm={this.toggleShareLinkFormHandler} />}
-        </div>
-      </div>
-    );
-  }
-
-}
-
-ShareLink.propTypes = {
-  t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-const ShareLinkWrapperFC = (props) => {
-  const { t } = useTranslation();
-  return <ShareLink t={t} {...props} />;
-};
-
-/**
- * Wrapper component for using unstated
- */
-const ShareLinkWrapper = withUnstatedContainers(ShareLinkWrapperFC, [PageContainer]);
-
-export default ShareLinkWrapper;

+ 76 - 0
packages/app/src/components/ShareLink/ShareLink.tsx

@@ -0,0 +1,76 @@
+import React, {
+  useState, useCallback,
+} from 'react';
+
+import { useTranslation } from 'react-i18next';
+
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Delete } from '~/client/util/apiv3-client';
+import { useCurrentPageId } from '~/stores/context';
+import { useSWRxSharelink } from '~/stores/share-link';
+
+import ShareLinkForm from './ShareLinkForm';
+import ShareLinkList from './ShareLinkList';
+
+const ShareLink = (): JSX.Element => {
+  const { t } = useTranslation();
+  const [isOpenShareLinkForm, setIsOpenShareLinkForm] = useState<boolean>(false);
+
+  const { data: currentPageId } = useCurrentPageId();
+
+  const { data: currentShareLinks, mutate } = useSWRxSharelink(currentPageId);
+
+  const toggleShareLinkFormHandler = useCallback(() => {
+    setIsOpenShareLinkForm(prev => !prev);
+    mutate();
+  }, [mutate]);
+
+  const deleteAllLinksButtonHandler = useCallback(async() => {
+    try {
+      const res = await apiv3Delete('/share-links/', { relatedPage: currentPageId });
+      const count = res.data.n;
+      toastSuccess(t('toaster.remove_share_link', { count }));
+      mutate();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [mutate, currentPageId, t]);
+
+  const deleteLinkById = useCallback(async(shareLinkId) => {
+    try {
+      const res = await apiv3Delete(`/share-links/${shareLinkId}`);
+      const { deletedShareLink } = res.data;
+      toastSuccess(t('toaster.remove_share_link_success', { shareLinkId: deletedShareLink._id }));
+      mutate();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [mutate, t]);
+
+  return (
+    <div className="container p-0" data-testid="share-link-management">
+      <h3 className="grw-modal-head d-flex pb-2">
+        { t('share_links.share_link_list') }
+        <button className="btn btn-danger ml-auto " type="button" onClick={deleteAllLinksButtonHandler}>{t('delete_all')}</button>
+      </h3>
+      <div>
+        <ShareLinkList
+          shareLinks={currentShareLinks == null ? [] : currentShareLinks}
+          onClickDeleteButton={deleteLinkById}
+        />
+        <button
+          className="btn btn-outline-secondary d-block mx-auto px-5"
+          type="button"
+          onClick={toggleShareLinkFormHandler}
+        >
+          {isOpenShareLinkForm ? t('Close') : t('New')}
+        </button>
+        {isOpenShareLinkForm && <ShareLinkForm onCloseForm={toggleShareLinkFormHandler} />}
+      </div>
+    </div>
+  );
+};
+
+export default ShareLink;

+ 146 - 136
packages/app/src/components/ShortcutsModal.tsx

@@ -14,10 +14,151 @@ const ShortcutsModal = (): JSX.Element => {
 
   const { data: status, close } = useShortcutsModal();
 
-  // add classes to cmd-key by OS
-  const platform = window.navigator.platform.toLowerCase();
-  const isMac = (platform.indexOf('mac') > -1);
-  const additionalClassByOs = isMac ? 'mac' : 'key-longer win';
+  const bodyContent = () => {
+    if (status == null || !status.isOpened) {
+      return <></>;
+    }
+
+    // add classes to cmd-key by OS
+    const platform = window.navigator.platform.toLowerCase();
+    const isMac = (platform.indexOf('mac') > -1);
+    const additionalClassByOs = isMac ? 'mac' : 'key-longer win';
+
+    return (
+      <div className="container">
+        <div className="row">
+          <div className="col-lg-6">
+            <h3>
+              <strong>{t('modal_shortcuts.global.title')}</strong>
+            </h3>
+
+            <table className="table">
+              <tbody>
+                <tr>
+                  <th>
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Open/Close shortcut help') }} />:
+                  </th>
+                  <td>
+                    <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">/</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.global.Create Page')}:</th>
+                  <td>
+                    <span className="key">C</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.global.Edit Page')}:</th>
+                  <td>
+                    <span className="key">E</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.global.Search')}:</th>
+                  <td><span className="key">/</span></td>
+                </tr>
+                <tr>
+                  <th>
+                    {/* eslint-disable-next-line react/no-danger */}
+                    <span dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Show Contributors') }} />:
+                  </th>
+                  <td className='text-nowrap'>
+                    <a href="{ t('modal_shortcuts.global.konami_code_url') }" target="_blank">
+                      {t('modal_shortcuts.global.Konami Code')}
+                    </a>
+                    <br />
+                    <span className="key key-small">&uarr;</span>&nbsp;<span className="key key-small">&uarr;</span>
+                    <span className="key key-small">&darr;</span>&nbsp;<span className="key key-small">&darr;</span>
+                    <br />
+                    <span className="key key-small">&larr;</span>&nbsp;<span className="key key-small">&rarr;</span>
+                    <span className="key key-small">&larr;</span>&nbsp;<span className="key key-small">&rarr;</span>
+                    <br />
+                    <span className="key key-small">B</span>&nbsp;<span className="key key-small">A</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.global.MirrorMode')}:</th>
+                  <td className='text-nowrap'>
+                    <a href="{ t('modal_shortcuts.global.konami_code_url') }" target="_blank">
+                      {t('modal_shortcuts.global.Konami Code')}
+                    </a>
+                    <br />
+                    <span className="key key-small">X</span>&nbsp;<span className="key key-small">X</span>
+                    <span className="key key-small">B</span>&nbsp;<span className="key key-small">B</span>
+                    <br />
+                    <span className="key key-small">A</span>&nbsp;<span className="key key-small">Y</span>
+                    <span className="key key-small">A</span>&nbsp;<span className="key key-small">Y</span>
+                    <br />
+                    <span className="key key-small">&darr;</span>&nbsp;<span className="key key-small">&larr;</span>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+          <div className="col-lg-6">
+            <h3>
+              <strong>{t('modal_shortcuts.editor.title')}</strong>
+            </h3>
+            <table className="table">
+              <tbody>
+                <tr>
+                  <th>{t('modal_shortcuts.editor.Indent')}:</th>
+                  <td>
+                    <span className="key key-longer">Tab</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.editor.Outdent')}:</th>
+                  <td className="text-nowrap">
+                    <span className="key key-long">Shift</span> + <span className="key key-longer">Tab</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.editor.Save Page')}:</th>
+                  <td>
+                    <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">S</span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.editor.Delete Line')}:</th>
+                  <td>
+                    <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">D</span>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+
+            <h3>
+              <strong>{t('modal_shortcuts.commentform.title')}</strong>
+            </h3>
+
+            <table className="table">
+              <tbody>
+                <tr>
+                  <th>{t('modal_shortcuts.commentform.Post')}:</th>
+                  <td className="text-nowrap">
+                    <span className={`key cmd-key ${additionalClassByOs}`}></span> +
+                    <span className="key key-longer">
+                      <KeyboardReturnEnterIcon />
+                    </span>
+                  </td>
+                </tr>
+                <tr>
+                  <th>{t('modal_shortcuts.editor.Delete Line')}:</th>
+                  <td>
+                    <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">D</span>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+    );
+  };
 
   return (
     <>
@@ -27,138 +168,7 @@ const ShortcutsModal = (): JSX.Element => {
             {t('Shortcuts')}
           </ModalHeader>
           <ModalBody>
-            <div className="container">
-              <div className="row">
-                <div className="col-lg-6">
-                  <h3>
-                    <strong>{t('modal_shortcuts.global.title')}</strong>
-                  </h3>
-
-                  <table className="table">
-                    <tbody>
-                      <tr>
-                        <th>
-                          {/* eslint-disable-next-line react/no-danger */}
-                          <span dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Open/Close shortcut help') }} />:
-                        </th>
-                        <td>
-                          <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">/</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.global.Create Page')}:</th>
-                        <td>
-                          <span className="key">C</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.global.Edit Page')}:</th>
-                        <td>
-                          <span className="key">E</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.global.Search')}:</th>
-                        <td><span className="key">/</span></td>
-                      </tr>
-                      <tr>
-                        <th>
-                          {/* eslint-disable-next-line react/no-danger */}
-                          <span dangerouslySetInnerHTML={{ __html: t('modal_shortcuts.global.Show Contributors') }} />:
-                        </th>
-                        <td className='text-nowrap'>
-                          <a href="{ t('modal_shortcuts.global.konami_code_url') }" target="_blank">
-                            {t('modal_shortcuts.global.Konami Code')}
-                          </a>
-                          <br />
-                          <span className="key key-small">&uarr;</span>&nbsp;<span className="key key-small">&uarr;</span>
-                          <span className="key key-small">&darr;</span>&nbsp;<span className="key key-small">&darr;</span>
-                          <br />
-                          <span className="key key-small">&larr;</span>&nbsp;<span className="key key-small">&rarr;</span>
-                          <span className="key key-small">&larr;</span>&nbsp;<span className="key key-small">&rarr;</span>
-                          <br />
-                          <span className="key key-small">B</span>&nbsp;<span className="key key-small">A</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.global.MirrorMode')}:</th>
-                        <td className='text-nowrap'>
-                          <a href="{ t('modal_shortcuts.global.konami_code_url') }" target="_blank">
-                            {t('modal_shortcuts.global.Konami Code')}
-                          </a>
-                          <br />
-                          <span className="key key-small">X</span>&nbsp;<span className="key key-small">X</span>
-                          <span className="key key-small">B</span>&nbsp;<span className="key key-small">B</span>
-                          <br />
-                          <span className="key key-small">A</span>&nbsp;<span className="key key-small">Y</span>
-                          <span className="key key-small">A</span>&nbsp;<span className="key key-small">Y</span>
-                          <br />
-                          <span className="key key-small">&darr;</span>&nbsp;<span className="key key-small">&larr;</span>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-
-                <div className="col-lg-6">
-                  <h3>
-                    <strong>{t('modal_shortcuts.editor.title')}</strong>
-                  </h3>
-                  <table className="table">
-                    <tbody>
-                      <tr>
-                        <th>{t('modal_shortcuts.editor.Indent')}:</th>
-                        <td>
-                          <span className="key key-longer">Tab</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.editor.Outdent')}:</th>
-                        <td className="text-nowrap">
-                          <span className="key key-long">Shift</span> + <span className="key key-longer">Tab</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.editor.Save Page')}:</th>
-                        <td>
-                          <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">S</span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.editor.Delete Line')}:</th>
-                        <td>
-                          <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">D</span>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-
-                  <h3>
-                    <strong>{t('modal_shortcuts.commentform.title')}</strong>
-                  </h3>
-
-                  <table className="table">
-                    <tbody>
-                      <tr>
-                        <th>{t('modal_shortcuts.commentform.Post')}:</th>
-                        <td className="text-nowrap">
-                          <span className={`key cmd-key ${additionalClassByOs}`}></span> +
-                          <span className="key key-longer">
-                            <KeyboardReturnEnterIcon />
-                          </span>
-                        </td>
-                      </tr>
-                      <tr>
-                        <th>{t('modal_shortcuts.editor.Delete Line')}:</th>
-                        <td>
-                          <span className={`key cmd-key ${additionalClassByOs}`}></span> + <span className="key">D</span>
-                        </td>
-                      </tr>
-                    </tbody>
-                  </table>
-                </div>
-              </div>
-            </div>
+            {bodyContent()}
           </ModalBody>
         </Modal>
       ) }

+ 2 - 2
packages/app/src/components/Sidebar/RecentChanges.tsx

@@ -153,7 +153,7 @@ const RecentChanges = (): JSX.Element => {
   }, [retrieveSizePreferenceFromLocalStorage]);
 
   return (
-    <>
+    <div data-testid="grw-recent-changes">
       <div className="grw-sidebar-content-header p-3 d-flex">
         <h3 className="mb-0  text-nowrap">{t('Recent Changes')}</h3>
         <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => swr.mutate()}>
@@ -188,7 +188,7 @@ const RecentChanges = (): JSX.Element => {
           </InfiniteScroll>
         </ul>
       </div>
-    </>
+    </div>
   );
 
 };

+ 20 - 2
packages/app/src/components/Theme/ThemeAntarctic.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeAntarctic = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeAntarctic = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeAntarctic;

+ 20 - 2
packages/app/src/components/Theme/ThemeChristmas.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeChristmas = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeChristmas = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeChristmas;

+ 189 - 185
packages/app/src/components/Theme/ThemeDefault.module.scss

@@ -16,197 +16,201 @@
 
 //== Light Mode
 //
-.theme[data-color-scheme='light'] :global {
-  $primary: #122c55;
-  $accent: #209fd8;
-
-  // Background colors
-  $bgcolor-global: white;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: $gray-50;
-  $bgcolor-blinked-section: rgba($primary, 0.1);
-  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
-
-  // Font colors
-  $color-global: #112744;
-  $color-reversal: $light;
-  // $color-header: #2b2b2b;
-  $color-link: #1938ba;
-  $color-link-hover: lighten($color-link, 20%);
-  $color-link-wiki: $color-link;
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $gray-500;
-  $color-inline-code: darken($red, 15%); // optional
-
-  // List Group colors
-  // $color-list: $color-global; // optional
-  // $bgcolor-list: $bgcolor-global; // optional
-  // $color-list-hover: $color-global; // optional
-  // $bgcolor-list-hover: darken($bgcolor-global, 3%); // optional
-  // $color-list-active: $color-reversal; // optional
-  // $bgcolor-list-active: $primary; // optional
-
-  // Table colors
-  // $bgcolor-subnav: #; // optional
-  // $color-table: #; // optional
-  // $bgcolor-table: #; // optional
-  // $border-color-table: #; // optional
-  // $color-table-hover: #; // optional
-  // $bgcolor-table-hover: #; // optional
-
-  // Navbar
-  $bgcolor-navbar: $gray-900;
-  $bgcolor-search-top-dropdown: $accent;
-  $border-image-navbar: linear-gradient(to right, #36c9ff 0%, #36c9ff 33%, #7926ff 66%, #ff2eff 100%);
-
-  // Logo colors
-  $bgcolor-logo: $bgcolor-navbar;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
-
-  // Sidebar
-  $bgcolor-sidebar: $primary;
-  $bgcolor-sidebar-nav-item-active: rgba(black, 0.37); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-reversal;
-  $bgcolor-resize-button: $accent;
-  $color-resize-button-hover: $color-reversal;
-  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: lighten($primary, 77%);
-  // Sidebar list group
-  $bgcolor-sidebar-list-group: $gray-50; // optional
-
-  // Subnavigation
-  // $bgcolor-subnav: #fafafa; // optional
-
-  // Tabs
-  // $color-nav-tabs-link-active: #; //optional
-  // $bordercolor-nav-tabs-hover: # # $bordercolor-nav-tabs; // optional
-  // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
-
-  // Tags
-  // $color-tags: #; //optional
-  // $bgcolor-tags: #; //optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $gray-400;
-  $bordercolor-inline-code: $gray-400; // optional
-
-  // Dropdown colors
-  $bgcolor-dropdown-link-active: $growi-blue;
-
-  // admin theme box
-  $color-theme-color-box: lighten($primary, 20%);
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-light';
-
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
+.theme {
+  [data-color-scheme='light'] :global {
+    $primary: #122c55;
+    $accent: #209fd8;
+
+    // Background colors
+    $bgcolor-global: white;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: $gray-50;
+    $bgcolor-blinked-section: rgba($primary, 0.1);
+    //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+    // Font colors
+    $color-global: #112744;
+    $color-reversal: $light;
+    // $color-header: #2b2b2b;
+    $color-link: #1938ba;
+    $color-link-hover: lighten($color-link, 20%);
+    $color-link-wiki: $color-link;
+    $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+    $color-link-nabvar: $gray-500;
+    $color-inline-code: darken($red, 15%); // optional
+
+    // List Group colors
+    // $color-list: $color-global; // optional
+    // $bgcolor-list: $bgcolor-global; // optional
+    // $color-list-hover: $color-global; // optional
+    // $bgcolor-list-hover: darken($bgcolor-global, 3%); // optional
+    // $color-list-active: $color-reversal; // optional
+    // $bgcolor-list-active: $primary; // optional
+
+    // Table colors
+    // $bgcolor-subnav: #; // optional
+    // $color-table: #; // optional
+    // $bgcolor-table: #; // optional
+    // $border-color-table: #; // optional
+    // $color-table-hover: #; // optional
+    // $bgcolor-table-hover: #; // optional
+
+    // Navbar
+    $bgcolor-navbar: $gray-900;
+    $bgcolor-search-top-dropdown: $accent;
+    $border-image-navbar: linear-gradient(to right, #36c9ff 0%, #36c9ff 33%, #7926ff 66%, #ff2eff 100%);
+
+    // Logo colors
+    $bgcolor-logo: $bgcolor-navbar;
+    $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+
+    // Sidebar
+    $bgcolor-sidebar: $primary;
+    $bgcolor-sidebar-nav-item-active: rgba(black, 0.37); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-reversal;
+    $bgcolor-resize-button: $accent;
+    $color-resize-button-hover: $color-reversal;
+    $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $color-sidebar-context: $color-global;
+    $bgcolor-sidebar-context: lighten($primary, 77%);
+    // Sidebar list group
+    $bgcolor-sidebar-list-group: $gray-50; // optional
+
+    // Subnavigation
+    // $bgcolor-subnav: #fafafa; // optional
+
+    // Tabs
+    // $color-nav-tabs-link-active: #; //optional
+    // $bordercolor-nav-tabs-hover: # # $bordercolor-nav-tabs; // optional
+    // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
+
+    // Tags
+    // $color-tags: #; //optional
+    // $bgcolor-tags: #; //optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $gray-400;
+    $bordercolor-inline-code: $gray-400; // optional
+
+    // Dropdown colors
+    $bgcolor-dropdown-link-active: $growi-blue;
+
+    // admin theme box
+    $color-theme-color-box: lighten($primary, 20%);
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-light';
+
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
+      }
     }
   }
 }
 
 //== Dark Mode
 //
-.theme[data-color-scheme='dark'] :global {
-  $primary: #115cd3;
-  $accent: #db00c2;
-
-  // Background colors
-  $bgcolor-global: #131418;
-  $bgcolor-inline-code: #1f1f22; //optional
-  $bgcolor-card: darken($bgcolor-global, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.4);
-  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
-
-  // Font colors
-  $color-global: $gray-400;
-  $color-reversal: $gray-900;
-  // $color-header: desaturate($primary, 20%);
-  $color-link: #7b9ad5;
-  $color-link-hover: lighten($color-link, 10%);
-  $color-link-wiki: $color-link;
-  $color-link-wiki-hover: lighten($color-link-wiki, 10%);
-  $color-link-nabvar: #a7a7a7;
-  $color-inline-code: #c7254e; // optional
-
-  // List Group colors
-  // $color-list: $color-global; // optional
-  // $bgcolor-list: $bgcolor-global; // optional
-  // $color-list-hover: $color-global; // optional
-  // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
-  // $color-list-active:white ; // optional
-  // $bgcolor-list-active: $primary; // optional
-
-  // Table colors
-  // $color-table: #; // optional
-  // $bgcolor-table: #; // optional
-  // $border-color-table: #; // optional
-  // $color-table-hover: #; // optional
-  // $bgcolor-table-hover: #; // optional
-
-  // Navbar
-  $bgcolor-navbar: #2a2929;
-  $bgcolor-search-top-dropdown: $accent;
-  $border-image-navbar: linear-gradient(to right, #44bfe3 0%, #b04aff 50%, #ff1794 100%);
-
-  // Logo colors
-  $bgcolor-logo: $bgcolor-navbar;
-  $fillcolor-logo-mark: $gray-700;
-
-  // Sidebar
-  $bgcolor-sidebar: #122c55;
-  $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
-  // Sidebar resize button
-  $color-resize-button: white;
-  $bgcolor-resize-button: $accent;
-  $color-resize-button-hover: white;
-  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $bgcolor-sidebar-context: lighten($bgcolor-global, 8%);
-  $color-sidebar-context: $color-global;
-  // Sidebar list group
-  $bgcolor-sidebar-list-group: #1c2a3e; // optional
-
-  // Subnavigation
-  $bgcolor-subnav: lighten($bgcolor-global, 4%); // optional
-
-  // Tabs
-  $bordercolor-nav-tabs: $gray-700; // optional
-  // $color-nav-tabs-link-active: #; //optional
-  $bordercolor-nav-tabs-hover: #666 #666 $bordercolor-nav-tabs; // optional
-  // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
-
-  // Tags
-  // $color-tags: #; //optional
-  // $bgcolor-tags: #; //optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $gray-400;
-  $bordercolor-inline-code: $secondary; // optional
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-dark';
-
-  //Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 30%), lighten($primary, 20%), $primary, darken($primary, 20%));
+.theme {
+  [data-color-scheme='dark'] :global {
+    $primary: #115cd3;
+    $accent: #db00c2;
+
+    // Background colors
+    $bgcolor-global: #131418;
+    $bgcolor-inline-code: #1f1f22; //optional
+    $bgcolor-card: darken($bgcolor-global, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.4);
+    $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+    // Font colors
+    $color-global: $gray-400;
+    $color-reversal: $gray-900;
+    // $color-header: desaturate($primary, 20%);
+    $color-link: #7b9ad5;
+    $color-link-hover: lighten($color-link, 10%);
+    $color-link-wiki: $color-link;
+    $color-link-wiki-hover: lighten($color-link-wiki, 10%);
+    $color-link-nabvar: #a7a7a7;
+    $color-inline-code: #c7254e; // optional
+
+    // List Group colors
+    // $color-list: $color-global; // optional
+    // $bgcolor-list: $bgcolor-global; // optional
+    // $color-list-hover: $color-global; // optional
+    // $bgcolor-list-hover: lighten($bgcolor-global, 3%); // optional
+    // $color-list-active:white ; // optional
+    // $bgcolor-list-active: $primary; // optional
+
+    // Table colors
+    // $color-table: #; // optional
+    // $bgcolor-table: #; // optional
+    // $border-color-table: #; // optional
+    // $color-table-hover: #; // optional
+    // $bgcolor-table-hover: #; // optional
+
+    // Navbar
+    $bgcolor-navbar: #2a2929;
+    $bgcolor-search-top-dropdown: $accent;
+    $border-image-navbar: linear-gradient(to right, #44bfe3 0%, #b04aff 50%, #ff1794 100%);
+
+    // Logo colors
+    $bgcolor-logo: $bgcolor-navbar;
+    $fillcolor-logo-mark: $gray-700;
+
+    // Sidebar
+    $bgcolor-sidebar: #122c55;
+    $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+    // Sidebar resize button
+    $color-resize-button: white;
+    $bgcolor-resize-button: $accent;
+    $color-resize-button-hover: white;
+    $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $bgcolor-sidebar-context: lighten($bgcolor-global, 8%);
+    $color-sidebar-context: $color-global;
+    // Sidebar list group
+    $bgcolor-sidebar-list-group: #1c2a3e; // optional
+
+    // Subnavigation
+    $bgcolor-subnav: lighten($bgcolor-global, 4%); // optional
+
+    // Tabs
+    $bordercolor-nav-tabs: $gray-700; // optional
+    // $color-nav-tabs-link-active: #; //optional
+    $bordercolor-nav-tabs-hover: #666 #666 $bordercolor-nav-tabs; // optional
+    // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
+
+    // Tags
+    // $color-tags: #; //optional
+    // $bgcolor-tags: #; //optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $gray-400;
+    $bordercolor-inline-code: $secondary; // optional
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-dark';
+
+    //Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 30%), lighten($primary, 20%), $primary, darken($primary, 20%));
+      }
     }
   }
 }

+ 192 - 188
packages/app/src/components/Theme/ThemeFireRed.module.scss

@@ -2,204 +2,208 @@
 @use '../../styles/bootstrap/variables' as *;
 @use '../../styles/theme/mixins/page-editor-mode-manager';
 
-.theme[data-color-scheme='light'] :global {
-  // Theme colors
-  $themecolor: #ea5532;
-  $themelight: #ffffff;
-  $accentcolor: #bfbfbf;
-  $subthemecolor: #e6e6e6;
-
-  $primary: $themecolor;
-
-  // Background colors
-  $bgcolor-global: $themelight;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: $accentcolor;
-  $bgcolor-blinked-section: rgba($primary, 0.1);
-  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
-
-  // Font colors
-  $color-global: #2c2c2c;
-  $color-reversal: $gray-100;
-  $color-link: $primary;
-  $color-link-hover: lighten($color-link, 12%);
-  $color-link-wiki: $primary;
-  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e; // optional
-  $color-search: $color-global;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $color-search;
-  $bgcolor-list-hover: darken($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: $color-global;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
-
-  // Logo colors
-  $bgcolor-logo: $themelight;
-  $fillcolor-logo-mark: $themelight;
-
-  // Sidebar
-  $bgcolor-sidebar: $accentcolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
-  // Sidebar resize button
-  $color-resize-button: #ffffff;
-  $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-reversal;
-  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: #ececec;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $primary;
-  $bordercolor-inline-code: #ccc8c8; // optional
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-light';
-
-  // Navs {
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        background-color: transparent;
+.theme {
+  [data-color-scheme='light'] :global {
+    // Theme colors
+    $themecolor: #ea5532;
+    $themelight: #ffffff;
+    $accentcolor: #bfbfbf;
+    $subthemecolor: #e6e6e6;
+
+    $primary: $themecolor;
+
+    // Background colors
+    $bgcolor-global: $themelight;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: $accentcolor;
+    $bgcolor-blinked-section: rgba($primary, 0.1);
+    //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+    // Font colors
+    $color-global: #2c2c2c;
+    $color-reversal: $gray-100;
+    $color-link: $primary;
+    $color-link-hover: lighten($color-link, 12%);
+    $color-link-wiki: $primary;
+    $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: #c7254e; // optional
+    $color-search: $color-global;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $color-search;
+    $bgcolor-list-hover: darken($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: $color-global;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+    // Logo colors
+    $bgcolor-logo: $themelight;
+    $fillcolor-logo-mark: $themelight;
+
+    // Sidebar
+    $bgcolor-sidebar: $accentcolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
+    // Sidebar resize button
+    $color-resize-button: #ffffff;
+    $bgcolor-resize-button: $primary;
+    $color-resize-button-hover: $color-reversal;
+    $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $color-sidebar-context: $color-global;
+    $bgcolor-sidebar-context: #ececec;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $primary;
+    $bordercolor-inline-code: #ccc8c8; // optional
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-light';
+
+    // Navs {
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          background-color: transparent;
+        }
       }
     }
-  }
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+      }
     }
   }
 }
 
-.theme[data-color-scheme='dark'] :global {
-  // Theme colors
-  $themecolor: #ea5532;
-  $themedark: #333333;
-  $accentcolor: #212121;
-  $subthemecolor: #2e2e2e;
-
-  $primary: #ea5532;
-  $dark: #a7a7a7;
-
-  // Background colors
-  $bgcolor-global: $themedark;
-  $bgcolor-navbar: #2b2b2b;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.5);
-  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
-
-  // Font colors
-  $color-global: #ffffff;
-  $color-reversal: $gray-100;
-  $color-link: $primary;
-  $color-link-hover: lighten($color-link, 12%);
-  $color-link-wiki: $primary;
-  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: $subthemecolor;
-  $color-inline-code: #c7254e; // optional
-  $color-search: $dark;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $accentcolor;
-  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: #2c2c2c;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #ea5532 0%, #c9171e 100%);
-
-  // Logo colors
-  $bgcolor-logo: #ffffff;
-  $fillcolor-logo-mark: #ffffff;
-  // $fillcolor-logo-mark: #4e5a60;
-
-  // Sidebar
-  $bgcolor-sidebar: $accentcolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-global;
-  $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-global;
-  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $bgcolor-sidebar-context: #413f3f;
-  $color-sidebar-context: $color-global;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $primary;
-  $bordercolor-inline-code: #4d4d4d; // optional
-
-  // Dropdown colors
-  $color-dropdown-link-active: $color-global;
-  $color-dropdown-link-hover: $color-reversal;
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-dark';
-
-  // Navs
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        color: $color-link;
-        background-color: transparent;
-        border-color: $border-color-theme;
+.theme {
+  [data-color-scheme='dark'] :global {
+    // Theme colors
+    $themecolor: #ea5532;
+    $themedark: #333333;
+    $accentcolor: #212121;
+    $subthemecolor: #2e2e2e;
+
+    $primary: #ea5532;
+    $dark: #a7a7a7;
+
+    // Background colors
+    $bgcolor-global: $themedark;
+    $bgcolor-navbar: #2b2b2b;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: darken($themedark, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.5);
+    $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+    // Font colors
+    $color-global: #ffffff;
+    $color-reversal: $gray-100;
+    $color-link: $primary;
+    $color-link-hover: lighten($color-link, 12%);
+    $color-link-wiki: $primary;
+    $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: $subthemecolor;
+    $color-inline-code: #c7254e; // optional
+    $color-search: $dark;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $accentcolor;
+    // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: #2c2c2c;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, #ea5532 0%, #c9171e 100%);
+
+    // Logo colors
+    $bgcolor-logo: #ffffff;
+    $fillcolor-logo-mark: #ffffff;
+    // $fillcolor-logo-mark: #4e5a60;
+
+    // Sidebar
+    $bgcolor-sidebar: $accentcolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-global;
+    $bgcolor-resize-button: $primary;
+    $color-resize-button-hover: $color-global;
+    $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $bgcolor-sidebar-context: #413f3f;
+    $color-sidebar-context: $color-global;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $primary;
+    $bordercolor-inline-code: #4d4d4d; // optional
+
+    // Dropdown colors
+    $color-dropdown-link-active: $color-global;
+    $color-dropdown-link-hover: $color-reversal;
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-dark';
+
+    // Navs
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          color: $color-link;
+          background-color: transparent;
+          border-color: $border-color-theme;
+        }
       }
     }
-  }
 
-  // Table
-  .table {
-    color: white;
-  }
+    // Table
+    .table {
+      color: white;
+    }
 
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+      }
     }
   }
 }

+ 20 - 2
packages/app/src/components/Theme/ThemeHalloween.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeHalloween = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeHalloween = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeHalloween;

+ 257 - 253
packages/app/src/components/Theme/ThemeHufflepuff.module.scss

@@ -33,289 +33,293 @@
 
 //== Light Mode
 //
-.theme[data-color-scheme='light'] :global {
-  // Theme colors
-  $themecolor: #eaab20;
-  $themelight: #efe2cf;
-  $subthemecolor: #231e1d;
-  $third-main-color: #f0c05a;
-  $accentcolor: #993439;
-
-  $primary: $themecolor;
-  // $secondary: $subthemecolor;
-  $secondary: $third-main-color;
-
-  // Background colors
-  $bgcolor-global: lighten($themelight, 10%);
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: $gray-100;
-  $bgcolor-blinked-section: rgba($primary, 0.5);
-  $bgcolor-keyword-highlighted: $grw-marker-green;
-
-  // Font colors
-  $color-global: $subthemecolor;
-  $color-reversal: white;
-  $color-link: $accentcolor;
-  $color-link-hover: lighten($accentcolor, 10%);
-  $color-link-wiki: $accentcolor;
-  $color-link-wiki-hover: lighten($color-link-wiki, 10%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e; // optional
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: lighten($themecolor, 10%);
-  // $bgcolor-list-hover: darken($bgcolor-list, 2%);
-  // $color-list-active: $bgcolor-global;
-  // $bgcolor-list-active: $accentcolor;
-
-  // Navbar
-  $bgcolor-navbar: $third-main-color;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #90a555 0%, #a84be6 50%, #eaab20 100%);
-
-  // Logo colors
-  $bgcolor-logo: $bgcolor-navbar;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
-
-  // Sidebar
-  $bgcolor-sidebar: $themecolor;
-  // Sidebar resize button
-  $color-resize-button: $color-reversal;
-  $bgcolor-resize-button: $subthemecolor;
-  $color-resize-button-hover: $color-reversal;
-  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 10%);
-  // Sidebar contents
-  $color-sidebar-context: $accentcolor;
-  $bgcolor-sidebar-context: lighten($themelight, 8%);
-  // Sidebar list group
-  $bgcolor-sidebar-list-group: lighten($themelight, 10%);
-
-  // Icon colors
-  $color-editor-icons: $accentcolor;
-
-  // Border colors
-  $border-color-theme: lighten($subthemecolor, 40%);
-  $bordercolor-inline-code: #ccc8c8; // optional
-
-  // Dropdown colors
-  $bgcolor-dropdown-link-active: $growi-blue;
-
-  // admin theme box
-  $color-theme-color-box: darken($primary, 5%);
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-light';
-
-  //Button
-  .btn.btn-outline-primary {
-    @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 50%), darken($primary, 50%), lighten($primary, 20%));
-  }
-  .btn-group.grw-page-editor-mode-manager {
+.theme {
+  [data-color-scheme='light'] :global {
+    // Theme colors
+    $themecolor: #eaab20;
+    $themelight: #efe2cf;
+    $subthemecolor: #231e1d;
+    $third-main-color: #f0c05a;
+    $accentcolor: #993439;
+
+    $primary: $themecolor;
+    // $secondary: $subthemecolor;
+    $secondary: $third-main-color;
+
+    // Background colors
+    $bgcolor-global: lighten($themelight, 10%);
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: $gray-100;
+    $bgcolor-blinked-section: rgba($primary, 0.5);
+    $bgcolor-keyword-highlighted: $grw-marker-green;
+
+    // Font colors
+    $color-global: $subthemecolor;
+    $color-reversal: white;
+    $color-link: $accentcolor;
+    $color-link-hover: lighten($accentcolor, 10%);
+    $color-link-wiki: $accentcolor;
+    $color-link-wiki-hover: lighten($color-link-wiki, 10%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: #c7254e; // optional
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: lighten($themecolor, 10%);
+    // $bgcolor-list-hover: darken($bgcolor-list, 2%);
+    // $color-list-active: $bgcolor-global;
+    // $bgcolor-list-active: $accentcolor;
+
+    // Navbar
+    $bgcolor-navbar: $third-main-color;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, #90a555 0%, #a84be6 50%, #eaab20 100%);
+
+    // Logo colors
+    $bgcolor-logo: $bgcolor-navbar;
+    $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
+
+    // Sidebar
+    $bgcolor-sidebar: $themecolor;
+    // Sidebar resize button
+    $color-resize-button: $color-reversal;
+    $bgcolor-resize-button: $subthemecolor;
+    $color-resize-button-hover: $color-reversal;
+    $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 10%);
+    // Sidebar contents
+    $color-sidebar-context: $accentcolor;
+    $bgcolor-sidebar-context: lighten($themelight, 8%);
+    // Sidebar list group
+    $bgcolor-sidebar-list-group: lighten($themelight, 10%);
+
+    // Icon colors
+    $color-editor-icons: $accentcolor;
+
+    // Border colors
+    $border-color-theme: lighten($subthemecolor, 40%);
+    $bordercolor-inline-code: #ccc8c8; // optional
+
+    // Dropdown colors
+    $bgcolor-dropdown-link-active: $growi-blue;
+
+    // admin theme box
+    $color-theme-color-box: darken($primary, 5%);
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-light';
+
+    //Button
     .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 70%), lighten($primary, 5%), lighten($primary, 20%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 50%), darken($primary, 50%), lighten($primary, 20%));
     }
-  }
-
-  .growi:not(.login-page) {
-    // add background-image
-    .page-editor-preview-container {
-      background-image: url('/images/themes/hufflepuff/badger-light3.png');
-      background-attachment: fixed;
-      background-position: bottom;
-      background-size: cover;
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(darken($primary, 70%), lighten($primary, 5%), lighten($primary, 20%));
+      }
     }
-  }
 
-  // login and register
-  .nologin {
-    #page-wrapper {
-      background-color: $themelight;
-      background-image: url('/images/themes/hufflepuff/badger-light.png');
-      background-attachment: fixed;
-      background-position: bottom;
-      background-size: cover;
+    .growi:not(.login-page) {
+      // add background-image
+      .page-editor-preview-container {
+        background-image: url('/images/themes/hufflepuff/badger-light3.png');
+        background-attachment: fixed;
+        background-position: bottom;
+        background-size: cover;
+      }
     }
 
-    .noLogin-header,
-    .noLogin-dialog {
-      background-color: rgba(black, 0.1);
-    }
+    // login and register
+    .nologin {
+      #page-wrapper {
+        background-color: $themelight;
+        background-image: url('/images/themes/hufflepuff/badger-light.png');
+        background-attachment: fixed;
+        background-position: bottom;
+        background-size: cover;
+      }
 
-    .link-switch {
-      color: $color-global;
-    }
+      .noLogin-header,
+      .noLogin-dialog {
+        background-color: rgba(black, 0.1);
+      }
 
-    .grw-external-auth-form {
-      border-color: $accentcolor !important;
+      .link-switch {
+        color: $color-global;
+      }
+
+      .grw-external-auth-form {
+        border-color: $accentcolor !important;
+      }
     }
-  }
 
-  .table {
-    background-color: $bgcolor-global;
-  }
+    .table {
+      background-color: $bgcolor-global;
+    }
 
-  .card-timeline > .card-header {
-    background-color: $third-main-color;
-  }
+    .card-timeline > .card-header {
+      background-color: $third-main-color;
+    }
 
-  .nav.nav-tabs {
-    > .nav-item {
-      > .nav-link.active {
-        color: $subthemecolor;
+    .nav.nav-tabs {
+      > .nav-item {
+        > .nav-link.active {
+          color: $subthemecolor;
+        }
       }
     }
   }
 }
 
-.theme[data-color-scheme='dark'] :global {
-  // Theme colors
-  $themecolor: #eaab20;
-  $themedark: #3d3f38;
-  $subthemecolor: #231e1d;
-  $third-main-color: #967224;
-  $accentcolor: #993439;
-
-  $primary: darken($themecolor, 10%);
-  $secondary: $third-main-color;
-  $dark: #031018;
-
-  // Background colors
-  $bgcolor-global: $themedark;
-  // $bgcolor-navbar: #27343b;
-  $bgcolor-inline-code: $subthemecolor;
-  $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.5);
-  $bgcolor-keyword-highlighted: darken($grw-marker-cyan, 40%);
-
-  // Font colors
-  $color-global: #efe2cf;
-  $color-reversal: $gray-100;
-  $color-link: lighten($themecolor, 20%);
-  $color-link-hover: lighten($color-link, 10%);
-  $color-link-wiki: lighten($primary, 20%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: $themecolor;
-  // $color-inline-code: #c7254e; // optional
-  // $color-search: #000102;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $accentcolor;
-  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: $third-main-color;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #90a555 0%, #3d98a3 50%, #eaab20 100%);
-
-  // Logo colors
-  $bgcolor-logo: #13191c;
-  $fillcolor-logo-mark: white;
-
-  // Sidebar
-  $bgcolor-sidebar: $themecolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #cc951e; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-global;
-  $bgcolor-resize-button: $accentcolor;
-  $color-resize-button-hover: $color-global;
-  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 7%);
-  // Sidebar contents
-  $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: lighten($themedark, 5%);
-  // Sidebar list group
-  $bgcolor-sidebar-list-group: lighten($subthemecolor, 5%);
-
-  // Icon colors
-  $color-editor-icons: $themecolor;
-
-  // Border colors
-  $border-color-theme: darken($themecolor, 25%);
-  $bordercolor-inline-code: #4d4d4d; // optional
-
-  // Dropdown colors
-  $color-dropdown-link-active: $color-reversal;
-  $color-dropdown-link-hover: $color-global;
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-dark';
-
-  // Navs
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        color: $color-link;
-        background-color: transparent;
-        border-color: $border-color-theme;
+.theme {
+  [data-color-scheme='dark'] :global {
+    // Theme colors
+    $themecolor: #eaab20;
+    $themedark: #3d3f38;
+    $subthemecolor: #231e1d;
+    $third-main-color: #967224;
+    $accentcolor: #993439;
+
+    $primary: darken($themecolor, 10%);
+    $secondary: $third-main-color;
+    $dark: #031018;
+
+    // Background colors
+    $bgcolor-global: $themedark;
+    // $bgcolor-navbar: #27343b;
+    $bgcolor-inline-code: $subthemecolor;
+    $bgcolor-card: darken($themedark, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.5);
+    $bgcolor-keyword-highlighted: darken($grw-marker-cyan, 40%);
+
+    // Font colors
+    $color-global: #efe2cf;
+    $color-reversal: $gray-100;
+    $color-link: lighten($themecolor, 20%);
+    $color-link-hover: lighten($color-link, 10%);
+    $color-link-wiki: lighten($primary, 20%);
+    $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: $themecolor;
+    // $color-inline-code: #c7254e; // optional
+    // $color-search: #000102;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $accentcolor;
+    // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: $third-main-color;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, #90a555 0%, #3d98a3 50%, #eaab20 100%);
+
+    // Logo colors
+    $bgcolor-logo: #13191c;
+    $fillcolor-logo-mark: white;
+
+    // Sidebar
+    $bgcolor-sidebar: $themecolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #cc951e; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-global;
+    $bgcolor-resize-button: $accentcolor;
+    $color-resize-button-hover: $color-global;
+    $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 7%);
+    // Sidebar contents
+    $color-sidebar-context: $color-global;
+    $bgcolor-sidebar-context: lighten($themedark, 5%);
+    // Sidebar list group
+    $bgcolor-sidebar-list-group: lighten($subthemecolor, 5%);
+
+    // Icon colors
+    $color-editor-icons: $themecolor;
+
+    // Border colors
+    $border-color-theme: darken($themecolor, 25%);
+    $bordercolor-inline-code: #4d4d4d; // optional
+
+    // Dropdown colors
+    $color-dropdown-link-active: $color-reversal;
+    $color-dropdown-link-hover: $color-global;
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-dark';
+
+    // Navs
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          color: $color-link;
+          background-color: transparent;
+          border-color: $border-color-theme;
+        }
       }
     }
-  }
 
-  // Table
-  .table {
-    color: white;
-  }
+    // Table
+    .table {
+      color: white;
+    }
 
-  // Button
-  .btn.btn-outline-primary {
-    @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 10%), darken($primary, 30%));
-  }
-  .btn-group.grw-page-editor-mode-manager {
+    // Button
     .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 0%), darken($primary, 30%));
+      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 10%), darken($primary, 30%));
     }
-  }
-
-  .card-timeline > .card-header {
-    background-color: $accentcolor;
-  }
-
-  .growi:not(.login-page) {
-    // add background-image
-    .page-editor-preview-container {
-      background-image: url('/images/themes/hufflepuff/badger-dark.jpg');
-      background-attachment: fixed;
-      background-position: bottom;
-      background-size: cover;
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 40%), lighten($primary, 15%), darken($primary, 0%), darken($primary, 30%));
+      }
     }
-  }
 
-  // login and register
-  .nologin {
-    #page-wrapper {
-      background-color: $themedark;
-      background-image: url('/images/themes/hufflepuff/badger-light.png');
-      background-attachment: fixed;
-      background-position: bottom;
-      background-size: cover;
+    .card-timeline > .card-header {
+      background-color: $accentcolor;
     }
 
-    .noLogin-header,
-    .noLogin-dialog {
-      background-color: rgba(black, 0.1);
+    .growi:not(.login-page) {
+      // add background-image
+      .page-editor-preview-container {
+        background-image: url('/images/themes/hufflepuff/badger-dark.jpg');
+        background-attachment: fixed;
+        background-position: bottom;
+        background-size: cover;
+      }
     }
 
-    .link-switch {
-      color: $color-global;
-    }
+    // login and register
+    .nologin {
+      #page-wrapper {
+        background-color: $themedark;
+        background-image: url('/images/themes/hufflepuff/badger-light.png');
+        background-attachment: fixed;
+        background-position: bottom;
+        background-size: cover;
+      }
 
-    .grw-external-auth-form {
-      border-color: $accentcolor !important;
+      .noLogin-header,
+      .noLogin-dialog {
+        background-color: rgba(black, 0.1);
+      }
+
+      .link-switch {
+        color: $color-global;
+      }
+
+      .grw-external-auth-form {
+        border-color: $accentcolor !important;
+      }
     }
   }
 }

+ 20 - 2
packages/app/src/components/Theme/ThemeHufflepuff.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -15,7 +17,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeHufflepuff = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeHufflepuff = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeHufflepuff;

+ 20 - 2
packages/app/src/components/Theme/ThemeIsland.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeIsland = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeIsland = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeIsland;

+ 192 - 188
packages/app/src/components/Theme/ThemeJadeGreen.module.scss

@@ -2,204 +2,208 @@
 @use '../../styles/bootstrap/variables' as *;
 @use '../../styles/theme/mixins/page-editor-mode-manager';
 
-.theme[data-color-scheme='light'] :global {
-  // Theme colors
-  $themecolor: #38b48b;
-  $themelight: #ffffff;
-  $accentcolor: #bfbfbf;
-  $subthemecolor: #e6e6e6;
-
-  $primary: $themecolor;
-
-  // Background colors
-  $bgcolor-global: $themelight;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: $accentcolor;
-  $bgcolor-blinked-section: rgba($primary, 0.1);
-  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
-
-  // Font colors
-  $color-global: #2c2c2c;
-  $color-reversal: $gray-100;
-  $color-link: $primary;
-  $color-link-hover: lighten($color-link, 12%);
-  $color-link-wiki: $primary;
-  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e; // optional
-  $color-search: $color-global;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $color-search;
-  $bgcolor-list-hover: darken($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: $color-global;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
-
-  // Logo colors
-  $bgcolor-logo: $themelight;
-  $fillcolor-logo-mark: $themelight;
-
-  // Sidebar
-  $bgcolor-sidebar: $accentcolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
-  // Sidebar resize button
-  $color-resize-button: #ffffff;
-  $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-reversal;
-  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: #ebebeb;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $primary;
-  $bordercolor-inline-code: #ccc8c8; // optional
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-light';
-
-  // Navs {
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        background-color: transparent;
+.theme {
+  [data-color-scheme='light'] :global {
+    // Theme colors
+    $themecolor: #38b48b;
+    $themelight: #ffffff;
+    $accentcolor: #bfbfbf;
+    $subthemecolor: #e6e6e6;
+
+    $primary: $themecolor;
+
+    // Background colors
+    $bgcolor-global: $themelight;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: $accentcolor;
+    $bgcolor-blinked-section: rgba($primary, 0.1);
+    //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+    // Font colors
+    $color-global: #2c2c2c;
+    $color-reversal: $gray-100;
+    $color-link: $primary;
+    $color-link-hover: lighten($color-link, 12%);
+    $color-link-wiki: $primary;
+    $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: #c7254e; // optional
+    $color-search: $color-global;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $color-search;
+    $bgcolor-list-hover: darken($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: $color-global;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+    // Logo colors
+    $bgcolor-logo: $themelight;
+    $fillcolor-logo-mark: $themelight;
+
+    // Sidebar
+    $bgcolor-sidebar: $accentcolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #ffffff; // optional
+    // Sidebar resize button
+    $color-resize-button: #ffffff;
+    $bgcolor-resize-button: $primary;
+    $color-resize-button-hover: $color-reversal;
+    $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $color-sidebar-context: $color-global;
+    $bgcolor-sidebar-context: #ebebeb;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $primary;
+    $bordercolor-inline-code: #ccc8c8; // optional
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-light';
+
+    // Navs {
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          background-color: transparent;
+        }
       }
     }
-  }
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, lighten($primary, 20%));
+      }
     }
   }
 }
 
-.theme[data-color-scheme='dark'] :global {
-  // Theme colors
-  $themecolor: #38b48b;
-  $themedark: #333333;
-  $accentcolor: #212121;
-  $subthemecolor: #2e2e2e;
-
-  $primary: #38b48b;
-  $dark: #a7a7a7;
-
-  // Background colors
-  $bgcolor-global: $themedark;
-  $bgcolor-navbar: #2b2b2b;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.5);
-  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
-
-  // Font colors
-  $color-global: #ffffff;
-  $color-reversal: $gray-100;
-  $color-link: $primary;
-  $color-link-hover: lighten($color-link, 12%);
-  $color-link-wiki: $primary;
-  $color-link-wiki-hover: lighten($color-link-wiki, 12%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: $subthemecolor;
-  $color-inline-code: #c7254e; // optional
-  $color-search: $dark;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $accentcolor;
-  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: #2c2c2c;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
-
-  // Logo colors
-  $bgcolor-logo: #ffffff;
-  $fillcolor-logo-mark: #ffffff;
-  // $fillcolor-logo-mark: #4e5a60;
-
-  // Sidebar
-  $bgcolor-sidebar: $accentcolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-global;
-  $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-global;
-  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $bgcolor-sidebar-context: #3c403c;
-  $color-sidebar-context: $color-global;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $primary;
-  $bordercolor-inline-code: #4d4d4d; // optional
-
-  // Dropdown colors
-  $color-dropdown-link-active: $color-global;
-  $color-dropdown-link-hover: $color-reversal;
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-dark';
-
-  // Navs
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        color: $color-link;
-        background-color: transparent;
-        border-color: $border-color-theme;
+.theme {
+  [data-color-scheme='dark'] :global {
+    // Theme colors
+    $themecolor: #38b48b;
+    $themedark: #333333;
+    $accentcolor: #212121;
+    $subthemecolor: #2e2e2e;
+
+    $primary: #38b48b;
+    $dark: #a7a7a7;
+
+    // Background colors
+    $bgcolor-global: $themedark;
+    $bgcolor-navbar: #2b2b2b;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: darken($themedark, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.5);
+    $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+    // Font colors
+    $color-global: #ffffff;
+    $color-reversal: $gray-100;
+    $color-link: $primary;
+    $color-link-hover: lighten($color-link, 12%);
+    $color-link-wiki: $primary;
+    $color-link-wiki-hover: lighten($color-link-wiki, 12%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: $subthemecolor;
+    $color-inline-code: #c7254e; // optional
+    $color-search: $dark;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $accentcolor;
+    // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: #2c2c2c;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, $primary 0%, darken($primary, 5%) 100%);
+
+    // Logo colors
+    $bgcolor-logo: #ffffff;
+    $fillcolor-logo-mark: #ffffff;
+    // $fillcolor-logo-mark: #4e5a60;
+
+    // Sidebar
+    $bgcolor-sidebar: $accentcolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px $primary; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-global;
+    $bgcolor-resize-button: $primary;
+    $color-resize-button-hover: $color-global;
+    $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $bgcolor-sidebar-context: #3c403c;
+    $color-sidebar-context: $color-global;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $primary;
+    $bordercolor-inline-code: #4d4d4d; // optional
+
+    // Dropdown colors
+    $color-dropdown-link-active: $color-global;
+    $color-dropdown-link-hover: $color-reversal;
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-dark';
+
+    // Navs
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          color: $color-link;
+          background-color: transparent;
+          border-color: $border-color-theme;
+        }
       }
     }
-  }
 
-  // Table
-  .table {
-    color: white;
-  }
+    // Table
+    .table {
+      color: white;
+    }
 
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(#ffffff, $primary, $primary, darken($primary, 20%));
+      }
     }
   }
 }

+ 20 - 20
packages/app/src/components/Theme/ThemeKibela.module.scss

@@ -7,26 +7,6 @@ $themelight: #f4f5f6;
 $subthemecolor: rgb(88, 130, 250);
 $lightthemecolor: rgba(181, 203, 247, 0.61);
 
-.main {
-  .container,
-  .container-sm,
-  .container-md,
-  .container-lg,
-  .container-fluid {
-    padding-top: 30px;
-    padding-bottom: 30px;
-    background-color: white;
-    border-radius: 0.35em;
-  }
-}
-
-.user-page-footer {
-  margin-top: 3rem;
-  margin-bottom: 3rem;
-  background-color: white;
-  border-radius: 0.35em;
-}
-
 // Light Mode
 .theme :global {
   // Background colors
@@ -101,6 +81,26 @@ $lightthemecolor: rgba(181, 203, 247, 0.61);
   @import '../../styles/theme/apply-colors';
   @import '../../styles/theme/apply-colors-light';
 
+  .main {
+    .container,
+    .container-sm,
+    .container-md,
+    .container-lg,
+    .container-fluid {
+      padding-top: 30px;
+      padding-bottom: 30px;
+      background-color: white;
+      border-radius: 0.35em;
+    }
+  }
+
+  .user-page-footer {
+    margin-top: 3rem;
+    margin-bottom: 3rem;
+    background-color: white;
+    border-radius: 0.35em;
+  }
+
   //Button
   .grw-page-editor-mode-manager {
     .btn.btn-outline-primary {

+ 188 - 184
packages/app/src/components/Theme/ThemeMonoBlue.module.scss

@@ -2,200 +2,204 @@
 @use '../../styles/bootstrap/variables' as *;
 @use '../../styles/theme/mixins/page-editor-mode-manager';
 
-.theme[data-color-scheme='light'] :global {
-  // Theme colors
-  $themecolor: #00587a;
-  $themelight: #f7fbfd;
-  $accentcolor: #16617d;
-  $subthemecolor: #186718;
-
-  $primary: $themecolor;
-
-  // Background colors
-  $bgcolor-global: $themelight;
-  $bgcolor-inline-code: $gray-100; //optional
-  $bgcolor-card: darken($themelight, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.1);
-  //$bgcolor-keyword-highlighted: $grw-marker-yellow;
-
-  // Font colors
-  $color-global: $themecolor;
-  $color-reversal: $gray-100;
-  $color-link: lighten($primary, 5%);
-  $color-link-hover: lighten($color-link, 12%);
-  $color-link-wiki: lighten($primary, 20%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e; // optional
-  $color-search: #c0d6df;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $color-search;
-  $bgcolor-list-hover: lighten($primary, 70%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: #2a2929;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
-
-  // Logo colors
-  $bgcolor-logo: $themecolor;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
-
-  // Sidebar
-  $bgcolor-sidebar: $themecolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-reversal;
-  $bgcolor-resize-button: #209fd8;
-  $color-resize-button-hover: $color-reversal;
-  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $color-sidebar-context: $color-global;
-  $bgcolor-sidebar-context: #f1fcff;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: $gray-300;
-  $bordercolor-inline-code: #ccc8c8; // optional
-
-  // admin theme box
-  $color-theme-color-box: lighten($primary, 20%);
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-light';
-
-  // Navs {
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        background-color: transparent;
+.theme {
+  [data-color-scheme='light'] :global {
+    // Theme colors
+    $themecolor: #00587a;
+    $themelight: #f7fbfd;
+    $accentcolor: #16617d;
+    $subthemecolor: #186718;
+
+    $primary: $themecolor;
+
+    // Background colors
+    $bgcolor-global: $themelight;
+    $bgcolor-inline-code: $gray-100; //optional
+    $bgcolor-card: darken($themelight, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.1);
+    //$bgcolor-keyword-highlighted: $grw-marker-yellow;
+
+    // Font colors
+    $color-global: $themecolor;
+    $color-reversal: $gray-100;
+    $color-link: lighten($primary, 5%);
+    $color-link-hover: lighten($color-link, 12%);
+    $color-link-wiki: lighten($primary, 20%);
+    $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: #c7254e; // optional
+    $color-search: #c0d6df;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $color-search;
+    $bgcolor-list-hover: lighten($primary, 70%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: #2a2929;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
+
+    // Logo colors
+    $bgcolor-logo: $themecolor;
+    $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+
+    // Sidebar
+    $bgcolor-sidebar: $themecolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-reversal;
+    $bgcolor-resize-button: #209fd8;
+    $color-resize-button-hover: $color-reversal;
+    $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $color-sidebar-context: $color-global;
+    $bgcolor-sidebar-context: #f1fcff;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: $gray-300;
+    $bordercolor-inline-code: #ccc8c8; // optional
+
+    // admin theme box
+    $color-theme-color-box: lighten($primary, 20%);
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-light';
+
+    // Navs {
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          background-color: transparent;
+        }
       }
     }
-  }
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager($primary, lighten($primary, 65%), lighten($primary, 70%));
+      }
     }
   }
 }
 
-.theme[data-color-scheme='dark'] :global {
-  // Theme colors
-  $themecolor: #00587a;
-  $themedark: #061f2f;
-  $accentcolor: #16617d;
-  $subthemecolor: #c1f1f0;
-
-  $primary: #0090c8;
-  $dark: #031018;
-
-  // Background colors
-  $bgcolor-global: $themedark;
-  $bgcolor-navbar: #27343b;
-  $bgcolor-inline-code: #1f1f22; //optional
-  $bgcolor-card: darken($themedark, 5%);
-  $bgcolor-blinked-section: rgba($primary, 0.5);
-  $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
-
-  // Font colors
-  $color-global: #d3d4d4;
-  $color-reversal: $gray-100;
-  $color-link: #97d1f0;
-  $color-link-hover: darken($color-link, 12%);
-  $color-link-wiki: lighten($primary, 20%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: $subthemecolor;
-  $color-inline-code: #c7254e; // optional
-  $color-search: #000102;
-
-  // List Group colors
-  // $color-list: $color-global;
-  $bgcolor-list: transparent;
-  $color-list-hover: $accentcolor;
-  // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
-  // $color-list-active: $color-reversal;
-  // $bgcolor-list-active: $primary;
-
-  // Navbar
-  $bgcolor-navbar: #2a2929;
-  $bgcolor-search-top-dropdown: $themecolor;
-  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
-
-  // Logo colors
-  $bgcolor-logo: #13191c;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
-  // $fillcolor-logo-mark: #4e5a60;
-
-  // Sidebar
-  $bgcolor-sidebar: $accentcolor;
-  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
-  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
-  // Sidebar resize button
-  $color-resize-button: $color-global;
-  $bgcolor-resize-button: $themecolor;
-  $color-resize-button-hover: $color-global;
-  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
-  // Sidebar contents
-  $bgcolor-sidebar-context: darken($bgcolor-sidebar, 13%);
-  $color-sidebar-context: $color-global;
-  // Sidebar list group
-  // $bgcolor-sidebar-list-group: #; // optional
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  // Border colors
-  $border-color-theme: #146aa0;
-  $bordercolor-inline-code: #4d4d4d; // optional
-
-  // admin theme box
-  $color-theme-color-box: $primary;
-
-  @import '../../styles/theme/apply-colors';
-  @import '../../styles/theme/apply-colors-dark';
-
-  // Navs
-  .nav-tabs {
-    border-bottom: $border-color-theme 1px solid;
-    .nav-link {
-      &:hover {
-        border-color: lighten($border-color-theme, 10%);
-        border-bottom: none;
-      }
-      &.active {
-        color: $color-link;
-        background-color: transparent;
-        border-color: $border-color-theme;
+.theme {
+  [data-color-scheme='dark'] :global {
+    // Theme colors
+    $themecolor: #00587a;
+    $themedark: #061f2f;
+    $accentcolor: #16617d;
+    $subthemecolor: #c1f1f0;
+
+    $primary: #0090c8;
+    $dark: #031018;
+
+    // Background colors
+    $bgcolor-global: $themedark;
+    $bgcolor-navbar: #27343b;
+    $bgcolor-inline-code: #1f1f22; //optional
+    $bgcolor-card: darken($themedark, 5%);
+    $bgcolor-blinked-section: rgba($primary, 0.5);
+    $bgcolor-keyword-highlighted: darken($grw-marker-red, 30%);
+
+    // Font colors
+    $color-global: #d3d4d4;
+    $color-reversal: $gray-100;
+    $color-link: #97d1f0;
+    $color-link-hover: darken($color-link, 12%);
+    $color-link-wiki: lighten($primary, 20%);
+    $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+    $color-link-nabvar: $color-reversal;
+    $color-inline-code: $subthemecolor;
+    $color-inline-code: #c7254e; // optional
+    $color-search: #000102;
+
+    // List Group colors
+    // $color-list: $color-global;
+    $bgcolor-list: transparent;
+    $color-list-hover: $accentcolor;
+    // $bgcolor-list-hover: lighten($bgcolor-global, 3%);
+    // $color-list-active: $color-reversal;
+    // $bgcolor-list-active: $primary;
+
+    // Navbar
+    $bgcolor-navbar: #2a2929;
+    $bgcolor-search-top-dropdown: $themecolor;
+    $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
+
+    // Logo colors
+    $bgcolor-logo: #13191c;
+    $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+    // $fillcolor-logo-mark: #4e5a60;
+
+    // Sidebar
+    $bgcolor-sidebar: $accentcolor;
+    // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+    $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+    // Sidebar resize button
+    $color-resize-button: $color-global;
+    $bgcolor-resize-button: $themecolor;
+    $color-resize-button-hover: $color-global;
+    $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+    // Sidebar contents
+    $bgcolor-sidebar-context: darken($bgcolor-sidebar, 13%);
+    $color-sidebar-context: $color-global;
+    // Sidebar list group
+    // $bgcolor-sidebar-list-group: #; // optional
+
+    // Icon colors
+    $color-editor-icons: $color-global;
+
+    // Border colors
+    $border-color-theme: #146aa0;
+    $bordercolor-inline-code: #4d4d4d; // optional
+
+    // admin theme box
+    $color-theme-color-box: $primary;
+
+    @import '../../styles/theme/apply-colors';
+    @import '../../styles/theme/apply-colors-dark';
+
+    // Navs
+    .nav-tabs {
+      border-bottom: $border-color-theme 1px solid;
+      .nav-link {
+        &:hover {
+          border-color: lighten($border-color-theme, 10%);
+          border-bottom: none;
+        }
+        &.active {
+          color: $color-link;
+          background-color: transparent;
+          border-color: $border-color-theme;
+        }
       }
     }
-  }
 
-  // Table
-  .table {
-    color: white;
-  }
+    // Table
+    .table {
+      color: white;
+    }
 
-  // Button
-  .btn-group.grw-page-editor-mode-manager {
-    .btn.btn-outline-primary {
-      @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 30%), $primary, darken($primary, 10%), darken($primary, 20%));
+    // Button
+    .btn-group.grw-page-editor-mode-manager {
+      .btn.btn-outline-primary {
+        @include page-editor-mode-manager.btn-page-editor-mode-manager(lighten($primary, 30%), $primary, darken($primary, 10%), darken($primary, 20%));
+      }
     }
   }
 }

+ 20 - 2
packages/app/src/components/Theme/ThemeSpring.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeSpring = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeSpring = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeSpring;

+ 20 - 2
packages/app/src/components/Theme/ThemeWood.tsx

@@ -1,3 +1,5 @@
+import Image from 'next/image';
+
 import { Themes } from '~/stores/use-next-themes';
 
 import { ThemeInjector } from './utils/ThemeInjector';
@@ -11,7 +13,23 @@ export const getBackgroundImageSrc = (colorScheme: Themes): string => {
   }
 };
 
-const ThemeWood = ({ children }: { children: JSX.Element }): JSX.Element => {
-  return <ThemeInjector className={styles.theme}>{children}</ThemeInjector>;
+type Props = {
+  children: JSX.Element,
+  colorScheme?: Themes,
+}
+
+const ThemeWood = ({ children, colorScheme }: Props): JSX.Element => {
+  const newChildren = (
+    <>
+      {colorScheme != null && (
+        <div className='grw-bg-image-wrapper'>
+          <Image className='grw-bg-image' alt='background image' src={getBackgroundImageSrc(colorScheme)} layout='fill' quality="100" />
+        </div>
+      )}
+      {children}
+    </>
+  );
+  return <ThemeInjector className={styles.theme}>{newChildren}</ThemeInjector>;
 };
+
 export default ThemeWood;

+ 0 - 34
packages/app/src/components/Theme/utils/ThemeImageProvider.tsx

@@ -1,34 +0,0 @@
-import { GrowiThemes } from '~/interfaces/theme';
-import { Themes } from '~/stores/use-next-themes';
-
-import { getBackgroundImageSrc as getAntarcticBackgroundImageSrc } from '../ThemeAntarctic';
-import { getBackgroundImageSrc as getChristmasBackgroundImageSrc } from '../ThemeChristmas';
-import { getBackgroundImageSrc as getHalloweenBackgroundImageSrc } from '../ThemeHalloween';
-import { getBackgroundImageSrc as getHuffulePuffBackgroundImageSrc } from '../ThemeHufflepuff';
-import { getBackgroundImageSrc as getIslandBackgroundImageSrc } from '../ThemeIsland';
-import { getBackgroundImageSrc as getSpringBackgroundImageSrc } from '../ThemeSpring';
-import { getBackgroundImageSrc as getWoodBackgroundImageSrc } from '../ThemeWood';
-
-export const getBackgroundImageSrc = (theme: GrowiThemes | undefined, colorScheme: Themes | undefined): string | undefined => {
-  if (theme == null || colorScheme == null) {
-    return undefined;
-  }
-  switch (theme) {
-    case GrowiThemes.ANTARCTIC:
-      return getAntarcticBackgroundImageSrc(colorScheme);
-    case GrowiThemes.CHRISTMAS:
-      return getChristmasBackgroundImageSrc(colorScheme);
-    case GrowiThemes.HALLOWEEN:
-      return getHalloweenBackgroundImageSrc(colorScheme);
-    case GrowiThemes.ISLAND:
-      return getIslandBackgroundImageSrc(colorScheme);
-    case GrowiThemes.HUFFLEPUFF:
-      return getHuffulePuffBackgroundImageSrc(colorScheme);
-    case GrowiThemes.SPRING:
-      return getSpringBackgroundImageSrc(colorScheme);
-    case GrowiThemes.WOOD:
-      return getWoodBackgroundImageSrc(colorScheme);
-    default:
-      return undefined;
-  }
-};

+ 1 - 1
packages/app/src/components/Theme/utils/ThemeInjector.tsx

@@ -8,5 +8,5 @@ type Props = {
 
 export const ThemeInjector = ({ children, className: themeClassName }: Props): JSX.Element => {
   const className = `${children.props.className ?? ''} ${themeClassName}`;
-  return React.cloneElement(children, { className });
+  return React.cloneElement(<div>{children}</div>, { className });
 };

+ 10 - 8
packages/app/src/components/Theme/utils/ThemeProvider.tsx

@@ -4,6 +4,7 @@ import React from 'react';
 import dynamic from 'next/dynamic';
 
 import { GrowiThemes } from '~/interfaces/theme';
+import { Themes } from '~/stores/use-next-themes';
 
 
 const ThemeAntarctic = dynamic(() => import('../ThemeAntarctic'));
@@ -26,26 +27,27 @@ const ThemeWood = dynamic(() => import('../ThemeWood'));
 type Props = {
   children: JSX.Element,
   theme?: GrowiThemes,
+  colorScheme?: Themes,
 }
 
-export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
+export const ThemeProvider = ({ theme, children, colorScheme }: Props): JSX.Element => {
   switch (theme) {
     case GrowiThemes.ANTARCTIC:
-      return <ThemeAntarctic>{children}</ThemeAntarctic>;
+      return <ThemeAntarctic colorScheme={colorScheme}>{children}</ThemeAntarctic>;
     case GrowiThemes.BLACKBOARD:
       return <ThemeBlackboard>{children}</ThemeBlackboard>;
     case GrowiThemes.CHRISTMAS:
-      return <ThemeChristmas>{children}</ThemeChristmas>;
+      return <ThemeChristmas colorScheme={colorScheme}>{children}</ThemeChristmas>;
     case GrowiThemes.FIRE_RED:
       return <ThemeFireRed>{children}</ThemeFireRed>;
     case GrowiThemes.FUTURE:
       return <ThemeFuture>{children}</ThemeFuture>;
     case GrowiThemes.HALLOWEEN:
-      return <ThemeHalloween>{children}</ThemeHalloween>;
+      return <ThemeHalloween colorScheme={colorScheme}>{children}</ThemeHalloween>;
     case GrowiThemes.HUFFLEPUFF:
-      return <ThemeHufflepuff>{children}</ThemeHufflepuff>;
+      return <ThemeHufflepuff colorScheme={colorScheme}>{children}</ThemeHufflepuff>;
     case GrowiThemes.ISLAND:
-      return <ThemeIsland>{children}</ThemeIsland>;
+      return <ThemeIsland colorScheme={colorScheme}>{children}</ThemeIsland>;
     case GrowiThemes.JADE_GREEN:
       return <ThemeJadeGreen>{children}</ThemeJadeGreen>;
     case GrowiThemes.KIBELA:
@@ -55,9 +57,9 @@ export const ThemeProvider = ({ theme, children }: Props): JSX.Element => {
     case GrowiThemes.NATURE:
       return <ThemeNature>{children}</ThemeNature>;
     case GrowiThemes.SPRING:
-      return <ThemeSpring>{children}</ThemeSpring>;
+      return <ThemeSpring colorScheme={colorScheme}>{children}</ThemeSpring>;
     case GrowiThemes.WOOD:
-      return <ThemeWood>{children}</ThemeWood>;
+      return <ThemeWood colorScheme={colorScheme}>{children}</ThemeWood>;
     default:
       return <ThemeDefault>{children}</ThemeDefault>;
   }

+ 3 - 3
packages/app/src/components/TrashPageList.jsx → packages/app/src/components/TrashPageList.tsx

@@ -1,4 +1,4 @@
-import React, { useMemo } from 'react';
+import React, { FC, useMemo } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -8,7 +8,7 @@ import EmptyTrashButton from './EmptyTrashButton';
 import PageListIcon from './Icons/PageListIcon';
 
 
-const TrashPageList = () => {
+export const TrashPageList: FC = () => {
   const { t } = useTranslation();
 
   const navTabMapping = useMemo(() => {
@@ -33,4 +33,4 @@ const TrashPageList = () => {
   );
 };
 
-export default TrashPageList;
+TrashPageList.displayName = 'TrashPageList';

+ 4 - 0
packages/app/src/interfaces/share-link.ts

@@ -0,0 +1,4 @@
+// Todo: specify more detailed Type
+export type IResShareLinkList = {
+  shareLinksResult: any[],
+};

+ 5 - 5
packages/app/src/pages/[[...path]].page.tsx

@@ -71,6 +71,11 @@ import {
 // import { useCurrentPageSWR } from '../stores/page';
 
 
+const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
+const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
+const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
+
 const logger = loggerFactory('growi:pages:all');
 
 const {
@@ -168,11 +173,6 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
   // const { t } = useTranslation();
   const router = useRouter();
 
-  const NotCreatablePage = dynamic(() => import('../components/NotCreatablePage').then(mod => mod.NotCreatablePage), { ssr: false });
-  const ForbiddenPage = dynamic(() => import('../components/ForbiddenPage'), { ssr: false });
-  const UnsavedAlertDialog = dynamic(() => import('./UnsavedAlertDialog'), { ssr: false });
-  const GrowiSubNavigationSwitcher = dynamic(() => import('../components/Navbar/GrowiSubNavigationSwitcher'), { ssr: false });
-
   const { data: currentUser } = useCurrentUser(props.currentUser ?? null);
 
   // register global EventEmitter

+ 16 - 16
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -103,7 +103,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
   // TODO: refactoring adminPagesMap => https://redmine.weseek.co.jp/issues/102694
   const adminPagesMap = {
     home: {
-      title: useCustomTitle(props, t('Wiki Management Home Page')),
+      title:  t('wiki_management_home_page'),
       component: <AdminHome
         nodeVersion={props.nodeVersion}
         npmVersion={props.npmVersion}
@@ -112,31 +112,31 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       />,
     },
     app: {
-      title: useCustomTitle(props, t('App Settings')),
+      title: t('app_settings'),
       component: <AppSettingsPageContents />,
     },
     security: {
-      title: useCustomTitle(props, t('security_settings')),
+      title: t('security_settings.security_settings'),
       component: <SecurityManagementContents />,
     },
     markdown: {
-      title: useCustomTitle(props, t('Markdown Settings')),
+      title: t('markdown_settings'),
       component: <MarkDownSettingContents />,
     },
     customize: {
-      title: useCustomTitle(props, t('Customize Settings')),
+      title: t('Customize Settings'),
       component: <CustomizeSettingContents />,
     },
     importer: {
-      title: useCustomTitle(props, t('Import Data')),
+      title: t('Import Data'),
       component: <DataImportPageContents />,
     },
     export: {
-      title: useCustomTitle(props, t('Export Archive Data')),
+      title: t('Export Archive Data'),
       component: <ExportArchiveDataPage />,
     },
     notification: {
-      title: useCustomTitle(props, t('Notification Settings')),
+      title: t('external_notification.external_notification'),
       component: <NotificationSetting />,
     },
     'global-notification': {
@@ -144,37 +144,37 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
       component: <>global-notification</>,
     },
     'slack-integration': {
-      title: useCustomTitle(props, t('slack_integration')),
+      title: t('slack_integration.slack_integration'),
       component: <SlackIntegration />,
     },
     'slack-integration-legacy': {
-      title: useCustomTitle(props, t('Legacy_Slack_Integration')),
+      title: t('slack_integration_legacy.slack_integration_legacy'),
       component: <LegacySlackIntegration />,
     },
     users: {
-      title: useCustomTitle(props, t('User_Management')),
+      title: t('user_management.user_management'),
       component: <UserManagement />,
       'external-accounts': {
-        title: useCustomTitle(props, t('external_account_management')),
+        title: t('external_account_management'),
         component: <ManageExternalAccount />,
       },
     },
     'user-groups': {
-      title: useCustomTitle(props, t('UserGroup Management')),
+      title:  t('user_group_management.user_group_management'),
       component: <UserGroupPage />,
     },
     'user-group-detail': {
       [userGroupId]: {
-        title: t('UserGroup Management'),
+        title: t('user_group_management.user_group_management'),
         component: <UserGroupDetailPage userGroupId={userGroupId} />,
       },
     },
     search: {
-      title: useCustomTitle(props, t('Full Text Search Management')),
+      title: t('full_text_search_management'),
       component: <ElasticsearchManagement />,
     },
     'audit-log': {
-      title: useCustomTitle(props, t('AuditLog')),
+      title: t('audit_log_management.audit_log'),
       component: <AuditLogManagement />,
     },
   };

+ 102 - 0
packages/app/src/pages/trash.page.tsx

@@ -0,0 +1,102 @@
+import {
+  IUser, IUserHasId,
+} from '@growi/core';
+
+import dynamic from 'next/dynamic';
+import { NextPage, GetServerSideProps, GetServerSidePropsContext } from 'next';
+
+import GrowiContextualSubNavigation from '~/components/Navbar/GrowiContextualSubNavigation';
+import { CrowiRequest } from '~/interfaces/crowi-request';
+import { IUserUISettings } from '~/interfaces/user-ui-settings';
+import UserUISettings from '~/server/models/user-ui-settings';
+
+import { BasicLayout } from '../components/Layout/BasicLayout';
+import {
+  useCurrentUser, useIsTrashPage, useCurrentPagePath, useCurrentPathname,
+  useIsSearchServiceConfigured, useIsSearchServiceReachable,
+  useIsSearchScopeChildrenAsDefault,
+} from '../stores/context';
+
+import {
+  CommonProps, getServerSideCommonProps, useCustomTitle,
+} from './utils/commons';
+
+type Props = CommonProps & {
+  currentUser: IUser,
+  isSearchServiceConfigured: boolean,
+  isSearchServiceReachable: boolean,
+  isSearchScopeChildrenAsDefault: boolean,
+  userUISettings?: IUserUISettings
+};
+
+const TrashPage: NextPage<CommonProps> = (props: Props) => {
+  const TrashPageList = dynamic(() => import('~/components/TrashPageList').then(mod => mod.TrashPageList), { ssr: false });
+
+  useCurrentUser(props.currentUser ?? null);
+
+  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
+  useIsSearchServiceReachable(props.isSearchServiceReachable);
+  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
+
+  useIsTrashPage(true);
+  useCurrentPathname('/trash');
+  useCurrentPagePath('/trash');
+
+  return (
+    <>
+      <BasicLayout title={useCustomTitle(props, 'GROWI')} >
+        <header className="py-0 position-relative">
+          <GrowiContextualSubNavigation isLinkSharingDisabled={false} />
+        </header>
+        <div className="grw-container-convertible mb-5 pb-5">
+          <TrashPageList />
+        </div>
+      </BasicLayout>
+    </>
+  );
+};
+
+async function injectUserUISettings(context: GetServerSidePropsContext, props: Props): Promise<void> {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+  const userUISettings = user == null ? null : await UserUISettings.findOne({ user: user._id }).exec();
+
+  if (userUISettings != null) {
+    props.userUISettings = userUISettings.toObject();
+  }
+}
+
+function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
+  const req: CrowiRequest = context.req as CrowiRequest;
+  const { crowi } = req;
+  const {
+    searchService, configManager,
+  } = crowi;
+
+  props.isSearchServiceConfigured = searchService.isConfigured;
+  props.isSearchServiceReachable = searchService.isReachable;
+  props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
+}
+
+export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
+  const req = context.req as CrowiRequest<IUserHasId & any>;
+  const { user } = req;
+  const result = await getServerSideCommonProps(context);
+
+  if (!('props' in result)) {
+    throw new Error('invalid getSSP result');
+  }
+  const props: Props = result.props as Props;
+
+  if (user != null) {
+    props.currentUser = user.toObject();
+  }
+  await injectUserUISettings(context, props);
+  injectServerConfigurations(context, props);
+
+  return {
+    props,
+  };
+};
+
+export default TrashPage;

+ 1 - 1
packages/app/src/server/routes/apiv3/security-setting.js

@@ -977,7 +977,7 @@ module.exports = (crowi) => {
         crowi.passportService.parseABLCRule(rule);
       }
       catch (err) {
-        return res.apiv3Err(req.t('form_validation.invalid_syntax', req.t('security_setting.form_item_name.ABLCRule')), 400);
+        return res.apiv3Err(req.t('form_validation.invalid_syntax', req.t('security_settings.form_item_name.ABLCRule')), 400);
       }
     }
 

+ 1 - 1
packages/app/src/server/routes/index.js

@@ -222,7 +222,7 @@ module.exports = function(crowi, app) {
 
   app.get('/_search'                            , loginRequired, next.delegateToNext);
 
-  app.get('/trash$'                   , loginRequired, injectUserUISettings, page.trashPageShowWrapper);
+  app.get('/trash$'                   , loginRequired, injectUserUISettings, next.delegateToNext);
   app.get('/trash/$'                  , loginRequired, (req, res) => res.redirect('/trash'));
   app.get('/trash/*/$'                , loginRequired, injectUserUISettings, page.deletedPageListShowWrapper);
 

+ 5 - 5
packages/app/src/server/service/acl.js

@@ -67,17 +67,17 @@ class AclService {
 
   getRestrictGuestModeLabels() {
     const labels = {};
-    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_DENY] = 'security_setting.guest_mode.deny';
-    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_setting.guest_mode.readonly';
+    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_DENY] = 'security_settings.guest_mode.deny';
+    labels[this.labels.SECURITY_RESTRICT_GUEST_MODE_READONLY] = 'security_settings.guest_mode.readonly';
 
     return labels;
   }
 
   getRegistrationModeLabels() {
     const labels = {};
-    labels[this.labels.SECURITY_REGISTRATION_MODE_OPEN] = 'security_setting.registration_mode.open';
-    labels[this.labels.SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_setting.registration_mode.restricted';
-    labels[this.labels.SECURITY_REGISTRATION_MODE_CLOSED] = 'security_setting.registration_mode.closed';
+    labels[this.labels.SECURITY_REGISTRATION_MODE_OPEN] = 'security_settings.registration_mode.open';
+    labels[this.labels.SECURITY_REGISTRATION_MODE_RESTRICTED] = 'security_settings.registration_mode.restricted';
+    labels[this.labels.SECURITY_REGISTRATION_MODE_CLOSED] = 'security_settings.registration_mode.closed';
 
     return labels;
   }

+ 2 - 2
packages/app/src/server/views/admin/app.html

@@ -1,12 +1,12 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('App Settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('app_settings')) }}{% endblock %}
 
 {% block head_warn_alert_siteurl_undefined %} {# remove including block for './widget/alert_siteurl_undefined.html' #}
 {% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('App Settings') }}</h1>
+<h1 class="title">{{ t('app_settings') }}</h1>
 {% endblock %}
 
 

+ 2 - 2
packages/app/src/server/views/admin/audit-log.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('AuditLog')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('audit_log_management.audit_log')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('AuditLog') }}</h1>
+<h1 class="title">{{ t('audit_log_management.audit_log') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 1
packages/app/src/server/views/admin/external-accounts.html

@@ -3,7 +3,7 @@
 {% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_account_management')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('User_Management') }} / {{ t('external_account_management') }}</h1>
+<h1 class="title">{{ t('user_management.user_management') }} / {{ t('external_account_management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/global-notification-detail.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('External_Notification')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_notification.external_notification')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('External_Notification') }}</h1>
+<h1 class="title">{{ t('external_notification.external_notification') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/markdown.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Markdown Settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('markdown_settings')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Markdown Settings') }}</h1>
+<h1 class="title">{{ t('markdown_settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/notification.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('External_Notification')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('external_notification.external_notification')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('External_Notification') }}</h1>
+<h1 class="title">{{ t('external_notification.external_notification') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/search.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Full Text Search Management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('full_text_search_management')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Full Text Search Management') }}</h1>
+<h1 class="title">{{ t('full_text_search_management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/security.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('security_settings')) }} · {% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('security_settings.security_settings')) }} · {% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('security_settings') }}</h1>
+<h1 class="title">{{ t('security_settings.security_settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/slack-integration-legacy.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Legacy_Slack_Integration')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('slack_integration_legacy.slack_integration_legacy')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Legacy_Slack_Integration') }}</h1>
+<h1 class="title">{{ t('slack_integration_legacy.slack_integration_legacy') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/slack-integration.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('slack_integration')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('slack_integration.slack_integration')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('slack_integration') }}</h1>
+<h1 class="title">{{ t('slack_integration.slack_integration') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/user-group-detail.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('UserGroup Management') + '/' + userGroup.name) | preventXss }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_group_management.user_group_management') + '/' + userGroup.name) | preventXss }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('UserGroup Management') + '/' + userGroup.name | preventXss }}</h1>
+<h1 class="title">{{ t('user_group_management.user_group_management') + '/' + userGroup.name | preventXss }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/user-groups.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('UserGroup Management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_group_management.user_group_management')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('UserGroup Management') }}</h1>
+<h1 class="title">{{ t('user_group_management.user_group_management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
packages/app/src/server/views/admin/users.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('User_Management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('user_management.user_management')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('User_Management') }}</h1>
+<h1 class="title">{{ t('user_management.user_management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 1 - 1
packages/app/src/server/views/widget/alert_siteurl_undefined.html

@@ -1,6 +1,6 @@
 {% if !getConfig('crowi', 'app:siteUrl') %}
 <div class="alert alert-danger rounded-0 d-edit-none mb-0 px-4 py-2">
   <i class="icon-exclamation"></i>
-  {{ t("security_setting.alert_siteUrl_is_not_set", { link: t('App Settings')}) }} &gt;&gt; <a href="/admin/app">{{t('App Settings')}}<i class="icon-login"></i></a>
+  {{ t("security_setting.alert_siteUrl_is_not_set", { link: t('app_settings')}) }} &gt;&gt; <a href="/admin/app">{{t('app_settings')}}<i class="icon-login"></i></a>
 </div>
 {% endif %}

+ 6 - 0
packages/app/src/server/views/widget/headers/drawio.html

@@ -27,6 +27,12 @@
       // Set responsive option.
       // refs: https://github.com/jgraph/drawio/blob/v13.9.1/src/main/webapp/js/diagramly/GraphViewer.js#L89-L95
       DrawioViewer.prototype.responsive = true;
+
+      // Set z-index ($zindex-dropdown + 200) for lightbox.
+      // 'lightbox' is like a modal dialog that appears when click on a drawio diagram.
+      // z-index refs: https://github.com/twbs/bootstrap/blob/v4.6.2/scss/_variables.scss#L681
+      DrawioViewer.prototype.lightboxZIndex = 1200;
+      DrawioViewer.prototype.toolbarZIndex = 1200;
     }
   };
 </script>

+ 14 - 0
packages/app/src/stores/share-link.tsx

@@ -0,0 +1,14 @@
+import { Nullable } from '@growi/core';
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { IResShareLinkList } from '~/interfaces/share-link';
+
+const fetchShareLinks = async(endpoint, pageId) => {
+  const res = await apiv3Get<IResShareLinkList>(endpoint, { relatedPage: pageId });
+  return res.data.shareLinksResult;
+};
+
+export const useSWRxSharelink = (currentPageId: Nullable<string>): SWRResponse<IResShareLinkList['shareLinksResult'], Error> => {
+  return useSWR(currentPageId == null ? null : ['/share-links/', currentPageId], (endpoint => fetchShareLinks(endpoint, currentPageId)));
+};

+ 167 - 0
packages/app/test/cypress/integration/50-sidebar/access-to-side-bar.spec.ts

@@ -0,0 +1,167 @@
+context('Access to sidebar', () => {
+  const ssPrefix = 'access-to-sidebar-';
+
+  beforeEach(() => {
+    // login
+    cy.fixture("user-admin.json").then(user => {
+      cy.login(user.username, user.password);
+    });
+    // collapse sidebar
+    cy.collapseSidebar(false);
+  });
+
+  it('Successfully show/collapse sidebar', () => {
+    cy.visit('/');
+    cy.screenshot(`${ssPrefix}-1-sidebar-shown`, {capture: 'viewport'});
+    cy.getByTestid('grw-navigation-resize-button').click({force: true});
+    cy.screenshot(`${ssPrefix}-2-sidebar-collapsed`, {capture: 'viewport'});
+
+  });
+  it('Successfully access recent changes side bar ', () => {
+    cy.visit('/');
+    cy.getByTestid('grw-sidebar-nav-primary-recent-changes').click();
+    cy.getByTestid('grw-contextual-navigation-sub').then(($el) => {
+      if($el.hasClass('d-none')){
+        cy.getByTestid('grw-navigation-resize-button').click({force: true});
+      }
+    });
+
+    cy.getByTestid('grw-recent-changes').should('be.visible');
+
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}recent-changes-1-page-list`);
+
+    cy.get('#grw-sidebar-contents-wrapper').within(() => {
+      cy.get('#recentChangesResize').click({force: true});
+      cy.screenshot(`${ssPrefix}recent-changes-2-switch-sidebar-size`);
+    });
+  });
+
+  it('Successfully create a custom sidebar page', () => {
+    cy.visit('/');
+    cy.getByTestid('grw-sidebar-nav-primary-custom-sidebar').click();
+    cy.getByTestid('grw-contextual-navigation-sub').then(($el) => {
+      if($el.hasClass('d-none')){
+        cy.getByTestid('grw-navigation-resize-button').click({force: true});
+      }
+    });
+
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}custom-sidebar-1-click-on-custom-sidebar`);
+
+    // create /Sidebar contents
+    const content = '# HELLO \n ## Hello\n ### Hello';
+    cy.get('.grw-sidebar-content-header.h5').find('a').click();
+    cy.get('.CodeMirror textarea').type(content, {force: true});
+    cy.screenshot(`${ssPrefix}custom-sidebar-2-custom-sidebar-editor`);
+    cy.get('.dropup > .btn-submit').click();
+    cy.get('body').should('not.have.class', 'on-edit');
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}custom-sidebar-3-custom-sidebar-created`);
+  });
+
+  it('Successfully performed page operation from "page tree"', () => {
+    cy.visit('/');
+    cy.getByTestid('grw-sidebar-nav-primary-page-tree').click();
+    cy.getByTestid('grw-contextual-navigation-sub').then(($el) => {
+      if($el.hasClass('d-none')){
+        cy.getByTestid('grw-navigation-resize-button').click({force: true});
+      }
+    });
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-1-access-to-page-tree`);
+    cy.get('.grw-pagetree-triangle-btn').eq(0).click();
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-2-hide-page-tree-item`);
+    cy.get('.grw-pagetree-triangle-btn').eq(0).click();
+
+    cy.get('.grw-pagetree-item-children').eq(0).within(() => {
+      cy.getByTestid('open-page-item-control-btn').find('button').eq(0).invoke('css','display','block').click()
+    });
+
+    cy.screenshot(`${ssPrefix}page-tree-3-click-three-dots-menu`);
+    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+      cy.getByTestid('add-remove-bookmark-btn').click();
+    });
+    cy.screenshot(`${ssPrefix}page-tree-4-add-bookmark`);
+
+
+    cy.get('.grw-pagetree-item-children').eq(0).within(() => {
+      cy.getByTestid('open-page-item-control-btn').find('button').eq(0).invoke('css','display','block').click()
+    });
+    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+      cy.getByTestid('open-page-duplicate-modal-btn').click();
+    });
+
+    cy.getByTestid('page-duplicate-modal').should('be.visible').within(() => {
+      cy.get('.rbt-input-main').type('_test');
+      cy.screenshot(`${ssPrefix}page-tree-5-duplicate-page`);
+      cy.get('.modal-header > button').click();
+    });
+
+    cy.get('.grw-pagetree-item-children').eq(0).within(() => {
+      cy.getByTestid('open-page-item-control-btn').find('button').eq(0).invoke('css','display','block').click()
+    });
+    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+      cy.getByTestid('open-page-move-rename-modal-btn').click();
+    });
+
+    cy.get('.grw-pagetree-item-children').eq(0).within(() => {
+      cy.get('.flex-fill > input').type('_newname');
+    });
+
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}page-tree-6-rename-page`);
+    cy.get('body').click(0,0);
+
+    cy.get('.grw-pagetree-item-children').eq(0).within(() => {
+      cy.getByTestid('open-page-item-control-btn').find('button').eq(0).invoke('css','display','block').click()
+    });
+    cy.get('.dropdown-menu.show').should('be.visible').within(() => {
+      cy.getByTestid('open-page-delete-modal-btn').click();
+    });
+
+    cy.getByTestid('page-delete-modal').should('be.visible').within(() => {
+      cy.screenshot(`${ssPrefix}page-tree-7-delete-page`);
+      cy.get('.modal-header > button').click();
+    });
+
+  });
+
+  it('Successfully performed page operation from "Tags" ', () => {
+    cy.visit('/');
+    cy.getByTestid('grw-sidebar-nav-primary-tags').click();
+    cy.getByTestid('grw-contextual-navigation-sub').then(($el) => {
+      if($el.hasClass('d-none')){
+        cy.getByTestid('grw-navigation-resize-button').click({force: true});
+      }
+    });
+    cy.getByTestid('grw-contextual-navigation-sub').screenshot(`${ssPrefix}tags-1-access-to-tags`);
+
+    cy.get('.grw-container-convertible > div > .btn-primary').click({force: true});
+
+    // collapse sidebar
+    cy.collapseSidebar(true);
+
+    cy.screenshot(`${ssPrefix}tags-2-check-all-tags`);
+  });
+
+  it('Successfully access to My Drafts page', () => {
+    cy.visit('/');
+    cy.get('.grw-sidebar-nav-secondary-container').within(() => {
+      cy.get('a[href*="/me/drafts"]').click();
+    });
+    cy.screenshot(`${ssPrefix}access-to-drafts-page`);
+  });
+  it('Successfully access to Growi Docs page', () => {
+    cy.visit('/');
+    cy.get('.grw-sidebar-nav-secondary-container').within(() => {
+      cy.get('a[href*="https://docs.growi.org"]').then(($a) => {
+        const url = $a.prop('href')
+        cy.request(url).its('body').should('include', '</html>');
+      });
+    });
+  });
+
+  it('Successfully access to trash page', () => {
+    cy.visit('/');
+    cy.get('.grw-sidebar-nav-secondary-container').within(() => {
+      cy.get('a[href*="/trash"]').click();
+    });
+    cy.screenshot(`${ssPrefix}access-to-trash-page`);
+  });
+});

+ 2 - 2
packages/app/test/cypress/integration/50-switch-sidebar-mode/switching-sidebar-mode.spec.ts → packages/app/test/cypress/integration/50-sidebar/switching-sidebar-mode.spec.ts

@@ -23,10 +23,10 @@ context('Switch sidebar mode', () => {
     cy.visit('/');
     cy.get('.grw-apperance-mode-dropdown').click();
 
-    cy.get('[for="swSidebarModeOnEditor"]').click();
+    cy.get('[for="swSidebarMode"]').click({force: true});
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode`, { capture: 'viewport' });
 
-    cy.get('[for="swSidebarModeOnEditor"]').click();
+    cy.get('[for="swSidebarMode"]').click({force: true});
     cy.screenshot(`${ssPrefix}-switch-sidebar-mode-back`, { capture: 'viewport' });
   });
 

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/codemirror-textlint",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "license": "MIT",
   "main": "dist/index.js",
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-attachment-refs",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 3 - 3
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/plugin-lsx",
-  "version": "5.1.3-RC.0",
+  "version": "5.1.4-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [
@@ -28,8 +28,8 @@
     "test": ""
   },
   "dependencies": {
-    "@growi/core": "^5.1.3-RC.0",
-    "@growi/remark-growi-plugin": "^5.1.3-RC.0"
+    "@growi/core": "^5.1.4-RC.0",
+    "@growi/remark-growi-plugin": "^5.1.4-RC.0"
   },
   "devDependencies": {
     "eslint-plugin-regex": "^1.8.0",

Некоторые файлы не были показаны из-за большого количества измененных файлов