armansansd 10 hónapja
commit
4b7c97a02e
100 módosított fájl, 25951 hozzáadás és 0 törlés
  1. 0 0
      config/backups.yaml
  2. 17 0
      config/groups.yaml
  3. 263 0
      config/media.yaml
  4. 101 0
      config/plugins/admin.yaml
  5. 2 0
      config/plugins/auto-author.yaml
  6. 11 0
      config/plugins/autoseo.yaml
  7. 13 0
      config/plugins/cookieconsent.yaml
  8. 19 0
      config/plugins/email.yaml
  9. 3 0
      config/plugins/langswitcher.yaml
  10. 3 0
      config/plugins/pagination.yaml
  11. 33 0
      config/plugins/social-seo-metatags.yaml
  12. 2 0
      config/plugins/taxonomylist.yaml
  13. 80 0
      config/plugins/tinymce-editor.yaml
  14. 28 0
      config/plugins/tntsearch.yaml
  15. 0 0
      config/scheduler.yaml
  16. 1 0
      config/security.yaml
  17. 19 0
      config/site.yaml
  18. 0 0
      config/streams.yaml
  19. 239 0
      config/system.yaml
  20. 3 0
      config/themes/plain.yaml
  21. 25 0
      config/versions.yaml
  22. 12 0
      languages/en.yaml
  23. 12 0
      languages/fr.yaml
  24. 0 0
      plugins/.gitkeep
  25. 2 0
      plugins/admin-addon-media-metadata/.gitignore
  26. 83 0
      plugins/admin-addon-media-metadata/.php_cs
  27. 37 0
      plugins/admin-addon-media-metadata/CHANGELOG.md
  28. 21 0
      plugins/admin-addon-media-metadata/LICENSE
  29. 90 0
      plugins/admin-addon-media-metadata/README.md
  30. 40 0
      plugins/admin-addon-media-metadata/admin-addon-media-metadata.css
  31. 113 0
      plugins/admin-addon-media-metadata/admin-addon-media-metadata.js
  32. 321 0
      plugins/admin-addon-media-metadata/admin-addon-media-metadata.php
  33. 21 0
      plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml
  34. BIN
      plugins/admin-addon-media-metadata/assets/1-open-form.jpg
  35. BIN
      plugins/admin-addon-media-metadata/assets/2-form-opened.jpg
  36. 30 0
      plugins/admin-addon-media-metadata/blueprints.yaml
  37. 29 0
      plugins/admin-addon-media-metadata/composer.json
  38. 23 0
      plugins/admin-addon-media-metadata/composer.lock
  39. 11 0
      plugins/admin-addon-media-metadata/languages/de.yaml
  40. 11 0
      plugins/admin-addon-media-metadata/languages/en.yaml
  41. 11 0
      plugins/admin-addon-media-metadata/languages/fr.yaml
  42. 2 0
      plugins/admin-addon-media-metadata/templates/additionalInlineJS.html.twig
  43. 36 0
      plugins/admin-addon-media-metadata/templates/metadata-modal.html.twig
  44. 7 0
      plugins/admin-addon-media-metadata/vendor/autoload.php
  45. 445 0
      plugins/admin-addon-media-metadata/vendor/composer/ClassLoader.php
  46. 21 0
      plugins/admin-addon-media-metadata/vendor/composer/LICENSE
  47. 10 0
      plugins/admin-addon-media-metadata/vendor/composer/autoload_classmap.php
  48. 9 0
      plugins/admin-addon-media-metadata/vendor/composer/autoload_namespaces.php
  49. 10 0
      plugins/admin-addon-media-metadata/vendor/composer/autoload_psr4.php
  50. 55 0
      plugins/admin-addon-media-metadata/vendor/composer/autoload_real.php
  51. 36 0
      plugins/admin-addon-media-metadata/vendor/composer/autoload_static.php
  52. 1 0
      plugins/admin-addon-media-metadata/vendor/composer/installed.json
  53. 17 0
      plugins/admin/.editorconfig
  54. 8 0
      plugins/admin/.gitattributes
  55. 8 0
      plugins/admin/.github/FUNDING.yml
  56. 6 0
      plugins/admin/.github/dependabot.yml
  57. 17 0
      plugins/admin/.gitignore
  58. 1895 0
      plugins/admin/CHANGELOG.md
  59. 1 0
      plugins/admin/CONTRIBUTING.md
  60. 21 0
      plugins/admin/LICENSE
  61. 152 0
      plugins/admin/README.md
  62. 6 0
      plugins/admin/UPGRADE.md
  63. 1350 0
      plugins/admin/admin.php
  64. 84 0
      plugins/admin/admin.yaml
  65. BIN
      plugins/admin/assets/admin-dashboard.png
  66. 781 0
      plugins/admin/blueprints.yaml
  67. 36 0
      plugins/admin/blueprints/config/media.yaml
  68. 2507 0
      plugins/admin/classes/plugin/Admin.php
  69. 1174 0
      plugins/admin/classes/plugin/AdminBaseController.php
  70. 3073 0
      plugins/admin/classes/plugin/AdminController.php
  71. 182 0
      plugins/admin/classes/plugin/AdminForm.php
  72. 51 0
      plugins/admin/classes/plugin/AdminFormFactory.php
  73. 414 0
      plugins/admin/classes/plugin/Controllers/AbstractController.php
  74. 359 0
      plugins/admin/classes/plugin/Controllers/AdminController.php
  75. 634 0
      plugins/admin/classes/plugin/Controllers/Login/LoginController.php
  76. 442 0
      plugins/admin/classes/plugin/Gpm.php
  77. 310 0
      plugins/admin/classes/plugin/Popularity.php
  78. 79 0
      plugins/admin/classes/plugin/Router.php
  79. 93 0
      plugins/admin/classes/plugin/Routers/LoginRouter.php
  80. 70 0
      plugins/admin/classes/plugin/ScssCompiler.php
  81. 59 0
      plugins/admin/classes/plugin/ScssList.php
  82. 29 0
      plugins/admin/classes/plugin/Themes.php
  83. 138 0
      plugins/admin/classes/plugin/Twig/AdminTwigExtension.php
  84. 61 0
      plugins/admin/classes/plugin/Utils.php
  85. 100 0
      plugins/admin/classes/plugin/WhiteLabel.php
  86. 18 0
      plugins/admin/codeception.yml
  87. 64 0
      plugins/admin/composer.json
  88. 3945 0
      plugins/admin/composer.lock
  89. 15 0
      plugins/admin/hebe.json
  90. 24 0
      plugins/admin/languages/ar.yaml
  91. 24 0
      plugins/admin/languages/bg.yaml
  92. 4 0
      plugins/admin/languages/bn.yaml
  93. 592 0
      plugins/admin/languages/br.yaml
  94. 643 0
      plugins/admin/languages/ca.yaml
  95. 1112 0
      plugins/admin/languages/cs.yaml
  96. 89 0
      plugins/admin/languages/cy.yaml
  97. 760 0
      plugins/admin/languages/da.yaml
  98. 965 0
      plugins/admin/languages/de.yaml
  99. 24 0
      plugins/admin/languages/el.yaml
  100. 1154 0
      plugins/admin/languages/en.yaml

+ 0 - 0
config/backups.yaml


+ 17 - 0
config/groups.yaml

@@ -0,0 +1,17 @@
+lecteur:
+  access:
+    admin:
+      login: false
+      super: false
+      cache: false
+      configuration: false
+      pages: false
+      maintenance: false
+      statistics: false
+      plugins: false
+      themes: false
+      tools: false
+      users: false
+      flex-objects: false
+  readableName: Lecteur
+  enabled: true

+ 263 - 0
config/media.yaml

@@ -0,0 +1,263 @@
+types:
+  defaults:
+    type: file
+    thumb: media/thumb.png
+    mime: application/octet-stream
+    image:
+      filters:
+        default: [enableProgressive]
+  jpg:
+    type: image
+    thumb: media/thumb-jpg.png
+    mime: image/jpeg
+    image: null
+  jpe:
+    type: image
+    thumb: media/thumb-jpg.png
+    mime: image/jpeg
+    image: null
+  jpeg:
+    type: image
+    thumb: media/thumb-jpg.png
+    mime: image/jpeg
+    image: null
+  png:
+    type: image
+    thumb: media/thumb-png.png
+    mime: image/png
+    image: null
+  gif:
+    type: animated
+    thumb: media/thumb-gif.png
+    mime: image/gif
+    image: null
+  svg:
+    type: vector
+    thumb: media/thumb-svg.png
+    mime: image/svg+xml
+    image: null
+  mp4:
+    type: video
+    thumb: media/thumb-mp4.png
+    mime: video/mp4
+    image: null
+  mov:
+    type: video
+    thumb: media/thumb-mov.png
+    mime: video/quicktime
+    image: null
+  m4v:
+    type: video
+    thumb: media/thumb-m4v.png
+    mime: video/x-m4v
+    image: null
+  swf:
+    type: video
+    thumb: media/thumb-swf.png
+    mime: video/x-flv
+    image: null
+  flv:
+    type: video
+    thumb: media/thumb-flv.png
+    mime: video/x-flv
+    image: null
+  webm:
+    type: video
+    thumb: media/thumb-webm.png
+    mime: video/webm
+    image: null
+  ogv:
+    type: video
+    thumb: media/thumb-ogg.png
+    mime: video/ogg
+    image: null
+  mp3:
+    type: audio
+    thumb: media/thumb-mp3.png
+    mime: audio/mp3
+    image: null
+  ogg:
+    type: audio
+    thumb: media/thumb-ogg.png
+    mime: audio/ogg
+    image: null
+  wma:
+    type: audio
+    thumb: media/thumb-wma.png
+    mime: audio/wma
+    image: null
+  m4a:
+    type: audio
+    thumb: media/thumb-m4a.png
+    mime: audio/m4a
+    image: null
+  wav:
+    type: audio
+    thumb: media/thumb-wav.png
+    mime: audio/wav
+    image: null
+  aiff:
+    type: audio
+    thumb: media/thumb-aif.png
+    mime: audio/aiff
+    image: null
+  aif:
+    type: audio
+    thumb: media/thumb-aif.png
+    mime: audio/aif
+    image: null
+  txt:
+    type: file
+    thumb: media/thumb-txt.png
+    mime: text/plain
+    image: null
+  xml:
+    type: file
+    thumb: media/thumb-xml.png
+    mime: application/xml
+    image: null
+  doc:
+    type: file
+    thumb: media/thumb-doc.png
+    mime: application/msword
+    image: null
+  docx:
+    type: file
+    thumb: media/thumb-docx.png
+    mime: application/msword
+    image: null
+  xls:
+    type: file
+    thumb: media/thumb-xls.png
+    mime: application/vnd.ms-excel
+    image: null
+  xlsx:
+    type: file
+    thumb: media/thumb-xlsx.png
+    mime: application/vnd.ms-excel
+    image: null
+  ppt:
+    type: file
+    thumb: media/thumb-ppt.png
+    mime: application/vnd.ms-powerpoint
+    image: null
+  pptx:
+    type: file
+    thumb: media/thumb-pptx.png
+    mime: application/vnd.ms-powerpoint
+    image: null
+  pps:
+    type: file
+    thumb: media/thumb-pps.png
+    mime: application/vnd.ms-powerpoint
+    image: null
+  rtf:
+    type: file
+    thumb: media/thumb-rtf.png
+    mime: application/rtf
+    image: null
+  bmp:
+    type: file
+    thumb: media/thumb-bmp.png
+    mime: image/bmp
+    image: null
+  tiff:
+    type: file
+    thumb: media/thumb-tiff.png
+    mime: image/tiff
+    image: null
+  mpeg:
+    type: file
+    thumb: media/thumb-mpg.png
+    mime: video/mpeg
+    image: null
+  mpg:
+    type: file
+    thumb: media/thumb-mpg.png
+    mime: video/mpeg
+    image: null
+  mpe:
+    type: file
+    thumb: media/thumb-mpe.png
+    mime: video/mpeg
+    image: null
+  avi:
+    type: file
+    thumb: media/thumb-avi.png
+    mime: video/msvideo
+    image: null
+  wmv:
+    type: file
+    thumb: media/thumb-wmv.png
+    mime: video/x-ms-wmv
+    image: null
+  html:
+    type: file
+    thumb: media/thumb-html.png
+    mime: text/html
+    image: null
+  htm:
+    type: file
+    thumb: media/thumb-html.png
+    mime: text/html
+    image: null
+  ics:
+    type: iCal
+    thumb: media/thumb-ics.png
+    mime: text/calendar
+    image: null
+  pdf:
+    type: file
+    thumb: media/thumb-pdf.png
+    mime: application/pdf
+    image: null
+  ai:
+    type: file
+    thumb: media/thumb-ai.png
+    mime: image/ai
+    image: null
+  psd:
+    type: file
+    thumb: media/thumb-psd.png
+    mime: image/psd
+    image: null
+  zip:
+    type: file
+    thumb: media/thumb-zip.png
+    mime: application/zip
+    image: null
+  7z:
+    type: file
+    thumb: media/thumb-7z.png
+    mime: application/x-7z-compressed
+    image: null
+  gz:
+    type: file
+    thumb: media/thumb-gz.png
+    mime: application/gzip
+    image: null
+  tar:
+    type: file
+    thumb: media/thumb-tar.png
+    mime: application/x-tar
+    image: null
+  css:
+    type: file
+    thumb: media/thumb-css.png
+    mime: text/css
+    image: null
+  js:
+    type: file
+    thumb: media/thumb-js.png
+    mime: application/javascript
+    image: null
+  json:
+    type: file
+    thumb: media/thumb-json.png
+    mime: application/json
+    image: null
+  odt:
+    type: file
+    thumb: null
+    mime: null
+    image: null

+ 101 - 0
config/plugins/admin.yaml

@@ -0,0 +1,101 @@
+enabled: true
+route: /admin
+cache_enabled: true
+theme: grav
+logo_text: null
+body_classes: null
+content_padding: true
+twofa_enabled: true
+sidebar:
+  activate: tab
+  hover_delay: 100
+  size: auto
+dashboard:
+  days_of_stats: 7
+widgets_display:
+  dashboard-maintenance: 'true'
+  dashboard-statistics: 'true'
+  dashboard-notifications: 'false'
+  dashboard-feed: 'false'
+  dashboard-pages: 'true'
+pages:
+  show_parents: both
+  show_modular: true
+  parents_levels: null
+session:
+  timeout: 6000
+edit_mode: normal
+frontend_preview_target: inline
+show_github_msg: true
+admin_icons: line-awesome
+enable_auto_updates_check: false
+notifications:
+  feed: false
+  dashboard: false
+  plugins: false
+  themes: false
+popularity:
+  enabled: true
+  ignore:
+    - '/test*'
+    - /modular
+  history:
+    daily: '30'
+    monthly: '12'
+    visitors: '20'
+whitelabel:
+  quicktray_recompile: false
+  codemirror_theme: paper
+  codemirror_fontsize: md
+  codemirror_md_font: sans
+  logo_custom: {  }
+  logo_login: {  }
+  color_scheme:
+    accents:
+      primary-accent: button
+      secondary-accent: notice
+      tertiary-accent: critical
+    colors:
+      logo-bg: '#323640'
+      logo-link: '#FFFFFF'
+      nav-bg: '#3D424E'
+      nav-text: '#B7B9BD'
+      nav-link: '#ffffff'
+      nav-selected-bg: '#323640'
+      nav-selected-link: '#ffffff'
+      nav-hover-bg: '#434753'
+      nav-hover-link: '#ffffff'
+      toolbar-bg: '#ffffff'
+      toolbar-text: '#3D424E'
+      page-bg: '#F6F6F6'
+      page-text: '#6f7b8a'
+      page-link: '#0090D9'
+      content-bg: '#ffffff'
+      content-text: '#6f7b8a'
+      content-link: '#0090D9'
+      content-link2: '#da4b46'
+      content-header: '#414147'
+      content-tabs-bg: '#e6e6e6'
+      content-tabs-text: '#808080'
+      button-bg: '#0090D9'
+      button-text: '#ffffff'
+      notice-bg: '#06A599'
+      notice-text: '#ffffff'
+      update-bg: '#77559D'
+      update-text: '#ffffff'
+      critical-bg: '#F45857'
+      critical-text: '#ffffff'
+      content-highlight: '#ffffd7'
+    name: null
+  custom_footer: null
+  custom_css: null
+  custom_presets: null
+show_beta_msg: null
+pagemedia:
+  resize_width: 0
+  resize_height: 0
+  res_min_width: 0
+  res_min_height: 0
+  res_max_width: 0
+  res_max_height: 0
+  resize_quality: 0.8

+ 2 - 0
config/plugins/auto-author.yaml

@@ -0,0 +1,2 @@
+enabled: true
+user: true

+ 11 - 0
config/plugins/autoseo.yaml

@@ -0,0 +1,11 @@
+enabled: true
+description:
+  enabled: true
+  length: 30
+keywords:
+  enabled: true
+  length: 20
+facebook:
+  enabled: true
+twitter:
+  enabled: true

+ 13 - 0
config/plugins/cookieconsent.yaml

@@ -0,0 +1,13 @@
+enabled: true
+cdn: true
+popup_background_color: '#000000'
+popup_text_color: '#ffffff'
+button_background_color: '#e94d1a'
+button_text_color: '#000000'
+button_border_color: '#e94d1a'
+position: bottom
+theme: block
+content_message: 'Ce site contient des cookies déposés via des services de partage de vidéos, ils sont destinés à permettre à l''utilisateur de visualiser le contenu multimédia directement sur le site.'
+content_dismiss: 'J''accepte'
+content_link: 'En savoir plus'
+content_href: 'https://policies.google.com/privacy?hl=fr&gl=fr'

+ 19 - 0
config/plugins/email.yaml

@@ -0,0 +1,19 @@
+enabled: true
+from: contact@armansansd.net
+to: contact@armansansd.net
+mailer:
+  engine: sendmail
+  smtp:
+    server: localhost
+    port: 25
+    encryption: none
+    user: null
+    password: null
+  sendmail:
+    bin: '/usr/sbin/sendmail -bs'
+content_type: text/html
+debug: false
+cc: null
+bcc: null
+reply_to: null
+body: null

+ 3 - 0
config/plugins/langswitcher.yaml

@@ -0,0 +1,3 @@
+enabled: true
+built_in_css: true
+untranslated_pages_behavior: none

+ 3 - 0
config/plugins/pagination.yaml

@@ -0,0 +1,3 @@
+enabled: true
+built_in_css: true
+delta: 0

+ 33 - 0
config/plugins/social-seo-metatags.yaml

@@ -0,0 +1,33 @@
+enabled: true
+seo:
+  robots: without
+  length: 20
+  taxonomy:
+    enabled: true
+  page_content:
+    enabled: false
+  breadcrumb: false
+  keywords:
+    taxonomy:
+      enabled: true
+    page_content:
+      enabled: true
+    length: 20
+quote:
+  convert_simple: true
+image:
+  use_cache: true
+social_pages:
+  pages:
+    twitter:
+      enabled: true
+      type: summary_large_image
+      username: null
+    facebook:
+      opengraph:
+        enabled: false
+      insights:
+        enabled: false
+        appid: '1234567890'
+default:
+  image: {  }

+ 2 - 0
config/plugins/taxonomylist.yaml

@@ -0,0 +1,2 @@
+enabled: true
+route: /actualites

+ 80 - 0
config/plugins/tinymce-editor.yaml

@@ -0,0 +1,80 @@
+enabled: true
+restrictions:
+  whitelist: false
+  blacklist: false
+whitelist:
+  items:
+    - '@root.descendants'
+  filter:
+    published: true
+    non-published: true
+blacklist:
+  items:
+    - '@none'
+  filter:
+    published: true
+    non-published: true
+apikey: r2duxdbywfre1kmk3ft7z998eyt5mnsmkyqcp41hthzk5jcc
+plugins:
+  - advlist
+  - anchor
+  - autoresize
+  - charmap
+  - code
+  - colorpicker
+  - emoticons
+  - fullscreen
+  - hr
+  - image
+  - insertdatetime
+  - link
+  - lists
+  - media
+  - nonbreaking
+  - pagebreak
+  - paste
+  - print
+  - searchreplace
+  - table
+  - textcolor
+  - toc
+  - visualchars
+  - wordcount
+parameters:
+  -
+    name: paste_data_images
+    value: '1'
+  -
+    name: paste_data_images
+    value: '1'
+evals: ''
+menubar: false
+menu:
+  -
+    title: File
+    items: 'newdocument print'
+  -
+    title: Edit
+    items: 'undo redo | cut copy paste pastetext | selectall | searchreplace'
+  -
+    title: Insert
+    items: 'media link image | pagebreak charmap anchor hr insertdatetime nonbreaking toc'
+  -
+    title: View
+    items: 'visualchars visualaid | fullscreen'
+  -
+    title: Format
+    items: 'bold italic underline strikethrough superscript subscript | formats | removeformat'
+  -
+    title: Table
+    items: 'inserttable tableprops deletetable | cell row column'
+  -
+    title: Tools
+    items: code
+toolbar:
+  -
+    row: 'formatselect | undo redo | visualchars | image media  | link unlink  | blockquote | nonbreaking |  | bold italic underline | bullist numlist | superscript subscript | removeformat table'
+  -
+    row: null
+branding: false
+statusbar: false

+ 28 - 0
config/plugins/tntsearch.yaml

@@ -0,0 +1,28 @@
+enabled: true
+search_route: /search
+query_route: /s
+built_in_css: true
+built_in_js: true
+built_in_search_page: true
+enable_admin_page_events: true
+search_type: auto
+fuzzy: true
+phrases: true
+stemmer: default
+display_route: true
+display_hits: true
+display_time: true
+live_uri_update: true
+limit: '20'
+min: '3'
+snippet: '300'
+index_page_by_default: true
+scheduled_index:
+  enabled: true
+  at: '0 0 * * 1'
+  logs: logs/tntsearch-index.out
+filter:
+  items:
+    - root@.descendants
+powered_by: true
+search_object_type: Grav

+ 0 - 0
config/scheduler.yaml


+ 1 - 0
config/security.yaml

@@ -0,0 +1 @@
+salt: w2FWPbaOYk5Sxg

+ 19 - 0
config/site.yaml

@@ -0,0 +1,19 @@
+title: 'Tournons la Page'
+default_lang: fr
+author:
+  name: 'tournons la page'
+  email: contact@tournonslapage.org
+taxonomies:
+  - category
+  - tag
+metadata:
+  description: 'Tournons La Page est un mouvement citoyen qui entend rassembler le plus largement possible autour d''une vision et des valeurs communes de respect des droits humains fondamentaux et des principes démocratiques.'
+summary:
+  enabled: true
+  format: short
+  size: 300
+  delimiter: '==='
+redirects: null
+routes: null
+blog:
+  route: /blog

+ 0 - 0
config/streams.yaml


+ 239 - 0
config/system.yaml

@@ -0,0 +1,239 @@
+absolute_urls: false
+timezone: null
+param_sep: ':'
+wrapped_site: false
+reverse_proxy_setup: false
+force_ssl: false
+force_lowercase_urls: true
+custom_base_url: null
+username_regex: '^[a-z0-9_-]{3,16}$'
+pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
+intl_enabled: true
+http_x_forwarded:
+  protocol: true
+  host: false
+  port: true
+  ip: true
+languages:
+  supported:
+    - fr
+    - en
+  default_lang: fr
+  include_default_lang: true
+  include_default_lang_file_extension: true
+  translations: true
+  translations_fallback: true
+  session_store_active: false
+  http_accept_language: false
+  override_locale: false
+  pages_fallback_only: false
+  debug: false
+home:
+  alias: /home
+  hide_in_urls: false
+pages:
+  type: regular
+  dirs:
+    - 'page://'
+  theme: plain
+  order:
+    by: default
+    dir: asc
+  list:
+    count: 20
+  dateformat:
+    default: null
+    short: 'jS M Y'
+    long: 'F jS \a\t g:ia'
+  publish_dates: true
+  process:
+    markdown: true
+    twig: false
+  twig_first: false
+  never_cache_twig: false
+  events:
+    page: true
+    twig: true
+  markdown:
+    extra: false
+    auto_line_breaks: false
+    auto_url_links: false
+    escape_markup: false
+    special_chars:
+      '>': gt
+      '<': lt
+    valid_link_attributes:
+      - rel
+      - target
+      - id
+      - class
+      - classes
+  types:
+    - html
+    - htm
+    - xml
+    - txt
+    - json
+    - rss
+    - atom
+  append_url_extension: null
+  expires: 604800
+  cache_control: null
+  last_modified: false
+  etag: false
+  vary_accept_encoding: false
+  redirect_default_code: '302'
+  redirect_trailing_slash: 1
+  redirect_default_route: 0
+  ignore_files:
+    - .DS_Store
+  ignore_folders:
+    - .git
+    - .idea
+  ignore_hidden: true
+  hide_empty_folders: false
+  url_taxonomy_filters: true
+  frontmatter:
+    process_twig: false
+    ignore_fields:
+      - form
+      - forms
+cache:
+  enabled: false
+  check:
+    method: file
+  driver: auto
+  prefix: g
+  purge_at: '0 4 * * *'
+  clear_at: '0 3 * * *'
+  clear_job_type: all
+  clear_images_by_default: true
+  cli_compatibility: false
+  lifetime: 604800
+  gzip: true
+  allow_webserver_gzip: true
+  redis:
+    socket: null
+    password: null
+    database: null
+    server: null
+    port: null
+  memcache:
+    server: null
+    port: null
+  memcached:
+    server: null
+    port: null
+twig:
+  cache: true
+  debug: false
+  auto_reload: true
+  autoescape: false
+  undefined_functions: true
+  undefined_filters: true
+  safe_functions: {  }
+  safe_filters: {  }
+  umask_fix: false
+assets:
+  css_pipeline: true
+  css_pipeline_include_externals: true
+  css_pipeline_before_excludes: true
+  css_minify: true
+  css_minify_windows: false
+  css_rewrite: true
+  js_pipeline: true
+  js_pipeline_include_externals: true
+  js_pipeline_before_excludes: true
+  js_module_pipeline: false
+  js_module_pipeline_include_externals: true
+  js_module_pipeline_before_excludes: true
+  js_minify: true
+  enable_asset_timestamp: false
+  enable_asset_sri: false
+  collections:
+    jquery: 'system://assets/jquery/jquery-2.x.min.js'
+errors:
+  display: 1
+  log: false
+log:
+  handler: file
+  syslog:
+    facility: local6
+    tag: grav
+debugger:
+  enabled: true
+  provider: debugbar
+  censored: false
+  shutdown:
+    close_connection: true
+  twig: true
+images:
+  default_image_quality: 85
+  cache_all: false
+  cache_perms: '0755'
+  debug: false
+  auto_fix_orientation: false
+  seofriendly: false
+  cls:
+    auto_sizes: false
+    aspect_ratio: false
+    retina_scale: '1'
+  defaults:
+    loading: auto
+  watermark:
+    image: 'system://images/watermark.png'
+    position_y: center
+    position_x: center
+    scale: 33
+    watermark_all: false
+media:
+  enable_media_timestamp: false
+  unsupported_inline_types: null
+  allowed_fallback_types: null
+  auto_metadata_exif: false
+  upload_limit: 8388608
+session:
+  enabled: true
+  initialize: true
+  timeout: 3000
+  name: grav-site
+  uniqueness: path
+  secure: false
+  secure_https: true
+  httponly: true
+  samesite: Lax
+  split: true
+  domain: null
+  path: null
+gpm:
+  releases: stable
+  official_gpm_only: true
+  method: auto
+  verify_peer: false
+http:
+  method: auto
+  enable_proxy: true
+  proxy_url: null
+  proxy_cert_path: null
+  concurrent_connections: 5
+  verify_peer: true
+  verify_host: true
+accounts:
+  type: regular
+  storage: file
+  avatar: gravatar
+flex:
+  cache:
+    index:
+      enabled: true
+      lifetime: 60
+    object:
+      enabled: true
+      lifetime: 600
+    render:
+      enabled: true
+      lifetime: 600
+strict_mode:
+  yaml_compat: true
+  twig_compat: true
+  blueprint_compat: true

+ 3 - 0
config/themes/plain.yaml

@@ -0,0 +1,3 @@
+enabled: true
+dropdown:
+  enabled: true

+ 25 - 0
config/versions.yaml

@@ -0,0 +1,25 @@
+core:
+  grav:
+    version: 1.7.42.1
+    history:
+      version: 1.6.31
+      date: '2021-01-15 14:11:30'
+      0: { version: 1.7.5, date: '2021-02-08 13:39:58' }
+      1: { version: 1.7.7, date: '2021-03-04 13:20:41' }
+      2: { version: 1.7.9, date: '2021-03-22 17:05:54' }
+      3: { version: 1.7.10, date: '2021-04-07 08:19:14' }
+      4: { version: 1.7.15, date: '2021-05-20 08:16:55' }
+      5: { version: 1.7.16, date: '2021-06-04 13:32:43' }
+      6: { version: 1.7.17, date: '2021-06-21 07:55:11' }
+      7: { version: 1.7.18, date: '2021-08-19 07:46:49' }
+      8: { version: 1.7.23, date: '2021-10-26 12:14:15' }
+      9: { version: 1.7.25, date: '2021-12-11 13:23:27' }
+      10: { version: 1.7.26.1, date: '2022-01-13 15:56:19' }
+      11: { version: 1.7.27.1, date: '2022-01-13 15:58:17' }
+      12: { version: 1.7.30, date: '2022-02-28 13:58:46' }
+      13: { version: 1.7.31, date: '2022-03-15 10:23:00' }
+      14: { version: 1.7.32, date: '2022-04-22 09:52:03' }
+      15: { version: 1.7.33, date: '2022-05-26 10:42:44' }
+      16: { version: 1.7.35, date: '2022-08-10 12:01:48' }
+      17: { version: 1.7.42.1, date: '2023-06-27 13:17:25' }
+    schema: 1.7.0_2020-11-20_1

+ 12 - 0
languages/en.yaml

@@ -0,0 +1,12 @@
+DANGER:
+  ARREST: Arrested since
+  PRISON: In prison since
+  FREE: Free since
+  MENACE: Menaced since
+COALITION: Coalitions
+MEMBER: Members under threat supported by our emergency fund
+ORGA: Organisations
+MEDIA:
+  DOWNLOAD: Download
+  CONNECTION: Login to access the media
+  LOGOUT: Log out

+ 12 - 0
languages/fr.yaml

@@ -0,0 +1,12 @@
+DANGER:
+  ARREST: Arrêté·e le
+  PRISON: En prison depuis
+  FREE: Libéré·e le
+  MENACE: Menacé·e depuis le
+COALITION: Coalitions
+MEMBER: Membres en danger appuyés par notre fonds d’urgence
+ORGA: Organisations
+MEDIA:
+  DOWNLOAD: Télécharger
+  CONNECTION: Connectez-vous pour accéder au média
+  LOGOUT: Déconnection

+ 0 - 0
plugins/.gitkeep


+ 2 - 0
plugins/admin-addon-media-metadata/.gitignore

@@ -0,0 +1,2 @@
+.DS_Store
+.php_cs.cache

+ 83 - 0
plugins/admin-addon-media-metadata/.php_cs

@@ -0,0 +1,83 @@
+<?php
+/**
+ * This file represents the configuration for Code Sniffing PSR-2-related
+ * checks of coding guidelines and is based on a template from the news extension
+ * by Georg Ringer for the TYPO3 CMS.
+ *
+ * Install @fabpot's great php-cs-fixer tool via
+ *
+ *  $ composer global require friendsofphp/php-cs-fixer
+ *
+ * And then simply run
+ *
+ *  $ php-cs-fixer fix --config .php_cs
+ *
+ * For more information read:
+ * 	 http://www.php-fig.org/psr/psr-2/
+ * 	 http://cs.sensiolabs.org
+ */
+
+if (PHP_SAPI !== 'cli') {
+    die('This script supports command line usage only. Please check your command.');
+}
+// Define in which folders to search and which folders to exclude
+// Exclude some directories that are excluded by Git anyways to speed up the sniffing
+$finder = PhpCsFixer\Finder::create()
+    ->exclude('vendor/')
+    ->in(__DIR__);
+
+// Return a Code Sniffing configuration using
+// all sniffers needed for PSR-2
+// and additionally:
+//  - Remove leading slashes in use clauses.
+//  - PHP single-line arrays should not have trailing comma.
+//  - Single-line whitespace before closing semicolon are prohibited.
+//  - Remove unused use statements in the PHP source code
+//  - Ensure Concatenation to have at least one whitespace around
+//  - Remove trailing whitespace at the end of blank lines.
+return PhpCsFixer\Config::create()
+    ->setRiskyAllowed(true)
+    ->setRules([
+        '@PSR2' => true,
+        'align_multiline_comment' => [
+            'comment_type' => 'all_multiline',
+        ],
+        'array_syntax' => [
+            'syntax' => 'short'
+        ],
+        'binary_operator_spaces' => [
+            'default' => 'single_space'
+        ],
+        'concat_space' => [
+            'spacing' => 'one'
+        ],
+        'function_typehint_space' => true,
+        'hash_to_slash_comment' => true,
+        'lowercase_cast' => true,
+        'native_function_casing' => true,
+        'no_alias_functions' => true,
+        'no_blank_lines_after_phpdoc' => true,
+        'no_empty_statement' => true,
+        'no_extra_consecutive_blank_lines' => true,
+        'no_leading_import_slash' => true,
+        'no_leading_namespace_whitespace' => true,
+        'no_trailing_comma_in_singleline_array' => true,
+        'no_short_bool_cast' => true,
+        'no_singleline_whitespace_before_semicolons' => true,
+        'no_unused_imports' => true,
+        'no_unneeded_control_parentheses' => true,
+        'no_whitespace_in_blank_line' => true,
+        'ordered_imports' => true,
+        'phpdoc_no_empty_return' => true,
+        'phpdoc_no_package' => false,
+        'phpdoc_scalar' => true,
+        'phpdoc_trim' => true,
+        'return_type_declaration' => [
+            'space_before' => 'none'
+        ],
+        'self_accessor' => true,
+        'single_quote' => true,
+        'standardize_not_equals' => true,
+        'whitespace_after_comma_in_array' => true,
+    ])
+    ->setFinder($finder);

+ 37 - 0
plugins/admin-addon-media-metadata/CHANGELOG.md

@@ -0,0 +1,37 @@
+# v.1.1.0
+## 05-06-2020
+
+1. [](#new)
+    * Page specific media metadata fields can be added to a page’s frontmatter (see README --> Configuration)
+2. [](#improved)
+    * meta.yaml file will not be created upon upload anymore: this allows the creation of the file with the setting media.auto_metadata_exif turned on (in Admin Plugin 1.9); in Admin Plugin 1.10 it did not work anyway – **NOTE: in Admin Plugin 1.10 with setting media.auto_metadata_exif turned on, make sure to save the page with a newly uploaded image before adding other metadata using this plugin**
+
+# v1.0.2
+##  16-04-2020
+
+1. [](#improved)
+    * The button next to a media file will now also be translated. It was hard-coded to English before.
+
+# v1.0.1
+##  10-04-2020
+
+1. [](#bugfix)
+    * meta.yaml file will now be created for an already uploaded media file that did not have an associated meta.yaml file before
+
+# v1.0.0
+##  20-03-2020
+
+1. [](#improved)
+    * using core technology (Grav\Common\Yaml) for parsing and writing the meta.yaml files – kudos to https://github.com/renards for adding crucial parts to the code
+
+# v0.9.1
+##  16-03-2020
+
+1. [](#bugfix)
+    * adding /vendor/ and composer.lock, needed for composer autoload functionality
+
+# v0.9.0
+##  15-03-2020
+
+1. [](#new)
+    * Initial release

+ 21 - 0
plugins/admin-addon-media-metadata/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Clive Beckett
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 90 - 0
plugins/admin-addon-media-metadata/README.md

@@ -0,0 +1,90 @@
+# Admin Addon Media Metadata Plugin
+
+The **Admin Addon Media Metadata** Plugin is an extension for the [Grav CMS](http://github.com/getgrav/grav) [Admin plugin](https://github.com/getgrav/grav-plugin-admin). It lets you add and **edit metadata for media files** in the Page Media browser.
+
+The Admin plugin has not been offering a feature like this yet. In order to add/edit metadata e.g. for an image you had to create a [image.filename].meta.yaml for the image in your file browser and edit it in a text editor.
+
+## Usage and Features
+
+- the plugin will create and edit [mediafile].meta.yaml files in your page folder via a simple form in the Admin plugin
+- by default you can add/edit a **title,** **alt** text, and a **caption** – see *Configuration* section below on how to adapt this for your Grav installation
+- multiline text can be added (e.g. for caption)
+- in case you are storing additional, manually added data in your [mediafile].meta.yaml file, it will not be overwritten even if the form does not let you change it
+
+### How to use it
+
+*(see also screenshots below)*
+
+1. hover any file in your Page Media section
+2. hit the small «i» button to open the metadata form *(the regular «i» button which just showed the metadata will be overwritten by the plugin)*
+
+### NOTE on setting “Auto metadata from Exif”
+
+If your system is set to automatically write EXIF metadata  
+`(system.yaml → media.auto_metadata_exif: true)`
+
+- On **Admin Plugin 1.10:**  
+Please **save the page** after uploading an image and **before** you write additional metadata using this plugin. On saving the page, the Admin Plugin will create the meta.yaml file including the EXIF data – but it won’t do so if a meta.yaml file already exists.
+- On **Admin Plugin 1.9** and **admin-addon-media-metadata >= 1.1.0:**  
+There’s no problem here: the EXIF data will be written immediately upon upload whereas this plugin now only writes or edits meta.yaml files when needed.
+
+## Installation
+
+### Grav Package Manager (GPM)
+
+If you can access your Grav installation via the command line, install the plugin by typing the following from your Grav root:
+
+```
+bin/gpm install admin-addon-media-metadata
+```
+
+### Admin Tool Web Interface
+
+In the **Plugins** section, hit the **[+ Add]** button, search for Admin Addon Media Metadata and install.
+
+### Manual Installation
+
+To install the plugin manually, download the ZIP version of the latest release of this repository and unzip it under `/your/site/grav/user/plugins`. Then rename the folder to `admin-addon-media-metadata`. You can find these files on [GitHub](https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/releases).
+
+## Configuration
+
+The default fields in the metadata form are alt, title, and caption. If you want to add more data to your meta.yaml files, please copy  
+`user/plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml` to  
+`user/config/plugins/admin-addon-media-metadata.yaml`  
+and add more form fields to the form by updating the copy. E.g. if you want to add a field for a web link, you might add the following lines:
+
+```
+  - type: text
+    label: Web link
+    name: weblink
+    placeholder: https://domain.tld/
+```
+
+The URL field will be available in your metadata form and you’ll be able edit the information.
+
+### Additional Page Specific Metadata Fields (v1.1 and later)
+
+You may also add page specific fields to a page’s frontmatter. *Note: The fields will be added to the form, not override the ones from the above mentioned config files:*
+
+```
+admin-addon-media-metadata:
+  metadata_form:
+    fields:
+      -
+        type: text
+        label: 'Yet another field'
+        name: yetAnotherField
+        placeholder: 'yet yet yet'
+```
+
+## Credits
+
+I have based the plugin on Dávid Szabó’s [Admin Addon Media Rename plugin](https://github.com/david-szabo97/grav-plugin-admin-addon-media-rename). Much of the code would not have been possible for me without Dávid’s work.
+
+[@renards](https://github.com/renards) helped a lot in replacing my original self-written Yaml parsing and writing code with Grav core technology and thus made version 1.0.0 possible.
+
+## Screenshots
+
+![](assets/1-open-form.jpg)
+
+![](assets/2-form-opened.jpg)

+ 40 - 0
plugins/admin-addon-media-metadata/admin-addon-media-metadata.css

@@ -0,0 +1,40 @@
+span[data-dz-name] {
+	cursor: text;
+}
+
+.dz-metadata-edit {
+	display: none;
+	position: absolute;
+	width: 25px;
+	height: 25px;
+	right: -26px;
+	font-size: 0;
+	cursor: pointer;
+	background: #ededed;
+/* 	bottom: 25px; */
+	top: 49px;
+}
+
+.dz-metadata-edit:after {
+	text-align: center;
+	display: block;
+	font-family: FontAwesome;
+	font-size: 18px;
+	line-height: 25px;
+/* 	content: "\f044"; */
+	content: "\f05a";
+	color: #737c81;
+}
+
+.dz-metadata-edit:hover:after {
+	color: #0082ba;
+}
+
+.dropzone .dz-preview:hover .dz-metadata-edit {
+	display: block;
+}
+
+div.form-label { padding-right: 1rem; }
+[data-remodal-id=modal-admin-addon-media-metadata] h1 { line-height: 1; }
+form h1 span.header { }
+form h1 span.filename { font-size: 1rem; }

+ 113 - 0
plugins/admin-addon-media-metadata/admin-addon-media-metadata.js

@@ -0,0 +1,113 @@
+/**
+ * some of the code is based on the Admin Addon Media Rename by Dávid Szabó
+ *     see https://github.com/david-szabo97/grav-plugin-admin-addon-media-rename
+ */
+$(function() {
+
+	adminAddonMediaMetadata.metadata = {};
+
+	var clickedElement;
+	var fileName;
+	var $elementName;
+	var modal;
+	var $modal;
+	var isPageMedia = false;
+
+	// Append modal
+	$('body').append(adminAddonMediaMetadata.MODAL);
+
+	$(document).off('click', '[data-dz-name], .dz-metadata-edit');
+	$(document).on('click', '[data-dz-name], .dz-metadata-edit', function(e) {
+		clickedElement = $(this);
+		modal = $.remodal.lookup[$('[data-remodal-id=modal-admin-addon-media-metadata]').data('remodal')];
+		$modal = modal.$modal;
+		modal.open();
+
+		// Populate fields
+		elementName = clickedElement.closest('.dz-preview').find('[data-dz-name]')
+		fileName = elementName.text();
+		$('[name=filename]', $modal).val(fileName);
+		if (mediaListOnLoad[fileName] !== undefined) {
+			for (var i = 0; i < metadataFormFields.length; i++) {
+				$('[name=' + metadataFormFields[i] + ']', $modal).val(mediaListOnLoad[fileName][metadataFormFields[i]]);
+			}
+		} else {
+			for (var i = 0; i < metadataFormFields.length; i++) {
+				$('[name=' + metadataFormFields[i] + ']', $modal).val('');
+			}
+		}
+		$modal.find('form span.filename').text(fileName);
+
+		// Reset loading state
+		$('.loading', $modal).addClass('hidden');
+//		$('.button', $modal).removeClass('hidden').css('visibility', 'hidden');
+		$('.button', $modal).removeClass('hidden');
+
+		isPageMedia = !clickedElement.closest('.dz-preview').hasClass('dz-no-editor');
+		$modal.find('.block-toggle').toggleClass('hidden', !isPageMedia);
+		$modal.find('.page-media-info').toggleClass('hidden', !isPageMedia);
+		$modal.find('.non-page-media-info').toggleClass('hidden', isPageMedia);
+	});
+
+	/**
+	 * add the new data dynamically to mediaListOnLoad JSON object
+	 * for newly uploaded media files as well as for “older” files, overwriting the data already in that JSON object
+	 * and save it in the YAML file
+	 * Variables from above ($(document).on), like fileName and $modal are still available
+	 */
+	$(document).on('click', '[data-remodal-id=modal-admin-addon-media-metadata] .button', function(e) {
+		// hiding the button and showing the loading icon
+		$('.loading', $modal).removeClass('hidden');
+		$('.button', $modal).addClass('hidden');
+
+		// create the file object for newly uploaded files
+		if (mediaListOnLoad[fileName] === undefined) {
+			mediaListOnLoad[fileName] = new Object();
+			mediaListOnLoad[fileName]['filename'] = fileName;
+		}
+
+		// Do request with JS Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
+		var data = new FormData();
+		data.append('filename', fileName);
+		data.append('admin-nonce', GravAdmin.config.admin_nonce);
+
+		// add the new values to mediaListOnLoad (for later updates before page reload)
+		// append the new values to the FormData (data.append)
+		for (var i = 0; i < metadataFormFields.length; i++) {
+			var newVal = $('[name=' + metadataFormFields[i] + ']', $modal).val();
+			mediaListOnLoad[fileName][metadataFormFields[i]] = newVal;
+			data.append(metadataFormFields[i], newVal);
+		}
+
+		fetch(adminAddonMediaMetadata.PATH, { method: 'POST', body: data, credentials: 'same-origin' })
+			//.then(res => res.json())
+			.then(result => {
+				if (result.error) {
+					var alertModal = $.remodal.lookup[$('[data-remodal-id=modal-admin-addon-media-metadata-alert]').data('remodal')];
+					alertModal.open();
+					$('p', alertModal.$modal).html(result.error.msg);
+					return;
+				}
+
+				modal.close();
+			});
+	});
+
+	// adding the “edit metadata” button next to all media files
+	setInterval(function addMetadataButton() {
+		$('.dz-preview').each(function(i, dz) {
+			if ($(this).find('.dz-metadata-edit').length == 0) {
+				var editButton = document.createElement('a');
+				//editButton.href = 'javascript:undefined;';
+				editButton.title = adminAddonMediaMetadataButton;
+				editButton.className = 'dz-metadata-edit';
+				editButton.innerText = adminAddonMediaMetadataButton;
+				var fileName = $(this).find('[data-dz-name]').text();
+				editButton.setAttribute('data-filename', fileName);
+				$(this).append(editButton);
+			}
+			$(this).find('.dz-metadata').remove();
+		});
+	}, 1000);
+});
+

+ 321 - 0
plugins/admin-addon-media-metadata/admin-addon-media-metadata.php

@@ -0,0 +1,321 @@
+<?php
+namespace Grav\Plugin;
+
+use Composer\Autoload\ClassLoader;
+use Grav\Common\Page\Media;
+use Grav\Common\Plugin;
+use Grav\Common\Yaml;
+use RocketTheme\Toolbox\File\File;
+
+/**
+ * Class AdminAddonMediaMetadataPlugin
+ * some of the code is based on the Admin Addon Media Rename by Dávid Szabó
+ *     see https://github.com/david-szabo97/grav-plugin-admin-addon-media-rename
+ * @package Grav\Plugin
+ */
+class AdminAddonMediaMetadataPlugin extends Plugin
+{
+    const ROUTE = '/admin-addon-media-metadata';
+    const TASK_METADATA = 'AdminAddonMediaMetadataEdit';
+
+    /**
+     * @return array
+     *
+     * The getSubscribedEvents() gives the core a list of events
+     * that the plugin wants to listen to. The key of each
+     * array section is the event that the plugin listens to
+     * and the value (in the form of an array) contains the
+     * callable (or function) as well as the priority. The
+     * higher the number the higher the priority.
+     */
+    public static function getSubscribedEvents()
+    {
+        return [
+            ['autoload', 100000], // TODO: Remove when plugin requires Grav >=1.7
+            'onPluginsInitialized' => ['onPluginsInitialized', 0]
+        ];
+    }
+
+    /**
+     * Composer autoload.
+     * is
+     * @return ClassLoader
+     */
+    public function autoload(): ClassLoader
+    {
+        return require __DIR__ . '/vendor/autoload.php';
+    }
+
+    public function getPath()
+    {
+        return '/' . trim($this->grav['admin']->base, '/') . '/' . trim(self::ROUTE, '/');
+    }
+
+    public function buildBaseUrl()
+    {
+        return rtrim($this->grav['uri']->rootUrl(true), '/') . '/' . trim($this->getPath(), '/');
+    }
+
+    /**
+     * Initialize the plugin
+     */
+    public function onPluginsInitialized()
+    {
+        if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
+            return;
+        }
+
+        if ($this->grav['uri']->path() == $this->getPath()) {
+            $this->enable([
+                'onPagesInitialized' => ['processRenameRequest', 0]
+            ]);
+            return;
+        }
+
+        $this->enable([
+            'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
+            'onPagesInitialized' => ['onTwigExtensions', 0],
+            //'onAdminAfterAddMedia' => ['createMetaYaml', 0], // removing the call of this method: see method below
+            'onAdminTaskExecute' => ['editMetaDataFile', 0],
+        ]);
+    }
+
+    public function onTwigTemplatePaths()
+    {
+        $this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
+    }
+
+    public function onTwigExtensions()
+    {
+        $page = $this->grav['admin']->page(true);
+        if (!$page) {
+            return;
+        }
+
+        /**
+         * get metadata form field config from plugin admin-addon-media-metadata.yaml file
+         * or local override in user/config/plugins/admin-addon-media-metadata.yaml
+         * or from page frontmatter
+         */
+        //$formFields = $this->config->get('plugins.admin-addon-media-metadata.metadata_form');
+        $config = $this->mergeConfig($page, true);
+        $formFields = $config->get('metadata_form');
+
+        /**
+         * list all needed data keys from the fields configuration
+         */
+        $arrMetaKeys = $this->editableFields($formFields['fields']);
+
+        $path = $page->path();
+        $media = new Media($path);
+        $allMedia = $media->all();
+        $arrFiles = [];
+        $i = 0;
+        foreach ($allMedia as $filename => $file) {
+            $metadata = $file->meta();
+            $arrFiles[$filename] = [
+                'filename' => $filename,
+            ];
+            /**
+             * for each file: write stored metadata for each editable field into an array
+             * this will be output as inline JS variable adminAddonMediaMetadata
+             */
+            foreach ($arrMetaKeys as $metaKey => $info) {
+                $arrFiles[$filename][$metaKey] = $metadata->$metaKey;
+            }
+            $i++;
+        }
+
+        $jsArrFormFields = '';
+        $i = 0;
+        foreach ($arrMetaKeys as $metaKey => $info) {
+            $jsArrFormFields .= ($i > 0) ? ",'" . $metaKey . "'" : "'" . $metaKey . "'";
+            $i++;
+        }
+
+        $inlineJs = 'var metadataFormFields = [' . $jsArrFormFields . '];';
+        $inlineJs .= PHP_EOL . 'var mediaListOnLoad = ' . json_encode($arrFiles) . ';';
+        $modal = $this->grav['twig']->twig()->render('metadata-modal.html.twig', $formFields);
+        $jsConfig = [
+            'PATH' => $this->buildBaseUrl() . '/' . $page->route() . '/task:' . self::TASK_METADATA,
+            'MODAL' => $modal
+        ];
+        $inlineJs .= PHP_EOL . 'var adminAddonMediaMetadata = ' . json_encode($jsConfig) . ';';
+        $inlineJs .= PHP_EOL . $this->grav['twig']->twig()->render('additionalInlineJS.html.twig');
+        $this->grav['assets']->addInlineJs($inlineJs, -1000);
+        $this->grav['assets']->addCss('plugin://admin-addon-media-metadata/admin-addon-media-metadata.css', -1000);
+        $this->grav['assets']->addJs('plugin://admin-addon-media-metadata/admin-addon-media-metadata.js', -1000);
+    }
+
+    public function editTest()
+    {
+        $this->outputError('blob');
+    }
+
+    /**
+     * writes metadata into the [mediafile].meta.yaml file
+     * creates the .meta.yaml file if it does not yet exist
+     */
+    public function editMetaDataFile($e)
+    {
+        $method = $e['method'];
+        if ($method === 'task' . self::TASK_METADATA) {
+            $fileName = $_POST['filename'];
+
+            $pageObj = $this->grav['admin']->page();
+            $basePath = $pageObj->path() . DS;
+
+            $filePath = $basePath . $fileName;
+
+/**
+ * temporarily removing the condition that checks for the media file to exist
+ * as in Admin plugin 1.10 a media file will be uploaded to a tmp folder first
+ * and moved to the page folder on saving the page
+ *
+ * there needs to be a better solution for this
+ *
+ * also: take care of system.yaml → media.auto_metadata_exif
+ *     if set to TRUE, a meta.yaml with EXIF data will be written, but only if the meta.yaml does not yet exist
+ *     in Admin 1.10 you need to save a page in order to have the meta.yaml file with EXIF data created
+ *     in Admin 1.9 this is not a problem since this plugin now (>=1.1.0) writes metadata only when needed
+ */
+//          if (!file_exists($filePath)) {
+//              $this->outputError($this->grav['language']->translate(['PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ERRORS.MEDIA_FILE_NOT_FOUND', $filePath]));
+//          } else {
+                $metaDataFilePath = $filePath . '.meta.yaml';
+
+                /**
+                 * get the list of form data from the fields configuration
+                 */
+                $arrMetaKeys = $this->editableFields();
+
+                if (file_exists($metaDataFilePath)) {
+                    /**
+                     * get array of all current metadata for the file
+                     * this is to avoid overwriting data that has been added to the meta.yaml file in the file browser
+                     */
+                    $storedMetaData = Yaml::parse(file_get_contents($metaDataFilePath));
+
+                    /**
+                     * overwrite the currently stored data for each field in the form
+                     */
+                    foreach ($arrMetaKeys as $metaKey => $info) {
+                        if (isset($_POST[$metaKey])) {
+                            $storedMetaData[$metaKey] = $_POST[$metaKey];
+                        }
+                    }
+
+                } else {
+                    /**
+                     * create metaData in case the meta data file does not yet exist
+                     */
+                    $storedMetaData = [];
+                    foreach ($arrMetaKeys as $metaKey => $info) {
+                        if (isset($_POST[$metaKey])) {
+                            $storedMetaData[$metaKey] = $_POST[$metaKey];
+                        } else {
+                            $storedMetaData[$metaKey] = '';
+                        }
+                    }
+                }
+
+                /**
+                 * Get an instance of the meta file and write the data to it
+                 * @see \Grav\Common\Page\Media
+                 */
+                $metaDataFile = File::instance($metaDataFilePath);
+                $metaDataFile->save(Yaml::dump($storedMetaData));
+
+                //$this->outputError($newYamlText);
+//          }
+        }
+    }
+
+/**
+ * this method will not be called in Admin Plugin v1.10 (up until rc.11 anyway)
+ * additionally, writing the YAML file on upload will meddle with the system functionality
+ *     that writes exif data
+ *     in Admin Plugin v1.9 this functionality writes the meta.yaml file
+ *         on upload, this plugin will then just add the other metadata
+ *         when writing the meta.yaml file upon upload the system functionality will be overwritten
+ *     in Admin Plugin v1.10 you need to save the page before using this plugin on a file
+ *         if you want the exif data to be stored
+ */
+    /**
+     * creates an image.meta.yaml file
+     * this file will be deleted by the core when deleting an image
+     * will be called after a media file has been added (see onPluginsInitialized())
+     */
+//    public function createMetaYaml()
+//    {
+//        $fileName = $_FILES['file']['name'];
+//
+//        $pageObj = $this->grav['admin']->page();
+//        $basePath = $pageObj->path() . DS;
+//
+//        $filePath = $basePath . $fileName;
+//        if (!file_exists($filePath)) {
+//            $this->outputError($this->grav['language']->translate(['PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ERRORS.MEDIA_FILE_NOT_FOUND', $filePath]));
+//        } else {
+//            // TODO: do that only for image files?
+//            $metaDataFileName = $fileName . '.meta.yaml';
+//            $metaDataFilePath = $basePath . $metaDataFileName;
+//            if (!file_exists($metaDataFilePath)) {
+//                /**
+//                 * get the list of form data from the fields configuration
+//                 */
+//                $arrMetaKeys = $this->editableFields();
+//
+//                $newMetaData = [];
+//                foreach ($arrMetaKeys as $metaKey => $info) {
+//                    $newMetaData[$metaKey] = '';
+//                }
+//
+//                /**
+//                 * Get an instance of the meta file and write the data to it
+//                 * @see \Grav\Common\Page\Media
+//                 */
+//                $metaDataFile = File::instance($metaDataFilePath);
+//                $metaDataFile->save(Yaml::dump($newMetaData));
+//            }
+//        }
+//    }
+
+    /**
+     * return all editable fields from form configuration
+     */
+    private function editableFields($fieldsConf = null)
+    {
+        if ($fieldsConf === null) {
+            $page = $this->grav['admin']->page(true);
+            if (!$page) {
+                return;
+            }
+            /**
+             * get metadata form field config from plugin admin-addon-media-metadata.yaml file
+             * or local override in user/config/plugins/admin-addon-media-metadata.yaml
+             * or from page frontmatter
+             */
+            //$formFields = $this->config->get('plugins.admin-addon-media-metadata.metadata_form');
+            $config = $this->mergeConfig($page, true);
+            $formFields = $config->get('metadata_form');
+            $fieldsConf = $formFields['fields'];
+        }
+        $arrMetaKeys = [];
+        foreach ($fieldsConf as $singleFieldConf) {
+            if (isset($singleFieldConf['name'], $singleFieldConf['type']) && $singleFieldConf['name'] !== 'filename') {
+                $arrMetaKeys[$singleFieldConf['name']] = [
+                    'name' => $singleFieldConf['name'],
+                    'type' => $singleFieldConf['type']
+                ];
+            }
+        }
+        return $arrMetaKeys;
+    }
+
+    public function outputError($msg)
+    {
+        header('HTTP/1.1 400 Bad Request');
+        die(json_encode(['error' => ['msg' => $msg]]));
+    }
+}

+ 21 - 0
plugins/admin-addon-media-metadata/admin-addon-media-metadata.yaml

@@ -0,0 +1,21 @@
+enabled: true
+
+metadata_form:
+  fields:
+  - type: hidden
+    title: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.FILENAME
+    name: filename
+    readonly: true
+
+  - type: text
+    label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.TITLE
+    name: title
+    
+  - type: text
+    label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.ALT
+    name: alt
+
+  - type: textarea
+    label: PLUGIN_ADMIN_ADDON_MEDIA_METADATA.CAPTION
+    name: caption
+    rows: 3

BIN
plugins/admin-addon-media-metadata/assets/1-open-form.jpg


BIN
plugins/admin-addon-media-metadata/assets/2-form-opened.jpg


+ 30 - 0
plugins/admin-addon-media-metadata/blueprints.yaml

@@ -0,0 +1,30 @@
+name: Admin Addon Media Metadata
+version: 1.1.0
+description: 'This plugin is an addon for the Grav CMS Admin plugin and lets you add and edit metadata for media files'
+icon: plug
+author:
+  name: Clive Beckett
+  email: clive@musikinsnetz.de
+homepage: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata
+keywords: grav, admin, plugin, metadata, image, alt, title, caption
+bugs: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/issues
+docs: https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata/blob/develop/README.md
+license: MIT
+
+dependencies:
+  - { name: grav, version: '1.5 - 1.7' }
+  - { name: admin, version: '1.8 - 1.10' }
+
+form:
+  validation: loose
+  fields:
+    enabled:
+      type: toggle
+      label: PLUGIN_ADMIN.PLUGIN_STATUS
+      highlight: 1
+      default: 0
+      options:
+        1: PLUGIN_ADMIN.ENABLED
+        0: PLUGIN_ADMIN.DISABLED
+      validate:
+        type: bool

+ 29 - 0
plugins/admin-addon-media-metadata/composer.json

@@ -0,0 +1,29 @@
+{
+    "name": "clivebeckett/admin-addon-media-metadata",
+    "type": "grav-plugin",
+    "description": "Add Metadata (title, alt, caption) to image files in admin plugin",
+    "keywords": ["plugin"],
+    "homepage": "https://github.com/clivebeckett/grav-plugin-admin-addon-media-metadata",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Clive Beckett",
+            "email": "clive@musikinsnetz.de",
+            "role": "Developer"
+        }
+    ],
+    "require": {
+        "php": ">=7.1.3"
+    },
+    "autoload": {
+        "psr-4": {
+            "Grav\\Plugin\\AdminAddonMediaMetadata\\": "classes/"
+        },
+        "classmap":    ["admin-addon-media-metadata.php"]
+    },
+    "config": {
+        "platform": {
+            "php": "7.1.3"
+        }
+    }
+}

+ 23 - 0
plugins/admin-addon-media-metadata/composer.lock

@@ -0,0 +1,23 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "b884fad27830ca91d1b5af86da9aff06",
+    "packages": [],
+    "packages-dev": [],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": [],
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": ">=7.1.3"
+    },
+    "platform-dev": [],
+    "platform-overrides": {
+        "php": "7.1.3"
+    },
+    "plugin-api-version": "1.1.0"
+}

+ 11 - 0
plugins/admin-addon-media-metadata/languages/de.yaml

@@ -0,0 +1,11 @@
+PLUGIN_ADMIN_ADDON_MEDIA_METADATA:
+  BUTTON: Metadaten ändern
+  FORM_HEADER: Metadaten für Mediendatei ändern
+  FILENAME: Dateiname
+  TITLE: Titel
+  ALT: Beschreibung für den Fall, dass die Datei nicht angezeigt werden kann (alternativer Text)
+  CAPTION: Bildunterschrift
+  WRITING_DATA: Metadaten werden geschrieben
+  WRITING_FAILED: Metadaten konnten nicht geschrieben werden
+  ERRORS:
+    MEDIA_FILE_NOT_FOUND: Die Mediendatei konnte nicht gefunden werden

+ 11 - 0
plugins/admin-addon-media-metadata/languages/en.yaml

@@ -0,0 +1,11 @@
+PLUGIN_ADMIN_ADDON_MEDIA_METADATA:
+  BUTTON: Edit metadata
+  FORM_HEADER: Edit metadata for media file
+  FILENAME: Filename
+  TITLE: Title
+  ALT: Describe the media file for when it can’t be displayed (alternative text)
+  CAPTION: Caption
+  WRITING_DATA: Writing metadata
+  WRITING_FAILED: Writing image metadata failed
+  ERRORS:
+    MEDIA_FILE_NOT_FOUND: The media file could not be found

+ 11 - 0
plugins/admin-addon-media-metadata/languages/fr.yaml

@@ -0,0 +1,11 @@
+PLUGIN_ADMIN_ADDON_MEDIA_METADATA:
+  BUTTON: Editer les métadonnées pour ce média
+  FORM_HEADER: Editer les métadonnées pour ce média
+  FILENAME: Nom du fichier
+  TITLE: Titre
+  ALT: Description du média lorsqu'il ne peut être affiché (texte alternatif)
+  CAPTION: Légende
+  WRITING_DATA: Ajout des métadonnées
+  WRITING_FAILED: Impossible d'ajouter les métadonnées
+  ERRORS:
+    MEDIA_FILE_NOT_FOUND: Le fichier du média ne peut être trouvé

+ 2 - 0
plugins/admin-addon-media-metadata/templates/additionalInlineJS.html.twig

@@ -0,0 +1,2 @@
+var adminAddonMediaMetadataButton = '{{ 'PLUGIN_ADMIN_ADDON_MEDIA_METADATA.BUTTON'|t }}';
+var adminAddonMediaMetadataLang = '{{ grav.user.language|default('en') }}';

+ 36 - 0
plugins/admin-addon-media-metadata/templates/metadata-modal.html.twig

@@ -0,0 +1,36 @@
+<div class="remodal" data-remodal-id="modal-admin-addon-media-metadata" data-remodal-options="hashTracking: false">
+	<form method="post" onsubmit='return false;'>
+		<div class="block block-section">
+			<h1>
+				<span class="header">{{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.FORM_HEADER"|t }}</span><br>
+				<span class="filename"></span>
+			</h1>
+		</div>
+		{% for field in fields %}
+			{% if field.type %}
+				{% set value = data.value(field.name) %}
+				<div class="block block-{{field.type}}">
+					{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
+				</div>
+			{% endif %}
+		{% endfor %}
+
+		<div class="button-bar">
+			<div class="loading">
+				{{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.WRITING_DATA"|t }} … <i class="fa fa-spinner fa-spin"></i>
+			</div>
+
+			<button class="button primary">{{ "PLUGIN_ADMIN.SAVE"|tu }}</button>
+		</div>
+	</form>
+</div>
+
+<div class="remodal" data-remodal-id="modal-admin-addon-media-metadata-alert" data-remodal-options="hashTracking: false">
+	<form method="post" onsubmit='return false;'>
+		<h1>{{ "PLUGIN_ADMIN_ADDON_MEDIA_METADATA.WRITING_FAILED"|t }}</h1>
+		<p class="bigger"></p>
+		<div class="button-bar">
+			<button data-remodal-action="confirm" class="button primary">{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
+		</div>
+	</form>
+</div>

+ 7 - 0
plugins/admin-addon-media-metadata/vendor/autoload.php

@@ -0,0 +1,7 @@
+<?php
+
+// autoload.php @generated by Composer
+
+require_once __DIR__ . '/composer/autoload_real.php';
+
+return ComposerAutoloaderInit487bda5860f2ea416ef2181e18c0fcbf::getLoader();

+ 445 - 0
plugins/admin-addon-media-metadata/vendor/composer/ClassLoader.php

@@ -0,0 +1,445 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\Autoload;
+
+/**
+ * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
+ *
+ *     $loader = new \Composer\Autoload\ClassLoader();
+ *
+ *     // register classes with namespaces
+ *     $loader->add('Symfony\Component', __DIR__.'/component');
+ *     $loader->add('Symfony',           __DIR__.'/framework');
+ *
+ *     // activate the autoloader
+ *     $loader->register();
+ *
+ *     // to enable searching the include path (eg. for PEAR packages)
+ *     $loader->setUseIncludePath(true);
+ *
+ * In this example, if you try to use a class in the Symfony\Component
+ * namespace or one of its children (Symfony\Component\Console for instance),
+ * the autoloader will first look for the class under the component/
+ * directory, and it will then fallback to the framework/ directory if not
+ * found before giving up.
+ *
+ * This class is loosely based on the Symfony UniversalClassLoader.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Jordi Boggiano <j.boggiano@seld.be>
+ * @see    http://www.php-fig.org/psr/psr-0/
+ * @see    http://www.php-fig.org/psr/psr-4/
+ */
+class ClassLoader
+{
+    // PSR-4
+    private $prefixLengthsPsr4 = array();
+    private $prefixDirsPsr4 = array();
+    private $fallbackDirsPsr4 = array();
+
+    // PSR-0
+    private $prefixesPsr0 = array();
+    private $fallbackDirsPsr0 = array();
+
+    private $useIncludePath = false;
+    private $classMap = array();
+    private $classMapAuthoritative = false;
+    private $missingClasses = array();
+    private $apcuPrefix;
+
+    public function getPrefixes()
+    {
+        if (!empty($this->prefixesPsr0)) {
+            return call_user_func_array('array_merge', $this->prefixesPsr0);
+        }
+
+        return array();
+    }
+
+    public function getPrefixesPsr4()
+    {
+        return $this->prefixDirsPsr4;
+    }
+
+    public function getFallbackDirs()
+    {
+        return $this->fallbackDirsPsr0;
+    }
+
+    public function getFallbackDirsPsr4()
+    {
+        return $this->fallbackDirsPsr4;
+    }
+
+    public function getClassMap()
+    {
+        return $this->classMap;
+    }
+
+    /**
+     * @param array $classMap Class to filename map
+     */
+    public function addClassMap(array $classMap)
+    {
+        if ($this->classMap) {
+            $this->classMap = array_merge($this->classMap, $classMap);
+        } else {
+            $this->classMap = $classMap;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix, either
+     * appending or prepending to the ones previously set for this prefix.
+     *
+     * @param string       $prefix  The prefix
+     * @param array|string $paths   The PSR-0 root directories
+     * @param bool         $prepend Whether to prepend the directories
+     */
+    public function add($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            if ($prepend) {
+                $this->fallbackDirsPsr0 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr0
+                );
+            } else {
+                $this->fallbackDirsPsr0 = array_merge(
+                    $this->fallbackDirsPsr0,
+                    (array) $paths
+                );
+            }
+
+            return;
+        }
+
+        $first = $prefix[0];
+        if (!isset($this->prefixesPsr0[$first][$prefix])) {
+            $this->prefixesPsr0[$first][$prefix] = (array) $paths;
+
+            return;
+        }
+        if ($prepend) {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixesPsr0[$first][$prefix]
+            );
+        } else {
+            $this->prefixesPsr0[$first][$prefix] = array_merge(
+                $this->prefixesPsr0[$first][$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace, either
+     * appending or prepending to the ones previously set for this namespace.
+     *
+     * @param string       $prefix  The prefix/namespace, with trailing '\\'
+     * @param array|string $paths   The PSR-4 base directories
+     * @param bool         $prepend Whether to prepend the directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function addPsr4($prefix, $paths, $prepend = false)
+    {
+        if (!$prefix) {
+            // Register directories for the root namespace.
+            if ($prepend) {
+                $this->fallbackDirsPsr4 = array_merge(
+                    (array) $paths,
+                    $this->fallbackDirsPsr4
+                );
+            } else {
+                $this->fallbackDirsPsr4 = array_merge(
+                    $this->fallbackDirsPsr4,
+                    (array) $paths
+                );
+            }
+        } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
+            // Register directories for a new namespace.
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        } elseif ($prepend) {
+            // Prepend directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                (array) $paths,
+                $this->prefixDirsPsr4[$prefix]
+            );
+        } else {
+            // Append directories for an already registered namespace.
+            $this->prefixDirsPsr4[$prefix] = array_merge(
+                $this->prefixDirsPsr4[$prefix],
+                (array) $paths
+            );
+        }
+    }
+
+    /**
+     * Registers a set of PSR-0 directories for a given prefix,
+     * replacing any others previously set for this prefix.
+     *
+     * @param string       $prefix The prefix
+     * @param array|string $paths  The PSR-0 base directories
+     */
+    public function set($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr0 = (array) $paths;
+        } else {
+            $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Registers a set of PSR-4 directories for a given namespace,
+     * replacing any others previously set for this namespace.
+     *
+     * @param string       $prefix The prefix/namespace, with trailing '\\'
+     * @param array|string $paths  The PSR-4 base directories
+     *
+     * @throws \InvalidArgumentException
+     */
+    public function setPsr4($prefix, $paths)
+    {
+        if (!$prefix) {
+            $this->fallbackDirsPsr4 = (array) $paths;
+        } else {
+            $length = strlen($prefix);
+            if ('\\' !== $prefix[$length - 1]) {
+                throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
+            }
+            $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
+            $this->prefixDirsPsr4[$prefix] = (array) $paths;
+        }
+    }
+
+    /**
+     * Turns on searching the include path for class files.
+     *
+     * @param bool $useIncludePath
+     */
+    public function setUseIncludePath($useIncludePath)
+    {
+        $this->useIncludePath = $useIncludePath;
+    }
+
+    /**
+     * Can be used to check if the autoloader uses the include path to check
+     * for classes.
+     *
+     * @return bool
+     */
+    public function getUseIncludePath()
+    {
+        return $this->useIncludePath;
+    }
+
+    /**
+     * Turns off searching the prefix and fallback directories for classes
+     * that have not been registered with the class map.
+     *
+     * @param bool $classMapAuthoritative
+     */
+    public function setClassMapAuthoritative($classMapAuthoritative)
+    {
+        $this->classMapAuthoritative = $classMapAuthoritative;
+    }
+
+    /**
+     * Should class lookup fail if not found in the current class map?
+     *
+     * @return bool
+     */
+    public function isClassMapAuthoritative()
+    {
+        return $this->classMapAuthoritative;
+    }
+
+    /**
+     * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
+     *
+     * @param string|null $apcuPrefix
+     */
+    public function setApcuPrefix($apcuPrefix)
+    {
+        $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
+    }
+
+    /**
+     * The APCu prefix in use, or null if APCu caching is not enabled.
+     *
+     * @return string|null
+     */
+    public function getApcuPrefix()
+    {
+        return $this->apcuPrefix;
+    }
+
+    /**
+     * Registers this instance as an autoloader.
+     *
+     * @param bool $prepend Whether to prepend the autoloader or not
+     */
+    public function register($prepend = false)
+    {
+        spl_autoload_register(array($this, 'loadClass'), true, $prepend);
+    }
+
+    /**
+     * Unregisters this instance as an autoloader.
+     */
+    public function unregister()
+    {
+        spl_autoload_unregister(array($this, 'loadClass'));
+    }
+
+    /**
+     * Loads the given class or interface.
+     *
+     * @param  string    $class The name of the class
+     * @return bool|null True if loaded, null otherwise
+     */
+    public function loadClass($class)
+    {
+        if ($file = $this->findFile($class)) {
+            includeFile($file);
+
+            return true;
+        }
+    }
+
+    /**
+     * Finds the path to the file where the class is defined.
+     *
+     * @param string $class The name of the class
+     *
+     * @return string|false The path if found, false otherwise
+     */
+    public function findFile($class)
+    {
+        // class map lookup
+        if (isset($this->classMap[$class])) {
+            return $this->classMap[$class];
+        }
+        if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
+            return false;
+        }
+        if (null !== $this->apcuPrefix) {
+            $file = apcu_fetch($this->apcuPrefix.$class, $hit);
+            if ($hit) {
+                return $file;
+            }
+        }
+
+        $file = $this->findFileWithExtension($class, '.php');
+
+        // Search for Hack files if we are running on HHVM
+        if (false === $file && defined('HHVM_VERSION')) {
+            $file = $this->findFileWithExtension($class, '.hh');
+        }
+
+        if (null !== $this->apcuPrefix) {
+            apcu_add($this->apcuPrefix.$class, $file);
+        }
+
+        if (false === $file) {
+            // Remember that this class does not exist.
+            $this->missingClasses[$class] = true;
+        }
+
+        return $file;
+    }
+
+    private function findFileWithExtension($class, $ext)
+    {
+        // PSR-4 lookup
+        $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
+
+        $first = $class[0];
+        if (isset($this->prefixLengthsPsr4[$first])) {
+            $subPath = $class;
+            while (false !== $lastPos = strrpos($subPath, '\\')) {
+                $subPath = substr($subPath, 0, $lastPos);
+                $search = $subPath . '\\';
+                if (isset($this->prefixDirsPsr4[$search])) {
+                    $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
+                    foreach ($this->prefixDirsPsr4[$search] as $dir) {
+                        if (file_exists($file = $dir . $pathEnd)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-4 fallback dirs
+        foreach ($this->fallbackDirsPsr4 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 lookup
+        if (false !== $pos = strrpos($class, '\\')) {
+            // namespaced class name
+            $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
+                . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
+        } else {
+            // PEAR-like class name
+            $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
+        }
+
+        if (isset($this->prefixesPsr0[$first])) {
+            foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
+                if (0 === strpos($class, $prefix)) {
+                    foreach ($dirs as $dir) {
+                        if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                            return $file;
+                        }
+                    }
+                }
+            }
+        }
+
+        // PSR-0 fallback dirs
+        foreach ($this->fallbackDirsPsr0 as $dir) {
+            if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
+                return $file;
+            }
+        }
+
+        // PSR-0 include paths.
+        if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
+            return $file;
+        }
+
+        return false;
+    }
+}
+
+/**
+ * Scope isolated include.
+ *
+ * Prevents access to $this/self from included files.
+ */
+function includeFile($file)
+{
+    include $file;
+}

+ 21 - 0
plugins/admin-addon-media-metadata/vendor/composer/LICENSE

@@ -0,0 +1,21 @@
+
+Copyright (c) Nils Adermann, Jordi Boggiano
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is furnished
+to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+

+ 10 - 0
plugins/admin-addon-media-metadata/vendor/composer/autoload_classmap.php

@@ -0,0 +1,10 @@
+<?php
+
+// autoload_classmap.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Grav\\Plugin\\AdminAddonMediaMetadataPlugin' => $baseDir . '/admin-addon-media-metadata.php',
+);

+ 9 - 0
plugins/admin-addon-media-metadata/vendor/composer/autoload_namespaces.php

@@ -0,0 +1,9 @@
+<?php
+
+// autoload_namespaces.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+);

+ 10 - 0
plugins/admin-addon-media-metadata/vendor/composer/autoload_psr4.php

@@ -0,0 +1,10 @@
+<?php
+
+// autoload_psr4.php @generated by Composer
+
+$vendorDir = dirname(dirname(__FILE__));
+$baseDir = dirname($vendorDir);
+
+return array(
+    'Grav\\Plugin\\AdminAddonMediaMetadata\\' => array($baseDir . '/classes'),
+);

+ 55 - 0
plugins/admin-addon-media-metadata/vendor/composer/autoload_real.php

@@ -0,0 +1,55 @@
+<?php
+
+// autoload_real.php @generated by Composer
+
+class ComposerAutoloaderInit487bda5860f2ea416ef2181e18c0fcbf
+{
+    private static $loader;
+
+    public static function loadClassLoader($class)
+    {
+        if ('Composer\Autoload\ClassLoader' === $class) {
+            require __DIR__ . '/ClassLoader.php';
+        }
+    }
+
+    /**
+     * @return \Composer\Autoload\ClassLoader
+     */
+    public static function getLoader()
+    {
+        if (null !== self::$loader) {
+            return self::$loader;
+        }
+
+        spl_autoload_register(array('ComposerAutoloaderInit487bda5860f2ea416ef2181e18c0fcbf', 'loadClassLoader'), true, true);
+        self::$loader = $loader = new \Composer\Autoload\ClassLoader();
+        spl_autoload_unregister(array('ComposerAutoloaderInit487bda5860f2ea416ef2181e18c0fcbf', 'loadClassLoader'));
+
+        $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
+        if ($useStaticLoader) {
+            require_once __DIR__ . '/autoload_static.php';
+
+            call_user_func(\Composer\Autoload\ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::getInitializer($loader));
+        } else {
+            $map = require __DIR__ . '/autoload_namespaces.php';
+            foreach ($map as $namespace => $path) {
+                $loader->set($namespace, $path);
+            }
+
+            $map = require __DIR__ . '/autoload_psr4.php';
+            foreach ($map as $namespace => $path) {
+                $loader->setPsr4($namespace, $path);
+            }
+
+            $classMap = require __DIR__ . '/autoload_classmap.php';
+            if ($classMap) {
+                $loader->addClassMap($classMap);
+            }
+        }
+
+        $loader->register(true);
+
+        return $loader;
+    }
+}

+ 36 - 0
plugins/admin-addon-media-metadata/vendor/composer/autoload_static.php

@@ -0,0 +1,36 @@
+<?php
+
+// autoload_static.php @generated by Composer
+
+namespace Composer\Autoload;
+
+class ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf
+{
+    public static $prefixLengthsPsr4 = array (
+        'G' => 
+        array (
+            'Grav\\Plugin\\AdminAddonMediaMetadata\\' => 36,
+        ),
+    );
+
+    public static $prefixDirsPsr4 = array (
+        'Grav\\Plugin\\AdminAddonMediaMetadata\\' => 
+        array (
+            0 => __DIR__ . '/../..' . '/classes',
+        ),
+    );
+
+    public static $classMap = array (
+        'Grav\\Plugin\\AdminAddonMediaMetadataPlugin' => __DIR__ . '/../..' . '/admin-addon-media-metadata.php',
+    );
+
+    public static function getInitializer(ClassLoader $loader)
+    {
+        return \Closure::bind(function () use ($loader) {
+            $loader->prefixLengthsPsr4 = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$prefixDirsPsr4;
+            $loader->classMap = ComposerStaticInit487bda5860f2ea416ef2181e18c0fcbf::$classMap;
+
+        }, null, ClassLoader::class);
+    }
+}

+ 1 - 0
plugins/admin-addon-media-metadata/vendor/composer/installed.json

@@ -0,0 +1 @@
+[]

+ 17 - 0
plugins/admin/.editorconfig

@@ -0,0 +1,17 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+charset = utf-8
+end_of_line = lf
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+
+# 4 space indentation
+[*.php]
+indent_size = 4

+ 8 - 0
plugins/admin/.gitattributes

@@ -0,0 +1,8 @@
+# Linguist Normalizer
+*.yaml linguistic-language=PHP
+*.twig linguistic-language=PHP
+**/gulpfile.babel.js linguist-vendored
+**/webpack.conf.js linguist-vendored
+**/js/*.js linguist-vendored
+**/js/*.json linguist-vendored
+**/css-compiled/*.css linguist-vendored

+ 8 - 0
plugins/admin/.github/FUNDING.yml

@@ -0,0 +1,8 @@
+# These are supported funding model platforms
+
+github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: # Replace with a single Patreon username
+open_collective: grav
+ko_fi: # Replace with a single Ko-fi username
+tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+custom: # Replace with a single custom sponsorship URL

+ 6 - 0
plugins/admin/.github/dependabot.yml

@@ -0,0 +1,6 @@
+version: 2
+updates:
+- package-ecosystem: "github-actions"
+  directory: "/"
+  schedule:
+      interval: "weekly"

+ 17 - 0
plugins/admin/.gitignore

@@ -0,0 +1,17 @@
+themes/grav/.sass-cache
+.DS_Store
+crowdin.yaml
+
+# Node Modules
+**/node_modules/**
+themes/grav/js/admin.js
+themes/grav/js/vendor.js
+themes/grav/js/*.map
+.idea
+
+tests/_output/*
+tests/_support/_generated/*
+tests/cache/*
+tests/error.log
+/crowdin.yaml
+.vscode

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 1895 - 0
plugins/admin/CHANGELOG.md


+ 1 - 0
plugins/admin/CONTRIBUTING.md

@@ -0,0 +1 @@
+Please read the <a href="https://github.com/getgrav/grav/blob/develop/CONTRIBUTING.md" target="_blank">Contributing Guidelines of the Grav Project</a>

+ 21 - 0
plugins/admin/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2017 Grav
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 152 - 0
plugins/admin/README.md

@@ -0,0 +1,152 @@
+# Grav Standard Administration Panel Plugin
+
+This **admin plugin** for [Grav](https://github.com/getgrav/grav) is an HTML user interface that provides a convenient way to configure Grav and easily create and modify pages.  This will remain a totally optional plugin, and is not in any way required or needed to use Grav effectively.  In fact, the admin provides an intentionally limited view to ensure it remains easy to use and not overwhelming.  I'm sure power users will still prefer to work with the configuration files directly.
+
+![](assets/admin-dashboard.png)
+
+# Features
+
+* User login with automatic password encryption
+* Forgot password functionality
+* Logged-in-user management
+* One click Grav core updates
+* Dashboard with maintenance status, site activity and latest page updates
+* Notifications system for latest news, blogs, and announcements
+* Ajax-powered backup capability
+* Ajax-powered clear-cache capability
+* System configuration management
+* Site configuration management
+* Normal and Expert modes which allow editing via forms or YAML
+* Page listing with filtering and search
+* Page creation, editing, moving, copying, and deleting
+* Powerful syntax highlighting code editor with instant Grav-powered preview
+* Editor features, hot keys, toolbar, and distraction-free fullscreen mode
+* Drag-n-drop upload of page media files including drag-n-drop placement in the editor
+* One click theme and plugin updates
+* Plugin manager that allows listing and configuration of installed plugins
+* Theme manager that allows listing and configuration of installed themes
+* GPM-powered installation of new plugins and themes
+
+# Support
+
+#### Support
+
+We have tested internally, but we hope to use this public beta phase to identify, isolate, and fix issues related to the plugin to ensure it is as solid and reliable as possible.
+
+For **live chatting**, please use the dedicated [Discord Chat Room](https://getgrav.org/discord) for discussions directly related to Grav.
+
+For **bugs, features, improvements**, please ensure you [create issues in the admin plugin GitHub repository](https://github.com/getgrav/grav-plugin-admin).
+
+# Installation
+
+First ensure you are running the latest **Grav 1.6.7 or later**.  This is required for the admin plugin to run properly (`-f` forces a refresh of the GPM index).
+
+```
+$ bin/gpm selfupgrade -f
+```
+
+The admin plugin actually requires the help of 3 other plugins, so to get the admin plugin to work you first need to install **admin**, **login**, **forms**, and **email** plugins.  These are available via GPM, and because the plugin has dependencies you just need to proceed and install the admin plugin, and agree when prompted to install the others:
+
+```
+$ bin/gpm install admin
+```
+
+### Manual Installation
+
+Manual installation is not the recommended method of installation, however, it is still possible to install the admin plugin manually. Basically, you need to download each of the following plugins individually:
+
+* [admin](https://github.com/getgrav/grav-plugin-admin/archive/develop.zip)
+* [login](https://github.com/getgrav/grav-plugin-login/archive/develop.zip)
+* [form](https://github.com/getgrav/grav-plugin-form/archive/develop.zip)
+* [email](https://github.com/getgrav/grav-plugin-email/archive/develop.zip)
+
+Extract each archive file into your `user/plugins` folder, then ensure the folders are renamed to just `admin/`, `login/`, `form/`, and `email/`.  Then proceed with the **Usage instructions below**.
+
+# Usage
+
+### Create User with CLI
+
+After this you need to create a user account with admin privileges:
+
+```
+$ bin/plugin login new-user
+```
+
+### Create User Manually
+
+Alternatively, you can create a user account manually, in a file called `user/accounts/admin.yaml`. This **filename** is actually the **username** that you will use to login. The contents will contain the other information for the user.
+
+```
+password: 'password'
+email: 'youremail@mail.com'
+fullname: 'Johnny Appleseed'
+title: 'Site Administrator'
+access:
+  admin:
+    login: true
+    super: true
+```
+
+Of course you should edit your `email`, `password`, `fullname`, and `title` to suit your needs.
+
+> You can use any password when you manually put it in this `.yaml` file.  However, when you change your password in the admin, it must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters.
+
+# Accessing the Admin
+
+By default, you can access the admin by pointing your browser to `http://yoursite.com/admin`. You can simply log in with the `username` and `password` set in the YAML file you configured earlier.
+
+> After logging in, your **plaintext password** will be removed and replaced by an **encrypted** one.
+
+# Standard Free & Paid Pro Versions
+
+If you have been following the [blog](https://getgrav.org/blog), [Twitter](https://twitter.com/getgrav), [Discord chat](https://getgrav.org/discord), etc., you probably already know now that our intention is to provide two versions of this plugin.
+
+The **standard free version**, is very powerful, and has more functionality than most commercial flat-file CMS systems.
+
+We also intend to release in the near future a more feature-rich **pro version** that will include enhanced functionality, as well as some additional nice-to-have capabilities. This pro version will be a **paid** plugin the price of which is not yet 100% finalized.
+
+# Admin Events
+
+## General events
+
+- onAdminRegisterPermissions - (admin)
+- onAdminThemeInitialized
+- onAdminPage - (page)
+- onAdminMenu
+- onAdminTwigTemplatePaths - (paths)
+
+## Page specific events
+
+- onAdminDashboard
+- onAdminTools - (tools)
+- onAdminLogFiles - (logs)
+- onAdminGenerateReports - (reports)
+
+## Tasks
+
+- onAdminControllerInit - (controller)
+- onAdminTaskExecute - (controller, method)
+
+## Editing
+
+- onAdminData
+- onAdminSave - (object)
+- onAdminAfterSave - (object)
+
+## Pages
+
+- onAdminPageTypes - (types)
+- onAdminModularPageTypes
+- onAdminSave - (page)
+- onAdminAfterSaveAs - (path)
+- onAdminAfterSave - (page)
+- onAdminAfterDelete - (page)
+- onAdminAfterAddMedia - (page)
+- onAdminAfterDelMedia - (page)
+- onAdminCreatePageFrontmatter - (header, data)
+
+
+# Running Tests
+
+First install the dev dependencies by running `composer update` from the Grav root.
+Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.

+ 6 - 0
plugins/admin/UPGRADE.md

@@ -0,0 +1,6 @@
+# Upgrading to Admin 1.10
+
+Twig:
+
+* **Admin link**: When linking to another admin page, use `{{ admin_route('/config/site') }}` instead of any other method, such as `{{ base_url_relative }}/config/site` (fixes multi-language issues)
+

+ 1350 - 0
plugins/admin/admin.php

@@ -0,0 +1,1350 @@
+<?php
+namespace Grav\Plugin;
+
+use Composer\Autoload\ClassLoader;
+use Grav\Common\Cache;
+use Grav\Common\Data\Data;
+use Grav\Common\Debugger;
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Grav;
+use Grav\Common\Helpers\LogViewer;
+use Grav\Common\Inflector;
+use Grav\Common\Language\Language;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Page;
+use Grav\Common\Page\Pages;
+use Grav\Common\Plugin;
+use Grav\Common\Plugins;
+use Grav\Common\Processors\Events\RequestHandlerEvent;
+use Grav\Common\Session;
+use Grav\Common\Twig\Twig;
+use Grav\Common\Uri;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Common\Yaml;
+use Grav\Events\PermissionsRegisterEvent;
+use Grav\Framework\Acl\PermissionsReader;
+use Grav\Framework\Psr7\Response;
+use Grav\Framework\Session\Exceptions\SessionException;
+use Grav\Plugin\Admin\Admin;
+use Grav\Plugin\Admin\AdminFormFactory;
+use Grav\Plugin\Admin\Popularity;
+use Grav\Plugin\Admin\Router;
+use Grav\Plugin\Admin\Themes;
+use Grav\Plugin\Admin\AdminController;
+use Grav\Plugin\Admin\Twig\AdminTwigExtension;
+use Grav\Plugin\Admin\WhiteLabel;
+use Grav\Plugin\Form\Form;
+use Grav\Plugin\Form\Forms;
+use Grav\Plugin\Login\Login;
+use Pimple\Container;
+use Psr\Http\Message\ResponseInterface;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+/**
+ * Class AdminPlugin
+ * @package Grav\Plugin\Admin
+ */
+class AdminPlugin extends Plugin
+{
+    public $features = [
+        'blueprints' => 1000,
+    ];
+
+    /** @var bool */
+    protected $active = false;
+    /** @var string */
+    protected $template;
+    /** @var  string */
+    protected $theme;
+    /** @var string */
+    protected $route;
+    /** @var string */
+    protected $admin_route;
+    /** @var Uri */
+    protected $uri;
+    /** @var Admin */
+    protected $admin;
+    /** @var Session */
+    protected $session;
+    /** @var Popularity */
+    protected $popularity;
+    /** @var string */
+    protected $base;
+    /** @var string */
+    protected $version;
+
+    /**
+     * @return array
+     */
+    public static function getSubscribedEvents()
+    {
+        return [
+            'onPluginsInitialized' => [
+                ['setup', 100000],
+                ['onPluginsInitialized', 1001]
+            ],
+            'onRequestHandlerInit' => [
+                ['onRequestHandlerInit', 100000]
+            ],
+            'onFormRegisterTypes'  => ['onFormRegisterTypes', 0],
+            'onPageInitialized'    => ['onPageInitialized', 0],
+            'onShutdown'           => ['onShutdown', 1000],
+            PermissionsRegisterEvent::class => ['onRegisterPermissions', 1000],
+        ];
+    }
+
+    /**
+     * Get list of form field types specified in this plugin. Only special types needs to be listed.
+     *
+     * @return array
+     */
+    public function getFormFieldTypes()
+    {
+        return [
+            'column'   => [
+                'input@' => false
+            ],
+            'columns'  => [
+                'input@' => false
+            ],
+            'fieldset' => [
+                'input@' => false
+            ],
+            'section'  => [
+                'input@' => false
+            ],
+            'list'     => [
+                'array' => true
+            ],
+            'elements'  => [
+              'input@' => true
+            ],
+            'element'  => [
+              'input@' => false
+            ],
+            'file'     => [
+                'array' => true,
+                'media_field' => true,
+                'validate' => [
+                    'type' => 'ignore'
+                ]
+            ],
+            'pagemedia' => [
+                'array' => true,
+                'media_field' => true,
+                'validate' => [
+                    'type' => 'ignore'
+                ]
+            ],
+            'filepicker' => [
+                'media_picker_field' => true
+            ],
+            'pagemediaselect' => [
+                'media_picker_field' => true
+            ],
+            'permissions' => [
+                'ignore_empty' => true,
+                'validate' => [
+                    'type' => 'array'
+                ],
+                'filter' => [
+                    'type' => 'flatten_array',
+                    'value_type' => 'bool',
+                ]
+            ],
+            'acl_picker' => [
+                'ignore_empty' => true,
+                'validate' => [
+                    'type' => 'array'
+                ],
+                'filter' => [
+                    'type' => 'array',
+                    'key_type' => 'string',
+                    'value_type' => 'bool',
+                ]
+            ],
+            'taxonomy' => [
+                'multiple' => true,
+                'validate' => [
+                    'type' => 'array'
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @return ClassLoader
+     */
+    public function autoload(): ClassLoader
+    {
+        return require __DIR__ . '/vendor/autoload.php';
+    }
+
+    /**
+     * @param Event $event
+     * @return void
+     */
+    public function onFormRegisterTypes(Event $event): void
+    {
+        /** @var Forms $forms */
+        $forms = $event['forms'];
+        $forms->registerType('admin', new AdminFormFactory());
+    }
+
+    /**
+     * [onPluginsInitialized:100000]
+     *
+     * If the admin path matches, initialize the Login plugin configuration and set the admin
+     * as active.
+     *
+     * @return void
+     */
+    public function setup()
+    {
+        // Only enable admin if it has a route.
+        $route = $this->config->get('plugins.admin.route');
+        if (!$route) {
+            return;
+        }
+
+        /** @var Uri uri */
+        $this->uri = $this->grav['uri'];
+
+        $this->base = '/' . trim($route, '/');
+        $this->admin_route = rtrim($this->grav['pages']->base(), '/') . $this->base;
+
+        $inAdmin = $this->isAdminPath();
+
+        // If no users found, go to register.
+        if (!$inAdmin && !Admin::doAnyUsersExist()) {
+            $this->grav->redirect($this->admin_route);
+        }
+
+        // Only setup admin if we're inside the admin path.
+        if ($inAdmin) {
+            $this->setupAdmin();
+        }
+    }
+
+    /**
+     * [onPluginsInitialized:1001]
+     *
+     * If the admin plugin is set as active, initialize the admin
+     *
+     * @return void
+     */
+    public function onPluginsInitialized()
+    {
+        // Only activate admin if we're inside the admin path.
+        if ($this->active) {
+            $this->initializeAdmin();
+        }
+
+        // Always initialize popularity.
+        $this->popularity = new Popularity();
+    }
+
+    /**
+     * [onRequestHandlerInit:100000]
+     *
+     * @param RequestHandlerEvent $event
+     * @return void
+     */
+    public function onRequestHandlerInit(RequestHandlerEvent $event)
+    {
+        // Store this version.
+        $this->version = $this->getBlueprint()->get('version');
+
+        $route = $event->getRoute();
+        $base = $route->getRoute(0, 1);
+
+        if ($base === $this->base) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addMessage('Admin v' . $this->version);
+
+            $event->addMiddleware('admin_router', new Router($this->grav, $this->admin));
+        }
+    }
+
+    /**
+     * @param Event $event
+     * @return void
+     */
+    public function onAdminControllerInit(Event $event): void
+    {
+        $eventController = $event['controller'];
+
+        // Blacklist login related views.
+        $loginViews = ['login', 'forgot', 'register', 'reset'];
+        $eventController->blacklist_views = array_merge($eventController->blacklist_views, $loginViews);
+    }
+
+    /**
+     * Force compile during save if admin plugin save
+     *
+     * @param Event $event
+     * @return void
+     */
+    public function onAdminSave(Event $event)
+    {
+        $obj = $event['object'];
+
+        if ($obj instanceof Data
+            && ($blueprint = $obj->blueprints()) && $blueprint && $blueprint->getFilename() === 'admin/blueprints') {
+            [$status, $msg] = $this->grav['admin-whitelabel']->compilePresetScss($obj);
+            if (!$status) {
+                $this->grav['messages']->add($msg, 'error');
+            }
+        }
+    }
+
+    /**
+     * [onPageInitialized:0]
+     *
+     * @return void
+     */
+    public function onPageInitialized()
+    {
+        $template = $this->uri->param('tmpl');
+        if ($template) {
+            /** @var PageInterface $page */
+            $page = $this->grav['page'];
+            $page->template($template);
+        }
+    }
+
+    /**
+     * [onShutdown:1000]
+     *
+     * Handles the shutdown
+     *
+     * @return void
+     */
+    public function onShutdown()
+    {
+        if ($this->active) {
+            //only activate when Admin is active
+            if ($this->admin->shouldLoadAdditionalFilesInBackground()) {
+                $this->admin->loadAdditionalFilesInBackground();
+            }
+        } elseif ($this->popularity && $this->config->get('plugins.admin.popularity.enabled')) {
+            //if popularity is enabled, track non-admin hits
+            $this->popularity->trackHit();
+        }
+    }
+
+    /**
+     * [onAdminDashboard:0]
+     *
+     * @return void
+     */
+    public function onAdminDashboard()
+    {
+        $lang = $this->grav['language'];
+        $this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
+            'name' => $lang->translate('PLUGIN_ADMIN.MAINTENANCE'),
+            'template' => 'dashboard-maintenance',
+        ];
+        $this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
+            'name' => $lang->translate('PLUGIN_ADMIN.VIEWS_STATISTICS'),
+            'template' => 'dashboard-statistics',
+        ];
+        $this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
+            'name' => $lang->translate('PLUGIN_ADMIN.NOTIFICATIONS'),
+            'template' => 'dashboard-notifications',
+        ];
+        $this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
+            'name' => $lang->translate('PLUGIN_ADMIN.NEWS_FEED'),
+            'template' => 'dashboard-feed',
+        ];
+        $this->grav['twig']->plugins_hooked_dashboard_widgets_main[] = [
+            'name' => $lang->translate('PLUGIN_ADMIN.LATEST_PAGE_UPDATES'),
+            'template' => 'dashboard-pages',
+        ];
+    }
+
+
+    /**
+     * [onAdminTools:0]
+     *
+     * Provide the tools for the Tools page, currently only direct install
+     *
+     * @return void
+     */
+    public function onAdminTools(Event $event)
+    {
+        $event['tools'] = array_merge($event['tools'], [
+            'backups'        => [['admin.maintenance', 'admin.super'], 'PLUGIN_ADMIN.BACKUPS'],
+            'scheduler'      => [['admin.super'], 'PLUGIN_ADMIN.SCHEDULER'],
+            'logs'           => [['admin.super'], 'PLUGIN_ADMIN.LOGS'],
+            'reports'        => [['admin.super'], 'PLUGIN_ADMIN.REPORTS'],
+            'direct-install' => [['admin.super'], 'PLUGIN_ADMIN.DIRECT_INSTALL'],
+        ]);
+    }
+
+    /**
+     * Sets longer path to the home page allowing us to have list of pages when we enter to pages section.
+     *
+     * @return void
+     */
+    public function onPagesInitialized()
+    {
+        $config = $this->config;
+
+        // Force SSL with redirect if required
+        if ($config->get('system.force_ssl')) {
+            if (!isset($_SERVER['HTTPS']) || strtolower($_SERVER['HTTPS']) !== 'on') {
+                Admin::DEBUG && Admin::addDebugMessage('Admin SSL forced on, redirect');
+                $url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
+                $this->grav->redirect($url);
+            }
+        }
+
+        $this->session = $this->grav['session'];
+
+        // set session variable if it's passed via the url
+        if (!$this->session->user->authorize('admin.super') || $this->uri->param('mode') === 'normal') {
+            $this->session->expert = false;
+        } elseif ($this->uri->param('mode') === 'expert') {
+            $this->session->expert = true;
+        } else {
+            // set the default if not set before
+            $this->session->expert = $this->session->expert ?? false;
+        }
+
+        // make sure page is not frozen!
+        unset($this->grav['page']);
+
+        // Call the controller if it has been set.
+        $adminParams = $this->admin->request->getAttribute('admin');
+        $page = null;
+        if (isset($adminParams['controller'])) {
+            $controllerParams = $adminParams['controller'];
+            $class = $controllerParams['class'];
+            if (!class_exists($class)) {
+                throw new \RuntimeException(sprintf('Admin controller %s does not exist', $class));
+            }
+
+            /** @var \Grav\Plugin\Admin\Controllers\AdminController $controller */
+            $controller = new $class($this->grav);
+            $method = $controllerParams['method'];
+            $params = $controllerParams['params'] ?? [];
+
+            if (!is_callable([$controller, $method])) {
+                throw new \RuntimeException(sprintf('Admin controller method %s() does not exist', $method));
+            }
+
+            /** @var ResponseInterface $response */
+            $response = $controller->{$method}(...$params);
+            if ($response->getStatusCode() !== 418) {
+                $this->grav->close($response);
+            }
+
+            $page = $controller->getPage();
+            if (!$page) {
+                throw new \RuntimeException('Not Found', 404);
+            }
+
+            $this->grav['page'] = $page;
+            $this->admin->form = $controller->getActiveForm();
+            $legacyController = false;
+        } else {
+            $legacyController = true;
+        }
+
+        /** @var UserInterface $user */
+        $user = $this->grav['user'];
+
+        // Replace page service with admin.
+        if (empty($this->grav['page'])) {
+            $this->grav['page'] = function () use ($user) {
+                $page = new Page();
+
+                // Plugins may not have the correct Cache-Control header set, force no-store for the proxies.
+                $page->expires(0);
+
+                if ($user->authorize('admin.login')) {
+                    $event = new Event(['page' => $page]);
+                    $event = $this->grav->fireEvent('onAdminPage', $event);
+
+                    /** @var PageInterface $page */
+                    $page = $event['page'];
+                    if ($page->slug()) {
+                        Admin::DEBUG && Admin::addDebugMessage('Admin page: from event');
+                        return $page;
+                    }
+                }
+
+                // Look in the pages provided by the Admin plugin itself
+                if (file_exists(__DIR__ . "/pages/admin/{$this->template}.md")) {
+                    Admin::DEBUG && Admin::addDebugMessage("Admin page: {$this->template}");
+
+                    $page->init(new \SplFileInfo(__DIR__ . "/pages/admin/{$this->template}.md"));
+                    $page->slug(Utils::basename($this->template));
+
+                    return $page;
+                }
+
+                /** @var UniformResourceLocator $locator */
+                $locator = $this->grav['locator'];
+
+                // If not provided by Admin, lookup pages added by other plugins
+                /** @var Plugins $plugins */
+                $plugins = $this->grav['plugins'];
+                foreach ($plugins as $plugin) {
+                    if ($this->config->get("plugins.{$plugin->name}.enabled") !== true) {
+                        continue;
+                    }
+
+                    $path = $locator->findResource("plugins://{$plugin->name}/admin/pages/{$this->template}.md");
+                    if ($path) {
+                        Admin::DEBUG && Admin::addDebugMessage("Admin page: plugin {$plugin->name}/{$this->template}");
+
+                        $page->init(new \SplFileInfo($path));
+                        $page->slug(Utils::basename($this->template));
+
+                        return $page;
+                    }
+                }
+
+                return null;
+            };
+        }
+
+        if (empty($this->grav['page'])) {
+            if ($user->authenticated) {
+                Admin::DEBUG && Admin::addDebugMessage('Admin page: fire onPageNotFound event');
+                $event = new Event(['page' => null]);
+                $event->page = null;
+                $event = $this->grav->fireEvent('onPageNotFound', $event);
+                /** @var PageInterface $page */
+                $page = $event->page;
+
+                if (!$page || !$page->routable()) {
+                    Admin::DEBUG && Admin::addDebugMessage('Admin page: 404 Not Found');
+                    $error_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/error.md');
+                    $page = new Page();
+                    $page->init(new \SplFileInfo($error_file));
+                    $page->slug(Utils::basename($this->route));
+                    $page->routable(true);
+                }
+
+                unset($this->grav['page']);
+                $this->grav['page'] = $page;
+            } else {
+                Admin::DEBUG && Admin::addDebugMessage('Admin page: login');
+                // Not Found and not logged in: Display login page.
+                $login_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/login.md');
+                $page = new Page();
+                $page->init(new \SplFileInfo($login_file));
+                $page->slug(Utils::basename($this->route));
+                unset($this->grav['page']);
+                $this->grav['page'] = $page;
+            }
+        }
+
+        if ($legacyController) {
+            // Handle tasks.
+            $this->admin->task = $task = $this->grav['task'] ?? $this->grav['action'];
+            if ($task) {
+                Admin::DEBUG && Admin::addDebugMessage("Admin task: {$task}");
+
+                // Make local copy of POST.
+                $post = $this->grav['uri']->post();
+
+                $this->initializeController($task, $post);
+            } elseif ($this->template === 'logs' && $this->route) {
+                // Display RAW error message.
+                $response = new Response(200, [], $this->admin->logEntry());
+
+                $this->grav->close($response);
+            }
+
+        }
+
+        // Explicitly set a timestamp on assets
+        $this->grav['assets']->setTimestamp(substr(md5(GRAV_VERSION . $this->grav['config']->checksum()), 0, 10));
+    }
+
+    /**
+     * Handles initializing the assets
+     *
+     * @return void
+     */
+    public function onAssetsInitialized()
+    {
+        // Disable Asset pipelining
+        $assets = $this->grav['assets'];
+        $assets->setJsPipeline(false);
+        $assets->setCssPipeline(false);
+
+    }
+
+    /**
+     * Add twig paths to plugin templates.
+     *
+     * @return void
+     */
+    public function onTwigTemplatePaths()
+    {
+        $twig_paths = [];
+        $this->grav->fireEvent('onAdminTwigTemplatePaths', new Event(['paths' => &$twig_paths]));
+
+        $twig_paths[] = __DIR__ . '/themes/' . $this->theme . '/templates';
+
+        $this->grav['twig']->twig_paths = $twig_paths;
+
+    }
+
+    /**
+     * Set all twig variables for generating output.
+     *
+     * @return void
+     */
+    public function onTwigSiteVariables()
+    {
+        /** @var Twig $twig */
+        $twig = $this->grav['twig'];
+        /** @var PageInterface $page */
+        $page = $this->grav['page'];
+
+        $twig->twig_vars['location'] = $this->template;
+        $twig->twig_vars['nav_route'] = trim($this->template . '/' . $this->route, '/') . '/';
+        $twig->twig_vars['base_url_relative_frontend'] = $twig->twig_vars['base_url_relative'] ?: '/';
+        $twig->twig_vars['admin_route'] = trim($this->admin_route, '/');
+        $twig->twig_vars['template_route'] = $this->template;
+        $twig->twig_vars['current_route'] = '/' . $twig->twig_vars['admin_route'] . '/' . $this->template . '/' . $this->route;
+        $twig->twig_vars['base_url_relative'] = $twig->twig_vars['base_url_simple'] . '/' . $twig->twig_vars['admin_route'];
+        $twig->twig_vars['current_url'] = rtrim($twig->twig_vars['base_url_relative'] . '/' . $this->template . '/' . $this->route, '/');
+        $theme_url = '/' . ltrim($this->grav['locator']->findResource('plugin://admin/themes/' . $this->theme,
+            false), '/');
+        $twig->twig_vars['theme_url'] = $theme_url;
+        $twig->twig_vars['base_url'] = $twig->twig_vars['base_url_relative'];
+        $twig->twig_vars['base_path'] = GRAV_ROOT;
+        $twig->twig_vars['admin'] = $this->admin;
+        $twig->twig_vars['user'] = $this->admin->user;
+        $twig->twig_vars['admin_version'] = $this->version;
+        $twig->twig_vars['logviewer'] = new LogViewer();
+        $twig->twig_vars['form_max_filesize'] = Utils::getUploadLimit() / 1024 / 1024;
+
+        // Start white label functionality
+        $twig->twig_vars['whitelabel_presets'] = $this->getPresets();
+
+        $custom_logo = $this->config->get('plugins.admin.whitelabel.logo_custom', false);
+        $custom_login_logo = $this->config->get('plugins.admin.whitelabel.logo_login', false);
+        $custom_footer = $this->config->get('plugins.admin.whitelabel.custom_footer', false);
+
+        if ($custom_logo && is_array($custom_logo)) {
+            $custom_logo = array_keys($custom_logo);
+            $path = array_shift($custom_logo);
+            $twig->twig_vars['custom_admin_logo'] = $path;
+        }
+
+        if ($custom_login_logo && is_array($custom_login_logo)) {
+            $custom_login_logo = array_keys($custom_login_logo);
+            $path = array_shift($custom_login_logo);
+            $twig->twig_vars['custom_login_logo'] = $path;
+        }
+
+        if ($custom_footer) {
+            $footer = Utils::processMarkdown($custom_footer);
+            $twig->twig_vars['custom_admin_footer'] = $footer;
+        }
+
+        $custom_css = $this->config->get('plugins.admin.whitelabel.custom_css', false);
+
+        if ($custom_css) {
+            $this->grav['assets']->addInlineCss($custom_css);
+        }
+        // End white label functionality
+
+        $fa_icons_file = CompiledYamlFile::instance($this->grav['locator']->findResource('plugin://admin/themes/grav/templates/forms/fields/iconpicker/icons' . YAML_EXT));
+        $fa_icons = $fa_icons_file->content();
+        $fa_icons = array_map(function ($icon) {
+            //only pick used values
+            return ['id' => $icon['id'], 'unicode' => $icon['unicode']];
+        }, $fa_icons['icons']);
+
+        $twig->twig_vars['fa_icons'] = $fa_icons;
+
+        // add form if it exists in the page
+        $header = $page->header();
+
+        $forms = [];
+        if (isset($header->forms)) foreach ($header->forms as $key => $form) {
+            $forms[$key] = new Form($page, null, $form);
+        }
+        $twig->twig_vars['forms'] = $forms;
+
+        // preserve form validation
+        if ($this->admin->form) {
+            $twig->twig_vars['form'] = $this->admin->form;
+        } elseif (!isset($twig->twig_vars['form'])) {
+            if (isset($header->form)) {
+                $twig->twig_vars['form'] = new Form($page);
+            } elseif (isset($header->forms)) {
+                $twig->twig_vars['form'] = new Form($page, null, reset($header->forms));
+            }
+        }
+
+        // Gather all nav items
+        $this->grav['twig']->plugins_hooked_nav = [];
+        $this->grav->fireEvent('onAdminMenu');
+        uasort($this->grav['twig']->plugins_hooked_nav, function ($a, $b) {
+            $ac = $a['priority'] ?? 0;
+            $bc = $b['priority'] ?? 0;
+
+            return $bc <=> $ac;
+        });
+
+        // Gather Plugin-hooked dashboard items
+        $this->grav->fireEvent('onAdminDashboard');
+
+        switch ($this->template) {
+            case 'dashboard':
+                $twig->twig_vars['popularity'] = $this->popularity;
+                break;
+        }
+
+        $flashData = $this->grav['session']->getFlashCookieObject(Admin::TMP_COOKIE_NAME);
+
+        if (isset($flashData->message)) {
+            $this->grav['messages']->add($flashData->message, $flashData->status);
+        }
+    }
+
+    /**
+     * Add images to twig template paths to allow inclusion of SVG files
+     *
+     * @return void
+     */
+    public function onTwigLoader()
+    {
+        /** @var Twig $twig */
+        $twig = $this->grav['twig'];
+
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+
+        $theme_paths = $locator->findResources('plugins://admin/themes/' . $this->theme . '/images');
+        foreach($theme_paths as $images_path) {
+            $twig->addPath($images_path, 'admin-images');
+        }
+    }
+
+    /**
+     * Add the Admin Twig Extensions
+     *
+     * @return void
+     */
+    public function onTwigExtensions()
+    {
+        /** @var Twig $twig */
+        $twig = $this->grav['twig'];
+        $twig->twig->addExtension(new AdminTwigExtension);
+    }
+
+    /**
+     * @param Event $event
+     * @return void
+     */
+    public function onAdminAfterSave(Event $event)
+    {
+        // Special case to redirect after changing the admin route to avoid 'breaking'
+        $obj = $event['object'];
+
+        if (null !== $obj && method_exists($obj, 'blueprints')) {
+            $blueprint = $obj->blueprints()->getFilename();
+
+            if ($blueprint === 'admin/blueprints' && isset($obj->route) && $this->admin_route !== $obj->route) {
+                $redirect = preg_replace('/^' . str_replace('/','\/',$this->admin_route) . '/',$obj->route,$this->uri->path());
+                $this->grav->redirect($redirect);
+            }
+        }
+    }
+
+    /**
+     * Convert some types where we want to process out of the standard config path
+     *
+     * @param Event $e
+     * @return void
+     */
+    public function onAdminData(Event $e)
+    {
+        $type = $e['type'] ?? null;
+        switch ($type) {
+            case 'config':
+                $e['type'] = $this->admin->authorize(['admin.configuration.system', 'admin.super']) ? 'config/system' : 'config/site';
+                break;
+            case 'tools/scheduler':
+                $e['type'] = 'config/scheduler';
+                break;
+            case 'tools':
+            case 'tools/backups':
+                $e['type'] = 'config/backups';
+                break;
+        }
+    }
+
+    /**
+     * @return void
+     */
+    public function onOutputGenerated()
+    {
+        // Clear flash objects for previously uploaded files whenever the user switches page or reloads
+        // ignoring any JSON / extension call
+        if ($this->admin->task !== 'save' && empty($this->uri->extension())) {
+            // Discard any previously uploaded files session and remove all uploaded files.
+            if ($flash = $this->session->getFlashObject('files-upload')) {
+                $flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
+                foreach ($flash as $key => $value) {
+                    if ($key !== 'tmp_name') {
+                        continue;
+                    }
+                    @unlink($value);
+                }
+            }
+        }
+    }
+
+    /**
+     * Initial stab at registering permissions (WIP)
+     *
+     * @param PermissionsRegisterEvent $event
+     * @return void
+     */
+    public function onRegisterPermissions(PermissionsRegisterEvent $event): void
+    {
+        $actions = PermissionsReader::fromYaml("plugin://{$this->name}/permissions.yaml");
+
+        $permissions = $event->permissions;
+        $permissions->addActions($actions);
+    }
+
+    /**
+     * @return void
+     */
+    public function onAdminMenu()
+    {
+        /** @var Twig $twig */
+        $twig = $this->grav['twig'];
+
+        // Dashboard
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.DASHBOARD'] = [
+            'route' => 'dashboard',
+            'icon' => 'fa-th',
+            'authorize' => ['admin.login', 'admin.super'],
+            'priority' => 10
+        ];
+
+        // Configuration
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.CONFIGURATION'] = [
+            'route' => 'config',
+            'icon' => 'fa-wrench',
+            'authorize' => [
+                'admin.configuration.system',
+                'admin.configuration.site',
+                'admin.configuration.media',
+                'admin.configuration.security',
+                'admin.configuration.info',
+                'admin.super'],
+            'priority' => 9
+        ];
+
+        // Pages
+        $count = new Container(['count' => function () { return $this->admin->pagesCount(); }]);
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.PAGES'] = [
+            'route' => 'pages',
+            'icon' => 'fa-file-text-o',
+            'authorize' => ['admin.pages', 'admin.super'],
+            'badge' => $count,
+            'priority' => 5
+        ];
+
+        // Plugins
+        $count = new Container(['updates' => 0, 'count' => function () { return count($this->admin->plugins()); }]);
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.PLUGINS'] = [
+            'route' => 'plugins',
+            'icon' => 'fa-plug',
+            'authorize' => ['admin.plugins', 'admin.super'],
+            'badge' => $count,
+            'priority' => -8
+        ];
+
+        // Themes
+        $count = new Container(['updates' => 0, 'count' => function () { return count($this->admin->themes()); }]);
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.THEMES'] = [
+            'route' => 'themes',
+            'icon' => 'fa-tint',
+            'authorize' => ['admin.themes', 'admin.super'],
+            'badge' => $count,
+            'priority' => -9
+        ];
+
+        // Tools
+        $twig->plugins_hooked_nav['PLUGIN_ADMIN.TOOLS'] = [
+            'route' => 'tools',
+            'icon' => 'fa-briefcase',
+            'authorize' => $this->admin::toolsPermissions(),
+            'priority' => -10
+        ];
+    }
+
+    /**
+     * Check if the current route is under the admin path
+     *
+     * @return bool
+     */
+    public function isAdminPath()
+    {
+        $route = $this->uri->route();
+
+        return $route === $this->base || 0 === strpos($route, $this->base . '/');
+    }
+
+    /**
+     * Helper function to replace Pages::Types() and to provide an event to manipulate the data
+     *
+     * Dispatches 'onAdminPageTypes' event with 'types' data member which is a reference to the data
+     *
+     * @param bool $keysOnly
+     * @return array
+     */
+    public static function pagesTypes(bool $keysOnly = false)
+    {
+        $types = Pages::types();
+
+        // First filter by configuration
+        $hideTypes = (array)Grav::instance()['config']->get('plugins.admin.hide_page_types');
+        foreach ($hideTypes as $hide) {
+            if (isset($types[$hide])) {
+                unset($types[$hide]);
+            } else {
+                foreach ($types as $key => $type) {
+                    $match = preg_match('#' . $hide . '#i', $key);
+                    if ($match) {
+                        unset($types[$key]);
+                    }
+                }
+            }
+        }
+
+        // Allow manipulating of the data by event
+        $e = new Event(['types' => &$types]);
+        Grav::instance()->fireEvent('onAdminPageTypes', $e);
+
+        return $keysOnly ? array_keys($types) : $types;
+    }
+
+    /**
+     * Helper function to replace Pages::modularTypes() and to provide an event to manipulate the data
+     *
+     * Dispatches 'onAdminModularPageTypes' event with 'types' data member which is a reference to the data
+     *
+     * @param bool $keysOnly
+     * @return array
+     */
+    public static function pagesModularTypes(bool $keysOnly = false)
+    {
+        $types = Pages::modularTypes();
+
+        // First filter by configuration
+        $hideTypes = (array)Grav::instance()['config']->get('plugins.admin.hide_modular_page_types');
+        foreach ($hideTypes as $hide) {
+            if (isset($types[$hide])) {
+                unset($types[$hide]);
+            } else {
+                foreach ($types as $key => $type) {
+                    $match = preg_match('#' . $hide . '#i', $key);
+                    if ($match) {
+                        unset($types[$key]);
+                    }
+                }
+            }
+        }
+
+        // Allow manipulating of the data by event
+        $e = new Event(['types' => &$types]);
+        Grav::instance()->fireEvent('onAdminModularPageTypes', $e);
+
+        return $keysOnly ? array_keys($types) : $types;
+    }
+
+    /**
+     * Validate a value. Currently validates
+     *
+     * - 'user' for username format and username availability.
+     * - 'password1' for password format
+     * - 'password2' for equality to password1
+     *
+     * @param string $type  The field type
+     * @param string $value The field value
+     * @param string $extra Any extra value required
+     *
+     * @return bool
+     */
+    protected function validate($type, $value, $extra = '')
+    {
+        /** @var Login $login */
+        $login = $this->grav['login'];
+
+        return $login->validateField($type, $value, $extra);
+    }
+
+    /**
+     * @param string $task
+     * @param array|null $post
+     * @return void
+     */
+    protected function initializeController($task, $post = null): void
+    {
+        Admin::DEBUG && Admin::addDebugMessage('Admin controller: execute');
+
+        $controller = new AdminController();
+        $controller->initialize($this->grav, $this->template, $task, $this->route, $post);
+        $controller->execute();
+        $controller->redirect();
+    }
+
+    /**
+     * @return void
+     */
+    protected function setupAdmin()
+    {
+        // Set cache based on admin_cache option.
+        /** @var Cache $cache */
+        $cache = $this->grav['cache'];
+        $cache->setEnabled($this->config->get('plugins.admin.cache_enabled'));
+
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+        // Disable frontend pages in admin.
+        $pages->disablePages();
+        // Force file hash checks to fix caching on moved and deleted pages.
+        $pages->setCheckMethod('hash');
+
+        /** @var Session $session */
+        $session = $this->grav['session'];
+        // Make sure that the session has been initialized.
+        try {
+            $session->init();
+        } catch (SessionException $e) {
+            $session->init();
+
+            $message = 'Session corruption detected, restarting session...';
+
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addMessage($message);
+
+            $this->grav['messages']->add($message, 'error');
+        }
+
+        $this->active = true;
+    }
+
+    /**
+     * Initialize the admin.
+     *
+     * @return void
+     * @throws \RuntimeException
+     */
+    protected function initializeAdmin()
+    {
+        // Check for required plugins
+        if (!$this->grav['config']->get('plugins.login.enabled') || !$this->grav['config']->get('plugins.form.enabled') || !$this->grav['config']->get('plugins.email.enabled')) {
+            throw new \RuntimeException('One of the required plugins is missing or not enabled');
+        }
+
+        /** @var Cache $cache */
+        $cache = $this->grav['cache'];
+
+        // Have a unique Admin-only Cache key
+        $cache_key = $cache->getKey();
+        $cache->setKey($cache_key . '$');
+
+        /** @var Session $session */
+        $session = $this->grav['session'];
+
+        /** @var Language $language */
+        $language = $this->grav['language'];
+
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+
+        // Turn on Twig autoescaping
+        if ($this->uri->param('task') !== 'processmarkdown') {
+            $this->grav['twig']->setAutoescape(true);
+        }
+
+        // Initialize Admin Language if needed
+        if ($language->enabled() && empty($session->admin_lang)) {
+            $session->admin_lang = $language->getLanguage();
+        }
+
+        // Decide admin template and route.
+        $path = trim(substr($this->uri->route(), strlen($this->base)), '/');
+
+        if (empty($this->template)) {
+            $this->template = 'dashboard';
+        }
+
+        // Can't access path directly...
+        if ($path && $path !== 'register') {
+            $array = explode('/', $path, 2);
+            $this->template = array_shift($array);
+            $this->route = array_shift($array);
+        }
+
+        // Initialize admin class (also registers it to Grav services).
+        $this->admin = new Admin($this->grav, $this->admin_route, $this->template, $this->route);
+
+        // Get theme for admin
+        $this->theme = $this->config->get('plugins.admin.theme', 'grav');
+
+        // Replace themes service with admin.
+        $this->grav['themes'] = function () {
+            return new Themes($this->grav);
+        };
+
+        // Initialize white label functionality
+        $this->grav['admin-whitelabel'] = new WhiteLabel();
+
+        // Compile a missing preset.css file
+        $preset_css = 'asset://admin-preset.css';
+        $preset_path = $this->grav['locator']->findResource($preset_css);
+        if (!$preset_path) {
+            $this->grav['admin-whitelabel']->compilePresetScss($this->config->get('plugins.admin.whitelabel'));
+        }
+
+        // These events are needed for login.
+        $this->enable([
+            'onTwigExtensions'           => ['onTwigExtensions', 1000],
+            'onPagesInitialized'         => ['onPagesInitialized', 1000],
+            'onTwigLoader'               => ['onTwigLoader', 1000],
+            'onTwigTemplatePaths'        => ['onTwigTemplatePaths', 1000],
+            'onTwigSiteVariables'        => ['onTwigSiteVariables', 1000],
+            'onAssetsInitialized'        => ['onAssetsInitialized', 1000],
+        ]);
+
+        // Do not do more if user isn't logged in.
+        if (!$this->admin->user->authorize('admin.login')) {
+            return;
+        }
+
+        // These events are not needed during login.
+        $this->enable([
+            'onAdminControllerInit'     => ['onAdminControllerInit', 1001],
+            'onAdminDashboard'          => ['onAdminDashboard', 0],
+            'onAdminMenu'               => ['onAdminMenu', 1000],
+            'onAdminTools'              => ['onAdminTools', 0],
+            'onAdminSave'               => ['onAdminSave', 0],
+            'onAdminAfterSave'          => ['onAdminAfterSave', 0],
+            'onAdminData'               => ['onAdminData', 0],
+            'onOutputGenerated'         => ['onOutputGenerated', 0],
+        ]);
+
+        // Double check we have system.yaml, site.yaml etc
+        $config_path = $locator->findResource('user://config');
+        foreach ($this->admin::configurations() as $config_file) {
+            if ($config_file === 'info') {
+                continue;
+            }
+            $config_file = "{$config_path}/{$config_file}.yaml";
+            if (!file_exists($config_file)) {
+                touch($config_file);
+            }
+        }
+
+        $assets = $this->grav['assets'];
+        $translations = 'this.GravAdmin = this.GravAdmin || {}; if (!this.GravAdmin.translations) this.GravAdmin.translations = {}; ' . PHP_EOL . 'this.GravAdmin.translations.PLUGIN_ADMIN = {';
+
+        // Enable language translations
+        $translations_actual_state = $this->config->get('system.languages.translations');
+        $this->config->set('system.languages.translations', true);
+
+        $strings = [
+            'EVERYTHING_UP_TO_DATE',
+            'UPDATES_ARE_AVAILABLE',
+            'IS_AVAILABLE_FOR_UPDATE',
+            'AND',
+            'IS_NOW_AVAILABLE',
+            'CURRENT',
+            'UPDATE_GRAV_NOW',
+            'TASK_COMPLETED',
+            'UPDATE',
+            'UPDATING_PLEASE_WAIT',
+            'GRAV_SYMBOLICALLY_LINKED',
+            'OF_YOUR',
+            'OF_THIS',
+            'HAVE_AN_UPDATE_AVAILABLE',
+            'UPDATE_AVAILABLE',
+            'UPDATES_AVAILABLE',
+            'FULLY_UPDATED',
+            'DAYS',
+            'PAGE_MODES',
+            'PAGE_TYPES',
+            'ACCESS_LEVELS',
+            'NOTHING_TO_SAVE',
+            'FILE_UNSUPPORTED',
+            'FILE_ERROR_ADD',
+            'FILE_ERROR_UPLOAD',
+            'DROP_FILES_HERE_TO_UPLOAD',
+            'DELETE',
+            'UNSET',
+            'INSERT',
+            'METADATA',
+            'VIEW',
+            'UNDO',
+            'REDO',
+            'HEADERS',
+            'BOLD',
+            'ITALIC',
+            'STRIKETHROUGH',
+            'SUMMARY_DELIMITER',
+            'LINK',
+            'IMAGE',
+            'BLOCKQUOTE',
+            'UNORDERED_LIST',
+            'ORDERED_LIST',
+            'EDITOR',
+            'PREVIEW',
+            'FULLSCREEN',
+            'MODULE',
+            'NON_MODULE',
+            'VISIBLE',
+            'NON_VISIBLE',
+            'ROUTABLE',
+            'NON_ROUTABLE',
+            'PUBLISHED',
+            'NON_PUBLISHED',
+            'PLUGINS',
+            'THEMES',
+            'ALL',
+            'FROM',
+            'TO',
+            'DROPZONE_CANCEL_UPLOAD',
+            'DROPZONE_CANCEL_UPLOAD_CONFIRMATION',
+            'DROPZONE_DEFAULT_MESSAGE',
+            'DROPZONE_FALLBACK_MESSAGE',
+            'DROPZONE_FALLBACK_TEXT',
+            'DROPZONE_FILE_TOO_BIG',
+            'DROPZONE_INVALID_FILE_TYPE',
+            'DROPZONE_MAX_FILES_EXCEEDED',
+            'DROPZONE_REMOVE_FILE',
+            'DROPZONE_RESPONSE_ERROR'
+        ];
+
+        foreach ($strings as $string) {
+            $separator = (end($strings) === $string) ? '' : ',';
+            $translations .= '"' . $string . '": "' . htmlspecialchars($this->admin::translate('PLUGIN_ADMIN.' . $string)) . '"' . $separator;
+        }
+
+        $translations .= '};';
+
+        $translations .= 'this.GravAdmin.translations.PLUGIN_FORM = {';
+        $strings = ['RESOLUTION_MIN', 'RESOLUTION_MAX'];
+        foreach ($strings as $string) {
+            $separator = (end($strings) === $string) ? '' : ',';
+            $translations .= '"' . $string . '": "' . $this->admin::translate('PLUGIN_FORM.' . $string) . '"' . $separator;
+        }
+        $translations .= '};';
+
+        $translations .= 'this.GravAdmin.translations.GRAV_CORE = {';
+        $strings = [
+            'NICETIME.SECOND',
+            'NICETIME.MINUTE',
+            'NICETIME.HOUR',
+            'NICETIME.DAY',
+            'NICETIME.WEEK',
+            'NICETIME.MONTH',
+            'NICETIME.YEAR',
+            'CRON.EVERY',
+            'CRON.EVERY_HOUR',
+            'CRON.EVERY_MINUTE',
+            'CRON.EVERY_DAY_OF_WEEK',
+            'CRON.EVERY_DAY_OF_MONTH',
+            'CRON.EVERY_MONTH',
+            'CRON.TEXT_PERIOD',
+            'CRON.TEXT_MINS',
+            'CRON.TEXT_TIME',
+            'CRON.TEXT_DOW',
+            'CRON.TEXT_MONTH',
+            'CRON.TEXT_DOM',
+            'CRON.ERROR1',
+            'CRON.ERROR2',
+            'CRON.ERROR3',
+            'CRON.ERROR4',
+            'MONTHS_OF_THE_YEAR',
+            'DAYS_OF_THE_WEEK'
+        ];
+        foreach ($strings as $string) {
+            $separator = (end($strings) === $string) ? '' : ',';
+            $translations .= '"' . $string . '": ' . json_encode($this->admin::translate('GRAV.'.$string)) . $separator;
+        }
+        $translations .= '};';
+
+        // set the actual translations state back
+        $this->config->set('system.languages.translations', $translations_actual_state);
+
+        $assets->addInlineJs($translations);
+
+        // Fire even to register permissions from other plugins
+        $this->grav->fireEvent('onAdminRegisterPermissions', new Event(['admin' => $this->admin]));
+    }
+
+    /**
+     * @return array
+     */
+    public static function themeOptions()
+    {
+        static $options;
+
+        if (null === $options) {
+            $options = [];
+            $theme_files = glob(__dir__ . '/themes/grav/css/codemirror/themes/*.css');
+            foreach ($theme_files as $theme_file) {
+                $theme = Utils::basename(Utils::basename($theme_file, '.css'));
+                $options[$theme] = Inflector::titleize($theme);
+            }
+        }
+
+        return $options;
+    }
+
+    /**
+     * @return array
+     */
+    public function getPresets()
+    {
+        $filename = $this->grav['locator']->findResource('plugin://admin/presets.yaml', false);
+
+        $file     = CompiledYamlFile::instance($filename);
+        $presets     = (array)$file->content();
+
+        $custom_presets = $this->config->get('plugins.admin.whitelabel.custom_presets');
+
+        if (isset($custom_presets)) {
+            $custom_presets = Yaml::parse($custom_presets);
+
+            if (is_array($custom_presets)) {
+                if (isset($custom_presets['name'], $custom_presets['colors'], $custom_presets['accents'])) {
+                    $preset = [Inflector::hyphenize($custom_presets['name']) => $custom_presets];
+                    $presets = $preset + $presets;
+                } else {
+                    foreach ($custom_presets as $value) {
+                        if (isset($value['name'], $value['colors'], $value['accents'])) {
+                            $preset = [Inflector::hyphenize($value['name']) => $value];
+                            $presets = $preset + $presets;
+                        }
+                    }
+                }
+            }
+        }
+
+        return $presets;
+    }
+}

+ 84 - 0
plugins/admin/admin.yaml

@@ -0,0 +1,84 @@
+enabled: true
+route: '/admin'
+cache_enabled: true
+theme: grav
+logo_text: ''
+body_classes: ''
+content_padding: true
+twofa_enabled: true
+sidebar:
+  activate: tab
+  hover_delay: 100
+  size: auto
+dashboard:
+  days_of_stats: 7
+widgets_display:
+  dashboard-maintenance: true
+  dashboard-statistics: true
+  dashboard-notifications: true
+  dashboard-feed: true
+  dashboard-pages: true
+pages:
+  show_parents: both
+  show_modular: true
+session:
+  timeout: 1800
+edit_mode: normal
+frontend_preview_target: inline
+show_github_msg: true
+admin_icons: line-awesome
+enable_auto_updates_check: true
+notifications:
+  feed: true
+  dashboard: true
+  plugins: true
+  themes: true
+popularity:
+  enabled: true
+  ignore: ['/test*','/modular']
+  history:
+    daily: 30
+    monthly: 12
+    visitors: 20
+whitelabel:
+  quicktray_recompile: false
+  codemirror_theme: paper
+  codemirror_fontsize: md
+  codemirror_md_font: sans
+  logo_custom:
+  logo_login:
+  color_scheme:
+    accents:
+      primary-accent: button
+      secondary-accent: notice
+      tertiary-accent: critical
+    colors:
+      logo-bg: '#323640'
+      logo-link: '#FFFFFF'
+      nav-bg: '#3D424E'
+      nav-text: '#B7B9BD'
+      nav-link: '#ffffff'
+      nav-selected-bg: '#323640'
+      nav-selected-link: '#ffffff'
+      nav-hover-bg: '#434753'
+      nav-hover-link: '#ffffff'
+      toolbar-bg: '#ffffff'
+      toolbar-text: '#3D424E'
+      page-bg: '#F6F6F6'
+      page-text: '#6f7b8a'
+      page-link: '#0090D9'
+      content-bg: '#ffffff'
+      content-text: '#6f7b8a'
+      content-link: '#0090D9'
+      content-link2: '#da4b46'
+      content-header: '#414147'
+      content-tabs-bg: '#e6e6e6'
+      content-tabs-text: '#808080'
+      button-bg: '#0090D9'
+      button-text: '#ffffff'
+      notice-bg: '#06A599'
+      notice-text: '#ffffff'
+      update-bg: '#77559D'
+      update-text: '#ffffff'
+      critical-bg: '#F45857'
+      critical-text: '#ffffff'

BIN
plugins/admin/assets/admin-dashboard.png


+ 781 - 0
plugins/admin/blueprints.yaml

@@ -0,0 +1,781 @@
+name: Admin Panel
+slug: admin
+type: plugin
+version: 1.10.42
+description: Adds an advanced administration panel to manage your site
+icon: empire
+author:
+  name: Team Grav
+  email: devs@getgrav.org
+  url: https://getgrav.org
+homepage: https://github.com/getgrav/grav-plugin-admin
+keywords: admin, plugin, manager, panel
+bugs: https://github.com/getgrav/grav-plugin-admin/issues
+docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
+license: MIT
+
+dependencies:
+    - { name: grav, version: '>=1.7.42' }
+    - { name: form, version: '>=6.0.1' }
+    - { name: login, version: '>=3.7.0' }
+    - { name: email, version: '>=3.1.6' }
+    - { name: flex-objects, version: '>=1.2.0' }
+
+form:
+  validation: loose
+  fields:
+    admin_tabs:
+      type: tabs
+      fields:
+        config_tab:
+          type: tab
+          title: PLUGIN_ADMIN.CONFIGURATION
+
+          fields:
+
+            Basics:
+              type: section
+              title: PLUGIN_ADMIN.BASICS
+              underline: false
+
+            enabled:
+              type: hidden
+              label: PLUGIN_ADMIN.PLUGIN_STATUS
+              highlight: 1
+              default: 0
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+
+            cache_enabled:
+              type: toggle
+              label: PLUGIN_ADMIN.ADMIN_CACHING
+              help: PLUGIN_ADMIN.ADMIN_CACHING_HELP
+              highlight: 1
+              options:
+                  1: PLUGIN_ADMIN.YES
+                  0: PLUGIN_ADMIN.NO
+              validate:
+                  type: bool
+
+            twofa_enabled:
+              type: toggle
+              label: PLUGIN_LOGIN.2FA_TITLE
+              help: PLUGIN_LOGIN.2FA_ENABLED_HELP
+              default: 1
+              highlight: 1
+              options:
+                  1: PLUGIN_ADMIN.YES
+                  0: PLUGIN_ADMIN.NO
+              validate:
+                  type: bool
+
+            route:
+              type: text
+              label: PLUGIN_ADMIN.ADMIN_PATH
+              size: medium
+              placeholder: ADMIN_PATH_PLACEHOLDER
+              help: ADMIN_PATH_HELP
+
+            logo_text:
+              type: text
+              label: PLUGIN_ADMIN.LOGO_TEXT
+              size: medium
+              placeholder: "Grav"
+              help: PLUGIN_ADMIN.LOGO_TEXT_HELP
+
+            content_padding:
+              type: toggle
+              label: PLUGIN_ADMIN.CONTENT_PADDING
+              help: PLUGIN_ADMIN.CONTENT_PADDING_HELP
+              highlight: 1
+              options:
+                  1: PLUGIN_ADMIN.YES
+                  0: PLUGIN_ADMIN.NO
+              validate:
+                  type: bool
+
+            body_classes:
+              type: text
+              label: PLUGIN_ADMIN.BODY_CLASSES
+              size: medium
+              help: PLUGIN_ADMIN.BODY_CLASSES_HELP
+
+            sidebar.activate:
+              type: select
+              label: PLUGIN_ADMIN.SIDEBAR_ACTIVATION
+              help: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HELP
+              size: small
+              default: tab
+              options:
+                tab: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_TAB
+                hover: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HOVER
+
+            sidebar.hover_delay:
+              type: text
+              size: x-small
+              append: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY_APPEND
+              label: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY
+              default: 500
+              validate:
+                type: number
+                min: 1
+
+
+            sidebar.size:
+              type: select
+              label: PLUGIN_ADMIN.SIDEBAR_SIZE
+              help: PLUGIN_ADMIN.SIDEBAR_SIZE_HELP
+              size: medium
+              default: auto
+              options:
+                auto: PLUGIN_ADMIN.SIDEBAR_SIZE_AUTO
+                small: PLUGIN_ADMIN.SIDEBAR_SIZE_SMALL
+
+            theme:
+              type: hidden
+              label: PLUGIN_ADMIN.THEME
+              default: grav
+
+            edit_mode:
+              type: select
+              label: PLUGIN_ADMIN.EDIT_MODE
+              size: small
+              default: normal
+              options:
+                normal: PLUGIN_ADMIN.NORMAL
+                expert: PLUGIN_ADMIN.EXPERT
+              help: PLUGIN_ADMIN.EDIT_MODE_HELP
+
+            frontend_preview_target:
+              type: select
+              label: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET
+              size: medium
+              default: inline
+              options:
+                inline: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_INLINE
+                _blank: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_NEW
+                _self: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_CURRENT
+
+            pages.show_parents:
+              type: select
+              size: medium
+              label: PLUGIN_ADMIN.PARENT_DROPDOWN
+              highlight: 1
+              options:
+                both: PLUGIN_ADMIN.PARENT_DROPDOWN_BOTH
+                folder: PLUGIN_ADMIN.PARENT_DROPDOWN_FOLDER
+                fullpath: PLUGIN_ADMIN.PARENT_DROPDOWN_FULLPATH
+
+            pages.parents_levels:
+              type: text
+              label: PLUGIN_ADMIN.PARENTS_LEVELS
+              size: small
+              help: PLUGIN_ADMIN.PARENTS_LEVELS_HELP
+
+            pages.show_modular:
+              type: toggle
+              label: PLUGIN_ADMIN.MODULAR_PARENTS
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.MODULAR_PARENTS_HELP
+
+            show_beta_msg:
+              type: hidden
+
+            show_github_msg:
+              type: toggle
+              label: PLUGIN_ADMIN.SHOW_GITHUB_LINK
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.SHOW_GITHUB_LINK_HELP
+
+            enable_auto_updates_check:
+              type: toggle
+              label: PLUGIN_ADMIN.AUTO_UPDATES
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.AUTO_UPDATES_HELP
+
+            session.timeout:
+              type: text
+              size: small
+              label: PLUGIN_ADMIN.TIMEOUT
+              append: GRAV.NICETIME.SECOND_PLURAL
+              help: PLUGIN_ADMIN.TIMEOUT_HELP
+              validate:
+                  type: number
+                  min: 1
+
+            hide_page_types:
+              type: select
+              size: large
+              label: PLUGIN_ADMIN.HIDE_PAGE_TYPES
+              classes: fancy
+              multiple: true
+              array: true
+              selectize:
+                create: true
+              data-options@: ['\Grav\Plugin\AdminPlugin::pagesTypes', true]
+
+            hide_modular_page_types:
+              type: select
+              size: large
+              label: PLUGIN_ADMIN.HIDE_MODULAR_PAGE_TYPES
+              classes: fancy
+              multiple: true
+              array: true
+              selectize:
+                create: true
+              data-options@: ['\Grav\Plugin\AdminPlugin::pagesModularTypes', true]
+
+            Dashboard:
+              type: section
+              title: PLUGIN_ADMIN.DASHBOARD
+              underline: true
+
+            widgets_display:
+              type: widgets
+              label: PLUGIN_ADMIN.WIDGETS_DISPLAY
+              validate:
+                type: array
+
+            Notifications:
+              type: section
+              title: PLUGIN_ADMIN.NOTIFICATIONS
+              underline: true
+
+            notifications.feed:
+              type: toggle
+              label: PLUGIN_ADMIN.FEED_NOTIFICATIONS
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.FEED_NOTIFICATIONS_HELP
+
+            notifications.dashboard:
+              type: toggle
+              label: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS_HELP
+
+            notifications.plugins:
+              type: toggle
+              label: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS_HELP
+
+            notifications.themes:
+              type: toggle
+              label: PLUGIN_ADMIN.THEMES_NOTIFICATIONS
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.THEMES_NOTIFICATIONS_HELP
+
+        customization_tab:
+          type: tab
+          title: PLUGIN_ADMIN.CUSTOMIZATION
+
+          fields:
+            whitelabel.logos:
+              type: section
+              underline: true
+              title: PLUGIN_ADMIN.LOGOS
+
+            whitelabel.logo_login:
+              type: file
+              label: PLUGIN_ADMIN.LOGIN_SCREEN_CUSTOM_LOGO_LABEL
+              destination: 'user://assets'
+              accept:
+                - image/*
+
+            whitelabel.logo_custom:
+              type: file
+              label: PLUGIN_ADMIN.TOP_LEFT_CUSTOM_LOGO_LABEL
+              destination: 'user://assets'
+              accept:
+                - image/*
+
+            codemirror_section:
+              type: section
+              underline: true
+              title: PLUGIN_ADMIN.CODEMIRROR
+
+            whitelabel.codemirror_theme:
+              type: select
+              label: PLUGIN_ADMIN.CODEMIRROR_THEME
+              default: paper
+              markdown: true
+              data-options@: '\Grav\Plugin\AdminPlugin::themeOptions'
+              description: PLUGIN_ADMIN.CODEMIRROR_THEME_DESC
+
+            whitelabel.codemirror_fontsize:
+              type: select
+              label: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE
+              default: md
+              options:
+                sm: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_SM
+                md: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_MD
+                lg: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_LG
+
+            whitelabel.codemirror_md_font:
+              type: select
+              label: PLUGIN_ADMIN.CODEMIRROR_MD_FONT
+              default: sans
+              options:
+                sans: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_SANS
+                mono: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_MONO
+
+            customization_section:
+              type: section
+              underline: true
+              title: PLUGIN_ADMIN.CUSTOMIZATION
+
+            whitelabel.quicktray_recompile:
+              type: toggle
+              label: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE
+              help: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE_HELP
+              highlight: 0
+              default: 0
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+
+
+
+            whitelabel.color_scheme.name:
+              type: text
+              label: PLUGIN_ADMIN.COLOR_SCHEME_NAME
+              help: PLUGIN_ADMIN.COLOR_SCHEME_NAME_HELP
+              placeholder: PLUGIN_ADMIN.COLOR_SCHEME_NAME_PLACEHOLDER
+
+            themes-preview:
+              type: themepreview
+              ignore: true;
+              label: PLUGIN_ADMIN.PRESETS
+              style: vertical
+
+            colorschemes:
+              type: colorscheme
+              label: PLUGIN_ADMIN.COLOR_SCHEME_LABEL
+              style: vertical
+              help: PLUGIN_ADMIN.COLOR_SCHEME_HELP
+
+              fields:
+                whitelabel.color_scheme.colors.logo-bg:
+                  type: colorscheme.color
+                  default: '#1e333e'
+                  help: PLUGIN_ADMIN.LOGO_BG_HELP
+
+                whitelabel.color_scheme.colors.logo-link:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.LOGO_LINK_HELP
+
+                whitelabel.color_scheme.colors.nav-bg:
+                  type: colorscheme.color
+                  default: '#253a47'
+                  help: PLUGIN_ADMIN.NAV_BG_HELP
+
+                whitelabel.color_scheme.colors.nav-text:
+                  type: colorscheme.color
+                  default: '#afc7d5'
+                  help: PLUGIN_ADMIN.NAV_TEXT_HELP
+
+                whitelabel.color_scheme.colors.nav-link:
+                  type: colorscheme.color
+                  default: '#d1dee7'
+                  help: PLUGIN_ADMIN.NAV_LINK_HELP
+
+                whitelabel.color_scheme.colors.nav-selected-bg:
+                  type: colorscheme.color
+                  default: '#2d4d5b'
+                  help: PLUGIN_ADMIN.NAV_SELECTED_BG_HELP
+
+                whitelabel.color_scheme.colors.nav-selected-link:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.NAV_SELECTED_LINK_HELP
+
+                whitelabel.color_scheme.colors.nav-hover-bg:
+                  type: colorscheme.color
+                  default: '#1e333e'
+                  help: PLUGIN_ADMIN.NAV_HOVER_BG_HELP
+
+                whitelabel.color_scheme.colors.nav-hover-link:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.NAV_HOVER_LINK_HELP
+
+                whitelabel.color_scheme.colors.toolbar-bg:
+                  type: colorscheme.color
+                  default: '#349886'
+                  help: PLUGIN_ADMIN.TOOLBAR_BG_HELP
+
+                whitelabel.color_scheme.colors.toolbar-text:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.TOOLBAR_TEXT_HELP
+
+                whitelabel.color_scheme.colors.page-bg:
+                  type: colorscheme.color
+                  default: '#314d5b'
+                  help: PLUGIN_ADMIN.PAGE_BG_HELP
+
+                whitelabel.color_scheme.colors.page-text:
+                  type: colorscheme.color
+                  default: '#81a5b5'
+                  help: PLUGIN_ADMIN.PAGE_TEXT_HELP
+
+                whitelabel.color_scheme.colors.page-link:
+                  type: colorscheme.color
+                  default: '#aad9ed'
+                  help: PLUGIN_ADMIN.PAGE_LINK_HELP
+
+                whitelabel.color_scheme.colors.content-bg:
+                  type: colorscheme.color
+                  default: '#eeeeee'
+                  help: PLUGIN_ADMIN.CONTENT_BG_HELP
+
+                whitelabel.color_scheme.colors.content-text:
+                  type: colorscheme.color
+                  default: '#737c81'
+                  help: PLUGIN_ADMIN.CONTENT_TEXT_HELP
+
+                whitelabel.color_scheme.colors.content-link:
+                  type: colorscheme.color
+                  default: '#0082ba'
+                  help: PLUGIN_ADMIN.CONTENT_LINK_HELP
+
+                whitelabel.color_scheme.colors.content-link2:
+                  type: colorscheme.color
+                  default: '#da4b46'
+                  help: PLUGIN_ADMIN.CONTENT_LINK2_HELP
+
+                whitelabel.color_scheme.colors.content-header:
+                  type: colorscheme.color
+                  default: '#314d5b'
+                  help: PLUGIN_ADMIN.CONTENT_HEADER_HELP
+
+                whitelabel.color_scheme.colors.content-tabs-bg:
+                  type: colorscheme.color
+                  default: '#223a47'
+                  help: PLUGIN_ADMIN.CONTENT_TABS_BG_HELP
+
+                whitelabel.color_scheme.colors.content-tabs-text:
+                  type: colorscheme.color
+                  default: '#d1dee7'
+                  help: PLUGIN_ADMIN.CONTENT_TABS_TEXT_HELP
+
+                whitelabel.color_scheme.colors.content-highlight:
+                  type: colorscheme.color
+                  default: '#ffffd7'
+                  help: PLUGIN_ADMIN.CONTENT_HIGHLIGHT_HELP
+
+                whitelabel.color_scheme.colors.button-bg:
+                  type: colorscheme.color
+                  default: '#41bea8'
+                  help: PLUGIN_ADMIN.BUTTON_BG_HELP
+
+                whitelabel.color_scheme.colors.button-text:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.BUTTON_TEXT_HELP
+
+                whitelabel.color_scheme.colors.notice-bg:
+                  type: colorscheme.color
+                  default: '#00a6cf'
+                  help: PLUGIN_ADMIN.NOTICE_BG_HELP
+
+                whitelabel.color_scheme.colors.notice-text:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.NOTICE_TEXT_HELP
+
+                whitelabel.color_scheme.colors.update-bg:
+                  type: colorscheme.color
+                  default: '#8f5aad'
+                  help: PLUGIN_ADMIN.UPDATES_BG_HELP
+
+                whitelabel.color_scheme.colors.update-text:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.UPDATES_TEXT_HELP
+
+                whitelabel.color_scheme.colors.critical-bg:
+                  type: colorscheme.color
+                  default: '#da4b46'
+                  help: PLUGIN_ADMIN.CRITICAL_BG_HELP
+
+                whitelabel.color_scheme.colors.critical-text:
+                  type: colorscheme.color
+                  default: '#ffffff'
+                  help: PLUGIN_ADMIN.CRITICAL_TEXT_HELP
+
+            whitelabel.color_scheme.accents.primary-accent:
+              type: select
+              size: meidum
+              classes: fancy
+              label: PLUGIN_ADMIN.PRIMARY_ACCENT_LABEL
+              help: PLUGIN_ADMIN.PRIMARY_ACCENT_HELP
+              options:
+                button: PLUGIN_ADMIN.BUTTON_COLORS
+                content: PLUGIN_ADMIN.CONTENT_COLORS
+                tabs: PLUGIN_ADMIN.TABS_COLORS
+                critical: PLUGIN_ADMIN.CRITICAL_COLORS
+                logo: PLUGIN_ADMIN.LOGO_COLORS
+                nav: PLUGIN_ADMIN.NAV_COLORS
+                notice: PLUGIN_ADMIN.NOTICE_COLORS
+                page: PLUGIN_ADMIN.PAGE_COLORS
+                toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
+                update: PLUGIN_ADMIN.UPDATE_COLORS
+
+            whitelabel.color_scheme.accents.secondary-accent:
+              type: select
+              size: meidum
+              classes: fancy
+              label: PLUGIN_ADMIN.SECONDARY_ACCENT_LABEL
+              help: PLUGIN_ADMIN.SECONDARY_ACCENT_HELP
+              options:
+                button: PLUGIN_ADMIN.BUTTON_COLORS
+                content: PLUGIN_ADMIN.CONTENT_COLORS
+                tabs: PLUGIN_ADMIN.TABS_COLORS
+                critical: PLUGIN_ADMIN.CRITICAL_COLORS
+                logo: PLUGIN_ADMIN.LOGO_COLORS
+                nav: PLUGIN_ADMIN.NAV_COLORS
+                notice: PLUGIN_ADMIN.NOTICE_COLORS
+                page: PLUGIN_ADMIN.PAGE_COLORS
+                toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
+                update: PLUGIN_ADMIN.UPDATE_COLORS
+
+            whitelabel.color_scheme.accents.tertiary-accent:
+              type: select
+              size: meidum
+              classes: fancy
+              label: PLUGIN_ADMIN.TERTIARY_ACCENT_LABEL
+              help: PLUGIN_ADMIN.TERTIARY_ACCENT_HELP
+              options:
+                button: PLUGIN_ADMIN.BUTTON_COLORS
+                content: PLUGIN_ADMIN.CONTENT_COLORS
+                tabs: PLUGIN_ADMIN.TABS_COLORS
+                critical: PLUGIN_ADMIN.CRITICAL_COLORS
+                logo: PLUGIN_ADMIN.LOGO_COLORS
+                nav: PLUGIN_ADMIN.NAV_COLORS
+                notice: PLUGIN_ADMIN.NOTICE_COLORS
+                page: PLUGIN_ADMIN.PAGE_COLORS
+                toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
+                update: PLUGIN_ADMIN.UPDATE_COLORS
+
+            whitelabel.custom_footer:
+              type: textarea
+              rows: 2
+              label: PLUGIN_ADMIN.CUSTOM_FOOTER
+              help: PLUGIN_ADMIN.CUSTOM_FOOTER_HELP
+              placeholder: PLUGIN_ADMIN.CUSTOM_FOOTER_PLACEHOLDER
+
+
+            whitelabel.custom_css:
+              label: PLUGIN_ADMIN.CUSTOM_CSS_LABEL
+              placeholder: PLUGIN_ADMIN.CUSTOM_CSS_PLACEHOLDER
+              help: PLUGIN_ADMIN.CUSTOM_CSS_HELP
+              type: editor
+              codemirror:
+                mode: 'css'
+                indentUnit: 2
+                autofocus: true
+                indentWithTabs: true
+                lineNumbers: true
+                styleActiveLine: true
+
+            whitelabel.custom_presets:
+              label: PLUGIN_ADMIN.CUSTOM_PRESETS
+              help: PLUGIN_ADMIN.CUSTOM_PRESETS_HELP
+              placeholder: PLUGIN_ADMIN.CUSTOM_PRESETS_PLACEHOLDER
+              type: editor
+              codemirror:
+                mode: 'yaml'
+                indentUnit: 2
+                autofocus: true
+                indentWithTabs: false
+                lineNumbers: true
+                styleActiveLine: true
+                gutters: ['CodeMirror-lint-markers']
+                lint: true
+
+        extras_tab:
+          type: tab
+          title: PLUGIN_ADMIN.EXTRAS
+
+          fields:
+
+            Popularity:
+              type: section
+              title: PLUGIN_ADMIN.POPULARITY
+              underline: true
+
+            popularity.enabled:
+              type: toggle
+              label: PLUGIN_ADMIN.VISITOR_TRACKING
+              highlight: 1
+              default: 1
+              options:
+                1: PLUGIN_ADMIN.ENABLED
+                0: PLUGIN_ADMIN.DISABLED
+              validate:
+                type: bool
+              help: PLUGIN_ADMIN.VISITOR_TRACKING_HELP
+
+            dashboard.days_of_stats:
+              type: text
+              label: PLUGIN_ADMIN.DAYS_OF_STATS
+              append: days
+              size: x-small
+              default: 7
+              help: PLUGIN_ADMIN.DAYS_OF_STATS_HELP
+              validate:
+                type: int
+
+            popularity.ignore:
+              type: array
+              label: PLUGIN_ADMIN.IGNORE_URLS
+              size: large
+              help: PLUGIN_ADMIN.IGNORE_URLS_HELP
+              default: ['/test*','/modular']
+              value_only: true
+              placeholder_value: /ignore-this-route
+
+            popularity.history.daily:
+              type: hidden
+              label: PLUGIN_ADMIN.DAILY_HISTORY
+              default: 30
+
+            popularity.history.monthly:
+              type: hidden
+              label: PLUGIN_ADMIN.MONTHLY_HISTORY
+              default: 12
+
+            popularity.history.visitors:
+              type: hidden
+              label: PLUGIN_ADMIN.VISITORS_HISTORY
+              default: 20
+
+            MediaResize:
+              type: section
+              title: PLUGIN_ADMIN.MEDIA_RESIZE
+              underline: true
+
+            MediaResizeNote:
+              type: spacer
+              text: PLUGIN_ADMIN.PAGEMEDIA_RESIZER
+              markdown: true
+
+            pagemedia.resize_width:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RESIZE_WIDTH
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RESIZE_WIDTH_HELP
+
+            pagemedia.resize_height:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RESIZE_HEIGHT
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RESIZE_HEIGHT_HELP
+
+            pagemedia.res_min_width:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RES_MIN_WIDTH
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RES_MIN_WIDTH_HELP
+
+            pagemedia.res_min_height:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RES_MIN_HEIGHT
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RES_MIN_HEIGHT_HELP
+
+            pagemedia.res_max_width:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RES_MAX_WIDTH
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RES_MAX_WIDTH_HELP
+
+            pagemedia.res_max_height:
+              type: number
+              size: x-small
+              append: PLUGIN_ADMIN.PIXELS
+              label: PLUGIN_ADMIN.RES_MAX_HEIGHT
+              default: 0
+              validate:
+                type: number
+              help: PLUGIN_ADMIN.RES_MAX_HEIGHT_HELP
+
+            pagemedia.resize_quality:
+              type: number
+              size: x-small
+              append: 0...1
+              label: PLUGIN_ADMIN.RESIZE_QUALITY
+              default: 0.8
+              validate:
+                type: number
+                step: 0.01
+              help: PLUGIN_ADMIN.RESIZE_QUALITY_HELP

+ 36 - 0
plugins/admin/blueprints/config/media.yaml

@@ -0,0 +1,36 @@
+title: PLUGIN_ADMIN.MEDIA
+form:
+  validation: loose
+  fields:
+    'types':
+      name: medias
+      type: list
+      label: PLUGIN_ADMIN.MEDIA_TYPES
+      style: vertical
+      key: extension
+      controls: both
+      collapsed: true
+
+      fields:
+        .extension:
+          type: key
+          label: PLUGIN_ADMIN.FILE_EXTENSION
+        .type:
+          type: text
+          label: PLUGIN_ADMIN.TYPE
+        .thumb:
+          type: text
+          label: PLUGIN_ADMIN.THUMB
+        .mime:
+          type: text
+          label: PLUGIN_ADMIN.MIME_TYPE
+          validate:
+            type: lower
+        .image:
+          type: textarea
+          yaml: true
+          label: PLUGIN_ADMIN.IMAGE_OPTIONS
+          validate:
+            type: yaml
+
+

+ 2507 - 0
plugins/admin/classes/plugin/Admin.php

@@ -0,0 +1,2507 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use DateTime;
+use Grav\Common\Data;
+use Grav\Common\Data\Data as GravData;
+use Grav\Common\Debugger;
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Flex\Types\Users\UserObject;
+use Grav\Common\GPM\GPM;
+use Grav\Common\GPM\Licenses;
+use Grav\Common\Grav;
+use Grav\Common\Helpers\YamlLinter;
+use Grav\Common\HTTP\Response;
+use Grav\Common\Language\Language;
+use Grav\Common\Language\LanguageCodes;
+use Grav\Common\Page\Collection;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Page;
+use Grav\Common\Page\Pages;
+use Grav\Common\Plugins;
+use Grav\Common\Security;
+use Grav\Common\Session;
+use Grav\Common\Themes;
+use Grav\Common\Uri;
+use Grav\Common\User\Interfaces\UserCollectionInterface;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Framework\Acl\Action;
+use Grav\Framework\Acl\Permissions;
+use Grav\Framework\Collection\ArrayCollection;
+use Grav\Framework\Flex\Flex;
+use Grav\Framework\Flex\Interfaces\FlexInterface;
+use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
+use Grav\Framework\Route\Route;
+use Grav\Framework\Route\RouteFactory;
+use Grav\Plugin\AdminPlugin;
+use Grav\Plugin\Login\Login;
+use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
+use JsonException;
+use PicoFeed\Parser\MalformedXmlException;
+use Psr\Http\Message\ServerRequestInterface;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\File\File;
+use RocketTheme\Toolbox\File\JsonFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use RocketTheme\Toolbox\Session\Message;
+use Grav\Common\Yaml;
+use Composer\Semver\Semver;
+use PicoFeed\Reader\Reader;
+
+define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
+
+/**
+ * Class Admin
+ * @package Grav\Plugin\Admin
+ */
+class Admin
+{
+    /** @var int */
+    public const DEBUG = 1;
+    /** @var int */
+    public const MEDIA_PAGINATION_INTERVAL = 20;
+    /** @var string */
+    public const TMP_COOKIE_NAME = 'tmp-admin-message';
+
+    /** @var Grav */
+    public $grav;
+    /** @var ServerRequestInterface|null */
+    public $request;
+    /** @var AdminForm */
+    public $form;
+    /** @var string */
+    public $base;
+    /** @var string */
+    public $location;
+    /** @var string */
+    public $route;
+    /** @var UserInterface */
+    public $user;
+    /** @var array */
+    public $forgot;
+    /** @var string */
+    public $task;
+    /** @var array */
+    public $json_response;
+    /** @var Collection */
+    public $collection;
+    /** @var bool */
+    public $multilang;
+    /** @var string */
+    public $language;
+    /** @var array */
+    public $languages_enabled = [];
+    /** @var Uri $uri */
+
+    /** @var array */
+    public $routes = [];
+    protected $uri;
+    /** @var array */
+    protected $pages = [];
+    /** @var Session */
+    protected $session;
+    /** @var Data\Blueprints */
+    protected $blueprints;
+    /** @var GPM */
+    protected $gpm;
+    /** @var int */
+    protected $pages_count;
+    /** @var bool */
+    protected $load_additional_files_in_background = false;
+    /** @var bool */
+    protected $loading_additional_files_in_background = false;
+    /** @var array */
+    protected $temp_messages = [];
+
+    /**
+     * Constructor.
+     *
+     * @param Grav   $grav
+     * @param string $base
+     * @param string $location
+     * @param string|null $route
+     */
+    public function __construct(Grav $grav, $base, $location, $route)
+    {
+        // Register admin to grav because of calling $grav['user'] requires it.
+        $grav['admin']     = $this;
+
+        $this->grav        = $grav;
+        $this->base        = $base;
+        $this->location    = $location;
+        $this->route       = $route ?? '';
+        $this->uri         = $grav['uri'];
+        $this->session     = $grav['session'];
+
+        /** @var FlexInterface|null $flex */
+        $flex = $grav['flex_objects'] ?? null;
+
+        /** @var UserInterface $user */
+        $user = $grav['user'];
+
+        // Convert old user to Flex User if Flex Objects plugin has been enabled.
+        if ($flex && !$user instanceof FlexObjectInterface) {
+            $managed = !method_exists($flex, 'isManaged') || $flex->isManaged('user-accounts');
+            $directory = $managed ? $flex->getDirectory('user-accounts') : null;
+
+            /** @var UserObject|null $test */
+            $test = $directory ? $directory->getObject(mb_strtolower($user->username)) : null;
+            if ($test) {
+                $test = clone $test;
+                $test->access = $user->access;
+                $test->groups = $user->groups;
+                $test->authenticated = $user->authenticated;
+                $test->authorized = $user->authorized;
+                $user = $test;
+            }
+        }
+        $this->user = $user;
+
+        /** @var Language $language */
+        $language = $grav['language'];
+
+        $this->multilang = $language->enabled();
+
+        // Load utility class
+        if ($this->multilang) {
+            $this->language = $language->getActive() ?? '';
+            $this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []);
+
+            //Set the currently active language for the admin
+            $languageCode = $this->uri->param('lang');
+            if (null === $languageCode && !$this->session->admin_lang) {
+                $this->session->admin_lang = $language->getActive() ?? '';
+            }
+        } else {
+            $this->language = '';
+        }
+
+        // Set admin route language.
+        RouteFactory::setLanguage($this->language);
+    }
+
+    /**
+     * @param string $message
+     * @param array|object $data
+     * @return void
+     */
+    public static function addDebugMessage(string $message, $data = [])
+    {
+        /** @var Debugger $debugger */
+        $debugger = Grav::instance()['debugger'];
+        $debugger->addMessage($message, 'debug', $data);
+    }
+
+    /**
+     * @return string[]
+     */
+    public static function contentEditor()
+    {
+        $options = [
+            'default' => 'Default',
+            'codemirror' => 'CodeMirror'
+        ];
+        $event = new Event(['options' => &$options]);
+        Grav::instance()->fireEvent('onAdminListContentEditors', $event);
+        return $options;
+    }
+
+    /**
+     * Return the languages available in the admin
+     *
+     * @return array
+     */
+    public static function adminLanguages()
+    {
+        $languages = [];
+
+        $path = Grav::instance()['locator']->findResource('plugins://admin/languages');
+
+        foreach (new \DirectoryIterator($path) as $file) {
+            if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getFilename(), '.')) {
+                continue;
+            }
+
+            $lang = $file->getBasename('.yaml');
+
+            $languages[$lang] = LanguageCodes::getNativeName($lang);
+
+        }
+
+        // sort languages
+        asort($languages);
+
+        return $languages;
+    }
+
+    /**
+     * @return string
+     */
+    public function getLanguage(): string
+    {
+        return $this->language ?: $this->grav['language']->getLanguage() ?: 'en';
+    }
+
+    /**
+     * Return the found configuration blueprints
+     *
+     * @param bool $checkAccess
+     * @return array
+     */
+    public static function configurations(bool $checkAccess = false): array
+    {
+        $grav = Grav::instance();
+
+        /** @var Admin $admin */
+        $admin = $grav['admin'];
+
+        /** @var UniformResourceIterator $iterator */
+        $iterator = $grav['locator']->getIterator('blueprints://config');
+
+        // Find all main level configuration files.
+        $configurations = [];
+        foreach ($iterator as $file) {
+            if ($file->isDir() || !preg_match('/^[^.].*.yaml$/', $file->getFilename())) {
+                continue;
+            }
+
+            $name = $file->getBasename('.yaml');
+
+            // Check that blueprint exists and is not hidden.
+            $data = $admin->getConfigurationData('config/'. $name);
+            if (!is_callable([$data, 'blueprints'])) {
+                continue;
+            }
+
+            $blueprint = $data->blueprints();
+            if (!$blueprint) {
+                continue;
+            }
+
+            $test = $blueprint->toArray();
+            if (empty($test['form']['hidden']) && (!empty($test['form']['field']) || !empty($test['form']['fields']))) {
+                $configurations[$name] = true;
+            }
+        }
+
+        // Remove scheduler and backups configs (they belong to the tools).
+        unset($configurations['scheduler'], $configurations['backups']);
+
+        // Sort configurations.
+        ksort($configurations);
+        $configurations = ['system' => true, 'site' => true] + $configurations + ['info' => true];
+
+        if ($checkAccess) {
+            // ACL checks.
+            foreach ($configurations as $name => $value) {
+                if (!$admin->authorize(['admin.configuration.' . $name, 'admin.super'])) {
+                    unset($configurations[$name]);
+                }
+            }
+        }
+
+        return array_keys($configurations);
+    }
+
+    /**
+     * Return the tools found
+     *
+     * @return array
+     */
+    public static function tools()
+    {
+        $tools = [];
+        Grav::instance()->fireEvent('onAdminTools', new Event(['tools' => &$tools]));
+
+        return $tools;
+    }
+
+    /**
+     * @return array
+     */
+    public static function toolsPermissions()
+    {
+        $tools = static::tools();
+        $perms = [];
+
+        foreach ($tools as $tool) {
+            $perms = array_merge($perms, $tool[0]);
+        }
+
+        return array_unique($perms);
+    }
+
+    /**
+     * Return the languages available in the site
+     *
+     * @return array
+     */
+    public static function siteLanguages()
+    {
+        $languages = [];
+        $lang_data = (array) Grav::instance()['config']->get('system.languages.supported', []);
+
+        foreach ($lang_data as $index => $lang) {
+            $languages[$lang] = LanguageCodes::getNativeName($lang);
+        }
+
+        return $languages;
+    }
+
+    /**
+     * Static helper method to return the admin form nonce
+     *
+     * @param string $action
+     * @return string
+     */
+    public static function getNonce(string $action = 'admin-form')
+    {
+        return Utils::getNonce($action);
+    }
+
+    /**
+     * Static helper method to return the last used page name
+     *
+     * @return string
+     */
+    public static function getLastPageName()
+    {
+        return Grav::instance()['session']->lastPageName ?: 'default';
+    }
+
+    /**
+     * Static helper method to return the last used page route
+     *
+     * @return string
+     */
+    public static function getLastPageRoute()
+    {
+        /** @var Session $session */
+        $session = Grav::instance()['session'];
+        $route = $session->lastPageRoute;
+        if ($route) {
+            return $route;
+        }
+
+        /** @var Admin $admin */
+        $admin = Grav::instance()['admin'];
+
+        return $admin->getCurrentRoute();
+    }
+
+    /**
+     * @param string $path
+     * @param string|null $languageCode
+     * @return Route
+     */
+    public function getAdminRoute(string $path = '', $languageCode = null): Route
+    {
+        /** @var Language $language */
+        $language = $this->grav['language'];
+        $languageCode = $languageCode ?? ($language->getActive() ?: null);
+        $languagePrefix = $languageCode ? '/' . $languageCode : '';
+
+        $root = $this->grav['uri']->rootUrl();
+        $subRoute = rtrim($this->grav['pages']->base(), '/');
+        $adminRoute = rtrim($this->grav['config']->get('plugins.admin.route'), '/');
+
+        $parts = [
+            'path' => $path,
+            'query' => '',
+            'query_params' => [],
+            'grav' => [
+                // TODO: Make URL to be /admin/en, not /en/admin.
+                'root' => preg_replace('`//+`', '/', $root . $subRoute . $languagePrefix . $adminRoute),
+                'language' => '', //$languageCode,
+                'route' => ltrim($path, '/'),
+                'params' => ''
+            ],
+        ];
+
+        return RouteFactory::createFromParts($parts);
+    }
+
+    /**
+     * @param string $route
+     * @param string|null $languageCode
+     * @return string
+     */
+    public function adminUrl(string $route = '', $languageCode = null)
+    {
+        return $this->getAdminRoute($route, $languageCode)->toString(true);
+    }
+
+    /**
+     * Static helper method to return current route.
+     *
+     * @return string
+     * @deprecated 1.10 Use $admin->getCurrentRoute() instead
+     */
+    public static function route()
+    {
+        user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Admin 1.9.7, use $admin->getCurrentRoute() instead', E_USER_DEPRECATED);
+
+        $admin = Grav::instance()['admin'];
+
+        return $admin->getCurrentRoute();
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getCurrentRoute()
+    {
+        $pages = static::enablePages();
+
+        $route = '/' . ltrim($this->route, '/');
+
+        /** @var PageInterface $page */
+        $page         = $pages->find($route);
+        $parent_route = null;
+        if ($page) {
+            /** @var PageInterface $parent */
+            $parent       = $page->parent();
+            $parent_route = $parent->rawRoute();
+        }
+
+        return $parent_route;
+    }
+
+    /**
+     * Redirect to the route stored in $this->redirect
+     *
+     * Route may or may not be prefixed by /en or /admin or /en/admin.
+     *
+     * @param string $redirect
+     * @param int $redirectCode
+     * @return void
+     */
+    public function redirect($redirect, $redirectCode = 303)
+    {
+        // No redirect, do nothing.
+        if (!$redirect) {
+            return;
+        }
+
+        Admin::DEBUG && Admin::addDebugMessage("Admin redirect: {$redirectCode} {$redirect}");
+
+        $redirect = '/' . ltrim(preg_replace('`//+`', '/', $redirect), '/');
+        $base = $this->base;
+        $root = Grav::instance()['uri']->rootUrl();
+        if ($root === '/') {
+            $root = '';
+        }
+
+        $pattern = '`^((' . preg_quote($root, '`') . ')?(/[^/]+)?)' . preg_quote($base, '`') . '`ui';
+        // Check if we already have an admin path: /admin, /en/admin, /root/admin or /root/en/admin.
+        if (preg_match($pattern, $redirect)) {
+            $redirect = preg_replace('|^' . preg_quote($root, '|') . '|', '', $redirect);
+
+            $this->grav->redirect($redirect, $redirectCode);
+        }
+
+        if ($this->isMultilang()) {
+            // Check if URL does not have language prefix.
+            if (!Utils::pathPrefixedByLangCode($redirect)) {
+                /** @var Language $language */
+                $language = $this->grav['language'];
+
+                // Prefix path with language prefix: /en
+                // TODO: Use /admin/en instead of /en/admin in the future.
+                $redirect = $language->getLanguageURLPrefix($this->grav['session']->admin_lang) . $base . $redirect;
+            } else {
+                // TODO: Use /admin/en instead of /en/admin in the future.
+                //$redirect = preg_replace('`^(/[^/]+)/admin`', '\\1', $redirect);
+
+                // Check if we already have language prefixed admin path: /en/admin
+                $this->grav->redirect($redirect, $redirectCode);
+            }
+        } else {
+            // TODO: Use /admin/en instead of /en/admin in the future.
+            // Prefix path with /admin
+            $redirect = $base . $redirect;
+        }
+
+        $this->grav->redirect($redirect, $redirectCode);
+    }
+
+    /**
+     * Return true if multilang is active
+     *
+     * @return bool True if multilang is active
+     */
+    protected function isMultilang()
+    {
+        return count($this->grav['config']->get('system.languages.supported', [])) > 1;
+    }
+
+    /**
+     * @return string
+     */
+    public static function getTempDir()
+    {
+        try {
+            $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
+        } catch (\Exception $e) {
+            $tmp_dir = Grav::instance()['locator']->findResource('cache://', true, true) . '/tmp';
+        }
+
+        return $tmp_dir;
+    }
+
+    /**
+     * @return array
+     */
+    public static function getPageMedia()
+    {
+        $files = [];
+        $grav = Grav::instance();
+
+        $pages = static::enablePages();
+
+        $route = '/' . ltrim($grav['admin']->route, '/');
+
+        /** @var PageInterface $page */
+        $page = $pages->find($route);
+        $parent_route = null;
+        if ($page) {
+            $media = $page->media()->all();
+            $files = array_keys($media);
+        }
+        return $files;
+
+    }
+
+    /**
+     * Get current session.
+     *
+     * @return Session
+     */
+    public function session()
+    {
+        return $this->session;
+    }
+
+    /**
+     * Fetch and delete messages from the session queue.
+     *
+     * @param string|null $type
+     * @return array
+     */
+    public function messages($type = null)
+    {
+        /** @var Message $messages */
+        $messages = $this->grav['messages'];
+
+        return $messages->fetch($type);
+    }
+
+    /**
+     * Authenticate user.
+     *
+     * @param array $credentials User credentials.
+     * @param array $post
+     * @return never-return
+     */
+    public function authenticate($credentials, $post)
+    {
+        /** @var Login $login */
+        $login = $this->grav['login'];
+
+        // Remove login nonce from the form.
+        $credentials = array_diff_key($credentials, ['admin-nonce' => true]);
+        $twofa = $this->grav['config']->get('plugins.admin.twofa_enabled', false);
+
+        $rateLimiter = $login->getRateLimiter('login_attempts');
+
+        $userKey = (string)($credentials['username'] ?? '');
+        $ipKey = Uri::ip();
+        $redirect = $post['redirect'] ?? $this->base . $this->route;
+
+        // Pseudonymization of the IP
+        $ipKey = sha1($ipKey . $this->grav['config']->get('security.salt'));
+
+        // Check if the current IP has been used in failed login attempts.
+        $attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
+
+        $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
+
+        // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
+        if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
+            Admin::DEBUG && Admin::addDebugMessage('Admin login: rate limit, redirecting', $credentials);
+
+            $this->setMessage(static::translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
+
+            $this->grav->redirect('/');
+        }
+
+        Admin::DEBUG && Admin::addDebugMessage('Admin login', $credentials);
+
+        // Fire Login process.
+        $event = $login->login(
+            $credentials,
+            ['admin' => true, 'twofa' => $twofa],
+            ['authorize' => 'admin.login', 'return_event' => true]
+        );
+        $user = $event->getUser();
+
+        Admin::DEBUG && Admin::addDebugMessage('Admin login: user', $user);
+
+        if ($user->authenticated) {
+            $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
+            if ($user->authorized) {
+                $event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
+
+                $event->defRedirect($post['redirect'] ?? $redirect);
+            } else {
+                $this->session->redirect = $redirect;
+            }
+        } else {
+            if ($user->authorized) {
+                $event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
+            } else {
+                $event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
+            }
+        }
+
+        $event->defRedirect($redirect);
+
+        $message = $event->getMessage();
+        if ($message) {
+            $this->setMessage(static::translate($message), $event->getMessageType());
+        }
+
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+        $redirect = $pages->baseRoute() . $event->getRedirect();
+
+        $this->grav->redirect($redirect, $event->getRedirectCode());
+    }
+
+    /**
+     * Check Two-Factor Authentication.
+     *
+     * @param array $data
+     * @param array $post
+     * @return never-return
+     */
+    public function twoFa($data, $post)
+    {
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+        $baseRoute = $pages->baseRoute();
+
+        /** @var Login $login */
+        $login = $this->grav['login'];
+
+        /** @var TwoFactorAuth $twoFa */
+        $twoFa = $login->twoFactorAuth();
+        $user = $this->grav['user'];
+
+        $code = $data['2fa_code'] ?? null;
+
+        $secret = $user->twofa_secret ?? null;
+
+        if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
+            $login->logout(['admin' => true]);
+
+            $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
+
+            $this->grav->redirect($baseRoute . $this->uri->route(), 303);
+        }
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
+
+        $user->authorized = true;
+
+        $redirect = $baseRoute . $post['redirect'];
+
+        $this->grav->redirect($redirect);
+    }
+
+    /**
+     * Logout from admin.
+     *
+     * @param array $data
+     * @param array $post
+     * @return never-return
+     */
+    public function logout($data, $post)
+    {
+        /** @var Login $login */
+        $login = $this->grav['login'];
+
+        $event = $login->logout(['admin' => true], ['return_event' => true]);
+
+        $event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info');
+        $message = $event->getMessage();
+        if ($message) {
+            $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]);
+        }
+
+        $this->grav->redirect($this->base);
+    }
+
+    /**
+     * @return bool
+     */
+    public static function doAnyUsersExist()
+    {
+        $accounts = Grav::instance()['accounts'] ?? null;
+
+        return $accounts && $accounts->count() > 0;
+    }
+
+    /**
+     * Add message into the session queue.
+     *
+     * @param string $msg
+     * @param string $type
+     * @return void
+     */
+    public function setMessage($msg, $type = 'info')
+    {
+        /** @var Message $messages */
+        $messages = $this->grav['messages'];
+        $messages->add($msg, $type);
+    }
+
+    /**
+     * @param string $msg
+     * @param string $type
+     * @return void
+     */
+    public function addTempMessage($msg, $type)
+    {
+        $this->temp_messages[] = ['message' => $msg, 'scope' => $type];
+    }
+
+    /**
+     * @return array
+     */
+    public function getTempMessages()
+    {
+        return $this->temp_messages;
+    }
+
+    /**
+     * Translate a string to the user-defined language
+     *
+     * @param array|string $args
+     * @param array|null $languages
+     * @return string|string[]|null
+     */
+    public static function translate($args, $languages = null)
+    {
+        $grav = Grav::instance();
+
+        if (is_array($args)) {
+            $lookup = array_shift($args);
+        } else {
+            $lookup = $args;
+            $args   = [];
+        }
+
+        if (!$languages) {
+            if ($grav['config']->get('system.languages.translations_fallback', true)) {
+                $languages = $grav['language']->getFallbackLanguages();
+            } else {
+                $languages = (array)$grav['language']->getDefault();
+            }
+            $languages = $grav['user']->authenticated ? [$grav['user']->language] : $languages;
+        } else {
+            $languages = (array)$languages;
+        }
+
+        foreach ((array)$languages as $lang) {
+            $translation = $grav['language']->getTranslation($lang, $lookup, true);
+
+            if (!$translation) {
+                $language    = $grav['language']->getDefault() ?: 'en';
+                $translation = $grav['language']->getTranslation($language, $lookup, true);
+            }
+
+            if (!$translation) {
+                $language    = 'en';
+                $translation = $grav['language']->getTranslation($language, $lookup, true);
+            }
+
+            if ($translation) {
+                if (count($args) >= 1) {
+                    return vsprintf($translation, $args);
+                }
+
+                return $translation;
+            }
+        }
+
+        return $lookup;
+    }
+
+    /**
+     * Checks user authorisation to the action.
+     *
+     * @param  string|string[] $action
+     * @return bool
+     */
+    public function authorize($action = 'admin.login')
+    {
+        $action = (array)$action;
+
+        $user = $this->user;
+
+        foreach ($action as $a) {
+            // Ignore 'admin.super' if it's not the only value to be checked.
+            if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
+                continue;
+            }
+            if ($user->authorize($a)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Gets configuration data.
+     *
+     * @param string $type
+     * @param array  $post
+     * @return object
+     * @throws \RuntimeException
+     */
+    public function data($type, array $post = [])
+    {
+        if (!$post) {
+            $post = $this->preparePost($this->grav['uri']->post()['data'] ?? []);
+        }
+
+        try {
+            return $this->getConfigurationData($type, $post);
+        } catch (\RuntimeException $e) {
+            return new Data\Data();
+        }
+    }
+
+    /**
+     * Get configuration data.
+     *
+     * Note: If you pass $post, make sure you pass all the fields in the blueprint or data gets lost!
+     *
+     * @param string $type
+     * @param array|null  $post
+     * @return object
+     * @throws \RuntimeException
+     */
+    public function getConfigurationData($type, array $post = null)
+    {
+        static $data = [];
+
+        if (isset($data[$type])) {
+            $obj = $data[$type];
+            if ($post) {
+                if ($obj instanceof Data\Data) {
+                    $obj = $this->mergePost($obj, $post);
+                } elseif ($obj instanceof UserInterface) {
+                    $obj->update($this->cleanUserPost($post));
+                }
+            }
+
+            return $obj;
+        }
+
+        // Check to see if a data type is plugin-provided, before looking into core ones
+        $event = $this->grav->fireEvent('onAdminData', new Event(['type' => &$type]));
+        if ($event) {
+            if (isset($event['data_type'])) {
+                return $event['data_type'];
+            }
+
+            if (is_string($event['type'])) {
+                $type = $event['type'];
+            }
+        }
+
+        /** @var UniformResourceLocator $locator */
+        $locator  = $this->grav['locator'];
+
+        // Configuration file will be saved to the existing config stream.
+        $filename = $locator->findResource('config://') . "/{$type}.yaml";
+        $file     = CompiledYamlFile::instance($filename);
+
+        if (preg_match('|plugins/|', $type)) {
+            $obj = Plugins::get(preg_replace('|plugins/|', '', $type));
+            if (null === $obj) {
+                throw new \RuntimeException("Plugin '{$type}' doesn't exist!");
+            }
+            $obj->file($file);
+
+        } elseif (preg_match('|themes/|', $type)) {
+            /** @var Themes $themes */
+            $themes = $this->grav['themes'];
+            $obj = $themes->get(preg_replace('|themes/|', '', $type));
+            if (null === $obj) {
+                throw new \RuntimeException("Theme '{$type}' doesn't exist!");
+            }
+            $obj->file($file);
+
+        } elseif (preg_match('|users?/|', $type)) {
+            /** @var UserCollectionInterface $users */
+            $users = $this->grav['accounts'];
+
+            $obj = $users->load(preg_replace('|users?/|', '', $type));
+
+        } elseif (preg_match('|config/|', $type)) {
+            $type       = preg_replace('|config/|', '', $type);
+            $blueprints = $this->blueprints("config/{$type}");
+            if (!$blueprints->form()) {
+                throw new \RuntimeException("Configuration type '{$type}' doesn't exist!");
+            }
+
+            // Configuration file will be saved to the existing config stream.
+            $filename = $locator->findResource('config://') . "/{$type}.yaml";
+            $file     = CompiledYamlFile::instance($filename);
+
+            $config = $this->grav['config'];
+            $obj = new Data\Data($config->get($type, []), $blueprints);
+            $obj->file($file);
+
+        } elseif (preg_match('|media-manager/|', $type)) {
+            $filename = base64_decode(preg_replace('|media-manager/|', '', $type));
+
+            $file = File::instance($filename);
+
+            $pages = static::enablePages();
+
+            $obj = new \stdClass();
+            $obj->title = $file->basename();
+            $obj->path = $file->filename();
+            $obj->file = $file;
+            $obj->page = $pages->get(dirname($obj->path));
+
+            $fileInfo = Utils::pathinfo($obj->title);
+            $filename = str_replace(['@3x', '@2x'], '', $fileInfo['filename']);
+            if (isset($fileInfo['extension'])) {
+                $filename .= '.' . $fileInfo['extension'];
+            }
+
+            if ($obj->page && isset($obj->page->media()[$filename])) {
+                $obj->metadata = new Data\Data($obj->page->media()[$filename]->metadata());
+            }
+
+        } else {
+            throw new \RuntimeException("Data type '{$type}' doesn't exist!");
+        }
+
+        $data[$type] = $obj;
+        if ($post) {
+            if ($obj instanceof Data\Data) {
+                $obj = $this->mergePost($obj, $post);
+            } elseif ($obj instanceof UserInterface) {
+                $obj->update($this->cleanUserPost($post));
+            }
+        }
+
+        return $obj;
+    }
+
+    /**
+     * @param Data\Data $object
+     * @param array $post
+     * @return Data\Data
+     */
+    protected function mergePost(Data\Data $object, array $post)
+    {
+        $object->merge($post);
+
+        $blueprint = $object->blueprints();
+        $data = $blueprint->flattenData($post, true);
+
+        foreach ($data as $key => $val) {
+            if ($val === null) {
+                $object->set($key, $val);
+            }
+        }
+
+        return $object;
+    }
+
+    /**
+     * Clean user form post and remove extra stuff that may be passed along
+     *
+     * @param array $post
+     * @return array
+     */
+    public function cleanUserPost($post)
+    {
+        // Clean fields for all users
+        unset($post['hashed_password']);
+
+        // Clean field for users who shouldn't be able to modify these fields
+        if (!$this->authorize(['admin.user', 'admin.super'])) {
+            unset($post['access'], $post['state']);
+        }
+
+        return $post;
+    }
+
+    /**
+     * @return bool
+     */
+    protected function hasErrorMessage()
+    {
+        $msgs = $this->grav['messages']->all();
+        foreach ($msgs as $msg) {
+            if (isset($msg['scope']) && $msg['scope'] === 'error') {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns blueprints for the given type.
+     *
+     * @param string $type
+     * @return Data\Blueprint
+     */
+    public function blueprints($type)
+    {
+        if ($this->blueprints === null) {
+            $this->blueprints = new Data\Blueprints('blueprints://');
+        }
+
+        return $this->blueprints->get($type);
+    }
+
+    /**
+     * Converts dot notation to array notation.
+     *
+     * @param  string $name
+     * @return string
+     */
+    public function field($name)
+    {
+        $path = explode('.', $name);
+
+        return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
+    }
+
+    /**
+     * Get all routes.
+     *
+     * @param bool $unique
+     * @return array
+     */
+    public function routes($unique = false)
+    {
+        $pages = static::enablePages();
+
+        if ($unique) {
+            $routes = array_unique($pages->routes());
+        } else {
+            $routes = $pages->routes();
+        }
+
+        return $routes;
+    }
+
+    /**
+     * Count the pages
+     *
+     * @return int
+     */
+    public function pagesCount()
+    {
+        if (!$this->pages_count) {
+            $pages = static::enablePages();
+            $this->pages_count = count($pages->all());
+        }
+
+        return $this->pages_count;
+    }
+
+    /**
+     * Get all template types
+     *
+     * @param array|null $ignore
+     * @return array
+     */
+    public function types(?array $ignore = [])
+    {
+        if (null === $ignore) {
+            return AdminPlugin::pagesTypes();
+        }
+
+        $types = Pages::types();
+
+        return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
+    }
+
+    /**
+     * Get all modular template types
+     *
+     * @param array|null $ignore
+     * @return array
+     */
+    public function modularTypes(?array $ignore = [])
+    {
+        if (null === $ignore) {
+            return AdminPlugin::pagesModularTypes();
+        }
+
+        $types = Pages::modularTypes();
+
+        return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
+    }
+
+    /**
+     * Get all access levels
+     *
+     * @return array
+     */
+    public function accessLevels()
+    {
+        $pages = static::enablePages();
+
+        if (method_exists($pages, 'accessLevels')) {
+            return $pages->accessLevels();
+        }
+
+        return [];
+    }
+
+    /**
+     * @param string|null $package_slug
+     * @return string[]|string
+     */
+    public function license($package_slug)
+    {
+        return Licenses::get($package_slug);
+    }
+
+    /**
+     * Generate an array of dependencies for a package, used to generate a list of
+     * packages that can be removed when removing a package.
+     *
+     * @param string $slug The package slug
+     * @return array|bool
+     */
+    public function dependenciesThatCanBeRemovedWhenRemoving($slug)
+    {
+        $gpm = $this->gpm();
+        if (!$gpm) {
+            return false;
+        }
+
+        $dependencies = [];
+
+        $package = $this->getPackageFromGPM($slug);
+
+        if ($package && $package->dependencies) {
+            foreach ($package->dependencies as $dependency) {
+//                if (count($gpm->getPackagesThatDependOnPackage($dependency)) > 1) {
+//                    continue;
+//                }
+                if (isset($dependency['name'])) {
+                    $dependency = $dependency['name'];
+                }
+
+                if (!in_array($dependency, $dependencies, true) && !in_array($dependency, ['admin', 'form', 'login', 'email', 'php'])) {
+                    $dependencies[] = $dependency;
+                }
+            }
+        }
+
+        return $dependencies;
+    }
+
+    /**
+     * Get the GPM instance
+     *
+     * @return GPM The GPM instance
+     */
+    public function gpm()
+    {
+        if (!$this->gpm) {
+            try {
+                $this->gpm = new GPM();
+            } catch (\Exception $e) {
+                $this->setMessage($e->getMessage(), 'error');
+            }
+        }
+
+        return $this->gpm;
+    }
+
+    /**
+     * @param string $package_slug
+     * @return mixed
+     */
+    public function getPackageFromGPM($package_slug)
+    {
+        $package = $this->plugins(true)[$package_slug];
+        if (!$package) {
+            $package = $this->themes(true)[$package_slug];
+        }
+
+        return $package;
+    }
+
+    /**
+     * Get all plugins.
+     *
+     * @param bool $local
+     * @return mixed
+     */
+    public function plugins($local = true)
+    {
+        $gpm = $this->gpm();
+
+        if (!$gpm) {
+            return false;
+        }
+
+        if ($local) {
+            return $gpm->getInstalledPlugins();
+        }
+
+        $plugins = $gpm->getRepositoryPlugins();
+        if ($plugins) {
+            return $plugins->filter(function ($package, $slug) use ($gpm) {
+                return !$gpm->isPluginInstalled($slug);
+            });
+        }
+
+        return [];
+    }
+
+    /**
+     * Get all themes.
+     *
+     * @param bool $local
+     * @return mixed
+     */
+    public function themes($local = true)
+    {
+        $gpm = $this->gpm();
+
+        if (!$gpm) {
+            return false;
+        }
+
+        if ($local) {
+            return $gpm->getInstalledThemes();
+        }
+
+        $themes = $gpm->getRepositoryThemes();
+        if ($themes) {
+            return $themes->filter(function ($package, $slug) use ($gpm) {
+                return !$gpm->isThemeInstalled($slug);
+            });
+        }
+
+        return [];
+    }
+
+    /**
+     * Get list of packages that depend on the passed package slug
+     *
+     * @param string $slug The package slug
+     *
+     * @return array|bool
+     */
+    public function getPackagesThatDependOnPackage($slug)
+    {
+        $gpm = $this->gpm();
+        if (!$gpm) {
+            return false;
+        }
+
+        return $gpm->getPackagesThatDependOnPackage($slug);
+    }
+
+    /**
+     * Check the passed packages list can be updated
+     *
+     * @param array $packages
+     * @return bool
+     * @throws \Exception
+     */
+    public function checkPackagesCanBeInstalled($packages)
+    {
+        $gpm = $this->gpm();
+        if (!$gpm) {
+            return false;
+        }
+
+        $this->gpm->checkPackagesCanBeInstalled($packages);
+
+        return true;
+    }
+
+    /**
+     * Get an array of dependencies needed to be installed or updated for a list of packages
+     * to be installed.
+     *
+     * @param array $packages The packages slugs
+     * @return array|bool
+     */
+    public function getDependenciesNeededToInstall($packages)
+    {
+        $gpm = $this->gpm();
+        if (!$gpm) {
+            return false;
+        }
+
+        return $this->gpm->getDependencies($packages);
+    }
+
+    /**
+     * Used by the Dashboard in the admin to display the X latest pages
+     * that have been modified
+     *
+     * @param  int $count number of pages to pull back
+     * @return array|null
+     */
+    public function latestPages($count = 10)
+    {
+        /** @var Flex $flex */
+        $flex = $this->grav['flex_objects'] ?? null;
+        $directory = $flex ? $flex->getDirectory('pages') : null;
+        if ($directory) {
+            return $directory->getIndex()->sort(['timestamp' => 'DESC'])->slice(0, $count);
+        }
+
+        $pages = static::enablePages();
+
+        $latest = [];
+
+        if (null === $pages->routes()) {
+            return null;
+        }
+
+        foreach ($pages->routes() as $url => $path) {
+            $page = $pages->find($url, true);
+            if ($page && $page->routable()) {
+                $latest[$page->route()] = ['modified' => $page->modified(), 'page' => $page];
+            }
+        }
+
+        // sort based on modified
+        uasort($latest, function ($a, $b) {
+            if ($a['modified'] == $b['modified']) {
+                return 0;
+            }
+
+            return ($a['modified'] > $b['modified']) ? -1 : 1;
+        });
+
+        // build new array with just pages in it
+        $list = [];
+        foreach ($latest as $item) {
+            $list[] = $item['page'];
+        }
+
+        return array_slice($list, 0, $count);
+    }
+
+    /**
+     * Get log file for fatal errors.
+     *
+     * @return string
+     */
+    public function logEntry()
+    {
+        $file    = File::instance($this->grav['locator']->findResource("log://{$this->route}.html"));
+        $content = $file->content();
+        $file->free();
+
+        return $content;
+    }
+
+    /**
+     * Search in the logs when was the latest backup made
+     *
+     * @return array Array containing the latest backup information
+     */
+    public function lastBackup()
+    {
+        $backup_file = $this->grav['locator']->findResource('log://backup.log');
+        $content = null;
+
+        if ($backup_file) {
+            $file    = JsonFile::instance((string) $backup_file);
+            $content = $file->content() ?? null;
+        }
+
+        if (!file_exists($backup_file) || is_null($content) || !isset($content['time'])) {
+            return [
+                'days'        => '&infin;',
+                'chart_fill'  => 100,
+                'chart_empty' => 0
+            ];
+        }
+
+        $backup = new \DateTime();
+        $backup->setTimestamp($content['time']);
+        $diff = $backup->diff(new \DateTime());
+        $days = $diff->days;
+        $chart_fill = $days > 30 ? 100 : round($days / 30 * 100);
+        return [
+            'days'        => $days,
+            'chart_fill'  => $chart_fill,
+            'chart_empty' => 100 - $chart_fill
+        ];
+    }
+
+    /**
+     * Determine if the plugin or theme info passed is from Team Grav
+     *
+     * @param object $info Plugin or Theme info object
+     * @return bool
+     */
+    public function isTeamGrav($info)
+    {
+        return isset($info['author']['name']) && ($info['author']['name'] === 'Team Grav' || Utils::contains($info['author']['name'], 'Trilby Media'));
+    }
+
+    /**
+     * Determine if the plugin or theme info passed is premium
+     *
+     * @param object $info Plugin or Theme info object
+     * @return bool
+     */
+    public function isPremiumProduct($info)
+    {
+        return isset($info['premium']);
+    }
+
+    /**
+     * Renders phpinfo
+     *
+     * @return string The phpinfo() output
+     */
+    public function phpinfo()
+    {
+        if (function_exists('phpinfo')) {
+            ob_start();
+            phpinfo();
+            $pinfo = ob_get_clean();
+            $pinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo);
+
+            return $pinfo;
+        }
+
+        return 'phpinfo() method is not available on this server.';
+    }
+
+    /**
+     * Guest date format based on euro/US
+     *
+     * @param string|null $date
+     * @return string
+     */
+    public function guessDateFormat($date)
+    {
+        static $guess;
+
+        $date_formats = [
+            'm/d/y',
+            'm/d/Y',
+            'n/d/y',
+            'n/d/Y',
+            'd-m-Y',
+            'd-m-y',
+        ];
+
+        $time_formats = [
+            'H:i',
+            'G:i',
+            'h:ia',
+            'g:ia'
+        ];
+
+        $date = (string)$date;
+        if (!isset($guess[$date])) {
+            $guess[$date] = 'd-m-Y H:i';
+            foreach ($date_formats as $date_format) {
+                foreach ($time_formats as $time_format) {
+                    $full_format = "{$date_format} {$time_format}";
+                    if ($this->validateDate($date, $full_format)) {
+                        $guess[$date] = $full_format;
+                        break 2;
+                    }
+                    $full_format = "{$time_format} {$date_format}";
+                    if ($this->validateDate($date, $full_format)) {
+                        $guess[$date] = $full_format;
+                        break 2;
+                    }
+                }
+            }
+        }
+
+        return $guess[$date];
+    }
+
+    /**
+     * @param string $date
+     * @param string $format
+     * @return bool
+     */
+    public function validateDate($date, $format)
+    {
+        $d = DateTime::createFromFormat($format, $date);
+
+        return $d && $d->format($format) == $date;
+    }
+
+    /**
+     * @param string $php_format
+     * @return string
+     */
+    public function dateformatToMomentJS($php_format)
+    {
+        $SYMBOLS_MATCHING = [
+            // Day
+            'd' => 'DD',
+            'D' => 'ddd',
+            'j' => 'D',
+            'l' => 'dddd',
+            'N' => 'E',
+            'S' => 'Do',
+            'w' => 'd',
+            'z' => 'DDD',
+            // Week
+            'W' => 'W',
+            // Month
+            'F' => 'MMMM',
+            'm' => 'MM',
+            'M' => 'MMM',
+            'n' => 'M',
+            't' => '',
+            // Year
+            'L' => '',
+            'o' => 'GGGG',
+            'Y' => 'YYYY',
+            'y' => 'yy',
+            // Time
+            'a' => 'a',
+            'A' => 'A',
+            'B' => 'SSS',
+            'g' => 'h',
+            'G' => 'H',
+            'h' => 'hh',
+            'H' => 'HH',
+            'i' => 'mm',
+            's' => 'ss',
+            'u' => '',
+            // Timezone
+            'e' => '',
+            'I' => '',
+            'O' => 'ZZ',
+            'P' => 'Z',
+            'T' => 'z',
+            'Z' => '',
+            // Full Date/Time
+            'c' => '',
+            'r' => 'llll ZZ',
+            'U' => 'X'
+        ];
+        $js_format        = '';
+        $escaping         = false;
+        $len = strlen($php_format);
+        for ($i = 0; $i < $len; $i++) {
+            $char = $php_format[$i];
+            if ($char === '\\') // PHP date format escaping character
+            {
+                $i++;
+                if ($escaping) {
+                    $js_format .= $php_format[$i];
+                } else {
+                    $js_format .= '\'' . $php_format[$i];
+                }
+                $escaping = true;
+            } else {
+                if ($escaping) {
+                    $js_format .= "'";
+                    $escaping = false;
+                }
+                if (isset($SYMBOLS_MATCHING[$char])) {
+                    $js_format .= $SYMBOLS_MATCHING[$char];
+                } else {
+                    $js_format .= $char;
+                }
+            }
+        }
+
+        return $js_format;
+    }
+
+    /**
+     * Gets the entire permissions array
+     *
+     * @return array
+     * @deprecated 1.10 Use $grav['permissions']->getInstances() instead.
+     */
+    public function getPermissions()
+    {
+        user_error(__METHOD__ . '() is deprecated since Admin 1.10, use $grav[\'permissions\']->getInstances() instead', E_USER_DEPRECATED);
+
+        $grav = $this->grav;
+        /** @var Permissions $permissions */
+        $permissions = $grav['permissions'];
+
+        return array_fill_keys(array_keys($permissions->getInstances()), 'boolean');
+    }
+
+    /**
+     * Sets the entire permissions array
+     *
+     * @param array $permissions
+     * @deprecated 1.10 Use PermissionsRegisterEvent::class event instead.
+     */
+    public function setPermissions($permissions)
+    {
+        user_error(__METHOD__ . '() is deprecated since Admin 1.10, use PermissionsRegisterEvent::class event instead', E_USER_DEPRECATED);
+
+        $this->addPermissions($permissions);
+    }
+
+    /**
+     * Adds a permission to the permissions array
+     *
+     * @param array $permissions
+     * @deprecated 1.10 Use RegisterPermissionsEvent::class event instead.
+     */
+    public function addPermissions($permissions)
+    {
+        user_error(__METHOD__ . '() is deprecated since Admin 1.10, use RegisterPermissionsEvent::class event instead', E_USER_DEPRECATED);
+
+        $grav = $this->grav;
+        /** @var Permissions $object */
+        $object = $grav['permissions'];
+        foreach ($permissions as $name => $type) {
+            if (!$object->hasAction($name)) {
+                $action = new Action($name);
+                $object->addAction($action);
+            }
+        }
+    }
+
+    public function getNotifications($force = false)
+    {
+        $last_checked = null;
+        $filename = $this->grav['locator']->findResource('user://data/notifications/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
+        $userStatus = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
+
+        $notifications_file = CompiledYamlFile::instance($filename);
+        $notifications_content = (array)$notifications_file->content();
+
+        $userStatus_file = CompiledYamlFile::instance($userStatus);
+        $userStatus_content = (array)$userStatus_file->content();
+
+        $last_checked = $notifications_content['last_checked'] ?? null;
+        $notifications = $notifications_content['data'] ?? array();
+        $timeout = $this->grav['config']->get('system.session.timeout', 1800);
+
+        if ($force || !$last_checked || empty($notifications) || (time() - $last_checked > $timeout)) {
+            $body = Response::get('https://getgrav.org/notifications.json?' . time());
+//            $body = Response::get('http://localhost/notifications.json?' . time());
+            $notifications = json_decode($body, true);
+
+            // Sort by date
+            usort($notifications, function ($a, $b) {
+                return strcmp($a['date'], $b['date']);
+            });
+
+            // Reverse order and create a new array
+            $notifications = array_reverse($notifications);
+            $cleaned_notifications = [];
+
+            foreach ($notifications as $key => $notification) {
+
+                if (isset($notification['permissions']) && !$this->authorize($notification['permissions'])) {
+                    continue;
+                }
+
+                if (isset($notification['dependencies'])) {
+                    foreach ($notification['dependencies'] as $dependency => $constraints) {
+                        if ($dependency === 'grav') {
+                            if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
+                                continue 2;
+                            }
+                        } else {
+                            $packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
+                            if (!isset($packages[$dependency])) {
+                                continue 2;
+                            } else {
+                                $version = $packages[$dependency]['version'];
+                                if (!Semver::satisfies($version, $constraints)) {
+                                    continue 2;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                $cleaned_notifications[] = $notification;
+
+            }
+
+            // reset notifications
+            $notifications = [];
+
+            foreach($cleaned_notifications as $notification) {
+                foreach ($notification['location'] as $location) {
+                    $notifications = array_merge_recursive($notifications, [$location => [$notification]]);
+                }
+            }
+
+
+            $notifications_file->content(['last_checked' => time(), 'data' => $notifications]);
+            $notifications_file->save();
+        }
+
+        foreach ($notifications as $location => $list) {
+            $notifications[$location] = array_filter($list, function ($notification) use ($userStatus_content) {
+                $element      = $userStatus_content[$notification['id']] ?? null;
+                if (isset($element)) {
+                    if (isset($notification['reappear_after'])) {
+                        $now = new \DateTime();
+                        $hidden_on = new \DateTime($element);
+                        $hidden_on->modify($notification['reappear_after']);
+
+                        if ($now >= $hidden_on) {
+                            return true;
+                        }
+                    }
+
+                    return false;
+                }
+
+                return true;
+            });
+        }
+
+
+        return $notifications;
+    }
+
+    /**
+     * Get https://getgrav.org news feed
+     *
+     * @return mixed
+     * @throws MalformedXmlException
+     */
+    public function getFeed($force = false)
+    {
+        $last_checked = null;
+        $filename = $this->grav['locator']->findResource('user://data/feed/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
+
+        $feed_file = CompiledYamlFile::instance($filename);
+        $feed_content = (array)$feed_file->content();
+
+        $last_checked = $feed_content['last_checked'] ?? null;
+        $feed = $feed_content['data'] ?? array();
+        $timeout = $this->grav['config']->get('system.session.timeout', 1800);
+
+        if ($force || !$last_checked || empty($feed) || ($last_checked && (time() - $last_checked > $timeout))) {
+            $feed_url = 'https://getgrav.org/blog.atom';
+            $body = Response::get($feed_url);
+
+            $reader = new Reader();
+            $parser = $reader->getParser($feed_url, $body, 'utf-8');
+            $data = $parser->execute()->getItems();
+
+            // Get top 10
+            $data = array_slice($data, 0, 10);
+
+            $feed = array_map(function ($entry) {
+                $simple_entry['title'] = $entry->getTitle();
+                $simple_entry['url'] = $entry->getUrl();
+                $simple_entry['date'] = $entry->getDate()->getTimestamp();
+                $simple_entry['nicetime'] = $this->adminNiceTime($simple_entry['date']);
+                return $simple_entry;
+            }, $data);
+
+            $feed_file->content(['last_checked' => time(), 'data' => $feed]);
+            $feed_file->save();
+        }
+
+        return $feed;
+
+    }
+
+    public function adminNiceTime($date, $long_strings = true)
+    {
+        if (empty($date)) {
+            return $this->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null);
+        }
+
+        if ($long_strings) {
+            $periods = [
+                'NICETIME.SECOND',
+                'NICETIME.MINUTE',
+                'NICETIME.HOUR',
+                'NICETIME.DAY',
+                'NICETIME.WEEK',
+                'NICETIME.MONTH',
+                'NICETIME.YEAR',
+                'NICETIME.DECADE'
+            ];
+        } else {
+            $periods = [
+                'NICETIME.SEC',
+                'NICETIME.MIN',
+                'NICETIME.HR',
+                'NICETIME.DAY',
+                'NICETIME.WK',
+                'NICETIME.MO',
+                'NICETIME.YR',
+                'NICETIME.DEC'
+            ];
+        }
+
+        $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
+
+        $now = time();
+
+        // check if unix timestamp
+        if ((string)(int)$date === (string)$date) {
+            $unix_date = $date;
+        } else {
+            $unix_date = strtotime($date);
+        }
+
+        // check validity of date
+        if (empty($unix_date)) {
+            return $this->translate('GRAV.NICETIME.BAD_DATE', null);
+        }
+
+        // is it future date or past date
+        if ($now > $unix_date) {
+            $difference = $now - $unix_date;
+            $tense      = $this->translate('GRAV.NICETIME.AGO', null);
+
+        } else {
+            $difference = $unix_date - $now;
+            $tense      = $this->translate('GRAV.NICETIME.FROM_NOW', null);
+        }
+
+        $len = count($lengths) - 1;
+        for ($j = 0; $difference >= $lengths[$j] && $j < $len; $j++) {
+            $difference /= $lengths[$j];
+        }
+
+        $difference = round($difference);
+
+        if ($difference !== 1) {
+            $periods[$j] .= '_PLURAL';
+        }
+
+        if ($this->grav['language']->getTranslation($this->grav['user']->language,
+            $periods[$j] . '_MORE_THAN_TWO')
+        ) {
+            if ($difference > 2) {
+                $periods[$j] .= '_MORE_THAN_TWO';
+            }
+        }
+
+        $periods[$j] = $this->translate('GRAV.'.$periods[$j], null);
+
+        return "{$difference} {$periods[$j]} {$tense}";
+    }
+
+    public function findFormFields($type, $fields, $found_fields = [])
+    {
+        foreach ($fields as $key => $field) {
+
+            if (isset($field['type']) && $field['type'] == $type) {
+                $found_fields[$key] = $field;
+            } elseif (isset($field['fields'])) {
+                $result = $this->findFormFields($type, $field['fields'], $found_fields);
+                if (!empty($result)) {
+                    $found_fields = array_merge($found_fields, $result);
+                }
+            }
+        }
+
+        return $found_fields;
+    }
+
+    public function getPagePathFromToken($path, $page = null)
+    {
+        return Utils::getPagePathFromToken($path, $page ?: $this->page(true));
+    }
+
+    /**
+     * Returns edited page.
+     *
+     * @param bool $route
+     *
+     * @param null $path
+     *
+     * @return PageInterface
+     */
+    public function page($route = false, $path = null)
+    {
+        if (!$path) {
+            $path = $this->route;
+        }
+
+        if ($route && !$path) {
+            $path = '/';
+        }
+
+        if (!isset($this->pages[$path])) {
+            $this->pages[$path] = $this->getPage($path);
+        }
+
+        return $this->pages[$path];
+    }
+
+    /**
+     * Returns the page creating it if it does not exist.
+     *
+     * @param string $path
+     *
+     * @return PageInterface|null
+     */
+    public function getPage($path)
+    {
+        $pages = static::enablePages();
+
+        if ($path && $path[0] !== '/') {
+            $path = "/{$path}";
+        }
+
+        // Fix for entities in path causing looping...
+        $path = urldecode($path);
+
+        $page = $path ? $pages->find($path, true) : $pages->root();
+
+        if (!$page) {
+            $slug = Utils::basename($path);
+
+            if ($slug === '') {
+                return null;
+            }
+
+            $ppath = str_replace('\\', '/', dirname($path));
+
+            // Find or create parent(s).
+            $parent = $this->getPage($ppath !== '/' ? $ppath : '');
+
+            // Create page.
+            $page = new Page();
+            $page->parent($parent);
+            $page->filePath($parent->path() . '/' . $slug . '/' . $page->name());
+
+            // Add routing information.
+            $pages->addPage($page, $path);
+
+            // Set if Modular
+            $page->modularTwig($slug[0] === '_');
+
+            // Determine page type.
+            if (isset($this->session->{$page->route()})) {
+                // Found the type and header from the session.
+                $data = $this->session->{$page->route()};
+
+                // Set the key header value
+                $header = ['title' => $data['title']];
+
+                if (isset($data['visible'])) {
+                    if ($data['visible'] === '' || $data['visible']) {
+                        // if auto (ie '')
+                        $pageParent = $page->parent();
+                        $children = $pageParent ? $pageParent->children() : [];
+                        foreach ($children as $child) {
+                            if ($child->order()) {
+                                // set page order
+                                $page->order(AdminController::getNextOrderInFolder($pageParent->path()));
+                                break;
+                            }
+                        }
+                    }
+                    if ((int)$data['visible'] === 1 && !$page->order()) {
+                        $header['visible'] = $data['visible'];
+                    }
+
+                }
+
+                if ($data['name'] === 'modular') {
+                    $header['body_classes'] = 'modular';
+                }
+
+                $name = $page->isModule() ? str_replace('modular/', '', $data['name']) : $data['name'];
+                $page->name($name . '.md');
+
+                // Fire new event to allow plugins to manipulate page frontmatter
+                $this->grav->fireEvent('onAdminCreatePageFrontmatter', new Event(['header' => &$header,
+                        'data' => $data]));
+
+                $page->header($header);
+                $page->frontmatter(Yaml::dump((array)$page->header(), 20));
+            } else {
+                // Find out the type by looking at the parent.
+                $type = $parent->childType() ?: $parent->blueprints()->get('child_type', 'default');
+                $page->name($type . CONTENT_EXT);
+                $page->header();
+            }
+        }
+
+        return $page;
+    }
+
+    public function generateReports()
+    {
+        $reports = new ArrayCollection();
+
+        $pages = static::enablePages();
+
+        // Default to XSS Security Report
+        $result = Security::detectXssFromPages($pages, true);
+
+        $reports['Grav Security Check'] = $this->grav['twig']->processTemplate('reports/security.html.twig', [
+            'result' => $result,
+        ]);
+
+        // Linting Issues
+
+        $result = YamlLinter::lint();
+
+        $reports['Grav Yaml Linter'] = $this->grav['twig']->processTemplate('reports/yamllinter.html.twig', [
+           'result' => $result,
+        ]);
+
+        // Fire new event to allow plugins to manipulate page frontmatter
+        $this->grav->fireEvent('onAdminGenerateReports', new Event(['reports' => $reports]));
+
+        return $reports;
+    }
+
+    public function getRouteDetails()
+    {
+        return [$this->base, $this->location, $this->route];
+    }
+
+    /**
+     * Get the files list
+     *
+     * @param bool $filtered
+     * @param int $page_index
+     * @return array|null
+     * @todo allow pagination
+     */
+    public function files($filtered = true, $page_index = 0)
+    {
+        $param_type = $this->grav['uri']->param('type');
+        $param_date = $this->grav['uri']->param('date');
+        $param_page = $this->grav['uri']->param('page');
+        $param_page = str_replace('\\', '/', $param_page);
+
+        $files_cache_key = 'media-manager-files';
+
+        if ($param_type) {
+            $files_cache_key .= "-{$param_type}";
+        }
+        if ($param_date) {
+            $files_cache_key .= "-{$param_date}";
+        }
+        if ($param_page) {
+            $files_cache_key .= "-{$param_page}";
+        }
+
+        $page_files = null;
+
+        $cache_enabled = $this->grav['config']->get('plugins.admin.cache_enabled');
+        if (!$cache_enabled) {
+            $this->grav['cache']->setEnabled(true);
+        }
+
+        $page_files = $this->grav['cache']->fetch(md5($files_cache_key));
+
+        if (!$cache_enabled) {
+            $this->grav['cache']->setEnabled(false);
+        }
+
+        if (!$page_files) {
+            $page_files = [];
+            $pages = static::enablePages();
+
+            if ($param_page) {
+                $page = $pages->find($param_page);
+
+                $page_files = $this->getFiles('images', $page, $page_files, $filtered);
+                $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
+                $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
+                $page_files = $this->getFiles('files', $page, $page_files, $filtered);
+            } else {
+                $allPages = $pages->all();
+
+                if ($allPages) foreach ($allPages as $page) {
+                    $page_files = $this->getFiles('images', $page, $page_files, $filtered);
+                    $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
+                    $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
+                    $page_files = $this->getFiles('files', $page, $page_files, $filtered);
+                }
+            }
+
+            if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
+                $this->shouldLoadAdditionalFilesInBackground(true);
+            }
+
+            if (!$cache_enabled) {
+                $this->grav['cache']->setEnabled(true);
+            }
+            $this->grav['cache']->save(md5($files_cache_key), $page_files, 600); //cache for 10 minutes
+            if (!$cache_enabled) {
+                $this->grav['cache']->setEnabled(false);
+            }
+
+        }
+
+        if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
+            $page_files = array_slice($page_files, $page_index * self::MEDIA_PAGINATION_INTERVAL, self::MEDIA_PAGINATION_INTERVAL);
+        }
+
+        return $page_files;
+    }
+
+    public function shouldLoadAdditionalFilesInBackground($status = null)
+    {
+        if ($status) {
+            $this->load_additional_files_in_background = true;
+        }
+
+        return $this->load_additional_files_in_background;
+    }
+
+    public function loadAdditionalFilesInBackground($status = null)
+    {
+        if (!$this->loading_additional_files_in_background) {
+            $this->loading_additional_files_in_background = true;
+            $this->files(false, false);
+            $this->shouldLoadAdditionalFilesInBackground(false);
+            $this->loading_additional_files_in_background = false;
+        }
+    }
+
+    private function getFiles($type, $page, $page_files, $filtered)
+    {
+        $page_files = $this->getMediaOfType($type, $page, $page_files);
+
+        if ($filtered) {
+            $page_files = $this->filterByType($page_files);
+            $page_files = $this->filterByDate($page_files);
+        }
+
+        return $page_files;
+    }
+
+    /**
+     * Get all the media of a type ('images' | 'audios' | 'videos' | 'files')
+     *
+     * @param string $type
+     * @param PageInterface|null $page
+     * @param array $files
+     *
+     * @return array
+     */
+    private function getMediaOfType($type, ?PageInterface $page, array $files)
+    {
+        if ($page) {
+            $media = $page->media();
+            $mediaOfType = $media->$type();
+
+            foreach($mediaOfType as $title => $file) {
+                $files[] = [
+                    'title' => $title,
+                    'type' => $type,
+                    'page_route' => $page->route(),
+                    'file' => $file->higherQualityAlternative()
+                ];
+            }
+
+            return $files;
+        }
+
+        return [];
+    }
+
+    /**
+     * Filter media by type
+     *
+     * @param array $filesFiltered
+     *
+     * @return array
+     */
+    private function filterByType($filesFiltered)
+    {
+        $filter_type = $this->grav['uri']->param('type');
+        if (!$filter_type) {
+            return $filesFiltered;
+        }
+
+        $filesFiltered = array_filter($filesFiltered, function ($file) use ($filter_type) {
+            return $file['type'] == $filter_type;
+        });
+
+        return $filesFiltered;
+    }
+
+    /**
+     * Filter media by date
+     *
+     * @param array $filesFiltered
+     *
+     * @return array
+     */
+    private function filterByDate($filesFiltered)
+    {
+        $filter_date = $this->grav['uri']->param('date');
+        if (!$filter_date) {
+            return $filesFiltered;
+        }
+
+        $year = substr($filter_date, 0, 4);
+        $month = substr($filter_date, 5, 2);
+
+        $filesFilteredByDate = [];
+
+        foreach($filesFiltered as $file) {
+            $filedate = $this->fileDate($file['file']);
+            $fileYear = $filedate->format('Y');
+            $fileMonth = $filedate->format('m');
+
+            if ($fileYear == $year && $fileMonth == $month) {
+                $filesFilteredByDate[] = $file;
+            }
+        }
+
+        return $filesFilteredByDate;
+    }
+
+    /**
+     * Return the DateTime object representation of a file modified date
+     *
+     * @param File $file
+     *
+     * @return DateTime
+     */
+    private function fileDate($file) {
+        $datetime = new \DateTime();
+        $datetime->setTimestamp($file->toArray()['modified']);
+        return $datetime;
+    }
+
+    /**
+     * Get the files dates list to be used in the Media Files filter
+     *
+     * @return array
+     */
+    public function filesDates()
+    {
+        $files = $this->files(false);
+        $dates = [];
+
+        foreach ($files as $file) {
+            $datetime = $this->fileDate($file['file']);
+            $year = $datetime->format('Y');
+            $month = $datetime->format('m');
+
+            if (!isset($dates[$year])) {
+                $dates[$year] = [];
+            }
+
+            if (!isset($dates[$year][$month])) {
+                $dates[$year][$month] = 1;
+            } else {
+                $dates[$year][$month]++;
+            }
+        }
+
+        return $dates;
+    }
+
+    /**
+     * Get the pages list to be used in the Media Files filter
+     *
+     * @return array
+     */
+    public function pages()
+    {
+        $pages = static::enablePages();
+
+        $collection = $pages->all();
+
+        $pagesWithFiles = [];
+        foreach ($collection as $page) {
+            if (count($page->media()->all())) {
+                $pagesWithFiles[] = $page;
+            }
+        }
+
+        return $pagesWithFiles;
+    }
+
+    /**
+     * @return Pages
+     */
+    public static function enablePages()
+    {
+        static $pages;
+
+        if ($pages) {
+            return $pages;
+        }
+
+        $grav = Grav::instance();
+        $admin = $grav['admin'];
+
+        /** @var Pages $pages */
+        $pages = Grav::instance()['pages'];
+        $pages->enablePages();
+
+        // If page is null, the default page does not exist, and we cannot route to it
+        $page = $pages->find('/', true);
+        if ($page) {
+            // Set original route for the home page.
+            $home = '/' . trim($grav['config']->get('system.home.alias'), '/');
+
+            $page->route($home);
+        }
+
+        $admin->routes = $pages->routes();
+
+        // Remove default route from routes.
+        if (isset($admin->routes['/'])) {
+            unset($admin->routes['/']);
+        }
+
+        return $pages;
+    }
+
+    /**
+     * Return HTTP_REFERRER if set
+     *
+     * @return null
+     */
+    public function getReferrer()
+    {
+        return $_SERVER['HTTP_REFERER'] ?? null;
+    }
+
+
+    /**
+     * Get Grav system log files
+     *
+     * @return array
+     */
+    public function getLogFiles()
+    {
+        $logs = new GravData(['grav.log' => 'Grav System Log', 'email.log' => 'Email Log']);
+        Grav::instance()->fireEvent('onAdminLogFiles', new Event(['logs' => &$logs]));
+        return $logs->toArray();
+    }
+
+    /**
+     * Get changelog for a given GPM package based on slug
+     *
+     * @param string|null $slug
+     * @return array
+     */
+    public function getChangelog($slug = null)
+    {
+        $gpm = $this->gpm();
+        $changelog = [];
+
+        if (!empty($slug)) {
+            $package = $gpm->findPackage($slug);
+        } else {
+            $package = $gpm->grav;
+        }
+
+
+        if ($package) {
+            $changelog = $package->getChangelog();
+        }
+
+        return $changelog;
+    }
+
+    /**
+     * Prepare and return POST data.
+     *
+     * @param array $post
+     * @return array
+     */
+    public function preparePost($post): array
+    {
+        if (!is_array($post)) {
+            return [];
+        }
+
+        unset($post['task']);
+
+        // Decode JSON encoded fields and merge them to data.
+        if (isset($post['_json'])) {
+            $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
+            unset($post['_json']);
+        }
+
+        return $this->cleanDataKeys($post);
+    }
+
+    /**
+     * Recursively JSON decode data.
+     *
+     * @param array $data
+     * @return array
+     * @throws JsonException
+     */
+    private function jsonDecode(array $data): array
+    {
+        foreach ($data as &$value) {
+            if (is_array($value)) {
+                $value = $this->jsonDecode($value);
+            } else {
+                $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * @param array $source
+     * @return array
+     */
+    private function cleanDataKeys(array $source): array
+    {
+        $out = [];
+        foreach ($source as $key => $value) {
+            $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
+            if (is_array($value)) {
+                $out[$key] = $this->cleanDataKeys($value);
+            } else {
+                $out[$key] = $value;
+            }
+        }
+
+        return $out;
+    }
+}

+ 1174 - 0
plugins/admin/classes/plugin/AdminBaseController.php

@@ -0,0 +1,1174 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Cache;
+use Grav\Common\Config\Config;
+use Grav\Common\Data\Data;
+use Grav\Common\Debugger;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Grav;
+use Grav\Common\Media\Interfaces\MediaInterface;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Media;
+use Grav\Common\Security;
+use Grav\Common\Uri;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Common\Plugin;
+use Grav\Common\Theme;
+use Grav\Framework\Controller\Traits\ControllerResponseTrait;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use JsonException;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\File\File;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+/**
+ * Class AdminController
+ *
+ * @package Grav\Plugin
+ */
+class AdminBaseController
+{
+    use ControllerResponseTrait;
+
+    /** @var Grav */
+    public $grav;
+    /** @var string */
+    public $view;
+    /** @var string */
+    public $task;
+    /** @var string */
+    public $route;
+    /** @var array */
+    public $post;
+    /** @var array|null */
+    public $data;
+    /** @var array */
+    public $blacklist_views = [];
+
+    /** @var Uri */
+    protected $uri;
+    /** @var Admin */
+    protected $admin;
+    /** @var string */
+    protected $redirect;
+    /** @var int */
+    protected $redirectCode;
+
+    /** @var string[] */
+    protected $upload_errors = [
+        0 => 'There is no error, the file uploaded with success',
+        1 => 'The uploaded file exceeds the max upload size',
+        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML',
+        3 => 'The uploaded file was only partially uploaded',
+        4 => 'No file was uploaded',
+        6 => 'Missing a temporary folder',
+        7 => 'Failed to write file to disk',
+        8 => 'A PHP extension stopped the file upload'
+    ];
+
+    /**
+     * Performs a task.
+     *
+     * @return bool True if the action was performed successfully.
+     */
+    public function execute()
+    {
+        if (null === $this->admin) {
+            $this->admin = $this->grav['admin'];
+        }
+
+        // Ignore blacklisted views.
+        if (in_array($this->view, $this->blacklist_views, true)) {
+            return false;
+        }
+
+        // Make sure that user is logged into admin.
+        if (!$this->admin->authorize()) {
+            return false;
+        }
+
+        // Always validate nonce.
+        if (!$this->validateNonce()) {
+            return false;
+        }
+
+        $method = 'task' . ucfirst($this->task);
+
+        if (method_exists($this, $method)) {
+            try {
+                $response = $this->{$method}();
+            } catch (RequestException $e) {
+                /** @var Debugger $debugger */
+                $debugger = $this->grav['debugger'];
+                $debugger->addException($e);
+
+                $response = $this->createErrorResponse($e);
+            } catch (\RuntimeException $e) {
+                /** @var Debugger $debugger */
+                $debugger = $this->grav['debugger'];
+                $debugger->addException($e);
+
+                $response = true;
+                $this->admin->setMessage($e->getMessage(), 'error');
+            }
+        } else {
+            $response = $this->grav->fireEvent('onAdminTaskExecute',
+                new Event(['controller' => $this, 'method' => $method]));
+        }
+
+        if ($response instanceof ResponseInterface) {
+            $this->close($response);
+        }
+
+        // Grab redirect parameter.
+        $redirect = $this->post['_redirect'] ?? null;
+        unset($this->post['_redirect']);
+
+        // Redirect if requested.
+        if ($redirect) {
+            $this->setRedirect($redirect);
+        }
+
+        return $response;
+    }
+
+    protected function validateNonce()
+    {
+        if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
+            if (isset($this->post['admin-nonce'])) {
+                $nonce = $this->post['admin-nonce'];
+            } else {
+                $nonce = $this->grav['uri']->param('admin-nonce');
+            }
+
+            if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
+                if ($this->task === 'addmedia') {
+
+                    $message = sprintf($this->admin::translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null),
+                        ini_get('post_max_size'));
+
+                    //In this case it's more likely that the image is too big than POST can handle. Show message
+                    $this->admin->json_response = [
+                        'status'  => 'error',
+                        'message' => $message
+                    ];
+
+                    return false;
+                }
+
+                $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
+                ];
+
+                return false;
+            }
+            unset($this->post['admin-nonce']);
+        } else {
+            if ($this->task === 'logout') {
+                $nonce = $this->grav['uri']->param('logout-nonce');
+                if (null === $nonce || !Utils::verifyNonce($nonce, 'logout-form')) {
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
+                        'error');
+                    $this->admin->json_response = [
+                        'status'  => 'error',
+                        'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
+                    ];
+
+                    return false;
+                }
+            } else {
+                $nonce = $this->grav['uri']->param('admin-nonce');
+                if (null === $nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
+                        'error');
+                    $this->admin->json_response = [
+                        'status'  => 'error',
+                        'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
+                    ];
+
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Sets the page redirect.
+     *
+     * @param string $path The path to redirect to
+     * @param int    $code The HTTP redirect code
+     * @return void
+     */
+    public function setRedirect($path, $code = 303)
+    {
+        $this->redirect     = $path;
+        $this->redirectCode = $code;
+    }
+
+    /**
+     * Sends JSON response and terminates the call.
+     *
+     * @param array $json
+     * @param int $code
+     * @return never-return
+     */
+    protected function sendJsonResponse(array $json, $code = 200): void
+    {
+        // JSON response.
+        $response = $this->createJsonResponse($json, $code);
+
+        $this->close($response);
+    }
+
+    /**
+     * @param ResponseInterface $response
+     * @return never-return
+     */
+    protected function close(ResponseInterface $response): void
+    {
+        $this->grav->close($response);
+    }
+
+    /**
+     * Handles ajax upload for files.
+     * Stores in a flash object the temporary file and deals with potential file errors.
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskFilesUpload()
+    {
+        if (null === $_FILES || !$this->authorizeTask('upload file', $this->dataPermissions())) {
+            return false;
+        }
+
+        /** @var Config $config */
+        $config   = $this->grav['config'];
+        $data     = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
+        $settings = $data->blueprints()->schema()->getProperty($this->post['name']);
+        $settings = (object)array_merge([
+            'avoid_overwriting' => false,
+            'random_name'       => false,
+            'accept'            => ['image/*'],
+            'limit'             => 10,
+            'filesize'          => Utils::getUploadLimit()
+        ], (array)$settings, ['name' => $this->post['name']]);
+
+        $upload = $this->normalizeFiles($_FILES['data'], $settings->name);
+
+        $filename = $upload->file->name;
+
+        // Handle bad filenames.
+        if (!Utils::checkFilename($filename)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
+            ];
+
+            return false;
+        }
+
+        if (!isset($settings->destination)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED', null)
+            ];
+
+            return false;
+        }
+
+        // Do not use self@ outside of pages
+        if ($this->view !== 'pages' && in_array($settings->destination, ['@self', 'self@', '@self@'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null),
+                    htmlspecialchars($settings->destination, ENT_QUOTES | ENT_HTML5, 'UTF-8'))
+            ];
+
+            return false;
+        }
+
+        // Handle errors and breaks without proceeding further
+        if ($upload->file->error !== UPLOAD_ERR_OK) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
+                    $this->upload_errors[$upload->file->error])
+            ];
+
+            return false;
+        }
+
+        // Handle file size limits
+        $settings->filesize *= 1048576; // 2^20 [MB in Bytes]
+        if ($settings->filesize > 0 && $upload->file->size > $settings->filesize) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
+            ];
+
+            return false;
+        }
+
+        // Handle Accepted file types
+        // Accept can only be mime types (image/png | image/*) or file extensions (.pdf|.jpg)
+        $accepted = false;
+        $errors   = [];
+
+        // Do not trust mimetype sent by the browser
+        $mime = Utils::getMimeByFilename($filename);
+
+        foreach ((array)$settings->accept as $type) {
+            // Force acceptance of any file when star notation
+            if ($type === '*') {
+                $accepted = true;
+                break;
+            }
+
+            $isMime = strstr($type, '/');
+            $find   = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
+
+            if ($isMime) {
+                $match = preg_match('#' . $find . '$#', $mime);
+                if (!$match) {
+                    $errors[] = htmlspecialchars('The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
+                } else {
+                    $accepted = true;
+                    break;
+                }
+            } else {
+                $match = preg_match('#' . $find . '$#', $filename);
+                if (!$match) {
+                    $errors[] = htmlspecialchars('The File Extension for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
+                } else {
+                    $accepted = true;
+                    break;
+                }
+            }
+        }
+
+        if (!$accepted) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => implode('<br />', $errors)
+            ];
+
+            return false;
+        }
+
+        // Remove the error object to avoid storing it
+        unset($upload->file->error);
+
+        // we need to move the file at this stage or else
+        // it won't be available upon save later on
+        // since php removes it from the upload location
+        $tmp_dir  = Admin::getTempDir();
+        $tmp_file = $upload->file->tmp_name;
+        $tmp      = $tmp_dir . '/uploaded-files/' . Utils::basename($tmp_file);
+
+        Folder::create(dirname($tmp));
+        if (!move_uploaded_file($tmp_file, $tmp)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => sprintf(
+                    $this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null),
+                    '',
+                    htmlspecialchars($tmp, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+                )
+            ];
+
+            return false;
+        }
+
+        // Special Sanitization for SVG
+        if (Utils::contains($mime, 'svg', false)) {
+            Security::sanitizeSVG($tmp);
+        }
+
+        $upload->file->tmp_name = $tmp;
+
+        // Retrieve the current session of the uploaded files for the field
+        // and initialize it if it doesn't exist
+        $sessionField = base64_encode($this->grav['uri']->url());
+        $flash        = $this->admin->session()->getFlashObject('files-upload') ?? [];
+        if (!isset($flash[$sessionField])) {
+            $flash[$sessionField] = [];
+        }
+        if (!isset($flash[$sessionField][$upload->field])) {
+            $flash[$sessionField][$upload->field] = [];
+        }
+
+        // Set destination
+        if ($this->grav['locator']->isStream($settings->destination)) {
+            $destination = $this->grav['locator']->findResource($settings->destination, false, true);
+        } else {
+            $destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
+            $destination = $this->admin->getPagePathFromToken($destination);
+        }
+
+        // Create destination if needed
+        if (!is_dir($destination)) {
+            Folder::mkdir($destination);
+        }
+
+        // Generate random name if required
+        if ($settings->random_name) { // TODO: document
+            $extension          = Utils::pathinfo($upload->file->name, PATHINFO_EXTENSION);
+            $upload->file->name = Utils::generateRandomString(15) . '.' . $extension;
+        }
+
+        // Handle conflicting name if needed
+        if ($settings->avoid_overwriting) { // TODO: document
+            if (file_exists($destination . '/' . $upload->file->name)) {
+                $upload->file->name = date('YmdHis') . '-' . $upload->file->name;
+            }
+        }
+
+        // Prepare object for later save
+        $path               = $destination . '/' . $upload->file->name;
+        $upload->file->path = $path;
+        // $upload->file->route = $page ? $path : null;
+
+        // Prepare data to be saved later
+        $flash[$sessionField][$upload->field][$path] = (array)$upload->file;
+
+        // Finally store the new uploaded file in the field session
+        $this->admin->session()->setFlashObject('files-upload', $flash);
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'session' => \json_encode([
+                'sessionField' => base64_encode($this->grav['uri']->url()),
+                'path'         => $upload->file->path,
+                'field'        => $settings->name
+            ])
+        ];
+
+        return true;
+    }
+
+    /**
+     * Checks if the user is allowed to perform the given task with its associated permissions
+     *
+     * @param string $task        The task to execute
+     * @param array  $permissions The permissions given
+     *
+     * @return bool True if authorized. False if not.
+     */
+    public function authorizeTask($task = '', $permissions = [])
+    {
+        if (!$this->admin->authorize($permissions)) {
+            if ($this->grav['uri']->extension() === 'json') {
+                $this->admin->json_response = [
+                    'status'  => 'unauthorized',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.'
+                ];
+            } else {
+                $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.',
+                    'error');
+            }
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Checks if the user is allowed to perform the given task with its associated permissions.
+     * Throws exception if the check fails.
+     *
+     * @param string $task        The task to execute
+     * @param array  $permissions The permissions given
+     * @throws RequestException
+     */
+    public function checkTaskAuthorization($task = '', $permissions = [])
+    {
+        if (!$this->admin->authorize($permissions)) {
+            throw new RequestException($this->getRequest(), $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.', 403);
+        }
+    }
+
+    /**
+     * Gets the permissions needed to access a given view
+     *
+     * @return array An array of permissions
+     */
+    protected function dataPermissions()
+    {
+        $type        = $this->view;
+        $permissions = ['admin.super'];
+
+        switch ($type) {
+            case 'config':
+                $type = $this->route ?: 'system';
+                $permissions[] = 'admin.configuration.' . $type;
+                break;
+            case 'plugins':
+                $permissions[] = 'admin.plugins';
+                break;
+            case 'themes':
+                $permissions[] = 'admin.themes';
+                break;
+            case 'users':
+                $permissions[] = 'admin.users';
+                break;
+            case 'user':
+                $permissions[] = 'admin.login';
+                $permissions[] = 'admin.users';
+                break;
+            case 'pages':
+                $permissions[] = 'admin.pages';
+                break;
+            default:
+                $permissions[] = 'admin.configuration.' . $type;
+                $permissions[] = 'admin.configuration_' . $type;
+        }
+
+        return $permissions;
+    }
+
+    /**
+     * Gets the configuration data for a given view & post
+     *
+     * @param array $data
+     *
+     * @return array
+     */
+    protected function prepareData(array $data)
+    {
+        return $data;
+    }
+
+    /**
+     * Internal method to normalize the $_FILES array
+     *
+     * @param array  $data $_FILES starting point data
+     * @param string $key
+     *
+     * @return object a new Object with a normalized list of files
+     */
+    protected function normalizeFiles($data, $key = '')
+    {
+        $files        = new \stdClass();
+        $files->field = $key;
+        $files->file  = new \stdClass();
+
+        foreach ($data as $fieldName => $fieldValue) {
+            // Since Files Upload are always happening via Ajax
+            // we are not interested in handling `multiple="true"`
+            // because they are always handled one at a time.
+            // For this reason we normalize the value to string,
+            // in case it is arriving as an array.
+            $value                     = (array)Utils::getDotNotation($fieldValue, $key);
+            $files->file->{$fieldName} = array_shift($value);
+        }
+
+        return $files;
+    }
+
+    /**
+     * Removes a file from the flash object session, before it gets saved
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskFilesSessionRemove()
+    {
+        if (!$this->authorizeTask('delete file', $this->dataPermissions())) {
+            return false;
+        }
+
+        // Retrieve the current session of the uploaded files for the field
+        // and initialize it if it doesn't exist
+        $sessionField = base64_encode($this->grav['uri']->url());
+        $request      = \json_decode($this->post['session']);
+
+        // Ensure the URI requested matches the current one, otherwise fail
+        if ($request->sessionField !== $sessionField) {
+            return false;
+        }
+
+        // Retrieve the flash object and remove the requested file from it
+        $flash    = $this->admin->session()->getFlashObject('files-upload') ?? [];
+        $endpoint = $flash[$request->sessionField][$request->field][$request->path] ?? null;
+
+        if (isset($endpoint)) {
+            if (file_exists($endpoint['tmp_name'])) {
+                unlink($endpoint['tmp_name']);
+            }
+
+            unset($endpoint);
+        }
+
+        // Walk backward to cleanup any empty field that's left
+        // Field
+        if (isset($flash[$request->sessionField][$request->field][$request->path])) {
+            unset($flash[$request->sessionField][$request->field][$request->path]);
+        }
+
+        // Field
+        if (isset($flash[$request->sessionField][$request->field]) && empty($flash[$request->sessionField][$request->field])) {
+            unset($flash[$request->sessionField][$request->field]);
+        }
+
+        // Session Field
+        if (isset($flash[$request->sessionField]) && empty($flash[$request->sessionField])) {
+            unset($flash[$request->sessionField]);
+        }
+
+
+        // If there's anything left to restore in the flash object, do so
+        if (count($flash)) {
+            $this->admin->session()->setFlashObject('files-upload', $flash);
+        }
+
+        $this->admin->json_response = ['status' => 'success'];
+
+        return true;
+    }
+
+    /**
+     * Redirect to the route stored in $this->redirect
+     *
+     * Route may or may not be prefixed by /en or /admin or /en/admin.
+     *
+     * @return void
+     */
+    public function redirect()
+    {
+        $this->admin->redirect($this->redirect, $this->redirectCode);
+    }
+
+    /**
+     * Prepare and return POST data.
+     *
+     * @param array $post
+     * @return array
+     */
+    protected function getPost($post)
+    {
+        if (!is_array($post)) {
+            return [];
+        }
+
+        unset($post['task']);
+
+        // Decode JSON encoded fields and merge them to data.
+        if (isset($post['_json'])) {
+            $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
+            unset($post['_json']);
+        }
+
+        return $this->cleanDataKeys($post);
+    }
+
+    /**
+     * Recursively JSON decode data.
+     *
+     * @param array $data
+     * @return array
+     * @throws JsonException
+     * @internal Do not use directly!
+     */
+    protected function jsonDecode(array $data): array
+    {
+        foreach ($data as &$value) {
+            if (is_array($value)) {
+                $value = $this->jsonDecode($value);
+            } else {
+                $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * @param array $source
+     * @return array
+     * @internal Do not use directly!
+     */
+    protected function cleanDataKeys(array $source): array
+    {
+        $out = [];
+        foreach ($source as $key => $value) {
+            $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
+            if (is_array($value)) {
+                $out[$key] = $this->cleanDataKeys($value);
+            } else {
+                $out[$key] = $value;
+            }
+        }
+
+        return $out;
+    }
+
+    /**
+     * Return true if multilang is active
+     *
+     * @return bool True if multilang is active
+     */
+    protected function isMultilang()
+    {
+        return count($this->grav['config']->get('system.languages.supported', [])) > 1;
+    }
+
+    /**
+     * @param PageInterface|UserInterface|Data $obj
+     *
+     * @return PageInterface|UserInterface|Data
+     */
+    protected function storeFiles($obj)
+    {
+        // Process previously uploaded files for the current URI
+        // and finally store them. Everything else will get discarded
+        $queue = $this->admin->session()->getFlashObject('files-upload');
+        if (is_array($queue)) {
+            $queue = $queue[base64_encode($this->grav['uri']->url())];
+            foreach ($queue as $key => $files) {
+                foreach ($files as $destination => $file) {
+                    if (!rename($file['tmp_name'], $destination)) {
+                        throw new \RuntimeException(sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE',
+                            null), '"' . $file['tmp_name'] . '"', $destination));
+                    }
+
+                    unset($files[$destination]['tmp_name']);
+                }
+
+                if ($this->view === 'pages') {
+                    $keys     = explode('.', preg_replace('/^header./', '', $key));
+                    $init_key = array_shift($keys);
+                    if (count($keys) > 0) {
+                        $new_data = $obj->header()->{$init_key} ?? [];
+                        Utils::setDotNotation($new_data, implode('.', $keys), $files, true);
+                    } else {
+                        $new_data = $files;
+                    }
+                    if (isset($obj->header()->{$init_key})) {
+                        $obj->modifyHeader($init_key,
+                            array_replace_recursive([], $obj->header()->{$init_key}, $new_data));
+                    } else {
+                        $obj->modifyHeader($init_key, $new_data);
+                    }
+                } elseif ($obj instanceof UserInterface and $key === 'avatar') {
+                    $obj->set($key, $files);
+                } else {
+                    // TODO: [this is JS handled] if it's single file, remove existing and use set, if it's multiple, use join
+                    $obj->join($key, $files); // stores
+                }
+
+            }
+        }
+
+        return $obj;
+    }
+
+    /**
+     * Used by the filepicker field to get a list of files in a folder.
+     *
+     * @return bool
+     */
+    protected function taskGetFilesInFolder()
+    {
+        if (!$this->authorizeTask('get files', $this->dataPermissions())) {
+            return false;
+        }
+
+        $data = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
+
+        if (null === $data) {
+            return false;
+        }
+
+        if (method_exists($data, 'blueprints')) {
+            $settings = $data->blueprints()->schema()->getProperty($this->post['name']);
+        } elseif (method_exists($data, 'getBlueprint')) {
+            $settings = $data->getBlueprint()->schema()->getProperty($this->post['name']);
+        }
+
+        if (isset($settings['folder'])) {
+            $folder = $settings['folder'];
+        } else {
+            $folder = 'self@';
+        }
+
+        // Do not use self@ outside of pages
+        if ($this->view !== 'pages' && in_array($folder, ['@self', 'self@', '@self@'])) {
+            if (!$data instanceof MediaInterface) {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
+                ];
+
+                return false;
+            }
+
+            $media = $data->getMedia();
+        } else {
+            /** @var UniformResourceLocator $locator */
+            $locator = $this->grav['locator'];
+            if ($locator->isStream($folder)) {
+                $folder = $locator->findResource($folder);
+            }
+
+            // Set destination
+            $folder = Folder::getRelativePath(rtrim($folder, '/'));
+            $folder = $this->admin->getPagePathFromToken($folder);
+
+            $media = new Media($folder);
+        }
+
+        $available_files = [];
+        $metadata = [];
+        $thumbs = [];
+
+
+        foreach ($media->all() as $name => $medium) {
+
+           $available_files[] = $name;
+
+            if (isset($settings['include_metadata'])) {
+                $img_metadata = $medium->metadata();
+                if ($img_metadata) {
+                    $metadata[$name] = $img_metadata;
+                }
+            }
+
+        }
+
+        // Peak in the flashObject for optimistic filepicker updates
+        $pending_files = [];
+        $sessionField  = base64_encode($this->grav['uri']->url());
+        $flash         = $this->admin->session()->getFlashObject('files-upload');
+
+        if ($flash && isset($flash[$sessionField])) {
+            foreach ($flash[$sessionField] as $field => $data) {
+                foreach ($data as $file) {
+                    if (dirname($file['path']) === $folder) {
+                        $pending_files[] = $file['name'];
+                    }
+                }
+            }
+        }
+
+        $this->admin->session()->setFlashObject('files-upload', $flash);
+
+        // Handle Accepted file types
+        // Accept can only be file extensions (.pdf|.jpg)
+        if (isset($settings['accept'])) {
+            $available_files = array_filter($available_files, function ($file) use ($settings) {
+                return $this->filterAcceptedFiles($file, $settings);
+            });
+
+            $pending_files = array_filter($pending_files, function ($file) use ($settings) {
+                return $this->filterAcceptedFiles($file, $settings);
+            });
+        }
+
+        // Generate thumbs if needed
+        if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
+            foreach ($available_files as $filename) {
+                $thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
+            }
+        }
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'files'   => array_values($available_files),
+            'pending' => array_values($pending_files),
+            'folder'  => $folder,
+            'metadata' => $metadata,
+            'thumbs' => $thumbs
+        ];
+
+        return true;
+    }
+
+    /**
+     * @param string $file
+     * @param array $settings
+     * @return false
+     */
+    protected function filterAcceptedFiles($file, $settings)
+    {
+        $valid = false;
+
+        foreach ((array)$settings['accept'] as $type) {
+            $find = str_replace('*', '.*', $type);
+            $valid |= preg_match('#' . $find . '$#i', $file);
+        }
+
+        return $valid;
+    }
+
+    /**
+     * Handle deleting a file from a blueprint
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskRemoveFileFromBlueprint()
+    {
+        if (!$this->authorizeTask('remove file', $this->dataPermissions())) {
+            return false;
+        }
+
+        /** @var Uri $uri */
+        $uri       = $this->grav['uri'];
+        $blueprint = base64_decode($uri->param('blueprint'));
+        $path      = base64_decode($uri->param('path'));
+        $route     = base64_decode($uri->param('proute'));
+        $type      = $uri->param('type');
+        $field     = $uri->param('field');
+
+        $filename  = Utils::basename($this->post['filename'] ?? '');
+        if ($filename === '') {
+           $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => 'Filename is empty'
+            ];
+
+            return false;
+        }
+
+        // Get Blueprint
+        if ($type === 'pages' || strpos($blueprint, 'pages/') === 0) {
+            $page = $this->admin->page(true, $route);
+            if (!$page) {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => 'Page not found'
+                ];
+
+                return false;
+            }
+            $blueprints = $page->blueprints();
+            $path = Folder::getRelativePath($page->path());
+            $settings = (object)$blueprints->schema()->getProperty($field);
+        } else {
+            $page = null;
+            if ($type === 'themes' || $type === 'plugins') {
+                $obj = $this->grav[$type]->get(Utils::substrToString($blueprint, '/')); //here
+                $settings = (object) $obj->blueprints()->schema()->getProperty($field);
+            } else {
+                $settings = (object)$this->admin->blueprints($blueprint)->schema()->getProperty($field);
+            }
+        }
+
+        // Get destination
+        if ($this->grav['locator']->isStream($settings->destination)) {
+            $destination = $this->grav['locator']->findResource($settings->destination, false, true);
+
+        } else {
+            $destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
+            $destination = $this->admin->getPagePathFromToken($destination, $page);
+        }
+
+        // Not in path
+        if (!Utils::startsWith($path, $destination)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => 'Path not valid for this data type'
+            ];
+
+            return false;
+        }
+
+        // Only remove files from correct destination...
+        $this->taskRemoveMedia($destination . '/' . $filename);
+
+        if ($page) {
+            $keys = explode('.', preg_replace('/^header./', '', $field));
+            $header = (array)$page->header();
+            $data_path = implode('.', $keys);
+            $data = Utils::getDotNotation($header, $data_path);
+
+            if (isset($data[$path])) {
+                unset($data[$path]);
+                Utils::setDotNotation($header, $data_path, $data);
+                $page->header($header);
+            }
+
+            $page->save();
+        } elseif ($type === 'user') {
+            $user = Grav::instance()['user'];
+            unset($user->avatar);
+            $user->save();
+        } else {
+
+            $blueprint_prefix = $type === 'config' ? '' : $type . '.';
+            $blueprint_name   = str_replace(['config/', '/blueprints'], '', $blueprint);
+            $blueprint_field  = $blueprint_prefix . $blueprint_name . '.' . $field;
+
+            $files            = $this->grav['config']->get($blueprint_field);
+
+            if ($files) {
+                foreach ($files as $key => $value) {
+                    if ($key == $path) {
+                        unset($files[$key]);
+                    }
+                }
+            }
+
+            $this->grav['config']->set($blueprint_field, $files);
+
+            switch ($type) {
+                case 'config':
+                    $data   = $this->grav['config']->get($blueprint_name);
+                    $config = $this->admin->data($blueprint, $data);
+                    $config->save();
+                    break;
+                case 'themes':
+                    Theme::saveConfig($blueprint_name);
+                    break;
+                case 'plugins':
+                    Plugin::saveConfig($blueprint_name);
+                    break;
+            }
+        }
+
+        Cache::clearCache('invalidate');
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
+        ];
+
+        return true;
+    }
+
+    /**
+     * Handles removing a media file
+     *
+     * @note This task cannot be used anymore.
+     *
+     * @return bool True if the action was performed
+     */
+    public function taskRemoveMedia($filename = null)
+    {
+        if (!$this->canEditMedia()) {
+            return false;
+        }
+
+        if (null === $filename) {
+            throw new \RuntimeException('Admin task RemoveMedia has been disabled.');
+        }
+
+        $file                  = File::instance($filename);
+        $resultRemoveMedia     = false;
+
+        if ($file->exists()) {
+            $resultRemoveMedia = $file->delete();
+
+            $fileParts = Utils::pathinfo($filename);
+
+            foreach (scandir($fileParts['dirname']) as $file) {
+                $regex_pattern = '/' . preg_quote($fileParts['filename'], '/') . "@\d+x\." . $fileParts['extension'] . "(?:\.meta\.yaml)?$|" . preg_quote($fileParts['basename'], '/') . "\.meta\.yaml$/";
+                if (preg_match($regex_pattern, $file)) {
+                    $path = $fileParts['dirname'] . '/' . $file;
+                    @unlink($path);
+                }
+            }
+
+        }
+
+        if ($resultRemoveMedia) {
+            if ($this->grav['uri']->extension() === 'json') {
+                $this->admin->json_response = [
+                    'status'  => 'success',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
+                ];
+            } else {
+                $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL'), 'info');
+                $this->clearMediaCache();
+                $this->setRedirect('/media-manager');
+            }
+
+            return true;
+        }
+
+        if ($this->grav['uri']->extension() === 'json') {
+            $this->admin->json_response = [
+                'status'  => 'success',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_FAILED')
+            ];
+        } else {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REMOVE_FAILED'), 'error');
+        }
+
+        return false;
+    }
+
+    /**
+     * Handles clearing the media cache
+     *
+     * @return bool True if the action was performed
+     */
+    protected function clearMediaCache()
+    {
+        $key   = 'media-manager-files';
+        $cache = $this->grav['cache'];
+        $cache->delete(md5($key));
+
+        return true;
+    }
+
+    /**
+     * Determine if the user can edit media
+     *
+     * @param string $type
+     *
+     * @return bool True if the media action is allowed
+     */
+    protected function canEditMedia($type = 'media')
+    {
+        if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @param string $message
+     * @param string $type
+     * @return $this
+     */
+    protected function setMessage($message, $type = 'info')
+    {
+        $this->admin->setMessage($message, $type);
+
+        return $this;
+    }
+
+    /**
+     * @return Config
+     */
+    protected function getConfig(): Config
+    {
+        return $this->grav['config'];
+    }
+
+    /**
+     * @return ServerRequestInterface
+     */
+    protected function getRequest(): ServerRequestInterface
+    {
+        return $this->grav['request'];
+    }
+}

+ 3073 - 0
plugins/admin/classes/plugin/AdminController.php

@@ -0,0 +1,3073 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Backup\Backups;
+use Grav\Common\Cache;
+use Grav\Common\Config\Config;
+use Grav\Common\Debugger;
+use Grav\Common\File\CompiledYamlFile;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Flex\Types\Pages\PageIndex;
+use Grav\Common\GPM\GPM as GravGPM;
+use Grav\Common\GPM\Installer;
+use Grav\Common\Grav;
+use Grav\Common\Data;
+use Grav\Common\Helpers\Excerpts;
+use Grav\Common\Language\Language;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Media;
+use Grav\Common\Page\Medium\ImageMedium;
+use Grav\Common\Page\Medium\Medium;
+use Grav\Common\Page\Page;
+use Grav\Common\Page\Pages;
+use Grav\Common\Page\Collection;
+use Grav\Common\Plugins;
+use Grav\Common\Security;
+use Grav\Common\User\Interfaces\UserCollectionInterface;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Framework\Flex\Flex;
+use Grav\Framework\Psr7\Response;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
+use Grav\Common\Yaml;
+use PicoFeed\Parser\MalformedXmlException;
+use Psr\Http\Message\ResponseInterface;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\File\File;
+use RocketTheme\Toolbox\File\YamlFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use Twig\Loader\FilesystemLoader;
+
+/**
+ * Class AdminController
+ *
+ * @package Grav\Plugin
+ */
+class AdminController extends AdminBaseController
+{
+    /**
+     * @param Grav|null $grav
+     * @param string|null $view
+     * @param string|null $task
+     * @param string|null $route
+     * @param array|null $post
+     * @return void
+     */
+    public function initialize(Grav $grav = null, $view = null, $task = null, $route = null, $post = null)
+    {
+        $this->grav = $grav;
+        $this->admin = $this->grav['admin'];
+        $this->view = $view;
+        $this->task = $task ?: 'display';
+        if (isset($post['data'])) {
+            $this->data = $this->getPost($post['data']);
+            unset($post['data']);
+        } else {
+            // Backwards compatibility for Form plugin <= 1.2
+            $this->data = $this->getPost($post);
+        }
+        $this->post  = $this->getPost($post);
+        $this->route = $route;
+
+        $this->grav->fireEvent('onAdminControllerInit', new Event(['controller' => &$this]));
+    }
+
+    // GENERAL TASKS
+
+    /**
+     * Keep alive
+     *
+     * Route: POST /task:keepAlive (AJAX call)
+     *
+     * @return void
+     */
+    protected function taskKeepAlive(): void
+    {
+        // This task is available for all admin users.
+        $response = new Response(200);
+
+        $this->close($response);
+    }
+
+    /**
+     * Clear the cache.
+     *
+     * Route: GET /cache.json/task:clearCache (AJAX call)
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskClearCache()
+    {
+        if (!$this->authorizeTask('clear cache', ['admin.cache', 'admin.maintenance', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        // get optional cleartype param
+        $clear_type = $this->grav['uri']->param('cleartype');
+
+        if ($clear_type) {
+            $clear = $clear_type;
+        } else {
+            $clear = 'standard';
+        }
+
+        if ($clear === 'purge') {
+            $msg = Cache::purgeJob();
+            $this->admin->json_response = [
+                'status'  => 'success',
+                'message' => $msg,
+            ];
+        } else {
+            $results = Cache::clearCache($clear);
+            if (count($results) > 0) {
+                $this->admin->json_response = [
+                    'status'  => 'success',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.CACHE_CLEARED') . ' <br />' . $this->admin::translate('PLUGIN_ADMIN.METHOD') . ': ' . $clear . ''
+                ];
+            } else {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.ERROR_CLEARING_CACHE')
+                ];
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Handles form and saves the input data if its valid.
+     *
+     * Route: POST /pages?task:save
+     * Route: POST /user?task:save
+     * Route: POST /*?task:save
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskSave()
+    {
+        if (!$this->authorizeTask('save', $this->dataPermissions())) {
+            return false;
+        }
+
+        $this->grav['twig']->twig_vars['current_form_data'] = (array)$this->data;
+
+        switch ($this->view) {
+            case 'pages':
+                // Not used if Flex-Objects plugin handles pages.
+                return $this->savePage();
+            case 'user':
+                // Not used if Flex-Objects plugin handles users.
+                return $this->saveUser();
+            default:
+                if ($this->saveDefault()) {
+                    $route = $this->grav['uri']::getCurrentRoute();
+                    $this->setRedirect($route->withGravParam('task', null)->toString(), 302);
+                    $this->redirect();
+                }
+
+                return false;
+        }
+    }
+
+    /**
+     * @return bool
+     */
+    protected function saveDefault()
+    {
+        try {
+            // Handle standard data types.
+            $type = $this->getDataType();
+
+            $obj = $this->admin->getConfigurationData($type, $this->data);
+            $obj->validate();
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->setMessage($e->getMessage(), 'error');
+
+            return false;
+        }
+
+        $obj->filter(false, true);
+
+        $obj = $this->storeFiles($obj);
+
+        if ($obj) {
+            // Event to manipulate data before saving the object
+            $this->grav->fireEvent('onAdminSave', new Event(['object' => &$obj]));
+            $obj->save();
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
+            $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $obj]));
+        }
+
+        Cache::clearCache('invalidate');
+
+        // Force configuration reload.
+        /** @var Config $config */
+        $config = $this->grav['config'];
+        $config->reload();
+
+        if ($this->view === 'config') {
+            $this->setRedirect($this->admin->getAdminRoute("/{$this->view}/{$this->route}")->toString());
+        }
+
+        return true;
+    }
+
+    // USER TASKS
+
+    /**
+     * Handle logout.
+     *
+     * Route: GET /task:logout
+     * Route: POST ?task=logout
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskLogout()
+    {
+        if (!$this->authorizeTask('logout', ['admin.login', 'admin.super'])) {
+            return false;
+        }
+
+        $this->admin->logout($this->data, $this->post);
+    }
+
+    /**
+     * Route: POST /ajax.json/task:regenerate2FASecret (AJAX call)
+     *
+     * @return bool
+     */
+    public function taskRegenerate2FASecret()
+    {
+        if (!$this->authorizeTask('regenerate 2FA Secret', ['admin.login', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        try {
+            /** @var UserInterface $user */
+            $user = $this->grav['user'];
+
+            /** @var TwoFactorAuth $twoFa */
+            $twoFa = $this->grav['login']->twoFactorAuth();
+            $secret = $twoFa->createSecret();
+            $image = $twoFa->getQrImageData($user->username, $secret);
+
+            $user->set('twofa_secret', $secret);
+
+            // TODO: data user can also use save, but please test it before removing this code.
+            if ($user instanceof \Grav\Common\User\DataUser\User) {
+                // Save secret into the user file.
+                $file = $user->file();
+                if ($file->exists()) {
+                    $content = (array)$file->content();
+                    $content['twofa_secret'] = $secret;
+                    $file->save($content);
+                    $file->free();
+                }
+            } else {
+                $user->save();
+            }
+
+            $this->admin->json_response = ['status' => 'success', 'image' => $image, 'secret' => preg_replace('|(\w{4})|', '\\1 ', $secret)];
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Save user account.
+     *
+     * Called by more general save task.
+     *
+     * @note Not used if Flex-Objects plugin handles users.
+     *
+     * @return bool
+     */
+    protected function saveUser()
+    {
+        /** @var UserCollectionInterface $users */
+        $users = $this->grav['accounts'];
+
+        $user = $users->load($this->admin->route);
+
+        if (!$this->admin->authorize(['admin.super', 'admin.users'])) {
+            // no user file or not admin.super or admin.users
+            if ($user->username !== $this->grav['user']->username) {
+                $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.','error');
+
+                return false;
+            }
+        }
+
+        /** @var Data\Blueprint $blueprint */
+        $blueprint = $user->blueprints();
+        $data = $blueprint->processForm($this->admin->cleanUserPost((array)$this->data));
+        $data = new Data\Data($data, $blueprint);
+
+        try {
+            $data->validate();
+            $data->filter();
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->setMessage($e->getMessage(), 'error');
+
+            return false;
+        }
+
+        $user->update($data->toArray());
+
+        $user = $this->storeFiles($user);
+
+        if ($user) {
+            // Event to manipulate data before saving the object
+            $this->grav->fireEvent('onAdminSave', new Event(['object' => &$user]));
+            $user->save();
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
+            $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $user]));
+        }
+
+        if ($user->username === $this->grav['user']->username) {
+            /** @var UserCollectionInterface $users */
+            $users = $this->grav['accounts'];
+
+            //Editing current user. Reload user object
+            $this->grav['user']->undef('avatar');
+            $this->grav['user']->merge($users->load($this->admin->route)->toArray());
+        }
+
+        return true;
+    }
+
+    // DASHBOARD TASKS
+
+    /**
+     * Get Notifications
+     *
+     * Route: POST /task:getNotifications (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskGetNotifications()
+    {
+        if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        // do we need to force a reload
+        $refresh = $this->data['refresh'] === 'true';
+        $filter = $this->data['filter'] ?? '';
+        $filter_types = !empty($filter) ? array_map('trim', explode(',', $filter)) : [];
+
+        try {
+            $notifications = $this->admin->getNotifications($refresh);
+            $notification_data = [];
+
+            foreach ($notifications as $type => $type_notifications) {
+                if ($filter_types && in_array($type, $filter_types, true)) {
+                    $twig_template = 'partials/notification-' . $type . '-block.html.twig';
+                    $notification_data[$type] = $this->grav['twig']->processTemplate($twig_template, ['notifications' => $type_notifications]);
+                }
+            }
+
+            $json_response = [
+                'status'        => 'success',
+                'notifications' => $notification_data
+            ];
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+        }
+
+        $this->sendJsonResponse($json_response);
+    }
+
+
+    /**
+     * Hide notifications.
+     *
+     * Route: POST /notifications.json/task:hideNotification/notification_id:ID (AJAX call)
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskHideNotification()
+    {
+        if (!$this->authorizeTask('hide notification', ['admin.login', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $notification_id = $this->grav['uri']->param('notification_id');
+
+        if (!$notification_id) {
+            $this->admin->json_response = [
+                'status' => 'error'
+            ];
+
+            return false;
+        }
+
+        $filename = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
+        $file     = CompiledYamlFile::instance($filename);
+        $data     = (array)$file->content();
+
+        $date = new \DateTime();
+        $data[$notification_id] = $date->format('r');
+        $file->save($data);
+
+        $this->admin->json_response = [
+            'status' => 'success'
+        ];
+
+        return true;
+    }
+
+    /**
+     * Get Newsfeeds
+     *
+     * Route: POST /ajax.json/task:getNewsFeed (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskGetNewsFeed()
+    {
+        if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $refresh = $this->data['refresh'] === 'true' ? true : false;
+
+        try {
+            $feed = $this->admin->getFeed($refresh);
+            $feed_data = $this->grav['twig']->processTemplate('partials/feed-block.html.twig', ['feed' => $feed]);
+
+            $json_response = [
+                'status'    => 'success',
+                'feed_data' => $feed_data
+            ];
+        } catch (MalformedXmlException $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+        }
+
+        $this->sendJsonResponse($json_response);
+    }
+
+    // BACKUP TASKS
+
+    /**
+     * Handle the backup action
+     *
+     * Route: GET /backup.json/id:BACKUP_ID/task:backup (AJAX call)
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskBackup()
+    {
+        if (!$this->authorizeTask('backup', ['admin.maintenance', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $param_sep = $this->grav['config']->get('system.param_sep', ':');
+        $download = $this->grav['uri']->param('download');
+
+        try {
+            if ($download) {
+                $filename = Utils::basename(base64_decode(urldecode($download)));
+                $file = $this->grav['locator']->findResource("backup://{$filename}", true);
+                if (!$file || !Utils::endsWith($filename, '.zip', false)) {
+                    header('HTTP/1.1 401 Unauthorized');
+                    exit();
+                }
+
+                Utils::download($file, true);
+            }
+
+            $id = $this->grav['uri']->param('id', 0);
+            $backup = Backups::backup($id);
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = [
+                'status' => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')
+            ];
+
+            return true;
+        }
+
+        $download = urlencode(base64_encode($backup));
+        $url = rtrim($this->grav['uri']->rootUrl(false), '/') . '/' . trim($this->admin->base,
+                '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
+
+        $this->admin->json_response = [
+            'status' => 'success',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.YOUR_BACKUP_IS_READY_FOR_DOWNLOAD') . '. <a href="' . $url . '" class="button">' . $this->admin::translate('PLUGIN_ADMIN.DOWNLOAD_BACKUP') . '</a>',
+            'toastr' => [
+                'timeOut' => 0,
+                'extendedTimeOut' => 0,
+                'closeButton' => true
+            ]
+        ];
+
+        return true;
+    }
+
+    /**
+     * Handle delete backup action
+     *
+     * Route: GET /backup.json/backup:BACKUP_FILE/task:backupDelete (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskBackupDelete()
+    {
+        if (!$this->authorizeTask('backup', ['admin.maintenance', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $backup = $this->grav['uri']->param('backup', null);
+
+        if (null !== $backup) {
+            $filename = Utils::basename(base64_decode(urldecode($backup)));
+            $file = $this->grav['locator']->findResource("backup://{$filename}", true);
+
+            if ($file && Utils::endsWith($filename, '.zip', false)) {
+                unlink($file);
+
+                $this->admin->json_response = [
+                    'status'  => 'success',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.BACKUP_DELETED'),
+                    'toastr'  => [
+                        'closeButton' => true
+                    ]
+                ];
+
+                return true;
+            }
+        }
+
+        $this->admin->json_response = [
+            'status'  => 'error',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.BACKUP_NOT_FOUND'),
+        ];
+
+        return true;
+    }
+
+    // PLUGIN / THEME TASKS
+
+    /**
+     * Enable a plugin.
+     *
+     * Route: GET /plugins/SLUG/task:enable
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskEnable()
+    {
+        if ($this->view !== 'plugins') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('enable plugin', ['admin.plugins', 'admin.super'])) {
+            return false;
+        }
+
+        $type = $this->getDataType();
+        $this->updatePluginState($type, ['enabled' => true]);
+
+        $this->post = ['_redirect' => 'plugins'];
+        if ($this->grav['uri']->param('redirect')) {
+            $this->post = ['_redirect' => 'plugins/' . $this->route];
+        }
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_ENABLED_PLUGIN'), 'info');
+
+        Cache::clearCache('invalidate');
+
+        return true;
+    }
+
+    /**
+     * Disable a plugin.
+     *
+     * Route: GET /plugins/SLUG/task:disable
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskDisable()
+    {
+        if ($this->view !== 'plugins') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('disable plugin', ['admin.plugins', 'admin.super'])) {
+            return false;
+        }
+
+        $type = $this->getDataType();
+        $this->updatePluginState($type, ['enabled' => false]);
+
+        $this->post = ['_redirect' => 'plugins'];
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_DISABLED_PLUGIN'), 'info');
+
+        Cache::clearCache('invalidate');
+
+        return true;
+    }
+
+    /**
+     * @param string $type
+     * @param array $value
+     * @return void
+     */
+    protected function updatePluginState(string $type, array $value): void
+    {
+        $obj = Plugins::get(preg_replace('|plugins/|', '', $type));
+        if (null === $obj) {
+            throw new \RuntimeException("Plugin '{$type}' doesn't exist!");
+        }
+
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+
+        // Configuration file will be saved to the existing config stream.
+        $filename = $locator->findResource('config://') . "/{$type}.yaml";
+
+        $file = YamlFile::instance($filename);
+        $contents = $value + $file->content();
+
+        $file->save($contents);
+    }
+
+    /**
+     * Set the default theme.
+     *
+     * Route: GET /themes/SLUG/task:activate
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskActivate()
+    {
+        if ($this->view !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('activate theme', ['admin.themes', 'admin.super'])) {
+            return false;
+        }
+
+        $this->post = ['_redirect' => 'themes' ];
+
+        // Make sure theme exists (throws exception)
+        $name = $this->route;
+        $this->grav['themes']->get($name);
+
+        // Store system configuration.
+        $system = $this->admin->getConfigurationData('config/system');
+        $system->set('pages.theme', $name);
+        $system->save();
+
+        // Force configuration reload and save.
+        /** @var Config $config */
+        $config = $this->grav['config'];
+        $config->reload()->save();
+
+        $config->set('system.pages.theme', $name);
+
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_CHANGED_THEME'), 'info');
+
+        Cache::clearCache('invalidate');
+
+        $this->post = ['_redirect' => 'themes/' . $name ];
+
+        return true;
+    }
+
+    // INSTALL & UPGRADE
+
+    /**
+     * Handles updating Grav
+     *
+     * Route: GET /update.json/task:updategrav (AJAX call)
+     *
+     * @return bool False if user has no permissions.
+     */
+    public function taskUpdategrav()
+    {
+        if (!$this->authorizeTask('install grav', ['admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $gpm     = Gpm::GPM();
+        $version = $gpm->grav->getVersion();
+        $result  = Gpm::selfupgrade();
+
+        if ($result) {
+            $json_response = [
+                'status'  => 'success',
+                'type'    => 'updategrav',
+                'version' => $version,
+                'message' => $this->admin::translate('PLUGIN_ADMIN.GRAV_WAS_SUCCESSFULLY_UPDATED_TO') . ' ' . $version
+            ];
+        } else {
+            $json_response = [
+                'status'  => 'error',
+                'type'    => 'updategrav',
+                'version' => GRAV_VERSION,
+                'message' => $this->admin::translate('PLUGIN_ADMIN.GRAV_UPDATE_FAILED') . ' <br>' . Installer::lastErrorMsg()
+            ];
+        }
+
+        $this->sendJsonResponse($json_response);
+    }
+
+    /**
+     * Handles uninstalling plugins and themes
+     *
+     * @return bool True if the action was performed
+     * @deprecated Not being used anymore
+     */
+    public function taskUninstall()
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('uninstall ' . $type, ['admin.' . $type, 'admin.super'])) {
+            return false;
+        }
+
+        $package = $this->route;
+
+        $result = Gpm::uninstall($package, []);
+
+        if ($result) {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL'), 'info');
+        } else {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNINSTALL_FAILED'), 'error');
+        }
+
+        $this->post = ['_redirect' => $this->view];
+
+        return true;
+    }
+
+    /**
+     * Toggle the gpm.releases setting
+     *
+     * Route: POST /ajax.json/task:gpmRelease (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskGpmRelease()
+    {
+        if (!$this->authorizeTask('configuration', ['admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        // Default release state
+        $release = 'stable';
+        $reload  = false;
+
+        // Get the testing release value if set
+        if ($this->post['release'] === 'testing') {
+            $release = 'testing';
+        }
+
+        $config          = $this->grav['config'];
+        $current_release = $config->get('system.gpm.releases');
+
+        // If the releases setting is different, save it in the system config
+        if ($current_release !== $release) {
+            $data = new Data\Data($config->get('system'));
+            $data->set('gpm.releases', $release);
+
+            // Get the file location
+            $file = CompiledYamlFile::instance($this->grav['locator']->findResource('config://system.yaml'));
+            $data->file($file);
+
+            // Save the configuration
+            $data->save();
+            $config->reload();
+            $reload = true;
+        }
+
+        $this->admin->json_response = ['status' => 'success', 'reload' => $reload];
+
+        return true;
+    }
+
+    /**
+     * Get update status from GPM
+     *
+     * Request: POST /update.json/task:getUpdates (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskGetUpdates()
+    {
+        if ($this->view !== 'update') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('dashboard', ['admin.plugins', 'admin.themes', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data  = $this->post;
+        $flush = !empty($data['flush']);
+
+        if (isset($this->grav['session'])) {
+            $this->grav['session']->close();
+        }
+
+        try {
+            $gpm = new GravGPM($flush);
+
+            $resources_updates = $gpm->getUpdatable();
+            foreach ($resources_updates as $key => $update) {
+                if (!is_iterable($update)) {
+                    continue;
+                }
+
+                foreach ($update as $slug => $item) {
+                    $resources_updates[$key][$slug] = $item;
+                }
+            }
+
+            if ($gpm->grav !== null) {
+                $grav_updates = [
+                    'isUpdatable' => $gpm->grav->isUpdatable(),
+                    'assets'      => $gpm->grav->getAssets(),
+                    'version'     => GRAV_VERSION,
+                    'available'   => $gpm->grav->getVersion(),
+                    'date'        => $gpm->grav->getDate(),
+                    'isSymlink'   => $gpm->grav->isSymlink()
+                ];
+
+                $this->admin->json_response = [
+                    'status'  => 'success',
+                    'payload' => [
+                        'resources' => $resources_updates,
+                        'grav'      => $grav_updates,
+                        'installed' => $gpm->countInstalled(),
+                        'flushed'   => $flush
+                    ]
+                ];
+            } else {
+                $this->admin->json_response = ['status' => 'error', 'message' => 'Cannot connect to the GPM'];
+
+                return false;
+            }
+
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle getting a new package dependencies needed to be installed.
+     *
+     * Route: /plugins.json/task:getPackagesDependencies (AJAX call)
+     * Route: /themes.json/task:getPackagesDependencies (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskGetPackagesDependencies()
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('get package dependencies', ['admin.' . $type, 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data     = $this->post;
+        $packages = isset($data['packages']) ? explode(',', $data['packages']) : '';
+        $packages = (array)$packages;
+
+        try {
+            $this->admin->checkPackagesCanBeInstalled($packages);
+            $dependencies = $this->admin->getDependenciesNeededToInstall($packages);
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            return false;
+        }
+
+        $this->admin->json_response = ['status' => 'success', 'dependencies' => $dependencies];
+
+        return true;
+    }
+
+    /**
+     * Route: /plugins.json/task:installDependenciesOfPackages (AJAX call)
+     * Route: /themes.json/task:installDependenciesOfPackages (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskInstallDependenciesOfPackages()
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('install dependencies', ['admin.' . $type, 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data = $this->post;
+        $packages = isset($data['packages']) ? explode(',', $data['packages']) : '';
+        $packages = (array)$packages;
+
+        try {
+            $dependencies = $this->admin->getDependenciesNeededToInstall($packages);
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            return false;
+        }
+
+        $result = Gpm::install(array_keys($dependencies), ['theme' => $type === 'themes']);
+
+        if ($result) {
+            $this->admin->json_response = ['status' => 'success', 'message' => 'Dependencies installed successfully'];
+        } else {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSTALLATION_FAILED')
+            ];
+        }
+
+        return true;
+    }
+
+    /**
+     * Route: /plugins.json/task:installPackage (AJAX call)
+     * Route: /themes.json/task:installPackage (AJAX call)
+     *
+     * @param bool $reinstall
+     * @return bool
+     */
+    protected function taskInstallPackage($reinstall = false)
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data = $this->post;
+        $package = $data['package'] ?? '';
+        try {
+            $result = Gpm::install($package, ['theme' => $type === 'themes']);
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $msg = $e->getMessage();
+            $msg = Utils::contains($msg, '401 Unauthorized') ? "ERROR: License key for this resource is invalid." : $msg;
+            $msg = Utils::contains($msg, '404 Not Found') ? "ERROR: Resource not found" : $msg;
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($msg, ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            return false;
+        }
+
+        if ($result) {
+            $this->admin->json_response = [
+                'status'  => 'success',
+                'message' => $this->admin::translate(is_string($result) ? $result : sprintf($this->admin::translate($reinstall ?: 'PLUGIN_ADMIN.PACKAGE_X_REINSTALLED_SUCCESSFULLY',
+                    null), $package))
+            ];
+        } else {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate($reinstall ?: 'PLUGIN_ADMIN.INSTALLATION_FAILED')
+            ];
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle removing a package
+     *
+     * Route: /plugins.json/task:removePackage (AJAX call)
+     * Route: /themes.json/task:removePackage (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskRemovePackage(): bool
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('uninstall ' . $type, ['admin.' . $type, 'admin.super'])) {
+            $json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            $this->sendJsonResponse($json_response, 403);
+        }
+
+        $data    = $this->post;
+        $package = $data['package'] ?? '';
+        $result = false;
+
+        //check if there are packages that have this as a dependency. Abort and show which ones
+        $dependent_packages = $this->admin->getPackagesThatDependOnPackage($package);
+        if (count($dependent_packages) > 0) {
+            if (count($dependent_packages) > 1) {
+                $message = 'The installed packages <cyan>' . implode('</cyan>, <cyan>',
+                        $dependent_packages) . '</cyan> depends on this package. Please remove those first.';
+            } else {
+                $message = 'The installed package <cyan>' . implode('</cyan>, <cyan>',
+                        $dependent_packages) . '</cyan> depends on this package. Please remove it first.';
+            }
+
+            $json_response = ['status' => 'error', 'message' => $message];
+
+            $this->sendJsonResponse($json_response, 200);
+        }
+
+        $dependencies = false;
+        try {
+            $dependencies = $this->admin->dependenciesThatCanBeRemovedWhenRemoving($package);
+            $result       = Gpm::uninstall($package, []);
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            $this->sendJsonResponse($json_response, 200);
+        }
+
+        if ($result) {
+            $json_response = [
+                'status'       => 'success',
+                'dependencies' => $dependencies,
+                'message'      => $this->admin::translate(is_string($result) ? $result : 'PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL')
+            ];
+
+            $this->sendJsonResponse($json_response, 200);
+        }
+
+        $json_response = [
+            'status'  => 'error',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.UNINSTALL_FAILED')
+        ];
+
+        $this->sendJsonResponse($json_response, 200);
+    }
+
+    /**
+     * Handle reinstalling a package
+     *
+     * Route: /plugins.json/task:reinstallPackage (AJAX call)
+     * Route: /themes.json/task:reinstallPackage (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskReinstallPackage()
+    {
+        $type = $this->view;
+        if ($type !== 'plugins' && $type !== 'themes') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
+            $json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            $this->sendJsonResponse($json_response, 403);
+        }
+
+        $data = $this->post;
+        $slug            = $data['slug'] ?? '';
+        $package_name    = $data['package_name'] ?? '';
+        $current_version = $data['current_version'] ?? '';
+
+        $url = "https://getgrav.org/download/{$type}s/$slug/$current_version";
+
+        $result = Gpm::directInstall($url);
+
+        if ($result === true) {
+            $this->admin->json_response = [
+                'status'  => 'success',
+                'message' => $this->admin::translate(sprintf($this->admin::translate('PLUGIN_ADMIN.PACKAGE_X_REINSTALLED_SUCCESSFULLY',
+                    null), $package_name))
+            ];
+        } else {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.REINSTALLATION_FAILED')
+            ];
+        }
+
+        return true;
+    }
+
+    /**
+     * Handle direct install.
+     *
+     * Request: POST /tools/direct-install?task=directInstall
+     *
+     * @return bool
+     */
+    protected function taskDirectInstall()
+    {
+        if (!$this->authorizeTask('install', ['admin.super'])) {
+            return false;
+        }
+
+        $file_path = $this->data['file_path'] ?? null;
+
+        if (isset($_FILES['uploaded_file'])) {
+
+            // Check $_FILES['file']['error'] value.
+            switch ($_FILES['uploaded_file']['error']) {
+                case UPLOAD_ERR_OK:
+                    break;
+                case UPLOAD_ERR_NO_FILE:
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.NO_FILES_SENT'), 'error');
+                    return false;
+                case UPLOAD_ERR_INI_SIZE:
+                case UPLOAD_ERR_FORM_SIZE:
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 'error');
+                    return false;
+                case UPLOAD_ERR_NO_TMP_DIR:
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 'error');
+                    return false;
+                default:
+                    $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 'error');
+                    return false;
+            }
+
+            $file_name = $_FILES['uploaded_file']['name'];
+            $file_path = $_FILES['uploaded_file']['tmp_name'];
+
+            // Handle bad filenames.
+            if (!Utils::checkFilename($file_name)) {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
+                ];
+
+                return false;
+            }
+        }
+
+
+        $result = Gpm::directInstall($file_path);
+
+        if ($result === true) {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSTALLATION_SUCCESSFUL'), 'info');
+        } else {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSTALLATION_FAILED') . ': ' . $result,
+                'error');
+        }
+
+        $this->setRedirect('/tools');
+
+        return true;
+    }
+
+    // PAGE TASKS
+
+    /**
+     * Handles creating an empty page folder (without markdown file)
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskSaveNewFolder()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('new folder', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
+            return false;
+        }
+
+        $data = (array)$this->data;
+
+        $folder = $data['folder'] ?? '';
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Creating folder failed: bad folder name', 400);
+        }
+
+        if ($data['route'] === '' || $data['route'] === '/') {
+            $path = $this->grav['locator']->findResource('page://');
+        } else {
+            $pages = $this->admin::enablePages();
+
+            $page = $pages->find($data['route']);
+            if (!$page) {
+                return false;
+            }
+            $path = $page->path();
+        }
+
+        $orderOfNewFolder = static::getNextOrderInFolder($path);
+        $new_path         = $path . '/' . $orderOfNewFolder . '.' . $folder;
+
+        Folder::create($new_path);
+        Cache::clearCache('invalidate');
+
+        $this->grav->fireEvent('onAdminAfterSaveAs', new Event(['path' => $new_path]));
+
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
+
+        $this->setRedirect($this->admin->getAdminRoute("/{$this->view}")->toString());
+
+        return true;
+    }
+
+    /**
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool
+     */
+    protected function savePage()
+    {
+        $reorder = true;
+
+        $data = (array)$this->data;
+        $this->grav['twig']->twig_vars['current_form_data'] = $data;
+
+        $pages = $this->admin::enablePages();
+
+        // Find new parent page in order to build the path.
+        $path = trim($data['route'] ?? dirname($this->admin->route), '/');
+        if ($path === '.') {
+            $path = '';
+        }
+
+        /** @var PageInterface $obj */
+        $obj = $this->admin->page(true);
+
+        $folder = $data['folder'] ?? null;
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Saving page failed: bad folder name', 400);
+        }
+
+        if (!isset($data['folder']) || !$data['folder']) {
+            $data['folder'] = $obj->slug();
+            $this->data['folder'] = $obj->slug();
+        }
+
+        // Check for valid frontmatter
+        if (isset($data['frontmatter']) && !$this->checkValidFrontmatter($data['frontmatter'])) {
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_FRONTMATTER_COULD_NOT_SAVE'),
+                'error');
+            return false;
+        }
+
+        // XSS Checks for page content
+        $xss_whitelist = $this->grav['config']->get('security.xss_whitelist', 'admin.super');
+        if (!$this->admin->authorize($xss_whitelist)) {
+            $check_what = ['header' => $data['header'] ?? '', 'frontmatter' => $data['frontmatter'] ?? '', 'content' => $data['content'] ?? ''];
+            $results = Security::detectXssFromArray($check_what);
+            if (!empty($results)) {
+                $this->admin->setMessage('<i class="fa fa-ban"></i> ' . $this->admin::translate('PLUGIN_ADMIN.XSS_ONSAVE_ISSUE'),
+                    'error');
+                return false;
+            }
+        }
+
+        if ($path !== '') {
+            // First try to get page by its path.
+            $parent = $pages->get(GRAV_ROOT . '/' . $path);
+            if (!$parent) {
+                // Fall back using the route.
+                $route = '/' . preg_replace(PageIndex::PAGE_ROUTE_REGEX, '/', $path);
+                $parent = $pages->find($route, true);
+                if (!$parent) {
+                    throw new \RuntimeException('New parent page cannot be resolved!');
+                }
+            }
+        } else {
+            $parent = $pages->root();
+        }
+
+        $original_order = (int)trim($obj->order(), '.');
+
+        try {
+            // Change parent if needed and initialize move (might be needed also on ordering/folder change).
+            $obj = $obj->move($parent);
+            $this->preparePage($obj, false, $obj->language());
+            $obj->validate();
+
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->setMessage($e->getMessage(), 'error');
+
+            return false;
+        }
+        $obj->filter();
+
+        // rename folder based on visible
+        if ($original_order === 1000) {
+            // increment order to force reshuffle
+            $obj->order($original_order + 1);
+        }
+
+        if (isset($data['order']) && !empty($data['order'])) {
+            $reorder = explode(',', $data['order']);
+        }
+
+        // add or remove numeric prefix based on ordering value
+        if (isset($data['ordering'])) {
+            if ($data['ordering'] && !$obj->order()) {
+                $obj->order(static::getNextOrderInFolder($obj->parent()->path()));
+                $reorder = false;
+            } elseif (!$data['ordering'] && $obj->order()) {
+                $obj->folder($obj->slug());
+            }
+        }
+
+        $obj = $this->storeFiles($obj);
+
+        if ($obj) {
+            // Event to manipulate data before saving the object
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminSave', new Event(['object' => &$obj, 'page' => &$obj]));
+
+            $obj->save($reorder);
+
+            Cache::clearCache('invalidate');
+
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $obj, 'page' => $obj]));
+        }
+
+        if (method_exists($obj, 'unsetRouteSlug')) {
+            $obj->unsetRouteSlug();
+        }
+
+        $multilang = $this->isMultilang();
+
+        if ($multilang && !$obj->language()) {
+            $obj->language($this->admin->getLanguage());
+        }
+        $admin_route = $this->admin->base;
+
+        $route           = $obj->rawRoute();
+        $redirect_url = ($multilang ? '/' . $obj->language() : '') . $admin_route . '/' . $this->view . $route;
+        $this->setRedirect($redirect_url);
+
+        return true;
+    }
+
+    /**
+     * Save page as a new copy.
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool True if the action was performed.
+     * @throws \RuntimeException
+     */
+    protected function taskCopy()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('copy page', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
+            return false;
+        }
+
+        try {
+            $pages = $this->admin::enablePages();
+
+            // Get the current page.
+            $original_page = $this->admin->page(true);
+
+            // Find new parent page in order to build the path.
+            $parent = $original_page->parent() ?: $pages->root();
+            // Make a copy of the current page and fill the updated information into it.
+            $page = $original_page->copy($parent);
+
+            $order = 0;
+            if ($page->order()) {
+                $order = $this->getNextOrderInFolder($page->parent()->path());
+            }
+
+            // Make sure the header is loaded in case content was set through raw() (expert mode)
+            $page->header();
+
+            if ($page->order()) {
+                $page->order($order);
+            }
+
+            $folder = $this->findFirstAvailable('folder', $page);
+            $slug   = $this->findFirstAvailable('slug', $page);
+
+            $page->path($page->parent()->path() . DS . $page->order() . $folder);
+            $page->route($page->parent()->route() . '/' . $slug);
+            $page->rawRoute($page->parent()->rawRoute() . '/' . $slug);
+
+            // Append progressive number to the copied page title
+            $match  = preg_split('/(\d+)(?!.*\d)/', $original_page->title(), 2, PREG_SPLIT_DELIM_CAPTURE);
+            $header = $page->header();
+            if (!isset($match[1])) {
+                $header->title = $match[0] . ' 2';
+            } else {
+                $header->title = $match[0] . ((int)$match[1] + 1);
+            }
+
+            $page->header($header);
+            $page->save(false);
+
+            $redirect = $this->view . $page->rawRoute();
+            $header   = $page->header();
+
+            if (isset($header->slug)) {
+                $match        = preg_split('/-(\d+)$/', $header->slug, 2, PREG_SPLIT_DELIM_CAPTURE);
+                $header->slug = $match[0] . '-' . (isset($match[1]) ? (int)$match[1] + 1 : 2);
+            }
+
+            $page->header($header);
+
+            $page->save();
+
+            Cache::clearCache('invalidate');
+
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $page, 'page' => $page]));
+
+            // Enqueue message and redirect to new location.
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_COPIED'), 'info');
+            $this->setRedirect($redirect);
+
+        } catch (\Exception $e) {
+            throw new \RuntimeException('Copying page failed on error: ' . $e->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Reorder pages.
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskReorder()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('reorder pages', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
+            return false;
+        }
+
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REORDERING_WAS_SUCCESSFUL'), 'info');
+
+        return true;
+    }
+
+    /**
+     * Delete page.
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool True if the action was performed.
+     * @throws \RuntimeException
+     */
+    protected function taskDelete()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('delete page', ['admin.pages', 'admin.pages.delete', 'admin.super'])) {
+            return false;
+        }
+
+        try {
+            $page = $this->admin->page();
+
+            if (count($page->translatedLanguages()) > 1) {
+                $page->file()->delete();
+            } else {
+                Folder::delete($page->path());
+            }
+
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminAfterDelete', new Event(['object' => $page, 'page' => $page]));
+
+            Cache::clearCache('invalidate');
+
+            // Set redirect to pages list.
+            $redirect = 'pages';
+
+            $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_DELETED'), 'info');
+            $this->setRedirect($redirect);
+
+        } catch (\Exception $e) {
+            throw new \RuntimeException('Deleting page failed on error: ' . $e->getMessage());
+        }
+
+        return true;
+    }
+
+    /**
+     * Switch the content language. Optionally redirect to a different page.
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool
+     */
+    protected function taskSwitchlanguage()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('switch language', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
+            return false;
+        }
+
+        $data = (array)$this->data;
+
+        $language = $data['lang'] ?? $this->grav['uri']->param('lang');
+
+        if (isset($data['redirect'])) {
+            $redirect = '/pages/' . $data['redirect'];
+        } else {
+            $redirect = '/pages';
+        }
+
+
+        if ($language) {
+            $this->grav['session']->admin_lang = $language ?: 'en';
+        }
+
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info');
+
+        $this->setRedirect($this->admin->getAdminRoute($redirect)->toString());
+
+        return true;
+    }
+
+    /**
+     * Save the current page in a different language. Automatically switches to that language.
+     *
+     * Route: /pages
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskSaveas()
+    {
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('save as', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
+            return false;
+        }
+
+        /** @var Language $language */
+        $language = $this->grav['language'];
+
+        $data     = (array)$this->data;
+        $lang = $data['lang'] ?? null;
+
+        if ($lang) {
+            $this->grav['session']->admin_lang = $lang ?: 'en';
+        }
+
+        $uri = $this->grav['uri'];
+        $obj = $this->admin->page($uri->route());
+        $this->preparePage($obj, false, $lang);
+
+        $file = $obj->file();
+        if ($file) {
+            $filename = $this->determineFilenameIncludingLanguage($obj->name(), $lang);
+
+            $path  = $obj->path() . DS . $filename;
+            $aFile = File::instance($path);
+            $aFile->save();
+
+            $aPage = new Page();
+            $aPage->init(new \SplFileInfo($path), $lang . '.md');
+            $aPage->header($obj->header());
+            $aPage->rawMarkdown($obj->rawMarkdown());
+            $aPage->template($obj->template());
+            $aPage->validate();
+            $aPage->filter();
+
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminSave', new Event(['object' => $aPage, 'page' => &$aPage]));
+
+            $aPage->save();
+
+            Cache::clearCache('invalidate');
+
+            // DEPRECATED: page
+            $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $aPage, 'page' => $aPage]));
+        }
+
+        $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info');
+
+        // TODO: better multilanguage support needed.
+        $this->setRedirect($language->getLanguageURLPrefix($lang) . $uri->route());
+
+        return true;
+    }
+
+    /**
+     * Continue to the new page.
+     *
+     * @note Not used if Flex-Objects plugin handles pages, users and user groups.
+     *
+     * @return bool True if the action was performed.
+     */
+    public function taskContinue()
+    {
+        $data = (array)$this->data;
+
+        if ($this->view === 'users') {
+            $username = strip_tags(strtolower($data['username']));
+            $this->setRedirect("{$this->view}/{$username}");
+
+            return true;
+        }
+
+        if ($this->view !== 'pages') {
+            return false;
+        }
+
+        $route  = $data['route'] !== '/' ? $data['route'] : '';
+        $folder = $data['folder'] ?? null;
+        $title = $data['title'] ?? null;
+
+        // Handle @slugify-{field} value, automatically slugifies the specified field
+        if (null !== $folder && 0 === strpos($folder, '@slugify-')) {
+            $folder = \Grav\Plugin\Admin\Utils::slug($data[substr($folder, 9)] ?? '');
+        }
+        if (!$folder) {
+            $folder = \Grav\Plugin\Admin\Utils::slug($title) ?: '';
+        }
+        $folder = ltrim($folder, '_');
+        if ($folder === '' || mb_strpos($folder, '/') !== false) {
+            throw new \RuntimeException('Creating page failed: bad folder name', 400);
+        }
+
+        if (!empty($data['modular'])) {
+            $folder = '_' . $folder;
+        }
+
+        $data['folder'] = $folder;
+
+        $path = $route . '/' . $folder;
+        $this->admin->session()->{$path} = $data;
+
+        // Store the name and route of a page, to be used pre-filled defaults of the form in the future
+        $this->admin->session()->lastPageName  = $data['name'];
+        $this->admin->session()->lastPageRoute = $data['route'];
+
+        $this->setRedirect("{$this->view}/" . ltrim($path, '/'));
+
+        return true;
+    }
+
+    /**
+     * $data['route'] = $this->grav['uri']->param('route');
+     * $data['sortby'] = $this->grav['uri']->param('sortby', null);
+     * $data['filters'] = $this->grav['uri']->param('filters', null);
+     * $data['page'] $this->grav['uri']->param('page', true);
+     * $data['base'] = $this->grav['uri']->param('base');
+     * $initial = (bool) $this->grav['uri']->param('initial');
+     *
+     * @return ResponseInterface
+     * @throws RequestException
+     */
+    protected function taskGetLevelListing(): ResponseInterface
+    {
+        $this->checkTaskAuthorization('save', $this->dataPermissions());
+
+        $request = $this->getRequest();
+        $data = $request->getParsedBody();
+
+        if (!isset($data['field'])) {
+            throw new RequestException($request, 'Bad Request', 400);
+        }
+
+        // Base64 decode the route
+        $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
+
+        $initial = $data['initial'] ?? null;
+        if ($initial) {
+            $data['leaf_route'] = $data['route'];
+            $data['route'] = null;
+            $data['level'] = 1;
+        }
+
+        [$status, $message, $response,] = $this->getLevelListing($data);
+
+        $json = [
+            'status'  => $status,
+            'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
+            'data' => array_values($response)
+        ];
+
+        return $this->createJsonResponse($json, 200);
+    }
+
+    /**
+     * Route: /ajax.json
+     *
+     * @note Not used if Flex-Objects plugin handles pages.
+     *
+     * @return bool
+     */
+    protected function taskGetChildTypes()
+    {
+        if ($this->view !== 'ajax') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('get childtypes', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data = $this->post;
+
+        $route = $data['rawroute'] ?? null;
+        if ($route) {
+            /** @var Flex $flex */
+            $flex = $this->grav['flex'];
+            $page = $flex->getObject(trim($route, '/'), 'pages');
+            if ($page instanceof PageInterface) {
+                $child_type = $page->childType();
+                if ($child_type !== '') {
+                    $this->admin->json_response = [
+                        'status' => 'success',
+                        'child_type' => $child_type
+                    ];
+                    return true;
+                }
+            }
+        }
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'child_type' => '',
+        ];
+
+        return true;
+    }
+
+    /**
+     * Handles filtering the page by modular/visible/routable in the pages list.
+     *
+     * @note Used only in legacy pages.
+     *
+     * @return bool
+     */
+    protected function taskFilterPages()
+    {
+        if ($this->view !== 'pages-filter') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('filter pages', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data = $this->post;
+
+        $flags   = !empty($data['flags']) ? array_map('strtolower', explode(',', $data['flags'])) : [];
+        $queries = !empty($data['query']) ? explode(',', $data['query']) : [];
+
+        $pages = $this->admin::enablePages();
+
+        /** @var Collection $collection */
+        $collection = $pages->all();
+
+        if (count($flags)) {
+            // Filter by state
+            $pageStates = [
+                'modular',
+                'nonmodular',
+                'visible',
+                'nonvisible',
+                'routable',
+                'nonroutable',
+                'published',
+                'nonpublished'
+            ];
+
+            if (count(array_intersect($pageStates, $flags)) > 0) {
+                if (in_array('modular', $flags, true)) {
+                    $collection = $collection->modular();
+                }
+
+                if (in_array('nonmodular', $flags, true)) {
+                    $collection = $collection->nonModular();
+                }
+
+                if (in_array('visible', $flags, true)) {
+                    $collection = $collection->visible();
+                }
+
+                if (in_array('nonvisible', $flags, true)) {
+                    $collection = $collection->nonVisible();
+                }
+
+                if (in_array('routable', $flags, true)) {
+                    $collection = $collection->routable();
+                }
+
+                if (in_array('nonroutable', $flags, true)) {
+                    $collection = $collection->nonRoutable();
+                }
+
+                if (in_array('published', $flags, true)) {
+                    $collection = $collection->published();
+                }
+
+                if (in_array('nonpublished', $flags, true)) {
+                    $collection = $collection->nonPublished();
+                }
+            }
+            foreach ($pageStates as $pageState) {
+                if (($pageState = array_search($pageState, $flags, true)) !== false) {
+                    unset($flags[$pageState]);
+                }
+            }
+
+            // Filter by page type
+            if ($flags) {
+                $types = [];
+
+                $pageTypes = array_keys(Pages::pageTypes());
+                foreach ($pageTypes as $pageType) {
+                    if (($pageKey = array_search($pageType, $flags, true)) !== false) {
+                        $types[] = $pageType;
+                        unset($flags[$pageKey]);
+                    }
+                }
+
+                if (count($types)) {
+                    $collection = $collection->ofOneOfTheseTypes($types);
+                }
+            }
+
+            // Filter by page type
+            if ($flags) {
+                $accessLevels = $flags;
+                $collection   = $collection->ofOneOfTheseAccessLevels($accessLevels);
+            }
+        }
+
+        if (!empty($queries)) {
+            foreach ($collection as $page) {
+                foreach ($queries as $query) {
+                    $query = trim($query);
+                    if (stripos($page->getRawContent(), $query) === false
+                        && stripos($page->title(), $query) === false
+                        && stripos($page->folder(), $query) === false
+                        && stripos($page->slug(), \Grav\Plugin\Admin\Utils::slug($query)) === false
+                    ) {
+                        $collection->remove($page);
+                    }
+                }
+            }
+        }
+
+        $results = [];
+        foreach ($collection as $path => $page) {
+            $results[] = $page->route();
+        }
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.PAGES_FILTERED'),
+            'results' => $results
+        ];
+        $this->admin->collection = $collection;
+
+        return true;
+    }
+
+
+    /**
+     * Process the page Markdown
+     *
+     * Preview task in the builtin editor.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskProcessMarkdown()
+    {
+        if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.pages.read', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        try {
+            $page = $this->admin->page(true);
+
+            if (!$page) {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
+                ];
+
+                return false;
+            }
+
+            $this->preparePage($page, true);
+            $page->header();
+            $page->templateFormat('html');
+
+            // Add theme template paths to Twig loader
+            $template_paths = $this->grav['locator']->findResources('theme://templates');
+            $this->grav['twig']->twig->getLoader()->addLoader(new FilesystemLoader($template_paths));
+
+            $html = $page->content();
+
+            $this->admin->json_response = ['status' => 'success', 'preview' => $html];
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
+
+            return false;
+        }
+
+        return true;
+    }
+
+    // MEDIA TASKS
+
+    /**
+     * Determines the file types allowed to be uploaded
+     *
+     * Used by pagemedia field. Works only within legacy pages.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskListmedia()
+    {
+        if ($this->view !== 'media') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('list media', ['admin.pages', 'admin.pages.read', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $media = $this->getMedia();
+        if (!$media) {
+            $this->admin->json_response = [
+                'status' => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
+            ];
+
+            return false;
+        }
+
+        $media_list = [];
+        /**
+         * @var string $name
+         * @var Medium|ImageMedium $medium
+         */
+        foreach ($media->all() as $name => $medium) {
+
+            $metadata = [];
+            $img_metadata = $medium->metadata();
+            if ($img_metadata) {
+                $metadata = $img_metadata;
+            }
+
+            // Get original name
+            /** @var ImageMedium $source */
+            $source = method_exists($medium, 'higherQualityAlternative') ? $medium->higherQualityAlternative() : null;
+
+            $media_list[$name] = [
+                'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
+                'size' => $medium->get('size'),
+                'metadata' => $metadata,
+                'original' => $source ? $source->get('filename') : null
+            ];
+        }
+
+        $this->admin->json_response = ['status' => 'success', 'results' => $media_list];
+
+        return true;
+    }
+
+    /**
+     * Handles adding a media file to a page.
+     *
+     * Used by pagemedia field. Works only within legacy pages.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskAddmedia()
+    {
+        if ($this->view !== 'media') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('add media', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        /** @var Config $config */
+        $config = $this->grav['config'];
+
+        if (empty($_FILES)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_POSTMAX_LIMIT')
+            ];
+
+            return false;
+        }
+
+        if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_PARAMETERS')
+            ];
+
+            return false;
+        }
+
+        // Check $_FILES['file']['error'] value.
+        switch ($_FILES['file']['error']) {
+            case UPLOAD_ERR_OK:
+                break;
+            case UPLOAD_ERR_NO_FILE:
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.NO_FILES_SENT')
+                ];
+
+                return false;
+            case UPLOAD_ERR_INI_SIZE:
+            case UPLOAD_ERR_FORM_SIZE:
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT')
+                ];
+
+                return false;
+            case UPLOAD_ERR_NO_TMP_DIR:
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR')
+                ];
+
+                return false;
+            default:
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
+                ];
+
+                return false;
+        }
+
+        $filename = $_FILES['file']['name'];
+
+        // Handle bad filenames.
+        if (!Utils::checkFilename($filename)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'),
+                    htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
+            ];
+
+            return false;
+        }
+
+        // You should also check filesize here.
+        $grav_limit = Utils::getUploadLimit();
+        if ($grav_limit > 0 && $_FILES['file']['size'] > $grav_limit) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
+            ];
+
+            return false;
+        }
+
+
+        // Check extension
+        $extension = strtolower(Utils::pathinfo($filename, PATHINFO_EXTENSION));
+
+        // If not a supported type, return
+        if (!$extension || !$config->get("media.types.{$extension}")) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension
+            ];
+
+            return false;
+        }
+
+        $page = $this->admin->page($this->route);
+        $media = $page ? $this->getMedia($page) : null;
+        if (!$media) {
+            $this->admin->json_response = [
+                'status' => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
+            ];
+
+            return false;
+        }
+
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+        $path = $media->getPath();
+        if ($locator->isStream($path)) {
+            $path = $locator->findResource($path, true, true);
+        }
+
+        // Special Sanitization for SVG
+        if (Utils::contains($extension, 'svg', false)) {
+            Security::sanitizeSVG($_FILES['file']['tmp_name']);
+        }
+
+        // Upload it
+        if (!move_uploaded_file($_FILES['file']['tmp_name'], sprintf('%s/%s', $path, $filename))) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE')
+            ];
+
+            return false;
+        }
+
+        Cache::clearCache('invalidate');
+
+        // Add metadata if needed
+        $include_metadata = Grav::instance()['config']->get('system.media.auto_metadata_exif', false);
+        $basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
+
+        $metadata = [];
+
+        if ($include_metadata && isset($media[$basename])) {
+            $img_metadata = $media[$basename]->metadata();
+            if ($img_metadata) {
+                $metadata = $img_metadata;
+            }
+        }
+
+        // DEPRECATED: page
+        $this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $page, 'page' => $page]));
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
+            'metadata' => $metadata,
+        ];
+
+        return true;
+    }
+
+    /**
+     * Request: POST .json/task:compileScss (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskCompileScss()
+    {
+        if (!$this->authorizeTask('compile scss', ['admin.plugins', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $default_scheme = $this->grav['config']->get('plugins.admin.whitelabel.color_scheme');
+
+        $preview = $this->post['preview'] ?? false;
+        $data = ['color_scheme' => $this->data['whitelabel']['color_scheme'] ?? $default_scheme];
+        $output_file = $preview ? 'admin-preset.css' : 'admin-preset__tmp.css';
+
+        $options = [
+            'input' => 'plugin://admin/themes/grav/scss/preset.scss',
+            'output' => 'asset://' .$output_file
+        ];
+
+        [$compile_status, $msg] = $this->grav['admin-whitelabel']->compilePresetScss($data, $options);
+
+        $json_response = [
+            'status'  => $compile_status ? 'success' : 'error',
+            'message' => ($preview ? 'Preview ' : 'SCSS ') . $msg,
+            'files' => [
+                'color_scheme' => Utils::url($options['output'])
+            ]
+        ];
+
+        $this->sendJsonResponse($json_response);
+    }
+
+    /**
+     * Request: POST .json/task:exportScss (AJAX call)
+     *
+     * @return bool
+     */
+    protected function taskExportScss()
+    {
+        if (!$this->authorizeTask('export scss', ['admin.plugins', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $data = ['color_scheme' => $this->data['whitelabel']['color_scheme'] ?? null];
+        $name = empty($this->data['whitelabel']['color_scheme']['name']) ? 'admin-theme-export' : \Grav\Plugin\Admin\Utils::slug($this->data['whitelabel']['color_scheme']['name']);
+
+        $location  = 'asset://' . $name . '.yaml';
+
+        [$status, $msg] = $this->grav['admin-whitelabel']->exportPresetScsss($data, $location);
+
+        $json_response = [
+            'status'  => $status ? 'success' : 'error',
+            'message' => $msg,
+            'files' => [
+                'download' => Utils::url($location)
+            ]
+        ];
+
+        $this->sendJsonResponse($json_response);
+    }
+
+    /**
+     * Handles deleting a media file from a page.
+     *
+     * Used by pagemedia field.
+     *
+     * @return bool True if the action was performed.
+     */
+    protected function taskDelmedia()
+    {
+        if ($this->view !== 'media') {
+            return false;
+        }
+
+        if (!$this->authorizeTask('delete media', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $page = $this->admin->page($this->route);
+        $media = $page ? $this->getMedia($page) : null;
+        if (null === $media) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
+            ];
+
+            return false;
+        }
+
+        $filename = !empty($this->post['filename']) ? Utils::basename($this->post['filename']) : null;
+
+        // Handle bad filenames.
+        if (!$filename || !Utils::checkFilename($filename)) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.NO_FILE_FOUND')
+            ];
+
+            return false;
+        }
+
+        /** @var UniformResourceLocator $locator */
+        $locator = $this->grav['locator'];
+
+        $targetPath = $media->getPath() . '/' . $filename;
+        if ($locator->isStream($targetPath)) {
+            $targetPath = $locator->findResource($targetPath, true, true);
+        }
+        $fileParts  = Utils::pathinfo($filename);
+
+        $found = false;
+
+        if (file_exists($targetPath)) {
+            $found  = true;
+            $result = unlink($targetPath);
+
+            if (!$result) {
+                $this->admin->json_response = [
+                    'status'  => 'error',
+                    'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+                ];
+
+                return false;
+            }
+        }
+
+        // Remove Extra Files
+        foreach (scandir($media->getPath(), SCANDIR_SORT_NONE) as $file) {
+            if (preg_match("/{$fileParts['filename']}@\d+x\.{$fileParts['extension']}(?:\.meta\.yaml)?$|{$filename}\.meta\.yaml$/", $file)) {
+
+                $targetPath = $media->getPath() . '/' . $file;
+                if ($locator->isStream($targetPath)) {
+                    $targetPath = $locator->findResource($targetPath, true, true);
+                }
+
+                $result = unlink($targetPath);
+
+                if (!$result) {
+                    $this->admin->json_response = [
+                        'status'  => 'error',
+                        'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+                    ];
+
+                    return false;
+                }
+
+                $found = true;
+            }
+        }
+
+        Cache::clearCache('invalidate');
+
+        if (!$found) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+            ];
+
+            return false;
+        }
+
+        // DEPRECATED: page
+        $this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $page, 'page' => $page, 'media' => $media, 'filename' => $filename]));
+
+        $this->admin->json_response = [
+            'status'  => 'success',
+            'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+        ];
+
+        return true;
+    }
+
+    /**
+     * @param array $data
+     * @return array
+     */
+    protected function getLevelListing($data)
+    {
+        // Valid types are dir|file|link
+        $default_filters =  ['type'=> ['root', 'dir'], 'name' => null, 'extension' => null];
+
+        $pages = $this->admin::enablePages();
+
+        $page_instances = $pages->instances();
+
+        $is_page = $data['page'] ?? true;
+        $route = $data['route'] ?? null;
+        $leaf_route = $data['leaf_route'] ?? null;
+        $sortby = $data['sortby'] ?? 'filename';
+        $order = $data['order'] ?? SORT_ASC;
+        $initial = $data['initial'] ?? null;
+        $filters = isset($data['filters']) ? $default_filters + json_decode($data['filters']) : $default_filters;
+        $filter_type = (array) $filters['type'];
+
+        $status = 'error';
+        $msg = null;
+        $response = [];
+        $children = null;
+        $sub_route = null;
+        $extra = null;
+        $root = false;
+
+        // Handle leaf_route
+        if ($leaf_route && $route !== $leaf_route) {
+            $nodes = explode('/', $leaf_route);
+            $sub_route =  '/' . implode('/', array_slice($nodes, 1, $data['level']++));
+            $data['route'] = $sub_route;
+
+            [$status, $msg, $children, $extra] = $this->getLevelListing($data);
+        }
+
+        // Handle no route, assume page tree root
+        if (!$route) {
+            $is_page = false;
+            $route = $this->grav['locator']->findResource('page://', true);
+            $root = true;
+        }
+
+        if ($is_page) {
+            // Try the path
+            /** @var PageInterface $page */
+            $page = $pages->get(GRAV_ROOT . $route);
+
+            // Try a real route (like homepage)
+            if (is_null($page)) {
+                $page = $pages->find($route);
+            }
+
+            $path = $page ? $page->path() : null;
+        } else {
+            // Try a physical path
+            if (!Utils::startsWith($route, GRAV_ROOT)) {
+                $try_path = GRAV_ROOT . $route;
+            } else {
+                $try_path = $route;
+            }
+
+            $path = file_exists($try_path) ? $try_path : null;
+        }
+
+        $blueprintsData = $this->admin->page(true);
+
+        if (null !== $blueprintsData) {
+            if (method_exists($blueprintsData, 'blueprints')) {
+                $settings = $blueprintsData->blueprints()->schema()->getProperty($data['field']);
+            } elseif (method_exists($blueprintsData, 'getBlueprint')) {
+                $settings = $blueprintsData->getBlueprint()->schema()->getProperty($data['field']);
+            }
+
+            $filters = array_merge([], $filters, ($settings['filters'] ?? []));
+            $filter_type = $filters['type'] ?? $filter_type;
+        }
+
+
+        if ($path) {
+            $status = 'success';
+            $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
+            foreach (new \DirectoryIterator($path) as $fileInfo) {
+                $fileName = $fileInfo->getFilename();
+                $filePath = str_replace('\\', '/', $fileInfo->getPathname());
+
+                if (($fileInfo->isDot() && $fileName !== '.' && $initial) || (Utils::startsWith($fileName, '.') && strlen($fileName) > 1)) {
+                    continue;
+                }
+
+                if ($fileInfo->isDot()) {
+                    if ($root) {
+                        $payload = [
+                            'name' => '<root>',
+                            'value' => '',
+                            'item-key' => '',
+                            'filename' => '.',
+                            'extension' => '',
+                            'type' => 'root',
+                            'modified' => $fileInfo->getMTime(),
+                            'size' => 0,
+                            'has-children' => false
+                        ];
+                    } else {
+                        continue;
+                    }
+                } else {
+                    $file_page = $page_instances[$filePath] ?? null;
+                    $file_path = Utils::replaceFirstOccurrence(GRAV_ROOT, '', $filePath);
+                    $type = $fileInfo->getType();
+
+                    $child_path = $file_page ? $file_page->path() : $filePath;
+                    $count_children = Folder::countChildren($child_path);
+
+                    $payload = [
+                        'name' => $file_page ? $file_page->title() : $fileName,
+                        'value' => $file_page ? $file_page->rawRoute() : $file_path,
+                        'item-key' => Utils::basename($file_page ? $file_page->route() : $file_path),
+                        'filename' => $fileName,
+                        'extension' => $type === 'dir' ? '' : $fileInfo->getExtension(),
+                        'type' => $type,
+                        'modified' => $fileInfo->getMTime(),
+                        'size' => $count_children,
+                        'symlink' => false,
+                        'has-children' => $count_children > 0
+                    ];
+                }
+
+                // Fix for symlink
+                if ($payload['type'] === 'link') {
+                    $payload['symlink'] = true;
+                    $physical_path = $fileInfo->getRealPath();
+                    $payload['type'] = is_dir($physical_path) ? 'dir' : 'file';
+                }
+
+                // filter types
+                if ($filters['type']) {
+                    if (!in_array($payload['type'], $filter_type)) {
+                        continue;
+                    }
+                }
+
+                // Simple filter for name or extension
+                if (($filters['name'] && Utils::contains($payload['basename'], $filters['name'])) ||
+                    ($filters['extension'] && Utils::contains($payload['extension'], $filters['extension']))) {
+                    continue;
+                }
+
+                // Add children if any
+                if ($filePath === $extra && is_array($children)) {
+                    $payload['children'] = array_values($children);
+                }
+
+                $response[] = $payload;
+            }
+        } else {
+            $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
+        }
+
+        // Sorting
+        $response = Utils::sortArrayByKey($response, $sortby, $order);
+
+        $temp_array = [];
+        foreach ($response as $index => $item) {
+            $temp_array[$item['type']][$index] = $item;
+        }
+
+        $sorted = Utils::sortArrayByArray($temp_array, $filter_type);
+        $response = Utils::arrayFlatten($sorted);
+
+        return [$status, $this->admin::translate($msg ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'), $response, $path];
+    }
+
+    /**
+     * Get page media.
+     *
+     * @param PageInterface|null $page
+     * @return Media|null
+     */
+    public function getMedia(PageInterface $page = null)
+    {
+        $page = $page ?? $this->admin->page($this->route);
+        if (!$page) {
+            return null;
+        }
+
+        $this->uri = $this->uri ?? $this->grav['uri'];
+
+        $field = (string)$this->uri->post('field', '');
+        $order = $this->uri->post('order') ?: null;
+        if ($order && is_string($order)) {
+            $order = array_map('trim', explode(',', $order));
+        }
+
+        $blueprints = $page->blueprints();
+        $settings = $this->getMediaFieldSettings($blueprints, $field);
+        $path = $settings['destination'] ?? $page->path();
+
+        return $path ? new Media($path, $order) : null;
+    }
+
+    /**
+     * @param Data\Blueprint|null $blueprint
+     * @param string $field
+     * @return array|null
+     */
+    protected function getMediaFieldSettings(?Data\Blueprint $blueprint, string $field): ?array
+    {
+        $schema = $blueprint ? $blueprint->schema() : null;
+        if (!$schema || $field === '') {
+            return null;
+        }
+
+        $settings = is_object($schema) ? (array)$schema->getProperty($field) : null;
+        if (null === $settings) {
+            return null;
+        }
+
+        if (empty($settings['destination']) || \in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) {
+            unset($settings['destination']);
+        }
+
+        return $settings + ['accept' => '*', 'limit' => 1000];
+    }
+
+    /**
+     * @return string
+     */
+    protected function getDataType()
+    {
+        return trim("{$this->view}/{$this->admin->route}", '/');
+    }
+
+    /**
+     * Gets the configuration data for a given view & post
+     *
+     * @param array $data
+     * @return object
+     */
+    protected function prepareData(array $data)
+    {
+        $type = $this->getDataType();
+
+        return $this->admin->data($type, $data);
+    }
+
+    /**
+     * Prepare a page to be stored: update its folder, name, template, header and content
+     *
+     * @param PageInterface          $page
+     * @param bool                   $clean_header
+     * @param string                 $languageCode
+     * @return void
+     */
+    protected function preparePage(PageInterface $page, $clean_header = false, $languageCode = ''): void
+    {
+        $input = (array)$this->data;
+
+        $this->admin::enablePages();
+
+        if (isset($input['folder']) && $input['folder'] !== $page->value('folder')) {
+            $order    = $page->value('order');
+            $ordering = $order ? sprintf('%02d.', $order) : '';
+            $page->folder($ordering . $input['folder']);
+        }
+
+        if (isset($input['name']) && !empty($input['name'])) {
+            $type = strtolower($input['name']);
+            $page->template($type);
+            $name = preg_replace('|.*/|', '', $type);
+
+            /** @var Language $language */
+            $language = $this->grav['language'];
+            if ($language->enabled()) {
+                $languageCode = $languageCode ?: $language->getLanguage();
+                if ($languageCode) {
+                    $isDefault = $languageCode === $language->getDefault();
+                    $includeLang = !$isDefault || (bool)$this->grav['config']->get('system.languages.include_default_lang_file_extension', true);
+                    if (!$includeLang) {
+                        // Check if the language specific file exists; use it if it does.
+                        $includeLang = file_exists("{$page->path()}/{$name}.{$languageCode}.md");
+                    }
+                    // Keep existing .md file if we're updating default language, otherwise always append the language.
+                    if ($includeLang) {
+                        $name .= '.' . $languageCode;
+                    }
+                }
+            }
+
+            $name .= '.md';
+            $page->name($name);
+        }
+
+        // Special case for Expert mode: build the raw, unset content
+        if (isset($input['frontmatter'], $input['content'])) {
+            $page->raw("---\n" . (string)$input['frontmatter'] . "\n---\n" . (string)$input['content']);
+            unset($input['content']);
+        // Handle header normally
+        } elseif (isset($input['header'])) {
+            $header = $input['header'];
+
+            foreach ($header as $key => $value) {
+                if ($key === 'metadata' && is_array($header[$key])) {
+                    foreach ($header['metadata'] as $key2 => $value2) {
+                        if (isset($input['toggleable_header']['metadata'][$key2]) && !$input['toggleable_header']['metadata'][$key2]) {
+                            $header['metadata'][$key2] = '';
+                        }
+                    }
+                } elseif ($key === 'taxonomy' && is_array($header[$key])) {
+                    foreach ($header[$key] as $taxkey => $taxonomy) {
+                        if (is_array($taxonomy) && \count($taxonomy) === 1 && trim($taxonomy[0]) === '') {
+                            unset($header[$key][$taxkey]);
+                        }
+                    }
+                } else {
+                    if (isset($input['toggleable_header'][$key]) && !$input['toggleable_header'][$key]) {
+                        $header[$key] = null;
+                    }
+                }
+            }
+            if ($clean_header) {
+                $header = Utils::arrayFilterRecursive($header, function ($k, $v) {
+                    return !(null === $v || $v === '');
+                });
+            }
+            $page->header((object)$header);
+            $page->frontmatter(Yaml::dump((array)$page->header(), 20));
+        }
+        // Fill content last because it also renders the output.
+        if (isset($input['content'])) {
+            $page->rawMarkdown((string)$input['content']);
+        }
+    }
+
+    /**
+     * Find the first available $item ('slug' | 'folder') for a page
+     * Used when copying a page, to determine the first available slot
+     *
+     * @param string        $item
+     * @param PageInterface $page
+     * @return string The first available slot
+     */
+    protected function findFirstAvailable($item, PageInterface $page)
+    {
+        $parent = $page->parent();
+        if (!$parent || !$parent->children()) {
+            return $page->{$item}();
+        }
+
+        $withoutPrefix = function ($string) {
+            $match = preg_split('/^[0-9]+\./u', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+            return $match[1] ?? $match[0];
+        };
+
+        $withoutPostfix = function ($string) {
+            $match = preg_split('/-(\d+)$/', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
+
+            return $match[0];
+        };
+
+        /* $appendedNumber = function ($string) {
+            $match  = preg_split('/-(\d+)$/', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
+            $append = (isset($match[1]) ? (int)$match[1] + 1 : 2);
+
+            return $append;
+        };*/
+
+        $highest                   = 1;
+        $siblings                  = $parent->children();
+        $findCorrectAppendedNumber = function ($item, $page_item, $highest) use (
+            $siblings,
+            &$findCorrectAppendedNumber,
+            &$withoutPrefix
+        ) {
+            foreach ($siblings as $sibling) {
+                if ($withoutPrefix($sibling->{$item}()) == ($highest === 1 ? $page_item : $page_item . '-' . $highest)) {
+                    $highest = $findCorrectAppendedNumber($item, $page_item, $highest + 1);
+
+                    return $highest;
+                }
+            }
+
+            return $highest;
+        };
+
+        $base = $withoutPrefix($withoutPostfix($page->$item()));
+
+        $return  = $base;
+        $highest = $findCorrectAppendedNumber($item, $base, $highest);
+
+        if ($highest > 1) {
+            $return .= '-' . $highest;
+        }
+
+        return $return;
+    }
+
+    /**
+     * @param string $frontmatter
+     * @return bool
+     */
+    public function checkValidFrontmatter($frontmatter)
+    {
+        try {
+            Yaml::parse($frontmatter);
+        } catch (\RuntimeException $e) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * The what should be the new filename when saving as a new language
+     *
+     * @param string $current_filename the current file name, including .md. Example: default.en.md
+     * @param string $language         The new language it will be saved as. Example: 'it' or 'en-GB'.
+     * @return string The new filename. Example: 'default.it'
+     */
+    public function determineFilenameIncludingLanguage($current_filename, $language)
+    {
+        $ext = '.md';
+        $filename = substr($current_filename, 0, -strlen($ext));
+        $languages_enabled = $this->grav['config']->get('system.languages.supported', []);
+
+        $parts = explode('.', trim($filename, '.'));
+        $lang = array_pop($parts);
+
+        if ($lang === $language) {
+            return $filename . $ext;
+        }
+
+        if (in_array($lang, $languages_enabled, true)) {
+            $filename = implode('.', $parts);
+        }
+
+        return $filename . '.' . $language . $ext;
+    }
+
+    /**
+     * Get the next available ordering number in a folder
+     *
+     * @param string $path
+     * @return string the correct order string to prepend
+     */
+    public static function getNextOrderInFolder($path)
+    {
+        $files = Folder::all($path, ['recursive' => false]);
+
+        $highestOrder = 0;
+        foreach ($files as $file) {
+            preg_match(PAGE_ORDER_PREFIX_REGEX, $file, $order);
+
+            if (isset($order[0])) {
+                $theOrder = (int)trim($order[0], '.');
+            } else {
+                $theOrder = 0;
+            }
+
+            if ($theOrder >= $highestOrder) {
+                $highestOrder = $theOrder;
+            }
+        }
+
+        $orderOfNewFolder = $highestOrder + 1;
+
+        if ($orderOfNewFolder < 10) {
+            $orderOfNewFolder = '0' . $orderOfNewFolder;
+        }
+
+        return $orderOfNewFolder;
+    }
+
+    /**
+     * Used in 3rd party editors (e.g. next-gen).
+     *
+     * @return ResponseInterface|false
+     */
+    protected function taskConvertUrls()
+    {
+        if (!$this->authorizeTask('access page', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
+            $this->admin->json_response = [
+                'status'  => 'error',
+                'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
+            ];
+
+            return false;
+        }
+
+        $request = $this->getRequest();
+        $data = $request->getParsedBody();
+        $converted_links = [];
+        $converted_images = [];
+        $status = 'success';
+        $message = 'All links converted';
+
+        $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
+        $data['data'] = json_decode($data['data'] ?? '{}', true);
+
+        // use the route if passed, else use current page in admin as reference
+        $page_route = $data['route'] ?? $this->admin->page(true);
+
+        /** @var PageInterface */
+        $pages = $this->admin::enablePages();
+        $page = $pages->find($page_route);
+
+        if (!$page) {
+            throw new RequestException($request,'Page Not Found', 404);
+        }
+
+
+        if (!isset($data['data'])) {
+            throw new RequestException($request, 'Bad Request', 400);
+        }
+
+        foreach ($data['data']['a'] ?? [] as $link) {
+            $converted_links[$link] = Excerpts::processLinkHtml($link, $page);
+        }
+
+        foreach ($data['data']['img'] ?? [] as $image) {
+            $converted_images[$image] = Excerpts::processImageHtml($image, $page);
+        }
+
+        $json = [
+            'status'  => $status,
+            'message' => $message,
+            'data' => ['links' => $converted_links, 'images' => $converted_images]
+        ];
+
+        return $this->createJsonResponse($json, 200);
+    }
+}

+ 182 - 0
plugins/admin/classes/plugin/AdminForm.php

@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use ArrayAccess;
+use Exception;
+use Grav\Common\Data\Blueprint;
+use Grav\Common\Data\Data;
+use Grav\Framework\Form\Interfaces\FormFlashInterface;
+use Grav\Framework\Form\Interfaces\FormInterface;
+use Grav\Framework\Form\Traits\FormTrait;
+use InvalidArgumentException;
+use JsonSerializable;
+
+/**
+ * Class AdminForm
+ * @package Grav\Plugin\Admin
+ */
+class AdminForm implements FormInterface, JsonSerializable
+{
+    use FormTrait;
+
+    /** @var string */
+    protected $nonce_name;
+    /** @var string */
+    protected $nonce_action;
+    /** @var callable */
+    protected $submitMethod;
+
+    /**
+     * AdminForm constructor.
+     *
+     * @param string $name
+     * @param array $options
+     */
+    public function __construct(string $name, array $options)
+    {
+        $this->name = $name;
+        $this->nonce_name = $options['nonce_name'] ?? 'admin-nonce';
+        $this->nonce_action = $options['nonce_action'] ?? 'admin-form';
+
+        $this->setId($options['id'] ?? $this->getName());
+        $this->setUniqueId($options['unique_id'] ?? $this->getName());
+        $this->setBlueprint($options['blueprint']);
+        $this->setSubmitMethod($options['submit_method'] ?? null);
+        $this->setFlashLookupFolder('tmp://admin/forms/[SESSIONID]');
+
+        if (!empty($options['reset'])) {
+            $this->getFlash()->delete();
+        }
+
+        $this->initialize();
+    }
+
+    /**
+     * @return $this
+     */
+    public function initialize(): AdminForm
+    {
+        $this->messages = [];
+        $this->submitted = false;
+        $this->unsetFlash();
+
+        /** @var FormFlashInterface $flash */
+        $flash = $this->getFlash();
+        if ($flash->exists()) {
+            $data = $flash->getData();
+            if (null !== $data) {
+                $data = new Data($data, $this->getBlueprint());
+                $data->setKeepEmptyValues(true);
+                $data->setMissingValuesAsNull(true);
+            }
+
+            $this->data = $data;
+            $this->files = $flash->getFilesByFields(false);
+        } else {
+            $this->data = new Data([], $this->getBlueprint());
+            $this->files = [];
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNonceName(): string
+    {
+        return $this->nonce_name;
+    }
+
+    /**
+     * @return string
+     */
+    public function getNonceAction(): string
+    {
+        return $this->nonce_action;
+    }
+
+    /**
+     * @return string
+     */
+    public function getScope(): string
+    {
+        return 'data.';
+    }
+
+    /**
+     * @param Blueprint $blueprint
+     */
+    public function setBlueprint(Blueprint $blueprint): void
+    {
+        if (null === $blueprint) {
+            throw new InvalidArgumentException('Blueprint is required');
+        }
+
+        $this->blueprint = $blueprint;
+    }
+
+    /**
+     * @param string $field
+     * @param mixed $value
+     */
+    public function setData(string $field, $value): void
+    {
+        $this->getData()->set($field, $value);
+    }
+
+    /**
+     * @return Blueprint
+     */
+    public function getBlueprint(): Blueprint
+    {
+        return $this->blueprint;
+    }
+
+    /**
+     * @param callable|null $submitMethod
+     */
+    public function setSubmitMethod(?callable $submitMethod): void
+    {
+        if (null === $submitMethod) {
+            throw new InvalidArgumentException('Submit method is required');
+        }
+
+        $this->submitMethod = $submitMethod;
+    }
+
+    /**
+     * @param array $data
+     * @param array $files
+     * @return void
+     * @throws Exception
+     */
+    protected function doSubmit(array $data, array $files): void
+    {
+        $method = $this->submitMethod;
+        $method($data, $files);
+
+        $this->reset();
+    }
+
+    /**
+     * Filter validated data.
+     *
+     * @param ArrayAccess|Data|null $data
+     * @return void
+     */
+    protected function filterData($data = null): void
+    {
+        if ($data instanceof Data) {
+            $data->filter(true, true);
+        }
+    }
+}

+ 51 - 0
plugins/admin/classes/plugin/AdminFormFactory.php

@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+declare(strict_types=1);
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Grav;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Page;
+use Grav\Framework\Form\Interfaces\FormFactoryInterface;
+use Grav\Framework\Form\Interfaces\FormInterface;
+
+/**
+ * Class FlexFormFactory
+ * @package Grav\Plugin\FlexObjects
+ */
+class AdminFormFactory implements FormFactoryInterface
+{
+    /**
+     * @param Page $page
+     * @param string $name
+     * @param array $form
+     * @return FormInterface|null
+     */
+    public function createPageForm(Page $page, string $name, array $form): ?FormInterface
+    {
+        return $this->createFormForPage($page, $name, $form);
+    }
+
+    /**
+     * @param PageInterface $page
+     * @param string $name
+     * @param array $form
+     * @return FormInterface|null
+     */
+    public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface
+    {
+        /** @var Admin|null $admin */
+        $admin = Grav::instance()['admin'] ?? null;
+        $object = $admin->form ?? null;
+
+        return $object && $object->getName() === $name ? $object : null;
+    }
+}

+ 414 - 0
plugins/admin/classes/plugin/Controllers/AbstractController.php

@@ -0,0 +1,414 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+declare(strict_types=1);
+
+namespace Grav\Plugin\Admin\Controllers;
+
+use Grav\Common\Debugger;
+use Grav\Common\Grav;
+use Grav\Common\Inflector;
+use Grav\Common\Language\Language;
+use Grav\Common\Utils;
+use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
+use Grav\Framework\Form\Interfaces\FormInterface;
+use Grav\Framework\Psr7\Response;
+use Grav\Framework\RequestHandler\Exception\NotFoundException;
+use Grav\Framework\RequestHandler\Exception\PageExpiredException;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Framework\Route\Route;
+use Grav\Framework\Session\SessionInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Session\Message;
+
+abstract class AbstractController implements RequestHandlerInterface
+{
+    /** @var string */
+    protected $nonce_action = 'admin-form';
+
+    /** @var string */
+    protected $nonce_name = 'admin-nonce';
+
+    /** @var ServerRequestInterface */
+    protected $request;
+
+    /** @var Grav */
+    protected $grav;
+
+    /** @var string */
+    protected $type;
+
+    /** @var string */
+    protected $key;
+
+    /**
+     * Handle request.
+     *
+     * Fires event: admin.[directory].[task|action].[command]
+     *
+     * @param ServerRequestInterface $request
+     * @return Response
+     */
+    public function handle(ServerRequestInterface $request): ResponseInterface
+    {
+        $attributes = $request->getAttributes();
+        $this->request = $request;
+        $this->grav = $attributes['grav'] ?? Grav::instance();
+        $this->type =  $attributes['type'] ?? null;
+        $this->key =  $attributes['key'] ?? null;
+
+        /** @var Route $route */
+        $route = $attributes['route'];
+        $post = $this->getPost();
+
+        if ($this->isFormSubmit()) {
+            $form = $this->getForm();
+            $this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName();
+            $this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction();
+        }
+
+        try {
+            $task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task');
+            if ($task) {
+                if (empty($attributes['forwarded'])) {
+                    $this->checkNonce($task);
+                }
+                $type = 'task';
+                $command = $task;
+            } else {
+                $type = 'action';
+                $command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display';
+            }
+            $command = strtolower($command);
+
+            $event = new Event(
+                [
+                    'controller' => $this,
+                    'response' => null
+                ]
+            );
+
+            $this->grav->fireEvent("admin.{$this->type}.{$type}.{$command}", $event);
+
+            $response = $event['response'];
+            if (!$response) {
+                /** @var Inflector $inflector */
+                $inflector = $this->grav['inflector'];
+                $method = $type . $inflector::camelize($command);
+                if ($method && method_exists($this, $method)) {
+                    $response = $this->{$method}($request);
+                } else {
+                    throw new NotFoundException($request);
+                }
+            }
+        } catch (\Exception $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $response = $this->createErrorResponse($e);
+        }
+
+        if ($response instanceof Response) {
+            return $response;
+        }
+
+        return $this->createJsonResponse($response);
+    }
+
+    /**
+     * Get request.
+     *
+     * @return ServerRequestInterface
+     */
+    public function getRequest(): ServerRequestInterface
+    {
+        return $this->request;
+    }
+
+    /**
+     * @param string|null $name
+     * @param mixed $default
+     * @return mixed
+     */
+    public function getPost(string $name = null, $default = null)
+    {
+        $body = $this->request->getParsedBody();
+
+        if ($name) {
+            return $body[$name] ?? $default;
+        }
+
+        return $body;
+    }
+
+    /**
+     * Check if a form has been submitted.
+     *
+     * @return bool
+     */
+    public function isFormSubmit(): bool
+    {
+        return (bool)$this->getPost('__form-name__');
+    }
+
+    /**
+     * Get form.
+     *
+     * @param string|null $type
+     * @return FormInterface
+     */
+    public function getForm(string $type = null): FormInterface
+    {
+        $object = $this->getObject();
+        if (!$object) {
+            throw new \RuntimeException('Not Found', 404);
+        }
+
+        $formName = $this->getPost('__form-name__');
+        $uniqueId = $this->getPost('__unique_form_id__') ?: $formName;
+
+        $form = $object->getForm($type ?? 'edit');
+        if ($uniqueId) {
+            $form->setUniqueId($uniqueId);
+        }
+
+        return $form;
+    }
+
+    /**
+     * @return FlexObjectInterface
+     */
+    abstract public function getObject();
+
+    /**
+     * Get Grav instance.
+     *
+     * @return Grav
+     */
+    public function getGrav(): Grav
+    {
+        return $this->grav;
+    }
+
+    /**
+     * Get session.
+     *
+     * @return SessionInterface
+     */
+    public function getSession(): SessionInterface
+    {
+        return $this->getGrav()['session'];
+    }
+
+    /**
+     * Display the current admin page.
+     *
+     * @return Response
+     */
+    public function createDisplayResponse(): ResponseInterface
+    {
+        return new Response(418);
+    }
+
+    /**
+     * Create custom HTML response.
+     *
+     * @param string $content
+     * @param int $code
+     * @return Response
+     */
+    public function createHtmlResponse(string $content, int $code = null): ResponseInterface
+    {
+        return new Response($code ?: 200, [], $content);
+    }
+
+    /**
+     * Create JSON response.
+     *
+     * @param array $content
+     * @return Response
+     */
+    public function createJsonResponse(array $content): ResponseInterface
+    {
+        $code = $content['code'] ?? 200;
+        if ($code >= 301 && $code <= 307) {
+            $code = 200;
+        }
+
+        return new Response($code, ['Content-Type' => 'application/json'], json_encode($content));
+    }
+
+    /**
+     * Create redirect response.
+     *
+     * @param string $url
+     * @param int $code
+     * @return Response
+     */
+    public function createRedirectResponse(string $url, int $code = null): ResponseInterface
+    {
+        if (null === $code || $code < 301 || $code > 307) {
+            $code = $this->grav['config']->get('system.pages.redirect_default_code', 302);
+        }
+
+        $accept = $this->getAccept(['application/json', 'text/html']);
+
+        if ($accept === 'application/json') {
+            return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
+        }
+
+        return new Response($code, ['Location' => $url]);
+    }
+
+    /**
+     * Create error response.
+     *
+     * @param  \Exception $exception
+     * @return Response
+     */
+    public function createErrorResponse(\Exception $exception): ResponseInterface
+    {
+        $validCodes = [
+            400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
+            422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
+        ];
+
+        if ($exception instanceof RequestException) {
+            $code = $exception->getHttpCode();
+            $reason = $exception->getHttpReason();
+        } else {
+            $code = $exception->getCode();
+            $reason = null;
+        }
+
+        if (!in_array($code, $validCodes, true)) {
+            $code = 500;
+        }
+
+        $message = $exception->getMessage();
+        $response = [
+            'code' => $code,
+            'status' => 'error',
+            'message' => htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8')
+        ];
+
+        $accept = $this->getAccept(['application/json', 'text/html']);
+
+        if ($accept === 'text/html') {
+            $method = $this->getRequest()->getMethod();
+
+            // On POST etc, redirect back to the previous page.
+            if ($method !== 'GET' && $method !== 'HEAD') {
+                $this->setMessage($message, 'error');
+                $referer = $this->request->getHeaderLine('Referer');
+                return $this->createRedirectResponse($referer, 303);
+            }
+
+            // TODO: improve error page
+            return $this->createHtmlResponse($response['message']);
+        }
+
+        return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
+    }
+
+    /**
+     * Translate a string.
+     *
+     * @param  string $string
+     * @return string
+     */
+    public function translate(string $string): string
+    {
+        /** @var Language $language */
+        $language = $this->grav['language'];
+
+        return $language->translate($string);
+    }
+
+    /**
+     * Set message to be shown in the admin.
+     *
+     * @param  string $message
+     * @param  string $type
+     * @return $this
+     */
+    public function setMessage($message, $type = 'info')
+    {
+        /** @var Message $messages */
+        $messages = $this->grav['messages'];
+        $messages->add($message, $type);
+
+        return $this;
+    }
+
+    /**
+     * Check if request nonce is valid.
+     *
+     * @param  string $task
+     * @throws PageExpiredException  If nonce is not valid.
+     */
+    protected function checkNonce(string $task): void
+    {
+        $nonce = null;
+
+        if (\in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
+            $nonce = $this->getPost($this->nonce_name);
+        }
+
+        if (!$nonce) {
+            $nonce = $this->grav['uri']->param($this->nonce_name);
+        }
+
+        if (!$nonce) {
+            $nonce = $this->grav['uri']->query($this->nonce_name);
+        }
+
+        if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) {
+            throw new PageExpiredException($this->request);
+        }
+    }
+
+    /**
+     * Return the best matching mime type for the request.
+     *
+     * @param  string[] $compare
+     * @return string|null
+     */
+    protected function getAccept(array $compare): ?string
+    {
+        $accepted = [];
+        foreach ($this->request->getHeader('Accept') as $accept) {
+            foreach (explode(',', $accept) as $item) {
+                if (!$item) {
+                    continue;
+                }
+
+                $split = explode(';q=', $item);
+                $mime = array_shift($split);
+                $priority = array_shift($split) ?? 1.0;
+
+                $accepted[$mime] = $priority;
+            }
+        }
+
+        arsort($accepted);
+
+        // TODO: add support for image/* etc
+        $list = array_intersect($compare, array_keys($accepted));
+        if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
+            return reset($compare) ?: null;
+        }
+
+        return reset($list) ?: null;
+    }
+}

+ 359 - 0
plugins/admin/classes/plugin/Controllers/AdminController.php

@@ -0,0 +1,359 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+declare(strict_types=1);
+
+namespace Grav\Plugin\Admin\Controllers;
+
+use Grav\Common\Config\Config;
+use Grav\Common\Data\Blueprint;
+use Grav\Common\Grav;
+use Grav\Common\Language\Language;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Page\Page;
+use Grav\Common\Page\Pages;
+use Grav\Common\Uri;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Framework\Controller\Traits\ControllerResponseTrait;
+use Grav\Framework\RequestHandler\Exception\PageExpiredException;
+use Grav\Framework\Session\SessionInterface;
+use Grav\Plugin\Admin\Admin;
+use Grav\Plugin\Admin\AdminForm;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use RocketTheme\Toolbox\Session\Message;
+
+abstract class AdminController
+{
+    use ControllerResponseTrait {
+        createRedirectResponse as traitCreateRedirectResponse;
+        getErrorJson as traitGetErrorJson;
+    }
+
+    /** @var string */
+    protected $nonce_action = 'admin-form';
+    /** @var string */
+    protected $nonce_name = 'admin-nonce';
+    /** @var Grav */
+    protected $grav;
+    /** @var PageInterface */
+    protected $page;
+    /** @var AdminForm|null */
+    protected $form;
+
+    public function __construct(Grav $grav)
+    {
+        $this->grav = $grav;
+    }
+
+    /**
+     * @return PageInterface|null
+     */
+    public function getPage(): ?PageInterface
+    {
+        return $this->page;
+    }
+
+    /**
+     * Get currently active form.
+     *
+     * @return AdminForm|null
+     */
+    public function getActiveForm(): ?AdminForm
+    {
+        if (null === $this->form) {
+            $post = $this->getPost();
+
+            $active = $post['__form-name__'] ?? null;
+
+            $this->form = $active ? $this->getForm($active) : null;
+        }
+
+        return $this->form;
+    }
+
+    /**
+     * Get a form.
+     *
+     * @param string $name
+     * @param array $options
+     * @return AdminForm|null
+     */
+    public function getForm(string $name, array $options = []): ?AdminForm
+    {
+        $post = $this->getPost();
+        $page = $this->getPage();
+        $forms = $page ? $page->forms() : [];
+        $blueprint = $forms[$name] ?? null;
+        if (null === $blueprint) {
+            return null;
+        }
+
+        $active = $post['__form-name__'] ?? null;
+        $unique_id = $active && $active === $name ? ($post['__unique_form_id__'] ?? null) : null;
+
+        $options += [
+            'unique_id' => $unique_id,
+            'blueprint' => new Blueprint(null, ['form' => $blueprint]),
+            'submit_method' => $this->getFormSubmitMethod($name),
+            'nonce_name' => $this->nonce_name,
+            'nonce_action' => $this->nonce_action,
+        ];
+
+        return new AdminForm($name, $options);
+    }
+
+    abstract protected function getFormSubmitMethod(string $name): callable;
+
+    /**
+     * @param string $route
+     * @param string|null $lang
+     * @return string
+     */
+    public function getAdminUrl(string $route, string $lang = null): string
+    {
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+        $admin = $this->getAdmin();
+
+        return $pages->baseUrl($lang) . $admin->base . $route;
+    }
+
+    /**
+     * @param string $route
+     * @param string|null $lang
+     * @return string
+     */
+    public function getAbsoluteAdminUrl(string $route, string $lang = null): string
+    {
+        /** @var Pages $pages */
+        $pages = $this->grav['pages'];
+        $admin = $this->getAdmin();
+
+        return $pages->baseUrl($lang, true) . $admin->base . $route;
+    }
+
+    /**
+     * Get session.
+     *
+     * @return SessionInterface
+     */
+    public function getSession(): SessionInterface
+    {
+        return $this->grav['session'];
+    }
+
+    /**
+     * @return Admin
+     */
+    protected function getAdmin(): Admin
+    {
+        return $this->grav['admin'];
+    }
+
+    /**
+     * @return UserInterface
+     */
+    protected function getUser(): UserInterface
+    {
+        return $this->getAdmin()->user;
+    }
+
+    /**
+     * @return ServerRequestInterface
+     */
+    public function getRequest(): ServerRequestInterface
+    {
+        return $this->getAdmin()->request;
+    }
+
+    /**
+     * @return array
+     */
+    public function getPost(): array
+    {
+        return (array)($this->getRequest()->getParsedBody() ?? []);
+    }
+
+    /**
+     * Translate a string.
+     *
+     * @param string $string
+     * @param mixed ...$args
+     * @return string
+     */
+    public function translate(string $string, ...$args): string
+    {
+        /** @var Language $language */
+        $language = $this->grav['language'];
+
+        array_unshift($args, $string);
+
+        return $language->translate($args);
+    }
+
+    /**
+     * Set message to be shown in the admin.
+     *
+     * @param string $message
+     * @param string $type
+     * @return $this
+     */
+    public function setMessage(string $message, string $type = 'info'): AdminController
+    {
+        /** @var Message $messages */
+        $messages = $this->grav['messages'];
+        $messages->add($message, $type);
+
+        return $this;
+    }
+
+    /**
+     * @return Config
+     */
+    protected function getConfig(): Config
+    {
+        return $this->grav['config'];
+    }
+
+    /**
+     * Check if request nonce is valid.
+     *
+     * @return void
+     * @throws PageExpiredException  If nonce is not valid.
+     */
+    protected function checkNonce(): void
+    {
+        $nonce = null;
+
+        $nonce_name = $this->form ? $this->form->getNonceName() : $this->nonce_name;
+        $nonce_action = $this->form ? $this->form->getNonceAction() : $this->nonce_action;
+
+        if (\in_array(strtoupper($this->getRequest()->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
+            $post = $this->getPost();
+            $nonce = $post[$nonce_name] ?? null;
+        }
+
+        /** @var Uri $uri */
+        $uri = $this->grav['uri'];
+        if (!$nonce) {
+            $nonce = $uri->param($nonce_name);
+        }
+
+        if (!$nonce) {
+            $nonce = $uri->query($nonce_name);
+        }
+
+        if (!$nonce || !Utils::verifyNonce($nonce, $nonce_action)) {
+            throw new PageExpiredException($this->getRequest());
+        }
+    }
+
+    /**
+     * Return the best matching mime type for the request.
+     *
+     * @param  string[] $compare
+     * @return string|null
+     */
+    protected function getAccept(array $compare): ?string
+    {
+        $accepted = [];
+        foreach ($this->getRequest()->getHeader('Accept') as $accept) {
+            foreach (explode(',', $accept) as $item) {
+                if (!$item) {
+                    continue;
+                }
+
+                $split = explode(';q=', $item);
+                $mime = array_shift($split);
+                $priority = array_shift($split) ?? 1.0;
+
+                $accepted[$mime] = $priority;
+            }
+        }
+
+        arsort($accepted);
+
+        // TODO: add support for image/* etc
+        $list = array_intersect($compare, array_keys($accepted));
+        if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
+            return reset($compare) ?: null;
+        }
+
+        return reset($list) ?: null;
+    }
+
+    /**
+     * @param string $template
+     * @return PageInterface
+     */
+    protected function createPage(string $template): PageInterface
+    {
+        $page = new Page();
+
+        // Plugins may not have the correct Cache-Control header set, force no-store for the proxies.
+        $page->expires(0);
+
+        $filename = "plugin://admin/pages/admin/{$template}.md";
+        if (!file_exists($filename)) {
+            throw new \RuntimeException(sprintf('Creating admin page %s failed: not found', $template));
+        }
+
+        Admin::DEBUG && Admin::addDebugMessage("Admin page: {$template}");
+
+        $page->init(new \SplFileInfo($filename));
+        $page->slug($template);
+
+        return $page;
+    }
+
+    /**
+     * @param string|null $url
+     * @param int|null $code
+     * @return ResponseInterface
+     */
+    protected function createRedirectResponse(string $url = null, int $code = null): ResponseInterface
+    {
+        $request = $this->getRequest();
+
+        if (null === $url || '' === $url) {
+            $url = (string)$request->getUri();
+        } elseif (mb_strpos($url, '/') === 0) {
+            $url = $this->getAbsoluteAdminUrl($url);
+        }
+
+        if (null === $code) {
+            if (in_array($request->getMethod(), ['GET', 'HEAD'])) {
+                $code = 302;
+            } else {
+                $code = 303;
+            }
+        }
+
+        return $this->traitCreateRedirectResponse($url, $code);
+    }
+
+    /**
+     * @param \Throwable $e
+     * @return array
+     */
+    protected function getErrorJson(\Throwable $e): array
+    {
+        $json = $this->traitGetErrorJson($e);
+        $code = $e->getCode();
+        if ($code === 401) {
+            $json['redirect'] = $this->getAbsoluteAdminUrl('/');
+        }
+
+        return $json;
+    }
+
+
+}

+ 634 - 0
plugins/admin/classes/plugin/Controllers/Login/LoginController.php

@@ -0,0 +1,634 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin\Controllers\Login;
+
+use Grav\Common\Debugger;
+use Grav\Common\Grav;
+use Grav\Common\Page\Pages;
+use Grav\Common\Uri;
+use Grav\Common\User\Interfaces\UserCollectionInterface;
+use Grav\Common\User\Interfaces\UserInterface;
+use Grav\Common\Utils;
+use Grav\Framework\RequestHandler\Exception\PageExpiredException;
+use Grav\Framework\RequestHandler\Exception\RequestException;
+use Grav\Plugin\Admin\Admin;
+use Grav\Plugin\Admin\Controllers\AdminController;
+use Grav\Plugin\Email\Email;
+use Grav\Plugin\Login\Login;
+use Psr\Http\Message\ResponseInterface;
+use RobThree\Auth\TwoFactorAuthException;
+
+/**
+ * Class LoginController
+ * @package Grav\Plugin\Admin\Controllers\Login
+ */
+class LoginController extends AdminController
+{
+    /** @var string */
+    protected $nonce_action = 'admin-login';
+    /** @var string */
+    protected $nonce_name = 'login-nonce';
+
+    /**
+     * @return ResponseInterface
+     */
+    public function displayLogin(): ResponseInterface
+    {
+        $this->page = $this->createPage('login');
+
+        $user = $this->getUser();
+        if ($this->is2FA($user)) {
+            $this->form = $this->getForm('login-twofa', ['reset' => true]);
+        } else {
+            $this->form = $this->getForm('login', ['reset' => true]);
+        }
+
+        return $this->createDisplayResponse();
+    }
+
+    /**
+     * @return ResponseInterface
+     */
+    public function displayForgot(): ResponseInterface
+    {
+        $this->page = $this->createPage('forgot');
+        $this->form = $this->getForm('admin-login-forgot', ['reset' => true]);
+
+        return $this->createDisplayResponse();
+    }
+
+    /**
+     * Handle the reset password action.
+     *
+     * @param string|null $username
+     * @param string|null $token
+     * @return ResponseInterface
+     */
+    public function displayReset(string $username = null, string $token = null): ResponseInterface
+    {
+        if ('' === (string)$username || '' === (string)$token) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
+
+            return $this->createRedirectResponse('/forgot');
+        }
+
+        $this->page = $this->createPage('reset');
+        $this->form = $this->getForm('admin-login-reset', ['reset' => true]);
+        $this->form->setData('username', $username);
+        $this->form->setData('token', $token);
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.RESET_NEW_PASSWORD'));
+
+        return $this->createDisplayResponse();
+    }
+
+    /**
+     * @return ResponseInterface
+     */
+    public function displayRegister(): ResponseInterface
+    {
+        $route = $this->getRequest()->getAttribute('admin')['route'] ?? '';
+        if ('' !== $route) {
+            return $this->createRedirectResponse('/');
+        }
+
+        $this->page = $this->createPage('register');
+        $this->form = $this->getForm('admin-login-register');
+
+        return $this->createDisplayResponse();
+    }
+
+    /**
+     * @return ResponseInterface
+     */
+    public function displayUnauthorized(): ResponseInterface
+    {
+        $uri = (string)$this->getRequest()->getUri();
+
+        $ext = Utils::pathinfo($uri, PATHINFO_EXTENSION);
+        $accept = $this->getAccept(['application/json', 'text/html']);
+        if ($ext === 'json' || $accept === 'application/json') {
+            return $this->createErrorResponse(new RequestException($this->getRequest(), $this->translate('PLUGIN_ADMIN.LOGGED_OUT'), 401));
+        }
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.LOGGED_OUT'), 'warning');
+
+        return $this->createRedirectResponse('/');
+    }
+
+    /**
+     * Handle login.
+     *
+     * @return ResponseInterface
+     */
+    public function taskLogin(): ResponseInterface
+    {
+        $this->page = $this->createPage('login');
+        $this->form = $this->getActiveForm() ?? $this->getForm('login');
+        try {
+            $this->checkNonce();
+        } catch (PageExpiredException $e) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+
+            return $this->createDisplayResponse();
+        }
+
+        $post = $this->getPost();
+        $credentials = (array)($post['data'] ?? []);
+        $login = $this->getLogin();
+        $config = $this->getConfig();
+
+        $userKey = (string)($credentials['username'] ?? '');
+        // Pseudonymization of the IP.
+        $ipKey = sha1(Uri::ip() . $config->get('security.salt'));
+
+        $rateLimiter = $login->getRateLimiter('login_attempts');
+
+        // Check if the current IP has been used in failed login attempts.
+        $attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
+
+        $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
+
+        // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
+        if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
+            Admin::DEBUG && Admin::addDebugMessage('Admin login: rate limit, redirecting', $credentials);
+
+            $this->setMessage($this->translate('PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()), 'error');
+
+            $this->form->reset();
+
+            /** @var Pages $pages */
+            $pages = $this->grav['pages'];
+
+            // Redirect to the home page of the site.
+            return $this->createRedirectResponse($pages->homeUrl(null, true));
+        }
+
+        Admin::DEBUG && Admin::addDebugMessage('Admin login', $credentials);
+
+        // Fire Login process.
+        $event = $login->login(
+            $credentials,
+            ['admin' => true, 'twofa' => $config->get('plugins.admin.twofa_enabled', false)],
+            ['authorize' => 'admin.login', 'return_event' => true]
+        );
+        $user = $event->getUser();
+
+        Admin::DEBUG && Admin::addDebugMessage('Admin login: user', $user);
+
+        $redirect = (string)$this->getRequest()->getUri();
+
+        if ($user->authenticated) {
+            $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
+            if ($user->authorized) {
+                $event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
+            }
+
+            $event->defRedirect($redirect);
+        } elseif ($user->authorized) {
+            $event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
+        } else {
+            $event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
+        }
+
+        $event->defRedirect($redirect);
+
+        $message = $event->getMessage();
+        if ($message) {
+            $this->setMessage($this->translate($message), $event->getMessageType());
+        }
+
+        $this->form->reset();
+
+        return $this->createRedirectResponse($event->getRedirect());
+    }
+
+    /**
+     * Handle logout when user isn't fully logged in or clicks logout after the session has been expired.
+     *
+     * @return ResponseInterface
+     */
+    public function taskLogout(): ResponseInterface
+    {
+        // We do not need to check the nonce here as user session has been expired or user hasn't fully logged in (2FA).
+        // Just be sure we terminate the current session.
+        $login = $this->getLogin();
+        $event = $login->logout(['admin' => true], ['return_event' => true]);
+
+        $event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info');
+        $message = $event->getMessage();
+        if ($message) {
+            $this->getSession()->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]);
+        }
+
+        return $this->createRedirectResponse('/');
+    }
+
+    /**
+     * Handle 2FA verification.
+     *
+     * @return ResponseInterface
+     */
+    public function taskTwofa(): ResponseInterface
+    {
+        $user = $this->getUser();
+        if (!$this->is2FA($user)) {
+            Admin::DEBUG && Admin::addDebugMessage('Admin login: user is not logged in or does not have 2FA enabled', $user);
+
+            // Task is visible only for users who have enabled 2FA.
+            return $this->createRedirectResponse('/');
+        }
+
+        $login = $this->getLogin();
+
+        $this->page = $this->createPage('login');
+        $this->form = $this->getForm('login-twofa');
+        try {
+            $this->checkNonce();
+        } catch (PageExpiredException $e) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+
+            // Failed 2FA nonce check, logout and redirect.
+            $login->logout(['admin' => true]);
+            $this->form->reset();
+
+            return $this->createRedirectResponse('/');
+        }
+
+
+        $post = $this->getPost();
+        $data = $post['data'] ?? [];
+
+        try {
+            $twoFa = $login->twoFactorAuth();
+        } catch (TwoFactorAuthException $e) {
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $twoFa = null;
+        }
+
+        $code = $data['2fa_code'] ?? '';
+        $secret = $user->twofa_secret ?? '';
+        $twofa_valid = $twoFa->verifyCode($secret, $code);
+
+        $yubikey_otp = $data['yubikey_otp'] ?? '';
+        $yubikey_id = $user->yubikey_id ?? '';
+        $yubikey_valid = $twoFa->verifyYubikeyOTP($yubikey_id, $yubikey_otp);
+
+        $redirect = (string)$this->getRequest()->getUri();
+
+        if (null === $twoFa || !$user->authenticated || (!$twofa_valid && !$yubikey_valid) ) {
+            Admin::DEBUG && Admin::addDebugMessage('Admin login: 2FA check failed, log out!');
+
+            // Failed 2FA auth, logout and redirect to the current page.
+            $login->logout(['admin' => true]);
+
+            $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
+
+            $this->form->reset();
+
+            return $this->createRedirectResponse($redirect);
+        }
+
+        // Successful 2FA, authorize user and redirect.
+        Grav::instance()['user']->authorized = true;
+
+        Admin::DEBUG && Admin::addDebugMessage('Admin login: 2FA check succeeded, authorize user and redirect');
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'));
+
+        $this->form->reset();
+
+        return $this->createRedirectResponse($redirect);
+    }
+
+    /**
+     * Handle the reset password action.
+     *
+     * @param string|null $username
+     * @param string|null $token
+     * @return ResponseInterface
+     */
+    public function taskReset(string $username = null, string $token = null): ResponseInterface
+    {
+        $this->page = $this->createPage('reset');
+        $this->form = $this->getForm('admin-login-reset');
+        try {
+            $this->checkNonce();
+        } catch (PageExpiredException $e) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+
+            return $this->createDisplayResponse();
+        }
+
+
+        $post = $this->getPost();
+        $data = $post['data'] ?? [];
+        $users = $this->getAccounts();
+
+        $username = $username ?? $data['username'] ?? null;
+        $token = $token ?? $data['token'] ?? null;
+
+        $user = $username ? $users->load($username) : null;
+        $password = $data['password'];
+
+        if ($user && $user->exists() && !empty($user->get('reset'))) {
+            [$good_token, $expire] = explode('::', $user->get('reset'));
+
+            if ($good_token === $token) {
+                if (time() > $expire) {
+                    $this->setMessage($this->translate('PLUGIN_ADMIN.RESET_LINK_EXPIRED'), 'error');
+
+                    $this->form->reset();
+
+                    return $this->createRedirectResponse('/forgot');
+                }
+
+                // Set new password.
+                $login = $this->getLogin();
+                try {
+                    $login->validateField('password1', $password);
+                } catch (\RuntimeException $e) {
+                    $this->setMessage($this->translate($e->getMessage()), 'error');
+
+                    return $this->createRedirectResponse("/reset/u/{$username}/{$token}");
+                }
+
+                $user->undef('hashed_password');
+                $user->undef('reset');
+                $user->update(['password' => $password]);
+                $user->save();
+
+                $this->form->reset();
+
+                $this->setMessage($this->translate('PLUGIN_ADMIN.RESET_PASSWORD_RESET'));
+
+                return $this->createRedirectResponse('/login');
+            }
+
+            Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: Token %s is not good', $token));
+        } else {
+            Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: User %s does not exist or has not requested reset', $username));
+        }
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
+
+        $this->form->reset();
+
+        return $this->createRedirectResponse('/forgot');
+    }
+
+    /**
+     * Handle the email password recovery procedure.
+     *
+     * Sends email to the user.
+     *
+     * @return ResponseInterface
+     */
+    public function taskForgot(): ResponseInterface
+    {
+        $this->page = $this->createPage('forgot');
+        $this->form = $this->getForm('admin-login-forgot');
+        try {
+            $this->checkNonce();
+        } catch (PageExpiredException $e) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+
+            return $this->createDisplayResponse();
+        }
+
+
+        $post = $this->getPost();
+        $data = $post['data'] ?? [];
+        $login = $this->getLogin();
+        $users = $this->getAccounts();
+        $email = $this->getEmail();
+
+        $current = (string)$this->getRequest()->getUri();
+
+        $search = isset($data['username']) ? strip_tags($data['username']) : '';
+        $user = !empty($search) ? $users->load($search) : null;
+        $username = $user->username ?? null;
+        $to = $user->email ?? null;
+
+        // Only send email to users which are enabled and have an email address.
+        if (null === $user || $user->state !== 'enabled' || !$to) {
+            Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed sending email: %s <%s> was not found or is blocked', $search, $to ?? 'N/A'));
+
+            $this->form->reset();
+
+            $this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'));
+
+            return $this->createRedirectResponse($current);
+        }
+
+        $config = $this->getConfig();
+
+        // Check rate limit for the user.
+        $rateLimiter = $login->getRateLimiter('pw_resets');
+        $rateLimiter->registerRateLimitedAction($username);
+        if ($rateLimiter->isRateLimited($username)) {
+            Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed sending email: user %s <%s> is rate limited', $search, $to));
+
+            $this->form->reset();
+
+            $interval = $config->get('plugins.login.max_pw_resets_interval', 2);
+
+            $this->setMessage($this->translate('PLUGIN_LOGIN.FORGOT_CANNOT_RESET_IT_IS_BLOCKED', $to, $interval), 'error');
+
+            return $this->createRedirectResponse($current);
+        }
+
+        $token  = md5(uniqid(mt_rand(), true));
+        $expire = time() + 3600; // 1 hour
+
+        $user->set('reset', $token . '::' . $expire);
+        $user->save();
+
+        $from = $config->get('plugins.email.from');
+        if (empty($from)) {
+            Admin::DEBUG && Admin::addDebugMessage('Failed sending email: from address is not configured in email plugin');
+
+            $this->form->reset();
+
+            $this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
+
+            return $this->createRedirectResponse($current);
+        }
+
+        // Do not trust username from the request.
+        $fullname = $user->fullname ?: $username;
+        $author = $config->get('site.author.name', '');
+        $sitename = $config->get('site.title', 'Website');
+        $reset_link = $this->getAbsoluteAdminUrl("/reset/u/{$username}/{$token}");
+
+        // For testing only!
+        //Admin::DEBUG && Admin::addDebugMessage(sprintf('Reset link: %s', $reset_link));
+
+        $subject = $this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_SUBJECT', $sitename);
+        $content = $this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_BODY', $fullname, $reset_link, $author, $sitename);
+
+        $this->grav['twig']->init();
+        $body = $this->grav['twig']->processTemplate('email/base.html.twig', ['content' => $content]);
+
+        try {
+            $message = $email->message($subject, $body, 'text/html')->setFrom($from)->setTo($to);
+            $sent = $email->send($message);
+            if ($sent < 1) {
+                throw new \RuntimeException('Sending email failed');
+            }
+
+            $this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'));
+        } catch (\Exception $e) {
+            $rateLimiter->resetRateLimit($username);
+
+            /** @var Debugger $debugger */
+            $debugger = $this->grav['debugger'];
+            $debugger->addException($e);
+
+            $this->form->reset();
+
+            $this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_FAILED_TO_EMAIL'), 'error');
+
+            return $this->createRedirectResponse('/forgot');
+        }
+
+        $this->form->reset();
+
+        return $this->createRedirectResponse('/login');
+    }
+
+    /**
+     * @return ResponseInterface
+     */
+    public function taskRegister(): ResponseInterface
+    {
+        $this->page = $this->createPage('register');
+        $this->form = $form = $this->getForm('admin-login-register');
+        try {
+            $this->checkNonce();
+        } catch (PageExpiredException $e) {
+            $this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
+
+            return $this->createDisplayResponse();
+        }
+
+        // Note: Calls $this->doRegistration() to perform the user registration.
+        $form->handleRequest($this->getRequest());
+        $error = $form->getError();
+        $errors = $form->getErrors();
+        if ($error || $errors) {
+            foreach ($errors as $field => $list) {
+                foreach ((array)$list as $message) {
+                    if ($message !== $error) {
+                        $this->setMessage($message, 'error');
+                    }
+                }
+            }
+
+            return $this->createDisplayResponse();
+        }
+
+        $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'));
+
+        return $this->createRedirectResponse('/');
+    }
+
+    /**
+     * @param UserInterface $user
+     * @return bool
+     */
+    protected function is2FA(UserInterface $user): bool
+    {
+        return $user && $user->authenticated && !$user->authorized && $user->get('twofa_enabled');
+    }
+
+    /**
+     * @param string $name
+     * @return callable
+     */
+    protected function getFormSubmitMethod(string $name): callable
+    {
+        switch ($name) {
+            case 'login':
+            case 'login-twofa':
+            case 'admin-login-forgot':
+            case 'admin-login-reset':
+                return static function(array $data, array $files) {};
+            case 'admin-login-register':
+                return function(array $data, array $files) {
+                    $this->doRegistration($data, $files);
+                };
+        }
+
+        throw new \RuntimeException('Unknown form');
+    }
+
+    /**
+     * Called by registration form when calling handleRequest().
+     *
+     * @param array $data
+     * @param array $files
+     */
+    private function doRegistration(array $data, array $files): void
+    {
+        if (Admin::doAnyUsersExist()) {
+            throw new \RuntimeException('A user account already exists, please create an admin account manually.', 400);
+        }
+
+        $login = $this->getLogin();
+        if (!$login) {
+            throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.PLUGIN_LOGIN_DISABLED', 500));
+        }
+
+        $data['title'] = $data['title'] ?? 'Administrator';
+
+        // Do not allow form to set the following fields (make super user):
+        $data['state'] = 'enabled';
+        $data['access'] = ['admin' => ['login' => true, 'super' => true], 'site' => ['login' => true]];
+        unset($data['groups']);
+
+        // Create user.
+        $user = $login->register($data, $files);
+
+        // Log in the new super admin user.
+        unset($this->grav['user']);
+        $this->grav['user'] = $user;
+        $this->grav['session']->user = $user;
+        $user->authenticated = true;
+        $user->authorized = $user->authorize('admin.login') ?? false;
+    }
+
+    /**
+     * @return Login
+     */
+    private function getLogin(): Login
+    {
+        return $this->grav['login'];
+    }
+
+    /**
+     * @return Email
+     */
+    private function getEmail(): Email
+    {
+        return $this->grav['Email'];
+    }
+
+    /**
+     * @return UserCollectionInterface
+     */
+    private function getAccounts(): UserCollectionInterface
+    {
+        return $this->grav['accounts'];
+    }
+}

+ 442 - 0
plugins/admin/classes/plugin/Gpm.php

@@ -0,0 +1,442 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Cache;
+use Grav\Common\Grav;
+use Grav\Common\GPM\GPM as GravGPM;
+use Grav\Common\GPM\Licenses;
+use Grav\Common\GPM\Installer;
+use Grav\Common\GPM\Upgrader;
+use Grav\Common\HTTP\Response;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\GPM\Common\Package;
+
+/**
+ * Class Gpm
+ *
+ * @package Grav\Plugin\Admin
+ */
+class Gpm
+{
+    // Probably should move this to Grav DI container?
+    /** @var GravGPM */
+    protected static $GPM;
+
+    public static function GPM()
+    {
+        if (!static::$GPM) {
+            static::$GPM = new GravGPM();
+        }
+
+        return static::$GPM;
+    }
+
+    /**
+     * Default options for the install
+     *
+     * @var array
+     */
+    protected static $options = [
+        'destination'     => GRAV_ROOT,
+        'overwrite'       => true,
+        'ignore_symlinks' => true,
+        'skip_invalid'    => true,
+        'install_deps'    => true,
+        'theme'           => false
+    ];
+
+    /**
+     * @param Package[]|string[]|string $packages
+     * @param array                     $options
+     *
+     * @return string|bool
+     */
+    public static function install($packages, array $options)
+    {
+        $options = array_merge(self::$options, $options);
+
+        if (!Installer::isGravInstance($options['destination']) || !Installer::isValidDestination($options['destination'],
+                [Installer::EXISTS, Installer::IS_LINK])
+        ) {
+            return false;
+        }
+
+        $packages = is_array($packages) ? $packages : [$packages];
+        $count    = count($packages);
+
+        $packages = array_filter(array_map(function ($p) {
+            return !is_string($p) ? $p instanceof Package ? $p : false : self::GPM()->findPackage($p);
+        }, $packages));
+
+        if (!$options['skip_invalid'] && $count !== count($packages)) {
+            return false;
+        }
+
+        $messages = '';
+
+        foreach ($packages as $package) {
+            if (isset($package->dependencies) && $options['install_deps']) {
+                $result = static::install($package->dependencies, $options);
+
+                if (!$result) {
+                    return false;
+                }
+            }
+
+            // Check destination
+            Installer::isValidDestination($options['destination'] . DS . $package->install_path);
+
+            if (!$options['overwrite'] && Installer::lastErrorCode() === Installer::EXISTS) {
+                return false;
+            }
+
+            if (!$options['ignore_symlinks'] && Installer::lastErrorCode() === Installer::IS_LINK) {
+                return false;
+            }
+
+            $license = Licenses::get($package->slug);
+            $local   = static::download($package, $license);
+
+            Installer::install($local, $options['destination'],
+                ['install_path' => $package->install_path, 'theme' => $options['theme']]);
+            Folder::delete(dirname($local));
+
+            $errorCode = Installer::lastErrorCode();
+            if ($errorCode) {
+                $msg = Installer::lastErrorMsg();
+                throw new \RuntimeException($msg);
+            }
+
+            if (count($packages) === 1) {
+                $message = Installer::getMessage();
+                if ($message) {
+                    return $message;
+                }
+
+                $messages .= $message;
+            }
+        }
+
+        Cache::clearCache();
+
+        return $messages ?: true;
+    }
+
+    /**
+     * @param Package[]|string[]|string $packages
+     * @param array                     $options
+     *
+     * @return string|bool
+     */
+    public static function update($packages, array $options)
+    {
+        $options['overwrite'] = true;
+
+        return static::install($packages, $options);
+    }
+
+    /**
+     * @param Package[]|string[]|string $packages
+     * @param array                     $options
+     *
+     * @return string|bool
+     */
+    public static function uninstall($packages, array $options)
+    {
+        $options = array_merge(self::$options, $options);
+
+        $packages = (array)$packages;
+        $count    = count($packages);
+
+        $packages = array_filter(array_map(function ($p) {
+
+            if (is_string($p)) {
+                $p      = strtolower($p);
+                $plugin = static::GPM()->getInstalledPlugin($p);
+                $p      = $plugin ?: static::GPM()->getInstalledTheme($p);
+            }
+
+            return $p instanceof Package ? $p : false;
+
+        }, $packages));
+
+        if (!$options['skip_invalid'] && $count !== count($packages)) {
+            return false;
+        }
+
+        foreach ($packages as $package) {
+
+            $location = Grav::instance()['locator']->findResource($package->package_type . '://' . $package->slug);
+
+            // Check destination
+            Installer::isValidDestination($location);
+
+            if (!$options['ignore_symlinks'] && Installer::lastErrorCode() === Installer::IS_LINK) {
+                return false;
+            }
+
+            Installer::uninstall($location);
+
+            $errorCode = Installer::lastErrorCode();
+            if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) {
+                $msg = Installer::lastErrorMsg();
+                throw new \RuntimeException($msg);
+            }
+
+            if (count($packages) === 1) {
+                $message = Installer::getMessage();
+                if ($message) {
+                    return $message;
+                }
+            }
+        }
+
+        Cache::clearCache();
+
+        return true;
+    }
+
+    /**
+     * Direct install a file
+     *
+     * @param string $package_file
+     *
+     * @return string|bool
+     */
+    public static function directInstall($package_file)
+    {
+        if (!$package_file) {
+            return Admin::translate('PLUGIN_ADMIN.NO_PACKAGE_NAME');
+        }
+
+        $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
+        $tmp_zip = $tmp_dir . '/Grav-' . uniqid('', false);
+
+        if (Response::isRemote($package_file)) {
+            $zip = GravGPM::downloadPackage($package_file, $tmp_zip);
+        } else {
+            $zip = GravGPM::copyPackage($package_file, $tmp_zip);
+        }
+
+        if (file_exists($zip)) {
+            $tmp_source = $tmp_dir . '/Grav-' . uniqid('', false);
+            $extracted  = Installer::unZip($zip, $tmp_source);
+
+            if (!$extracted) {
+                Folder::delete($tmp_source);
+                Folder::delete($tmp_zip);
+                return Admin::translate('PLUGIN_ADMIN.PACKAGE_EXTRACTION_FAILED');
+            }
+
+            $type = GravGPM::getPackageType($extracted);
+
+            if (!$type) {
+                Folder::delete($tmp_source);
+                Folder::delete($tmp_zip);
+                return Admin::translate('PLUGIN_ADMIN.NOT_VALID_GRAV_PACKAGE');
+            }
+
+            if ($type === 'grav') {
+                Installer::isValidDestination(GRAV_ROOT . '/system');
+                if (Installer::IS_LINK === Installer::lastErrorCode()) {
+                    Folder::delete($tmp_source);
+                    Folder::delete($tmp_zip);
+                    return Admin::translate('PLUGIN_ADMIN.CANNOT_OVERWRITE_SYMLINKS');
+                }
+
+                static::upgradeGrav($zip, $extracted);
+            } else {
+                $name = GravGPM::getPackageName($extracted);
+
+                if (!$name) {
+                    Folder::delete($tmp_source);
+                    Folder::delete($tmp_zip);
+                    return Admin::translate('PLUGIN_ADMIN.NAME_COULD_NOT_BE_DETERMINED');
+                }
+
+                $install_path = GravGPM::getInstallPath($type, $name);
+                $is_update    = file_exists($install_path);
+
+                Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
+                if (Installer::lastErrorCode() === Installer::IS_LINK) {
+                    Folder::delete($tmp_source);
+                    Folder::delete($tmp_zip);
+                    return Admin::translate('PLUGIN_ADMIN.CANNOT_OVERWRITE_SYMLINKS');
+                }
+
+                Installer::install($zip, GRAV_ROOT,
+                    ['install_path' => $install_path, 'theme' => $type === 'theme', 'is_update' => $is_update],
+                    $extracted);
+            }
+
+            Folder::delete($tmp_source);
+
+            if (Installer::lastErrorCode()) {
+                return Installer::lastErrorMsg();
+            }
+
+        } else {
+            return Admin::translate('PLUGIN_ADMIN.ZIP_PACKAGE_NOT_FOUND');
+        }
+
+        Folder::delete($tmp_zip);
+        Cache::clearCache();
+
+        return true;
+    }
+
+    /**
+     * @param Package $package
+     *
+     * @return string
+     */
+    private static function download(Package $package, $license = null)
+    {
+        $query = '';
+
+        if ($package->premium) {
+            $query = \json_encode(array_merge($package->premium, [
+                'slug'        => $package->slug,
+                'license_key' => $license,
+                'sid' => md5(GRAV_ROOT)
+            ]));
+
+            $query = '?d=' . base64_encode($query);
+        }
+
+        try {
+            $contents = Response::get($package->zipball_url . $query, []);
+        } catch (\Exception $e) {
+            throw new \RuntimeException($e->getMessage());
+        }
+
+        $tmp_dir = Admin::getTempDir() . '/Grav-' . uniqid('', false);
+        Folder::mkdir($tmp_dir);
+
+        $bad_chars = array_merge(array_map('chr', range(0, 31)), ['<', '>', ':', '"', '/', '\\', '|', '?', '*']);
+
+        $filename = $package->slug . str_replace($bad_chars, '', \Grav\Common\Utils::basename($package->zipball_url));
+        $filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename);
+
+        file_put_contents($tmp_dir . DS . $filename . '.zip', $contents);
+
+        return $tmp_dir . DS . $filename . '.zip';
+    }
+
+    /**
+     * @param array  $package
+     * @param string $tmp
+     *
+     * @return string
+     */
+    private static function _downloadSelfupgrade(array $package, $tmp)
+    {
+        $output = Response::get($package['download'], []);
+        Folder::mkdir($tmp);
+        file_put_contents($tmp . DS . $package['name'], $output);
+
+        return $tmp . DS . $package['name'];
+    }
+
+    /**
+     * @return bool
+     */
+    public static function selfupgrade()
+    {
+        $upgrader = new Upgrader();
+
+        if (!Installer::isGravInstance(GRAV_ROOT)) {
+            return false;
+        }
+
+        if (is_link(GRAV_ROOT . DS . 'index.php')) {
+            Installer::setError(Installer::IS_LINK);
+
+            return false;
+        }
+
+        if (method_exists($upgrader, 'meetsRequirements') &&
+            method_exists($upgrader, 'minPHPVersion') &&
+            !$upgrader->meetsRequirements()) {
+            $error   = [];
+            $error[] = '<p>Grav has increased the minimum PHP requirement.<br />';
+            $error[] = 'You are currently running PHP <strong>' . phpversion() . '</strong>';
+            $error[] = ', but PHP <strong>' . $upgrader->minPHPVersion() . '</strong> is required.</p>';
+            $error[] = '<p><a href="https://getgrav.org/blog/changing-php-requirements-to-5.5" class="button button-small secondary">Additional information</a></p>';
+
+            Installer::setError(implode("\n", $error));
+
+            return false;
+        }
+
+        $update = $upgrader->getAssets()['grav-update'];
+        $tmp    = Admin::getTempDir() . '/Grav-' . uniqid('', false);
+        if ($tmp) {
+            $file   = self::_downloadSelfupgrade($update, $tmp);
+            $folder = Installer::unZip($file, $tmp . '/zip');
+            $keepFolder = false;
+        } else {
+            // If you make $tmp empty, you can install your local copy of Grav (for testing purposes only).
+            $file = 'grav.zip';
+            $folder = '~/phpstorm/grav-clones/grav';
+            //$folder = '/home/matias/phpstorm/rockettheme/grav-devtools/grav-clones/grav';
+            $keepFolder = true;
+        }
+
+        static::upgradeGrav($file, $folder, $keepFolder);
+
+        $errorCode = Installer::lastErrorCode();
+
+        if ($tmp) {
+            Folder::delete($tmp);
+        }
+
+        return !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)));
+    }
+
+    private static function upgradeGrav($zip, $folder, $keepFolder = false)
+    {
+        static $ignores = [
+            'backup',
+            'cache',
+            'images',
+            'logs',
+            'tmp',
+            'user',
+            '.htaccess',
+            'robots.txt'
+        ];
+
+        if (!is_dir($folder)) {
+            Installer::setError('Invalid source folder');
+        }
+
+        try {
+            $script = $folder . '/system/install.php';
+            /** Install $installer */
+            if ((file_exists($script) && $install = include $script) && is_callable($install)) {
+                $install($zip);
+            } else {
+                Installer::install(
+                    $zip,
+                    GRAV_ROOT,
+                    ['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $ignores],
+                    $folder,
+                    $keepFolder
+                );
+
+                Cache::clearCache();
+            }
+        } catch (\Exception $e) {
+            Installer::setError($e->getMessage());
+        }
+    }
+}

+ 310 - 0
plugins/admin/classes/plugin/Popularity.php

@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Config\Config;
+use Grav\Common\Grav;
+use Grav\Common\Page\Interfaces\PageInterface;
+
+/**
+ * Class Popularity
+ * @package Grav\Plugin
+ */
+class Popularity
+{
+    /** @var Config */
+    protected $config;
+    protected $data_path;
+
+    protected $daily_file;
+    protected $monthly_file;
+    protected $totals_file;
+    protected $visitors_file;
+
+    protected $daily_data;
+    protected $monthly_data;
+    protected $totals_data;
+    protected $visitors_data;
+
+    const DAILY_FORMAT = 'd-m-Y';
+    const MONTHLY_FORMAT = 'm-Y';
+    const DAILY_FILE = 'daily.json';
+    const MONTHLY_FILE = 'monthly.json';
+    const TOTALS_FILE = 'totals.json';
+    const VISITORS_FILE = 'visitors.json';
+
+    public function __construct()
+    {
+        $this->config = Grav::instance()['config'];
+
+        $this->data_path     = Grav::instance()['locator']->findResource('log://popularity', true, true);
+        $this->daily_file    = $this->data_path . '/' . self::DAILY_FILE;
+        $this->monthly_file  = $this->data_path . '/' . self::MONTHLY_FILE;
+        $this->totals_file   = $this->data_path . '/' . self::TOTALS_FILE;
+        $this->visitors_file = $this->data_path . '/' . self::VISITORS_FILE;
+
+    }
+
+    public function trackHit()
+    {
+        // Don't track bot or crawler requests
+        if (!Grav::instance()['browser']->isHuman()) {
+            return;
+        }
+
+        // Respect visitors "do not track" setting
+        if (!Grav::instance()['browser']->isTrackable()) {
+            return;
+        }
+
+        /** @var PageInterface $page */
+        $page         = Grav::instance()['page'];
+        $relative_url = str_replace(Grav::instance()['base_url_relative'], '', $page->url());
+
+        // Don't track error pages or pages that have no route
+        if ($page->template() === 'error' || !$page->route()) {
+            return;
+        }
+
+        // Make sure no 'widcard-style' ignore matches this url
+        foreach ((array)$this->config->get('plugins.admin.popularity.ignore') as $ignore) {
+            if (fnmatch($ignore, $relative_url)) {
+                return;
+            }
+        }
+
+        // initial creation if it doesn't exist
+        if (!file_exists($this->data_path)) {
+            mkdir($this->data_path);
+            $this->flushPopularity();
+        }
+
+        // Update the data we want to track
+        $this->updateDaily();
+        $this->updateMonthly();
+        $this->updateTotals($page->route());
+        $this->updateVisitors(Grav::instance()['uri']->ip());
+
+    }
+
+    protected function updateDaily()
+    {
+
+        if (!$this->daily_data) {
+            $this->daily_data = $this->getData($this->daily_file);
+        }
+
+        $day_month_year = date(self::DAILY_FORMAT);
+
+        // get the daily access count
+        if (array_key_exists($day_month_year, $this->daily_data)) {
+            $this->daily_data[$day_month_year] = (int)$this->daily_data[$day_month_year] + 1;
+        } else {
+            $this->daily_data[$day_month_year] = 1;
+        }
+
+        // keep correct number as set by history
+        $count = (int)$this->config->get('plugins.admin.popularity.history.daily', 30);
+        $total = count($this->daily_data);
+
+        if ($total > $count) {
+            $this->daily_data = array_slice($this->daily_data, -$count, $count, true);
+        }
+
+        file_put_contents($this->daily_file, json_encode($this->daily_data));
+    }
+
+    /**
+     * @return array
+     */
+    public function getDailyChartData()
+    {
+        if (!$this->daily_data) {
+            $this->daily_data = $this->getData($this->daily_file);
+        }
+
+        $limit      = (int)$this->config->get('plugins.admin.popularity.dashboard.days_of_stats', 7);
+        $chart_data = array_slice($this->daily_data, -$limit, $limit);
+
+        $labels = [];
+        $data   = [];
+
+        /** @var Admin $admin */
+        $admin = Grav::instance()['admin'];
+        foreach ($chart_data as $date => $count) {
+            $labels[] = $admin::translate([
+                'PLUGIN_ADMIN.' . strtoupper(date('D', strtotime($date)))]) .
+                '<br>' . date('M d', strtotime($date));
+            $data[]   = $count;
+        }
+
+        return ['labels' => $labels, 'data' => $data];
+    }
+
+    /**
+     * @return int
+     */
+    public function getDailyTotal()
+    {
+        if (!$this->daily_data) {
+            $this->daily_data = $this->getData($this->daily_file);
+        }
+
+        if (isset($this->daily_data[date(self::DAILY_FORMAT)])) {
+            return $this->daily_data[date(self::DAILY_FORMAT)];
+        }
+
+        return 0;
+    }
+
+    /**
+     * @return int
+     */
+    public function getWeeklyTotal()
+    {
+        if (!$this->daily_data) {
+            $this->daily_data = $this->getData($this->daily_file);
+        }
+
+        $day   = 0;
+        $total = 0;
+        foreach (array_reverse($this->daily_data) as $daily) {
+            $total += $daily;
+            $day++;
+            if ($day === 7) {
+                break;
+            }
+        }
+
+        return $total;
+    }
+
+    /**
+     * @return int
+     */
+    public function getMonthlyTotal()
+    {
+        if (!$this->monthly_data) {
+            $this->monthly_data = $this->getData($this->monthly_file);
+        }
+        if (isset($this->monthly_data[date(self::MONTHLY_FORMAT)])) {
+            return $this->monthly_data[date(self::MONTHLY_FORMAT)];
+        }
+
+        return 0;
+    }
+
+    protected function updateMonthly()
+    {
+
+        if (!$this->monthly_data) {
+            $this->monthly_data = $this->getData($this->monthly_file);
+        }
+
+        $month_year = date(self::MONTHLY_FORMAT);
+
+        // get the monthly access count
+        if (array_key_exists($month_year, $this->monthly_data)) {
+            $this->monthly_data[$month_year] = (int)$this->monthly_data[$month_year] + 1;
+        } else {
+            $this->monthly_data[$month_year] = 1;
+        }
+
+        // keep correct number as set by history
+        $count              = (int)$this->config->get('plugins.admin.popularity.history.monthly', 12);
+        $total              = count($this->monthly_data);
+        $this->monthly_data = array_slice($this->monthly_data, $total - $count, $count);
+
+
+        file_put_contents($this->monthly_file, json_encode($this->monthly_data));
+    }
+
+    /**
+     * @return array
+     */
+    protected function getMonthyChartData()
+    {
+        if (!$this->monthly_data) {
+            $this->monthly_data = $this->getData($this->monthly_file);
+        }
+
+        $labels = [];
+        $data   = [];
+
+        foreach ($this->monthly_data as $date => $count) {
+            $labels[] = date('M', strtotime($date));
+            $data[]   = $count;
+        }
+
+        return ['labels' => $labels, 'data' => $data];
+    }
+
+    /**
+     * @param string $url
+     */
+    protected function updateTotals($url)
+    {
+        if (!$this->totals_data) {
+            $this->totals_data = $this->getData($this->totals_file);
+        }
+
+        // get the totals for this url
+        if (array_key_exists($url, $this->totals_data)) {
+            $this->totals_data[$url] = (int)$this->totals_data[$url] + 1;
+        } else {
+            $this->totals_data[$url] = 1;
+        }
+
+        file_put_contents($this->totals_file, json_encode($this->totals_data));
+    }
+
+    /**
+     * @param string $ip
+     */
+    protected function updateVisitors($ip)
+    {
+        if (!$this->visitors_data) {
+            $this->visitors_data = $this->getData($this->visitors_file);
+        }
+
+        // update with current timestamp
+        $this->visitors_data[hash('sha1', $ip)] = time();
+        $visitors                 = $this->visitors_data;
+        arsort($visitors);
+
+        $count               = (int)$this->config->get('plugins.admin.popularity.history.visitors', 20);
+        $this->visitors_data = array_slice($visitors, 0, $count, true);
+
+        file_put_contents($this->visitors_file, json_encode($this->visitors_data));
+    }
+
+    /**
+     * @param string $path
+     *
+     * @return array
+     */
+    protected function getData($path)
+    {
+        if (file_exists($path)) {
+            return (array)json_decode(file_get_contents($path), true);
+        }
+
+        return [];
+    }
+
+
+    public function flushPopularity()
+    {
+        file_put_contents($this->daily_file, []);
+        file_put_contents($this->monthly_file, []);
+        file_put_contents($this->totals_file, []);
+        file_put_contents($this->visitors_file, []);
+    }
+}

+ 79 - 0
plugins/admin/classes/plugin/Router.php

@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Grav;
+use Grav\Common\Processors\ProcessorBase;
+use Grav\Framework\Route\Route;
+use Grav\Plugin\Admin\Routers\LoginRouter;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Server\RequestHandlerInterface;
+
+class Router extends ProcessorBase
+{
+    public $id = 'admin_router';
+    public $title = 'Admin Panel';
+
+    /** @var Admin */
+    protected $admin;
+
+    public function __construct(Grav $container, Admin $admin)
+    {
+        parent::__construct($container);
+
+        $this->admin = $admin;
+    }
+
+    /**
+     * Handle routing to the dashboard, group and build objects.
+     *
+     * @param ServerRequestInterface $request
+     * @param RequestHandlerInterface $handler
+     * @return ResponseInterface
+     */
+    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
+    {
+        $this->startTimer();
+
+        $context = $request->getAttributes();
+        $query = $request->getQueryParams();
+
+        /** @var Route $route */
+        $route = $context['route'];
+        $normalized = mb_strtolower(trim($route->getRoute(), '/'));
+        $parts = explode('/', $normalized);
+        array_shift($parts); // Admin path
+        $routeStr = implode('/', $parts);
+        $view = array_shift($parts);
+        $path = implode('/', $parts);
+        $task = $this->container['task'] ?? $query['task'] ?? null;
+        $action = $this->container['action'] ?? $query['action'] ?? null;
+
+        $params = ['view' => $view, 'route' => $routeStr, 'path' => $path, 'parts' => $parts, 'task' => $task, 'action' => $action];
+        $request = $request->withAttribute('admin', $params);
+
+        // Run login controller if user isn't fully logged in or asks to logout.
+        $user = $this->admin->user;
+        if (!$user->authorized || !$user->authorize('admin.login')) {
+            $params = (new LoginRouter())->matchServerRequest($request);
+            $request = $request->withAttribute('admin', $params + $request->getAttribute('admin'));
+        }
+
+        $this->admin->request = $request;
+
+        $response = $handler->handle($request);
+
+        $this->stopTimer();
+
+        // Never allow admin pages to be rendered in <frame>, <iframe>, <embed> or <object> for improved security.
+        return $response->withHeader('X-Frame-Options', 'DENY');
+    }
+}

+ 93 - 0
plugins/admin/classes/plugin/Routers/LoginRouter.php

@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin\Routers;
+
+use Grav\Plugin\Admin\Admin;
+use Grav\Plugin\Admin\Controllers\Login\LoginController;
+use Psr\Http\Message\ServerRequestInterface;
+
+class LoginRouter
+{
+    /** @var string[] */
+    private $taskTemplates = [
+        'logout' => 'login',
+        'twofa' => 'login',
+        'forgot' => 'forgot',
+        'reset' => 'reset'
+    ];
+
+    /**
+     * @param ServerRequestInterface $request
+     * @return array
+     */
+    public function matchServerRequest(ServerRequestInterface $request): array
+    {
+        $adminInfo = $request->getAttribute('admin');
+        $task = $adminInfo['task'];
+        $class = LoginController::class;
+
+        // Special controller for the new sites.
+        if (!Admin::doAnyUsersExist()) {
+            $method = $task === 'register' ? 'taskRegister' : 'displayRegister';
+
+            return [
+                'controller' => [
+                    'class' => $class,
+                    'method' => $method,
+                    'params' => []
+                ],
+                'template' => 'register',
+            ];
+        }
+
+        $httpMethod = $request->getMethod();
+        $template = $this->taskTemplates[$task] ?? $adminInfo['view'];
+        $params = [];
+
+        switch ($template) {
+            case 'forgot':
+                break;
+            case 'reset':
+                $path = $adminInfo['path'];
+                if (str_starts_with($path, 'u/')) {
+                    // Path is 'u/username/token'
+                    $parts = explode('/', $path, 4);
+                    $user = $parts[1] ?? null;
+                    $token = $parts[2] ?? null;
+                } else {
+                    // Old path used to be 'task:reset/user:username/token:token'
+                    if ($httpMethod === 'GET' || $httpMethod === 'HEAD') {
+                        $task = null;
+                    }
+                    $route = $request->getAttribute('route');
+                    $user  = $route->getGravParam('user');
+                    $token = $route->getGravParam('token');
+                }
+                $params = [$user, $token];
+                break;
+            default:
+                $template = 'login';
+        }
+
+        $method = ($task ? 'task' : 'display') . ucfirst($task ?? $template);
+        if (!method_exists($class, $method)) {
+            $method = 'displayUnauthorized';
+        }
+
+        return [
+            'controller' => [
+                'class' => $class,
+                'method' => $method,
+                'params' => $params
+            ],
+            'template' => $template,
+        ];
+    }
+}

+ 70 - 0
plugins/admin/classes/plugin/ScssCompiler.php

@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use ScssPhp\ScssPhp\Compiler;
+use ScssPhp\ScssPhp\ValueConverter;
+
+class ScssCompiler
+{
+    protected $compiler;
+
+    public function compiler()
+    {
+        if ($this->compiler === null) {
+            $this->reset();
+        }
+        return $this->compiler;
+    }
+
+    public function reset()
+    {
+        $this->compiler = new Compiler();
+        return $this;
+    }
+
+    public function setVariables(array $variables)
+    {
+//        $parsed = ValueConverter::fromPhp($variables);
+        $parsed = [];
+        foreach ($variables as $key => $value) {
+            $parsed[$key] = ValueConverter::parseValue($value);
+        }
+
+        $this->compiler()->addVariables($parsed);
+        return $this;
+    }
+
+    public function setImportPaths(array $paths)
+    {
+        $this->compiler()->setImportPaths($paths);
+        return $this;
+    }
+
+    public function compile(string $input_file, string $output_file)
+    {
+        $input = file_get_contents($input_file);
+        $output = $this->compiler()->compile($input);
+        file_put_contents($output_file, $output);
+        return $this;
+    }
+
+    public function compileAll(array $input_paths, string $output_file)
+    {
+        $input = '';
+        foreach ($input_paths as $input_file) {
+            $input .= trim(file_get_contents($input_file)) . "\n\n";
+        }
+        $output = $this->compiler()->compileString($input)->getCss();
+        file_put_contents($output_file, $output);
+        return $this;
+    }
+
+}

+ 59 - 0
plugins/admin/classes/plugin/ScssList.php

@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+class ScssList
+{
+    /** @var string[] */
+    protected $list = [];
+
+    /**
+     * ScssList constructor.
+     * @param string|null $item
+     */
+    public function __construct($item = null)
+    {
+        if ($item) {
+            $this->add($item);
+        }
+    }
+
+    /**
+     * @return array
+     */
+    public function all(): array
+    {
+        return $this->list;
+    }
+
+    /**
+     * @param string $item
+     * @return void
+     */
+    public function add($item): void
+    {
+        if ($item) {
+            $this->list[] = $item;
+        }
+    }
+
+    /**
+     * @param string $item
+     * @return void
+     */
+    public function remove($item): void
+    {
+        $pos = array_search($item, $this->list, true);
+        if ($pos) {
+            unset($this->list[$pos]);
+        }
+    }
+
+}

+ 29 - 0
plugins/admin/classes/plugin/Themes.php

@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+/**
+ * Admin theme object
+ *
+ * @author  RocketTheme
+ * @license MIT
+ */
+class Themes extends \Grav\Common\Themes
+{
+    public function init()
+    {
+        /** @var Themes $themes */
+        $themes = $this->grav['themes'];
+        $themes->configure();
+        $themes->initTheme();
+
+        $this->grav->fireEvent('onAdminThemeInitialized');
+    }
+}

+ 138 - 0
plugins/admin/classes/plugin/Twig/AdminTwigExtension.php

@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin\Twig;
+
+use Grav\Common\Data\Data;
+use Grav\Common\Grav;
+use Grav\Common\Page\Interfaces\PageInterface;
+use Grav\Common\Utils;
+use Grav\Common\Yaml;
+use Grav\Common\Language\Language;
+use Twig\Extension\AbstractExtension;
+use Twig\TwigFilter;
+use Twig\TwigFunction;
+use Grav\Plugin\Admin\Admin;
+
+class AdminTwigExtension extends AbstractExtension
+{
+    /** @var Grav */
+    protected $grav;
+
+    /** @var Language $lang */
+    protected $lang;
+
+    public function __construct()
+    {
+        $this->grav = Grav::instance();
+        $this->lang = $this->grav['user']->language;
+    }
+
+    public function getFilters(): array
+    {
+        return [
+            new TwigFilter('tu', [$this, 'tuFilter']),
+            new TwigFilter('toYaml', [$this, 'toYamlFilter']),
+            new TwigFilter('fromYaml', [$this, 'fromYamlFilter']),
+            new TwigFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
+            new TwigFilter('nested', [$this, 'nestedFilter']),
+            new TwigFilter('flatten', [$this, 'flattenFilter']),
+        ];
+    }
+
+    public function getFunctions(): array
+    {
+        return [
+            new TwigFunction('admin_route', [$this, 'adminRouteFunc']),
+            new TwigFunction('getPageUrl', [$this, 'getPageUrl']),
+            new TwigFunction('clone', [$this, 'cloneFunc']),
+            new TwigFunction('data', [$this, 'dataFunc']),
+        ];
+    }
+
+    public function nestedFilter($current, $name)
+    {
+        $path = explode('.', trim($name, '.'));
+
+        foreach ($path as $field) {
+            if (is_object($current) && isset($current->{$field})) {
+                $current = $current->{$field};
+            } elseif (is_array($current) && isset($current[$field])) {
+                $current = $current[$field];
+            } else {
+                return null;
+            }
+        }
+
+        return $current;
+    }
+
+    public function flattenFilter($array)
+    {
+        return Utils::arrayFlattenDotNotation($array);
+    }
+
+    public function cloneFunc($obj)
+    {
+        return clone $obj;
+    }
+
+    public function adminRouteFunc(string $route = '', string $languageCode = null)
+    {
+        /** @var Admin $admin */
+        $admin = Grav::instance()['admin'];
+
+        return $admin->getAdminRoute($route, $languageCode)->toString(true);
+    }
+
+    public function getPageUrl(PageInterface $page)
+    {
+        /** @var Admin $admin */
+        $admin = Grav::instance()['admin'];
+
+        return $admin->getAdminRoute('/pages' . $page->rawRoute(), $page->language())->toString(true);
+    }
+
+    public static function tuFilter()
+    {
+        $args = func_get_args();
+        $numargs = count($args);
+        $lang = null;
+
+        if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) {
+            $lang = array_pop($args);
+        } elseif ($numargs === 2 && is_array($args[1])) {
+            $subs = array_pop($args);
+            $args = array_merge($args, $subs);
+        }
+
+        return Grav::instance()['admin']->translate($args, $lang);
+    }
+
+    public function toYamlFilter($value, $inline = null)
+    {
+        return Yaml::dump($value, $inline);
+
+    }
+
+    public function fromYamlFilter($value)
+    {
+        return Yaml::parse($value);
+    }
+
+    public function adminNicetimeFilter($date, $long_strings = true)
+    {
+        return Grav::instance()['admin']->adminNiceTime($date, $long_strings);
+    }
+
+    public function dataFunc(array $data, $blueprints = null)
+    {
+        return new Data($data, $blueprints);
+    }
+}

+ 61 - 0
plugins/admin/classes/plugin/Utils.php

@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Plugin\Admin;
+
+use Grav\Common\Grav;
+use Grav\Common\User\Interfaces\UserCollectionInterface;
+use Grav\Common\User\Interfaces\UserInterface;
+
+/**
+ * Admin utils class
+ *
+ * @license MIT
+ */
+class Utils
+{
+    /**
+     * Matches an email to a user
+     *
+     * @param string $email
+     *
+     * @return UserInterface
+     */
+    public static function findUserByEmail(string $email)
+    {
+        $grav = Grav::instance();
+
+        /** @var UserCollectionInterface $users */
+        $users = $grav['accounts'];
+
+        return $users->find($email, ['email']);
+    }
+
+    /**
+     * Generates a slug of the given string
+     *
+     * @param string $str
+     * @return string
+     */
+    public static function slug(string $str)
+    {
+        if (function_exists('transliterator_transliterate')) {
+            $str = transliterator_transliterate('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;', $str);
+        } else {
+            $str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
+        }
+
+        $str = strtolower($str);
+        $str = preg_replace('/[-\s]+/', '-', $str);
+        $str = preg_replace('/[^a-z0-9-]/i', '', $str);
+        $str = trim($str, '-');
+
+        return $str;
+    }
+}

+ 100 - 0
plugins/admin/classes/plugin/WhiteLabel.php

@@ -0,0 +1,100 @@
+<?php
+namespace Grav\Plugin\Admin;
+
+/**
+ * @package    Grav\Plugin\Admin
+ *
+ * @copyright  Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Grav;
+use Grav\Framework\File\File;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+use Symfony\Component\Yaml\Yaml;
+
+class WhiteLabel
+{
+    protected $grav;
+    protected $scss;
+
+    public function __construct()
+    {
+        $this->grav = Grav::instance();
+        $this->scss = new ScssCompiler();
+    }
+
+    public function compilePresetScss($config, $options = [
+            'input' => 'plugin://admin/themes/grav/scss/preset.scss',
+            'output' => 'asset://admin-preset.css'
+        ])
+    {
+        if (is_array($config)) {
+            $color_scheme   = $config['color_scheme'];
+        } else {
+            $color_scheme   = $config->get('whitelabel.color_scheme');
+        }
+
+        if ($color_scheme) {
+            /** @var UniformResourceLocator $locator */
+            $locator       = $this->grav['locator'];
+
+            // Use ScssList object to make it easier ot handle in event
+            $scss_list     = new ScssList($locator->findResource($options['input']));
+            $output_css    = $locator->findResource(($options['output']), true, true);
+
+            Folder::create(dirname($output_css));
+
+            Grav::instance()->fireEvent('onAdminCompilePresetSCSS', new Event(['scss' => $scss_list]));
+
+            // Convert bak to regular array now we have run the event
+            $input_scss = $scss_list->all();
+
+            $imports = [$locator->findResource('plugin://admin/themes/grav/scss')];
+            foreach ($input_scss as $scss) {
+                $input_path = dirname($scss);
+                if (!in_array($input_path, $imports)) {
+                    $imports[] = $input_path;
+                }
+            }
+
+            try {
+                $compiler = $this->scss->reset();
+
+                $compiler->setVariables($color_scheme['colors'] + $color_scheme['accents']);
+                $compiler->setImportPaths($imports);
+                $compiler->compileAll($input_scss, $output_css);
+            } catch (\Exception $e) {
+                return [false, $e->getMessage()];
+            }
+
+
+            return [true, 'Recompiled successfully'];
+
+        }
+        return [false, ' Could not be recompiled, missing color scheme...'];
+    }
+
+    public function exportPresetScsss($config, $location = 'asset://admin-theme-export.yaml')
+    {
+
+        if (isset($config['color_scheme'])) {
+
+            $color_scheme = $config['color_scheme'];
+
+            $body = Yaml::dump($color_scheme);
+
+            $file = new File($location);
+            $file->save($body);
+            // todo: handle errors/exceptions?
+
+            return [true, 'File created successfully'];
+
+        } else {
+            return [false, ' Could not export, missing color scheme...'];
+        }
+    }
+
+}

+ 18 - 0
plugins/admin/codeception.yml

@@ -0,0 +1,18 @@
+actor: Tester
+paths:
+    tests: tests
+    log: tests/_output
+    data: tests/_data
+    support: tests/_support
+    envs: tests/_envs
+settings:
+    bootstrap: _bootstrap.php
+    colors: true
+    memory_limit: 1024M
+extensions:
+    enabled:
+        - Codeception\Extension\RunFailed
+        # - Codeception\Extension\Recorder
+
+modules:
+    config:

+ 64 - 0
plugins/admin/composer.json

@@ -0,0 +1,64 @@
+{
+    "name": "getgrav/grav-plugin-admin",
+    "type": "grav-plugin",
+    "description": "Admin plugin for Grav CMS",
+    "keywords": ["admin", "plugin", "manager", "panel"],
+    "homepage": "https://github.com/getgrav/grav-plugin-admin",
+    "license": "MIT",
+    "authors": [
+        {
+            "name": "Team Grav",
+            "email": "devs@getgrav.org",
+            "homepage": "https://getgrav.org",
+            "role": "Developer"
+        }
+    ],
+    "support": {
+        "issues": "https://github.com/getgrav/grav-plugin-admin/issues",
+        "irc": "https://chat.getgrav.org",
+        "forum": "https://discourse.getgrav.org",
+        "docs": "https://github.com/getgrav/grav-plugin-admin/blob/master/README.md"
+    },
+    "require": {
+        "php": "^7.3.6 || ^8.0",
+        "ext-json": "*",
+        "scssphp/scssphp": "^1.11",
+        "laminas/laminas-zendframework-bridge": "^1.4",
+        "p3k/picofeed": "@stable"
+    },
+    "require-dev": {
+        "codeception/codeception": "^2.4",
+        "fzaninotto/faker": "^1.8",
+        "symfony/yaml": "~4.4",
+        "symfony/console": "~4.4",
+        "symfony/finder": "~4.4",
+        "symfony/event-dispatcher": "~4.4"
+    },
+    "replace": {
+        "symfony/polyfill-php72": "*",
+        "symfony/polyfill-php73": "*"
+    },
+    "autoload": {
+        "psr-4": {
+            "Grav\\Plugin\\Admin\\": "classes/plugin"
+        },
+        "classmap":  [
+            "admin.php"
+        ]
+    },
+    "config": {
+        "platform": {
+            "php": "7.3.6"
+        }
+    },
+    "scripts": {
+        "test": "vendor/bin/codecept run unit",
+        "test-windows": "vendor\\bin\\codecept run unit"
+    },
+    "repositories": [
+        {
+            "type": "vcs",
+            "url": "https://github.com/rhukster/picoFeed"
+        }
+    ]
+}

+ 3945 - 0
plugins/admin/composer.lock

@@ -0,0 +1,3945 @@
+{
+    "_readme": [
+        "This file locks the dependencies of your project to a known state",
+        "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+        "This file is @generated automatically"
+    ],
+    "content-hash": "bc8a9ba032ce28a2c93282b6f1a6e7b9",
+    "packages": [
+        {
+            "name": "laminas/laminas-xml",
+            "version": "1.4.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-xml.git",
+                "reference": "dcadeefdb6d7ed6b39d772b47e3845003d6ea60f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-xml/zipball/dcadeefdb6d7ed6b39d772b47e3845003d6ea60f",
+                "reference": "dcadeefdb6d7ed6b39d772b47e3845003d6ea60f",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-simplexml": "*",
+                "php": "^7.3 || ~8.0.0 || ~8.1.0"
+            },
+            "conflict": {
+                "zendframework/zendxml": "*"
+            },
+            "require-dev": {
+                "ext-iconv": "*",
+                "laminas/laminas-coding-standard": "~1.0.0",
+                "phpunit/phpunit": "^9.5.8",
+                "squizlabs/php_codesniffer": "3.6.1 as 2.9999999.9999999"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Laminas\\Xml\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Utility library for XML usage, best practices, and security in PHP",
+            "homepage": "https://laminas.dev",
+            "keywords": [
+                "laminas",
+                "security",
+                "xml"
+            ],
+            "support": {
+                "chat": "https://laminas.dev/chat",
+                "forum": "https://discourse.laminas.dev",
+                "issues": "https://github.com/laminas/laminas-xml/issues",
+                "rss": "https://github.com/laminas/laminas-xml/releases.atom",
+                "source": "https://github.com/laminas/laminas-xml"
+            },
+            "funding": [
+                {
+                    "url": "https://funding.communitybridge.org/projects/laminas-project",
+                    "type": "community_bridge"
+                }
+            ],
+            "time": "2021-11-30T02:16:35+00:00"
+        },
+        {
+            "name": "laminas/laminas-zendframework-bridge",
+            "version": "1.4.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+                "reference": "88bf037259869891afce6504cacc4f8a07b24d0f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/88bf037259869891afce6504cacc4f8a07b24d0f",
+                "reference": "88bf037259869891afce6504cacc4f8a07b24d0f",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.3 || ~8.0.0 || ~8.1.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^9.3",
+                "psalm/plugin-phpunit": "^0.15.1",
+                "squizlabs/php_codesniffer": "^3.5",
+                "vimeo/psalm": "^4.6"
+            },
+            "type": "library",
+            "extra": {
+                "laminas": {
+                    "module": "Laminas\\ZendFrameworkBridge"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/autoload.php"
+                ],
+                "psr-4": {
+                    "Laminas\\ZendFrameworkBridge\\": "src//"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+            "keywords": [
+                "ZendFramework",
+                "autoloading",
+                "laminas",
+                "zf"
+            ],
+            "support": {
+                "forum": "https://discourse.laminas.dev/",
+                "issues": "https://github.com/laminas/laminas-zendframework-bridge/issues",
+                "rss": "https://github.com/laminas/laminas-zendframework-bridge/releases.atom",
+                "source": "https://github.com/laminas/laminas-zendframework-bridge"
+            },
+            "funding": [
+                {
+                    "url": "https://funding.communitybridge.org/projects/laminas-project",
+                    "type": "community_bridge"
+                }
+            ],
+            "time": "2021-12-21T14:34:37+00:00"
+        },
+        {
+            "name": "p3k/picofeed",
+            "version": "1.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/rhukster/picofeed.git",
+                "reference": "8eacaa62f50a0935e26ca33f8d30d283344ca397"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/rhukster/picofeed/zipball/8eacaa62f50a0935e26ca33f8d30d283344ca397",
+                "reference": "8eacaa62f50a0935e26ca33f8d30d283344ca397",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-iconv": "*",
+                "ext-libxml": "*",
+                "ext-simplexml": "*",
+                "ext-xml": "*",
+                "laminas/laminas-xml": "^1.2",
+                "php": ">=5.3.0"
+            },
+            "replace": {
+                "miniflux/picofeed": "0.1.35"
+            },
+            "require-dev": {
+                "phpdocumentor/reflection-docblock": "2.0.4",
+                "phpunit/phpunit": "4.8.26",
+                "symfony/yaml": "2.8.7"
+            },
+            "suggest": {
+                "ext-curl": "PicoFeed will use cURL if present"
+            },
+            "bin": [
+                "picofeed"
+            ],
+            "type": "library",
+            "autoload": {
+                "psr-0": {
+                    "PicoFeed": "lib/"
+                }
+            },
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Frédéric Guillot"
+                }
+            ],
+            "description": "Modern library to handle RSS/Atom feeds",
+            "homepage": "https://github.com/aaronpk/picoFeed",
+            "support": {
+                "source": "https://github.com/rhukster/picofeed/tree/1.0.0"
+            },
+            "time": "2023-02-19T19:58:09+00:00"
+        },
+        {
+            "name": "scssphp/scssphp",
+            "version": "v1.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/scssphp/scssphp.git",
+                "reference": "33749d12c2569bb24071f94e9af828662dabb068"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/scssphp/scssphp/zipball/33749d12c2569bb24071f94e9af828662dabb068",
+                "reference": "33749d12c2569bb24071f94e9af828662dabb068",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "ext-json": "*",
+                "php": ">=5.6.0"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.4",
+                "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
+                "sass/sass-spec": "*",
+                "squizlabs/php_codesniffer": "~3.5",
+                "symfony/phpunit-bridge": "^5.1",
+                "thoughtbot/bourbon": "^7.0",
+                "twbs/bootstrap": "~5.0",
+                "twbs/bootstrap4": "4.6.1",
+                "zurb/foundation": "~6.5"
+            },
+            "suggest": {
+                "ext-iconv": "Can be used as fallback when ext-mbstring is not available",
+                "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv"
+            },
+            "bin": [
+                "bin/pscss"
+            ],
+            "type": "library",
+            "extra": {
+                "bamarni-bin": {
+                    "forward-command": false,
+                    "bin-links": false
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "ScssPhp\\ScssPhp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Anthon Pang",
+                    "email": "apang@softwaredevelopment.ca",
+                    "homepage": "https://github.com/robocoder"
+                },
+                {
+                    "name": "Cédric Morin",
+                    "email": "cedric@yterium.com",
+                    "homepage": "https://github.com/Cerdic"
+                }
+            ],
+            "description": "scssphp is a compiler for SCSS written in PHP.",
+            "homepage": "http://scssphp.github.io/scssphp/",
+            "keywords": [
+                "css",
+                "less",
+                "sass",
+                "scss",
+                "stylesheet"
+            ],
+            "support": {
+                "issues": "https://github.com/scssphp/scssphp/issues",
+                "source": "https://github.com/scssphp/scssphp/tree/v1.11.0"
+            },
+            "time": "2022-09-02T21:24:55+00:00"
+        }
+    ],
+    "packages-dev": [
+        {
+            "name": "behat/gherkin",
+            "version": "v4.9.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Behat/Gherkin.git",
+                "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Behat/Gherkin/zipball/0bc8d1e30e96183e4f36db9dc79caead300beff4",
+                "reference": "0bc8d1e30e96183e4f36db9dc79caead300beff4",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~7.2|~8.0"
+            },
+            "require-dev": {
+                "cucumber/cucumber": "dev-gherkin-22.0.0",
+                "phpunit/phpunit": "~8|~9",
+                "symfony/yaml": "~3|~4|~5"
+            },
+            "suggest": {
+                "symfony/yaml": "If you want to parse features, represented in YAML files"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-0": {
+                    "Behat\\Gherkin": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever.zet@gmail.com",
+                    "homepage": "http://everzet.com"
+                }
+            ],
+            "description": "Gherkin DSL parser for PHP",
+            "homepage": "http://behat.org/",
+            "keywords": [
+                "BDD",
+                "Behat",
+                "Cucumber",
+                "DSL",
+                "gherkin",
+                "parser"
+            ],
+            "support": {
+                "issues": "https://github.com/Behat/Gherkin/issues",
+                "source": "https://github.com/Behat/Gherkin/tree/v4.9.0"
+            },
+            "time": "2021-10-12T13:05:09+00:00"
+        },
+        {
+            "name": "codeception/codeception",
+            "version": "2.5.6",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Codeception/Codeception.git",
+                "reference": "b83a9338296e706fab2ceb49de8a352fbca3dc98"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Codeception/Codeception/zipball/b83a9338296e706fab2ceb49de8a352fbca3dc98",
+                "reference": "b83a9338296e706fab2ceb49de8a352fbca3dc98",
+                "shasum": ""
+            },
+            "require": {
+                "behat/gherkin": "^4.4.0",
+                "codeception/phpunit-wrapper": "^6.0.9|^7.0.6",
+                "codeception/stub": "^2.0",
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "facebook/webdriver": ">=1.1.3 <2.0",
+                "guzzlehttp/guzzle": ">=4.1.4 <7.0",
+                "guzzlehttp/psr7": "~1.0",
+                "php": ">=5.6.0 <8.0",
+                "symfony/browser-kit": ">=2.7 <5.0",
+                "symfony/console": ">=2.7 <5.0",
+                "symfony/css-selector": ">=2.7 <5.0",
+                "symfony/dom-crawler": ">=2.7 <5.0",
+                "symfony/event-dispatcher": ">=2.7 <5.0",
+                "symfony/finder": ">=2.7 <5.0",
+                "symfony/yaml": ">=2.7 <5.0"
+            },
+            "require-dev": {
+                "codeception/specify": "~0.3",
+                "facebook/graph-sdk": "~5.3",
+                "flow/jsonpath": "~0.2",
+                "monolog/monolog": "~1.8",
+                "pda/pheanstalk": "~3.0",
+                "php-amqplib/php-amqplib": "~2.4",
+                "predis/predis": "^1.0",
+                "squizlabs/php_codesniffer": "~2.0",
+                "symfony/process": ">=2.7 <5.0",
+                "vlucas/phpdotenv": "^3.0"
+            },
+            "suggest": {
+                "aws/aws-sdk-php": "For using AWS Auth in REST module and Queue module",
+                "codeception/phpbuiltinserver": "Start and stop PHP built-in web server for your tests",
+                "codeception/specify": "BDD-style code blocks",
+                "codeception/verify": "BDD-style assertions",
+                "flow/jsonpath": "For using JSONPath in REST module",
+                "league/factory-muffin": "For DataFactory module",
+                "league/factory-muffin-faker": "For Faker support in DataFactory module",
+                "phpseclib/phpseclib": "for SFTP option in FTP Module",
+                "stecman/symfony-console-completion": "For BASH autocompletion",
+                "symfony/phpunit-bridge": "For phpunit-bridge support"
+            },
+            "bin": [
+                "codecept"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": []
+            },
+            "autoload": {
+                "psr-4": {
+                    "Codeception\\": "src/Codeception",
+                    "Codeception\\Extension\\": "ext"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Michael Bodnarchuk",
+                    "email": "davert@mail.ua",
+                    "homepage": "http://codegyre.com"
+                }
+            ],
+            "description": "BDD-style testing framework",
+            "homepage": "http://codeception.com/",
+            "keywords": [
+                "BDD",
+                "TDD",
+                "acceptance testing",
+                "functional testing",
+                "unit testing"
+            ],
+            "support": {
+                "issues": "https://github.com/Codeception/Codeception/issues",
+                "source": "https://github.com/Codeception/Codeception/tree/2.5.6"
+            },
+            "time": "2019-04-24T11:28:19+00:00"
+        },
+        {
+            "name": "codeception/phpunit-wrapper",
+            "version": "7.8.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Codeception/phpunit-wrapper.git",
+                "reference": "dd44fc152433d27d3de03d59b4945449b3407af0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Codeception/phpunit-wrapper/zipball/dd44fc152433d27d3de03d59b4945449b3407af0",
+                "reference": "dd44fc152433d27d3de03d59b4945449b3407af0",
+                "shasum": ""
+            },
+            "require": {
+                "phpunit/php-code-coverage": "^6.0",
+                "phpunit/phpunit": "7.5.*",
+                "sebastian/comparator": "^3.0",
+                "sebastian/diff": "^3.0"
+            },
+            "require-dev": {
+                "codeception/specify": "*",
+                "vlucas/phpdotenv": "^3.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Codeception\\PHPUnit\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Davert",
+                    "email": "davert.php@resend.cc"
+                }
+            ],
+            "description": "PHPUnit classes used by Codeception",
+            "support": {
+                "issues": "https://github.com/Codeception/phpunit-wrapper/issues",
+                "source": "https://github.com/Codeception/phpunit-wrapper/tree/7.8.4"
+            },
+            "time": "2022-05-23T06:09:22+00:00"
+        },
+        {
+            "name": "codeception/stub",
+            "version": "2.1.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/Codeception/Stub.git",
+                "reference": "853657f988942f7afb69becf3fd0059f192c705a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/Codeception/Stub/zipball/853657f988942f7afb69becf3fd0059f192c705a",
+                "reference": "853657f988942f7afb69becf3fd0059f192c705a",
+                "shasum": ""
+            },
+            "require": {
+                "codeception/phpunit-wrapper": ">6.0.15 <6.1.0 | ^6.6.1 | ^7.7.1 | ^8.0.3"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Codeception\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Flexible Stub wrapper for PHPUnit's Mock Builder",
+            "support": {
+                "issues": "https://github.com/Codeception/Stub/issues",
+                "source": "https://github.com/Codeception/Stub/tree/master"
+            },
+            "time": "2019-03-02T15:35:10+00:00"
+        },
+        {
+            "name": "doctrine/instantiator",
+            "version": "1.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/doctrine/instantiator.git",
+                "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b",
+                "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "require-dev": {
+                "doctrine/coding-standard": "^9 || ^11",
+                "ext-pdo": "*",
+                "ext-phar": "*",
+                "phpbench/phpbench": "^0.16 || ^1",
+                "phpstan/phpstan": "^1.4",
+                "phpstan/phpstan-phpunit": "^1",
+                "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+                "vimeo/psalm": "^4.30 || ^5.4"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Marco Pivetta",
+                    "email": "ocramius@gmail.com",
+                    "homepage": "https://ocramius.github.io/"
+                }
+            ],
+            "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
+            "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
+            "keywords": [
+                "constructor",
+                "instantiate"
+            ],
+            "support": {
+                "issues": "https://github.com/doctrine/instantiator/issues",
+                "source": "https://github.com/doctrine/instantiator/tree/1.5.0"
+            },
+            "funding": [
+                {
+                    "url": "https://www.doctrine-project.org/sponsorship.html",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://www.patreon.com/phpdoctrine",
+                    "type": "patreon"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-12-30T00:15:36+00:00"
+        },
+        {
+            "name": "facebook/webdriver",
+            "version": "1.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-webdriver/php-webdriver-archive.git",
+                "reference": "e43de70f3c7166169d0f14a374505392734160e5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-webdriver/php-webdriver-archive/zipball/e43de70f3c7166169d0f14a374505392734160e5",
+                "reference": "e43de70f3c7166169d0f14a374505392734160e5",
+                "shasum": ""
+            },
+            "require": {
+                "ext-curl": "*",
+                "ext-json": "*",
+                "ext-mbstring": "*",
+                "ext-zip": "*",
+                "php": "^5.6 || ~7.0",
+                "symfony/process": "^2.8 || ^3.1 || ^4.0"
+            },
+            "require-dev": {
+                "friendsofphp/php-cs-fixer": "^2.0",
+                "jakub-onderka/php-parallel-lint": "^0.9.2",
+                "php-coveralls/php-coveralls": "^2.0",
+                "php-mock/php-mock-phpunit": "^1.1",
+                "phpunit/phpunit": "^5.7",
+                "sebastian/environment": "^1.3.4 || ^2.0 || ^3.0",
+                "squizlabs/php_codesniffer": "^2.6",
+                "symfony/var-dumper": "^3.3 || ^4.0"
+            },
+            "suggest": {
+                "ext-SimpleXML": "For Firefox profile creation"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-community": "1.5-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Facebook\\WebDriver\\": "lib/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "Apache-2.0"
+            ],
+            "description": "A PHP client for Selenium WebDriver",
+            "homepage": "https://github.com/facebook/php-webdriver",
+            "keywords": [
+                "facebook",
+                "php",
+                "selenium",
+                "webdriver"
+            ],
+            "support": {
+                "forum": "https://www.facebook.com/groups/phpwebdriver/",
+                "issues": "https://github.com/facebook/php-webdriver/issues",
+                "source": "https://github.com/facebook/php-webdriver"
+            },
+            "abandoned": "php-webdriver/webdriver",
+            "time": "2019-06-13T08:02:18+00:00"
+        },
+        {
+            "name": "fzaninotto/faker",
+            "version": "v1.9.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/fzaninotto/Faker.git",
+                "reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
+                "reference": "848d8125239d7dbf8ab25cb7f054f1a630e68c2e",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.3.3 || ^7.0"
+            },
+            "require-dev": {
+                "ext-intl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7",
+                "squizlabs/php_codesniffer": "^2.9.2"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.9-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Faker\\": "src/Faker/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "François Zaninotto"
+                }
+            ],
+            "description": "Faker is a PHP library that generates fake data for you.",
+            "keywords": [
+                "data",
+                "faker",
+                "fixtures"
+            ],
+            "support": {
+                "issues": "https://github.com/fzaninotto/Faker/issues",
+                "source": "https://github.com/fzaninotto/Faker/tree/v1.9.2"
+            },
+            "abandoned": true,
+            "time": "2020-12-11T09:56:16+00:00"
+        },
+        {
+            "name": "guzzlehttp/guzzle",
+            "version": "6.5.8",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/guzzle.git",
+                "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981",
+                "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "guzzlehttp/promises": "^1.0",
+                "guzzlehttp/psr7": "^1.9",
+                "php": ">=5.5",
+                "symfony/polyfill-intl-idn": "^1.17"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0",
+                "psr/log": "^1.1"
+            },
+            "suggest": {
+                "psr/log": "Required for using the Log middleware"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.5-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Jeremy Lindblom",
+                    "email": "jeremeamia@gmail.com",
+                    "homepage": "https://github.com/jeremeamia"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle is a PHP HTTP client library",
+            "homepage": "http://guzzlephp.org/",
+            "keywords": [
+                "client",
+                "curl",
+                "framework",
+                "http",
+                "http client",
+                "rest",
+                "web service"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/guzzle/issues",
+                "source": "https://github.com/guzzle/guzzle/tree/6.5.8"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-06-20T22:16:07+00:00"
+        },
+        {
+            "name": "guzzlehttp/promises",
+            "version": "1.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/promises.git",
+                "reference": "b94b2807d85443f9719887892882d0329d1e2598"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598",
+                "reference": "b94b2807d85443f9719887892882d0329d1e2598",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.5"
+            },
+            "require-dev": {
+                "symfony/phpunit-bridge": "^4.4 || ^5.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.5-dev"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\Promise\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "Guzzle promises library",
+            "keywords": [
+                "promise"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/promises/issues",
+                "source": "https://github.com/guzzle/promises/tree/1.5.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-08-28T14:55:35+00:00"
+        },
+        {
+            "name": "guzzlehttp/psr7",
+            "version": "1.9.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/guzzle/psr7.git",
+                "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/guzzle/psr7/zipball/e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+                "reference": "e4490cabc77465aaee90b20cfc9a770f8c04be6b",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.4.0",
+                "psr/http-message": "~1.0",
+                "ralouphie/getallheaders": "^2.0.5 || ^3.0.0"
+            },
+            "provide": {
+                "psr/http-message-implementation": "1.0"
+            },
+            "require-dev": {
+                "ext-zlib": "*",
+                "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10"
+            },
+            "suggest": {
+                "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/functions_include.php"
+                ],
+                "psr-4": {
+                    "GuzzleHttp\\Psr7\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Graham Campbell",
+                    "email": "hello@gjcampbell.co.uk",
+                    "homepage": "https://github.com/GrahamCampbell"
+                },
+                {
+                    "name": "Michael Dowling",
+                    "email": "mtdowling@gmail.com",
+                    "homepage": "https://github.com/mtdowling"
+                },
+                {
+                    "name": "George Mponos",
+                    "email": "gmponos@gmail.com",
+                    "homepage": "https://github.com/gmponos"
+                },
+                {
+                    "name": "Tobias Nyholm",
+                    "email": "tobias.nyholm@gmail.com",
+                    "homepage": "https://github.com/Nyholm"
+                },
+                {
+                    "name": "Márk Sági-Kazár",
+                    "email": "mark.sagikazar@gmail.com",
+                    "homepage": "https://github.com/sagikazarmark"
+                },
+                {
+                    "name": "Tobias Schultze",
+                    "email": "webmaster@tubo-world.de",
+                    "homepage": "https://github.com/Tobion"
+                }
+            ],
+            "description": "PSR-7 message implementation that also provides common utility methods",
+            "keywords": [
+                "http",
+                "message",
+                "psr-7",
+                "request",
+                "response",
+                "stream",
+                "uri",
+                "url"
+            ],
+            "support": {
+                "issues": "https://github.com/guzzle/psr7/issues",
+                "source": "https://github.com/guzzle/psr7/tree/1.9.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/GrahamCampbell",
+                    "type": "github"
+                },
+                {
+                    "url": "https://github.com/Nyholm",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-04-17T16:00:37+00:00"
+        },
+        {
+            "name": "myclabs/deep-copy",
+            "version": "1.11.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/myclabs/DeepCopy.git",
+                "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+                "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.1 || ^8.0"
+            },
+            "conflict": {
+                "doctrine/collections": "<1.6.8",
+                "doctrine/common": "<2.13.3 || >=3,<3.2.2"
+            },
+            "require-dev": {
+                "doctrine/collections": "^1.6.8",
+                "doctrine/common": "^2.13.3 || ^3.2.2",
+                "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/DeepCopy/deep_copy.php"
+                ],
+                "psr-4": {
+                    "DeepCopy\\": "src/DeepCopy/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Create deep copies (clones) of your objects",
+            "keywords": [
+                "clone",
+                "copy",
+                "duplicate",
+                "object",
+                "object graph"
+            ],
+            "support": {
+                "issues": "https://github.com/myclabs/DeepCopy/issues",
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1"
+            },
+            "funding": [
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2023-03-08T13:26:56+00:00"
+        },
+        {
+            "name": "phar-io/manifest",
+            "version": "1.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/manifest.git",
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-phar": "*",
+                "phar-io/version": "^2.0",
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+            "support": {
+                "issues": "https://github.com/phar-io/manifest/issues",
+                "source": "https://github.com/phar-io/manifest/tree/master"
+            },
+            "time": "2018-07-08T19:23:20+00:00"
+        },
+        {
+            "name": "phar-io/version",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phar-io/version.git",
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Heuer",
+                    "email": "sebastian@phpeople.de",
+                    "role": "Developer"
+                },
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "Library for handling version information and constraints",
+            "support": {
+                "issues": "https://github.com/phar-io/version/issues",
+                "source": "https://github.com/phar-io/version/tree/master"
+            },
+            "time": "2018-07-08T19:19:57+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-common",
+            "version": "2.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+                "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-2.x": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "opensource@ijaap.nl"
+                }
+            ],
+            "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+            "homepage": "http://www.phpdoc.org",
+            "keywords": [
+                "FQSEN",
+                "phpDocumentor",
+                "phpdoc",
+                "reflection",
+                "static analysis"
+            ],
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+            },
+            "time": "2020-06-27T09:03:43+00:00"
+        },
+        {
+            "name": "phpdocumentor/reflection-docblock",
+            "version": "5.3.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/622548b623e81ca6d78b721c5e029f4ce664f170",
+                "reference": "622548b623e81ca6d78b721c5e029f4ce664f170",
+                "shasum": ""
+            },
+            "require": {
+                "ext-filter": "*",
+                "php": "^7.2 || ^8.0",
+                "phpdocumentor/reflection-common": "^2.2",
+                "phpdocumentor/type-resolver": "^1.3",
+                "webmozart/assert": "^1.9.1"
+            },
+            "require-dev": {
+                "mockery/mockery": "~1.3.2",
+                "psalm/phar": "^4.8"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "me@mikevanriel.com"
+                },
+                {
+                    "name": "Jaap van Otterdijk",
+                    "email": "account@ijaap.nl"
+                }
+            ],
+            "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+            "support": {
+                "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+                "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.3.0"
+            },
+            "time": "2021-10-19T17:43:47+00:00"
+        },
+        {
+            "name": "phpdocumentor/type-resolver",
+            "version": "1.6.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpDocumentor/TypeResolver.git",
+                "reference": "77a32518733312af16a44300404e945338981de3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/77a32518733312af16a44300404e945338981de3",
+                "reference": "77a32518733312af16a44300404e945338981de3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0",
+                "phpdocumentor/reflection-common": "^2.0"
+            },
+            "require-dev": {
+                "ext-tokenizer": "*",
+                "psalm/phar": "^4.8"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-1.x": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "phpDocumentor\\Reflection\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Mike van Riel",
+                    "email": "me@mikevanriel.com"
+                }
+            ],
+            "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+            "support": {
+                "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+                "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.6.1"
+            },
+            "time": "2022-03-15T21:29:03+00:00"
+        },
+        {
+            "name": "phpspec/prophecy",
+            "version": "v1.17.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/phpspec/prophecy.git",
+                "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/phpspec/prophecy/zipball/15873c65b207b07765dbc3c95d20fdf4a320cbe2",
+                "reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.2 || ^2.0",
+                "php": "^7.2 || 8.0.* || 8.1.* || 8.2.*",
+                "phpdocumentor/reflection-docblock": "^5.2",
+                "sebastian/comparator": "^3.0 || ^4.0",
+                "sebastian/recursion-context": "^3.0 || ^4.0"
+            },
+            "require-dev": {
+                "phpspec/phpspec": "^6.0 || ^7.0",
+                "phpstan/phpstan": "^1.9",
+                "phpunit/phpunit": "^8.0 || ^9.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Prophecy\\": "src/Prophecy"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Konstantin Kudryashov",
+                    "email": "ever.zet@gmail.com",
+                    "homepage": "http://everzet.com"
+                },
+                {
+                    "name": "Marcello Duarte",
+                    "email": "marcello.duarte@gmail.com"
+                }
+            ],
+            "description": "Highly opinionated mocking framework for PHP 5.3+",
+            "homepage": "https://github.com/phpspec/prophecy",
+            "keywords": [
+                "Double",
+                "Dummy",
+                "fake",
+                "mock",
+                "spy",
+                "stub"
+            ],
+            "support": {
+                "issues": "https://github.com/phpspec/prophecy/issues",
+                "source": "https://github.com/phpspec/prophecy/tree/v1.17.0"
+            },
+            "time": "2023-02-02T15:41:36+00:00"
+        },
+        {
+            "name": "phpunit/php-code-coverage",
+            "version": "6.1.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+                "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.1",
+                "phpunit/php-file-iterator": "^2.0",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-token-stream": "^3.0",
+                "sebastian/code-unit-reverse-lookup": "^1.0.1",
+                "sebastian/environment": "^3.1 || ^4.0",
+                "sebastian/version": "^2.0.1",
+                "theseer/tokenizer": "^1.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "suggest": {
+                "ext-xdebug": "^2.6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "6.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+            "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+            "keywords": [
+                "coverage",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+                "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master"
+            },
+            "time": "2018-10-31T16:06:48+00:00"
+        },
+        {
+            "name": "phpunit/php-file-iterator",
+            "version": "2.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+                "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5",
+                "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+            "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+            "keywords": [
+                "filesystem",
+                "iterator"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+                "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-12-02T12:42:26+00:00"
+        },
+        {
+            "name": "phpunit/php-text-template",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-text-template.git",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.3.3"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Simple template engine.",
+            "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+            "keywords": [
+                "template"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+                "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+            },
+            "time": "2015-06-21T13:50:34+00:00"
+        },
+        {
+            "name": "phpunit/php-timer",
+            "version": "2.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-timer.git",
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+                "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Utility class for timing",
+            "homepage": "https://github.com/sebastianbergmann/php-timer/",
+            "keywords": [
+                "timer"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+                "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:20:02+00:00"
+        },
+        {
+            "name": "phpunit/php-token-stream",
+            "version": "3.1.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/php-token-stream.git",
+                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
+                "reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
+                "shasum": ""
+            },
+            "require": {
+                "ext-tokenizer": "*",
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Wrapper around PHP's tokenizer extension.",
+            "homepage": "https://github.com/sebastianbergmann/php-token-stream/",
+            "keywords": [
+                "tokenizer"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
+                "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "abandoned": true,
+            "time": "2021-07-26T12:15:06+00:00"
+        },
+        {
+            "name": "phpunit/phpunit",
+            "version": "7.5.20",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/phpunit.git",
+                "reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
+                "reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
+                "shasum": ""
+            },
+            "require": {
+                "doctrine/instantiator": "^1.1",
+                "ext-dom": "*",
+                "ext-json": "*",
+                "ext-libxml": "*",
+                "ext-mbstring": "*",
+                "ext-xml": "*",
+                "myclabs/deep-copy": "^1.7",
+                "phar-io/manifest": "^1.0.2",
+                "phar-io/version": "^2.0",
+                "php": "^7.1",
+                "phpspec/prophecy": "^1.7",
+                "phpunit/php-code-coverage": "^6.0.7",
+                "phpunit/php-file-iterator": "^2.0.1",
+                "phpunit/php-text-template": "^1.2.1",
+                "phpunit/php-timer": "^2.1",
+                "sebastian/comparator": "^3.0",
+                "sebastian/diff": "^3.0",
+                "sebastian/environment": "^4.0",
+                "sebastian/exporter": "^3.1",
+                "sebastian/global-state": "^2.0",
+                "sebastian/object-enumerator": "^3.0.3",
+                "sebastian/resource-operations": "^2.0",
+                "sebastian/version": "^2.0.1"
+            },
+            "conflict": {
+                "phpunit/phpunit-mock-objects": "*"
+            },
+            "require-dev": {
+                "ext-pdo": "*"
+            },
+            "suggest": {
+                "ext-soap": "*",
+                "ext-xdebug": "*",
+                "phpunit/php-invoker": "^2.0"
+            },
+            "bin": [
+                "phpunit"
+            ],
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "7.5-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "The PHP Unit Testing framework.",
+            "homepage": "https://phpunit.de/",
+            "keywords": [
+                "phpunit",
+                "testing",
+                "xunit"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20"
+            },
+            "time": "2020-01-08T08:45:45+00:00"
+        },
+        {
+            "name": "psr/container",
+            "version": "1.1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/container.git",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.0"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Container\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "https://www.php-fig.org/"
+                }
+            ],
+            "description": "Common Container Interface (PHP FIG PSR-11)",
+            "homepage": "https://github.com/php-fig/container",
+            "keywords": [
+                "PSR-11",
+                "container",
+                "container-interface",
+                "container-interop",
+                "psr"
+            ],
+            "support": {
+                "issues": "https://github.com/php-fig/container/issues",
+                "source": "https://github.com/php-fig/container/tree/1.1.1"
+            },
+            "time": "2021-03-05T17:36:06+00:00"
+        },
+        {
+            "name": "psr/http-message",
+            "version": "1.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/php-fig/http-message.git",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Psr\\Http\\Message\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "PHP-FIG",
+                    "homepage": "http://www.php-fig.org/"
+                }
+            ],
+            "description": "Common interface for HTTP messages",
+            "homepage": "https://github.com/php-fig/http-message",
+            "keywords": [
+                "http",
+                "http-message",
+                "psr",
+                "psr-7",
+                "request",
+                "response"
+            ],
+            "support": {
+                "source": "https://github.com/php-fig/http-message/tree/1.1"
+            },
+            "time": "2023-04-04T09:50:52+00:00"
+        },
+        {
+            "name": "ralouphie/getallheaders",
+            "version": "3.0.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/ralouphie/getallheaders.git",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+                "reference": "120b605dfeb996808c31b6477290a714d356e822",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "php-coveralls/php-coveralls": "^2.1",
+                "phpunit/phpunit": "^5 || ^6.5"
+            },
+            "type": "library",
+            "autoload": {
+                "files": [
+                    "src/getallheaders.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ralph Khattar",
+                    "email": "ralph.khattar@gmail.com"
+                }
+            ],
+            "description": "A polyfill for getallheaders.",
+            "support": {
+                "issues": "https://github.com/ralouphie/getallheaders/issues",
+                "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+            },
+            "time": "2019-03-08T08:55:37+00:00"
+        },
+        {
+            "name": "sebastian/code-unit-reverse-lookup",
+            "version": "1.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+                "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Looks up which function or method a line of code belongs to",
+            "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+                "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T08:15:22+00:00"
+        },
+        {
+            "name": "sebastian/comparator",
+            "version": "3.0.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/comparator.git",
+                "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1dc7ceb4a24aede938c7af2a9ed1de09609ca770",
+                "reference": "1dc7ceb4a24aede938c7af2a9ed1de09609ca770",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "sebastian/diff": "^3.0",
+                "sebastian/exporter": "^3.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@2bepublished.at"
+                }
+            ],
+            "description": "Provides the functionality to compare PHP values for equality",
+            "homepage": "https://github.com/sebastianbergmann/comparator",
+            "keywords": [
+                "comparator",
+                "compare",
+                "equality"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/comparator/issues",
+                "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.5"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2022-09-14T12:31:48+00:00"
+        },
+        {
+            "name": "sebastian/diff",
+            "version": "3.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/diff.git",
+                "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6296a0c086dd0117c1b78b059374d7fcbe7545ae",
+                "reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5 || ^8.0",
+                "symfony/process": "^2 || ^3.3 || ^4"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Kore Nordmann",
+                    "email": "mail@kore-nordmann.de"
+                }
+            ],
+            "description": "Diff implementation",
+            "homepage": "https://github.com/sebastianbergmann/diff",
+            "keywords": [
+                "diff",
+                "udiff",
+                "unidiff",
+                "unified diff"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/diff/issues",
+                "source": "https://github.com/sebastianbergmann/diff/tree/3.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2023-05-07T05:30:20+00:00"
+        },
+        {
+            "name": "sebastian/environment",
+            "version": "4.2.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/environment.git",
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+                "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^7.5"
+            },
+            "suggest": {
+                "ext-posix": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "4.2-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides functionality to handle HHVM/PHP environments",
+            "homepage": "http://www.github.com/sebastianbergmann/environment",
+            "keywords": [
+                "Xdebug",
+                "environment",
+                "hhvm"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/environment/issues",
+                "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:53:42+00:00"
+        },
+        {
+            "name": "sebastian/exporter",
+            "version": "3.1.5",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/exporter.git",
+                "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/73a9676f2833b9a7c36968f9d882589cd75511e6",
+                "reference": "73a9676f2833b9a7c36968f9d882589cd75511e6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "ext-mbstring": "*",
+                "phpunit/phpunit": "^8.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.1.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Volker Dusch",
+                    "email": "github@wallbash.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                },
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "Provides the functionality to export PHP variables for visualization",
+            "homepage": "http://www.github.com/sebastianbergmann/exporter",
+            "keywords": [
+                "export",
+                "exporter"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/exporter/issues",
+                "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.5"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2022-09-14T06:00:17+00:00"
+        },
+        {
+            "name": "sebastian/global-state",
+            "version": "2.0.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/global-state.git",
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "suggest": {
+                "ext-uopz": "*"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Snapshotting of global state",
+            "homepage": "http://www.github.com/sebastianbergmann/global-state",
+            "keywords": [
+                "global state"
+            ],
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/global-state/issues",
+                "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
+            },
+            "time": "2017-04-27T15:39:26+00:00"
+        },
+        {
+            "name": "sebastian/object-enumerator",
+            "version": "3.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+                "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0",
+                "sebastian/object-reflector": "^1.1.1",
+                "sebastian/recursion-context": "^3.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+            "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+                "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:40:27+00:00"
+        },
+        {
+            "name": "sebastian/object-reflector",
+            "version": "1.1.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/object-reflector.git",
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+                "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.1-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Allows reflection of object attributes, including inherited and non-public ones",
+            "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+                "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:37:18+00:00"
+        },
+        {
+            "name": "sebastian/recursion-context",
+            "version": "3.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/recursion-context.git",
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
+                "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^6.0"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "3.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                },
+                {
+                    "name": "Jeff Welch",
+                    "email": "whatthejeff@gmail.com"
+                },
+                {
+                    "name": "Adam Harvey",
+                    "email": "aharvey@php.net"
+                }
+            ],
+            "description": "Provides functionality to recursively process PHP variables",
+            "homepage": "http://www.github.com/sebastianbergmann/recursion-context",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+                "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:34:24+00:00"
+        },
+        {
+            "name": "sebastian/resource-operations",
+            "version": "2.0.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/resource-operations.git",
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+                "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de"
+                }
+            ],
+            "description": "Provides a list of PHP built-in functions that operate on resources",
+            "homepage": "https://www.github.com/sebastianbergmann/resource-operations",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+                "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/sebastianbergmann",
+                    "type": "github"
+                }
+            ],
+            "time": "2020-11-30T07:30:19+00:00"
+        },
+        {
+            "name": "sebastian/version",
+            "version": "2.0.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/sebastianbergmann/version.git",
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019",
+                "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=5.6"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.0.x-dev"
+                }
+            },
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Sebastian Bergmann",
+                    "email": "sebastian@phpunit.de",
+                    "role": "lead"
+                }
+            ],
+            "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+            "homepage": "https://github.com/sebastianbergmann/version",
+            "support": {
+                "issues": "https://github.com/sebastianbergmann/version/issues",
+                "source": "https://github.com/sebastianbergmann/version/tree/master"
+            },
+            "time": "2016-10-03T07:35:21+00:00"
+        },
+        {
+            "name": "symfony/browser-kit",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/browser-kit.git",
+                "reference": "2a1ff40723ef6b29c8229a860a9c8f815ad7dbbb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/browser-kit/zipball/2a1ff40723ef6b29c8229a860a9c8f815ad7dbbb",
+                "reference": "2a1ff40723ef6b29c8229a860a9c8f815ad7dbbb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/dom-crawler": "^3.4|^4.0|^5.0",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "require-dev": {
+                "symfony/css-selector": "^3.4|^4.0|^5.0",
+                "symfony/http-client": "^4.3|^5.0",
+                "symfony/mime": "^4.3|^5.0",
+                "symfony/process": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/process": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\BrowserKit\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/browser-kit/tree/v4.4.44"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-07-25T12:56:14+00:00"
+        },
+        {
+            "name": "symfony/console",
+            "version": "v4.4.49",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/console.git",
+                "reference": "33fa45ffc81fdcc1ca368d4946da859c8cdb58d9"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/console/zipball/33fa45ffc81fdcc1ca368d4946da859c8cdb58d9",
+                "reference": "33fa45ffc81fdcc1ca368d4946da859c8cdb58d9",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php73": "^1.8",
+                "symfony/polyfill-php80": "^1.16",
+                "symfony/service-contracts": "^1.1|^2"
+            },
+            "conflict": {
+                "psr/log": ">=3",
+                "symfony/dependency-injection": "<3.4",
+                "symfony/event-dispatcher": "<4.3|>=5",
+                "symfony/lock": "<4.4",
+                "symfony/process": "<3.3"
+            },
+            "provide": {
+                "psr/log-implementation": "1.0|2.0"
+            },
+            "require-dev": {
+                "psr/log": "^1|^2",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/event-dispatcher": "^4.3",
+                "symfony/lock": "^4.4|^5.0",
+                "symfony/process": "^3.4|^4.0|^5.0",
+                "symfony/var-dumper": "^4.3|^5.0"
+            },
+            "suggest": {
+                "psr/log": "For using the console logger",
+                "symfony/event-dispatcher": "",
+                "symfony/lock": "",
+                "symfony/process": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Console\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Eases the creation of beautiful and testable command line interfaces",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/console/tree/v4.4.49"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-05T17:10:16+00:00"
+        },
+        {
+            "name": "symfony/css-selector",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/css-selector.git",
+                "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/css-selector/zipball/bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
+                "reference": "bd0a6737e48de45b4b0b7b6fc98c78404ddceaed",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\CssSelector\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Jean-François Simon",
+                    "email": "jeanfrancois.simon@sensiolabs.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Converts CSS selectors to XPath expressions",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/css-selector/tree/v4.4.44"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-06-27T13:16:42+00:00"
+        },
+        {
+            "name": "symfony/deprecation-contracts",
+            "version": "v2.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/deprecation-contracts.git",
+                "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+                "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "2.5-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "function.php"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "A generic function and convention to trigger deprecation notices",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-01-02T09:53:40+00:00"
+        },
+        {
+            "name": "symfony/dom-crawler",
+            "version": "v4.4.45",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/dom-crawler.git",
+                "reference": "4b8daf6c56801e6d664224261cb100b73edc78a5"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/4b8daf6c56801e6d664224261cb100b73edc78a5",
+                "reference": "4b8daf6c56801e6d664224261cb100b73edc78a5",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-ctype": "~1.8",
+                "symfony/polyfill-mbstring": "~1.0",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "conflict": {
+                "masterminds/html5": "<2.6"
+            },
+            "require-dev": {
+                "masterminds/html5": "^2.6",
+                "symfony/css-selector": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/css-selector": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\DomCrawler\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Eases DOM navigation for HTML and XML documents",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/dom-crawler/tree/v4.4.45"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-08-03T12:57:57+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher.git",
+                "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/1e866e9e5c1b22168e0ce5f0b467f19bba61266a",
+                "reference": "1e866e9e5c1b22168e0ce5f0b467f19bba61266a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/event-dispatcher-contracts": "^1.1",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "conflict": {
+                "symfony/dependency-injection": "<3.4"
+            },
+            "provide": {
+                "psr/event-dispatcher-implementation": "1.0",
+                "symfony/event-dispatcher-implementation": "1.1"
+            },
+            "require-dev": {
+                "psr/log": "^1|^2|^3",
+                "symfony/config": "^3.4|^4.0|^5.0",
+                "symfony/dependency-injection": "^3.4|^4.0|^5.0",
+                "symfony/error-handler": "~3.4|~4.4",
+                "symfony/expression-language": "^3.4|^4.0|^5.0",
+                "symfony/http-foundation": "^3.4|^4.0|^5.0",
+                "symfony/service-contracts": "^1.1|^2",
+                "symfony/stopwatch": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/dependency-injection": "",
+                "symfony/http-kernel": ""
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\EventDispatcher\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher/tree/v4.4.44"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-07-20T09:59:04+00:00"
+        },
+        {
+            "name": "symfony/event-dispatcher-contracts",
+            "version": "v1.1.13",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+                "reference": "1d5cd762abaa6b2a4169d3e77610193a7157129e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/1d5cd762abaa6b2a4169d3e77610193a7157129e",
+                "reference": "1d5cd762abaa6b2a4169d3e77610193a7157129e",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3"
+            },
+            "suggest": {
+                "psr/event-dispatcher": "",
+                "symfony/event-dispatcher-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.1-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\EventDispatcher\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to dispatching event",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v1.1.13"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-01-02T09:41:36+00:00"
+        },
+        {
+            "name": "symfony/finder",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/finder.git",
+                "reference": "66bd787edb5e42ff59d3523f623895af05043e4f"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/finder/zipball/66bd787edb5e42ff59d3523f623895af05043e4f",
+                "reference": "66bd787edb5e42ff59d3523f623895af05043e4f",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Finder\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Finds files and directories via an intuitive fluent interface",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/finder/tree/v4.4.44"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-07-29T07:35:46+00:00"
+        },
+        {
+            "name": "symfony/polyfill-ctype",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-ctype.git",
+                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a",
+                "reference": "5bbc823adecdae860bb64756d639ecfec17b050a",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-ctype": "*"
+            },
+            "suggest": {
+                "ext-ctype": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Ctype\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Gert de Pagter",
+                    "email": "BackEndTea@gmail.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for ctype functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "ctype",
+                "polyfill",
+                "portable"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-idn",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-idn.git",
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da",
+                "reference": "639084e360537a19f9ee352433b84ce831f3d2da",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1",
+                "symfony/polyfill-intl-normalizer": "^1.10",
+                "symfony/polyfill-php72": "^1.10"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Idn\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Laurent Bassin",
+                    "email": "laurent@bassin.info"
+                },
+                {
+                    "name": "Trevor Rowbotham",
+                    "email": "trevor.rowbotham@pm.me"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "idn",
+                "intl",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-intl-normalizer",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "suggest": {
+                "ext-intl": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for intl's Normalizer class and related functions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "intl",
+                "normalizer",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-mbstring",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-mbstring.git",
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "provide": {
+                "ext-mbstring": "*"
+            },
+            "suggest": {
+                "ext-mbstring": "For best performance"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Mbstring\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill for the Mbstring extension",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "mbstring",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/polyfill-php80",
+            "version": "v1.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/polyfill-php80.git",
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "1.27-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "bootstrap.php"
+                ],
+                "psr-4": {
+                    "Symfony\\Polyfill\\Php80\\": ""
+                },
+                "classmap": [
+                    "Resources/stubs"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Ion Bazan",
+                    "email": "ion.bazan@gmail.com"
+                },
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "compatibility",
+                "polyfill",
+                "portable",
+                "shim"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-11-03T14:55:06+00:00"
+        },
+        {
+            "name": "symfony/process",
+            "version": "v4.4.44",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/process.git",
+                "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/process/zipball/5cee9cdc4f7805e2699d9fd66991a0e6df8252a2",
+                "reference": "5cee9cdc4f7805e2699d9fd66991a0e6df8252a2",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-php80": "^1.16"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Process\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Executes commands in sub-processes",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/process/tree/v4.4.44"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-06-27T13:16:42+00:00"
+        },
+        {
+            "name": "symfony/service-contracts",
+            "version": "v2.5.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/service-contracts.git",
+                "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
+                "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.2.5",
+                "psr/container": "^1.1",
+                "symfony/deprecation-contracts": "^2.1|^3"
+            },
+            "conflict": {
+                "ext-psr": "<1.1|>=2"
+            },
+            "suggest": {
+                "symfony/service-implementation": ""
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-main": "2.5-dev"
+                },
+                "thanks": {
+                    "name": "symfony/contracts",
+                    "url": "https://github.com/symfony/contracts"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Contracts\\Service\\": ""
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Nicolas Grekas",
+                    "email": "p@tchwork.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Generic abstractions related to writing services",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "abstractions",
+                "contracts",
+                "decoupling",
+                "interfaces",
+                "interoperability",
+                "standards"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/service-contracts/tree/v2.5.2"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-05-30T19:17:29+00:00"
+        },
+        {
+            "name": "symfony/yaml",
+            "version": "v4.4.45",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/yaml.git",
+                "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d",
+                "reference": "aeccc4dc52a9e634f1d1eebeb21eacfdcff1053d",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=7.1.3",
+                "symfony/polyfill-ctype": "~1.8"
+            },
+            "conflict": {
+                "symfony/console": "<3.4"
+            },
+            "require-dev": {
+                "symfony/console": "^3.4|^4.0|^5.0"
+            },
+            "suggest": {
+                "symfony/console": "For validating YAML files using the lint command"
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\Component\\Yaml\\": ""
+                },
+                "exclude-from-classmap": [
+                    "/Tests/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Fabien Potencier",
+                    "email": "fabien@symfony.com"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Loads and dumps YAML files",
+            "homepage": "https://symfony.com",
+            "support": {
+                "source": "https://github.com/symfony/yaml/tree/v4.4.45"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2022-08-02T15:47:23+00:00"
+        },
+        {
+            "name": "theseer/tokenizer",
+            "version": "1.2.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/theseer/tokenizer.git",
+                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
+                "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
+                "shasum": ""
+            },
+            "require": {
+                "ext-dom": "*",
+                "ext-tokenizer": "*",
+                "ext-xmlwriter": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "type": "library",
+            "autoload": {
+                "classmap": [
+                    "src/"
+                ]
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "authors": [
+                {
+                    "name": "Arne Blankerts",
+                    "email": "arne@blankerts.de",
+                    "role": "Developer"
+                }
+            ],
+            "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+            "support": {
+                "issues": "https://github.com/theseer/tokenizer/issues",
+                "source": "https://github.com/theseer/tokenizer/tree/1.2.1"
+            },
+            "funding": [
+                {
+                    "url": "https://github.com/theseer",
+                    "type": "github"
+                }
+            ],
+            "time": "2021-07-28T10:34:58+00:00"
+        },
+        {
+            "name": "webmozart/assert",
+            "version": "1.11.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/webmozarts/assert.git",
+                "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991",
+                "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991",
+                "shasum": ""
+            },
+            "require": {
+                "ext-ctype": "*",
+                "php": "^7.2 || ^8.0"
+            },
+            "conflict": {
+                "phpstan/phpstan": "<0.12.20",
+                "vimeo/psalm": "<4.6.1 || 4.6.2"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^8.5.13"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.10-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Webmozart\\Assert\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Bernhard Schussek",
+                    "email": "bschussek@gmail.com"
+                }
+            ],
+            "description": "Assertions to validate method input/output with nice error messages.",
+            "keywords": [
+                "assert",
+                "check",
+                "validate"
+            ],
+            "support": {
+                "issues": "https://github.com/webmozarts/assert/issues",
+                "source": "https://github.com/webmozarts/assert/tree/1.11.0"
+            },
+            "time": "2022-06-03T18:03:27+00:00"
+        }
+    ],
+    "aliases": [],
+    "minimum-stability": "stable",
+    "stability-flags": {
+        "p3k/picofeed": 0
+    },
+    "prefer-stable": false,
+    "prefer-lowest": false,
+    "platform": {
+        "php": "^7.3.6 || ^8.0",
+        "ext-json": "*"
+    },
+    "platform-dev": [],
+    "platform-overrides": {
+        "php": "7.3.6"
+    },
+    "plugin-api-version": "2.3.0"
+}

+ 15 - 0
plugins/admin/hebe.json

@@ -0,0 +1,15 @@
+{
+   "project":"grav-plugin-admin",
+   "platforms":{
+      "grav":{
+         "nodes":{
+            "plugin":[
+               {
+                  "source":"/",
+                  "destination":"/user/plugins/admin"
+               }
+            ]
+         }
+      }
+   }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 24 - 0
plugins/admin/languages/ar.yaml


A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 24 - 0
plugins/admin/languages/bg.yaml


+ 4 - 0
plugins/admin/languages/bn.yaml

@@ -0,0 +1,4 @@
+---
+PLUGIN_ADMIN:
+  LOGIN_BTN: "লগ ইন"
+  LOGIN_BTN_FORGOT: "ভুলে গেছি"

+ 592 - 0
plugins/admin/languages/br.yaml

@@ -0,0 +1,592 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "Un ermaeziadenn beta an hini eo! Arverit en endro produadur gant evezh..."
+  ADMIN_REPORT_ISSUE: "Kavet hoc'h eus ur gudenn? Danevellit anezhi war Github."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Lusket gant Grav</a> - Ar CMS Restr plad modern"
+  LOGIN_BTN: "Anv arveriad"
+  LOGIN_BTN_FORGOT: "Ankouaet"
+  LOGIN_BTN_RESET: "Adderaouekaat ar ger-tremen"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Kas an ditouroù adderaouekaat"
+  LOGIN_BTN_CLEAR: "Skarzhañ ar furmskrid"
+  LOGIN_BTN_CREATE_USER: "Krouiñ an arveriad"
+  LOGIN_LOGGED_IN: "Kennasket oc'h gant berzh"
+  LOGIN_FAILED: "C'hwitadenn war ar c'hennask"
+  LOGGED_OUT: "Digennasket oc'h"
+  RESET_NEW_PASSWORD: "Enankit ur ger-tremen nevez &hellip;"
+  RESET_LINK_EXPIRED: "Diamzeret eo an ere adderaouekaat, klaskit en-dro"
+  RESET_PASSWORD_RESET: "Adderaouekaet eo bet ar ger-tremen"
+  RESET_INVALID_LINK: "Ere adderaouekaat didalvoudek, klaskit en-dro"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Kaset eo bet an ditouroù da adderaouekaat ho ker-tremen d'ho chmolec'h postel"
+  FORGOT_FAILED_TO_EMAIL: "C'hwitadenn en ur gas an ditouroù, klaskit en-dro diwezhatoc'h"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "N'haller ket adderaouekaat ar ger-tremen evit %s, chomlec'h postel ebet arventennet"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "N'eus ket eus an arveriad gant an anv <b>%s</b>"
+  FORGOT_EMAIL_NOT_CONFIGURED: "N'haller ket adderaouekaat ar ger-tremen. N'eo ket kefluniet al lec'hienn evit kas posteloù"
+  FORGOT_EMAIL_SUBJECT: "%s Goulenn adderaouekaat ar ger-tremen"
+  FORGOT_EMAIL_BODY: "<h1>Adderaouekaat ar ger-tremen/h1><p>%1$s,</p><p>Graet eo bet un azgoulenn war <b>%4$s</b> evit adderaouekaat ho ker-tremen.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klikit amañ da adderaouekaat ho ker-tremen</a><br /><br /></p><p>Gallout a rit ivez eilañ an URL da heul er varrenn chomlec'h en ho merdeer:</p> <p>%2$s</p><p><br />A galon,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Ardeiñ ar pajennoù"
+  PAGES: "Pajennoù"
+  PLUGINS: "Enlugelladoù"
+  PLUGIN: "Enlugellad"
+  THEMES: "Neuzioù"
+  LOGOUT: "Digennaskañ"
+  BACK: "Distreiñ"
+  NEXT: "War-lerc'h"
+  PREVIOUS: "Diaraog"
+  ADD_PAGE: "Ouzhpennañ ur bajenn"
+  MOVE: "Dilec'hiañ"
+  DELETE: "Dilemel"
+  VIEW: "Gwel"
+  SAVE: "Enrollañ"
+  NORMAL: "Reoliek"
+  EXPERT: "Kemplezhoc'h"
+  EXPAND_ALL: "Astenn an holl"
+  COLLAPSE_ALL: "Bihanaat an holl"
+  ERROR: "Fazi"
+  CLOSE: "Serriñ"
+  CANCEL: "Nullañ"
+  CONTINUE: "Kenderc'hel"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Kadarnadur azgoulennet"
+  MODAL_CHANGED_DETECTED_TITLE: "Kemmoù dinoet"
+  MODAL_CHANGED_DETECTED_DESC: "Kemmoù dienrollet a zo. Sur oc'h e fell deoc'h kuitaat hep enrollañ?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Kadarnadur azgoulennet"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Sur oc'h e fell deoc'h dilemel ar restr-mañ? N'haller ket dizober ar gwered-mañ."
+  ADD_FILTERS: "Ouzhpennañ siloù"
+  SEARCH_PAGES: "Klask pajennoù"
+  VERSION: "Handelv"
+  WAS_MADE_WITH: "Savet eo bet gant"
+  BY: "Gant"
+  UPDATE_THEME: "Hizivaat an neuz"
+  UPDATE_PLUGIN: "Hizivaat an enlugellad"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "an neuz-mañ a zo hegerz"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "an enlugellad-mañ a zo hegerz"
+  AUTHOR: "Aozer"
+  HOMEPAGE: "Pennbajenn"
+  DEMO: "Tañva"
+  BUG_TRACKER: "Heulier beugoù"
+  KEYWORDS: "Gerioù-alc'hwez"
+  LICENSE: "Lañvaz"
+  DESCRIPTION: "Deskrivadur"
+  README: "Skoazell"
+  REMOVE_THEME: "Dilemel an neuz"
+  INSTALL_THEME: "Staliañ an neuz"
+  THEME: "Neuz"
+  BACK_TO_THEMES: "Distreiñ d'an neuzioù"
+  BACK_TO_PLUGINS: "Distreiñ d'an enlugelladoù"
+  CHECK_FOR_UPDATES: "Klask hizivadennoù"
+  ADD: "Ouzhpenañ"
+  CLEAR_CACHE: "Skarzhañ ar c'hrubuilh"
+  CLEAR_CACHE_ALL_CACHE: "Ar c'hrubuilh a-bezh"
+  CLEAR_CACHE_ASSETS_ONLY: "Loazioù nemetken"
+  CLEAR_CACHE_IMAGES_ONLY: "Skeudennoù nemetken"
+  CLEAR_CACHE_CACHE_ONLY: "Krubuilh nemetken"
+  CLEAR_CACHE_TMP_ONLY: "Padennek hepken"
+  UPDATES_AVAILABLE: "Hizivadennoù hegerz"
+  DAYS: "Devezhioù"
+  UPDATE: "Hizivadenn"
+  BACKUP: "Gwared"
+  STATISTICS: "Stadegoù"
+  TODAY: "Hiziv"
+  WEEK: "Sizhun"
+  MONTH: "Miz"
+  LATEST_PAGE_UPDATES: "Hizivadennoù diwezhañ ar bajenn"
+  MAINTENANCE: "Trezalc'h"
+  UPDATED: "Hizivaet"
+  MON: "Lun"
+  TUE: "Meu"
+  WED: "Mer"
+  THU: "Yao"
+  FRI: "Gwe"
+  SAT: "Sad"
+  SUN: "Sul"
+  COPY: "Eilañ"
+  EDIT: "Kemmañ"
+  CREATE: "Krouiñ"
+  GRAV_ADMIN: "Merour Grav"
+  GRAV_OFFICIAL_PLUGIN: "Enlugellad Kefridiel Grav"
+  GRAV_OFFICIAL_THEME: "Neuz Kefridiel Grav"
+  PLUGIN_SYMBOLICALLY_LINKED: "Gant un ere arouezus eo lakaet an enlugellad. Ne vo ket dinoet an hizivadennoù."
+  THEME_SYMBOLICALLY_LINKED: "Gant un ere arouezus eo lakaet an neuz. Ne vo ket dinoet an hizivadennoù"
+  REMOVE_PLUGIN: "Dilemel an enlugellad"
+  INSTALL_PLUGIN: "Staliañ an enlugellad"
+  AVAILABLE: "Hegerz"
+  INSTALLED: "Staliet"
+  INSTALL: "Staliañ"
+  ACTIVE_THEME: "Neuz oberiant"
+  SWITCHING_TO: "Kemmañ da"
+  SWITCHING_TO_DESCRIPTION: "En ur gemmañ d'un neuz disheñvel n'eus gwarant ebet e vo skoret an holl frammoù pajenn, ar pezh a zegasfe fazioù en ur gargañ ar pajennoù-mañ."
+  SWITCHING_TO_CONFIRMATION: "Fellout a ra deoc'h kenderc'hel ha kemmañ an neuz"
+  CREATE_NEW_USER: "Krouiñ un arveriad nevez"
+  REMOVE_USER: "Dilemel an arveriad"
+  ACCESS_DENIED: "Haeziñ nac'het"
+  ACCOUNT_NOT_ADMIN: "n'hoc'h eus ket an aotreoù a-zere"
+  PHP_INFO: "Titouroù PHP"
+  INSTALLER: "Stalier"
+  AVAILABLE_THEMES: "Neuzioù hegerz"
+  AVAILABLE_PLUGINS: "Enlugelladoù hegerz"
+  INSTALLED_THEMES: "Neuzioù staliet"
+  INSTALLED_PLUGINS: "Enlugelladoù staliet"
+  BROWSE_ERROR_LOGS: "Furchal er c'herzhlevr fazioù"
+  SITE: "Lec'hienn"
+  INFO: "Titouroù"
+  SYSTEM: "Reizhiad"
+  USER: "Arveriad"
+  ADD_ACCOUNT: "Ouzhpennañ ur gont"
+  SWITCH_LANGUAGE: "Kemmañ ar yezh"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Gweredekaet an enlugellad gant berzh"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Diweredekaet an enlugellad gant berzh"
+  SUCCESSFULLY_CHANGED_THEME: "Kemmet an neuz dre ziouer gant berzh"
+  INSTALLATION_FAILED: "C'hwitadenn war ar staliadur"
+  INSTALLATION_SUCCESSFUL: "Berzh war ar staliadur"
+  UNINSTALL_FAILED: "C'hwitadenn war an distaliadur"
+  UNINSTALL_SUCCESSFUL: "Berzh war an distaliadur"
+  SUCCESSFULLY_SAVED: "Enrollet gant berzh"
+  SUCCESSFULLY_COPIED: "Eilet gant berzh"
+  REORDERING_WAS_SUCCESSFUL: "Adurzhiet gant berzh"
+  SUCCESSFULLY_DELETED: "Dilamet gant berzh"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Kemmet ar yezh gant berzh"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "N'ho peus ket trawalc'h a aotreoù evit ar gwered"
+  CACHE_CLEARED: "Skarzhet ar c'hrubuilh"
+  METHOD: "Hentenn"
+  ERROR_CLEARING_CACHE: "Fazi en ur skarzhañ ar c'hrubuilh"
+  AN_ERROR_OCCURRED: "Degouezhet ez eus bet ur fazi"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Prest eo ho kwared da vezañ pellgarget"
+  DOWNLOAD_BACKUP: "Pellgargañ ar gwared"
+  PAGES_FILTERED: "Pajennoù silet"
+  NO_PAGE_FOUND: "Pajenn ebet kavet"
+  INVALID_PARAMETERS: "Arventennoù didalvoudek"
+  NO_FILES_SENT: "Restr ebet kaset"
+  UNKNOWN_ERRORS: "Fazioù dianav"
+  UNSUPPORTED_FILE_TYPE: "Doare restr amskor"
+  FAILED_TO_MOVE_UPLOADED_FILE: "C'hwitadenn en ur zilec'hiañ ar restr pellgaset."
+  FILE_UPLOADED_SUCCESSFULLY: "Restr pellgaset gant berzh"
+  FILE_DELETED: "Restr dilamet"
+  FILE_COULD_NOT_BE_DELETED: "N'haller ket dilemel ar restr"
+  FILE_NOT_FOUND: "N'eus ket bet kavet ar restr"
+  NO_FILE_FOUND: "Restr ebet kavet"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Hizivaet eo bet Grav da"
+  GRAV_UPDATE_FAILED: "C'hwitadenn war hizivadenn Grav"
+  EVERYTHING_UPDATED: "Hizivaet pep tra"
+  UPDATES_FAILED: "C'hwitadenn war a hizivadennoù"
+  AVATAR_BY: "Avatar gant"
+  LAST_BACKUP: "Gwared diwezhañ"
+  FULL_NAME: "Anv klok"
+  USERNAME: "Anv arveriad"
+  EMAIL: "Chomlec'h postel"
+  PASSWORD: "Ger-tremen"
+  PASSWORD_CONFIRM: "Kadarnat ar ger-tremen"
+  TITLE: "Titl"
+  LANGUAGE: "Yezh"
+  ACCOUNT: "Kont"
+  EMAIL_VALIDATION_MESSAGE: "Ret eo reiñ ur chomlec'h postel talvoudek"
+  PASSWORD_VALIDATION_MESSAGE: "Ret eo d'ar ger-tremen enderc'hel ur niverenn, ul lizherenn vras hag ul lizherenn vihan hag 8 arouezenn d'an nebeutañ"
+  LANGUAGE_HELP: "Dibabit ar yezh"
+  MEDIA: "Media"
+  DEFAULTS: "Dre ziouer"
+  SITE_TITLE: "Titl al lec'hienn"
+  SITE_TITLE_PLACEHOLDER: "Titl ledan al lec'hienn"
+  SITE_TITLE_HELP: "Titl dre ziouer ho lec'hienn, arveret en neuzioù"
+  SITE_DEFAULT_LANG: "Yezh defot"
+  DEFAULT_AUTHOR: "Aozer dre ziouer"
+  DEFAULT_AUTHOR_HELP: "Un anv aozer dre ziouer, arveret en neuzioù pe er pajennoù"
+  DEFAULT_EMAIL: "Chomlec'h postel dre ziouer"
+  DEFAULT_EMAIL_HELP: "Ur chomlec'h postel dre ziouer, arveret en neuze pe er pajennoù"
+  TAXONOMY_TYPES: "Doareoù rummadoù"
+  TAXONOMY_TYPES_HELP: "An doareoù rummadoù a rank bezañ erspizet amañ ma fell deoc'h arverañ anezho er pajennoù"
+  PAGE_SUMMARY: "Berradenn ar bajenn"
+  ENABLED: "Gweredekaet"
+  ENABLED_HELP: "Gweredekaat berradenn ar bajenn (ar verradenn a zistro an hevelep tra hag endalc'had ar bajenn)"
+  'YES': "Ya"
+  'NO': "Ket"
+  SUMMARY_SIZE: "Ment ar verradenn"
+  SUMMARY_SIZE_HELP: "An niverenn a arouezenn da arverañ evel berradenn ur bajenn"
+  FORMAT: "Mentrezh"
+  FORMAT_HELP: "berr = arverañ degouezh kentañ an disranner pe ment; hir = laosket e vo an disranner berradenn a-gostez"
+  SHORT: "Berr"
+  LONG: "Hir"
+  DELIMITER: "Disranner"
+  DELIMITER_HELP: "Disranner ar verradenn (diouer '===')"
+  METADATA: "Metaroadennoù"
+  METADATA_HELP: "Skrammet e vo ar gwerzhioù metaroadennoù dre ziouer war an holl bajennoù war-bouez m'eo flastret gant ar bajenn"
+  NAME: "Anv"
+  CONTENT: "Endalc'had"
+  REDIRECTS_AND_ROUTES: "Adheñchañ ha treugoù"
+  CUSTOM_REDIRECTS: "Adheñchañ personelaet"
+  CUSTOM_REDIRECTS_HELP: "treugoù da adheñchañ davet pajennoù all. Talvoudek eo an amsaviñ regex"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/un/anv"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/un/adeñchañ"
+  CUSTOM_ROUTES: "Treugoù personelaet"
+  CUSTOM_ROUTES_HELP: "treugoù da adheñchañ davet pajennoù all. Talvoudek eo an amsaviñ Regex"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/ho/anv"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/ho/treug"
+  FILE_STREAMS: "Lanvioù restroù"
+  DEFAULT: "Dre ziouer"
+  PAGE_MEDIA: "Media ar bajenn"
+  OPTIONS: "Dibarzhioù"
+  PUBLISHED: "Embannet"
+  PUBLISHED_HELP: "Dre ziouer eo embannet ur bajenn war-bouez m'eo lakaet da \"Embannet: ket\" pe dre un deiziad embann en dazont, pe un deiziad diembannañ tremenet"
+  DATE: "Deiziad"
+  DATE_HELP: "Ar vaezienn deiziad a laosk ac'hanoc'h da arventennañ un deiziad liammet gant ar bajenn."
+  PUBLISHED_DATE: "Deiziad embann"
+  PUBLISHED_DATE_HELP: "Gallout a rit reiñ un deiziad da embann ent emgefreek."
+  UNPUBLISHED_DATE: "Deiziad diembannañ"
+  UNPUBLISHED_DATE_HELP: "Gallout a rit reiñ un deiziad evit diembannañ ent emgefreek."
+  ROBOTS: "Robotoù"
+  TAXONOMIES: "Rummadoù"
+  TAXONOMY: "Rummad"
+  ADVANCED: "Kempleshoc'h"
+  SETTINGS: "Arventennoù"
+  FOLDER_NUMERIC_PREFIX: "Rakger niverel an teuliad"
+  FOLDER_NUMERIC_PREFIX_HELP: "Rakgerioù niverel evit urzhiañ gant an dorn ha emplegañ ar gwelusted"
+  FOLDER_NAME: "Anv an teuliad"
+  FOLDER_NAME_HELP: "Anv an teuliad a vo kadavet er reizhiad restroù evit ar bajenn"
+  PARENT: "Kar"
+  DEFAULT_OPTION_ROOT: "- Gwrizienn -"
+  DEFAULT_OPTION_SELECT: "- Diuzañ -"
+  DISPLAY_TEMPLATE: "Skrammañ ar patrom"
+  DISPLAY_TEMPLATE_HELP: "An doare pajenn a ziviz peseurt patrom twig a zeznaouo ar bajenn"
+  ORDERING: "Urzh"
+  PAGE_ORDER: "Urzh ar pajennoù"
+  OVERRIDES: "Flastrañ"
+  MENU: "Lañser"
+  MENU_HELP: "Ar chadennoù da arverañ el lañser. Ma n'eo ket arventennet, Titl a vo arveret."
+  SLUG: "Slug"
+  SLUG_HELP: "An argemenn slug a aotren ac'hanoc'h da arventennañ URL lodenn ar bajenn"
+  SLUG_VALIDATE_MESSAGE: "Lizherennoù bihan, sifroù ha tiredoù a c'hall bezañ er slug hepken"
+  PROCESS: "Keweriañ"
+  PROCESS_HELP: "Reoliañ penaos eo keweriet ar pajennoù. Gallout a ra bezañ lakaet dre bajenn kentoc'h eget en un doare hollek"
+  DEFAULT_CHILD_TYPE: "Doare bugel dre ziouer"
+  USE_GLOBAL: "Arverañ Hollek"
+  ROUTABLE: "Treugus"
+  ROUTABLE_HELP: "M'eo haezadus ar bajenn dre un URL"
+  CACHING: "Krubuilhiñ"
+  VISIBLE: "Gwelus"
+  VISIBLE_HELP: "Despizañ a ra gwelusted ur bajenn er merdeiñ."
+  DISABLED: "Diweredekaet"
+  ITEMS: "Ergorennoù"
+  ORDER_BY: "Urzhiañ dre"
+  ORDER: "Urzh"
+  FOLDER: "Teuliad"
+  ASCENDING: "War-gresk"
+  DESCENDING: "War-zigresk"
+  PAGE_TITLE: "Titl ar bajenn"
+  PAGE_TITLE_HELP: "Titl ar bajenn"
+  PAGE: "Pajenn"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Anv ar restr"
+  PARENT_PAGE: "Pajenn gar"
+  HOME_PAGE: "Pennbajenn"
+  HOME_PAGE_HELP: "Pajenn arveret gant Grav evel pajenn degemer dre ziouer"
+  DEFAULT_THEME: "Neuz dre ziouer"
+  DEFAULT_THEME_HELP: "Arventennañ an neuz arveret gant Grav dre ziouer (Antimatter dre ziouer)"
+  TIMEZONE: "Gwerzhid-eur"
+  TIMEZONE_HELP: "Flastrañ gwerzhid-eur dre ziouer an dafariad"
+  SHORT_DATE_FORMAT: "Mentrezh skrammañ an deiziad berr"
+  SHORT_DATE_FORMAT_HELP: "Arventennan ar mentrezh deiziad berr da arverañ gant an neuzioù"
+  LONG_DATE_FORMAT: "Mentrezh deiziad hir"
+  LONG_DATE_FORMAT_HELP: "Arventennañ ar mentrezh deiziad hir a vo arveret en neuzioù"
+  DEFAULT_ORDERING: "Urzh dre ziouer"
+  DEFAULT_ORDERING_HELP: "Pajennoù er roll a vo skrammet en urzh-mañ war-bouez m'eo flastret"
+  DEFAULT_ORDERING_DEFAULT: "Dre ziouer - diazezet war anv an teuliad"
+  DEFAULT_ORDERING_FOLDER: "Teuliad - diazezet war anv an teuliad hep rakger"
+  DEFAULT_ORDERING_TITLE: "Titl - diazezet war vaezienn ditl an talbenn"
+  DEFAULT_ORDERING_DATE: "Deiziad - diazezet war vaezienn deiziad an talbenn"
+  DEFAULT_ORDER_DIRECTION: "Tu an urzh dre ziouer"
+  DEFAULT_ORDER_DIRECTION_HELP: "Tu ar pajennoù er roll"
+  DEFAULT_PAGE_COUNT: "Niver a bajennoù dre ziouer"
+  DEFAULT_PAGE_COUNT_HELP: "Niver a bajennoù en ur roll d'ar muiañ"
+  DATE_BASED_PUBLISHING: "Embannadenn diazezet war un deiziad"
+  DATE_BASED_PUBLISHING_HELP: "(Di)embann pennadoù ent emgefreek hervez o deiziad"
+  EVENTS: "Darvoudoù"
+  EVENTS_HELP: "(Di)weredekaat darvoudoù resis. Diweredekaat anezho a c'hall terriñ enlugelladoù"
+  REDIRECT_DEFAULT_ROUTE: "Adheñchañ an treug dre ziouer"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Adheñchañ ent emgefreek d'un treug pajenn dre ziouer"
+  LANGUAGES: "Yezhoù"
+  SUPPORTED: "Skoret"
+  SUPPORTED_HELP: "Roll bonegoù yezh 2 lizherenn ennañ disrannet gant skejoù (skouer: 'br, cy, en')"
+  TRANSLATIONS_FALLBACK: "Troidigezh dre ziouer"
+  TRANSLATIONS_FALLBACK_HELP: "Arverañ un droidigezh all ma n'eus ket eus ar tezh oberiant"
+  ACTIVE_LANGUAGE_IN_SESSION: "Yezhoù oberiant en estez"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Kadaviñ ar yezh oberiant en estez"
+  HTTP_HEADERS: "Talbennoù HTTP"
+  EXPIRES: "Diamzer"
+  EXPIRES_HELP: "Arventennañ an talbenn diamzeriñ e eilennoù."
+  LAST_MODIFIED: "Kemmet da ziwezhañ"
+  LAST_MODIFIED_HELP: "Arventennañ an talbenn kemmet da ziwezhañ a c'hall skoazell da wellaat ar proksi ha krubuilh ar merdeer"
+  ETAG: "ETag"
+  ETAG_HELP: "Arventennañ an talbenn etag evit skoazell da c'houzout peur eo bet kemmet ur bajenn"
+  VARY_ACCEPT_ENCODING: "Vary accept encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Arventennañ a ra an talbenn `Vary: Accept Encoding` evit skoazell gant ar proksi hag ar c'hrubuilh CDN"
+  MARKDOWN_EXTRA_HELP: "Gweredekaat ar skor dre ziouer evit Markdown Ectra - https://michelf.ca/projects/php-markdown/extra/"
+  AUTO_LINE_BREAKS: "Tremen d'al linenn ent emgefreek"
+  AUTO_LINE_BREAKS_HELP: "Gweredekaat skor tremen al linenn ent emgefreek e Markdown"
+  AUTO_URL_LINKS: "Ereoù URL emgefreek"
+  AUTO_URL_LINKS_HELP: "Gweredekaat amdroadur emgefreek an URLoù da ereoù HTML"
+  ESCAPE_MARKUP: "Gwareziñ an HTML"
+  ESCAPE_MARKUP_HELP: "Gwareziñ ar c'hlavioù e elfennoù HTML"
+  CACHING_HELP: "Trec'haoler hollek evit (di)weredekaat krubuilh Grav"
+  CACHE_CHECK_METHOD: "Hentenn gwiriekaat ar c'hrubuilh"
+  CACHE_CHECK_METHOD_HELP: "Dibab an hentenn arveret gant Grav evit gwiriekaat m'eo bet kemmer ar restoù pajenn."
+  CACHE_DRIVER: "Sturier Krubuilh"
+  CACHE_DRIVER_HELP: "Dibab pe sturier krubuilh a zo arveret Grav. 'Dinoiñ emgefreek' a glask kavout pe zoare a zo an hini gwellañ"
+  CACHE_PREFIX: "Rakger ar c'hrubuilh"
+  CACHE_PREFIX_HELP: "Lodenn naoudi an alc'hwez Grav. Na gemmit anezhi ma n'ouzit ket petra rit."
+  CACHE_PREFIX_PLACEHOLDER: "Deveret eus an URL diazez (flastret en un enkañ ur chadenn dargouezhek)"
+  LIFETIME: "Padelezh buhez"
+  LIFETIME_HELP: "Arventennañ padelezh ar c'hrubuilh e eilennoù. 0 = anvevenn"
+  GZIP_COMPRESSION: "Koazhadur Gzip"
+  GZIP_COMPRESSION_HELP: "Gweredekaat koazhadur Gzip ar bajenn Grav evit kreskiñ an digonusted."
+  TWIG_TEMPLATING: "Patromiñ Twig"
+  TWIG_CACHING: "Krubuilh Twig"
+  TWIG_CACHING_HELP: "Reoliañ wikefre krubuilh Twig. Laoskit gweredekaet evit an digonusted gwellañ."
+  TWIG_DEBUG: "Diveugañ Twig"
+  TWIG_DEBUG_HELP: "Aotren an dibarzh evit chom hep kargañ an askouezh diveugañ Twig"
+  DETECT_CHANGES: "Dinoiñ ar c'hemmoù"
+  DETECT_CHANGES_HELP: "Adkempunet e vo krubuilh Twig ent emgefreek ma vez dinoet kemmoù er patromoù Twig"
+  AUTOESCAPE_VARIABLES: "Gwareziñ an argemennoù ent emgefreek"
+  AUTOESCAPE_VARIABLES_HELP: "Gwareziñ an holl argemennoù ent emgefreek. Moarvat e torro ho lec'hienn"
+  ASSETS: "Madoù"
+  CSS_PIPELINE: "Arrevellañ CSS"
+  CSS_PIPELINE_HELP: "Arrevellañ ar CSS a zo unvanadur meur a loaz CSS en ur restr hepken"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Ebarzhiñ restroù estren en arrevellañ CSS"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "URLoù diavaez a zo gant daveoù restroù daveel a-wechoù ha ne rankont ket bezañ arrevellet"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "Deoueziñ an arrevellañ CSS da gentañ"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Deoueziñ an arrevellañ CSS a-raok kement dave CSS all ha n'int ket enkorfet"
+  CSS_MINIFY: "Bihanadur CSS"
+  CSS_MINIFY_HELP: "Bihanaat ar CSS e-pad an arrevellañ"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "Amsaviñ bihanadur ar CSS Windows"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Amsaviñ ar bihanadur evit savennoù Windows. Faos dre ziouer abalamour da ThreadStackSize"
+  CSS_REWRITE: "Adskrivañ CSS"
+  CSS_REWRITE_HELP: "Adskrivañ kement URL daveel CSS e-pad an arrevellañ"
+  JAVASCRIPT_PIPELINE: "Arrevellañ Javascript"
+  JAVASCRIPT_PIPELINE_HELP: "An arrevellañ JS a zo unvanadur meur a restr JS en ur restr hepken"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Enkorfañ ar JS diavaez evit an arrevellañ"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Urloù diavaez o deus daveoù restroù daveel a-wechoù ha ne rankont ket bezañ arrevellet"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Arrevellañ JS da gentañ"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Deoueziñ an arrevellañ JS a-raok kement dave JS all ha n'int ket enkorfet"
+  JAVASCRIPT_MINIFY: "Bihanat ar javascript"
+  JAVASCRIPT_MINIFY_HELP: "Bihanaat ar JS e-pad an arrevellañ"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Gweredekaat ar boneg-amzer war al loazioù"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Gweredekaat bonegoù-amzer al loazioù"
+  COLLECTIONS: "Dastumadegoù"
+  ERROR_HANDLER: "Dornataour fazioù"
+  DISPLAY_ERRORS: "Skrammañ ar fazioù"
+  DISPLAY_ERRORS_HELP: "Skrammañ ur bajenn fazi gant munudoù"
+  LOG_ERRORS: "Kerzhlevr ar fazioù"
+  LOG_ERRORS_HELP: "Lakaat kerzhlevr ar fazioù en teuliad /logs"
+  DEBUGGER: "Diveuger"
+  DEBUGGER_HELP: "Gweredekaat diveuger Grav hag an arventennoù da heul"
+  DEBUG_TWIG: "Diveugañ Twig"
+  DEBUG_TWIG_HELP: "Gweredekaat diveugañ ar patromoù Twig"
+  SHUTDOWN_CLOSE_CONNECTION: "Shutdown a serr ar c'hennask"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Serriñ ar c'hennask a-raok gervel onShutdown(). 'false' evit diveugañ"
+  DEFAULT_IMAGE_QUALITY: "Perzhded skeudenn dre ziouer"
+  DEFAULT_IMAGE_QUALITY_HELP: "Perzhded skeudenn dre ziouer da arverañ e-pad adstandilhonañ ar skeudennoù (85%)"
+  CACHE_ALL: "Lakaat an holl skeudennoù er c'hrubuilh"
+  CACHE_ALL_HELP: "Lakaat an holl skeudennoù da dremen dre reizhiad krubuilh Grav zoken ma n'o deus dornatadur media ebet"
+  IMAGES_DEBUG: "Rouedigell diveugañ ar skeudenn"
+  IMAGES_DEBUG_HELP: "Diskouez un diflugell a-us d'ar skeudennoù a ziskouez an donder piksel pa labourer war Retina da skouer"
+  UPLOAD_LIMIT: "Bevenn ment ar restroù da bellgas"
+  UPLOAD_LIMIT_HELP: "Lakaat ar ment restroù uhelañ e eizhbitoù (0 a zo anvevenn)"
+  ENABLE_MEDIA_TIMESTAMP: "Gweredekaat ar boneg-amzer war ar media"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Ouzhpennañ ur boneg-amzer diazezet war an deiziad kemmadur evit pep elfenn media"
+  SESSION: "Estez"
+  SESSION_ENABLED_HELP: "Gweredekaat skor an estez evit Grav"
+  SESSION_NAME_HELP: "Un naoudi arveret da stummañ anv toupin an estez"
+  ABSOLUTE_URLS: "URL dizave"
+  ABSOLUTE_URLS_HELP: "URLoù dizave pe daveel evit 'base_url'"
+  PARAMETER_SEPARATOR: "Disranner arventenn"
+  PARAMETER_SEPARATOR_HELP: "An disranner evit an arventennoù tremenet a c'hall bezañ kemmet evit Apache war Windows"
+  TASK_COMPLETED: "Trevell echuet"
+  EVERYTHING_UP_TO_DATE: "Pep tra a zo hizivaet"
+  UPDATES_ARE_AVAILABLE: "hizivadennoù hegerz"
+  IS_AVAILABLE_FOR_UPDATE: "a zo gant un hizivadenn hegerz"
+  IS_NOW_AVAILABLE: "a zo hegerz"
+  CURRENT: "Bremanel"
+  UPDATE_GRAV_NOW: "Hizivaat Grav bremañ"
+  GRAV_SYMBOLICALLY_LINKED: "Gant un ere arouezel eo staliet Grav. Dihegerz eo an hizivadenn"
+  UPDATING_PLEASE_WAIT: "Oc'h hizivaat... gortozit, emañ o pellgargañ"
+  OF_THIS: "eus an"
+  OF_YOUR: "eus ho"
+  HAVE_AN_UPDATE_AVAILABLE: "en deus un hizivadenn hegerz"
+  SAVE_AS: "Enrollañ evel"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Sur oc'h e fell deoc'h dilemel ar bajenn-mañ hag holl he bugale? M'eo troet ar bajenn en ur yezh all e vo miret an troidigezhioù a rankout a reot o dilemel en un doare distag. E mod all e vo dilamet teuliad ar bajenn gant an is-pajennoù. N'haller ket dizober ar gwered-mañ."
+  AND: "ha"
+  UPDATE_AVAILABLE: "Hizivadenn hegerz"
+  METADATA_KEY: "Alc'hwez (sk. 'Gerioù-alc'hwez')"
+  METADATA_VALUE: "Gwerzh (sk. 'Blog, Grav')"
+  USERNAME_HELP: "Etre 3 ha 16 arouezenn e rank an anv arveriad bezañ o kontañ al lizherennoù bihan, an niverennoù, an islinennoù hag ar barrennigoù. N'eo ket aotreet al lizherennoù bras, an esaouennoù hag an arouezennoù arbennik"
+  FULLY_UPDATED: "Hizivaet"
+  SAVE_LOCATION: "Lec'hiadur enrollañ"
+  PAGE_FILE: "Patrom pajenn"
+  PAGE_FILE_HELP: "Anv restr patrom ar bajenn, ha patrom skrammañ ar bajenn dre ziouer"
+  NO_USER_ACCOUNTS: "Kont arveriad ebet kavet, krouit unan da gentañ..."
+  REDIRECT_TRAILING_SLASH: "Adheñchañ ar veskell dibenn"
+  REDIRECT_TRAILING_SLASH_HELP: "Ober un adheñchañ 301 e-lerc'h merañ an beskell dibenn an URI en un doare treuzwelus."
+  DEFAULT_DATE_FORMAT: "Mentrezh deiziad ar bajenn"
+  DEFAULT_DATE_FORMAT_HELP: "Mentrezh deiziad ar bajenn arveret gant Grav. Dre ziouer, Grav a glask divinout mentrezh an deiziad met gallout a rit erspizañ unan gant kevreadur deiziad PHP (sk.: Y-m-d H:i)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Divinout en emgefreek"
+  IGNORE_FILES: "Leuskel restroù a-gostez"
+  IGNORE_FILES_HELP: "Restroù da leuskel a-gostez e-pad keweriañ ar pajennoù"
+  IGNORE_FOLDERS: "Leuskel teuliadoù a-gostez"
+  IGNORE_FOLDERS_HELP: "Teuliadoù resis da leuskel a-gostez e-pad keweriañ ar pajennoù"
+  HTTP_ACCEPT_LANGUAGE: "Lakaat yezh ar merdeer"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Gallout a rit klask arventennañ ar yezh gant hini ar talbenn `http_accept_language` ar merdeer"
+  OVERRIDE_LOCALE: "Flastrañ ar yezh"
+  OVERRIDE_LOCALE_HELP: "Flastrañ arventenn yezh PHP diazezet war ar yezh vremanel"
+  REDIRECT: "Adheñchañ ar bajenn"
+  REDIRECT_HELP: "Enankit hent ur bajenn pe un URL diavaez da adheñchañ ar bajenn. Sk. '/un/hent' pe 'http://ulload.bzh'"
+  PLUGIN_STATUS: "Stad an elugellad"
+  INCLUDE_DEFAULT_LANG: "Enkorfañ ar yezh dre ziouer"
+  INCLUDE_DEFAULT_LANG_HELP: "Ouzhpennañ a raio ar yezh dre ziouer en holl URLoù er yezh dre ziouer. Sk. '/br/blog/post'"
+  ALLOW_URL_TAXONOMY_FILTERS: "URL siloù rummad"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Dastumadegoù pajennoù a aotren ac'hanoc'h da silañ dre '/rummad:gwerzh'."
+  REDIRECT_DEFAULT_CODE: "Boneg adheñchan dre ziouer"
+  REDIRECT_DEFAULT_CODE_HELP: "Boneg stad HTTP da arverañ evit adheñchañ"
+  IGNORE_HIDDEN: "Leuskel ar re kuzhet a-gostez"
+  IGNORE_HIDDEN_HELP: "Leuskel an holl restroù ha teuliadoù a grog gant ur POENT"
+  WRAPPED_SITE: "Lec'hienn enkorfet"
+  WRAPPED_SITE_HELP: "Evit ma ouife an neuzioù/enlugelladoù m'eo enkorfet Grav en ur savenn all"
+  FALLBACK_TYPES: "Aotren doareoù fallback"
+  FALLBACK_TYPES_HELP: "Doareoù restr aotreet a c'hall bezañ kavet m'int haezet dre hent ar bajenn. An holl zoareoù media skoret dre ziouer."
+  INLINE_TYPES: "Doareoù fallback enkorfet"
+  INLINE_TYPES_HELP: "Ur roll doareoù restroù a rank bezañ skrammet en un doare enkorfet kentoc'h eget pellgarget"
+  APPEND_URL_EXT: "Ouzhpennañ an astenn d'an URL"
+  APPEND_URL_EXT_HELP: "Ouzhpennañ a raio un astenn personelaet da URL ar bajenn. Talvezout a ra e glasko Grav ur patrom anvet `<patrom>.<astenn>.twig`"
+  PAGE_MODES: "Modoù pajenn"
+  PAGE_TYPES: "Doareoù pajenn"
+  ACCESS_LEVELS: "Liveoù haeziñ"
+  GROUPS: "Strolladoù"
+  GROUPS_HELP: "Roll ar strolladoù gant an arveriad enno"
+  ADMIN_ACCESS: "Haeziñ ardoer"
+  SITE_ACCESS: "Haeziñ d'al lec'hienn"
+  INVALID_SECURITY_TOKEN: "Reveziadenn diogelroez didalvoudek"
+  ACTIVATE: "Gweredekaat"
+  TWIG_UMASK_FIX: "Ratreadur Umask"
+  TWIG_UMASK_FIX_HELP: "Twig a grou ar restroù krubuilh gant 0755 dre ziouer, ar ratreañ a lak anezho da 0755"
+  CACHE_PERMS: "Aotreoù ar c'hrubuilh"
+  CACHE_PERMS_HELP: "Aotreoù dre ziouer teuliad ar c'hrubuilh. 0755 pe 0775 peurvuiañ, hervez ar c'hefluniadur"
+  REMOVE_SUCCESSFUL: "Dilamet gant berzh"
+  REMOVE_FAILED: "C'hwitadenn war an dilemel"
+  HIDE_HOME_IN_URLS: "Kuzhat hent ar pennbajenn en URL"
+  HIDE_HOME_IN_URLS_HELP: "Gwiriekaat a raio n'eo ket daveet hent skoueriek an degemer gant hentoù dre ziouer ar pajennoù dindan an degemer"
+  TWIG_FIRST: "Keweriañ an Twig da gentañ"
+  TWIG_FIRST_HELP: "M'ho peus gweredekaat keweriañ ar bajenn Twig e c'hallit kefluniañ Twig evit e geweriañ a-raok pe goude ar Markdown"
+  SESSION_SECURE: "Diogel"
+  SESSION_SECURE_HELP: "M'eo gwir, diskouez a ra eo ret d'ar c'hehentiñ evit an toupin-mañ bezañ graet war un treuzkas diogel. DIWALLIT: Gweredekait an dra-se war lec'hiennoù e HTTPS nemetken"
+  SESSION_HTTPONLY: "HTTP nemetken"
+  SESSION_HTTPONLY_HELP: "M'eo gwir, diskouez a ra eo ret d'ar c'hehentiñ evit an toupin-mañ bezañ graet war un treuzkas HTTP ha n'eo ket aotreet kemmañ ar Javascript"
+  REVERSE_PROXY: "Proksi en tu-gin"
+  REVERSE_PROXY_HELP: "Gweredekait an dra-se m'hoc'h a-dreñv ur proksi en tu-gin hag ho peus diaesterioù gant an URLoù oc'h enderc'hel ur porzh didalvoudek"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Frontmatter didalvoudek, n'haller ket enrollan"
+  ADD_FOLDER: "Ouzhpennañ un teuliad"
+  PROXY_URL: "URL ar proksi"
+  PROXY_URL_HELP: "Enankit HERBERC'HIER pe IP ar proksi hag ar PORZH"
+  NOTHING_TO_SAVE: "Netra da enrollañ"
+  FILE_ERROR_ADD: "Degouezhet ez eus bet ur fazi en ur glask enrollañ ar restr"
+  FILE_ERROR_UPLOAD: "Degouezhet ez eus bet ur fazi en ur glask pellgas ar restr"
+  FILE_UNSUPPORTED: "Doare restr anskor"
+  ADD_ITEM: "Ouzhpennañ un elfenn"
+  FILE_TOO_LARGE: "Re leden eo ar restr evit bezañ pellgaset. %s eo an uhelañ aotreet hervez <br> hoc'h arventennoù PHP. Kreskit an arventenn PHP`post_max_size`"
+  INSTALLING: "O staliañ"
+  LOADING: "O kargañ.."
+  DEPENDENCIES_NOT_MET_MESSAGE: "Ret eo deoc'h staliañ an amzalc'hoù da-heul a-raok:"
+  ERROR_INSTALLING_PACKAGES: "Fazi en ur staliañ ar pakad(où)"
+  INSTALLING_DEPENDENCIES: "O staliañ an amzalc'hoù..."
+  INSTALLING_PACKAGES: "O staliañ ar pakad(où).."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Pakad(où) staliet gant berzh."
+  READY_TO_INSTALL_PACKAGES: "Prest da staliañ ar pakad(où)"
+  PACKAGES_NOT_INSTALLED: "N'eo ket stalied ar pakadoù"
+  PACKAGES_NEED_UPDATE: "Staliet eo ar pakadoù endeo, met re gozh eo"
+  PACKAGES_SUGGESTED_UPDATE: "Staliet eo ar pakadoù endeo, dereat eo an handelv, met hizivaet e vint evit ma vefec'h en handelv diwezhañ"
+  REMOVE_THE: "Dilemel an %s"
+  CONFIRM_REMOVAL: "Sur oc'h e fell deoc'h dilemel %s?"
+  REMOVED_SUCCESSFULLY: "%s dilamet gant berzh"
+  ERROR_REMOVING_THE: "Fazi en ur zilemel %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "An amzalc'hoù da heul a zo azgoulennet gant %s, met n'eo ket azgoulennet gant ur pakad all. Ma ne arverit ket anezho e c'hallit o dilemel adalek amañ."
+  READY_TO_UPDATE_PACKAGES: "Prest da hizivaat ar pakad(où)"
+  ERROR_UPDATING_PACKAGES: "Fazi en ur hizivaat ar pakad(où)"
+  UPDATING_PACKAGES: "Oc'h hizivaat ar pakad(où).."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Pakad(où) hizivaet gant berzh."
+  UPDATING: "Hizivaet"
+  GPM_RELEASES: "Ermaeziadennoù GPM"
+  GPM_RELEASES_HELP: "Dibabit 'Amprouiñ' evit staliañ an handelv beta pe amprouiñ"
+  AUTO: "Oto"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stabil"
+  TESTING: "Amprouiñ"
+  FRONTMATTER_PROCESS_TWIG: "Keweriañ frontmatter Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "P'eo oberiant e c'hallit arverañ argemennoù kefluniañ Twig e frontmatter ar bajenn"
+  FRONTMATTER_IGNORE_FIELDS: "Leuskel maeziennoù Frontmatter a-gostez"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Maeziennoù Frontmatter a c'hall enderc'hel Twig met ne rankont ket bezañ keweriet, evel 'forms'"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Pakad %s staliet gant berzh"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Urzh ar c'har, diweredekaet eo an urzhiañ"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Diwelus eo ar bajenn, diweredekaet eo an urzhiañ"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "N'eo ket skoret an urzhiañ dre an ardeiñ dre ma zo ouzhpenn 200 c'hoar"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "EVEZHIADENN: n'hallit ket ouzhpennañ restroù media evit enrollañ ar bajenn. Klikit war 'Enrollañ' a-us"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "EVEZHIADENN: ret eo enrollañ ar bajenn a-raok pellgas restroù dezhi."
+  DROP_FILES_HERE_TO_UPLOAD: "Lakait ho restroù amañ pe <strong>klikit amañ</strong>"
+  INSERT: "Enlakaat"
+  UNDO: "Dizober"
+  REDO: "Adober"
+  HEADERS: "Talbennoù"
+  BOLD: "Tev"
+  ITALIC: "Stouet"
+  STRIKETHROUGH: "Barrennet"
+  SUMMARY_DELIMITER: "Bonner diverrad"
+  LINK: "Liamm"
+  IMAGE: "Skeudenn"
+  BLOCKQUOTE: "Meneg"
+  UNORDERED_LIST: "Roll dizurzh"
+  ORDERED_LIST: "Roll urzhiet"
+  EDITOR: "Embanner"
+  PREVIEW: "Alberz"
+  FULLSCREEN: "Skramm a-bezh"
+  NON_ROUTABLE: "Dihentus"
+  NON_VISIBLE: "Diwelus"
+  NON_PUBLISHED: "Diembannet"
+  CHARACTERS: "arouezioù"
+  PUBLISHING: "Embann"
+  MEDIA_TYPES: "Doareoù media"
+  IMAGE_OPTIONS: "Opsionoù skeudenn"
+  MIME_TYPE: "Doare Mime"
+  THUMB: "Miniaturenn"
+  TYPE: "Doare"
+  FILE_EXTENSION: "Astenn ar fichenn"
+  LEGEND: "Alc'hwez ar bajenn"
+  MEMCACHE_SERVER: "Servijer Memcached"
+  MEMCACHE_SERVER_HELP: "Chomlec'h ar servijer Memcached"
+  MEMCACHE_PORT: "Porzh Memcached"
+  MEMCACHE_PORT_HELP: "Porzh ar servijer Memcached"
+  MEMCACHED_SERVER: "Servijer Memcached"
+  MEMCACHED_SERVER_HELP: "Chomlec'h ar servijer Memcached"
+  MEMCACHED_PORT: "Porzh Memcached"
+  MEMCACHED_PORT_HELP: "Porzh ar servijer Memcached"
+  REDIS_SERVER: "Servijer Redis"
+  REDIS_SERVER_HELP: "Chomlec'h ar servijer Redis"
+  REDIS_PORT: "Porzh Redis"
+  REDIS_PORT_HELP: "Porzh ar servijer Redis"
+  ALL: "Tout"
+  FROM: "eus"
+  TO: "da"
+  RESOURCE_FILTER: "Sil..."
+  FORCE_SSL: "Forsañ SSL"
+  DROPZONE_CANCEL_UPLOAD: 'Nullañ ar gargamant'
+  DROPZONE_REMOVE_FILE: "Dilemel ar fichenn"
+  PREMIUM_PRODUCT: "Premium"
+  ERROR_SIMPLE: "Fazi simpl"
+  ERROR_SYSTEM: "Fazi Sistem"
+  NOT_SET: "Pas termenet"
+  PERMISSIONS: "Permisionoù"
+  REINSTALL_PLUGIN: "Adstaliañ ar Plugin"
+  REINSTALL_THEME: "Adstaliañ an Tem"
+  REINSTALL_THE: "Adstaliañ ar %s"
+  REINSTALLATION_FAILED: "Adstaliañ c'hwitet"
+  TOOLS: "Ostilhoù"
+  DIRECT_INSTALL: "Staliañ war-eeun"
+  2FA_CODE_INPUT: "000000"
+  CONFIGURATION: "Kefluniadur"
+  TIMEOUT: "Diamzeriñ"
+  TIMEOUT_HELP: "Lakaat an amzer diamzeriñ e eilennoù"
+  DASHBOARD: "Taolenn labour"
+  NOTIFICATIONS: "Notifiadenn"

+ 643 - 0
plugins/admin/languages/ca.yaml

@@ -0,0 +1,643 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "Aquesta és una versió beta! Utilitza-la en producció sota el teu propi risc..."
+  ADMIN_REPORT_ISSUE: "Has trobat algun problema? Sisplau, reporta'l a GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Funcionant amb Grav</a> - El CMS de fitxers plans modern"
+  LOGIN_BTN: "Inicia sessió"
+  LOGIN_BTN_FORGOT: "Ho he oblidat"
+  LOGIN_BTN_RESET: "Restablir contrasenya"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Envia instruccions pel reset"
+  LOGIN_BTN_CLEAR: "Netejar formulari"
+  LOGIN_BTN_CREATE_USER: "Crear usuari"
+  LOGIN_LOGGED_IN: "S'ha iniciat sessió correctament"
+  LOGIN_FAILED: "No s'ha pogut iniciar sessió"
+  LOGGED_OUT: "S'ha tancat la sessió"
+  RESET_NEW_PASSWORD: "Sisplau introdueix una nova contasenya &hellip;"
+  RESET_LINK_EXPIRED: "L'enllaç per a restablir contrasenya ha expirat, torna a provar"
+  RESET_PASSWORD_RESET: "S'ha restablert la contrasenya"
+  RESET_INVALID_LINK: "L'enllaç per a restablir contrasenya invàl·lid, torna a provar"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Les instruccions per a restablir la contrasenya s'han enviat per correu electrònic a %s"
+  FORGOT_FAILED_TO_EMAIL: "S'ha fallat al enviar les instruccions, sisplau torna-ho a provar"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "No es pot restablir la contrasenya per a %s, no té cap email assignat"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "L'usuari <b>%s</b> no existeix"
+  FORGOT_EMAIL_NOT_CONFIGURED: "No es pot restablir la contrasenya. Aquest lloc no està configurat per enviar missatges de correu electrònic"
+  FORGOT_EMAIL_SUBJECT: "%s Petició de restabliment de contrasenya"
+  FORGOT_EMAIL_BODY: "<h1>Restabliment de contrasenya</h1><p>Benvolgut/da %1$s,</p><p>S'ha fet una petició a <b>%4$s</b> per a restablir la contrasenya.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Fes clic aquí per a restablir la contrasenya</a><br /><br /></p><p>Altrament, copia el següent URL al teu navegador:</p><p>%2$s</p><p><br />Atentament,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Gestiona pàgines"
+  PAGES: "Pàgines"
+  PLUGINS: "Plugins"
+  PLUGIN: "Plugin"
+  THEMES: "Temes"
+  LOGOUT: "Tanca sessió"
+  BACK: "Enrere"
+  NEXT: "Següent"
+  PREVIOUS: "Anterior"
+  ADD_PAGE: "Afegeix pàgina"
+  MOVE: "Mou"
+  DELETE: "Esborra"
+  VIEW: "Visualitzar"
+  SAVE: "Desa"
+  NORMAL: "Normal"
+  EXPERT: "Expert"
+  EXPAND_ALL: "Expandeix tot"
+  COLLAPSE_ALL: "Col·lapsa tot"
+  ERROR: "Error"
+  CLOSE: "Tanca"
+  CANCEL: "Cancel·la"
+  CONTINUE: "Continua"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Confirmació requerida"
+  MODAL_CHANGED_DETECTED_TITLE: "Canvis detectats"
+  MODAL_CHANGED_DETECTED_DESC: "Tens canvis no desats. Estàs segur que vols sortir sense desar?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Confirmació requerida"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Estàs segur que vols eliminar aquest fitxer? Aquesta acció no es pot desfer."
+  ADD_FILTERS: "Afegeix filtres"
+  SEARCH_PAGES: "Cerca pàgines"
+  VERSION: "Versió"
+  WAS_MADE_WITH: "Es va fer amb"
+  BY: "Per"
+  UPDATE_THEME: "Actualitza tema"
+  UPDATE_PLUGIN: "Actualitza plugin"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "d'aquest tema està disponible"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "d'aquest plugin està disponible"
+  AUTHOR: "Autor/a"
+  HOMEPAGE: "Pàgina d'inici"
+  DEMO: "Demo"
+  BUG_TRACKER: "Rastrejador d'errors"
+  KEYWORDS: "Paraules clau"
+  LICENSE: "Llicència"
+  DESCRIPTION: "Descripció"
+  README: "Llegiu-me"
+  REMOVE_THEME: "Elimina tema"
+  INSTALL_THEME: "Instal·la tema"
+  THEME: "Tema"
+  BACK_TO_THEMES: "Torna a Temes"
+  BACK_TO_PLUGINS: "Torna a Plugins"
+  CHECK_FOR_UPDATES: "Cerca actualitzacions"
+  ADD: "Afegeix"
+  CLEAR_CACHE: "Neteja la memòria cache"
+  CLEAR_CACHE_ALL_CACHE: "Tota la cache"
+  CLEAR_CACHE_ASSETS_ONLY: "Només assets"
+  CLEAR_CACHE_IMAGES_ONLY: "Només imatges"
+  CLEAR_CACHE_CACHE_ONLY: "Només cache"
+  CLEAR_CACHE_TMP_ONLY: "Només tmp"
+  UPDATES_AVAILABLE: "Hi ha actualizacions disponibles"
+  DAYS: "Dies"
+  UPDATE: "Actualitza"
+  BACKUP: "Còpia de seguretat"
+  BACKUPS: "Còpia de seguretat"
+  BACKUP_NOW: "Fer còpia de seguretat ara"
+  BACKUPS_STATS: "Estadístiques de còpia de seguretat"
+  BACKUPS_HISTORY: "Historial de còpies de seguretat"
+  BACKUPS_PURGE_CONFIG: "Configuració de depuració de còpia de seguretat"
+  BACKUPS_PROFILES: "Perfils de còpia de seguretat"
+  BACKUPS_COUNT: "Nombre de còpies de seguretat"
+  BACKUPS_PROFILES_COUNT: "Nombre de perfils"
+  BACKUPS_TOTAL_SIZE: "Espai utilitzat"
+  STATISTICS: "Estadístiques"
+  TODAY: "Avui"
+  WEEK: "Setmana"
+  MONTH: "Mes"
+  LATEST_PAGE_UPDATES: "Últimes pàgines actualitzades"
+  MAINTENANCE: "Manteniment"
+  UPDATED: "Actualitzat"
+  MON: "Dl."
+  TUE: "Dt."
+  WED: "Dc."
+  THU: "Dj."
+  FRI: "Dv."
+  SAT: "Ds."
+  SUN: "Dg."
+  COPY: "Copia"
+  EDIT: "Edita"
+  CREATE: "Crea"
+  GRAV_ADMIN: "Administració Grav"
+  GRAV_OFFICIAL_PLUGIN: "Plugin oficial de Grav"
+  GRAV_OFFICIAL_THEME: "Tema oficial de Grav"
+  PLUGIN_SYMBOLICALLY_LINKED: "Aquest plugin està lligat simbòlicament. Les actualitzacions no seran detectades."
+  THEME_SYMBOLICALLY_LINKED: "Aquest tema està lligat simbòlicament. Les actualitzacions no seran detectades"
+  REMOVE_PLUGIN: "Elimina plugin"
+  INSTALL_PLUGIN: "Instal·la plugin"
+  AVAILABLE: "Disponible"
+  INSTALLED: "Instal·lat"
+  INSTALL: "Instal·la"
+  ACTIVE_THEME: "Tema actiu"
+  SWITCHING_TO: "Canviant a"
+  SWITCHING_TO_DESCRIPTION: "Al canviar a un tema diferent, no es garanteix que tots els estils de pàgina siguin compatibles, potencialment, pot haver-hi errors al intentar carregar aquestes pàgines."
+  SWITCHING_TO_CONFIRMATION: "Vols continuar i canviar de tema"
+  CREATE_NEW_USER: "Crea nou usuari"
+  REMOVE_USER: "Elimina usuari"
+  ACCESS_DENIED: "Accés denegat"
+  ACCOUNT_NOT_ADMIN: "el teu compte no té permisos d'administrador"
+  PHP_INFO: "Informació PHP"
+  INSTALLER: "Instal·lador"
+  AVAILABLE_THEMES: "Temes disponibles"
+  AVAILABLE_PLUGINS: "Plugins disponibles"
+  INSTALLED_THEMES: "Temes instal·lats"
+  INSTALLED_PLUGINS: "Plugins instal·lats"
+  BROWSE_ERROR_LOGS: "Examina registres d'errors"
+  SITE: "Lloc web"
+  INFO: "Info"
+  SYSTEM: "Sistema"
+  USER: "Usuari"
+  ADD_ACCOUNT: "Afegeix compte"
+  SWITCH_LANGUAGE: "Canvia l'idioma"
+  SUCCESSFULLY_ENABLED_PLUGIN: "El plugin s'ha activat correctament"
+  SUCCESSFULLY_DISABLED_PLUGIN: "El plugin s'ha desactivat correctament"
+  SUCCESSFULLY_CHANGED_THEME: "S'ha canviat el tema per defecte correctament"
+  INSTALLATION_FAILED: "La instal·lació ha fallat"
+  INSTALLATION_SUCCESSFUL: "Instal·lació satisfactòria"
+  UNINSTALL_FAILED: "Desinstal·lació fallida"
+  UNINSTALL_SUCCESSFUL: "Desinstal·lació satisfactòria"
+  SUCCESSFULLY_SAVED: "Desat satisfactòriament"
+  SUCCESSFULLY_COPIED: "Copiat satisfactòriament"
+  REORDERING_WAS_SUCCESSFUL: "Reordenació satisfactòria"
+  SUCCESSFULLY_DELETED: "Eliminat satisfactòriament"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Idioma canviat satisfactòriament"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "No tens permisos suficients per a la tasca"
+  CACHE_CLEARED: "Memòria cache esborrada"
+  METHOD: "Mètode"
+  ERROR_CLEARING_CACHE: "Error al esborrar cache"
+  AN_ERROR_OCCURRED: "S'ha produït un error"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "La teva còpia de seguretat està llesta per a descarregar"
+  DOWNLOAD_BACKUP: "Descarrega còpia de seguretat"
+  PAGES_FILTERED: "Pàgines filtrades"
+  NO_PAGE_FOUND: "No s'han trobat pàgines"
+  INVALID_PARAMETERS: "Els paràmetres són invàlids"
+  NO_FILES_SENT: "No s'han enviat fitxers"
+  EXCEEDED_FILESIZE_LIMIT: "S'ha excedit el límit de tamany de fitxer de la configuració de PHP"
+  UNKNOWN_ERRORS: "Hi ha hagut errors desconeguts"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "S'ha excedit el límit de tamany de fitxer de la configuració de Grav"
+  UNSUPPORTED_FILE_TYPE: "Tipus de fitxer no suportat"
+  FAILED_TO_MOVE_UPLOADED_FILE: "S'ha fallat al moure el fitxer carregat."
+  FILE_UPLOADED_SUCCESSFULLY: "S'ha carregat el fitxer amb èxit"
+  FILE_DELETED: "S'ha esborrat el fitxer"
+  FILE_COULD_NOT_BE_DELETED: "No s'ha pogut esborrar el fitxer"
+  FILE_NOT_FOUND: "No s'ha trobat el fitxer"
+  NO_FILE_FOUND: "No s'han trobat fitxers"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav ha estat actualitzat amb èxit a"
+  GRAV_UPDATE_FAILED: "Ha fallat l'actualització de Grav"
+  EVERYTHING_UPDATED: "Tot està actualitzat"
+  UPDATES_FAILED: "Han fallat les actualitzacions"
+  AVATAR_BY: "Avatar per"
+  LAST_BACKUP: "Última còpia de seguretat"
+  FULL_NAME: "Nom complet"
+  USERNAME: "Nom d'usuari"
+  EMAIL: "Email"
+  USERNAME_EMAIL: "Nom d'usuari o correu electrònic"
+  PASSWORD: "Contrasenya"
+  PASSWORD_CONFIRM: "Confirma contrasenya"
+  TITLE: "Títol"
+  LANGUAGE: "Llengua"
+  ACCOUNT: "Compte d'usuari"
+  EMAIL_VALIDATION_MESSAGE: "Ha de ser una adreça de correu electrònic vàl·lida"
+  PASSWORD_VALIDATION_MESSAGE: "La contrasenya ha de contenir almenys un número i una lletra majúscula i minúscula, i almenys 8 o més caràcters"
+  LANGUAGE_HELP: "Estableix la llengua preferida"
+  MEDIA: "Mèdia"
+  DEFAULTS: "Per defecte"
+  SITE_TITLE: "Títol del lloc"
+  SITE_TITLE_PLACEHOLDER: "Títol a tot el lloc"
+  SITE_TITLE_HELP: "Títol per defecte pel teu lloc, sovint utilitzat en els temes"
+  SITE_DEFAULT_LANG: "Llenguatge per defecte"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Llenguatge per defecte per a ser utilitzat per l'etiqueta del tema <HTML>"
+  SITE_DEFAULT_LANG_HELP: "Llenguatge per defecte per a ser utilitzat per l'etiqueta del tema <HTML>"
+  DEFAULT_AUTHOR: "Autor/a per defecte"
+  DEFAULT_AUTHOR_HELP: "Un nom d'autor/a per defecte, algunes vegades utilitzat en temes o contingut de les pàgines"
+  DEFAULT_EMAIL: "Email per defecte"
+  DEFAULT_EMAIL_HELP: "Un correu electrònic per referenciar en temes o pàgines"
+  TAXONOMY_TYPES: "Tipus de taxonomia"
+  TAXONOMY_TYPES_HELP: "Els tipus de taxonomia han de definir-se aquí si es desitja utilitzar-les en pàgines"
+  PAGE_SUMMARY: "Resum de pàgina"
+  ENABLED: "Habilitat"
+  ENABLED_HELP: "Habilita resum de pàgina (el resum retorna el mateix que el contingut de la pàgina)"
+  'YES': "Sí"
+  'NO': "No"
+  SUMMARY_SIZE: "Mida del resum"
+  SUMMARY_SIZE_HELP: "La quantitat de caràcters d'una pàgina a utilitzar com a resum del contingut"
+  FORMAT: "Format"
+  FORMAT_HELP: "curt = utilitza la primera ocurrència del delimitador o mida; llarg = s'ignorarà el delimitador del resum"
+  SHORT: "Curt"
+  LONG: "Llarg"
+  DELIMITER: "Delimitador"
+  DELIMITER_HELP: "El delimitador de resum (per defecte '===')"
+  METADATA: "Metadades"
+  METADATA_HELP: "Valors de metadades per defecte que es mostraran a cada pàgina excepte si es sobreescriuen a la pàgina"
+  NAME: "Nom"
+  CONTENT: "Contingut"
+  REDIRECTS_AND_ROUTES: "Redireccions i rutes"
+  CUSTOM_REDIRECTS: "Redireccions personalitzades"
+  CUSTOM_REDIRECTS_HELP: "rutes per redirigir a altres pàgines. Substitució de Regex estàndard és vàl·lida"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/el/teu/àlies"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/la/teva/redirecció"
+  CUSTOM_ROUTES: "Rutes personalitzades"
+  CUSTOM_ROUTES_HELP: "rutes a àlies a altres pàgines. Substitució de Regex estàndard és vàl·lida"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/el/teu/àlies"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/la/teva/ruta"
+  FILE_STREAMS: "Streams de fitxers"
+  DEFAULT: "Per defecte"
+  PAGE_MEDIA: "Contingut multimèdia de la pàgina"
+  OPTIONS: "Opcions"
+  PUBLISHED: "Publicat"
+  PUBLISHED_HELP: "Per defect, una pàgina es publica excepte que estableixis explícitament 'published': false o posant una publish_date al futur o una unpublish_date al passat"
+  DATE: "Data"
+  DATE_HELP: "La variable data permet definir específicament una data associada a aquesta pàgina."
+  PUBLISHED_DATE: "Data de publicació"
+  PUBLISHED_DATE_HELP: "Pots proporcionar una data per provocar automàticament la publicació."
+  UNPUBLISHED_DATE: "Data de despublicació"
+  UNPUBLISHED_DATE_HELP: "Pots proporcionar una data per provocar automàticament la despublicació."
+  ROBOTS: "Robots"
+  TAXONOMIES: "Taxonomies"
+  TAXONOMY: "Taxonomia"
+  ADVANCED: "Avançat"
+  SETTINGS: "Configuració"
+  FOLDER_NUMERIC_PREFIX: "Prefix numèric carpeta"
+  FOLDER_NUMERIC_PREFIX_HELP: "Prefix numèric que proporciona ordenació manual i implica la visibilitat"
+  FOLDER_NAME: "Nom de la carpeta"
+  FOLDER_NAME_HELP: "El nom de carpeta que s'emmagatzemarà al sistema de fitxers per a aquesta pàgina"
+  PARENT: "Pare"
+  DEFAULT_OPTION_ROOT: "-Root-"
+  DEFAULT_OPTION_SELECT: "- selecciona -"
+  DISPLAY_TEMPLATE: "Plantilla a mostrar"
+  DISPLAY_TEMPLATE_HELP: "El tipus de pàgina que es tradueix a la plantilla de Twig en què es renderitza la pàgina"
+  ORDERING: "Ordenació"
+  PAGE_ORDER: "Ordre de pàgines"
+  OVERRIDES: "Sobreescriu"
+  MENU: "Menú"
+  MENU_HELP: "El text a utilitzar en un menú. Si no s'estableix, s'utilitzarà el Títol."
+  SLUG: "Slug"
+  SLUG_HELP: "La variable slug permet definir específicament la part de l'URL que correspon a la pàgina"
+  SLUG_VALIDATE_MESSAGE: "L'slug ha de contenir només caràcters alfanumèrics en minúscules i guions"
+  PROCESS: "Processa"
+  PROCESS_HELP: "Control de com es processen les pàgines. Configurable per a pàgina enlloc de globalment"
+  DEFAULT_CHILD_TYPE: "Tipus de fill per defecte"
+  USE_GLOBAL: "Ús global"
+  ROUTABLE: "Accessible"
+  ROUTABLE_HELP: "Si aquesta pàgina és accessible des d'una URL"
+  CACHING: "Caching"
+  VISIBLE: "Visible"
+  VISIBLE_HELP: "Determina si una pàgina és visible en la navegació."
+  DISABLED: "Deshabilitat"
+  ITEMS: "Ítems"
+  ORDER_BY: "Ordena per"
+  ORDER: "Ordena"
+  FOLDER: "Carpeta"
+  ASCENDING: "Ascendent"
+  DESCENDING: "Descendent"
+  PAGE_TITLE: "Títol de pàgina"
+  PAGE_TITLE_HELP: "El títol de la pàgina"
+  PAGE: "Pàgina"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Nom de fitxer"
+  PARENT_PAGE: "Pàgina pare"
+  HOME_PAGE: "Pàgina d'inici"
+  HOME_PAGE_HELP: "La pàgina que utilitzarà Grav com a destinació per defecte"
+  DEFAULT_THEME: "Tema per defecte"
+  DEFAULT_THEME_HELP: "Estableix el tema per defecte que utilitzarà Grav (per defecte és Antimatter)"
+  TIMEZONE: "Zona horària"
+  TIMEZONE_HELP: "Sobreescriu la zona horària del servidor"
+  SHORT_DATE_FORMAT: "Format de data curt"
+  SHORT_DATE_FORMAT_HELP: "Estableix el format de data curt que poden utilitzar els temes"
+  LONG_DATE_FORMAT: "Format de data llarg"
+  LONG_DATE_FORMAT_HELP: "Estableix el format de data llarg que poden utilitzar els temes"
+  DEFAULT_ORDERING: "Ordre per defecte"
+  DEFAULT_ORDERING_HELP: "Les pàgines de la llista seran generades utilitzant aquest ordre excepte que se sobreescrigui"
+  DEFAULT_ORDERING_DEFAULT: "Per defecte - basat en el nom de la carpeta"
+  DEFAULT_ORDERING_FOLDER: "Carpeta - basat en el nom de carpeta sense prefix"
+  DEFAULT_ORDERING_TITLE: "Títol - basat en el camp de títol de la capçalera"
+  DEFAULT_ORDERING_DATE: "Data - basat en el camp data de la capçalera"
+  DEFAULT_ORDER_DIRECTION: "Direcció d'ordre per defecte"
+  DEFAULT_ORDER_DIRECTION_HELP: "La direcció de les pàgines en una llista"
+  DEFAULT_PAGE_COUNT: "Compte de pàgina per defecte"
+  DEFAULT_PAGE_COUNT_HELP: "Nombre màxim de compte de pàgines en una llista"
+  DATE_BASED_PUBLISHING: "Publicació basada en la data"
+  DATE_BASED_PUBLISHING_HELP: "(Des)publicar automàticament posts basant-se en la seva data"
+  EVENTS: "Esdeveniments"
+  EVENTS_HELP: "Habilita o inhabilita esdeveniments concrets.  Deshabilitant-los pot trencar plugins"
+  REDIRECT_DEFAULT_ROUTE: "Ruta de redirecció per defecte"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Redirigir automàticament a la ruta per defecte d'una pàgina"
+  LANGUAGES: "Idiomes"
+  SUPPORTED: "Suportat"
+  SUPPORTED_HELP: "Llista separada per comes de codis d'idioma de 2 lletres (per exemple 'en,fr,de')"
+  TRANSLATIONS_FALLBACK: "Fallback de traduccions"
+  TRANSLATIONS_FALLBACK_HELP: "Fallback en traduccions suportades si l'idioma actiu no existeix"
+  ACTIVE_LANGUAGE_IN_SESSION: "Idioma actiu a la sessió"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Emmagatzema l'idioma actiu a la sessió"
+  HTTP_HEADERS: "Capçaleres HTTP"
+  EXPIRES: "Caduca"
+  EXPIRES_HELP: "Estableix la capçalera d'expiració. El valor és en segons."
+  LAST_MODIFIED: "Darrera modificació"
+  LAST_MODIFIED_HELP: "Estableix la capçalera de darrera modificació que pot optimitzar el proxy i la cache del navegador"
+  ETAG: "ETag"
+  ETAG_HELP: "Estableix la capçalera d'etag per ajudar a identificar quan una pàgina ha estat modificada"
+  VARY_ACCEPT_ENCODING: "Variar accept encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Estableix la capçalera 'Vary: Accept Encoding' per ajudar amb el proxy i la cache CDN"
+  MARKDOWN_EXTRA_HELP: "Habilita suport per defecte per a Markdown Extra - https://michelf.ca/projects/php-markdown/extra/"
+  AUTO_LINE_BREAKS: "Salts de línia automàtics"
+  AUTO_LINE_BREAKS_HELP: "Habilita el suport per a salts de línia automàtics a Markdown"
+  AUTO_URL_LINKS: "Enllaços URL automàtics"
+  AUTO_URL_LINKS_HELP: "Habilita l'autoconversió d'URLs a hyperlinks HTML"
+  ESCAPE_MARKUP: "Escape markup"
+  ESCAPE_MARKUP_HELP: "Escape markup tags en entitats HTML"
+  CACHING_HELP: "Interruptor ON/OFF global per habilitar/deshabilitar cache de Grav"
+  CACHE_CHECK_METHOD: "Mètode de verificació de cache"
+  CACHE_CHECK_METHOD_HELP: "Seleccionar el mètode que utilitza Grav per comprovar si s'han modificat arxius de pàgina."
+  CACHE_DRIVER: "Contolador de cache"
+  CACHE_DRIVER_HELP: "Selecciona quin controlador de cache de Grav cal usar. 'Auto Detect' intenta trobar el millor per a tu"
+  CACHE_PREFIX: "Prefix de cache"
+  CACHE_PREFIX_HELP: "Un identificador per part de la clau de Grav.  No la canviïs excepte que sàpigues el que estàs fent."
+  CACHE_PREFIX_PLACEHOLDER: "Derivat de l'URL base (sobreescriu introduint strings aleatòris)"
+  LIFETIME: "Cicle de vida"
+  LIFETIME_HELP: "Defineix el cicle de vida de la cache en segons. 0 = infinit"
+  GZIP_COMPRESSION: "Compressió gzip"
+  GZIP_COMPRESSION_HELP: "Habilita la compressió GZip de la pàgina de Grav per augmentar el rendiment."
+  TWIG_TEMPLATING: "Plantilles de Twig"
+  TWIG_CACHING: "Cache de Twig"
+  TWIG_CACHING_HELP: "Controla el mecanisme de cache de Twig. Deixa'l habilitat per a millor rendiment."
+  TWIG_DEBUG: "Depuració de Twig"
+  TWIG_DEBUG_HELP: "Permet l'opció de no carregar l'extensió de depuració de Twig"
+  DETECT_CHANGES: "Detectar canvis"
+  DETECT_CHANGES_HELP: "Twig recompilarà automàticament la cache de Twig si detecta canvis a les plantilles de Twig"
+  AUTOESCAPE_VARIABLES: "Variables d'autoescape"
+  AUTOESCAPE_VARIABLES_HELP: "Autoescapa totes les variables. Això probablement trencarà el teu lloc"
+  ASSETS: "Assets"
+  CSS_PIPELINE: "CSS pipeline"
+  CSS_PIPELINE_HELP: "El CSS pipeline és l'unificació de diversos recursos CSS en un sol fitxer"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Inclou fitxers externs en el CSS pipeline"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "A vegades, algunes URLs externes tenen referències relatives de fitxer i no s'hi hauria de fer pipelining"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "Processar pimer el CSS pipeline"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Intrepreta el CSS pipeline abans de qualsevol altra referència CSS que no estigui inclosa"
+  CSS_MINIFY: "Minifica CSS"
+  CSS_MINIFY_HELP: "Minifica el CSS durant el pipelining"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "Sobreesciu la minificació de CSS a Windows"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Sobreescriu la minificació en plataformes Windows. Fals per defecte degut a ThreadStackSize"
+  CSS_REWRITE: "Reescriptura CSS"
+  CSS_REWRITE_HELP: "Reescriu qualsevol URL relativa de CSS durant el pipelining"
+  JAVASCRIPT_PIPELINE: "JavaScript pipeline"
+  JAVASCRIPT_PIPELINE_HELP: "El JS pipeline és l'unificació de diversos recursos JS en un sol fitxer"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Inclou fitxers externs en el JS pipeline"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "A vegades, les URLs externes tenen referències d'arxiu i no s'hi hauria de fer pipelining"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Interpreta primer el JS pipeline"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Interpreta el JS pipeline abans de qualsevol altra referència JS que no estigui inclosa"
+  JAVASCRIPT_MINIFY: "Minificació JavaScript"
+  JAVASCRIPT_MINIFY_HELP: "Minifica el JS durant el pipelining"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Habilita les marques de temps als assets"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Habilita les marques de temps a assets"
+  COLLECTIONS: "Col·leccions"
+  ERROR_HANDLER: "Controlador d'errors"
+  DISPLAY_ERRORS: "Mostra errors"
+  DISPLAY_ERRORS_HELP: "Mostra pàgina d'error full backstrace-style"
+  LOG_ERRORS: "Registre d'errors"
+  LOG_ERRORS_HELP: "Registre d'errors a la carpeta /logs"
+  DEBUGGER: "Depurador"
+  DEBUGGER_HELP: "Habilita depurador de Grav i les configuracions següents"
+  DEBUG_TWIG: "Depuració de Twig"
+  DEBUG_TWIG_HELP: "Habilita la depuració de plantilles de Twig"
+  SHUTDOWN_CLOSE_CONNECTION: "Al apagar tanca la connexió"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Tanca la connexió abans de cridar onShutdown(). False per a la depuració"
+  DEFAULT_IMAGE_QUALITY: "Qualitat d'imatge per defecte"
+  DEFAULT_IMAGE_QUALITY_HELP: "Qualitat d'imatge per defecte per a ser utilitzada quan es remostri o es guardi a cache les imatges (85%)"
+  CACHE_ALL: "Guardar totes les imatges a cache"
+  CACHE_ALL_HELP: "Guarda totes les imatges al sistema cache de Grav fins i tot si no tenen cap manipulació de mèdia"
+  IMAGES_DEBUG: "Marca d'aigua de depuració"
+  IMAGES_DEBUG_HELP: "Mostra un overlay sobre les imatges indicant la profunditat de píxels quan es treballa amb retina, per exemple"
+  UPLOAD_LIMIT: "Límit de tamany de fitxer"
+  UPLOAD_LIMIT_HELP: "Defineix el tamany màxim de càrrega en bytes (0 = il·limitat)"
+  ENABLE_MEDIA_TIMESTAMP: "Permet timestamps en fitxers multimèdia"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Afegeix un timestamp basat en la data d'última modificació a cada element multimèdia"
+  SESSION: "Sessió"
+  SESSION_ENABLED_HELP: "Habilita suport de sessions a Grav"
+  SESSION_NAME_HELP: "Un identificador usat per formar el nom de la galeta de sessió"
+  ABSOLUTE_URLS: "URLs absolutes"
+  ABSOLUTE_URLS_HELP: "URLs absolutes o relatives per a 'base_url'"
+  PARAMETER_SEPARATOR: "Separador de paràmetres"
+  PARAMETER_SEPARATOR_HELP: "Separador per a paràmetres passats que es poden canviar d'Apache a Windows"
+  TASK_COMPLETED: "Tasca completada"
+  EVERYTHING_UP_TO_DATE: "Tot està actualitzat"
+  UPDATES_ARE_AVAILABLE: "hi ha actualitzacions disponibles"
+  IS_AVAILABLE_FOR_UPDATE: "està disponible per a l'actualització"
+  IS_NOW_AVAILABLE: "ja està disponible"
+  CURRENT: "Actual"
+  UPDATE_GRAV_NOW: "Actualitza Grav ara"
+  GRAV_SYMBOLICALLY_LINKED: "Grav està lligat simbòlicament. Les actualitzacions no estaran disponibles"
+  UPDATING_PLEASE_WAIT: "Actualitzant... descarregant, espera sisplau"
+  OF_THIS: "d'aquest/a"
+  OF_YOUR: "del teu"
+  HAVE_AN_UPDATE_AVAILABLE: "té disponible una actualització"
+  SAVE_AS: "Desa com a"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Esteu segur que voleu suprimir aquesta pàgina i tots els seus fills? Si la pàgina es tradueix en altres llengües, les traduccions es mantindran i han de ser esborrades per separat. En cas contrari la carpeta de la pàgina serà eliminada juntament amb les seves subpàgines. Aquesta acció no es pot desfer."
+  AND: "i"
+  UPDATE_AVAILABLE: "Actualització disponible"
+  METADATA_KEY: "Clau (p. ex. 'paraules clau')"
+  METADATA_VALUE: "Valor (per exemple, \"Blog, Grav\")"
+  USERNAME_HELP: "El nom d'usuari ha de tenir entre 3 i 16 caràcters, incloent minúscules, números, guions i guions baixos. Lletres majúscules, espais i caràcters especials no estan permesos"
+  FULLY_UPDATED: "Completament actualitzat"
+  SAVE_LOCATION: "Desa a la ubicació"
+  PAGE_FILE: "Plantilla de pàgina"
+  PAGE_FILE_HELP: "Nom d'arxiu de la plantilla de pàgina i plantilla de visualització per defecte"
+  NO_USER_ACCOUNTS: "No s'han trobat comptes d'usuari, sisplau crea'n una..."
+  REDIRECT_TRAILING_SLASH: "Redirigeix barra final"
+  REDIRECT_TRAILING_SLASH_HELP: "Realitza una redirecció 301 en lloc de manteniment transparent de barra final."
+  DEFAULT_DATE_FORMAT: "Format de data de pàgina"
+  DEFAULT_DATE_FORMAT_HELP: "Format de data de pàgina utilitzat per Grav. Per defecte, Grav intenta endivinar el format de data, tot i això pots especificar un format utilitzant la sintaxi de data de PHP (p.e., Y-m-d H:i)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Endevina automàticament"
+  IGNORE_FILES: "Ignora fitxers"
+  IGNORE_FILES_HELP: "Arxius específics a ignorar al processar pàgines"
+  IGNORE_FOLDERS: "Ignora carpetes"
+  IGNORE_FOLDERS_HELP: "Carpetes específiques a ignorar al processar pàgines"
+  HTTP_ACCEPT_LANGUAGE: "Estableix la llengua a partir del navegador"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Pots optar per intentar establir la llengua basant-se en l'etiqueta de la capçalera 'http_accept_language' en el navegador"
+  OVERRIDE_LOCALE: "Sobreescriu la configuració local"
+  OVERRIDE_LOCALE_HELP: "Sobreescriu la configuració local en PHP basant-se en la llengua actual"
+  REDIRECT: "Redirecció de pàgina"
+  REDIRECT_HELP: "Escriu una ruta de pàgina o URL externa per aqueta pàgina a redirigir a per exemple, '/alguna/ruta' o 'https://algunlloc.com'"
+  PLUGIN_STATUS: "Estat del plugin"
+  INCLUDE_DEFAULT_LANG: "Inclou llengua predeterminada"
+  INCLUDE_DEFAULT_LANG_HELP: "Això sobreposarà totes les URLs amb la llengua per defecte. Per exemple, 'ca/blog/el-meu-post'"
+  ALLOW_URL_TAXONOMY_FILTERS: "Filtres de taxonomia d'URL"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Col·lecions basades en pàgines que et permeten filtrar per '/taxonomia:valor'."
+  REDIRECT_DEFAULT_CODE: "Codi de redirecció per defecte"
+  REDIRECT_DEFAULT_CODE_HELP: "El codi d'estat HTTP per utilitzar en redireccions"
+  IGNORE_HIDDEN: "Ignora ocults"
+  IGNORE_HIDDEN_HELP: "Ignora tots els fitxers i carpetes que comencin amb un punt"
+  WRAPPED_SITE: "Lloc encapsulat"
+  WRAPPED_SITE_HELP: "Per a que els temes/plugins sàpiguen si Grav està encapsulat en una altra plataforma"
+  FALLBACK_TYPES: "Tipus de fallback permesos"
+  FALLBACK_TYPES_HELP: "Tipus de fitxers permesos que es poden trobar si s'accedeix per la ruta de la pàgina. Per defecte, qualsevol tipus de mèdia compatible."
+  INLINE_TYPES: "Tipus de fallback en línia"
+  INLINE_TYPES_HELP: "Una llista de tipus d'arxius que han de ser mostrats en línia enlloc de descarregats"
+  APPEND_URL_EXT: "Afegeix extensió de direcció URL"
+  APPEND_URL_EXT_HELP: "Afegirà una extensió personalitzarà a l'URL de la pàgina. Tingues en compte que això farà que Grav busqui la plantilla '<template>'.<extension>.twig'"
+  PAGE_MODES: "Modes de pàgina"
+  PAGE_TYPES: "Tipus de pàgina"
+  ACCESS_LEVELS: "Nivells d'accés"
+  GROUPS: "Grups"
+  GROUPS_HELP: "Llista de grups dels quals l'usuari en forma part"
+  ADMIN_ACCESS: "Accés d'administrador"
+  SITE_ACCESS: "Accés al lloc"
+  INVALID_SECURITY_TOKEN: "Clau de seguretat invàl·lida"
+  ACTIVATE: "Activa"
+  TWIG_UMASK_FIX: "Corregeix Umask"
+  TWIG_UMASK_FIX_HELP: "Per defecte Twig crea arxius cache com a 0755, la correcció ho canvia a 0775"
+  CACHE_PERMS: "Permisos de cache"
+  CACHE_PERMS_HELP: "Permisos de carpeta de cache per defecte. Normalment 0755 o 0775 segons la configuració"
+  REMOVE_SUCCESSFUL: "Eliminació amb èxit"
+  REMOVE_FAILED: "No s'ha pogut eliminar"
+  HIDE_HOME_IN_URLS: "Amaga ruta de la pàgina inicial en les URLs"
+  HIDE_HOME_IN_URLS_HELP: "S'assegurarà que les rutes per defecte de qualsevol pàgina sota la pàgina inicial no facin referència a la ruta regular de la pàgina inicial"
+  TWIG_FIRST: "Processa Twig primer"
+  TWIG_FIRST_HELP: "Si el processament de pàgina de Twig està habilitat, es pot configurar que Twig faci el processament després de markdown"
+  SESSION_SECURE: "Segur"
+  SESSION_SECURE_HELP: "Si és cert, indica que la comunicació per aquesta cookie ha de ser sobre una connexió encriptada. ALERTA: Habilita aquesta opicó en llocs que funcionen exclusivament en HTTPS"
+  SESSION_HTTPONLY: "Només HTTP"
+  SESSION_HTTPONLY_HELP: "Si és cert, indica que les cookies hauran de ser utilitzades només sobre HTTP, i les modificacions de JavaScript no estan permeses"
+  REVERSE_PROXY: "Proxy invers"
+  REVERSE_PROXY_HELP: "Habilita-ho si estàs darrere d'un proxy invers i tens problemes amb URLs que continguin ports incorrectes"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Frontmatter invàlid, no s'ha pogut desar"
+  ADD_FOLDER: "Afegeix carpeta"
+  PROXY_URL: "URL del proxy"
+  PROXY_URL_HELP: "Escriu el HOST proxy o IP i PORt"
+  NOTHING_TO_SAVE: "Res a desar"
+  FILE_ERROR_ADD: "S'ha produït un error mentre s'intentava afegir el fitxer"
+  FILE_ERROR_UPLOAD: "S'ha produït un error mentre s'intentava carregar el fitxer"
+  FILE_UNSUPPORTED: "Tipus de fitxer no suportat"
+  ADD_ITEM: "Afegeix ítem"
+  FILE_TOO_LARGE: "El fitxers és massa gran per carregar-se, el màxim permès és %s segons<br>la teva configuració PHP. Incrementa la opció PHP 'post_max_size'"
+  INSTALLING: "Instal·lant"
+  LOADING: "Carregant..."
+  DEPENDENCIES_NOT_MET_MESSAGE: "Les següents dependències han de ser cobertes primer:"
+  ERROR_INSTALLING_PACKAGES: "S'ha produït un error mentre s'instal·lava el(s) paquet(s)"
+  INSTALLING_DEPENDENCIES: "Instal·lant dependències..."
+  INSTALLING_PACKAGES: "Instal·lant paquet(s)..."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Paquet(s) instal·lat(s) correctament."
+  READY_TO_INSTALL_PACKAGES: "Preparat per instal·lar el(s) paquet(s)"
+  PACKAGES_NOT_INSTALLED: "No s'han instal·lat els paquets"
+  PACKAGES_NEED_UPDATE: "Els paquets ja estaven instal·lats, però eren massa antics"
+  PACKAGES_SUGGESTED_UPDATE: "Els paquets ja estaven instal·lats, i la versió està bé, però s'actualitzaran per tal d'estar al dia"
+  REMOVE_THE: "Elimina el %s"
+  CONFIRM_REMOVAL: "Estàs segur que vols eliminar aquest %s?"
+  REMOVED_SUCCESSFULLY: "S'ha eliminat %s correctament"
+  ERROR_REMOVING_THE: "S'ha produït un error eliminant el %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "El %s requeria les següents dependències, les quals no són requerides per cap altre paquet instal·lat. Si no les estàs utilitzant, les pots eliminar directament des d'aquí."
+  READY_TO_UPDATE_PACKAGES: "Preparat per actualizar el(s) paquet(s)"
+  ERROR_UPDATING_PACKAGES: "S'ha produït un error al actualitzar el(s) paquet(s)"
+  UPDATING_PACKAGES: "Actualitzant paquet(s)..."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Paquet(s) actualitzat(s) correctament."
+  UPDATING: "Actualitzant"
+  GPM_RELEASES: "Versions GPM"
+  GPM_RELEASES_HELP: "Escull 'Provant' per instal·lar versions beta o en proves"
+  GPM_METHOD: "Mètode de recuperació remota"
+  GPM_METHOD_HELP: "Quan es posa a Automàtic, Grav determinarà si fopen està disponible i l'utilitzarà, d'altra forma s'utilitzarà cURL. Per a forçar l'ús d'un o altre, canvia la configuració."
+  GPM_VERIFY_PEER: "Verificació a distància de peer (SSL)"
+  GPM_VERIFY_PEER_HELP: "Alguns proveïdors semblen fallar en la verificació del certificat SSL de getgrav.org, el qual causa que GPM no funcioni. Si aquest és el teu cas, desactivar aquesta configuració pot ajudar"
+  AUTO: "Automàtic"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Estable"
+  TESTING: "Provant"
+  FRONTMATTER_PROCESS_TWIG: "Processa frontmatter de Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "Quan està habilitat, pots utilitzar les variables de configuració de Twig a la frontmatter de pàgina"
+  FRONTMATTER_IGNORE_FIELDS: "Ignora camps frontmatter"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Certs camps de frontmatter poden contenir Twig però no han de ser processats, com ara els formularis"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Paquet %s instal·lat correctament"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "L'ordre està definit pel pare, l'ordenament està deshabilitat"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "La pàgina no és visible, l'ordenament està deshabilitat"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "L'ordre a través de l'administrador no està suportat degut a que hi ha més de 200 elements"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "NOTA: No es poden afegir fitxers multimèdia fins que no s'hagi desat la pàgina. Fes clic a 'Desa' a la part superior"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "NOTA: Cal desar la pàgina haver de pujar-hi fitxers."
+  DROP_FILES_HERE_TO_UPLOAD: "Deixa anar els teus arxius aquí o <strong>fes clic en aquesta àrea</strong>"
+  INSERT: "Inserta"
+  UNDO: "Desfés"
+  REDO: "Refés"
+  HEADERS: "Capçaleres"
+  BOLD: "Negreta"
+  ITALIC: "Cursiva"
+  STRIKETHROUGH: "Tatxat"
+  SUMMARY_DELIMITER: "Delimitador del resum"
+  LINK: "Enllaç"
+  IMAGE: "Imatge"
+  BLOCKQUOTE: "Citació"
+  UNORDERED_LIST: "Llista sense ordre"
+  ORDERED_LIST: "Llista ordenada"
+  EDITOR: "Editor"
+  PREVIEW: "Vista prèvia"
+  FULLSCREEN: "Pantalla completa"
+  NON_ROUTABLE: "No accessible"
+  NON_VISIBLE: "No visible"
+  NON_PUBLISHED: "No publicat"
+  CHARACTERS: "caràcters"
+  PUBLISHING: "Publicació"
+  MEDIA_TYPES: "Tipus de multimèdia"
+  IMAGE_OPTIONS: "Opcions d'imatge"
+  MIME_TYPE: "Tipus Mime"
+  THUMB: "Miniatura"
+  TYPE: "Tipus"
+  FILE_EXTENSION: "Extensió de fitxer"
+  LEGEND: "Llegenda de pàgina"
+  MEMCACHE_SERVER: "Servidor Memcache"
+  MEMCACHE_SERVER_HELP: "L'adreça del servidor Memcache"
+  MEMCACHE_PORT: "Port Memcache"
+  MEMCACHE_PORT_HELP: "El port del servidor Memcache"
+  MEMCACHED_SERVER: "Servidor Memcached"
+  MEMCACHED_SERVER_HELP: "L'adreça del servidor Memcached"
+  MEMCACHED_PORT: "Port Memcached"
+  MEMCACHED_PORT_HELP: "El port del servidor Memcached"
+  REDIS_SERVER: "Servidor de Redis"
+  REDIS_SERVER_HELP: "L'adreça del servidor de Redis"
+  REDIS_PORT: "Port Redis"
+  REDIS_PORT_HELP: "El port del servidor Redis"
+  ALL: "Tot"
+  FROM: "de"
+  TO: "a"
+  RELEASE_DATE: "Data de llançament"
+  SORT_BY: "Ordena per"
+  RESOURCE_FILTER: "Filtrar..."
+  FORCE_SSL: "Força SSL"
+  FORCE_SSL_HELP: "Força SSL globalment, si està habilitat, quan el lloc web és accedir per HTTP, Grav envia un redireccionament a la pàgina HTTPS"
+  NEWS_FEED: "Canal de notícies"
+  EXTERNAL_URL: "URL externa"
+  CUSTOM_BASE_URL: "URL base personalitzada"
+  CUSTOM_BASE_URL_HELP: "Utilitza si vols reescriure el domini del lloc web o utilitzar una subcarpeta diferent que la que utilitza Grav. Per exemple: http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'No es pot utilitzar "%s" fora de les pàgines.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'No es pot pujar el fitxer %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'No es pot moure el fitxer %s to "%s"'
+  DROPZONE_CANCEL_UPLOAD: 'Cancel·la càrrega'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Estàs segur que vols cancel·lar aquesta càrrega?'
+  DROPZONE_DEFAULT_MESSAGE: 'Deixa anar els teus fitxers aquí o <strong>fes clic en aquesta àrea</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'El teu navegador no suporta arrosegar i deixar anar per a carregar fitxers.'
+  DROPZONE_FALLBACK_TEXT: 'Si us plau, utilitza el formulari clàssic inferior per carregar els teus fitxers com es feia abans.'
+  DROPZONE_FILE_TOO_BIG: 'El fitxer és molt gran ({{filesize}}MiB). Mida màxima de fitxer: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "No pots carregar fitxers d'aquest tipus."
+  DROPZONE_MAX_FILES_EXCEEDED: "No pots carregar més fitxers."
+  DROPZONE_REMOVE_FILE: "Esborra fitxer"
+  DROPZONE_RESPONSE_ERROR: "El servidor ha respost amb el codi {{statusCode}}."
+  PREMIUM_PRODUCT: "Premium"
+  DESTINATION_NOT_SPECIFIED: "Destinació no especificat"
+  UPLOAD_ERR_NO_TMP_DIR: "No es troba una carpeta temporal"
+  SESSION_SPLIT: "Separació de sessió"
+  SESSION_SPLIT_HELP: "Separació de sessions independent entre el lloc web i altres plugins (com ara l'administració)"
+  ERROR_FULL_BACKTRACE: "Error Complert Backtrace"
+  ERROR_SIMPLE: "Error simple"
+  ERROR_SYSTEM: "Error del sistema"
+  IMAGES_AUTO_FIX_ORIENTATION: "Fixar l'orientació automàtica"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Corregir automàticament l'orientació de la imatge basada en les dades Exif"
+  REDIS_SOCKET: "Socket Redis"
+  REDIS_SOCKET_HELP: "El Socket Redis"
+  NOT_SET: "No establert"
+  PERMISSIONS: "Permisos"
+  ALLOW_WEBSERVER_GZIP: "Permetre WebServer Gzip"
+  OFFLINE_WARNING: "La connexió a la GPM no es pot establir"
+  CONFIRM_REINSTALL: "Segur que vols re-instal·lar %s?"
+  REINSTALLED_SUCCESSFULLY: "%s re-instal·lat correctament"
+  TOOLS: "Eines"
+  GPM_OFFICIAL_ONLY: "Només GPM oficial"
+  SORTABLE_PAGES: "Pàgines ordenables:"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Ordre de visualització de descendents"
+  PWD_PLACEHOLDER: "cadena complexa de al menys 8 caràcters"
+  PWD_REGEX: "Regex clau"
+  USERNAME_REGEX: "RegEx nom d'usuari"
+  CONFIGURATION: "Configuració"
+  ADMIN_CACHING: "Habilita el cache de l'administració"
+  ADMIN_CACHING_HELP: "El cache a l'administració pot ser controlat independentment del de la pàgina front-end"
+  TIMEOUT: "Temps d'espera"
+  TIMEOUT_HELP: "Estableix el temps d'espera de la sessió en segons"
+  DASHBOARD: "Panell de control"
+  NOTIFICATIONS: "Notificacions"

+ 1112 - 0
plugins/admin/languages/cs.yaml

@@ -0,0 +1,1112 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_NOSCRIPT_MSG: "Prosím povolte JavaScript ve vašem prohlížeči."
+  ADMIN_BETA_MSG: "Jedná se o beta verzi! V ostrém provozu používejte pouze na vlastní nebezpečí..."
+  ADMIN_REPORT_ISSUE: "Objevili jste problém? Nahlaste ho, prosím, na GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Beží na Grav CMS</a> - Moderní správce obsahu pomocí souborů prostých textů"
+  LOGIN_BTN: "Přihlásit"
+  LOGIN_BTN_FORGOT: "Obnovit heslo"
+  LOGIN_BTN_RESET: "Obnovit heslo"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Odeslány pokyny pro obnovu hesla"
+  LOGIN_BTN_CLEAR: "Vymazat formulář"
+  LOGIN_BTN_CREATE_USER: "Vytvořit uživatele"
+  LOGIN_LOGGED_IN: "Byli jste úspěšně přihlášeni"
+  LOGIN_FAILED: "Přihlášení se nezdařilo"
+  LOGGED_OUT: "Byli jste odhlášeni"
+  RESET_NEW_PASSWORD: "Prosím, zadejte nové heslo &hellip;"
+  RESET_LINK_EXPIRED: "Odkaz pro obnovení hesla vypršel, zkuste to, prosím, znovu"
+  RESET_PASSWORD_RESET: "Heslo bylo změněno"
+  RESET_INVALID_LINK: "Použit neplatný odkaz pro obnovu hesla, zkuste to, prosím, znovu"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Pokyny pro obnovení hesla byly odeslány na vaši e-mailovou adresu"
+  FORGOT_FAILED_TO_EMAIL: "Nepodařilo se odeslat instrukce pro obnovu hesla, zkuste to, prosím, později"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Heslo pro %s nelze obnovit, není zadána e-mailová adresa"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Uživatel <b>%s</b> neexistuje"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Heslo nelze obnovit. Tento web nemá nastaveno odesílání e-mailů"
+  FORGOT_EMAIL_SUBJECT: "%s Požadavek na obnovení hesla"
+  FORGOT_EMAIL_BODY: "<h1>Obnovení hesla</h1><p>Vážený %1$s,</p><p>požadavek na obnovu hesla byl zadán na <b>%4$s</b>.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klikněte zde pro nastavení nového hesla</a><br /><br /></p><p>Případně, zkopírujte následující adresu URL do adresního řádku vašeho prohlížeče:</p> <p>%2$s</p><p><br />Děkujeme,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Spravovat stránky"
+  PAGES: "Stránky"
+  PLUGINS: "Doplňky"
+  PLUGIN: "Doplněk"
+  THEMES: "Šablony"
+  LOGOUT: "Odhlásit"
+  BACK: "Zpět"
+  NEXT: "Následující"
+  PREVIOUS: "Předchozí"
+  ADD_PAGE: "Přidat stránku"
+  MOVE: "Přesunout"
+  DELETE: "Smazat"
+  UNSET: "Zrušit nastavení"
+  VIEW: "Zobrazit"
+  SAVE: "Uložit"
+  NORMAL: "Normální"
+  EXPERT: "Expertní"
+  EXPAND_ALL: "Rozbalit vše"
+  COLLAPSE_ALL: "Sbalit vše"
+  ERROR: "Chyba"
+  CLOSE: "Zavřít"
+  CANCEL: "Zrušit"
+  CONTINUE: "Pokračovat"
+  CONFIRM: "Potvrdit"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Vyžadováno potvrzení"
+  MODAL_CHANGED_DETECTED_TITLE: "Zjištěny změny"
+  MODAL_CHANGED_DETECTED_DESC: "Máte neuložené změny. Jste si jisti, že chcete odejít bez uložení?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Vyžadováno potvrzení"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Opravdu chcete tento soubor smazat? Tato akce je nevratná."
+  MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "Chystáte se aktualizovat Grav na nejnovější dostupnou verzi. Chcete pokračovat?"
+  ADD_FILTERS: "Přidat filtry"
+  SEARCH_PAGES: "Hledat stránky"
+  VERSION: "Verze"
+  WAS_MADE_WITH: "Byl vytvořen s"
+  BY: "Od"
+  UPDATE_THEME: "Aktualizovat šablonu"
+  UPDATE_PLUGIN: "Aktualizovat doplněk"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "této šablony je nyní dostupná"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "tohoto doplňku je nyní dostupná"
+  AUTHOR: "Autor"
+  HOMEPAGE: "Domovská stránka"
+  DEMO: "Demo"
+  BUG_TRACKER: "Správa chyb"
+  KEYWORDS: "Klíčová slova"
+  LICENSE: "Licence"
+  DESCRIPTION: "Popis"
+  README: "Soubor README"
+  DOCS: "Dokumenty"
+  REMOVE_THEME: "Odstranit šablonu"
+  INSTALL_THEME: "Nainstalovat šablonu"
+  THEME: "Šablona"
+  BACK_TO_THEMES: "Zpět na šablony"
+  BACK_TO_PLUGINS: "Zpět na doplňky"
+  CHECK_FOR_UPDATES: "Zkontrolovat aktualizace"
+  ADD: "Přidat"
+  CLEAR_CACHE: "Vyprázdnit mezipaměť"
+  CLEAR_CACHE_ALL_CACHE: "Všechny mezipaměti"
+  CLEAR_CACHE_ASSETS_ONLY: "Pouze zdroje"
+  CLEAR_CACHE_IMAGES_ONLY: "Pouze obrázky"
+  CLEAR_CACHE_CACHE_ONLY: "Pouze mezipaměť"
+  CLEAR_CACHE_TMP_ONLY: "Pouze dočasné soubory"
+  UPDATES_AVAILABLE: "Dostupné aktualizace"
+  DAYS: "Dny"
+  UPDATE: "Aktualizovat"
+  BACKUP: "Zálohování"
+  BACKUPS: "Zálohování"
+  BACKUP_NOW: "Zálohovat nyní"
+  BACKUPS_STATS: "Statistiky zálohování"
+  BACKUPS_HISTORY: "Historie zálohování"
+  BACKUPS_PURGE_CONFIG: "Konfigurace vyčištění záloh"
+  BACKUPS_PROFILES: "Profily zálohování"
+  BACKUPS_COUNT: "Počet záloh"
+  BACKUPS_PROFILES_COUNT: "Počet profilů"
+  BACKUPS_TOTAL_SIZE: "Využité místo"
+  BACKUPS_NEWEST: "Nejnovější záloha"
+  BACKUPS_OLDEST: "Nejstarší záloha"
+  BACKUPS_PURGE: "Vyčistit"
+  BACKUPS_NOT_GENERATED: "Zatím nebyly vytvořeny žádné zálohy..."
+  BACKUPS_PURGE_NUMBER: "Použito %s z %s záložního místa"
+  BACKUPS_PURGE_TIME: "%s dnů záloh zbývá"
+  BACKUPS_PURGE_SPACE: "Použito %s z %s"
+  BACKUP_DELETED: "Záloha úspěšně smazána"
+  BACKUP_NOT_FOUND: "Záloha nalezena"
+  BACKUP_DATE: "Datum zálohy"
+  STATISTICS: "Statistiky"
+  VIEWS_STATISTICS: "Statistiky stránky"
+  TODAY: "Dnes"
+  WEEK: "Týden"
+  MONTH: "Měsíc"
+  LATEST_PAGE_UPDATES: "Poslední aktualizace stránky"
+  MAINTENANCE: "Údržba"
+  UPDATED: "Aktualizováno"
+  MON: "Po"
+  TUE: "Út"
+  WED: "St"
+  THU: "Čt"
+  FRI: "Pá"
+  SAT: "So"
+  SUN: "Ne"
+  COPY: "Kopírovat"
+  EDIT: "Upravit"
+  CREATE: "Vytvořit"
+  GRAV_ADMIN: "Správa Grav"
+  GRAV_OFFICIAL_PLUGIN: "Oficiální doplněk Gravu"
+  GRAV_OFFICIAL_THEME: "Oficiální šablona Gravu"
+  PLUGIN_SYMBOLICALLY_LINKED: "Tento zásuvný modul je nainstalován pomocí symbolického linku. Změny v něm nebudou zjištěny."
+  THEME_SYMBOLICALLY_LINKED: "Tato šablona je nainstalována pomocí symbolického linku. Změny v ní nebudou zjištěny"
+  REMOVE_PLUGIN: "Odstranit doplněk"
+  INSTALL_PLUGIN: "Nainstalovat doplněk"
+  AVAILABLE: "K dispozici"
+  INSTALLED: "Instalováno"
+  INSTALL: "Nainstalovat"
+  ACTIVE_THEME: "Aktivní šablona"
+  SWITCHING_TO: "Přepnout na"
+  SWITCHING_TO_DESCRIPTION: "Přepnutím na jinou šablonu nelze garantovat, že všechny vzhledy stránek budou podporované, což může způsobit jejich nedostupnost."
+  SWITCHING_TO_CONFIRMATION: "Chcete pokračovat a přepnout na jinou šablonu"
+  CREATE_NEW_USER: "Vytvořit nového uživatele"
+  REMOVE_USER: "Odstranit uživatele"
+  ACCESS_DENIED: "Přístup byl odepřen"
+  ACCOUNT_NOT_ADMIN: "váš účet nemá administrátorská oprávnění"
+  PHP_INFO: "PHP Info"
+  INSTALLER: "Instalátor"
+  AVAILABLE_THEMES: "Dostupné šablony"
+  AVAILABLE_PLUGINS: "Dostupné doplňky"
+  INSTALLED_THEMES: "Nainstalované šablony"
+  INSTALLED_PLUGINS: "Nainstalované doplňky"
+  BROWSE_ERROR_LOGS: "Prohlédnout systémové záznamy"
+  SITE: "Web"
+  INFO: "Info"
+  SYSTEM: "Systém"
+  USER: "Uživatel"
+  ADD_ACCOUNT: "Přidat účet"
+  SWITCH_LANGUAGE: "Přepnout jazyk"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Zásuvný modul byl úspěšně povolen"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Zásuvný modul byl úspěšně zakázán"
+  SUCCESSFULLY_CHANGED_THEME: "Úspěšně změněna výchozí šablona"
+  INSTALLATION_FAILED: "Instalace se nezdařila"
+  INSTALLATION_SUCCESSFUL: "Instalace byla úspěšná"
+  UNINSTALL_FAILED: "Odinstalace se nezdařila"
+  UNINSTALL_SUCCESSFUL: "Odinstalace byla úspěšná"
+  SUCCESSFULLY_SAVED: "Úspěšně uloženo"
+  SUCCESSFULLY_COPIED: "Úspěšně zkopírováno"
+  REORDERING_WAS_SUCCESSFUL: "Změna pořadí byla úspěšná"
+  SUCCESSFULLY_DELETED: "Úspěšně smazáno"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Jazyk úspěšně změněn"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "Nemáte dostatečná oprávnění pro úlohu"
+  CACHE_CLEARED: "Mezipaměť smazána"
+  METHOD: "Metoda"
+  ERROR_CLEARING_CACHE: "Nepodařilo se smazat mezipaměť"
+  AN_ERROR_OCCURRED: "Došlo k chybě"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Vaše záloha je připravena ke stažení"
+  DOWNLOAD_BACKUP: "Stáhnout zálohu"
+  PAGES_FILTERED: "Stránky vyfiltrovány"
+  NO_PAGE_FOUND: "Stránka nenalezena"
+  INVALID_PARAMETERS: "Neplatné parametry"
+  NO_FILES_SENT: "Žádné soubory nebyly odeslány"
+  EXCEEDED_FILESIZE_LIMIT: "Překročen limit velikosti souboru nastaveného konfiguračním souborem PHP"
+  EXCEEDED_POSTMAX_LIMIT: "Byl překročen post_max_size konfigurace PHP"
+  UNKNOWN_ERRORS: "Neznámé chyby"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Překročen limit velikosti souboru nastaveného konfiguračním souborem Grav"
+  UNSUPPORTED_FILE_TYPE: "Nepodporovaný typ souboru"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Přesunutí nahraného souboru se nezdařilo."
+  FILE_UPLOADED_SUCCESSFULLY: "Soubor úspěšně nahrán"
+  FILE_DELETED: "Soubor smazán"
+  FILE_COULD_NOT_BE_DELETED: "Soubor nebylo možné odstranit"
+  FILE_NOT_FOUND: "Soubor nenalezen"
+  NO_FILE_FOUND: "Nebyl nalezen žádný soubor"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav byl úspěšně aktualizován na verzi"
+  GRAV_UPDATE_FAILED: "Aktualizace Grav se nezdařila"
+  EVERYTHING_UPDATED: "Vše bylo aktulizováno"
+  UPDATES_FAILED: "Aktualizace se nezdařily"
+  AVATAR_BY: "Ikona uživatele od"
+  AVATAR_UPLOAD_OWN: "Nebo si nahrajte vlastní..."
+  LAST_BACKUP: "Poslední záloha"
+  FULL_NAME: "Jméno a příjmení"
+  USERNAME: "Uživatelské jméno"
+  EMAIL: "E-mailová adresa"
+  USERNAME_EMAIL: "Uživatelské jméno nebo e-mailová adresa"
+  PASSWORD: "Heslo"
+  PASSWORD_CONFIRM: "Potvrzení hesla"
+  TITLE: "Název"
+  LANGUAGE: "Jazyk"
+  ACCOUNT: "Účet"
+  EMAIL_VALIDATION_MESSAGE: "Musíte zadat platnou e-mailovou adresu"
+  PASSWORD_VALIDATION_MESSAGE: "Heslo musí obsahovat alespoň jedno číslo, jedno velké a malé písmeno a musí být alespoň 8 znaků dlouhé"
+  LANGUAGE_HELP: "Nastavte Váš preferovaný jazyk"
+  MEDIA: "Média"
+  DEFAULTS: "Výchozí hodnoty"
+  SITE_TITLE: "Název webu"
+  SITE_TITLE_PLACEHOLDER: "Titulek napříč celým webem"
+  SITE_TITLE_HELP: "Výchozí titulek webu, většinou použit v šablonách"
+  SITE_DEFAULT_LANG: "Výchozí jazyk"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Výchozí jazyk použitý ve značce <HTML> šablony"
+  SITE_DEFAULT_LANG_HELP: "Výchozí jazyk použitý ve značce <HTML> šablony"
+  DEFAULT_AUTHOR: "Výchozí autor"
+  DEFAULT_AUTHOR_HELP: "Výchozí jméno autora, často použito v šablonách či obsazích stránek"
+  DEFAULT_EMAIL: "Výchozí e-mailová adresa"
+  DEFAULT_EMAIL_HELP: "Výchozí e-mailová adresa, na kterou je odkazováno v šablonách či stránkách"
+  TAXONOMY_TYPES: "Typy taxonomií"
+  TAXONOMY_TYPES_HELP: "Typy taxonomií musí být vytvořeny zde, aby bylo možné je použít v nastavení stránek"
+  PAGE_SUMMARY: "Souhrn stránky"
+  ENABLED: "Zapnuto"
+  ENABLED_HELP: "Zapnout souhrn stránky (souhrn je stejný jako obsah stránky)"
+  'YES': "Ano"
+  'NO': "Ne"
+  SUMMARY_SIZE: "Souhrnná velikost"
+  SUMMARY_SIZE_HELP: "Počet znaků stránky, který je použit jako souhrn"
+  FORMAT: "Formát"
+  FORMAT_HELP: "krátký = až po první výskyt oddělovače nebo do délky souhrnu; dlouhý = ignorovat oddělovač"
+  SHORT: "Krátký"
+  LONG: "Dlouhý"
+  DELIMITER: "Oddělovač"
+  DELIMITER_HELP: "Oddělovač souhrnu (výchozí '===')"
+  METADATA: "Metadata"
+  METADATA_HELP: "Výchozí metadata, která budou zobrazena na každé stránce pokud nejsou přepsána v nastavení stránky"
+  NAME: "Název"
+  CONTENT: "Obsah"
+  SIZE: "Velikost"
+  ACTION: "Akce"
+  REDIRECTS_AND_ROUTES: "Přesměrování a cesty"
+  CUSTOM_REDIRECTS: "Vlastní přesměrování"
+  CUSTOM_REDIRECTS_HELP: "Cesty jsou přesměrovány na jiné stránky. Použití regulárních výrazů je možné"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/váš/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/vaše/přesměrování"
+  CUSTOM_ROUTES: "Vlastní cesty"
+  CUSTOM_ROUTES_HELP: "Cesty jsou aliasy pro jiné stránky. Použití regulárních výrazů je možné"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/váš/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/vaše/cesta"
+  FILE_STREAMS: "Soubory"
+  DEFAULT: "Výchozí"
+  PAGE_MEDIA: "Média stránky"
+  OPTIONS: "Možnosti"
+  PUBLISHED: "Zveřejněná"
+  PUBLISHED_HELP: "Ve výchozím nastavení je stránka zveřejněná, pokud výslovně nenastavíte published: false nebo publish_date jako budoucí nebo unpublish_date jako minulé"
+  DATE: "Datum"
+  DATE_HELP: "Můžete nastavit konkrétní datum, které bude přiřazené k této stránce."
+  PUBLISHED_DATE: "Datum zveřejnění"
+  PUBLISHED_DATE_HELP: "Můžete nastavit datum, kdy bude článek automaticky zveřejněn."
+  UNPUBLISHED_DATE: "Datum skrytí"
+  UNPUBLISHED_DATE_HELP: "Můžete nastavit datum, kdy bude článek automaticky skryt."
+  ROBOTS: "Roboti"
+  TAXONOMIES: "Taxonomie"
+  TAXONOMY: "Taxonomie"
+  ADVANCED: "Pokročilé"
+  SETTINGS: "Nastavení"
+  FOLDER_NUMERIC_PREFIX: "Číselná předpona složky"
+  FOLDER_NUMERIC_PREFIX_HELP: "Číselná předpona, která umožňuje manuální řazení a zajišťuje viditelnost"
+  FOLDER_NAME: "Název složky"
+  FOLDER_NAME_HELP: "Název složky, která bude vytvořena v souborovém systému pro tuto stránku"
+  PARENT: "Nadřazená stránka"
+  DEFAULT_OPTION_ROOT: "- Root -"
+  DEFAULT_OPTION_SELECT: "- Vyberte -"
+  DISPLAY_TEMPLATE: "Šablona pro zobrazení"
+  DISPLAY_TEMPLATE_HELP: "Název šablony, která bude použita pro zobrazení této stránky"
+  ORDERING: "Řazení"
+  PAGE_ORDER: "Pořadí stránek"
+  OVERRIDES: "Přepsat výchozí nastavení"
+  MENU: "Menu"
+  MENU_HELP: "Řetězec, který má být použit v menu. Pokud není nastaven, bude použit název."
+  SLUG: "Slug"
+  SLUG_HELP: "Slug umožněje nastavit část URL pro tuto konkrétní stránku"
+  SLUG_VALIDATE_MESSAGE: "Slug může obsahovat pouze znaky malé abecedy, čísla a pomlčky."
+  PROCESS: "Zpracování stránky"
+  PROCESS_HELP: "Nastavte jaký renderovací engine se má použít pro zpracování stránky"
+  DEFAULT_CHILD_TYPE: "Výchozí typ nové stránky"
+  USE_GLOBAL: "Použít globálně"
+  ROUTABLE: "Přístupná"
+  ROUTABLE_HELP: "Pokud je tato stránka dosažitelná prostřednictvím adresy URL"
+  CACHING: "Použít cache"
+  VISIBLE: "Viditelná"
+  VISIBLE_HELP: "Určuje, zda je stránka viditelná v navigaci."
+  DISABLED: "Vypnuto"
+  ITEMS: "Položky"
+  ORDER_BY: "Řadit podle"
+  ORDER: "Pořadí"
+  FOLDER: "Složka"
+  ASCENDING: "Vzestupně"
+  DESCENDING: "Sestupně"
+  PAGE_TITLE: "Název stránky"
+  PAGE_TITLE_HELP: "Titulek stránky"
+  PAGE: "Stránka"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Název souboru"
+  PARENT_PAGE: "Nadřazená stránka"
+  HOME_PAGE: "Domovská stránka"
+  HOME_PAGE_HELP: "Stránka, kterou Grav použije jako výchozí při příchodu na web"
+  DEFAULT_THEME: "Výchozí šablona"
+  DEFAULT_THEME_HELP: "Nastaví výchozí šablonu kterou bude Grav používat (výchozí je Antimatter)"
+  TIMEZONE: "Časové pásmo"
+  TIMEZONE_HELP: "Přepíše výchozí časovou zónu serveru"
+  SHORT_DATE_FORMAT: "Krátký formát data"
+  SHORT_DATE_FORMAT_HELP: "Nastaví krátký formát data, který lze použít v šablonách"
+  LONG_DATE_FORMAT: "Dlouhý formát data"
+  LONG_DATE_FORMAT_HELP: "Nastaví dlouhý formát data, který lze použít v šablonách"
+  DEFAULT_ORDERING: "Výchozí řazení"
+  DEFAULT_ORDERING_HELP: "Stránky budou v seznamu zobrazeny v tomto pořadí, pokud nemá konkrétní stránka jiné nastavení"
+  DEFAULT_ORDERING_DEFAULT: "Výchozí - podle názvu složky"
+  DEFAULT_ORDERING_FOLDER: "Složka - podle názvu složky bez prefixu"
+  DEFAULT_ORDERING_TITLE: "Název - podle názvu nastaveného v hlavičce stránky"
+  DEFAULT_ORDERING_DATE: "Datum - podle data nastaveného v hlavičce stránky"
+  DEFAULT_ORDER_DIRECTION: "Výchozí směr řazení"
+  DEFAULT_ORDER_DIRECTION_HELP: "Směr řazení ve výpisu stránek"
+  DEFAULT_PAGE_COUNT: "Výchozí počet stránek"
+  DEFAULT_PAGE_COUNT_HELP: "Výchozí počet zobrazených stránek v seznamu"
+  DATE_BASED_PUBLISHING: "Zveřejnění podle data"
+  DATE_BASED_PUBLISHING_HELP: "Automaticky zveřejnit/skrýt příspěvky podle jejich data"
+  EVENTS: "Události"
+  EVENTS_HELP: "Povolit nebo zakázat konkrétní události. Zakázání některých událostí může mít za následek nefunkčnost některých doplňků"
+  REDIRECT_DEFAULT_ROUTE: "Přesměrovat výchozí cestu"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Automatické přesměrování na výchozí cestu stránky"
+  LANGUAGES: "Jazyky"
+  SUPPORTED: "Podporováno"
+  SUPPORTED_HELP: "Dvouznakové jazykové kódy oddělené čárkou (například 'en,fr,de')"
+  SUPPORTED_PLACEHOLDER: "např. en, fr"
+  TRANSLATIONS_FALLBACK: "Hledat i v ostatních jazycích"
+  TRANSLATIONS_FALLBACK_HELP: "Pokud překlad neexistuje v aktivním jazyce, prohledat i ostatní jazyky"
+  ACTIVE_LANGUAGE_IN_SESSION: "Aktivní jazyk v session"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Uložit aktivní jazyk do session"
+  HTTP_HEADERS: "HTTP Hlavičky"
+  EXPIRES: "Expires"
+  EXPIRES_HELP: "Nastaví 'expires' záznam v hlavičce. Hodnota je v sekundách."
+  CACHE_CONTROL: "HTTP kontrola mezipaměti"
+  CACHE_CONTROL_HELP: "Nastavte platnou hodnotu kontroly mezipaměti, jako např. `no-cache, no-store, must-revalidate`"
+  CACHE_CONTROL_PLACEHOLDER: "např. public, max-age=31536000"
+  LAST_MODIFIED: "Naposledy změněno"
+  LAST_MODIFIED_HELP: "Nastaví datum 'Last Modified' v HTTP hlavičce, které může pomoci při optimalizaci cachování na straně proxy serveru a prohlížeče"
+  ETAG: "ETag"
+  ETAG_HELP: "Nastaví 'ETag' záznam v HTTP hlavičce, který pomáhá rozeznat kdy byla stránka naposledy upravena"
+  VARY_ACCEPT_ENCODING: "Vary: Accept Encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Nastaví 'Vary: Accept Encoding' záznam v HTTP hlavičce, který pomáhá s cachováním na straně proxy a CDN"
+  MARKDOWN: "Markdown"
+  MARKDOWN_EXTRA: "Markdown extra"
+  MARKDOWN_EXTRA_HELP: "Povolit ve výchozím nastavení syntaxi Markdown Extra - https://michelf.ca/projects/php-markdown/extra/"
+  MARKDOWN_EXTRA_ESCAPE_FENCES: "Escapovat prvky HTML v markdown zvláštním značením"
+  MARKDOWN_EXTRA_ESCAPE_FENCES_HELP: "Escapování prvků HTML v markdown zvláštním značením"
+  AUTO_LINE_BREAKS: "Automatické zalamování řádků"
+  AUTO_LINE_BREAKS_HELP: "Povolit ve výchozím nastavení automatické zalamování řádků v Markdownu"
+  AUTO_URL_LINKS: "Převádět URL na odkazy"
+  AUTO_URL_LINKS_HELP: "Povolit automatické převádění URL na odkazy pomocí <a> elementu"
+  ESCAPE_MARKUP: "Escapovat HTML specifické znaky"
+  ESCAPE_MARKUP_HELP: "Automaticky escapovat HTML specifické znaky na jejich bezpečné ekvivalenty"
+  CACHING_HELP: "Globální vypnutí/zapnutí cachování v Gravu"
+  CACHE_CHECK_METHOD: "Metoda kontroly cache stránek"
+  CACHE_CHECK_METHOD_HELP: "Vyberte metodu, pomocí které Grav může zkontrolovat jestli byly soubory stránky modifikovány"
+  CACHE_DRIVER: "Cache"
+  CACHE_DRIVER_HELP: "Vyberte jaký způsob cachování ma Grav používat. 'Auto Detect' se pokusí najít ten nejlepší pro Vaši konfiguraci."
+  CACHE_PREFIX: "Prefix cache"
+  CACHE_PREFIX_HELP: "Identifikátor pro klíče použité Gravem. Neupravujte pokud si nejste jistí co děláte."
+  CACHE_PREFIX_PLACEHOLDER: "Odvozený od URL webu (přepište náhodným řetězcem)"
+  CACHE_PURGE_JOB: "Spustit plánovanou čistící úlohu"
+  CACHE_PURGE_JOB_HELP: "Pomocí plánovače můžete pravidelně vymazat starou mezipaměť prostřednictvím této úlohy"
+  CACHE_CLEAR_JOB: "Spustit plánovanou čistící úlohu"
+  CACHE_CLEAR_JOB_HELP: "Prostřednictvím služby Plánovač můžete pravidelně mazat mezipaměť Gravu"
+  CACHE_JOB_TYPE: "Typ úlohy mezipaměti"
+  CACHE_JOB_TYPE_HELP: "Buď vymazat \"standardní\" složky mezipaměti, nebo \"všechny\" složky"
+  CACHE_PURGE: "Vyčistit starou mezipaměť"
+  LIFETIME: "Platnost"
+  LIFETIME_HELP: "Délka platnosti záznamů v cachi. 0 = nekonečně."
+  GZIP_COMPRESSION: "Komprese Gzip"
+  GZIP_COMPRESSION_HELP: "Zapnout GZip kompresi stránek pro rychlejší načítání."
+  TWIG_TEMPLATING: "Twig šablony"
+  TWIG_CACHING: "Cachovat Twig šablony"
+  TWIG_CACHING_HELP: "Nastavuje zda se využite cachování Twig šablon. Nechejte zapnuté pro nějlepší výkon."
+  TWIG_DEBUG: "Twig debug mód"
+  TWIG_DEBUG_HELP: "Umožňuje vypnout načítání rozšíření Twig Debugger"
+  DETECT_CHANGES: "Detekovat změny"
+  DETECT_CHANGES_HELP: "Twig bude automaticky aktualizovat cache pokud detekuje změnu v Twig šabloně"
+  AUTOESCAPE_VARIABLES: "Automaticky escapovat proměnné"
+  AUTOESCAPE_VARIABLES_HELP: "Escapování všech proměnných bude mít pravděpodobně za následek nefunkčnost webu"
+  ASSETS: "Zdroje"
+  CSS_PIPELINE: "Sloučit CSS soubory"
+  CSS_PIPELINE_HELP: "Spojí všechny CSS soubory do jednoho"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Zahrnout externí CSS do sloučeného CSS"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "Externí URL adresy někdy obsahují relativní odkazy na soubory a tudíž by neměli být sloučeny"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "Jako první vykreslit sloučené CSS"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Vykreslit sloučené CSS před jakékoliv jiné nezahrnuté CSS reference"
+  CSS_MINIFY: "Minifikovat CSS soubory"
+  CSS_MINIFY_HELP: "Minifikuje všechny CSS soubory"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "Minifikace CSS na Windows"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Přepsat nastavení minifikace CSS pro Windows. Ve výchozím nastavení je vypnuto kvůli ThreadStackSize"
+  CSS_REWRITE: "Přepsat URL uvnitř CSS"
+  CSS_REWRITE_HELP: "Přepíše relativní URL uvnitř CSS souborů"
+  JAVASCRIPT_PIPELINE: "Spojit soubory s JavaScripty"
+  JAVASCRIPT_PIPELINE_HELP: "Spojí všechny JavaScriptové soubory do jednoho"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Zahrnout externí JS v sloučeném JS"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Externí URL adresy někdy obsahují relativní odkazy na soubory a tudíž by neměli být sloučeny"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Jako první vykreslit sloučené JS"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Vykreslit sloučené JS před jakékoliv jiné nezahrnuté JS reference"
+  JAVASCRIPT_MINIFY: "Minifikovat JavaScripty"
+  JAVASCRIPT_MINIFY_HELP: "Minifikuje všechny JavaScriptové soubory"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Povolit časová razítka na zdrojích"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Povolit zdroji časová razítka"
+  ENABLED_SRI_ON_ASSETS: "Povolit SRI na zdrojích"
+  ENABLED_SRI_ON_ASSETS_HELP: "Povolit zdroji SRI"
+  COLLECTIONS: "Kolekce"
+  ERROR_HANDLER: "Hlášení chyb"
+  DISPLAY_ERRORS: "Zobrazit chyby"
+  DISPLAY_ERRORS_HELP: "Zobrazit celou stránku s výpisem z backtrace"
+  LOG_ERRORS: "Logovat chyby"
+  LOG_ERRORS_HELP: "Logovat chyby do /logs adresáře"
+  LOG_HANDLER: "Správce logů"
+  LOG_HANDLER_HELP: "Kam ukládat výstup z logu"
+  SYSLOG_FACILITY: "Syslog"
+  SYSLOG_FACILITY_HELP: "Výstup ze Syslog"
+  DEBUGGER: "Debugger"
+  DEBUGGER_HELP: "Povolit Grav debugger a následující nastavení"
+  DEBUG_TWIG: "Debugovat Twig"
+  DEBUG_TWIG_HELP: "Povolit debugování Twig šablon"
+  SHUTDOWN_CLOSE_CONNECTION: "Uzavírat spojení"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Uzavřít spojení před zavoláním 'onShutdown()' události. Vypnout při povoleném debuggování"
+  DEFAULT_IMAGE_QUALITY: "Výchozí kvalita obrázků"
+  DEFAULT_IMAGE_QUALITY_HELP: "Výchozí kvalita obrázků při úpravách nebo cachování (85%)"
+  CACHE_ALL: "Cachovat všechny obrázky"
+  CACHE_ALL_HELP: "Zpracovat všechny obrázky pomocí cachovacího systému Gravu ikdyž nevyžadují žádné úpravy"
+  IMAGES_DEBUG: "Vodoznak pro lepší ladění"
+  IMAGES_DEBUG_HELP: "Zobrazit vrstvu přes obrázek znázorňující hustotu pixelů v obrázku například pro retina displaye"
+  IMAGES_LOADING: "Chování načítání obrázku"
+  IMAGES_LOADING_HELP: "Atribut načítání umožňuje prohlížeči oddálit načítání snímků mimo obrazovku a iframů, dokud se uživatelé neposunou poblíž. Načítání podporuje tři hodnoty: auto, lazy, eager"
+  IMAGES_SEOFRIENDLY: "SEO-přívětivý název obrázku"
+  IMAGES_SEOFRIENDLY_HELP: "Je-li tato volba zapnuta, zobrazí se nejprve název obrázku a potom menší hodnota hash, která odráží zpracované operace"
+  UPLOAD_LIMIT: "Limit pro uploadované soubory"
+  UPLOAD_LIMIT_HELP: "Nastaví maximální povolenou velikost pro uploadované soubory v bytech (0 = neomezeně)"
+  ENABLE_MEDIA_TIMESTAMP: "Zapnout časové značky na médiích"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Přidá časovou značku podle poslední modifikace do každé URL odkazující na média"
+  SESSION: "Relace"
+  SESSION_ENABLED_HELP: "Povolit podporu relací v rámci Grav"
+  SESSION_NAME_HELP: "Identifikátor pro vytvoření názvu pro session cookie"
+  SESSION_UNIQUENESS: "Jedinečný řetězec"
+  SESSION_UNIQUENESS_HELP: "MD5 hash kořenové cesty Gravu, např. `GRAV_ROOT` (výchozí) nebo založený na náhodném `osoleném (security.salt)` řetězci."
+  ABSOLUTE_URLS: "Absolutní URL"
+  ABSOLUTE_URLS_HELP: "Absolutní nebo relativní URL v proměnné 'base_url'"
+  PARAMETER_SEPARATOR: "Oddělovač parametrů"
+  PARAMETER_SEPARATOR_HELP: "Oddělovač parametrů, který lze změnit pro Apache na Windows"
+  TASK_COMPLETED: "Úkol dokončen"
+  EVERYTHING_UP_TO_DATE: "Vše je aktualizováno"
+  UPDATES_ARE_AVAILABLE: "jsou dostupné nové aktualizace"
+  IS_AVAILABLE_FOR_UPDATE: "je možné aktualizovat"
+  IS_NOW_AVAILABLE: "je nyní k dispozici"
+  CURRENT: "Aktuální"
+  UPDATE_GRAV_NOW: "Aktualizovat nyní Grav"
+  GRAV_SYMBOLICALLY_LINKED: "Grav je symbolicky propojen, aktualizace nebude k dispozici"
+  UPDATING_PLEASE_WAIT: "Probíhá aktualizace... prosím čekejte na stáhnutí"
+  OF_THIS: "této"
+  OF_YOUR: "Vaší"
+  HAVE_AN_UPDATE_AVAILABLE: "má k dispozici novou verzi"
+  SAVE_AS: "Uložit jako"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Opravdu chcete smazat tuto stránku a všechny její podstránky? Překlady této stránky nebudou smazány společně s touto stránkou a musí být odstraněny samostatně. Adresář se stránkou bude smazán společně s jejími podstránkami. Tato akce je nevratná."
+  AND: "a"
+  UPDATE_AVAILABLE: "Dostupná aktualizace"
+  METADATA_KEY: "Klíč (např. 'Keywords')"
+  METADATA_VALUE: "Hodnota (např. 'Blog, Grav')"
+  USERNAME_HELP: "Uživatelské jméno by mělo mít 3 až 16 znaků včetně malých písmen, čísel, podtržítka a pomlčky. Velká písmena, mezery a speciální znaky nejsou povoleny"
+  FULLY_UPDATED: "Úplně aktualizované"
+  SAVE_LOCATION: "Uložit umístění"
+  PAGE_FILE: "Šablona stránky"
+  PAGE_FILE_HELP: "Název souboru se stránkou. Ve výchozím nastavení je to také název šablony použitý pro tuto stránku."
+  NO_USER_ACCOUNTS: "Nebyly nalezen žádný uživatelský účet, prosím, nejdříve si jeden vytvořte"
+  NO_USER_EXISTS: "Pro tento účet neexistuje žádný místní uživatel, nelze uložit..."
+  REDIRECT_TRAILING_SLASH: "Přesměrovat URL končící na '/'"
+  REDIRECT_TRAILING_SLASH_HELP: "Provést přesměrování pomocí kódu 301 namísto ignorování pokud URL končí lomítkem."
+  DEFAULT_DATE_FORMAT: "Výchozí formát data"
+  DEFAULT_DATE_FORMAT_HELP: "Formát data stránky používaný Gravem. Ve výchozím nastavení se Grav pokusí uhodnout zadaný formát data, ale můžete též přesně určit formát data použitím PHP syntaxe (např.: Y-m-d H:i nebo d.m.Y H:i:s)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Automaticky odhadnout"
+  IGNORE_FILES: "Ignorovat soubory"
+  IGNORE_FILES_HELP: "Ignorovat tyto soubory při zpracování stránky"
+  IGNORE_FOLDERS: "Ignorovat složky"
+  IGNORE_FOLDERS_HELP: "Ignorovat tyto adresáře při zpracování stránky"
+  HIDE_EMPTY_FOLDERS: "Skrýt prázdné složky"
+  HIDE_EMPTY_FOLDERS_HELP: "Pokud složka neobsahuje žádný soubor .md, měla by být skrytá v navigaci, stejně jako nepřístupná"
+  HTTP_ACCEPT_LANGUAGE: "Nastavit jazyk dle prohlížeče"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Zkusí nastavit jazyk podle `http_accept_language` hlavičky v prohlížeči"
+  OVERRIDE_LOCALE: "Přepsat lokalitu"
+  OVERRIDE_LOCALE_HELP: "Přepíše lokalitu nastavenou v PHP podle aktuálního jazyku"
+  REDIRECT: "Přesměrování stránky"
+  REDIRECT_HELP: "Zadejte cestu stránky nebo externí URL, kam bude tato stránka přesměrována. Např. `/nějaká/cesta` nebo `http://nějakýweb.com`"
+  PLUGIN_STATUS: "Stav doplňku"
+  INCLUDE_DEFAULT_LANG: "Zahrnout výchozí jazyk"
+  INCLUDE_DEFAULT_LANG_HELP: "Předřadí všem adresám URL ve výchozím jazyce výchozí jazyk. Např.: `/en/blog/my-post`"
+  INCLUDE_DEFAULT_LANG_FILE_EXTENSION: "Zahrnout výchozí jazyk do přípony souboru"
+  INCLUDE_DEFAULT_LANG_HELP_FILE_EXTENSION: "Pokud je povoleno, přidá výchozí jazyk do přípony souboru (např. `.en.md`). Zakažte jej pro zachování výchozího přípony souboru `.md` bez vyjádření jazyka."
+  PAGES_FALLBACK_ONLY: "Stránky jsou pouze záložní"
+  PAGES_FALLBACK_ONLY_HELP: "Pouze \"záložní\" pro vyhledání obsahu stránky pomocí podporovaných jazyků, výchozím chováním je zobrazení jakéhokoli nalezeného jazyka, pokud chybí aktivní jazyk"
+  ALLOW_URL_TAXONOMY_FILTERS: "Filtry taxonomie v URL"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Umožňuje filtrovat stránky pomocí taxonomií jako `/taxonomie:hodnota`."
+  REDIRECT_DEFAULT_CODE: "Výchozí kód přesměrování"
+  REDIRECT_DEFAULT_CODE_HELP: "Nastaví HTTP status kód použitý pro všechna přesměrování"
+  IGNORE_HIDDEN: "Ignorovat skryté"
+  IGNORE_HIDDEN_HELP: "Ignorovat všechny soubory, které začínají tečkou"
+  WRAPPED_SITE: "Součást jiného webu"
+  WRAPPED_SITE_HELP: "Pro šablony/doplňky, aby věděly, zda je Grav zapouzdřen další platformou"
+  FALLBACK_TYPES: "Povolené typy souborů"
+  FALLBACK_TYPES_HELP: "Povolené typy souborů, které jsou přístupné prostřednictvím cesty stránky. Ve výchozím nastavení jsou podporovány všechny typy médií."
+  INLINE_TYPES: "Typy souborů vložené přímo do stránky"
+  INLINE_TYPES_HELP: "Typy souborů, které mají být zobrazeny přímo ve stránce namísto stahování"
+  APPEND_URL_EXT: "Přidat za URL příponu"
+  APPEND_URL_EXT_HELP: "Přidá vlastní připonu do URL stránky. To znamená, že Grav bude hledat šablonu s názvem `<template>.<extension>.twig`"
+  PAGE_MODES: "Mód stránky"
+  PAGE_TYPES: "Typ stránky"
+  PAGE_TYPES_HELP: "Určuje typy stránek, které Grav podporuje, a pořadí určuje, na který typ se vrátí, pokud je požadavek nejednoznačný"
+  ACCESS_LEVELS: "Úrovně přístupu"
+  GROUPS: "Skupiny"
+  GROUPS_HELP: "Seznam skupin uživatele"
+  ADMIN_ACCESS: "Přístup správce"
+  SITE_ACCESS: "Přístup na web"
+  INVALID_SECURITY_TOKEN: "Neplatný bezpečnostní token"
+  ACTIVATE: "Povolit"
+  TWIG_UMASK_FIX: "Umask Fix"
+  TWIG_UMASK_FIX_HELP: "Ve výchozím nastavení Twig vytváří cachovací soubory s oprávněním 0755, po zapnutí bude nastavovat 0775"
+  CACHE_PERMS: "Oprávnění cache"
+  CACHE_PERMS_HELP: "Výchozí nastavení oprávnění adresářů. Většinou 0755 nebo 0775 podle konfigurace serveru"
+  REMOVE_SUCCESSFUL: "Úspěšně odstraněno"
+  REMOVE_FAILED: "Odstranění se nezdařilo"
+  HIDE_HOME_IN_URLS: "Skrýt cestu domovské stránky v URL"
+  HIDE_HOME_IN_URLS_HELP: "Zajistí, že výchozí cesty pro všechny stránky umístěné pod domovskou stránkou neobsahují cestu domovské stránky"
+  TWIG_FIRST: "Zpracovat nejdříve Twigem"
+  TWIG_FIRST_HELP: "Pokud povolíte zpracovávání stránek Twigem, můžete upravit stránku ještě před zpracování markdownem"
+  SESSION_SECURE: "Bezpečnost"
+  SESSION_SECURE_HELP: "Pokud zapnete, musí být veškerá komunikace identifikovaná touto cookie prováděna přes HTTPS. POZOR: Zapněte pouze pro weby, které běží pouze na HTTPS"
+  SESSION_HTTPONLY: "Pouze HTTP"
+  SESSION_HTTPONLY_HELP: "Pokud zapnete, cookie mohou být použity pouze přes HTTP a nemohou být upraveny pomocí JavaScriptu"
+  REVERSE_PROXY: "Reverzní proxy"
+  REVERSE_PROXY_HELP: "Zapněte, pokud jste za reverzní proxy and máte problémy s URL obsahující nesprávné porty"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Nesprávný formát frontmatter, nelze uložit"
+  ADD_FOLDER: "Přidat složku"
+  COPY_PAGE: "Kopírovat stránku"
+  PROXY_URL: "Adresa proxy serveru"
+  PROXY_URL_HELP: "Zadejte server proxy hostitele nebo adresu IP a PORT"
+  NOTHING_TO_SAVE: "Nic k uložení"
+  FILE_ERROR_ADD: "Došlo k chybě při pokusu o přidání souboru"
+  FILE_ERROR_UPLOAD: "Nastala chyba při nahrávání souborů"
+  FILE_UNSUPPORTED: "Nepodporovaný typ souboru"
+  ADD_ITEM: "Přidat položku"
+  FILE_TOO_LARGE: "Soubor je příliš velký. Maximální povolená velikost je %s.<br> Zvětšete prosím `post_max_size` v nastavení PHP"
+  INSTALLING: "Probíhá instalace"
+  LOADING: "Probíhá načítání.."
+  DEPENDENCIES_NOT_MET_MESSAGE: "Nejprve je třeba nainstalovat následující komponenty:"
+  ERROR_INSTALLING_PACKAGES: "Nastala chyba při instalování bálíčků"
+  INSTALLING_DEPENDENCIES: "Instalování komponentů..."
+  INSTALLING_PACKAGES: "Probíhá instalace balíčků.."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Balíčky byly úspěšně nainstalovány."
+  READY_TO_INSTALL_PACKAGES: "Vše připraveno na instalaci balíčků"
+  PACKAGES_NOT_INSTALLED: "Balíčky nenainstalovány"
+  PACKAGES_NEED_UPDATE: "Již nainstalované balíčky, které jsou příliš staré"
+  PACKAGES_SUGGESTED_UPDATE: "Balíčky nainstalované ve verzi, která je v pořádku, budou aktualizovány na nejnovější verzi"
+  REMOVE_THE: "Odstraňte %s"
+  CONFIRM_REMOVAL: "Opravdu chcete smazat %s?"
+  REMOVED_SUCCESSFULLY: "%s úspěšně odstraněn"
+  ERROR_REMOVING_THE: "Nastala chyba při odstraňování %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "%s vyžadoval následující komponenty, které nejsou vyžadovány ostatními nainstalovanými balíčky. Pokud je nepoužíváte, můžete je přímo zde hned odstranit."
+  READY_TO_UPDATE_PACKAGES: "Vše připraveno na aktualizaci balíčků"
+  ERROR_UPDATING_PACKAGES: "Nastala chyba při aktualizaci bálíčků"
+  UPDATING_PACKAGES: "Probíhá aktualizování balíčků.."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Balíčky byly úspěšně aktualizovány."
+  UPDATING: "Probíhá aktualizace"
+  GPM_RELEASES: "Verze GPM"
+  GPM_RELEASES_HELP: "Zvolte 'Testovací' pro instalaci beta nebo testovací verze"
+  GPM_METHOD: "Metoda Remote Fetch"
+  GPM_METHOD_HELP: "Pokud nastaveno na Auto, Grav zjistí, jestli je fopen povoleno a použije jej, jinak se vrátí zpět k používání cURL. K vynucení použití jednoho či druhého přepněte nastavení."
+  GPM_VERIFY_PEER: "Vzdálené ověření Peer (SSL)"
+  GPM_VERIFY_PEER_HELP: "Někteří poskytovatelé nepřijímají getgrav.org SSL certifikát, což způsobuje nefunkčnost GPM. Pokud je toto váš případ, zkuste vypnout toto nastavení"
+  AUTO: "Auto"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stabilní"
+  TESTING: "Testovací"
+  FRONTMATTER_PROCESS_TWIG: "Zpracovat Twig ve frontmatteru"
+  FRONTMATTER_PROCESS_TWIG_HELP: "Pokud je aktivní, můžete použít nastavovací proměnné Twigu ve frontmatteru stránky"
+  FRONTMATTER_IGNORE_FIELDS: "Ignorovat pole ve frontmatteru"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Určitá pole ve frontmatteru můžou obsahovat Twig, ale neměla by být zpracována. Například formuláře"
+  FRONTMATTER_IGNORE_FIELDS_PLACEHOLDER: "např. formuláře"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Balíček %s byl úspěšně nainstalován"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Pořadí nadřazeného nastavení, řazení zakázáno"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Stránka není viditelná, řazení zakázáno"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Řazení přes admin není podporováno, protože existuje více než 200 sourozenců"
+  ORDERING_DISABLED_BECAUSE_PAGE_NO_PREFIX: "Řazení stránek je zakázáno pro tuto stánku, protože <strong>Číselná předpona složky</strong> není povolena"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "Poznámka: Nelze přidat mediální soubory, dokud neuložíte na stránku. Stačí kliknout na 'Uložit' na horní části stránky"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "POZNÁMKA: Musíte stránku uložit před nahráním souborů."
+  DROP_FILES_HERE_TO_UPLOAD: "Přetáhněte sem své soubory nebo <strong>klikněte na tuto plochu</strong>"
+  INSERT: "Vložit"
+  UNDO: "Odvolat"
+  REDO: "Opakovat"
+  HEADERS: "Nadpisy"
+  BOLD: "Tučně"
+  ITALIC: "Kurzíva"
+  STRIKETHROUGH: "Přeškrtnout"
+  SUMMARY_DELIMITER: "Oddělovač shnutí"
+  LINK: "Odkaz"
+  IMAGE: "Obrázek"
+  BLOCKQUOTE: "Citace"
+  UNORDERED_LIST: "Nečíslovaný seznam"
+  ORDERED_LIST: "Číslovaný seznam"
+  EDITOR: "Editor"
+  PREVIEW: "Náhled"
+  FULLSCREEN: "Na celou obrazovku"
+  NON_ROUTABLE: "Nepřístupná"
+  NON_VISIBLE: "Neviditelná"
+  NON_PUBLISHED: "Nezveřejněná"
+  CHARACTERS: "znaků"
+  PUBLISHING: "Zveřejnění"
+  MEDIA_TYPES: "Typy médií"
+  IMAGE_OPTIONS: "Možnosti obrázku"
+  MIME_TYPE: "MIME typ"
+  THUMB: "Náhled"
+  TYPE: "Typ"
+  FILE_EXTENSION: "Přípona souboru"
+  LEGEND: "Legenda stránky"
+  MEMCACHE_SERVER: "Server memcache"
+  MEMCACHE_SERVER_HELP: "Adresa Memcache serveru"
+  MEMCACHE_PORT: "Memcached port"
+  MEMCACHE_PORT_HELP: "Port serveru Memcached"
+  MEMCACHED_SERVER: "Memcached server"
+  MEMCACHED_SERVER_HELP: "Adresu serveru Memcached"
+  MEMCACHED_PORT: "Memcached port"
+  MEMCACHED_PORT_HELP: "Port serveru Memcached"
+  REDIS_SERVER: "Redis server"
+  REDIS_SERVER_HELP: "Adresa Redis serveru"
+  REDIS_PORT: "Redis port"
+  REDIS_PORT_HELP: "Port Redis serveru"
+  REDIS_PASSWORD: "Heslo a tajný kód pro Redis"
+  REDIS_DATABASE: "ID Redis databáze"
+  REDIS_DATABASE_HELP: "ID instance databáze Redis"
+  ALL: "Vše"
+  FROM: "od"
+  TO: "do"
+  RELEASE_DATE: "Datum vydání"
+  SORT_BY: "Seřadit podle"
+  RESOURCE_FILTER: "Filtr..."
+  FORCE_SSL: "Vynutit SSL"
+  FORCE_SSL_HELP: "Globálně vynutit SSL, je-li povoleno, web dosažený prostřednictvím protokolu HTTP, Grav přesměruje na HTTPS stránku"
+  NEWS_FEED: "Novinky"
+  EXTERNAL_URL: "Externí adresa URL"
+  SESSION_SAMESITE: "Atribut SameSite relace"
+  SESSION_SAMESITE_HELP: "Lax|Strict|None. Více informací naleznete na https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"
+  CUSTOM_BASE_URL: "Vlastní základní adresa URL"
+  CUSTOM_BASE_URL_HELP: "Užívejte, chcete-li přepsat doménu stránky nebo užít jinou podsložku, než která je v Grav užívaná. Například http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'Nelze použít "%s" mimo stránky.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'Nepodařilo se nahrát soubor %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'Nepodařilo přesunout soubor %s do "%s"'
+  DROPZONE_CANCEL_UPLOAD: 'Zrušit nahrávání'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Opravdu si přejete toto nahrávání zrušit?'
+  DROPZONE_DEFAULT_MESSAGE: 'Přetáhněte sem své soubory nebo <strong>klikněte na tuto plochu</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'Váš prohlížeč nepodporuje "drag & drop" nahrávání souborů.'
+  DROPZONE_FALLBACK_TEXT: 'Použijte, prosím, záložní formulář níže pro nahrávání souborů.'
+  DROPZONE_FILE_TOO_BIG: 'Soubor je příliš velký ({{filesize}}MiB). Maximální velikost souboru: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "Nelze nahrát soubory tohoto typu."
+  DROPZONE_MAX_FILES_EXCEEDED: "Nelze nahrát žádné další soubory."
+  DROPZONE_REMOVE_FILE: "Odstranit soubor"
+  DROPZONE_RESPONSE_ERROR: "Server odpověděl kódem {{statusCode}}."
+  PREMIUM_PRODUCT: "Prémium"
+  DESTINATION_NOT_SPECIFIED: "Cíl není zadán"
+  UPLOAD_ERR_NO_TMP_DIR: "Chybí dočasná složka"
+  SESSION_SPLIT: "Rozdělit relaci"
+  SESSION_SPLIT_HELP: "Nezávisle rozdělené relace mezi webem a dalšími doplňky (jako admin)"
+  ERROR_FULL_BACKTRACE: "Úplná chybová hlášení"
+  ERROR_SIMPLE: "Jednoduchá chyba"
+  ERROR_SYSTEM: "Systémová chyba"
+  IMAGES_AUTO_FIX_ORIENTATION: "Opravit orientaci automaticky"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Automaticky opravovat natočení obrázku podle EXIF data souboru"
+  REDIS_SOCKET: "Redis socket"
+  REDIS_SOCKET_HELP: "Redis socket"
+  NOT_SET: "Nenastaveno"
+  PERMISSIONS: "Přístupová práva"
+  NEVER_CACHE_TWIG: "Nikdy nacachovat Twig"
+  NEVER_CACHE_TWIG_HELP: "Do mezipaměti ukládat jen obsah a Zpracovat Twig při každém načtení. Bude ignorovat nastavení twig_first."
+  ALLOW_WEBSERVER_GZIP: "Povolit WebServer Gzip"
+  ALLOW_WEBSERVER_GZIP_HELP: "Ve výchozím nastavení vypnuto. Je-li povolena, komprese Gzip/Deflate bude fungovat, ale připojení http nebudou uzavřeny před událostí onShutDown(), a tím způsobuje pomalejší načítání stránek"
+  OFFLINE_WARNING: "Nelze navázat spojení s GPM"
+  CLEAR_IMAGES_BY_DEFAULT: "Vyčistit mezipaměť obrázků jako výchozí"
+  CLEAR_IMAGES_BY_DEFAULT_HELP: "Ve výchozím nastavení jsou obrázky z mezipaměti odstraněny při jakémkoli její čištění. To může být vypnuto"
+  CLI_COMPATIBILITY: "CLI kompatibilita"
+  CLI_COMPATIBILITY_HELP: "Zajišťuje, že budou použity jen stabilní mezipamětní ovladače (soubor, redis, memcache, atd.)"
+  REINSTALL_PLUGIN: "Přeinstalovat doplněk"
+  REINSTALL_THEME: "Přeinstalovat šablonu"
+  REINSTALL_THE: "Reinstalovat %s"
+  CONFIRM_REINSTALL: "Jste si jistí, že chcete %s přeinstalovat?"
+  REINSTALLED_SUCCESSFULLY: "%s byl úspěšně přeinstalován"
+  ERROR_REINSTALLING_THE: "Chyba při reinstalaci %s"
+  PACKAGE_X_REINSTALLED_SUCCESSFULLY: "Balíček %s byl úspěšně přeinstalován"
+  REINSTALLATION_FAILED: "Chyba při přeinstalaci"
+  WARNING_REINSTALL_NOT_LATEST_RELEASE: "Není nainstalovaná nejnovější verze. Klepnutím na Pokračovat odeberete současnou verzi a nainstalujete nejnověšjí dostupnou verzi"
+  TOOLS: "Nástroje"
+  DIRECT_INSTALL: "Přímá instalace"
+  NO_PACKAGE_NAME: "Nespecifikovaný název balíčku"
+  PACKAGE_EXTRACTION_FAILED: "Extrakce baličku se nezdařila"
+  NOT_VALID_GRAV_PACKAGE: "Není platným balíčkem Grav"
+  NAME_COULD_NOT_BE_DETERMINED: "Jméno nemohlo být určeno"
+  CANNOT_OVERWRITE_SYMLINKS: "Nelze přepsat symbolické odkazy"
+  ZIP_PACKAGE_NOT_FOUND: "ZIP soubor nebyl nalezen"
+  GPM_OFFICIAL_ONLY: "Pouze oficiální GPM"
+  GPM_OFFICIAL_ONLY_HELP: "Povolit pouze přímé instalace z oficiálního repozitáře GPM."
+  NO_CHILD_TYPE: "Žádný odvozený typ pro zdrojové směrování"
+  SORTABLE_PAGES: "Tříditelné stánky:"
+  UNSORTABLE_PAGES: "Netříditelné stránky"
+  ADMIN_SPECIFIC_OVERRIDES: "Specifická přepsání admina"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Pořadí zobrazení dětí"
+  ADMIN_CHILDREN_DISPLAY_ORDER_HELP: "Pořadí v jakém bude odvozená stránka zobrazena v pohledu 'Strany' správcovského zásuvného modulu"
+  PWD_PLACEHOLDER: "komplexní řetězec nejméně 8 znaků dlouhý"
+  PWD_REGEX: "Regex hesla"
+  PWD_REGEX_HELP: "Výchozí: Hesla musí obsahovat minimálně jedno číslo, jedno velké a malé písmeno a jejich délka musí být nejméně 8 znaků"
+  USERNAME_PLACEHOLDER: "jen malá písmena, např. 'admin'"
+  USERNAME_REGEX: "Regex uživatelského jména"
+  USERNAME_REGEX_HELP: "Výchozí: jen malá písmena, znaky, číslice, pomlčky a podtržítka. 3 - 16 znaků"
+  ENABLE_AUTO_METADATA: "Automatická metadata z Exif"
+  ENABLE_AUTO_METADATA_HELP: "Automaticky generovat soubory matadat pro obrázky s Exif informacemi"
+  2FA_TITLE: "Dvoufaktorové ověření"
+  2FA_INSTRUCTIONS: "##### Dvoufaktorové ověřování\nMáte na svém účtu aktivováno **dvoufaktorové ověřování**. Pro dokončení přihlášení použijte svou aplikaci pro **dvoufaktorové ověřování ** a zadejte **šestimístný kód**."
+  2FA_REGEN_HINT: "Obnovení tajného kódu bude vyžadovat aktualizaci vaší autentifikační aplikace"
+  2FA_LABEL: "Přístup správce"
+  2FA_FAILED: "Špatně zadaný kód dvoufaktorového ověření, zkuste to znovu."
+  2FA_ENABLED: "Dvoufaktorové ověření povoleno"
+  2FA_CODE_INPUT: "000000"
+  2FA_SECRET: "Tajný kód dvoufaktorového ověření"
+  2FA_SECRET_HELP: "Naskenujte tento QR kód do vaší [Autentifikační aplikace](https://learn.getgrav.org/admin-panel/2fa#apps). Důležité je také uložit tajný kód na bezpečném místě, pokud bude zapotřebí aplikaci přeinstalovat. Pro více informací se podívejte do [dokumentace Grav](https://learn.getgrav.org/admin-panel/2fa)"
+  2FA_REGENERATE: "Přegenerovat"
+  FORCE_LOWERCASE_URLS: "Vynutit malá písmena v URL"
+  FORCE_LOWERCASE_URLS_HELP: "Ve výchozím nastavení Grav nastaví všechny slugy a cesty na malá písmena. Změnou nastavení na hodnotu false, mohou slugy a cesty obsahovat Velká Písmena"
+  INTL_ENABLED: "Integrace modulu Intl"
+  INTL_ENABLED_HELP: "Použijte modul Intl PHP a porovnávejte třídění s kolekcemi založenými na UTF8"
+  VIEW_SITE_TIP: "Zobrazit web"
+  TOOLS_DIRECT_INSTALL_TITLE: "Přímá instalace balíčků Grav"
+  TOOLS_DIRECT_INSTALL_UPLOAD_TITLE: "Instalovat balíček prostřednictvím staženého ZIP souboru"
+  TOOLS_DIRECT_INSTALL_UPLOAD_DESC: "Můžete snadno nainstalovat platnou Grav <strong>šablonu</strong>, platný <strong>doplněk</strong> nebo dokonce <strong>Grav</strong> aktualizační balíček Zip prostřednictvím této metody. Tento balíček nemusí být registrován prostřednictvím GPM a umožňuje vám snadno vrátit se k předchozí verzi nebo vytvořit instalaci pro testování."
+  TOOLS_DIRECT_INSTALL_URL_TITLE: "Instalovat balíček prostřednictvím odkazu vzdálené adresy URL"
+  TOOLS_DIRECT_INSTALL_URL_DESC: "Případně můžete také odkázat na úplnou adresu URL k balíčku ZIP souboru a nainstalovat jej prostřednictvím této vzdálené adresy URL."
+  TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Nahrát a instalovat"
+  ROUTE_OVERRIDES: "Přepsání cesty"
+  ROUTE_DEFAULT: "Výchozí cesta"
+  ROUTE_CANONICAL: "Kanonická cesta"
+  ROUTE_ALIASES: "Aliasy cesty"
+  OPEN_NEW_TAB: "Otevřít v nové záložce"
+  SESSION_INITIALIZE: "Inicializovat relaci"
+  SESSION_INITIALIZE_HELP: "Přinutí Grav k nastartování sezení. Tato funkce je zapotřebí pro jakoukoliv interakci s uživatelem jako je např. přihlášení, formuláře apod. Admin rozšíření není tímto nastavením ovlivněno."
+  STRICT_YAML_COMPAT: "YAML kompatibilita"
+  STRICT_YAML_COMPAT_HELP: "Přejít zpět na Symfony 2.4 YAML parser, pokud nativní nebo 3.4 parser selžou"
+  STRICT_TWIG_COMPAT: "Twig kompatibilita"
+  STRICT_TWIG_COMPAT_HELP: "Povolí zastaralé nastavení Twig autoescape.  Pokud je zakázáno, filter |raw je požadován pro HTML výstup, protože Twig bude výstup automaticky ukončovat"
+  SCHEDULER: "Plánovač"
+  SCHEDULER_INSTALL_INSTRUCTIONS: "Návod k instalaci"
+  SCHEDULER_INSTALLED_READY: "Nainstalováno a připraveno"
+  SCHEDULER_CRON_NA: "Cron není k dispozici pro uživatele: <b>%s</b>"
+  SCHEDULER_NOT_ENABLED: "Není povoleno pro uživatele: <b>%s</b>"
+  SCHEDULER_SETUP: "Nastavení plánovače"
+  SCHEDULER_INSTRUCTIONS: "<b>Grav plánovač</b> umožňuje vytvořit a naplánovat vlastní úlohy. Poskytuje také metodu pro Grav doplňky k programové integraci a dynamickému přidávaní úloh, které mají být spouštěny v pravidelných intervalech."
+  SCHEDULER_POST_INSTRUCTIONS: "Pro povolení funkce plánovače, musíte přidat <b>Grav plánovač</b> do souboru crontab vašeho systému pro uživatele <b>%s</b> . Spusťte příkaz výše z terminálu a přidejte jej automaticky. Po uložení obnovte tuto stránku pro zobrazení stavu."
+  SCHEDULER_JOBS: "Vlastní úlohy plánování"
+  SCHEDULER_STATUS: "Stav plánovače"
+  SCHEDULER_RUNAT: "Spustit v"
+  SCHEDULER_RUNAT_HELP: "Formát syntaxe \"v\" Cronu"
+  SCHEDULER_OUTPUT: "Výstupní soubor"
+  SCHEDULER_OUTPUT_HELP: "Cesta/název výstupního souboru (z kořenové složky instalace Gravu)"
+  SCHEDULER_OUTPUT_TYPE: "Typ výstupu"
+  SCHEDULER_OUTPUT_TYPE_HELP: "Buď připojit ke stejnému souboru každé spuštění, nebo přepsat soubor s každým spuštěním"
+  SCHEDULER_EMAIL: "E-mail"
+  SCHEDULER_EMAIL_HELP: "E-mail pro odeslání výstupu. Poznámka: vyžaduje nastavit výstupní soubor"
+  SCHEDULER_WARNING: "Plánovač používá ke spuštění příkazů crontab vašeho systému. Měli byste toto použít pouze v případě, že jste pokročilý uživatel a víte, co děláte. Špatná konfigurace nebo zneužití mohou vést k bezpečnostním problémům."
+  SECURITY: "Zabezpečení"
+  XSS_SECURITY: "XSS zabezpečení obsahu"
+  XSS_WHITELIST_PERMISSIONS: "Povolená oprávnění"
+  XSS_WHITELIST_PERMISSIONS_HELP: "Uživatelé s těmito oprávněními přeskočí XSS pravidla při ukládání obsahu"
+  XSS_ON_EVENTS: "Filtr událostí"
+  XSS_INVALID_PROTOCOLS: "Filtr neplatných protokolů"
+  XSS_INVALID_PROTOCOLS_LIST: "Seznam neplatných protokolů"
+  XSS_MOZ_BINDINGS: "Filtr Moz vazeb"
+  XSS_HTML_INLINE_STYLES: "Filtr HTML inline stylů"
+  XSS_DANGEROUS_TAGS: "Filtr nebezpečných značek HTML"
+  XSS_DANGEROUS_TAGS_LIST: "Seznam nebezpečných značek HTML"
+  XSS_ONSAVE_ISSUE: "Ukládání selhalo: zjištěn XSS problém..."
+  XSS_ISSUE: "<strong>UPOZORNĚNÍ:</strong>Grav nalezl potenciální XSS problémy v <strong>%s</strong>"
+  UPLOADS_SECURITY: "Bezpečnost nahrávání"
+  UPLOADS_DANGEROUS_EXTENSIONS: "Nebezpečná rozšíření"
+  UPLOADS_DANGEROUS_EXTENSIONS_HELP: "Blokovat tato rozšíření při nahrávání navzdory povoleným MIME typům"
+  REPORTS: "Zprávy"
+  LOGS: "Protokoly"
+  LOG_VIEWER_FILES: "Prohlížeč souborů protokolu"
+  LOG_VIEWER_FILES_HELP: "Soubory v /logs/, které budou k dispozici pro zobrazení v nabídce Nástroje - Protokoly. Například \"grav\" = /logs/grav.log"
+  BACKUPS_STORAGE_PURGE_TRIGGER: "Spouštěč vymazání úložiště záloh"
+  BACKUPS_MAX_COUNT: "Maximální počet záloh"
+  BACKUPS_MAX_COUNT_HELP: "0 je neomezené"
+  BACKUPS_MAX_SPACE: "Maximální prostor pro zálohy"
+  BACKUPS_MAX_RETENTION_TIME: "Maximální doba uchování"
+  BACKUPS_MAX_RETENTION_TIME_APPEND: "ve dnech"
+  BACKUPS_PROFILE_NAME: "Název zálohy"
+  BACKUPS_PROFILE_ROOT_FOLDER: "Kořenová složka"
+  BACKUPS_PROFILE_ROOT_FOLDER_HELP: "Může být absolutní cesta nebo proud"
+  BACKUPS_PROFILE_EXCLUDE_PATHS: "Vyloučit cesty"
+  BACKUPS_PROFILE_EXCLUDE_PATHS_HELP: "Absolutní cesty k vyloučení, jedna na řádek"
+  BACKUPS_PROFILE_EXCLUDE_FILES: "Vyloučit soubory"
+  BACKUPS_PROFILE_EXCLUDE_FILES_HELP: "Konkrétní soubory nebo složky k vyloučení, jeden/jedna na řádek"
+  BACKUPS_PROFILE_SCHEDULE: "Povolit naplánovanou úlohu"
+  BACKUPS_PROFILE_SCHEDULE_AT: "Spustit naplánovanou úlohu"
+  COMMAND: "Příkaz"
+  EXTRA_ARGUMENTS: "Další argumenty"
+  DEFAULT_LANG: "Přepsat výchozí jazyk"
+  DEFAULT_LANG_HELP: "Výchozí je první podporovaný jazyk. Toto může být přepsáno nastavením této možnosti, ale musí to být jeden z podporovaných jazyků"
+  DEBUGGER_PROVIDER: "Poskytovatel ladicího programu"
+  DEBUGGER_PROVIDER_HELP: "Výchozí je PHP Debug Bar, ale rozšíření prohlížeče Clockwork poskytuje méně rušivý přístup"
+  DEBUGGER_DEBUGBAR: "PHP Debug panel"
+  DEBUGGER_CLOCKWORK: "Rozšíření prohlížeče Clockwork"
+  PAGE_ROUTE_NOT_FOUND: "Cesta ke stránce nebyla nalezena"
+  PAGE_ROUTE_FOUND: "Cesta ke stránce nalezena"
+  NO_ROUTE_PROVIDED: "Nebyla zadána cesta"
+  CONTENT_LANGUAGE_FALLBACKS: "Záložní jazyk obsahu"
+  CONTENT_LANGUAGE_FALLBACKS_HELP: "Ve výchozím nastavení, pokud obsah není přeložen, Grav zobrazí obsah ve výchozím jazyce. Použijte toto nastavení pro přepsání chování podle jazyka."
+  CONTENT_LANGUAGE_FALLBACK: "Záložní jazyky"
+  CONTENT_LANGUAGE_FALLBACK_HELP: "Zadejte prosím seznam kódů jazyka. Vezměte prosím na vědomí, že pokud vynecháte výchozí kód jazyka, nebude použit."
+  CONTENT_FALLBACK_LANGUAGE_HELP: "Zadejte kód jazyka, který chcete upravit."
+  EXPERIMENTAL: "Experimentální"
+  PAGES_TYPE: "Typ stránky veřejné části"
+  PAGES_TYPE_HELP: "Tato volba umožňuje využití objektů Flex Object na front-endu. Admin Flex stránky vyžadují plugin Flex objektů"
+  ACCOUNTS_TYPE: "Typy účtů"
+  ACCOUNTS_TYPE_HELP: "Sytém Flex Objects ukládá uživatelské účty"
+  ACCOUNTS_STORAGE: "Úložiště účtů"
+  ACCOUNTS_STORAGE_HELP: "Mechanismus pro ukládání, který se použije pro typ účtu objektu Flex. Soubory jsou tradiční přístup, kde je účet uložen v YAML souboru v jediné složce, zatímco Složka vytváří novou složku pro každý účet"
+  FLEX: "Objekt Flex (EXPERIMENTÁLNÍ)"
+  REGULAR: "Běžný"
+  FILE: "Soubor"
+  SANITIZE_SVG: "Vyčistit SVG"
+  SANITIZE_SVG_HELP: "Odstraní jakýkoliv XSS kód ze SVG"
+  ACCOUNTS: "Účty"
+  USER_ACCOUNTS: "Uživatelské účty"
+  USER_GROUPS: "Uživatelské skupiny"
+  GROUP_NAME: "Název skupiny"
+  DISPLAY_NAME: "Zobrazované jméno"
+  ICON: "Ikona"
+  ACCESS: "Přístup"
+  NO_ACCESS: "Bez přístupu"
+  SUPER_USER: "Super uživatel"
+  ALLOWED: "Povoleno"
+  DENIED: "Zakázáno"
+  MODULE: "Modulární"
+  NON_MODULE: "Nemodulární"
+  ADD_MODULE: "Přidat modul"
+  MODULE_SETUP: "Nastavení modulu"
+  MODULE_TEMPLATE: "Šablona modulu"
+  ADD_MODULE_CONTENT: "Přidat obsah modulu"
+  CHANGELOG: "Přehled změn"
+  PAGE_ACCESS: "Přístup ke stránce"
+  PAGE PERMISSIONS: "Oprávnění stránky"
+  PAGE_ACCESS_HELP: "Uživatel s následujícími přístupovými oprávněními může vstoupit na stránku."
+  PAGE_VISIBILITY_REQUIRES_ACCESS: "Viditelnost nabídky vyžaduje přístup"
+  PAGE_VISIBILITY_REQUIRES_ACCESS_HELP: "Nastavte Ano pokud má být stránka zobrazena v nabídce pouze v případě, že k ní má uživatel přístup."
+  PAGE_INHERIT_PERMISSIONS: "Zdědit oprávnění"
+  PAGE_INHERIT_PERMISSIONS_HELP: "Zdědit přístup z nadřazené stránky."
+  PAGE_AUTHORS: "Autoři stránky"
+  PAGE_AUTHORS_HELP: "Členové skupiny Autoři stránek mají přístup na tuto stránku jako vlastník definovaný ve speciální skupině 'Autoři'."
+  PAGE_GROUPS: "Skupiny stránek"
+  PAGE_GROUPS_HELP: "Členové stránkových skupin mají zvláštní přístup k této stránce."
+  READ: "Číst"
+  PUBLISH: "Zveřejnit"
+  LIST: "Seznam"
+  ACCESS_SITE: "Web"
+  ACCESS_SITE_LOGIN: "Přihlášení ke stránkám"
+  ACCESS_ADMIN: "Správce"
+  ACCESS_ADMIN_LOGIN: "Přihlášení ke správě stránek"
+  ACCESS_ADMIN_SUPER: "Super uživatel"
+  ACCESS_ADMIN_CACHE: "Vyprázdnit mezipaměť"
+  ACCESS_ADMIN_CONFIGURATION: "Nastavení"
+  ACCESS_ADMIN_CONFIGURATION_SYSTEM: "Nastavení systému"
+  ACCESS_ADMIN_CONFIGURATION_SITE: "Nastavení Webu"
+  ACCESS_ADMIN_CONFIGURATION_MEDIA: "Nastavení médií"
+  ACCESS_ADMIN_CONFIGURATION_INFO: "Zobrazit informace o serveru"
+  ACCESS_ADMIN_SETTINGS: "Nastavení"
+  ACCESS_ADMIN_PAGES: "Spravovat stránky"
+  ACCESS_ADMIN_MAINTENANCE: "Údržba webu"
+  ACCESS_ADMIN_STATISTICS: "Statistiky webu"
+  ACCESS_ADMIN_PLUGINS: "Správa rozšíření"
+  ACCESS_ADMIN_THEMES: "Správa vzhledu"
+  ACCESS_ADMIN_TOOLS: "Přístup k nástrojům"
+  ACCESS_ADMIN_USERS: "Správa uživatelů"
+  USERS: "Uživatelé"
+  ACL: "Správa přístupu"
+  FLEX_CACHING: "Mezipaměť Flex"
+  FLEX_INDEX_CACHE_ENABLED: "Povolit mezipaměť indexu"
+  FLEX_INDEX_CACHE_LIFETIME: "Životnost mezipaměti indexu (v sekundách)"
+  FLEX_OBJECT_CACHE_ENABLED: "Povolit mezipaměť objektů"
+  FLEX_OBJECT_CACHE_LIFETIME: "Životnost mezipaměti objektů (v sekundách)"
+  FLEX_RENDER_CACHE_ENABLED: "Povolit mezipaměť vykreslování"
+  FLEX_RENDER_CACHE_LIFETIME: "Životnost mezipaměti vykreslování (v sekundách)"
+  DEBUGGER_CENSORED: "Odstranit citlivá data"
+  DEBUGGER_CENSORED_HELP: "Pro Clockwork zprostředkovatele: Pokud je nastaveno Ano, odstraní se potenciálně citlivá data (parametry POST, cookies, soubory, nastavení a většina polí/objektových dat z logů)"
+  LANGUAGE_TRANSLATIONS: "Překlady"
+  LANGUAGE_TRANSLATIONS_HELP: "Pokud není povoleno, jsou použity překladové klíče namísto přeložených řetězců. Tato funkce může být použita k opravě špatných překladů nebo k nalezení hardkódovaných anglických řetězců."
+  STRICT_BLUEPRINT_COMPAT: "Kompatibilita plánu"
+  STRICT_BLUEPRINT_COMPAT_HELP: "Umožňuje zpětně kompatibilní přísnou podporu pro plány. Pokud je vypnuto, nové chování provede ověření formuláře jako neúspěšné, pokud existují další data, která nejsou definována v plánu."
+  RESET: "Obnovit"
+  LOGOS: "Loga"
+  PRESETS: "Předvolby"
+  COLOR_SCHEME_LABEL: "Barevné schéma"
+  COLOR_SCHEME_HELP: "Vyberte barevné schéma ze seznamu předdefinovaných kombinací, nebo přidejte svůj vlastní styl"
+  COLOR_SCHEME_NAME: "Název vlastního barevného schéma"
+  COLOR_SCHEME_NAME_HELP: "Dejte název vašemu vlastnímu motivu pro export a sdílení"
+  COLOR_SCHEME_NAME_PLACEHOLDER: "Odstíny modré"
+  PRIMARY_ACCENT_LABEL: "Primární zvýraznění"
+  PRIMARY_ACCENT_HELP: "Vyberte, která barva by měla být použita pro barevné schéma"
+  SECONDARY_ACCENT_LABEL: "Sekundární zvýraznění"
+  SECONDARY_ACCENT_HELP: "Vyberte, která barva by měla být použita pro barevné schéma jako sekundární"
+  TERTIARY_ACCENT_LABEL: "Terciární zvýraznění"
+  TERTIARY_ACCENT_HELP: "Vyberte, která barva by měla být použita pro barevné schéma jako terciární"
+  WEB_FONTS_LABEL: "Webová písma"
+  WEB_FONTS_HELP: "Použít vlastní webová písma"
+  HEADER_FONT_LABEL: "Písmo záhlaví"
+  HEADER_FONT_HELP: "Písmo použité pro záhlaví, boční navigaci a názvy sekcí"
+  BODY_FONT_LABEL: "Písmo textu"
+  BODY_FONT_HELP: "Primární písmo použité v těle motivu"
+  CUSTOM_CSS_LABEL: "Vlastní styly"
+  CUSTOM_CSS_PLACEHOLDER: "Zde vložte vlastní CSS..."
+  CUSTOM_CSS_HELP: "Vlastní CSS které budou přidány do každé admin stránky"
+  CUSTOM_FOOTER: "Vlastní zápatí"
+  CUSTOM_FOOTER_HELP: "Zde můžete použít HTML a/nebo Markdown syntaxi"
+  CUSTOM_FOOTER_PLACEHOLDER: "Zadejte HTML/Markdown pro přepsání výchozího zápatí"
+  LOGIN_SCREEN_CUSTOM_LOGO_LABEL: "Vlastní přihlašovací logo"
+  TOP_LEFT_CUSTOM_LOGO_LABEL: "Základní vlastní logo"
+  LOAD_PRESET: "Načíst předvolbu"
+  RECOMPILE: "Rekompilovat"
+  EXPORT: "Export"
+  QUICKTRAY_RECOMPILE: "Ikona Rychlé rekompilace"
+  QUICKTRAY_RECOMPILE_HELP: "Překompiluje přednastavený SCSS pro vyzvednutí jakýchkoli změn nebo nových pluginů"
+  CODEMIRROR: "Editor kódu CodeMirror"
+  CODEMIRROR_THEME: "Téma editoru"
+  CODEMIRROR_THEME_DESC: "**POZNÁMKA:** Použít [Demo CodeMirror témata](https://codemirror.net/demo/theme.html?target=_blank) pro jejich náhled. **_Paper_** je výchozí Grav motiv."
+  CODEMIRROR_FONTSIZE: "Velikost písma editoru"
+  CODEMIRROR_FONTSIZE_SM: "Malé písmo"
+  CODEMIRROR_FONTSIZE_MD: "Střední písmo"
+  CODEMIRROR_FONTSIZE_LG: "Velké písmo"
+  CODEMIRROR_MD_FONT: "Písmo editoru Markdown"
+  CODEMIRROR_MD_FONT_SANS: "Sans písmo"
+  CODEMIRROR_MD_FONT_MONO: "Písmo Mono/Pevné šířky"
+  CUSTOM_PRESETS: "Vlastní předvolby"
+  CUSTOM_PRESETS_HELP: "Přetáhněte zde soubor .yaml tématu nebo můžete vytvořit pole předvoleb s textovými klíči"
+  CUSTOM_PRESETS_PLACEHOLDER: "Sem zadejte své předvolby"
+  GENERAL: "Obecné"
+  CONTENT_EDITOR: "Editor obsahu"
+  CONTENT_EDITOR_HELP: "Vlastní editory mohou být preferovány pro úpravy obsahu"
+  BAD_FILENAME: "Špatný název souboru"
+  SHOW_SENSITIVE: "Zobrazit citlivá data"
+  SHOW_SENSITIVE_HELP: "POUZE pro poskytovatele Clockwork: Odstranění potenciálně citlivých informací (POST parametry, cookies, soubory, konfigurace a většina dat polí/objektů v log souborech)"
+  VALID_LINK_ATTRIBUTES: "Platné atributy odkazu"
+  VALID_LINK_ATTRIBUTES_HELP: "Atributy, které budou automaticky přidány do prvku HTML média"
+  CONFIGURATION: "Nastavení"
+  CUSTOMIZATION: "Přizpůsobení"
+  EXTRAS: "Doplňky"
+  BASICS: "Základy"
+  ADMIN_CACHING: "Povolit Admin Caching"
+  ADMIN_CACHING_HELP: "Ukládání do mezipaměti v administraci lze ovládat nezávisle z front-end webu"
+  ADMIN_PATH: "Cesta ke správcovské části"
+  ADMIN_PATH_PLACEHOLDER: "Výchozí cesta pro administrátora (relativně k základně)"
+  ADMIN_PATH_HELP: "Pokud chcete změnit URL adresu správcovské části, můžete zde zadat cestu"
+  LOGO_TEXT: "Text loga"
+  LOGO_TEXT_HELP: "Text zobrazený místo výchozího loga Grav"
+  CONTENT_PADDING: "Odsazení obsahu"
+  CONTENT_PADDING_HELP: "Povolit/zakázat obsah odsazení kolem oblasti obsahu pro poskytnutí více místa"
+  BODY_CLASSES: "Třídy pro <body> element"
+  BODY_CLASSES_HELP: "Přidat názvy vlastních tříd oddělené mezerami"
+  SIDEBAR_ACTIVATION: "Aktivace postranního panelu"
+  SIDEBAR_ACTIVATION_HELP: "Určuje, jak je boční panel aktivován"
+  SIDEBAR_HOVER_DELAY: "Zpoždění při přechodu"
+  SIDEBAR_HOVER_DELAY_APPEND: "milisekund"
+  SIDEBAR_ACTIVATION_TAB: "Záložka"
+  SIDEBAR_ACTIVATION_HOVER: "Hover"
+  SIDEBAR_SIZE: "Velikost postranního panelu"
+  SIDEBAR_SIZE_HELP: "Nastavuje šířku postranního panelu"
+  SIDEBAR_SIZE_AUTO: "Automatická šířka"
+  SIDEBAR_SIZE_SMALL: "Malá šířka"
+  EDIT_MODE: "Režim úprav"
+  EDIT_MODE_HELP: "Automaticky použije plán, pokud je k dispozici, pokud není nalezen žádný z nich, použije režim \"Expert\"."
+  FRONTEND_PREVIEW_TARGET: "Cíl náhledu stránek"
+  FRONTEND_PREVIEW_TARGET_INLINE: "Použít Inline ve správcovské části"
+  FRONTEND_PREVIEW_TARGET_NEW: "Nová záložka"
+  FRONTEND_PREVIEW_TARGET_CURRENT: "Aktuální záložka"
+  PARENT_DROPDOWN: "Nadřazený rozevírací seznam"
+  PARENT_DROPDOWN_BOTH: "Zobrazit URL adresu a složku"
+  PARENT_DROPDOWN_FOLDER: "Zobrazit složku"
+  PARENT_DROPDOWN_FULLPATH: "Zobrazit celou cestu"
+  PARENTS_LEVELS: "Rodičovské úrovně"
+  PARENTS_LEVELS_HELP: "Počet úrovní zobrazených v nadřazeném seznamu"
+  MODULAR_PARENTS: "Modulární rodiče"
+  MODULAR_PARENTS_HELP: "Zobrazit modulární stránky v nadřazeném seznamu"
+  SHOW_GITHUB_LINK: "Zobrazit GitHub odkaz"
+  SHOW_GITHUB_LINK_HELP: "Zobrazit \"Nalezli jste problém? Nahlašte to prosím na GitHub.\""
+  PAGES_LIST_DISPLAY_FIELD: "Zobrazovat pole seznamu stránek"
+  PAGES_LIST_DISPLAY_FIELD_HELP: "Pole stránky, které se mají použít v seznamu stránek, pokud jsou přítomny. Výchozí nastavení/Zpět do titulku."
+  AUTO_UPDATES: "Vyhledávat aktualizace automaticky"
+  AUTO_UPDATES_HELP: "Zobrazí informativní zprávu v admin panelu, pokud je k dispozici aktualizace."
+  TIMEOUT: "Časový limit"
+  TIMEOUT_HELP: "Nastaví platnost session v sekundách"
+  HIDE_PAGE_TYPES: "Skrýt typy stránek v administraci"
+  HIDE_MODULAR_PAGE_TYPES: "Skrýt modulární typy stránek v administraci"
+  DASHBOARD: "Přehled"
+  WIDGETS_DISPLAY: "Stav zobrazení widgetu"
+  NOTIFICATIONS: "Notifikace"
+  FEED_NOTIFICATIONS: "Oznámení"
+  FEED_NOTIFICATIONS_HELP: "Zobrazit oznámení na základě zdroje zpráv"
+  DASHBOARD_NOTIFICATIONS: "Oznámení hlavního panelu"
+  DASHBOARD_NOTIFICATIONS_HELP: "Zobrazit oznámení na nástěnce"
+  PLUGINS_NOTIFICATIONS: "Upozornění rozšíření"
+  PLUGINS_NOTIFICATIONS_HELP: "Zobrazit oznámení k rozšířením"
+  THEMES_NOTIFICATIONS: "Upozornění na motivy"
+  THEMES_NOTIFICATIONS_HELP: "Zobrazit oznámení zaměřená na témata"
+  LOGO_BG_HELP: "Pozadí loga"
+  LOGO_LINK_HELP: "Odkaz na logo"
+  NAV_BG_HELP: "Pozadí navigace"
+  NAV_TEXT_HELP: "Text navigace"
+  NAV_LINK_HELP: "Odkaz navigace "
+  NAV_SELECTED_BG_HELP: "Pozadí zvolené navigace"
+  NAV_SELECTED_LINK_HELP: "Odkaz zvolené navigace"
+  NAV_HOVER_BG_HELP: "Pozadí přechodu navigace"
+  NAV_HOVER_LINK_HELP: "Odkaz přechodu navigace"
+  TOOLBAR_BG_HELP: "Pozadí nástrojové lišty"
+  TOOLBAR_TEXT_HELP: "Text nástrojové lišty"
+  PAGE_BG_HELP: "Pozadí stránky"
+  PAGE_TEXT_HELP: "Text stránky"
+  PAGE_LINK_HELP: "Odkaz na stránku"
+  CONTENT_BG_HELP: "Pozadí obsahu"
+  CONTENT_TEXT_HELP: "Text obsahu"
+  CONTENT_LINK_HELP: "Odkaz textu"
+  CONTENT_LINK2_HELP: "Odkaz 2 obsahu"
+  CONTENT_HEADER_HELP: "Hlavička obsahu"
+  CONTENT_TABS_BG_HELP: "Pozadí záložek obsahu"
+  CONTENT_TABS_TEXT_HELP: "Text záložek obsahu"
+  CONTENT_HIGHLIGHT_HELP: "Zvýraznění obsahu"
+  BUTTON_BG_HELP: "Pozadí tlačítek"
+  BUTTON_TEXT_HELP: "Text tlačítek"
+  NOTICE_BG_HELP: "Pozadí poznámek"
+  NOTICE_TEXT_HELP: "Text poznámek"
+  UPDATES_BG_HELP: "Pozadí aktualizací"
+  UPDATES_TEXT_HELP: "Text aktualizací"
+  CRITICAL_BG_HELP: "Pozadí kritických"
+  CRITICAL_TEXT_HELP: "Text kritický"
+  BUTTON_COLORS: "Barvy tlačítka"
+  CONTENT_COLORS: "Barvy obsahu"
+  TABS_COLORS: "Barvy záložek"
+  CRITICAL_COLORS: "Barvy kritických"
+  LOGO_COLORS: "Barvy loga"
+  NAV_COLORS: "Barvy navigace"
+  NOTICE_COLORS: "Barvy poznámek"
+  PAGE_COLORS: "Barvy stránek"
+  TOOLBAR_COLORS: "Barvy nástrojové lišty"
+  UPDATE_COLORS: "Barvy aktualizací"
+  POPULARITY: "Oblíbenost"
+  VISITOR_TRACKING: "Sledování návštěvníků"
+  VISITOR_TRACKING_HELP: "Povolit funkci shromažďování statistik návštěvníků"
+  DAYS_OF_STATS: "Počet dní statistiky"
+  DAYS_OF_STATS_HELP: "Zachovat statistiky pro zadaný počet dní, pak je vymazat"
+  IGNORE_URLS: "Ignorovat"
+  IGNORE_URLS_HELP: "URL pro ignorování"
+  DAILY_HISTORY: "Denní historie"
+  MONTHLY_HISTORY: "Měsíční historie"
+  VISITORS_HISTORY: "Historie návštěvníků"
+  MEDIA_RESIZE: "Změna velikosti obrázků stránky"
+  PAGEMEDIA_RESIZER: "> Následující nastavení platí pro obrázky nahrané prostřednictvím stránky, sekce Média stránky. Velikost šířky / výšky obrázku se automaticky proporcionálně zmenší, pokud překročí limity nastavené na vstupu. Hodnoty min a max určují rozsahy velikosti nahraných obrázků. Nastavte pole na hodnotu 0, aby nedošlo k žádné manipulaci."
+  RESIZE_WIDTH: "Změna šířky"
+  RESIZE_WIDTH_HELP: "Změnit velikost širokých obrázků na nastavenou hodnotu"
+  RESIZE_HEIGHT: "Změna výšky"
+  RESIZE_HEIGHT_HELP: "Změnit výšku vysokých obrázků na nastavenou hodnotu"
+  RES_MIN_WIDTH: "Minimální šířka rozlišení"
+  RES_MIN_WIDTH_HELP: "Minimální šířka pro přidání obrázku"
+  RES_MIN_HEIGHT: "Min. výška rozlišení"
+  RES_MIN_HEIGHT_HELP: "Minimální povolená výška pro přidání obrázku"
+  RES_MAX_WIDTH: "Maximální šířka rozlišení"
+  RES_MAX_WIDTH_HELP: "Maximální povolená šířka pro přidání obrázku"
+  RES_MAX_HEIGHT: "Maximální výška rozlišení"
+  RES_MAX_HEIGHT_HELP: "Maximální povolená výška pro přidání obrázku"
+  RESIZE_QUALITY: "Kvalita změny rozlišení"
+  RESIZE_QUALITY_HELP: "Kvalita použitá při změně velikosti obrázku. Hodnota mezi 0 a 1."
+  PIXELS: "pixely"
+  ACCESS_ADMIN_CONFIGURATION_SECURITY: "Spravovat nastavení zabezpečení"
+  SESSION_DOMAIN: "Doména relace"
+  SESSION_DOMAIN_HELP: "Používejte pouze v případě, pokud přepíšete doménu webu, například v Docker Containeru."
+  SESSION_PATH: "Cesta relace"
+  SESSION_PATH_HELP: "Používejte pouze v případě, pokud přepíšete cestu webu, například v Docker Containeru."
+  REDIRECT_OPTION_NO_REDIRECT: "Bez přesměrování"
+  REDIRECT_OPTION_DEFAULT_REDIRECT: "Použít výchozí kód přesměrování"
+  REDIRECT_OPTION_301: "301 - Přesunuto trvale"
+  REDIRECT_OPTION_302: "302 - Přesunuto dočasně"
+  REDIRECT_OPTION_303: "303 - Viz ostatní"
+  IMAGES_CLS_TITLE: "Kumulativní posun rozložení (CLS)"
+  IMAGES_CLS_AUTO_SIZES: "Povolit automatické velikosti"
+  IMAGES_CLS_AUTO_SIZES_HELP: "Automaticky přidat atributy \"šířka\" a \"výška\" k obrázkům do CLS adresy"
+  IMAGES_CLS_ASPECT_RATIO: "Povolit poměr stran"
+  IMAGES_CLS_ASPECT_RATIO_HELP: "Volitelná proměnná CSS, aplikovaná prostřednictvím atributu \"style\", kterou lze použít v CSS pro vlastní stylování"
+  IMAGES_CLS_RETINA_SCALE: "Faktor měřítka Retina"
+  IMAGES_CLS_RETINA_SCALE_HELP: "Vezme vypočítanou velikost a vydělí faktorem měřítka, aby zobrazil obrázek s vyšším rozlišením při menší velikosti pixelu pro lepší zpracování rozlišení HiDPI"
+  AUTOREGENERATE_FOLDER_SLUG: "Automaticky obnovit na základě názvu stránky"
+  ENABLE: Povolit
+  PLUGINS_MUST_BE_ENABLED: "Pro nastavení musí být doplněk povolen"
+  ACTIVATION_REQUIRED: "Pro nastavení je vyžadována aktivace"

+ 89 - 0
plugins/admin/languages/cy.yaml

@@ -0,0 +1,89 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "Mae hwn yn fersiwn beta! Defnyddio hwn yn cynhyrchu ar risg eich hun..."
+  ADMIN_REPORT_ISSUE: "Canfod problem? Rhowch wybod ar GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\"> wedi'u pweru gan Grav</a>-ffeil fflat Modern CMS"
+  LOGIN_BTN: "Mewngofnodi"
+  LOGIN_BTN_FORGOT: "Anghofio"
+  LOGIN_BTN_RESET: "Ailosod cyfrinair"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Anfon cyfarwyddiadau ailosod"
+  LOGIN_BTN_CLEAR: "Ffurflen clir"
+  LOGIN_BTN_CREATE_USER: "Creu defnyddiwr"
+  LOGIN_LOGGED_IN: "Wedi eich mewngofnodi llwyddiannus"
+  LOGIN_FAILED: "Wedi methu mewngofnodi"
+  LOGGED_OUT: "Chi allgofnodi"
+  RESET_LINK_EXPIRED: "Ailosod cysylltiad wedi dod i ben, rhowch gynnig arall arni"
+  RESET_PASSWORD_RESET: "Mae wedi'i ailosod cyfrinair"
+  RESET_INVALID_LINK: "Mae annilys yn ailosod cyswllt a ddefnyddir, rhowch gynnig arni eto"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Mae cyfarwyddiadau i ailosod eich cyfrinair wedi'u hanfon drwy e-bost at %s"
+  FORGOT_FAILED_TO_EMAIL: "Wedi methu anfon e-bost cyfarwyddiadau, rhowch gynnig arall arni rywbryd eto"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Does dim modd ailosod cyfrinair %s, pennir nad oes cyfeiriad e-bost"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Nid yw'r defnyddiwr gyda'r enw defnyddiwr <b>%s</b> yn bodoli"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Does dim modd ailosod cyfrinair. Nid yw'r safle hwn wedi'i ffurfweddu i anfon negeseuon e-bost"
+  FORGOT_EMAIL_SUBJECT: "Cais ailosod cyfrinair %s"
+  FORGOT_EMAIL_BODY: "<h1>Ailosod cyfrinair</h1> <p>%1$s Annwyl,</p> <p>Gwnaed cais ar <b>%4$s</b> i ailosod eich cyfrinair.</p> <p>< br / > <a href=\"%2$s\" class=\"btn-primary\"> hyn i ailosod eich cyfrinair cliciwch</a> < br / > < br / ></p> <p>Fel arall, gopïo URL canlynol i'r bar cyfeiriad eich porwr:</p> <p>%2$s</p> <p>< br / > Cofion, < br / > < br / >%3$s</p>"
+  MANAGE_PAGES: "Rheoli tudalennau"
+  PAGES: "Tudalennau"
+  PLUGINS: "Ategion"
+  PLUGIN: "Ategyn"
+  THEMES: "Themâu"
+  LOGOUT: "Allgofnodi"
+  BACK: "Yn ôl"
+  ADD_PAGE: "Ychwanegu Tudalen"
+  MOVE: "Symud"
+  DELETE: "Dileu"
+  SAVE: "Cadw"
+  NORMAL: "Arferol"
+  EXPERT: "Arbenigol"
+  EXPAND_ALL: "Ehangu'r cyfan"
+  COLLAPSE_ALL: "Crebachu'r cyfan"
+  ERROR: "Gwall"
+  CLOSE: "Cau"
+  CANCEL: "Canslo"
+  CONTINUE: "Yn parhau"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Cadarnhad sydd yn ofynnol"
+  MODAL_CHANGED_DETECTED_TITLE: "Ganfod newidiadau"
+  MODAL_CHANGED_DETECTED_DESC: "Wedi ichi golli'r newidiadau.  Ydych chi'n siŵr eich bod am adael heb arbed?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Cadarnhad sydd yn ofynnol"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Ydych chi'n siŵr eich bod am ddileu'r ffeil hon? Ni ellir dad-wneud y cam gweithredu hwn."
+  ADD_FILTERS: "Ychwanegu hidlydd"
+  SEARCH_PAGES: "Tudalennau chwilio"
+  VERSION: "Fersiwn"
+  WAS_MADE_WITH: "Wedi greu hefo"
+  BY: "Gan"
+  AUTHOR: "Awdur"
+  HOMEPAGE: "Hafan"
+  KEYWORDS: "Allweddeiriau"
+  LICENSE: "Trwydded"
+  DESCRIPTION: "Disgrifiad"
+  THEME: "Thema"
+  BACK_TO_THEMES: "Ôl i themâu"
+  BACK_TO_PLUGINS: "Ôl i ategion"
+  CHECK_FOR_UPDATES: "Chwilio am ddiweddariadau"
+  ADD: "Ychwanegu"
+  CLEAR_CACHE: "Clirio storfa"
+  CLEAR_CACHE_ALL_CACHE: "Holl storfa"
+  CLEAR_CACHE_ASSETS_ONLY: "Asedau yn unig"
+  CLEAR_CACHE_IMAGES_ONLY: "Llyniau yn unig"
+  CLEAR_CACHE_CACHE_ONLY: "Storfa yn unig"
+  UPDATES_AVAILABLE: "Diweddariadau ar gael"
+  DAYS: "Diwrnod"
+  UPDATE: "Diweddaru"
+  STATISTICS: "Ystadegau"
+  TODAY: "Heddiw"
+  WEEK: "Wythnos"
+  MONTH: "Mis"
+  UPDATED: "Wedi'w ddiweddaru"
+  MON: "Llu"
+  TUE: "Maw"
+  WED: "Mer"
+  THU: "Iau"
+  FRI: "Gwe"
+  SAT: "Sad"
+  SUN: "Sul"
+  COPY: "Copi"
+  EDIT: "Golygu"
+  CREATE: "Creu"
+  AVAILABLE: "Ar gael"
+  CONFIGURATION: "Ffurfweddiad"
+  DASHBOARD: "Dangosfwrdd"

+ 760 - 0
plugins/admin/languages/da.yaml

@@ -0,0 +1,760 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_NOSCRIPT_MSG: "Aktivér JavaScript i din browser."
+  ADMIN_BETA_MSG: "Dette er en beta-udgivelse! Brug i produktionsmiljøer er på egen risiko..."
+  ADMIN_REPORT_ISSUE: "Har du fundet et problem? Så bedes du rapportere det på GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Drevet af Grav</a> - det moderne fladfil-CMS"
+  LOGIN_BTN: "Login"
+  LOGIN_BTN_FORGOT: "Glemt"
+  LOGIN_BTN_RESET: "Nulstil adgangskode"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Send nulstillingsinstruktioner"
+  LOGIN_BTN_CLEAR: "Ryd formular"
+  LOGIN_BTN_CREATE_USER: "Opret bruger"
+  LOGIN_LOGGED_IN: "Du er nu logget ind"
+  LOGIN_FAILED: "Login fejlede"
+  LOGGED_OUT: "Du er nu logget ud"
+  RESET_NEW_PASSWORD: "Skriv venligst en ny adgangskode"
+  RESET_LINK_EXPIRED: "Nulstillingslinket er udløbet; prøv venligst igen"
+  RESET_PASSWORD_RESET: "Adgangskoden er blevet nulstillet"
+  RESET_INVALID_LINK: "Du har benyttet et ugyldigt nulstillingslink; prøv venligst igen"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Vejledning i at nulstille din adgangskode er sendt via e-mail til %s"
+  FORGOT_FAILED_TO_EMAIL: "Mislykkedes at maile vejledninger, forsøg venligst igen senere"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Kan ikke nulstille adgangskode til %s; der er ikke angivet en e-mail-adresse"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Der findes ikke en bruger med brugernavnet <b>%s</b>"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Kan ikke nulstille adgangskoden. Dette site er ikke konfigureret til at sende e-mails"
+  FORGOT_EMAIL_SUBJECT: "%s Anmodning om nulstilling af adgangskode"
+  FORGOT_EMAIL_BODY: "<h1>Adgangskodenulstilling</h1><p>Hej %1$s</p><p>Der blev den<b>%4$s</b> anmodet om at nulstille din adgangskode.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klik her for at gennemføre adgangskodenulstillingen</a><br /><br /></p><p>Alternativt kan du indsætte flg. URL i din browsers adressebjælke:</p> <p>%2$s</p><p><br />Venlig hilsen,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Administrer sider"
+  PAGES: "Sider"
+  PLUGINS: "Plugins"
+  PLUGIN: "Plugin"
+  THEMES: "Temaer"
+  LOGOUT: "Logud"
+  BACK: "Tilbage"
+  NEXT: "Næste"
+  PREVIOUS: "Foregående"
+  ADD_PAGE: "Tilføj side"
+  MOVE: "Flyt"
+  DELETE: "Slet"
+  UNSET: "Ikke-indstillet"
+  VIEW: "Se"
+  SAVE: "Gem"
+  NORMAL: "Normal"
+  EXPERT: "Ekspert"
+  EXPAND_ALL: "Udfold alle"
+  COLLAPSE_ALL: "Sammenfold alle"
+  ERROR: "Fejl"
+  CLOSE: "Luk"
+  CANCEL: "Annullér"
+  CONTINUE: "Fortsæt"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Bekræftelse kræves"
+  MODAL_CHANGED_DETECTED_TITLE: "Ændringer registreret"
+  MODAL_CHANGED_DETECTED_DESC: "Ændringer er ikke gemt. Sikker på, at du vil fortsætte uden at gemme?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Bekræftelse kræves"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Sikker på, at du vil slette denne fil? Handlingen kan ikke fortrydes."
+  ADD_FILTERS: "Tilføj filtre"
+  SEARCH_PAGES: "Søg efter sider"
+  VERSION: "Version"
+  WAS_MADE_WITH: "Blev lavet med"
+  BY: "Af"
+  UPDATE_THEME: "Opdatere tema"
+  UPDATE_PLUGIN: "Opdater Plugin"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "af dette tema er nu tilgængelig"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "af dette plugin er nu tilgængelig"
+  AUTHOR: "Forfatter"
+  HOMEPAGE: "Websted"
+  DEMO: "Demo"
+  BUG_TRACKER: "Sporing af fejl"
+  KEYWORDS: "Nøgleord"
+  LICENSE: "Licens"
+  DESCRIPTION: "Beskrivelse"
+  README: "Læs-mig"
+  REMOVE_THEME: "Fjern Tema"
+  INSTALL_THEME: "Installer Tema"
+  THEME: "Tema"
+  BACK_TO_THEMES: "Tilbage til Temaer"
+  BACK_TO_PLUGINS: "Tilbage til Plugins"
+  CHECK_FOR_UPDATES: "Søg efter Opdateringer"
+  ADD: "Tilføj"
+  CLEAR_CACHE: "Ryd Cache"
+  CLEAR_CACHE_ALL_CACHE: "Alle Caches"
+  CLEAR_CACHE_ASSETS_ONLY: "Kun Aktiver"
+  CLEAR_CACHE_IMAGES_ONLY: "Kun Billeder"
+  CLEAR_CACHE_CACHE_ONLY: "Kun Cache"
+  CLEAR_CACHE_TMP_ONLY: "Kun tmp"
+  UPDATES_AVAILABLE: "Opdateringer tilgængelige"
+  DAYS: "Dage"
+  UPDATE: "Opdatér"
+  BACKUP: "Backup"
+  BACKUPS: "Sikkerhedskopier"
+  BACKUP_NOW: "Sikkerhedskopiér nu"
+  BACKUPS_STATS: "Sikkerhedskopieringsstatistik"
+  BACKUPS_HISTORY: "Sikkerhedskopieringshistorik"
+  BACKUPS_PURGE_CONFIG: "Opsætning, Sikkerhedskopieringsrensning"
+  BACKUPS_PROFILES: "Sikkerhedskopieringsprofiler"
+  BACKUPS_COUNT: "Antal Sikkerhedskopier"
+  BACKUPS_PROFILES_COUNT: "Antal Profiler"
+  BACKUPS_TOTAL_SIZE: "Forbrugt lagerplads"
+  BACKUPS_NEWEST: "Seneste sikkerhedskopi"
+  BACKUPS_OLDEST: "Ældste sikkerhedskopi"
+  BACKUPS_PURGE: "Rense"
+  BACKUPS_NOT_GENERATED: "Ingen sikkerhedskopier oprettet endnu..."
+  BACKUPS_PURGE_NUMBER: "Benytter %s af %s sikkerhedskopipladser"
+  BACKUPS_PURGE_TIME: "%s dages sikkerhedskopiering resterer"
+  BACKUPS_PURGE_SPACE: "Benytter %s af %s"
+  BACKUP_DELETED: "Sikkerhedskopi blev slettet"
+  BACKUP_NOT_FOUND: "Sikkerhedskopi ikke fundet"
+  BACKUP_DATE: "Sikkerhedskopieringsdato"
+  STATISTICS: "Statistikker"
+  TODAY: "I dag"
+  WEEK: "Uge"
+  MONTH: "Måned"
+  LATEST_PAGE_UPDATES: "Seneste Sideopdateringer"
+  MAINTENANCE: "Vedligeholdelse"
+  UPDATED: "Opdateret"
+  MON: "Man"
+  TUE: "Tirs"
+  WED: "Ons"
+  THU: "Tor"
+  FRI: "Fre"
+  SAT: "Lør"
+  SUN: "Søn"
+  COPY: "Kopiér"
+  EDIT: "Redigér"
+  CREATE: "Opret"
+  GRAV_ADMIN: "Grav Admin"
+  GRAV_OFFICIAL_PLUGIN: "Grav Officielt Plugin"
+  GRAV_OFFICIAL_THEME: "Grav Officielt Tema"
+  PLUGIN_SYMBOLICALLY_LINKED: "Dette plugin er symbolsk linket. Ændringer opdages ikke."
+  THEME_SYMBOLICALLY_LINKED: "Dette tema er symbolsk linket. Ændringer opdaget ikke"
+  REMOVE_PLUGIN: "Fjern Plugin"
+  INSTALL_PLUGIN: "Installér Plugin"
+  AVAILABLE: "Tilgængelig"
+  INSTALLED: "Installeret"
+  INSTALL: "Installér"
+  ACTIVE_THEME: "Aktivt tema"
+  SWITCHING_TO: "Skifte til"
+  SWITCHING_TO_DESCRIPTION: "Der er ingen garanti for, at alle layoutsider understøttes ved skift til andet tema, hvilket potentielt kan forårsage fejl under indlæsning af nævnte sider."
+  SWITCHING_TO_CONFIRMATION: "Vil du forsætte og skifte til temaet"
+  CREATE_NEW_USER: "Opret ny bruger"
+  REMOVE_USER: "Fjern bruger"
+  ACCESS_DENIED: "Adgang nægtet"
+  ACCOUNT_NOT_ADMIN: "din konto har ikke administratorrettigheder"
+  PHP_INFO: "PHP-info"
+  INSTALLER: "Installation"
+  AVAILABLE_THEMES: "Tilgængelige temaer"
+  AVAILABLE_PLUGINS: "Tilgængelige plugins"
+  INSTALLED_THEMES: "Installerede temaer"
+  INSTALLED_PLUGINS: "Installerede plugins"
+  BROWSE_ERROR_LOGS: "Gennemse Fejl-logs"
+  SITE: "Site"
+  INFO: "Info"
+  SYSTEM: "System"
+  USER: "Bruger"
+  ADD_ACCOUNT: "Tilføj konto"
+  SWITCH_LANGUAGE: "Skift Sprog"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Plugin aktiveret"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Plugin deaktiveret"
+  SUCCESSFULLY_CHANGED_THEME: "Standardtema ændret"
+  INSTALLATION_FAILED: "Installation mislykkedes"
+  INSTALLATION_SUCCESSFUL: "Installation fuldført"
+  UNINSTALL_FAILED: "Afinstallation mislykkedes"
+  UNINSTALL_SUCCESSFUL: "Afinstallation fuldført"
+  SUCCESSFULLY_SAVED: "Gemt"
+  SUCCESSFULLY_COPIED: "Kopieret"
+  REORDERING_WAS_SUCCESSFUL: "Rækkefølge ændret"
+  SUCCESSFULLY_DELETED: "Slettet"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Sprog skiftet"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "Du har ikke tilstrækkelige tilladelser til opgaven"
+  CACHE_CLEARED: "Cache ryddet"
+  METHOD: "Metode"
+  ERROR_CLEARING_CACHE: "Fejl under sletning af cache"
+  AN_ERROR_OCCURRED: "Der opstod en fejl"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Din backup er klar til download"
+  DOWNLOAD_BACKUP: "Download backup"
+  PAGES_FILTERED: "Filtrerede sider"
+  NO_PAGE_FOUND: "Ingen side fundet"
+  INVALID_PARAMETERS: "Ugyldige parametre"
+  NO_FILES_SENT: "Ingen filer sendt"
+  EXCEEDED_FILESIZE_LIMIT: "Størrelsesbegrænsning for PHP-configurationsfil overskredet"
+  UNKNOWN_ERRORS: "Ukendt fejl"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Størrelsesbegrænsning for Grav-configurationsfil overskredet"
+  UNSUPPORTED_FILE_TYPE: "Ikke-understøttet filtype"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Kunne ikke flytte den uploadede fil."
+  FILE_UPLOADED_SUCCESSFULLY: "Fil-upload lykkedes"
+  FILE_DELETED: "Filen blev slettet"
+  FILE_COULD_NOT_BE_DELETED: "Filen kunne ikke slettes"
+  FILE_NOT_FOUND: "Filen blev ikke fundet"
+  NO_FILE_FOUND: "Ingen fil blev fundet"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav blev opdateret til"
+  GRAV_UPDATE_FAILED: "Opdatering af Grav mislykkedes"
+  EVERYTHING_UPDATED: "Alt opdateret"
+  UPDATES_FAILED: "Opdateringer mislykkedes"
+  AVATAR_BY: "Avatar af"
+  AVATAR_UPLOAD_OWN: "Eller upload din egen..."
+  LAST_BACKUP: "Seneste backup"
+  FULL_NAME: "Fulde navn"
+  USERNAME: "Brugernavn"
+  EMAIL: "E-mail"
+  USERNAME_EMAIL: "Brugernavn eller e-mail"
+  PASSWORD: "Adgangskode"
+  PASSWORD_CONFIRM: "Bekræft adgangskode"
+  TITLE: "Titel"
+  LANGUAGE: "Sprog"
+  ACCOUNT: "Konto"
+  EMAIL_VALIDATION_MESSAGE: "Skal være en gyldig e-mail"
+  PASSWORD_VALIDATION_MESSAGE: "Adgangskode skal indeholde minimum ét ciffer, én versal, én minuskel samt udgøres af minimum otte tegn"
+  LANGUAGE_HELP: "Opsæt foretrukne sprog"
+  MEDIA: "Medier"
+  DEFAULTS: "Standarder"
+  SITE_TITLE: "Sitets Titel"
+  SITE_TITLE_PLACEHOLDER: "Overordnet webstedstitel"
+  SITE_TITLE_HELP: "Sitets standard-titel. Bruges ofte i temaer"
+  SITE_DEFAULT_LANG: "Standardsprog"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Standardsprog benyttet af temaets <HTML>-etiket"
+  SITE_DEFAULT_LANG_HELP: "Standardsprog benyttet af temaets <HTML>-etiket"
+  DEFAULT_AUTHOR: "Standard-forfatter"
+  DEFAULT_AUTHOR_HELP: "Standard-forfatternavn. Bruges ofte i temaer og sideindhold"
+  DEFAULT_EMAIL: "Standard-e-mail"
+  DEFAULT_EMAIL_HELP: "En standard-e-mail, som der kan henvises til i temaer og på sider"
+  TAXONOMY_TYPES: "Taksonomi-typer"
+  TAXONOMY_TYPES_HELP: "Taksonomi-typer skal defineres her hvis du vil bruge dem på sider"
+  PAGE_SUMMARY: "Side-resume"
+  ENABLED: "Aktiveret"
+  ENABLED_HELP: "Aktiver side-resume (resumeet returnerer det samme som sideindholdet)"
+  'YES': "Ja"
+  'NO': "Nej"
+  SUMMARY_SIZE: "Resume-længde"
+  SUMMARY_SIZE_HELP: "Det antal af en sides karakter, som skal bruges som resume"
+  FORMAT: "Format"
+  FORMAT_HELP: "kort = brug første forekomst af afgrænser eller størrelse; lang = afgrænser ignoreres"
+  SHORT: "Kort"
+  LONG: "Lang"
+  DELIMITER: "Afgrænser"
+  DELIMITER_HELP: "Resume-afgrænseren (standard: '===')"
+  METADATA: "Metadata"
+  METADATA_HELP: "Standard-metadataværdier, som vises på alle sider, medmindre de tilsidesættes af siden"
+  NAME: "Navn"
+  CONTENT: "Indhold"
+  SIZE: "Størrelse"
+  ACTION: "Handling"
+  REDIRECTS_AND_ROUTES: "Omdirigeringer og ruter"
+  CUSTOM_REDIRECTS: "Brugerdefinerede omdirigeringer"
+  CUSTOM_REDIRECTS_HELP: "ruter til omdirigering til andre sider. Gængse regex-erstatninger kan benyttes"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/dit/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/din/omdirigering"
+  CUSTOM_ROUTES: "Tilpassede ruter"
+  CUSTOM_ROUTES_HELP: "ruter til alias til andre sider. Gængs regex-erstatning er gyldig"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/dit/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/din/rute"
+  FILE_STREAMS: "Filstrømme"
+  DEFAULT: "Standard"
+  PAGE_MEDIA: "Sidemedier"
+  OPTIONS: "Indstillinger"
+  PUBLISHED: "Udgivet"
+  PUBLISHED_HELP: "Som standard offentliggøres en side medmindre du udtrykkeligt sætter published: false eller via en fremtidig publish_date eller fortidig unpublish_date"
+  DATE: "Dato"
+  DATE_HELP: "Dato-variablen tillader dig at sætte en specifik dato associeret med denne side."
+  PUBLISHED_DATE: "Udgivelsesdato"
+  PUBLISHED_DATE_HELP: "Kan give en dato for automatisk at udløse udgivelse."
+  UNPUBLISHED_DATE: "Afpubliceringsdato"
+  UNPUBLISHED_DATE_HELP: "Opsætning en dato for automatisk at afpublisere."
+  ROBOTS: "Robotter"
+  TAXONOMIES: "Taksonomier"
+  TAXONOMY: "Taksonomi"
+  ADVANCED: "Avanceret"
+  SETTINGS: "Indstillinger"
+  FOLDER_NUMERIC_PREFIX: "Numerisk Mappepræfiks"
+  FOLDER_NUMERIC_PREFIX_HELP: "Numerisk præfiks, der muliggør manuel sortering og indebærer synlighed"
+  FOLDER_NAME: "Mappenavn"
+  FOLDER_NAME_HELP: "Mappenavnet, som skal gemmes i filsystemet, for denne side"
+  PARENT: "Overordnet"
+  DEFAULT_OPTION_ROOT: "- Rod -"
+  DEFAULT_OPTION_SELECT: "- Vælg -"
+  DISPLAY_TEMPLATE: "Visningsskabelon"
+  DISPLAY_TEMPLATE_HELP: "Sidetypen, som oversættes til den twig-template, som viser siden"
+  ORDERING: "Soreting"
+  PAGE_ORDER: "Side-sortering"
+  OVERRIDES: "Tilsidesættelser"
+  MENU: "Menu"
+  MENU_HELP: "Den streng, som skal anvendes i en menu. Hvis ingen angives, bruges Title."
+  SLUG: "Slug"
+  SLUG_HELP: "Slug-variablen bruges til specifikt at angive sidens andel af URL'en"
+  SLUG_VALIDATE_MESSAGE: "En slug må kun indeholde små alfanumeriske karakterer og bindestreger"
+  PROCESS: "Processer"
+  PROCESS_HELP: "Kontroller hvordan sider processeres. Kan angives per side i stedet for globalt"
+  DEFAULT_CHILD_TYPE: "Standardtitel for underordnede"
+  USE_GLOBAL: "Anvend Global"
+  ROUTABLE: "Kan routes"
+  ROUTABLE_HELP: "Om denne side kan tilgås via en URL"
+  CACHING: "Caching"
+  VISIBLE: "Synlig"
+  VISIBLE_HELP: "Afgør om en side er synlig i navigationen."
+  DISABLED: "Deaktiveret"
+  ITEMS: "Elementer"
+  ORDER_BY: "Sortér Efter"
+  ORDER: "Sortering"
+  FOLDER: "Mappe"
+  ASCENDING: "Stigende"
+  DESCENDING: "Faldende"
+  PAGE_TITLE: "Sidetitel"
+  PAGE_TITLE_HELP: "Sidens titel"
+  PAGE: "Side"
+  FRONTMATTER: "Forskræp"
+  FILENAME: "Filnavn"
+  PARENT_PAGE: "Overordnet Side"
+  HOME_PAGE: "Hjem-side"
+  HOME_PAGE_HELP: "Siden som Grav bruger som standard-destinationsside"
+  DEFAULT_THEME: "Standardtema"
+  DEFAULT_THEME_HELP: "Gravs standardtema (som udgangspunkt Antimatter)"
+  TIMEZONE: "Tidszone"
+  TIMEZONE_HELP: "Tilsidesæt serverens standardtidszone"
+  SHORT_DATE_FORMAT: "Kort datoformat-visning"
+  SHORT_DATE_FORMAT_HELP: "Angiv det korte datoformat, der kan bruges af temaer"
+  LONG_DATE_FORMAT: "Lang datoformat-visning"
+  LONG_DATE_FORMAT_HELP: "Angiv det lange datoformat, der kan bruges af temaer"
+  DEFAULT_ORDERING: "Standardsortering"
+  DEFAULT_ORDERING_HELP: "Sider i en liste bliver vist i denne rækkefølge, medmindre den tilsidesættes"
+  DEFAULT_ORDERING_DEFAULT: "Standard - baseret på mappenavn"
+  DEFAULT_ORDERING_FOLDER: "Mappe - baseret på mappenavn uden præfiks"
+  DEFAULT_ORDERING_TITLE: "Titel - baseret på titel-felt i headeren"
+  DEFAULT_ORDERING_DATE: "Dato - baseret på dato-felt i headeren"
+  DEFAULT_ORDER_DIRECTION: "Standard-sorteringsretning"
+  DEFAULT_ORDER_DIRECTION_HELP: "Den retning, som siderne i en liste skal sorteres i"
+  DEFAULT_PAGE_COUNT: "Standard-sideantal"
+  DEFAULT_PAGE_COUNT_HELP: "Standard for maksimalt antal sider i en liste"
+  DATE_BASED_PUBLISHING: "Datobaseret publicering"
+  DATE_BASED_PUBLISHING_HELP: "(Af)publicer automatisk indhold baseret på dets dato"
+  EVENTS: "Begivenheder"
+  EVENTS_HELP: "Aktiver og deaktiver specifikke begivenheder. Deaktivering af disse kan ødelægge plugins"
+  REDIRECT_DEFAULT_ROUTE: "Standardrute for omdirigeringer"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Omdiriger automatisk til en sides standardrute"
+  LANGUAGES: "Sprog"
+  SUPPORTED: "Understøttet"
+  SUPPORTED_HELP: "Kommasepareret liste af sprogkoder på hver to bogstaver (f.eks. 'en,da,fr')"
+  TRANSLATIONS_FALLBACK: "Fallback til oversættelser"
+  TRANSLATIONS_FALLBACK_HELP: "Fallback gennem understøttede oversættelser hvis det aktive sprog ikke findes"
+  ACTIVE_LANGUAGE_IN_SESSION: "Aktivt sprog i session"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Gem det aktive sprog i sessionen"
+  HTTP_HEADERS: "HTTP-headere"
+  EXPIRES: "Udløber"
+  EXPIRES_HELP: "Indstiller expires-headeren. Værdien angives i sekunder."
+  CACHE_CONTROL: "HTTP Cache-kontrol"
+  CACHE_CONTROL_HELP: "Angive en gyldig cache-kontrolværdi såsom 'no-cache, no-butik, must-revalidate'"
+  LAST_MODIFIED: "Senest ændret"
+  LAST_MODIFIED_HELP: "Angiver \"last modified\"-headeren, der kan hjælpe med at optimere proxy og browsercachelagring"
+  ETAG: "ETag"
+  ETAG_HELP: "Indstiller etag-header for at hjælpe med at identificere, hvornår en side er blevet ændret"
+  VARY_ACCEPT_ENCODING: "Vary accept encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Angiver \"Vary: Accept Encoding\"-headeren, der hjælper med proxy og CDN-cachelagring"
+  MARKDOWN: "Markdown"
+  MARKDOWN_EXTRA: "Markdown Extra"
+  MARKDOWN_EXTRA_HELP: "Aktiver standardunderstøttelse for Markdown Extra - https://michelf.ca/projects/php-markdown/extra/"
+  MARKDOWN_EXTRA_ESCAPE_FENCES: "Escape HTML-elementer i Markdown Extra-låger"
+  MARKDOWN_EXTRA_ESCAPE_FENCES_HELP: "Escaper HTML-elementer i Markdown Extra-låger"
+  AUTO_LINE_BREAKS: "Automatiske linjeskift"
+  AUTO_LINE_BREAKS_HELP: "Aktiver understøttelse af automatiske linjeskift i markdown"
+  AUTO_URL_LINKS: "Automatiske URL-links"
+  AUTO_URL_LINKS_HELP: "Aktiver automatisk konvertering af URL'er til HTML-hyperlinks"
+  ESCAPE_MARKUP: "Escape markup"
+  ESCAPE_MARKUP_HELP: "Escape markup-tags til HMTL-entiteter"
+  CACHING_HELP: "Global TÆND/SLUK-kontakt til aktivering/deaktivering af Grav-caching"
+  CACHE_CHECK_METHOD: "Metode til cache-tjek"
+  CACHE_CHECK_METHOD_HELP: "Angiv den metode, som Grav skal bruge til at tjekke om en side er blevet ændret."
+  CACHE_DRIVER: "Cache-driver"
+  CACHE_DRIVER_HELP: "Angiv hvilken cache-driver, som Grav skal bruge. 'Auto-detekter' forsøger at finde den bedste for dig"
+  CACHE_PREFIX: "Cache-præfiks"
+  CACHE_PREFIX_HELP: "Identifikator for en del af Grav-nøglen.  Ændr kun hvis du ved, hvad du laver."
+  CACHE_PREFIX_PLACEHOLDER: "Afledt af base-URL'en (tilsidesæt ved at indtaste en arbitrær streng)"
+  CACHE_PURGE_JOB: "Kør planlagt rensningsjob"
+  CACHE_PURGE_JOB_HELP: "Du kan med planlæggeren periodisk rydde ud gamle Doctrine-filcachemapper med dette job"
+  CACHE_CLEAR_JOB: "Kør planlagt rydningsjob"
+  CACHE_CLEAR_JOB_HELP: "Du kan med planlæggeren periodisk rydde Grav-cachen"
+  CACHE_JOB_TYPE: "Cache Jobtype"
+  CACHE_JOB_TYPE_HELP: "Ryd cachen enten for 'standard' eller 'alle' mapper"
+  CACHE_PURGE: "Rens gammel cache"
+  LIFETIME: "Livstid"
+  LIFETIME_HELP: "Angiver cache-livstiden i sekunder. 0 = uendelig"
+  GZIP_COMPRESSION: "Gzip-kompression"
+  GZIP_COMPRESSION_HELP: "Aktiver GZip komprimering af Grav siden for øget ydeevne."
+  TWIG_TEMPLATING: "Twig-skabeloner"
+  TWIG_CACHING: "Twig-cache"
+  TWIG_CACHING_HELP: "Indstil Twigs cache-mekanisme. Lad feltet være aktiveret for at opnå den bedste ydeevne."
+  TWIG_DEBUG: "Twig-debug"
+  TWIG_DEBUG_HELP: "Giver mulighed for ikke at indlæse Twigs debugger-udvidelse"
+  DETECT_CHANGES: "Registrer ændringer"
+  DETECT_CHANGES_HELP: "Twig genkompilerer automatisk sin cache hvis den registrerer ændringer i Twig-skabeloner"
+  AUTOESCAPE_VARIABLES: "Escape automatisk variabler"
+  AUTOESCAPE_VARIABLES_HELP: "Escaper automatisk alle variabler. Dette vil højst sandsynligt ødelægge dit site"
+  ASSETS: "Aktiver"
+  CSS_PIPELINE: "CSS-pipeline"
+  CSS_PIPELINE_HELP: "CSS-pipelinen forener flere CSS-ressourcer i én fil"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Inkluder eksterne enheder i CSS pipelinen"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "Eksterne URL'er har, til tider, relative fil referencer, og bør ikke blive pipelinet"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "Pre-rendering af CSS pipelinen"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Render CSS pipelinen før alle andre CSS referencer, der ikke er inkluderet"
+  CSS_MINIFY: "CSS-minificering"
+  CSS_MINIFY_HELP: "Minificer CSS ved brug af pipeline"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "Tilsidesæt Windows' CSS-minificering"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Tilsidesættelse af minificering på Windows-platforme. Standard = Fra pga. ThreadStackSize"
+  CSS_REWRITE: "CSS-omskrivning"
+  CSS_REWRITE_HELP: "Omskriv relative CSS-URL'er under pipelining"
+  JAVASCRIPT_PIPELINE: "Javascript-pipeline"
+  JAVASCRIPT_PIPELINE_HELP: "JS-pipelinen samler flere JS-ressourcer i én fil"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Inkluder eksterne enheder i JS pipelinen"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Eksterne URL'er har, til tider, relative fil referencer, og bør ikke blive pipelinet"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Pre-rendering af JS pipelinen"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Render JS pipelinen før alle andre JS referencer, der ikke er inkluderet"
+  JAVASCRIPT_MINIFY: "Javascript-minificering"
+  JAVASCRIPT_MINIFY_HELP: "Minificer JS ved brug af pipeline"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Aktiver tidsstempler på aktiver"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Aktiver tidsstempler på aktiver"
+  COLLECTIONS: "Kollektioner"
+  ERROR_HANDLER: "Fejlbehandler"
+  DISPLAY_ERRORS: "Visningsfejl"
+  DISPLAY_ERRORS_HELP: "Vis fuld backtrace på fejlside"
+  LOG_ERRORS: "Log fejl"
+  LOG_ERRORS_HELP: "Log fejl i mappen /logs"
+  LOG_HANDLER: "Loghåndtering"
+  LOG_HANDLER_HELP: "Hvor loggerne skal placeres"
+  SYSLOG_FACILITY: "Syslog-facilitet"
+  SYSLOG_FACILITY_HELP: "Syslog-facilitet til output"
+  DEBUGGER: "Fejlfinder"
+  DEBUGGER_HELP: "Aktivér Grav-fejlfinder og flg. indstillinger"
+  DEBUG_TWIG: "Fejlfind i Twig"
+  DEBUG_TWIG_HELP: "Aktivér fejlfinding i Twig-skabeloner"
+  SHUTDOWN_CLOSE_CONNECTION: "Luk forbindelse ved nedlukning"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Luk forbindelsen inden onShutdown()-kald. Slået fra ved debugging"
+  DEFAULT_IMAGE_QUALITY: "Standard-billedkvalitet"
+  DEFAULT_IMAGE_QUALITY_HELP: "Standardbilledkvalitet til brug ved cachelagring af billeder (85%)"
+  CACHE_ALL: "Cache alle billeder"
+  CACHE_ALL_HELP: "Kør alle billeder - også ikke-manipulerede - gennem Gravs cache-system"
+  IMAGES_DEBUG: "Vandmærke ved debugging af billeder"
+  IMAGES_DEBUG_HELP: "Vis overlejring på billeder. Overlejringen indikerer billedets pixel-dybde til brug ved f.eks. visning på retina-skærme"
+  IMAGES_SEOFRIENDLY: "SEO-venlige billednavne"
+  IMAGES_SEOFRIENDLY_HELP: "Når aktiveret, vises billednavnet først, dernæst en mindre hash, der afspejler udførte operationer"
+  UPLOAD_LIMIT: "Fil-upload grænse"
+  UPLOAD_LIMIT_HELP: "Angiv maks. filstørrelse i bytes (0 = ubegrænset)"
+  ENABLE_MEDIA_TIMESTAMP: "Aktivér tidsstempler på medier"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Føjer et tidsstempel baseret på seneste ændringsdato til hvert medieemne"
+  SESSION: "Session"
+  SESSION_ENABLED_HELP: "Aktivér sessionsunderstøttelse i Grav"
+  SESSION_NAME_HELP: "En identifikator, der benytts til at danne navnet på sessionens-cookie'en"
+  SESSION_UNIQUENESS: "Unik streng"
+  SESSION_UNIQUENESS_HELP: "MD5-hash af Gravs rodsti, dvs. `GRAV_ROOT` (standard) eller én baseret på den tilfældige` security.salt`-streng."
+  ABSOLUTE_URLS: "Absolutte URL'er"
+  ABSOLUTE_URLS_HELP: "Absolutte eller relative URL'er til 'base_url'"
+  PARAMETER_SEPARATOR: "Parameterseparator"
+  PARAMETER_SEPARATOR_HELP: "Separator til godkendte parametre, som kan ændres til Apache på Windows"
+  TASK_COMPLETED: "Opgave fuldført"
+  EVERYTHING_UP_TO_DATE: "Alt er opdateret"
+  UPDATES_ARE_AVAILABLE: "opdatering(-er) er tilgængelige"
+  IS_AVAILABLE_FOR_UPDATE: "er tilgængelig for opdatering"
+  IS_NOW_AVAILABLE: "er nu tilgængelig"
+  CURRENT: "Aktuel"
+  UPDATE_GRAV_NOW: "Opdater Grav nu"
+  GRAV_SYMBOLICALLY_LINKED: "Grav er forbundet med symbolsk link. Opgraderingen vil ikke være tilgængelig"
+  UPDATING_PLEASE_WAIT: "Opdatering... Vent venligst; downloader"
+  OF_THIS: "af denne"
+  OF_YOUR: "af dit"
+  HAVE_AN_UPDATE_AVAILABLE: "har en tilgængelig opdatering"
+  SAVE_AS: "Gem som"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Sikker på, at du vil slette denne side og alle dens undersider? Er siden oversat til andre sprog, vil disse oversættelser blive bevaret og skal slettes separat. I modsat fald bliver sidemappen og dens undersider slettet. Denne handling er irreversibel."
+  AND: "og"
+  UPDATE_AVAILABLE: "Opdatering tilgængelig"
+  METADATA_KEY: "Nøgle (f.eks. 'Nøgleord')"
+  METADATA_VALUE: "Værdi (f.eks. 'Blog, Grav')"
+  USERNAME_HELP: "Brugernavnet skal være mellem 3 og 16 karakterer og kan indeholde små bogstaver, tal, understreger og bindestreger. Store bogstaver, mellemrum og specielle karakter er ikke tilladt"
+  FULLY_UPDATED: "Fuldt opdateret"
+  SAVE_LOCATION: "Gemmeplacering"
+  PAGE_FILE: "Sideskabelon"
+  PAGE_FILE_HELP: "Sideskabelon filnavn, og standard visnings-skabelonen for denne side"
+  NO_USER_ACCOUNTS: "Der blev ikke fundet nogen brugerkonto; opret venligst en først..."
+  NO_USER_EXISTS: "Ingen lokalbruger til denne konto, kan ikke gemme..."
+  REDIRECT_TRAILING_SLASH: "Omdiriger efterstillet skråstreg"
+  REDIRECT_TRAILING_SLASH_HELP: "Udfør en 301-omdirigering i stedet for gennemsigtigt at håndtere URI'er med efterstillet skråstreg."
+  DEFAULT_DATE_FORMAT: "Sidedatoformat"
+  DEFAULT_DATE_FORMAT_HELP: "Det side-datoformat, som Grav skal bruge. Som standard forsøger Grav at gætte dit datoformat, men du kan specificeret et format med PHP's dato-syntaks (f.eks. Y-m-d H:i)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Gæt automatisk"
+  IGNORE_FILES: "Ignorer filer"
+  IGNORE_FILES_HELP: "Specifikke filer, som skal ignoreres ved behandlingen af sider"
+  IGNORE_FOLDERS: "Ignorer mapper"
+  IGNORE_FOLDERS_HELP: "Specifikke mapper, som skal ignoreres ved behandlingen af sider"
+  HIDE_EMPTY_FOLDERS: "Skjul tomme mapper"
+  HIDE_EMPTY_FOLDERS_HELP: "Har mappen ikke en .md-fil, skal den være skjult i navigationen og være ikke-rootbar"
+  HTTP_ACCEPT_LANGUAGE: "Indstil sprog fra browser"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Du kan forsøge at indstille sproget på basis af browserens \"http_accept_language\"-headertag"
+  OVERRIDE_LOCALE: "Tilsidesæt lokation"
+  OVERRIDE_LOCALE_HELP: "Tilsidesæt PHP's \"locale\"-indstilling baseret på det nuværende sprog"
+  REDIRECT: "Side-omdirigering"
+  REDIRECT_HELP: "Angiv en side-rute eller ekstern URL, som denne side skal omdirigere til, f.eks. \"/en/rute\" eller \"http://etsite.com\""
+  PLUGIN_STATUS: "Plugin-status"
+  INCLUDE_DEFAULT_LANG: "Inkluder standardsproget"
+  INCLUDE_DEFAULT_LANG_HELP: "Dette foranstiller standardsproget i alle standardsprogets URL'er, f.eks. \"/da/blog/mit-indlæg\""
+  PAGES_FALLBACK_ONLY: "Kun tilbagefaldsside"
+  PAGES_FALLBACK_ONLY_HELP: "Kun 'fallback' for at finde sideindhold via understøttede sprog. Standardadfærd er at vise et hvilket som helst tilgængeligt sprog, såfremt det aktivt sprog mangler"
+  ALLOW_URL_TAXONOMY_FILTERS: "URL-taksonomifiltre"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Sidebaserede samlingergør det muligt at filtrere via \"/taxonomy:værdi\"."
+  REDIRECT_DEFAULT_CODE: "Standardomdirigeringskode"
+  REDIRECT_DEFAULT_CODE_HELP: "HTTP-statuskode til brug ved omdirigeringer"
+  IGNORE_HIDDEN: "Ignorer skjulte"
+  IGNORE_HIDDEN_HELP: "Ignorer alle filer og mapper, der begynder med punktum"
+  WRAPPED_SITE: "Indpakket site"
+  WRAPPED_SITE_HELP: "Bruges af temaer/plugins til at bestemme, om Grav er pakket ind i en anden platform"
+  FALLBACK_TYPES: "Tilladte fallback-typer"
+  FALLBACK_TYPES_HELP: "Tilladte filtyper, der kan tilgås med en side-rute. Som standard sat til alle understøttede medietyper."
+  INLINE_TYPES: "Indlejrede fallback-typer"
+  INLINE_TYPES_HELP: "En liste over filtyper, der skal vises indlejret i stedet for hentes"
+  APPEND_URL_EXT: "Tilføj URL-efternavn"
+  APPEND_URL_EXT_HELP: "Tilføjer et brugerdefineret efternavn til sidens webadresse. Bemærk, at dette vil få Grav til at kigge efter \"<skabelon>. <efternavn>. twig\"-skabeloner"
+  PAGE_MODES: "Side-tilstande"
+  PAGE_TYPES: "Sidetyper"
+  PAGE_TYPES_HELP: "Bestemmer de sidetyper, som Grav understøtter, og rækkefølgen bestemmer fallback-typen, der skal benyttes, såfremt anmodningen er tvetydig"
+  ACCESS_LEVELS: "Adgangsniveauer"
+  GROUPS: "Grupper"
+  GROUPS_HELP: "Liste af grupper, som brugeren er medlem af"
+  ADMIN_ACCESS: "Admin-adgang"
+  SITE_ACCESS: "Webstedsadgang"
+  INVALID_SECURITY_TOKEN: "Ugyldigt sikkerhedstoken"
+  ACTIVATE: "Aktivér"
+  TWIG_UMASK_FIX: "Afmaskeringsfiks"
+  TWIG_UMASK_FIX_HELP: "Som standard opretter Twig cachede filer som 0755. Dette fiks skifter dette til 0775"
+  CACHE_PERMS: "Cachetilladelser"
+  CACHE_PERMS_HELP: "Standard cachemappetilladelser. Som regel 0755 eller 0775, afhængig af opsætningen"
+  REMOVE_SUCCESSFUL: "Fjernet"
+  REMOVE_FAILED: "Fjernelse mislykkedes"
+  HIDE_HOME_IN_URLS: "Skjul hjem-route i URL'er"
+  HIDE_HOME_IN_URLS_HELP: "Sikrer at standard-routes for enhver side under hjem ikke henviser til hjemsidens normale route"
+  TWIG_FIRST: "Behandl Twig først"
+  TWIG_FIRST_HELP: "Har du aktiveret Twig-sidebehandling, kan du opsætte Twig til at gøre dette før eller efter markdown"
+  SESSION_SECURE: "Sikker"
+  SESSION_SECURE_HELP: "Angiver, om kommunikation med denne cookie skal ske krypteret. ADVARSEL: Brug kun dette på sites, der kører udelukkende på HTTPS"
+  SESSION_HTTPONLY: "Kun HTTP"
+  SESSION_HTTPONLY_HELP: "Hvis denne indstilling er aktiveret bruges cookes kun over HTTP og Javascript-ændringer tillades ikke"
+  REVERSE_PROXY: "Omvendt proxy"
+  REVERSE_PROXY_HELP: "Aktiver denne hvis du er bag en omvendt proxy og har problemer med URL'er, der indeholder ukorrekte porte"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Ugyldigt forskræp; kunne ikke gemme"
+  ADD_FOLDER: "Tilføj mappe"
+  COPY_PAGE: "Kopiér side"
+  PROXY_URL: "Proxy-URL"
+  PROXY_URL_HELP: "Indtast proxyens host eller IP og port"
+  NOTHING_TO_SAVE: "Intet at gemme"
+  FILE_ERROR_ADD: "Der opstod en fejl under forsøg på at tilføje filen"
+  FILE_ERROR_UPLOAD: "Der opstod en fejl under forsøget på at uploade filer"
+  FILE_UNSUPPORTED: "Ikke-understøttet filtype"
+  ADD_ITEM: "Tilføj element"
+  FILE_TOO_LARGE: "Filen er for stor til at blive uploadet, maksimal tilladt er %s ifølge <br>dine PHP-indstillinger. Øge din 'post_max_size' PHP indstilling"
+  INSTALLING: "Installerer"
+  LOADING: "Indlæser.."
+  DEPENDENCIES_NOT_MET_MESSAGE: "De følgende afhængigheder skal være opfyldt først:"
+  ERROR_INSTALLING_PACKAGES: "Fejl under installering af af pakke(r)"
+  INSTALLING_DEPENDENCIES: "Installerer afhængigheder..."
+  INSTALLING_PACKAGES: "Installerer pakke(r).."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Pakke(r) installeret korrekt."
+  READY_TO_INSTALL_PACKAGES: "Klar til at installere pakke(r)"
+  PACKAGES_NOT_INSTALLED: "Pakker ikke installeret"
+  PACKAGES_NEED_UPDATE: "Pakker allerede installeret, men forældede"
+  PACKAGES_SUGGESTED_UPDATE: "Pakker allerede installeret, og versionen er i orden, men vil blive opdateret for at holde dig up to date"
+  REMOVE_THE: "Fjern %s"
+  CONFIRM_REMOVAL: "Er du sikker på, at du ønsker at slette %s?"
+  REMOVED_SUCCESSFULLY: "%s fjernet korrekt"
+  ERROR_REMOVING_THE: "Fejl i forsøget på at fjerne %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "%s krævede følgende afhængigheder, som ikke er påkrævet af andre pakker. Hvis du ikke bruger dem, kan du fjerne dem her."
+  READY_TO_UPDATE_PACKAGES: "Klar til at opdatere pakke(r)"
+  ERROR_UPDATING_PACKAGES: "Fejl under opdatering af pakke(r)"
+  UPDATING_PACKAGES: "Opdaterer pakke(r).."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Pakke(r) opdateret korrekt."
+  UPDATING: "Updaterer"
+  GPM_RELEASES: "GPM udgivelser"
+  GPM_RELEASES_HELP: "Vælg 'Test' for at installere beta eller testversioner"
+  GPM_METHOD: "Fjernhentningsmetode"
+  GPM_METHOD_HELP: "Når indstillet til Auto afgør Grav, om fopen er tilgængelig og benytter den, ellers benyttes cURL. For at gennemtvinge brugen af den ene eller den, skift indstillingen."
+  GPM_VERIFY_PEER: "Fjerngodkend Peer (SSL)"
+  GPM_VERIFY_PEER_HELP: "Visse udbydere synes at svigte med at verificere getgrav.org SSL-certifikat, hvilket betyder, at GPM ikke vil fungere. Er dette tilfældet for dig, vil deaktivering af denne indstilling måske hjælpe"
+  AUTO: "Auto"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stabil"
+  TESTING: "Tester"
+  FRONTMATTER_PROCESS_TWIG: "Processer frontmatter Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "Når aktiveret kan du bruge Twig's konfigurationsvariabler i sidens frontmatter"
+  FRONTMATTER_IGNORE_FIELDS: "Ignorer frontmatter felter"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Visse frontmatter felter kan indeholde Twig, men skal ikke behandles. Så som \"formularer\""
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Pakke %s installeret"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Overordnet rækkefølge-indstilling, orden de-aktiveret"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Siden er ikke synlig, orden de-aktiveret"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Sortering af rækkefølge via administrationen er ikke tilgængelig, fordi der er mere end 200 under-elementer"
+  ORDERING_DISABLED_BECAUSE_PAGE_NO_PREFIX: "Sidebestilling er deaktiveret for denne side, da <strong>Numerisk Mappenpræfiks</strong> ikke er aktiveret"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "OBS: Du kan ikke tilføje medie-filer før du har gemt siden. Klik på \"Gem\" øverst"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "OBS: Siden skal gemmes før du kan tilføje filer til den."
+  DROP_FILES_HERE_TO_UPLOAD: "Smid dine filer her eller, <strong>Klik i dette område</strong>"
+  INSERT: "Indsæt"
+  UNDO: "Fortryd"
+  REDO: "Gendan"
+  HEADERS: "Overskrifter"
+  BOLD: "Fed"
+  ITALIC: "Kursiv"
+  STRIKETHROUGH: "Gennemstreget"
+  SUMMARY_DELIMITER: "Sammendragsafgrænser"
+  LINK: "Link"
+  IMAGE: "Billede"
+  BLOCKQUOTE: "Citatblok"
+  UNORDERED_LIST: "Usorteret liste"
+  ORDERED_LIST: "Sorteret liste"
+  EDITOR: "Editor"
+  PREVIEW: "Eksempelvisning"
+  FULLSCREEN: "Fuld skærm"
+  NON_ROUTABLE: "Ikke tilgængelig"
+  NON_VISIBLE: "Ikke-synlig"
+  NON_PUBLISHED: "Ikke udgivet"
+  CHARACTERS: "tegn"
+  PUBLISHING: "Udgivelse"
+  MEDIA_TYPES: "Medietyper"
+  IMAGE_OPTIONS: "Billedmuligheder"
+  MIME_TYPE: "MIME-type"
+  THUMB: "Miniature"
+  TYPE: "Type"
+  FILE_EXTENSION: "Filendelse"
+  LEGEND: "Sideoversigt"
+  MEMCACHE_SERVER: "Memcache-server"
+  MEMCACHE_SERVER_HELP: "Memcache-serveradressen"
+  MEMCACHE_PORT: "Memcache-port"
+  MEMCACHE_PORT_HELP: "Memcache-serverporten"
+  MEMCACHED_SERVER: "Memcachet-server"
+  MEMCACHED_SERVER_HELP: "Memcachet-serveradresse"
+  MEMCACHED_PORT: "Memcache-port"
+  MEMCACHED_PORT_HELP: "Memcachet-serverport"
+  REDIS_SERVER: "Redis-server"
+  REDIS_SERVER_HELP: "Redis-serveradressen"
+  REDIS_PORT: "Redis-port"
+  REDIS_PORT_HELP: "Redis-serverport"
+  REDIS_PASSWORD: "Redis-adgangskode/hemmelighed"
+  ALL: "Alle"
+  FROM: "fra"
+  TO: "til"
+  RELEASE_DATE: "Udgivelsesdato"
+  SORT_BY: "Sortér efter"
+  RESOURCE_FILTER: "Filtrér..."
+  FORCE_SSL: "Gennemtving SSL"
+  FORCE_SSL_HELP: "Gennemtving SSL globalt, hvis aktiveret, når webstedet nås via HTTP, Grav sender en omdirigering til HTTPS-siden"
+  NEWS_FEED: "Nyhedsfeed"
+  EXTERNAL_URL: "Ekstern URL"
+  CUSTOM_BASE_URL: "Tilpasset basis-URL"
+  CUSTOM_BASE_URL_HELP: "Benyt, hvis du ønsker at omskrive webstedsdomæne eller benytte en anden undermappe end den, Grav benytter. Eksempel: http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'Kan ikke benytte "%s" uden for sider.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'Kan ikke uploade fil %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'Kan ikke flytte fil %s til "%s"'
+  DROPZONE_CANCEL_UPLOAD: 'Annullér upload'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Sikker på, at du vil annullere denne upload?'
+  DROPZONE_DEFAULT_MESSAGE: 'Slip dine filer hér eller, <strong>klik i dette område</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'Din browser understøtter ikke træk-og-slip fil-uploads.'
+  DROPZONE_FALLBACK_TEXT: 'Benyt venligst formularen nedenfor for alternativt at uploade filer ''som i gamle dage''.'
+  DROPZONE_FILE_TOO_BIG: 'Filen er for stor ({{filesize}}MiB). Maks. filstørrelse: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "Filer af denne type kan ikke uploades."
+  DROPZONE_MAX_FILES_EXCEEDED: "Der kan ikke uploades flere filer."
+  DROPZONE_REMOVE_FILE: "Fjern fil"
+  DROPZONE_RESPONSE_ERROR: "Serveren svarede med koden {{statusCode}}."
+  PREMIUM_PRODUCT: "Premium"
+  DESTINATION_NOT_SPECIFIED: "Destination er ikke angivet"
+  UPLOAD_ERR_NO_TMP_DIR: "Mangler en midlertidige mappe"
+  SESSION_SPLIT: "Sessionsopdeling"
+  SESSION_SPLIT_HELP: "Opdel sessioner uafhængigt mellem websted og andre plugins (f.eks. Admin)"
+  ERROR_FULL_BACKTRACE: "Vis detaljeret Backtrace fejlinfo"
+  ERROR_SIMPLE: "Vis simpel fejlinfo"
+  ERROR_SYSTEM: "Systemfejl"
+  IMAGES_AUTO_FIX_ORIENTATION: "Korrigér orientering automatisk"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Korrigér automatisk billedretningen baseret på Exif-dataene"
+  REDIS_SOCKET: "Redis socket"
+  REDIS_SOCKET_HELP: "Redis socket"
+  NOT_SET: "Ikke indstillet"
+  PERMISSIONS: "Rettigheder"
+  NEVER_CACHE_TWIG: "Mellemlagre aldrig Twig"
+  NEVER_CACHE_TWIG_HELP: "Cache kun indhold og behandle Twig for hver side. Ignorerer twig_first-indstilling."
+  ALLOW_WEBSERVER_GZIP: "Tillad WebServer Gzip"
+  ALLOW_WEBSERVER_GZIP_HELP: "Deaktiveret som standard. Når aktiveret, vil WebServer-konfigureret Gzip-/Deflate-kompression fungere, men HTTP-forbindelsen vil ikke blive lukket før onShutDown() begivenheden forårsager langsommere sideindlæsning"
+  OFFLINE_WARNING: "Kan ikke etablere forbindelse til GPM"
+  CLEAR_IMAGES_BY_DEFAULT: "Ryd som standard billede-cachen"
+  CLEAR_IMAGES_BY_DEFAULT_HELP: "Som standard ryddes behandlede billeder for alle caches. Dette kan deaktiveres"
+  CLI_COMPATIBILITY: "CLI-kompatibilitet"
+  CLI_COMPATIBILITY_HELP: "Sikrer, at kun ikke-flygtig Cache-drivere benyttes (fil, redis, memcache etc.)"
+  REINSTALL_PLUGIN: "Geninstallere plugin"
+  REINSTALL_THEME: "Geninstallere tema"
+  REINSTALL_THE: "Geninstallér %s"
+  CONFIRM_REINSTALL: "Sikker på, at du vil geninstallere dette %s?"
+  REINSTALLED_SUCCESSFULLY: "%s geninstalleret"
+  ERROR_REINSTALLING_THE: "Fejl under geninstallering af %s"
+  PACKAGE_X_REINSTALLED_SUCCESSFULLY: "Pakke %s geninstalleret"
+  REINSTALLATION_FAILED: "Geninstallation mislykkedes"
+  WARNING_REINSTALL_NOT_LATEST_RELEASE: "Den installerede version er ikke den seneste udgivelse. Ved at klikke på Fortsæt fjerner du fjerne den aktuelle version og installerer den senest tilgængelige version"
+  TOOLS: "Værktøjer"
+  NO_PACKAGE_NAME: "Pakkenavn ikke angivet"
+  PACKAGE_EXTRACTION_FAILED: "Pakkeudpakning mislykkedes"
+  NOT_VALID_GRAV_PACKAGE: "Ikke en gyldig Grav-pakke"
+  NAME_COULD_NOT_BE_DETERMINED: "Navn kunne ikke fastslås"
+  CANNOT_OVERWRITE_SYMLINKS: "Kan ikke overskrive symlinks"
+  ZIP_PACKAGE_NOT_FOUND: "ZIP-pakke blev ikke fundet"
+  GPM_OFFICIAL_ONLY: "Kun officiel GPM"
+  GPM_OFFICIAL_ONLY_HELP: "Tillade kun direkte installationer fra det officielle GPM-repositorie."
+  NO_CHILD_TYPE: "Ingen underordnet type for denne rawroute"
+  SORTABLE_PAGES: "Sorterbare sider:"
+  ADMIN_SPECIFIC_OVERRIDES: "Admin-specifikke tilsidesætter"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Underordnedes visningsrækkefølge"
+  ADMIN_CHILDREN_DISPLAY_ORDER_HELP: "Rækkefølgen, hvori undersider til denne side skal vises i visningen 'Sider' i Admin-plugin"
+  PWD_PLACEHOLDER: "kompleks streng minimum 8 tegn lang"
+  PWD_REGEX: "Adgangskode Regex"
+  PWD_REGEX_HELP: "Adgangskode skal som standard indeholde mindst ét ciffer, én versal og én minuskel samt udgøres af minimum otte tegn"
+  USERNAME_PLACEHOLDER: "kun minuskler, f.eks. 'admin'"
+  USERNAME_REGEX: "Brugernavn Regex"
+  USERNAME_REGEX_HELP: "Som standard kun minuskler, cifre, bindestreger og understregninger på 3-16 tegn"
+  ENABLE_AUTO_METADATA: "Auto-metadata fra Exif"
+  ENABLE_AUTO_METADATA_HELP: "Automatisk generere metadatafiler til billeder med exif-oplysninger"
+  2FA_TITLE: "Totrinsautentificering"
+  2FA_INSTRUCTIONS: "### 2-faktor Autentificering \nDu har **2FA** aktiveret for denne konto. Benyt venligst din **2FA**-app til at angive den aktuelle **6-cifrede kode** for at fuldføre loginprocessen."
+  2FA_LABEL: "Admin-adgang"
+  2FA_FAILED: "Ugyldig 2-faktor autentificeringskode, forsøg venligst igen..."
+  2FA_ENABLED: "2FA aktiveret"
+  2FA_CODE_INPUT: "000000"
+  2FA_SECRET: "2FA hemmelighed"
+  2FA_REGENERATE: "Regenerér"
+  FORCE_LOWERCASE_URLS: "Brug små bogstaver i URL'er"
+  VIEW_SITE_TIP: "Vis site"
+  TOOLS_DIRECT_INSTALL_TITLE: "Direkte installation af Grav-pakker"
+  TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Upload og installer"
+  OPEN_NEW_TAB: "Åbn i nyt faneblad"
+  STRICT_YAML_COMPAT: "YAML Kompatibilitet"
+  STRICT_TWIG_COMPAT: "Twig Kompatibilitet"
+  SCHEDULER_OUTPUT: "Output Fil"
+  SCHEDULER_EMAIL: "E-mail"
+  SECURITY: "Sikkerhed"
+  REPORTS: "Rapporter"
+  LOGS: "Logfiler"
+  BACKUPS_MAX_COUNT: "Maksimalt Antal Sikkerhedskopier"
+  BACKUPS_MAX_COUNT_HELP: "0 er ubegrænset"
+  BACKUPS_PROFILE_NAME: "Backup navn"
+  COMMAND: "Kommando"
+  CONFIGURATION: "Opsætning"
+  ADMIN_CACHING: "Aktivere Admin-cachelagring"
+  ADMIN_CACHING_HELP: "Cachelagring i admin kan styres uafhængigt af frontend-webstedet"
+  CONTENT_PADDING: "Indholdsrammer"
+  CONTENT_PADDING_HELP: "Aktivér/Deaktivér indholdsrammer omkring indholdsområde for at give mere plads"
+  TIMEOUT: "Timeout"
+  TIMEOUT_HELP: "Angiver sessionstimeout i sekunder"
+  DASHBOARD: "Kontrolpanel"
+  NOTIFICATIONS: "Notifikationer"
+  CONTENT_HIGHLIGHT_HELP: "Indholdsfremhævning"
+  REDIRECT_OPTION_NO_REDIRECT: "Ingen omdirigering"
+  REDIRECT_OPTION_DEFAULT_REDIRECT: "Benyt standardomdirigeringskode"
+  REDIRECT_OPTION_301: "301 - Flyttet permanent"
+  REDIRECT_OPTION_302: "302 - Flyttet midlertidigt"
+  REDIRECT_OPTION_303: "303 - Se andre"
+  IMAGES_CLS_TITLE: "Cumative Layer Shift (CLS)"
+  IMAGES_CLS_AUTO_SIZES: "Aktivér Auto-størrelser"
+  IMAGES_CLS_AUTO_SIZES_HELP: "Føj automatisk 'bredde'- og 'højde'-attributter til billeder for at adressere CLS"
+  IMAGES_CLS_ASPECT_RATIO: "Aktivér Størrelsesforhold"
+  IMAGES_CLS_ASPECT_RATIO_HELP: "Valgfri CSS-variabel, der bliver anvendt via en 'stil'-attribut, der kan bruges i CSS til tilpasset styling"
+  IMAGES_CLS_RETINA_SCALE: "Retinaskaleringsfaktor"
+  IMAGES_CLS_RETINA_SCALE_HELP: "Vil tage den beregnede størrelse og dividere med skaleringsfaktoren for at vise et billede med højere opløsning ved en mindre pixelstørrelse for bedre håndtering af HiDPI-opløsninger"
+  AUTOREGENERATE_FOLDER_SLUG: "Auto-regenerér baseret på sidetitel"
+  ENABLE: Aktivér
+  PLUGINS_MUST_BE_ENABLED: "Plugin skal være aktiveret for at opsætte"
+  ACTIVATION_REQUIRED: "Aktivering krævet for at opsætte"

+ 965 - 0
plugins/admin/languages/de.yaml

@@ -0,0 +1,965 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_NOSCRIPT_MSG: "Bitte aktivieren Sie JavaScript in Ihrem Browser."
+  ADMIN_BETA_MSG: "Dies ist eine Beta-Version! Benutzung auf eigene Gefahr..."
+  ADMIN_REPORT_ISSUE: "Fehler gefunden? Bitte melden Sie ihn auf GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\"> Umgesetzt mit Grav</a> - dem modernen Flat-File CMS"
+  LOGIN_BTN: "Anmelden"
+  LOGIN_BTN_FORGOT: "Passwort vergessen"
+  LOGIN_BTN_RESET: "Passwort zurücksetzen"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Neues Passwort anfordern"
+  LOGIN_BTN_CLEAR: "Formular leeren"
+  LOGIN_BTN_CREATE_USER: "Benutzer erstellen"
+  LOGIN_LOGGED_IN: "Anmeldung erfolgreich"
+  LOGIN_FAILED: "Anmeldung fehlgeschlagen"
+  LOGGED_OUT: "Sie wurden abgemeldet"
+  RESET_NEW_PASSWORD: "Bitte geben Sie ein neues Passwort ein &hellip;"
+  RESET_LINK_EXPIRED: "Der Link zum Zurücksetzen Ihres Passwortes ist abgelaufen, bitte probieren Sie es erneut"
+  RESET_PASSWORD_RESET: "Das Passwort wurde zurückgesetzt"
+  RESET_INVALID_LINK: "Der Link zum Zurücksetzen Ihres Passwortes ist ungültig, bitte probieren Sie es erneut"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Anweisungen zum Zurücksetzen des Passwortes wurden an %s gesendet"
+  FORGOT_FAILED_TO_EMAIL: "Anweisungen zum Zurücksetzen des Passwortes konnten nicht versendet werden, bitte probieren Sie es später erneut"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Das Passwort für %s kann nicht geändert werden, da keine E-Mail-Adresse hinterlegt ist"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Es existiert kein Benutzer mit dem Namen <b>%s</b>"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Passwort konnte nicht zurückgesetzt werden, da diese Seite nicht zum Versenden von E-Mails konfiguriert worden ist"
+  FORGOT_EMAIL_SUBJECT: "Zurücksetzen des Passwortes von %s"
+  FORGOT_EMAIL_BODY: "<h1>Passwort zurücksetzen</h1><p>Hallo %1$s,</p><p>Auf <b>%4$s</b> wurde die Zurücksetzung Ihres Passwortes angefordert.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klicken Sie hier um Ihr Passwort zurückzusetzen.</a><br /><br /></p><p>Alternativ können Sie auch die folgende Adresse in die Adresszeile Ihres Browsers kopieren:</p> <p>%2$s</p><p><br />Viele Grüße,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Seiten verwalten"
+  PAGES: "Seiten"
+  PLUGINS: "Plugins"
+  PLUGIN: "Plugin"
+  THEMES: "Themes"
+  LOGOUT: "Abmelden"
+  BACK: "Zurück"
+  NEXT: "Nächste"
+  PREVIOUS: "Vorherige"
+  ADD_PAGE: "Seite hinzufügen"
+  MOVE: "Verschieben"
+  DELETE: "Löschen"
+  UNSET: "Zurücksetzen"
+  VIEW: "Anzeigen"
+  SAVE: "Speichern"
+  NORMAL: "Normal"
+  EXPERT: "Expertenansicht"
+  EXPAND_ALL: "Alle ausklappen"
+  COLLAPSE_ALL: "Alle einklappen"
+  ERROR: "Fehler"
+  CLOSE: "Schließen"
+  CANCEL: "Abbrechen"
+  CONTINUE: "Weiter"
+  CONFIRM: "Bestätigen"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Bestätigung erforderlich"
+  MODAL_CHANGED_DETECTED_TITLE: "Änderungen erkannt"
+  MODAL_CHANGED_DETECTED_DESC: "Einige Änderungen wurden noch nicht gespeichert. Wollen Sie diese Seite wirklich verlassen?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Bestätigung erforderlich"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Wollen Sie diese Datei wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden."
+  MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "Sie sind dabei, Grav auf die neueste Version zu aktualisieren. Möchten Sie fortfahren?"
+  ADD_FILTERS: "Filter hinzufügen"
+  SEARCH_PAGES: "Seiten durchsuchen"
+  VERSION: "Version"
+  WAS_MADE_WITH: "Erstellt mit"
+  BY: "Von"
+  UPDATE_THEME: "Theme aktualisieren"
+  UPDATE_PLUGIN: "Plugins aktualisieren"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "dieses Themes ist verfügbar"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "dieses Plugins ist verfügbar"
+  AUTHOR: "Autor"
+  HOMEPAGE: "Homepage"
+  DEMO: "Demo"
+  BUG_TRACKER: "Bug-Tracker"
+  KEYWORDS: "Schlagwörter"
+  LICENSE: "Lizenz"
+  DESCRIPTION: "Beschreibung"
+  README: "Readme"
+  DOCS: "Dokumentation"
+  REMOVE_THEME: "Theme löschen"
+  INSTALL_THEME: "Theme installieren"
+  THEME: "Theme"
+  BACK_TO_THEMES: "Zurück zu den Themes"
+  BACK_TO_PLUGINS: "Zurück zu den Plugins"
+  CHECK_FOR_UPDATES: "Updates suchen"
+  ADD: "Hinzufügen"
+  CLEAR_CACHE: "Cache leeren"
+  CLEAR_CACHE_ALL_CACHE: "Alle Caches"
+  CLEAR_CACHE_ASSETS_ONLY: "Nur Assets"
+  CLEAR_CACHE_IMAGES_ONLY: "Nur Bilder"
+  CLEAR_CACHE_CACHE_ONLY: "Nur Cache"
+  CLEAR_CACHE_TMP_ONLY: "nur temporär"
+  UPDATES_AVAILABLE: "Updates verfügbar"
+  DAYS: "Tage"
+  UPDATE: "Aktualisieren"
+  BACKUP: "Sicherung"
+  BACKUPS: "Sicherungen"
+  BACKUP_NOW: "Sicherung jetzt erstellen"
+  BACKUPS_STATS: "Sicherungsstatistiken"
+  BACKUPS_HISTORY: "Sicherungsverlauf"
+  BACKUPS_PURGE_CONFIG: "Automatisches Löschen von Sicherungen"
+  BACKUPS_PROFILES: "Sicherungsprofile"
+  BACKUPS_COUNT: "Anzahl an Sicherungen"
+  BACKUPS_PROFILES_COUNT: "Anzahl der Profile"
+  BACKUPS_TOTAL_SIZE: "Belegter Speicherplatz"
+  BACKUPS_NEWEST: "Neuste Sicherung"
+  BACKUPS_OLDEST: "Älteste Sicherung"
+  BACKUPS_PURGE: "Bereinigen"
+  BACKUPS_NOT_GENERATED: "Bisher wurden keine Backups generiert..."
+  BACKUPS_PURGE_NUMBER: "Sie nutzen %s von insgesamt %s möglichen Backups"
+  BACKUPS_PURGE_TIME: "%s Tage an Sicherungen übrig"
+  BACKUPS_PURGE_SPACE: "%s von %s verwendet"
+  BACKUP_DELETED: "Backup erfolgreich gelöscht"
+  BACKUP_NOT_FOUND: "Sicherung wurde nicht gefunden"
+  BACKUP_DATE: "Sicherungsdatum"
+  STATISTICS: "Statistiken"
+  VIEWS_STATISTICS: "Seiten Statistik"
+  TODAY: "Heute"
+  WEEK: "Woche"
+  MONTH: "Monat"
+  LATEST_PAGE_UPDATES: "Zuletzt bearbeitete Seiten"
+  MAINTENANCE: "Wartung"
+  UPDATED: "Aktualisiert"
+  MON: "Mo"
+  TUE: "Di"
+  WED: "Mi"
+  THU: "Do"
+  FRI: "Fr"
+  SAT: "Sa"
+  SUN: "So"
+  COPY: "Kopieren"
+  EDIT: "Bearbeiten"
+  CREATE: "Erstellen"
+  GRAV_ADMIN: "Grav-Admin"
+  GRAV_OFFICIAL_PLUGIN: "Offizielles Grav Plugin"
+  GRAV_OFFICIAL_THEME: "Offizielles Grav Theme"
+  PLUGIN_SYMBOLICALLY_LINKED: "Dieses Plugin basiert auf einer symbolischen Verknüpfung. Updates werden daher nicht erkannt."
+  THEME_SYMBOLICALLY_LINKED: "Dieses Theme basiert auf einer symbolischen Verknüpfung. Updates werden daher nicht erkannt."
+  REMOVE_PLUGIN: "Plugin entfernen"
+  INSTALL_PLUGIN: "Plugin installieren"
+  AVAILABLE: "Verfügbar"
+  INSTALLED: "Installiert"
+  INSTALL: "Installieren"
+  ACTIVE_THEME: "Ausgewähltes Theme"
+  SWITCHING_TO: "Wechseln zu"
+  SWITCHING_TO_DESCRIPTION: "Durch das Wechseln des Themes kann nicht garantiert werden, dass das Layout aller Seiten unterstützt wird. Daher können beim Aufrufen der Seiten Fehler entstehen."
+  SWITCHING_TO_CONFIRMATION: "Möchten Sie fortfahren und zum Theme wechseln"
+  CREATE_NEW_USER: "Neuen Benutzer erstellen"
+  REMOVE_USER: "Benutzer entfernen"
+  ACCESS_DENIED: "Zugriff verweigert"
+  ACCOUNT_NOT_ADMIN: "Ihr Account verfügt über keine Administrationsberechtigungen"
+  PHP_INFO: "PHP-Info"
+  INSTALLER: "Installationsprogramm"
+  AVAILABLE_THEMES: "Verfügbare Themes"
+  AVAILABLE_PLUGINS: "Verfügbare Plugins"
+  INSTALLED_THEMES: "Installierte Themes"
+  INSTALLED_PLUGINS: "Installierte Plugins"
+  BROWSE_ERROR_LOGS: "Error Logs anzeigen"
+  SITE: "Site"
+  INFO: "Info"
+  SYSTEM: "System"
+  USER: "Benutzer"
+  ADD_ACCOUNT: "Benutzer hinzufügen"
+  SWITCH_LANGUAGE: "Sprache wechseln"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Plugin erfolgreich aktiviert"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Plugin erfolgreich deaktiviert"
+  SUCCESSFULLY_CHANGED_THEME: "Standard Theme erfolgreich geändert"
+  INSTALLATION_FAILED: "Installation fehlgeschlagen"
+  INSTALLATION_SUCCESSFUL: "Installation erfolgreich"
+  UNINSTALL_FAILED: "Deinstallation fehlgeschlagen"
+  UNINSTALL_SUCCESSFUL: "Deinstallation erfolgreich"
+  SUCCESSFULLY_SAVED: "Speichern erfolgreich"
+  SUCCESSFULLY_COPIED: "Kopieren erfolgreich"
+  REORDERING_WAS_SUCCESSFUL: "Umsortieren war erfolgreich"
+  SUCCESSFULLY_DELETED: "Erfolgreich gelöscht"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Sprache erfolgreich gewechselt"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "Sie haben unzureichende Berechtigungen für diese Aktion"
+  CACHE_CLEARED: "Cache geleert"
+  METHOD: "Methode"
+  ERROR_CLEARING_CACHE: "Fehler beim leeren des Cache"
+  AN_ERROR_OCCURRED: "Ein Fehler ist aufgetreten"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Ihr Backup steht zum Download bereit"
+  DOWNLOAD_BACKUP: "Backup herunterladen"
+  PAGES_FILTERED: "Seiten gefiltert"
+  NO_PAGE_FOUND: "Keine Seite gefunden"
+  INVALID_PARAMETERS: "Ungültige Eingabe"
+  NO_FILES_SENT: "Keine Dateien übertragen"
+  EXCEEDED_FILESIZE_LIMIT: "Dateigrößenbeschränkung aus PHP-Konfiguration überschritten"
+  EXCEEDED_POSTMAX_LIMIT: "PHP-Einstellung post_max_size überschritten"
+  UNKNOWN_ERRORS: "Unbekannte Fehler"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Dateigrößenbeschränkung in Grav-Konfiguration überschritten"
+  UNSUPPORTED_FILE_TYPE: "Dateityp nicht unterstützt"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Hochgeladene Datei konnte nicht verschoben werden."
+  FILE_UPLOADED_SUCCESSFULLY: "Datei erfolgreich hochgeladen"
+  FILE_DELETED: "Datei gelöscht"
+  FILE_COULD_NOT_BE_DELETED: "Datei konnte nicht gelöscht werden"
+  FILE_NOT_FOUND: "Datei nicht gefunden"
+  NO_FILE_FOUND: "Keine Datei gefunden"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav wurde erfolgreich aktualisiert auf"
+  GRAV_UPDATE_FAILED: "Grav Update fehlgeschlagen"
+  EVERYTHING_UPDATED: "Alles aktualisiert"
+  UPDATES_FAILED: "Updates fehlgeschlagen"
+  AVATAR_BY: "Avatar von"
+  AVATAR_UPLOAD_OWN: "Oder laden Sie Ihr eigenes hoch..."
+  LAST_BACKUP: "Letztes Backup"
+  FULL_NAME: "Voller Name"
+  USERNAME: "Benutzername"
+  EMAIL: "E-Mail"
+  USERNAME_EMAIL: "Benutzername oder E-Mail"
+  PASSWORD: "Passwort"
+  PASSWORD_CONFIRM: "Passwort bestätigen"
+  TITLE: "Titel"
+  LANGUAGE: "Sprache"
+  ACCOUNT: "Benutzer"
+  EMAIL_VALIDATION_MESSAGE: "Muss eine gültige E-Mail Adresse sein"
+  PASSWORD_VALIDATION_MESSAGE: "Das Passwort muss mindestens eine Zahl, einen Groß- und einen Kleinbuchstaben enthalten sowie mindestens 8 Zeichen lang sein."
+  LANGUAGE_HELP: "Bevorzugte Sprache einstellen"
+  MEDIA: "Medien"
+  DEFAULTS: "Standards"
+  SITE_TITLE: "Website Titel"
+  SITE_TITLE_PLACEHOLDER: "Titel der Website"
+  SITE_TITLE_HELP: "Titel der Site. Wird häufig von Themes verwendet."
+  SITE_DEFAULT_LANG: "Standardsprache"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Standardsprache die im Theme <HTML> Tag benutzt werden soll"
+  SITE_DEFAULT_LANG_HELP: "Standardsprache die im Theme <HTML> Tag benutzt werden soll"
+  DEFAULT_AUTHOR: "Hauptautor"
+  DEFAULT_AUTHOR_HELP: "Name des Hauptautors, häufig verwendet in Themes und Inhalten"
+  DEFAULT_EMAIL: "Standard E-Mail Adresse"
+  DEFAULT_EMAIL_HELP: "Standard E-Mail Adresse, die in Plugins und Themes verwendet wird"
+  TAXONOMY_TYPES: "Tag-Typen"
+  TAXONOMY_TYPES_HELP: "Wenn sie verschiedene Tag-Typen verwenden wollen, müssen Sie hier die verschiedenen Typen angeben."
+  PAGE_SUMMARY: "Seitenzusammenfassung"
+  ENABLED: "Aktiviert"
+  ENABLED_HELP: "Aktiviert die Seitenzusammenfassung (die Seitenzusammenfassung entspricht dem Inhalt der Seite)"
+  'YES': "Ja"
+  'NO': "Nein"
+  SUMMARY_SIZE: "Zusammenfassungslänge"
+  SUMMARY_SIZE_HELP: "Länge der Zusammenfassung in Buchstaben"
+  FORMAT: "Format"
+  FORMAT_HELP: "kurz = Kürzung der Zusammenfassung bis zum Trennzeichen oder der Zusammenfassungslänge; lang = Trennzeichen wird ignoriert"
+  SHORT: "Kurz"
+  LONG: "Lang"
+  DELIMITER: "Trennzeichen"
+  DELIMITER_HELP: "Das Trennzeichen für die Zusammenfassung (Standard '===')"
+  METADATA: "Metadaten"
+  METADATA_HELP: "Standardmetadaten, die auf jeder Seite angezeigt werden, es sei denn sie werden von einer Seite überschrieben"
+  NAME: "Name"
+  CONTENT: "Inhalt"
+  SIZE: "Größe"
+  ACTION: "Aktion"
+  REDIRECTS_AND_ROUTES: "Weiterleitungen & Routen"
+  CUSTOM_REDIRECTS: "Eigene Weiterleitungen"
+  CUSTOM_REDIRECTS_HELP: "Routen, die auf andere Seiten weiterleiten. Standard Regex Ersetzungen sind erlaubt"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/dein/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/deine/umleitung"
+  CUSTOM_ROUTES: "Eigene Routen"
+  CUSTOM_ROUTES_HELP: "Routen, die auf andere Seiten verweisen. Standard Regex Ersetzungen sind erlaubt"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/dein/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/deine/route"
+  FILE_STREAMS: "Datei Streams"
+  DEFAULT: "Standard"
+  PAGE_MEDIA: "Seitenmedien"
+  OPTIONS: "Optionen"
+  PUBLISHED: "Veröffentlicht"
+  PUBLISHED_HELP: "Standardmäßig ist eine Seite veröffentlicht, es sei denn sie wird explizit auf nicht veröffentlicht gesetzt, das Veröffentlichungsdatum liegt in der Zukunft oder das Ablaufdatum wurde erreicht."
+  DATE: "Datum"
+  DATE_HELP: "Die Datumsangabe erlaubt es diese Seite mit einem Datum zu versehen."
+  PUBLISHED_DATE: "Veröffentlichungsdatum"
+  PUBLISHED_DATE_HELP: "Datum ab dem die Seite automatisch veröffentlicht wird."
+  UNPUBLISHED_DATE: "Ablaufdatum"
+  UNPUBLISHED_DATE_HELP: "Datum ab dem die Veröffentlichung der Seite zurückgenommen wird."
+  ROBOTS: "Robots"
+  TAXONOMIES: "Tags"
+  TAXONOMY: "Tag"
+  ADVANCED: "Erweitert"
+  SETTINGS: "Einstellungen"
+  FOLDER_NUMERIC_PREFIX: "Numerischer Ordnerprefix"
+  FOLDER_NUMERIC_PREFIX_HELP: "Numerischer Prefix der die Reihenfolge und Sichtbarkeit der Seiten beeinflusst"
+  FOLDER_NAME: "Ordnername"
+  FOLDER_NAME_HELP: "Name des Ordners, der für diese Seite angelegt werden sollen"
+  PARENT: "Übergeordnet"
+  DEFAULT_OPTION_ROOT: "- Ursprung -"
+  DEFAULT_OPTION_SELECT: "- Auswählen -"
+  DISPLAY_TEMPLATE: "Template anzeigen"
+  DISPLAY_TEMPLATE_HELP: "Der Seitentyp der zur Wahl des richtigen Twig Templates verwendet wird."
+  ORDERING: "Sortierung"
+  PAGE_ORDER: "Seitensortierung"
+  OVERRIDES: "Überschreibungen"
+  MENU: "Menü"
+  MENU_HELP: "Text zur Anzeige im Menu. Wenn nicht gesetzt, wird der Titel verwendet."
+  SLUG: "Slug"
+  SLUG_HELP: "Der Slug bestimmt den Namen der Seite innerhalb der URL."
+  SLUG_VALIDATE_MESSAGE: "Ein Slug darf nur aus Kleinbuchstaben und Bindestrichen bestehen"
+  PROCESS: "Verarbeitung"
+  PROCESS_HELP: "Steuert die Verarbeitung einer Seite. Sollte pro Seite und nicht global gesetzt werden."
+  DEFAULT_CHILD_TYPE: "Standardtyp für Unterseiten"
+  USE_GLOBAL: "Globale Einstellungen verwenden"
+  ROUTABLE: "Aufrufbar"
+  ROUTABLE_HELP: "Ob diese Seite mit einer URL aufgerufen werden kann"
+  CACHING: "Caching"
+  VISIBLE: "Sichtbar"
+  VISIBLE_HELP: "Steuert die Sichtbarkeit einer Seite im Menü."
+  DISABLED: "Deaktiviert"
+  ITEMS: "Elemente"
+  ORDER_BY: "Sortieren nach"
+  ORDER: "Sortierung"
+  FOLDER: "Ordner"
+  ASCENDING: "Aufsteigend"
+  DESCENDING: "Absteigend"
+  PAGE_TITLE: "Seitentitel"
+  PAGE_TITLE_HELP: "Titel der Seite"
+  PAGE: "Seite"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Dateiname"
+  PARENT_PAGE: "Übergeordnete Seite"
+  HOME_PAGE: "Startseite"
+  HOME_PAGE_HELP: "Seite die von Grav als Startseite genutzt werden soll"
+  DEFAULT_THEME: "Standard Theme"
+  DEFAULT_THEME_HELP: "Setzt den Standard Theme von Grav (Standard ist Antimatter)"
+  TIMEZONE: "Zeitzone"
+  TIMEZONE_HELP: "Überschreibt die Zeitzone des Servers"
+  SHORT_DATE_FORMAT: "Kurzes Datumsformat"
+  SHORT_DATE_FORMAT_HELP: "Setzt das von Themes genutzte Kurzdatumsformat"
+  LONG_DATE_FORMAT: "Langes Datumsformat"
+  LONG_DATE_FORMAT_HELP: "Setzt das von Themes genutzte Langdatumsformat"
+  DEFAULT_ORDERING: "Standard Sortierung"
+  DEFAULT_ORDERING_HELP: "Seiten in einer Liste werden in der Standard Sortierung angezeigt, falls diese nicht überschrieben wird."
+  DEFAULT_ORDERING_DEFAULT: "Standard - basierend auf dem Ordnernamen"
+  DEFAULT_ORDERING_FOLDER: "Ordner - basierend auf dem Ordnernamen ohne Prefix"
+  DEFAULT_ORDERING_TITLE: "Titel - basieren auf dem Seitentitel"
+  DEFAULT_ORDERING_DATE: "Datum - basierend auf dem Datum der Seite"
+  DEFAULT_ORDER_DIRECTION: "Standard Sortierrichtung"
+  DEFAULT_ORDER_DIRECTION_HELP: "Sortierrichtung von Seiten in einer Liste"
+  DEFAULT_PAGE_COUNT: "Standard Seitenzahl"
+  DEFAULT_PAGE_COUNT_HELP: "Maximale Anzahl von Seiten in einer Liste"
+  DATE_BASED_PUBLISHING: "Datumsbasierte Veröffentlichungen"
+  DATE_BASED_PUBLISHING_HELP: "Seiten mit Veröffentlichungs- und Ablaufdatum automatisch veröffentlichen oder zurückziehen"
+  EVENTS: "Ereignisse"
+  EVENTS_HELP: "Von Grav gesteuerte Ereignisse aktivieren. Ein Deaktivieren kann Plugins in ihrer Funktionsweise stören."
+  REDIRECT_DEFAULT_ROUTE: "Weiterleiten auf Standardroute"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Automatisches Weiterleiten auf die Route einer Seite"
+  LANGUAGES: "Sprachen"
+  SUPPORTED: "Unterstützt"
+  SUPPORTED_HELP: "Durch Kommata getrennte Liste mit 2-stelligen Sprachcodes (zum Beispiel 'en,fr,de')"
+  SUPPORTED_PLACEHOLDER: "z.B. de, fr"
+  TRANSLATIONS_FALLBACK: "Fallbacksprache"
+  TRANSLATIONS_FALLBACK_HELP: "Falls Übersetzungen in einer Sprache nicht vorhanden sind, wird auf diese Sprache zurückgegriffen"
+  ACTIVE_LANGUAGE_IN_SESSION: "Aktive Sprache in Session speichern"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Speichert die aktive Sprache in der Session"
+  HTTP_HEADERS: "HTTP-Header"
+  EXPIRES: "Läuft ab"
+  EXPIRES_HELP: "Ändert den Expires-Header (Cache-Ablaufdatum). Angabe in Sekunden"
+  CACHE_CONTROL: "HTTP-Cache-Control"
+  CACHE_CONTROL_HELP: "Muss auf einen validen Cache-Control-Wert gesetzt werden, wie zum Beispiel 'no-cache, no-store, must-revalidate\""
+  CACHE_CONTROL_PLACEHOLDER: "z.B. public, max-age=31536000"
+  LAST_MODIFIED: "Zuletzt geändert"
+  LAST_MODIFIED_HELP: "Setzt das Datum der letzten Änderung um Caching zu optimieren"
+  ETAG: "ETag"
+  ETAG_HELP: "Setzt den ETag-Header um Änderungen der Seite für Proxies erkenntlich zu machen"
+  VARY_ACCEPT_ENCODING: "Vary Accept Encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Setzt den `Vary: Accept Encoding`-Header um Proxies und CDNs zu optimieren"
+  MARKDOWN: "Markdown"
+  MARKDOWN_EXTRA: "Markdown extra"
+  MARKDOWN_EXTRA_HELP: "Aktiviert Markdown Extra Unterstützung - https://michelf.ca/projects/php-markdown/extra/"
+  MARKDOWN_EXTRA_ESCAPE_FENCES: "Escape HTML-Elemente in Markdown extra fences"
+  MARKDOWN_EXTRA_ESCAPE_FENCES_HELP: "Escapes HTML-Elemente in Markdown extra fences"
+  AUTO_LINE_BREAKS: "Automatische Zeilenumbrüche"
+  AUTO_LINE_BREAKS_HELP: "Aktiviert Unterstützung für automatische Zeilenumbrüche in Markdown"
+  AUTO_URL_LINKS: "URLs zu Links verwandeln"
+  AUTO_URL_LINKS_HELP: "Verwandelt jede URL in einen HTML-Link"
+  ESCAPE_MARKUP: "HTML Ausgabe unterdrücken"
+  ESCAPE_MARKUP_HELP: "Ersetzt Markup-Tags durch HTML-Zeichen"
+  CACHING_HELP: "Globaler an/aus Schalter um Caching in Grav zu aktivieren/deaktivieren"
+  CACHE_CHECK_METHOD: "Cacheprüfungsmethode"
+  CACHE_CHECK_METHOD_HELP: "Methode, die Grav verwendet, um zu erkennen, ob Seiten geändert wurden."
+  CACHE_DRIVER: "Cache-Treiber"
+  CACHE_DRIVER_HELP: "Cache-Treiber den Grav verwendet um Dateien vorzuhalten. 'Automatisch' versucht selbstständig den besten Treiber zu ermitteln."
+  CACHE_PREFIX: "Cache-Prefix"
+  CACHE_PREFIX_HELP: "Cache-Prefix der verwendet wird um Dateien zu cachen. Sollte nicht verändert werden, wenn man nicht genau weiß, was man tut."
+  CACHE_PREFIX_PLACEHOLDER: "Abgeleitet von der Basis-URL (überschreiben durch einen zufälligen Text)"
+  CACHE_PURGE_JOB: "Geplante Bereinigung ausführen"
+  CACHE_PURGE_JOB_HELP: "Mit dem Zeitplaner können Sie die alten Ordner für den Cache der Doctrine-Datei mit diesem Job regelmäßig löschen"
+  CACHE_CLEAR_JOB: "Geplante Bereinigung ausführen"
+  CACHE_CLEAR_JOB_HELP: "Mit dem Scheduler können Sie den Grav-Cache periodisch löschen"
+  CACHE_JOB_TYPE: "Cache Job Typ"
+  CACHE_JOB_TYPE_HELP: "Entweder mit dem Cache Clear der 'Standard' Ordner löschen, oder mit 'allen' Ordnern"
+  CACHE_PURGE: "Alten Cache bereinigen"
+  LIFETIME: "Cache-Lebensdauer"
+  LIFETIME_HELP: "Setzt die Dauer des Caches in Sekunden. 0 = unendlich"
+  GZIP_COMPRESSION: "Gzip-Komprimierung"
+  GZIP_COMPRESSION_HELP: "Aktiviert Gzip-Komprimierung um Grav schneller an Browser senden zu können."
+  TWIG_TEMPLATING: "Twig Templates"
+  TWIG_CACHING: "Twig Caching"
+  TWIG_CACHING_HELP: "Twig Cache-Einstellungen. Sollte aktiviert bleiben um die beste Performance zu gewährleisten."
+  TWIG_DEBUG: "Twig debuggen"
+  TWIG_DEBUG_HELP: "Deaktiviert die Twig-Debug-Extension"
+  DETECT_CHANGES: "Änderungen erkennen"
+  DETECT_CHANGES_HELP: "Twig erkennt automatisch, wenn Themes geändert werden und baut den Cache neu auf"
+  AUTOESCAPE_VARIABLES: "Variablen automatisch escapen"
+  AUTOESCAPE_VARIABLES_HELP: "Alle Veriablen escapen. Ihre Seite könnte dadurch nicht korrekt angezeigt werden"
+  ASSETS: "Assets"
+  CSS_PIPELINE: "CSS Pipeline"
+  CSS_PIPELINE_HELP: "Die CSS Pipeline fasst alle Ihre CSS Dateien in einer einzigen Datei zusammen"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Externe CSS-Dateien mit in die Pipeline aufnehmen"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "Externe Resourcen haben evtl. relative Pfadangaben und sollten deshalb nicht mit in die Pipeline aufgenommen werden"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "CSS Pipeline zuerst"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Führt die CSS Pipeline vor ausgeschlossen CSS-Verweisen aus"
+  CSS_MINIFY: "CSS minifizieren"
+  CSS_MINIFY_HELP: "Komprimiert ihre CSS Dateien nach dem pipelining"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "CSS minify Windows"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Verwendet eine andere minify Version, die auf Windows-Betriebssystemen besser funktioniert"
+  CSS_REWRITE: "CSS Umschreiben"
+  CSS_REWRITE_HELP: "Ersetze alle relativen CSS URLs beim pipelining"
+  JAVASCRIPT_PIPELINE: "Javascript Pipeline"
+  JAVASCRIPT_PIPELINE_HELP: "Kombiniert alle JavaScript Dateien zu einer"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Externe Javascript-Dateien mit in die Pipeline aufnehmen"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Externe Resourcen haben evtl. relative Pfadangaben und sollten deshalb nicht mit in die Pipeline aufgenommen werden"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Javascript Pipeline zuerst"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Bindet zuerst die Pipline und dann die ausgenommenen Skripte ein"
+  JAVASCRIPT_MINIFY: "JavaScript minifizieren"
+  JAVASCRIPT_MINIFY_HELP: "Komprimiert die JavaScript Dateien nach dem pipelining"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Zeitstempel für Assets aktivieren"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Aktviert die Zeitstempel für Assets"
+  ENABLED_SRI_ON_ASSETS: "Aktiviere SRI für Assets"
+  ENABLED_SRI_ON_ASSETS_HELP: "Aktiviert Subresource Integrity (SRI) für Assets. Mehr Informationen: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity"
+  COLLECTIONS: "Sammlungen"
+  ERROR_HANDLER: "Fehlerbehandlung"
+  DISPLAY_ERRORS: "Fehler Anzeigen"
+  DISPLAY_ERRORS_HELP: "Volle Backtrace-Fehler-Seite anzeigen"
+  LOG_ERRORS: "Fehler protokollieren"
+  LOG_ERRORS_HELP: "Fehler im /logs Ordner speichern"
+  LOG_HANDLER: "Log-Handler"
+  LOG_HANDLER_HELP: "Ausgabeort von Logs"
+  SYSLOG_FACILITY: "Syslog-Facility"
+  SYSLOG_FACILITY_HELP: "Syslog-Facility für die Ausgabe"
+  DEBUGGER: "Debugger"
+  DEBUGGER_HELP: "Aktiviert den Grav Debugger und die folgenden Einstellungen"
+  DEBUG_TWIG: "Twig debuggen"
+  DEBUG_TWIG_HELP: "Twig Templates debuggen"
+  SHUTDOWN_CLOSE_CONNECTION: "Verbindung trennen"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Trennt die Verbindung vor dem Aufruf von onShutdown(). Deaktivieren für Debugging"
+  DEFAULT_IMAGE_QUALITY: "Standard Bildqualität"
+  DEFAULT_IMAGE_QUALITY_HELP: "Standardbildqualität beim Speichern von Bildern (85%)"
+  CACHE_ALL: "Alle Bilder cachen"
+  CACHE_ALL_HELP: "Alle Bilder, auch ohne Bearbeitung, im Grav Cache ablegen"
+  IMAGES_DEBUG: "Bild Debug Wasserzeichen"
+  IMAGES_DEBUG_HELP: "Wasserzeichen mit der Auflösung der Bilder anzeigen um Retina-Displays zu testen"
+  IMAGES_LOADING: "Bildladeverhalten"
+  IMAGES_LOADING_HELP: "Das \"loading\" Attribut erlaubt es dem Browser, Bilder oder iFrames erst zu laden, wenn sie in den Sichtbereich des Nutzers gelangen"
+  IMAGES_SEOFRIENDLY: "SEO freundliche Bildnamen"
+  IMAGES_SEOFRIENDLY_HELP: "Wenn diese Option aktiviert ist, wird zuerst der Bildname und dann ein kleinerer Hash angezeigt, um verarbeitete Vorgänge anzuzeigen"
+  UPLOAD_LIMIT: "Datei Upload Limit"
+  UPLOAD_LIMIT_HELP: "Maximale Dateigröße in Bytes (0 = ohne Beschränkung)"
+  ENABLE_MEDIA_TIMESTAMP: "Zeitstempel für Mediadateien"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Fügt jedem Medienelement einen Zeitstempel der letzten Bearbeitung hinzu"
+  SESSION: "Sitzung"
+  SESSION_ENABLED_HELP: "Sessions in Grav aktivieren"
+  SESSION_NAME_HELP: "Name für den Session-Cookie"
+  SESSION_UNIQUENESS: "Primärschlüssel"
+  SESSION_UNIQUENESS_HELP: "MD5 Hash des Root-Path von Grav, z.B. `GRAV_ROOT` (Standard) oder ein auf dem zufälligen `security.salt` basierender String."
+  ABSOLUTE_URLS: "Absolute URLs"
+  ABSOLUTE_URLS_HELP: "Absolute oder Relative Pfadangaben für `base_url`"
+  PARAMETER_SEPARATOR: "Parameter Trennzeichen"
+  PARAMETER_SEPARATOR_HELP: "Trennzeichen für Parameter in der URL"
+  TASK_COMPLETED: "Aufgabe erledigt"
+  EVERYTHING_UP_TO_DATE: "Keine Aktualisierungen verfügbar"
+  UPDATES_ARE_AVAILABLE: "Aktualisierung(en) verfügbar"
+  IS_AVAILABLE_FOR_UPDATE: "ist zur Aktualisierung verfügbar"
+  IS_NOW_AVAILABLE: "ist jetzt verfügbar"
+  CURRENT: "Aktuell"
+  UPDATE_GRAV_NOW: "Grav jetzt aktualisieren"
+  GRAV_SYMBOLICALLY_LINKED: "Grav ist symbolisch Verknüpft und kann daher nicht aktualisiert werden"
+  UPDATING_PLEASE_WAIT: "Aktualisiere, bitte warten…"
+  OF_THIS: "von diesem"
+  OF_YOUR: "von deinem"
+  HAVE_AN_UPDATE_AVAILABLE: "hat ein Update verfügbar"
+  SAVE_AS: "Speichern als"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Sind Sie sicher, dass Sie diese Seite und all deren Kinder löschen wollen? Wenn diese Seite noch in anderen Sprachen vorliegt, werden diese Übersetzungen behalten & müssen separat gelöscht werden. Ansonsten wird die Seite samt Ordner mit allen Unterseiten gelöscht. Diese Aktion kann nicht rückgängig gemacht werden."
+  AND: "und"
+  UPDATE_AVAILABLE: "Aktualisierung verfügbar"
+  METADATA_KEY: "Schlüssel (z.B. 'Stichwort')"
+  METADATA_VALUE: "Wert (z.B. 'Blog, Grav')"
+  USERNAME_HELP: "Der Nutzername sollte zwischen 3 bis 16 Zeichen lang sein und darf Kleinbuchstaben, Zahlen, Unterstrichen und Bindestrichen enthalten. Großbuchstaben, Leerzeichen und Sonderzeichen sind nicht erlaubt"
+  FULLY_UPDATED: "Vollständig aktualisiert"
+  SAVE_LOCATION: "Gespeichert unter"
+  PAGE_FILE: "Seiten-Template"
+  PAGE_FILE_HELP: "Seiten-Template Dateiname, und als Standard das Anzeige-Template für diese Seite"
+  NO_USER_ACCOUNTS: "Keine Benutzerkonten gefunden, bitte erstellen Sie zuerst eines..."
+  NO_USER_EXISTS: "Für dieses Konto existiert kein lokaler Benutzer, es kann nicht gespeichert werden..."
+  REDIRECT_TRAILING_SLASH: "Weiterleiten von nachgestellten '/'"
+  REDIRECT_TRAILING_SLASH_HELP: "Eine 301 Weiterleitung durchführen anstatt nachgestellte '/' transparent zu verarbeiten."
+  DEFAULT_DATE_FORMAT: "Seiten-Datumsformat"
+  DEFAULT_DATE_FORMAT_HELP: "Das von Grav für die Seite verwendete Datumsformat. Standardmässig versucht Grav das Datum anhand Ihrer Herkunft festzulegen. Sie können das Datumsformat mithilfe der PHP Syntax festlegen (z.B. Y-m-d-H:i)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Automatisch vorschlagen"
+  IGNORE_FILES: "Dateien ignorieren"
+  IGNORE_FILES_HELP: "Dateien die beim Verarbeiten von Seiten ignoriert werden sollen"
+  IGNORE_FOLDERS: "Ignoriere Ordner"
+  IGNORE_FOLDERS_HELP: "Verzeichnisse die beim Verarbeiten von Seiten ignoriert werden sollen"
+  HIDE_EMPTY_FOLDERS: "Leere Ordner verstecken"
+  HIDE_EMPTY_FOLDERS_HELP: "Wenn der Ordner keine .md-Datei hat, sollte er in der Navigation versteckt werden und nicht routbar sein"
+  HTTP_ACCEPT_LANGUAGE: "Sprache vom Browser übernehmen"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Sie können sich entscheiden die Sprache automatisch über den 'http_accept_language' Header einzustellen"
+  OVERRIDE_LOCALE: "Spracheinstellung überschreiben"
+  OVERRIDE_LOCALE_HELP: "Lokale PHP Spracheinstellung mit aktueller Sprache überschreiben"
+  REDIRECT: "Seiten-Weiterleitung"
+  REDIRECT_HELP: "Geben Sie eine Seiten-Adresse oder externe URL ein, auf welche diese Seite weiterleiten soll - z.B. '/eine/adresse' oder 'http://www.seite.de'"
+  PLUGIN_STATUS: "Plugin Status"
+  INCLUDE_DEFAULT_LANG: "Standardsprache hinzufügen"
+  INCLUDE_DEFAULT_LANG_HELP: "Dies wird alle URLs der Standardsprache um die Sprache ergänzen z.B. '/en/blog/post' oder '/de/blog/post'"
+  INCLUDE_DEFAULT_LANG_FILE_EXTENSION: "Standardsprache zu Dateiendung hinzufügen"
+  INCLUDE_DEFAULT_LANG_HELP_FILE_EXTENSION: "Dies fügt die Standardsprache zur Dateiendung hinzu (z.B. `.de.md`). Um `.md` für die Standardsprache beizubehalten, deaktiviere diese Option."
+  PAGES_FALLBACK_ONLY: "Seiten nur 'fallback'"
+  PAGES_FALLBACK_ONLY_HELP: "'Fallback' nur um Seiteninhalte durch unterstützte Sprachen zu finden, Standardverhalten ist es, jede Sprache anzuzeigen, die gefunden wird, wenn eine aktive Sprache fehlt"
+  ALLOW_URL_TAXONOMY_FILTERS: "URL Kategoriefilter"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Seiten-basierende Sammlungen erlauben das Filtern via '/taxonomy:value'."
+  REDIRECT_DEFAULT_CODE: "Standard Weiterleitungscode"
+  REDIRECT_DEFAULT_CODE_HELP: "HTTP Statuscode für Weiterleitungen"
+  IGNORE_HIDDEN: "Versteckte ignorieren"
+  IGNORE_HIDDEN_HELP: "Alle Dateien und Ordner ignorieren, die mit einem Punkt beginnen"
+  WRAPPED_SITE: "Eingefasste Seite"
+  WRAPPED_SITE_HELP: "Damit Themes/Plugins wissen, ob Grav in eine andere Plattform eingebunden ist"
+  FALLBACK_TYPES: "Zugelassene Fallback-Typen"
+  FALLBACK_TYPES_HELP: "Erlaubte Datei-Typen auf die über Seiten-Routen zugegriffen werden kann. Standardmäßig alle unterstützen Media Typen."
+  INLINE_TYPES: "Inline Ersatz Typ"
+  INLINE_TYPES_HELP: "Eine Liste an Datei Typen die Inline angezeigt werden sollte, statt heruntergeladen zu werden"
+  APPEND_URL_EXT: "URL Endung hinzufügen"
+  APPEND_URL_EXT_HELP: "Wird eine eigene Erweiterung zu der URL der Seite hinzufügen. Damit wird Grav nach `<template>.<extension>.twig` suchen"
+  PAGE_MODES: "Seitenmodus"
+  PAGE_TYPES: "Seitentyp"
+  PAGE_TYPES_HELP: "Bestimmt die Seitentypen, die Grav unterstützt, und die Reihenfolge bestimmt, auf welche Art per 'fallback' zurückgegriffen werden soll, wenn die Anfrage nicht eindeutig ist"
+  ACCESS_LEVELS: "Zugriffslevels"
+  GROUPS: "Gruppen"
+  GROUPS_HELP: "Liste aller Gruppen in denen der Benutzer Mitglied ist"
+  ADMIN_ACCESS: "Admin Zugriff"
+  SITE_ACCESS: "Website Zugriff"
+  INVALID_SECURITY_TOKEN: "Ungültiger Sicherheitstoken"
+  ACTIVATE: "Aktivieren"
+  TWIG_UMASK_FIX: "Umask Fix"
+  TWIG_UMASK_FIX_HELP: "Twig erstellt Cache Dateien standardmäßig mit 0755, Fix setzt dass auf 0775"
+  CACHE_PERMS: "Cache Berechtigungen"
+  CACHE_PERMS_HELP: "Standardberechtigungen für den Cache-Ordner. In der Regel 0755 oder 0775 je nach Setup"
+  REMOVE_SUCCESSFUL: "Entfernen erfolgreich"
+  REMOVE_FAILED: "Entfernen fehlgeschlagen"
+  HIDE_HOME_IN_URLS: "Startseiten Adresse in URLs ausblenden"
+  HIDE_HOME_IN_URLS_HELP: "Wird sicherstellen, dass die Standard-Adresse für alle Seiten unterhalb der Startseite nicht auf die Standard-Adresse der Startseite verweisen"
+  TWIG_FIRST: "Twig-Verarbeitung zuerst ausführen"
+  TWIG_FIRST_HELP: "Falls Sie die Seiten-Generierung mittels Twig aktiviert haben, können Sie einstellen, ob diese vor oder nach der Markdown-Verarbeitung geschehen soll"
+  SESSION_SECURE: "Sicher"
+  SESSION_SECURE_HELP: "Wenn aktiviert muss die Kommunikation für Cookies über eine verschlüsselte Verbindung stattfinden. Warnung: Aktivieren Sie diese Option nur auf Seiten, die ausschließlich auf HTTPS laufen"
+  SESSION_HTTPONLY: "Nur HTTP"
+  SESSION_HTTPONLY_HELP: "Wenn aktiv, werden Cookies nur über HTTP genutzt. Eine Änderung per JavaScript ist nicht erlaubt"
+  REVERSE_PROXY: "Reverse Proxy"
+  REVERSE_PROXY_HELP: "Aktivieren Sie dies, wenn sie sich hinter einem Reverse Proxy befinden und Probleme mit URLs und inkorrekten Ports haben"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Ungültiger Frontmatter, speichern nicht möglich"
+  ADD_FOLDER: "Ordner hinzufügen"
+  COPY_PAGE: "Seite kopieren"
+  PROXY_URL: "Proxy-URL"
+  PROXY_URL_HELP: "Geben Sie den Proxy-HOST oder IP und PORT ein"
+  NOTHING_TO_SAVE: "Nichts zu speichern"
+  FILE_ERROR_ADD: "Beim Versuch, die Dateien hinzuzufügen, ist ein Fehler aufgetreten"
+  FILE_ERROR_UPLOAD: "Beim Versuch, die Dateien hochzuladen, ist ein Fehler aufgetreten"
+  FILE_UNSUPPORTED: "Nicht unterstützter Dateityp"
+  ADD_ITEM: "Eintrag hinzufügen"
+  FILE_TOO_LARGE: "Die Datei ist zu groß für den Upload, maximal zulässig sind %s <br>entsprechend Ihrer PHP-Einstellungen. Erhöhen Sie die Einstellung von 'post_max_size'"
+  INSTALLING: "Installiere"
+  LOADING: "Laden..."
+  DEPENDENCIES_NOT_MET_MESSAGE: "Die folgenden Abhängigkeiten müssen zuerst erfüllt sein:"
+  ERROR_INSTALLING_PACKAGES: "Fehler während der Paketinstallation"
+  INSTALLING_DEPENDENCIES: "Installiere Abhängigkeiten…"
+  INSTALLING_PACKAGES: "Installiere Paket(e)…"
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Paket(e) erfolgreich installiert."
+  READY_TO_INSTALL_PACKAGES: "Bereit das/die Paket(e) zu installieren"
+  PACKAGES_NOT_INSTALLED: "Pakete nicht installiert"
+  PACKAGES_NEED_UPDATE: "Pakete bereits installiert, aber zu alt"
+  PACKAGES_SUGGESTED_UPDATE: "Pakete bereits installiert und die Version ist ok, werden aber trotzdem installiert um sie aktuell zu halten"
+  REMOVE_THE: "Entferne %s"
+  CONFIRM_REMOVAL: "Sind sie sicher, dass sie %s löschen möchten?"
+  REMOVED_SUCCESSFULLY: "%s erfolgreich entfernt"
+  ERROR_REMOVING_THE: "Fehler beim Entfernen der %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "Das %s benötigt die folgenden Abhängigkeiten, die nicht von anderen installierten Pakete benötigt werden. Wenn Sie diese nicht verwenden, können Sie direkt hier entfernt werden."
+  READY_TO_UPDATE_PACKAGES: "Bereit das/die Paket(e) zu aktualisieren"
+  ERROR_UPDATING_PACKAGES: "Fehler beim Aktualisieren des/der Pakete(s)"
+  UPDATING_PACKAGES: "Aktualisiere Paket(e) ..."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Paket(e) erfolgreich aktualisiert."
+  UPDATING: "Aktualisiere"
+  GPM_RELEASES: "GPM-Releases"
+  GPM_RELEASES_HELP: "Wählen Sie 'Testbetrieb' um Beta- oder Test-Versionen zu installieren"
+  GPM_METHOD: "Methode um entfernte Inhalte zu laden"
+  GPM_METHOD_HELP: "Wenn auf Auto gestellt, wird Grav feststellen ob PHP fopen Funktion verfügbar ist und diese nutzen, ansonsten cURL. Um die Nutzung einer der beiden Optionen zu erzwingen bitten den Schalter umstellen."
+  GPM_VERIFY_PEER: "Zertifikatsüberprüfung externer Server (SSL)"
+  GPM_VERIFY_PEER_HELP: "Einige Provider scheinen an der Überprüfung des getgrav.org SSL Zertifikates zu scheitern. Dies führt dazu das GPM nicht funktioniert. Ist dies der Fall, hilft vielleicht das abstellen dieser Einstellung"
+  AUTO: "Automatisch"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stabil"
+  TESTING: "Testbetrieb"
+  FRONTMATTER_PROCESS_TWIG: "Verarbeite Frontmatter Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "Wenn aktiviert können Twig Konfigurationsvariablen im Seiten Frontmatter verwendet werden"
+  FRONTMATTER_IGNORE_FIELDS: "Ignoriere Frontmatter Felder"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Bestimmte Frontmatter Felder können Twig enthalten sollten aber möglicherweise nicht verarbeitet werden, z. B. \"Formulare\""
+  FRONTMATTER_IGNORE_FIELDS_PLACEHOLDER: "z.B. Formulare"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Paket %s erfolgreich installiert"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Übergeordnete Sortiereinstellung, Sortierung deaktiviert"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Seite ist nicht sichtbar, Sortierung deaktiviert"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Sortierung via Admin wird nicht unterstützt, weil es mehr als 200 Elemente auf gleicher Ebene gibt"
+  ORDERING_DISABLED_BECAUSE_PAGE_NO_PREFIX: "Die Sortierung ist für diese Seite deaktiviert, weil <strong>Numerisches Ordner Präfix</strong> nicht aktiviert ist"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "Hinweis: Sie können keine Medien-Dateien hochladen bis die Seite gespeichert ist. Klicken Sie dazu \"Speichern\" oben rechts."
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "Hinweis: Seite muss gespeichert werden, bevor Sie Dateien hochladen können."
+  DROP_FILES_HERE_TO_UPLOAD: "Ziehen Sie Dateien hierauf oder <strong>klicken Sie in diesem Bereich</strong>"
+  INSERT: "Einfügen"
+  UNDO: "Rückgängigmachen"
+  REDO: "Wiederholen"
+  HEADERS: "Überschriften"
+  BOLD: "Fett gedruckt"
+  ITALIC: "Kursiv"
+  STRIKETHROUGH: "Durchgestrichen"
+  SUMMARY_DELIMITER: "Trennzeichen für Zusammenfassung"
+  LINK: "Link"
+  IMAGE: "Bild"
+  BLOCKQUOTE: "Zitat"
+  UNORDERED_LIST: "Ungeordnete Liste"
+  ORDERED_LIST: "Geordnete Liste"
+  EDITOR: "Editor"
+  PREVIEW: "Vorschau"
+  FULLSCREEN: "Vollbild"
+  NON_ROUTABLE: "Nicht aufrufbar"
+  NON_VISIBLE: "Nicht sichtbar"
+  NON_PUBLISHED: "Nicht veröffentlicht"
+  CHARACTERS: "Zeichen"
+  PUBLISHING: "Veröffentlichen"
+  MEDIA_TYPES: "Medien-Typen"
+  IMAGE_OPTIONS: "Bildoptionen"
+  MIME_TYPE: "MIME-Typ"
+  THUMB: "Vorschaubild"
+  TYPE: "Typ"
+  FILE_EXTENSION: "Dateiendung"
+  LEGEND: "Seitenlegende"
+  MEMCACHE_SERVER: "Memcache Server"
+  MEMCACHE_SERVER_HELP: "Die Memcache-Server-Adresse"
+  MEMCACHE_PORT: "Memcache Port"
+  MEMCACHE_PORT_HELP: "Der Memcache-Server Port"
+  MEMCACHED_SERVER: "Memcached server"
+  MEMCACHED_SERVER_HELP: "Die Memcache-Server-Adresse"
+  MEMCACHED_PORT: "Memcache Port"
+  MEMCACHED_PORT_HELP: "Der Memcache-Server Port"
+  REDIS_SERVER: "Redis server"
+  REDIS_SERVER_HELP: "Die Redis-Server-Adresse"
+  REDIS_PORT: "Redis port"
+  REDIS_PORT_HELP: "Der Redis-Server Port"
+  REDIS_PASSWORD: "Redis Passwort/Geheimnis"
+  REDIS_DATABASE: "Redis-Datenbank-ID"
+  REDIS_DATABASE_HELP: "Die Datenbank-ID der Redis Instanz"
+  ALL: "Alle"
+  FROM: "von"
+  TO: "an"
+  RELEASE_DATE: "Veröffentlichungsdatum"
+  SORT_BY: "Sortieren nach"
+  RESOURCE_FILTER: "Filtere..."
+  FORCE_SSL: "SSL erzwingen"
+  FORCE_SSL_HELP: "Globales SSL erzwingen, wenn aktiviert sendet Grav für eine HTTP Anfrage eine Weiterleitung zur HTTPS-Seite"
+  NEWS_FEED: "Neuigkeiten"
+  EXTERNAL_URL: "Externe URL"
+  SESSION_SAMESITE: "Session SameSite Attribut"
+  SESSION_SAMESITE_HELP: "Lax|Strict|None. Mehr Informationen: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"
+  CUSTOM_BASE_URL: "Benutzerdefinierte Base-URL"
+  CUSTOM_BASE_URL_HELP: "Verwenden Sie diese Einstellung, wenn ein Rewrite der Site-Domain durchgeführt oder ein anderer Unterordner als der von Grav verwendete genutzt werden soll. Beispiel: http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'Kann "%s" nicht außerhalb von Seiten verwenden.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'Kann Datei nicht hochladen %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'Kann Datei %s nicht nach "%s" verschieben'
+  DROPZONE_CANCEL_UPLOAD: 'Upload abbrechen'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Sind Sie sicher, dass Sie den Upload abbrechen wollen?'
+  DROPZONE_DEFAULT_MESSAGE: 'Ziehen Sie Dateien hierher oder <strong>klicken Sie in diesem Bereich</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'Ihr Browser unterstützt Drag und Drop Datei-Uploads nicht.'
+  DROPZONE_FALLBACK_TEXT: 'Bitte nutzen Sie das untenstehende Formular um Dateien wie in alten Zeiten hochzuladen.'
+  DROPZONE_FILE_TOO_BIG: 'Die Datei ist zu groß ({{filesize}}MiB). Maximale Dateigröße: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "Sie können Dateien dieses Typs nicht hochladen."
+  DROPZONE_MAX_FILES_EXCEEDED: "Sie können keine weiteren Dateien hochladen."
+  DROPZONE_REMOVE_FILE: "Datei entfernen"
+  DROPZONE_RESPONSE_ERROR: "Server antwortete mit Statuscode {{statusCode}}."
+  PREMIUM_PRODUCT: "Premium"
+  DESTINATION_NOT_SPECIFIED: "Ziel nicht angegeben"
+  UPLOAD_ERR_NO_TMP_DIR: "Ordner für temporäre Dateien fehlt"
+  SESSION_SPLIT: "Geteilte Session"
+  SESSION_SPLIT_HELP: "Unabhängig geteilte Sessions zwischen der Site und anderen Plugins (wie zum Beispiel Admin)"
+  ERROR_FULL_BACKTRACE: "Vollständiger Fehler-Backtrace"
+  ERROR_SIMPLE: "Einfacher Fehler"
+  ERROR_SYSTEM: "Systemfehler"
+  IMAGES_AUTO_FIX_ORIENTATION: "Bildausrichtung automatisch beheben"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Bildausrichtung anhand der Exif-Daten automatisch beheben"
+  REDIS_SOCKET: "Redis Socket"
+  REDIS_SOCKET_HELP: "Der Redis-Socket"
+  NOT_SET: "Nicht eingestellt"
+  PERMISSIONS: "Zugriffsrechte"
+  NEVER_CACHE_TWIG: "Twig niemals chachen"
+  NEVER_CACHE_TWIG_HELP: "Nur Inhalte zwischenspeichern und Twig für Seiten jedes Mal verarbeiten. Ignoriert die Einstellung twig_first."
+  ALLOW_WEBSERVER_GZIP: "Webserver Gzip erlauben"
+  ALLOW_WEBSERVER_GZIP_HELP: "Ist standardmäßig deaktiviert. Wenn diese Option aktiviert ist, wird die WebServer konfigurierte Gzip/Deflate-Komprimierung verwendet, aber die HTTP-Verbindung wird nicht vor dem onShutDown() Ereignis geschlossen, was ein langsameres laden der Seite bedeutet"
+  OFFLINE_WARNING: "Es kann keine Verbindung zu GPM hergestellt werden"
+  CLEAR_IMAGES_BY_DEFAULT: "Standardmässig wird der image cache gelöscht"
+  CLEAR_IMAGES_BY_DEFAULT_HELP: "Standardmäßig werden bearbeitete Bilder bei alle Cache-Löschungen gelöscht, dies kann deaktiviert werden"
+  CLI_COMPATIBILITY: "CLI Kompatibilität"
+  CLI_COMPATIBILITY_HELP: "Sorgt dafür, dass nur nicht-flüchtige Cache Treiber verwendet werden (Datei, Redis, Memcache, etc.)"
+  REINSTALL_PLUGIN: "Plugin erneut installieren"
+  REINSTALL_THEME: "Theme erneut installieren"
+  REINSTALL_THE: "%s erneut installieren"
+  CONFIRM_REINSTALL: "Sind sie sicher, dass sie %s erneut installieren möchten?"
+  REINSTALLED_SUCCESSFULLY: "%s erfolgreich erneut installiert"
+  ERROR_REINSTALLING_THE: "Fehler bei der erneuten Installation von %s"
+  PACKAGE_X_REINSTALLED_SUCCESSFULLY: "Paket %s erfolgreich erneut installiert"
+  REINSTALLATION_FAILED: "Erneute Installation fehlgeschlagen"
+  WARNING_REINSTALL_NOT_LATEST_RELEASE: "Die installierte Version ist nicht die neueste verfügbare Version. Durch Klicken auf Weiter, entfernen Sie die aktuelle Version und installieren Sie die neueste verfügbare Version"
+  TOOLS: "Werkzeuge"
+  DIRECT_INSTALL: "Direkte Installation"
+  NO_PACKAGE_NAME: "Name des Pakets nicht angegeben"
+  PACKAGE_EXTRACTION_FAILED: "Paket konnte nicht ausgepackt werden"
+  NOT_VALID_GRAV_PACKAGE: "Kein gültiges Grav-Paket"
+  NAME_COULD_NOT_BE_DETERMINED: "Name konnte nicht ermittelt werden"
+  CANNOT_OVERWRITE_SYMLINKS: "Symbolische Links können nicht überschrieben werden"
+  ZIP_PACKAGE_NOT_FOUND: "ZIP Archiv wurde nicht gefunden"
+  GPM_OFFICIAL_ONLY: "Nur offizielle GPM Server"
+  GPM_OFFICIAL_ONLY_HELP: "Erlauben Sie direkte Installationen nur aus dem offiziellen GPM-Repository."
+  NO_CHILD_TYPE: "Kein Unterseitentyp für diese Rawroute"
+  SORTABLE_PAGES: "Sortierbare Seiten:"
+  UNSORTABLE_PAGES: "Nicht sortierbare Seiten"
+  ADMIN_SPECIFIC_OVERRIDES: "Administrative Anpassungen"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Sortierung der Unterseiten"
+  ADMIN_CHILDREN_DISPLAY_ORDER_HELP: "Die Reihenfolge, die vom Admin Plugin verwendet werden soll um Unterseiten in der \"Seiten\" Ansicht anzuzeigen"
+  PWD_PLACEHOLDER: "komplexe Zeichenfolge mindestens 8 Zeichen lang"
+  PWD_REGEX: "Regulärer Ausdruck für Passwörter"
+  PWD_REGEX_HELP: "Standard: Das Passwort muss mindestens eine Zahl, einen Groß- und einen Kleinbuchstaben enthalten sowie mindestens 8 Zeichen lang sein"
+  USERNAME_PLACEHOLDER: "nur Kleinbuchstaben, z.B. 'admin'"
+  USERNAME_REGEX: "Regulärer Ausdruck für Benutzernamen"
+  USERNAME_REGEX_HELP: "Standard: nur Kleinbuchstaben, Ziffern, Bindestriche und Unterstriche. 3 - 16 Zeichen"
+  ENABLE_AUTO_METADATA: "Auto-Metadaten aus Exif"
+  ENABLE_AUTO_METADATA_HELP: "Automatische Generierung von Metadaten-Dateien für Bilder mit Exif-Informationen"
+  2FA_TITLE: "2-Faktor-Authentifizierung"
+  2FA_INSTRUCTIONS: "##### 2-Faktor-Authentifizierung\nSie haben **2FA** für dieses Konto aktiviert. Bitte benutzen Sie Ihre **2FA** App, um den aktuellen **6-stelligen Code** einzugeben, um den Anmeldevorgang abzuschließen."
+  2FA_REGEN_HINT: "Das Erneuern des Geheimnisses erfordert es, dass Sie Ihre Authenticator App aktualisieren"
+  2FA_LABEL: "Admin Zugriff"
+  2FA_FAILED: "Ungültiger 2-Faktor-Authentifizierungscode, bitte versuchen Sie es erneut..."
+  2FA_ENABLED: "2FA aktiviert"
+  2FA_CODE_INPUT: "000000"
+  2FA_SECRET: "2FA Geheimnis"
+  2FA_SECRET_HELP: "Scannen Sie diesen QR-Code in Ihre [Authenticator App] (https://learn.getgrav.org/admin-panel/2fa#apps). Außerdem ist es eine gute Idee, das Geheimnis an einem sicheren Ort zu sichern, falls Sie Ihre App neu installieren müssen. Weitere Informationen finden Sie in den [Grav docs] (https://learn.getgrav.org/admin-panel/2fa) "
+  2FA_REGENERATE: "Neu generieren"
+  FORCE_LOWERCASE_URLS: "Erzwinge URLs in Kleinbuchstaben"
+  FORCE_LOWERCASE_URLS_HELP: "Standardmäßig setzt Grav alle Slugs und Routen in Kleinbuchstaben. Wenn dieser Wert auf \"false\" gesetzt wird, können auch Großbuchstaben für Slugs und Routen verwendet werden"
+  INTL_ENABLED: "Intl Modulintegration"
+  INTL_ENABLED_HELP: "Verwenden Sie das Intl PHP-Modul um UTF8-basierte Sammlungen zu sortieren"
+  VIEW_SITE_TIP: "Seite anzeigen"
+  TOOLS_DIRECT_INSTALL_TITLE: "Direktinstallation von Grav-Paketen"
+  TOOLS_DIRECT_INSTALL_UPLOAD_TITLE: "Paket mittels \"Direct ZIP Upload\" installieren"
+  TOOLS_DIRECT_INSTALL_UPLOAD_DESC: "Sie können einfach ein gültiges Grav <strong>Theme</strong>, <strong>Plugin</strong>oder sogar <strong>Grav</strong> Update-Zip-Paket über diese Methode aktualisieren. Dieses Paket muss nicht über GPM registriert werden und erlaubt es Ihnen, einfach auf eine vorherige Version zurückzurollen oder zu testen."
+  TOOLS_DIRECT_INSTALL_URL_TITLE: "Paket mittels \"Remote URL Reference\" installieren"
+  TOOLS_DIRECT_INSTALL_URL_DESC: "Alternativ können Sie auch eine vollständige URL zu der gezippten Paketdatei angeben um das Paket über eine Fremd-URL zu installieren."
+  TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Hochladen und installieren"
+  ROUTE_OVERRIDES: "Route Overrides"
+  ROUTE_DEFAULT: "Standardroute"
+  ROUTE_CANONICAL: "Canonical Route"
+  ROUTE_ALIASES: "Routen-Aliase"
+  OPEN_NEW_TAB: "In neuem Tab öffnen"
+  SESSION_INITIALIZE: "Session initialisieren"
+  SESSION_INITIALIZE_HELP: "Lässt Grav eine Sitzung starten. Diese Funktion ist erforderlich, damit jede Benutzerinteraktion funktioniert, wie z. B. Login, Formulare usw. Das Admin-Plugin ist von dieser Einstellung nicht betroffen."
+  STRICT_YAML_COMPAT: "YAML Kompatibilität"
+  STRICT_YAML_COMPAT_HELP: "Greift zurück auf Symfony 2.4 YAML Parser, wenn Native oder 3.4 Parser nicht funktioniert"
+  STRICT_TWIG_COMPAT: "Twig Kompatibilität"
+  STRICT_TWIG_COMPAT_HELP: "Aktiviert die veraltete Twig Autoescape Einstellung.  Wenn sie deaktiviert ist, wird der |raw Filter benötigt, um HTML auszugeben, da Twig Autoescape ausgibt"
+  SCHEDULER: "Planer"
+  SCHEDULER_INSTALL_INSTRUCTIONS: "Installationsanleitung"
+  SCHEDULER_INSTALLED_READY: "Installiert und bereit"
+  SCHEDULER_CRON_NA: "Cron nicht verfügbar für Benutzer: <b>%s</b>"
+  SCHEDULER_NOT_ENABLED: "Nicht aktiviert für Benutzer: <b>%s</b>"
+  SCHEDULER_SETUP: "Planer-Setup"
+  SCHEDULER_INSTRUCTIONS: "Mit dem <b>Grav Scheduler</b> können benutzerdefinierte Jobs erstellt und geplant werden. Es stehe auch eine Schnittstelle für Grav-Plugins bereit um dynamisch Jobs hinzuzufügen, welche in regelmäßigen Abständen ausgeführt werden sollen."
+  SCHEDULER_JOBS: "Individuelle Scheduler Jobs"
+  SCHEDULER_STATUS: "Planer-Status"
+  SCHEDULER_RUNAT: "Starten um"
+  SCHEDULER_RUNAT_HELP: "Cron formatierte 'at' Syntax"
+  SCHEDULER_OUTPUT: "Ausgabedatei"
+  SCHEDULER_OUTPUT_HELP: "Pfad/Dateiname der Ausgabedatei (aus dem Root der Grav-Installation)"
+  SCHEDULER_OUTPUT_TYPE: "Ausgabetyp"
+  SCHEDULER_OUTPUT_TYPE_HELP: "Entweder an die gleiche Datei bei jedem Run anhängen oder die Datei bei jedem Run überschreiben"
+  SCHEDULER_EMAIL: "E-Mail"
+  SCHEDULER_EMAIL_HELP: "E-Mail-Adresse zum Empfangen der Ausgabedatei. HINWEIS: Die Ausgabedatei muss konfiguriert sein"
+  SECURITY: "Sicherheit"
+  XSS_SECURITY: "XSS-Sicherheit für Inhalte"
+  XSS_WHITELIST_PERMISSIONS: "Whitelist-Berechtigungen"
+  XSS_WHITELIST_PERMISSIONS_HELP: "Benutzer mit diesen Berechtigungen können die XSS-Regeln beim Speichern von Inhalten überspringen"
+  XSS_ON_EVENTS: "Filter On-events"
+  XSS_INVALID_PROTOCOLS: "Filter Ungültige Protokolle"
+  XSS_INVALID_PROTOCOLS_LIST: "Liste ungültiger Protokolle"
+  XSS_MOZ_BINDINGS: "Filter Moz-Bindungen"
+  XSS_HTML_INLINE_STYLES: "Filter HTML Inline Styles"
+  XSS_DANGEROUS_TAGS: "Filter Gefährliche HTML-Tags"
+  XSS_DANGEROUS_TAGS_LIST: "Liste gefährlicher HTML-Tags"
+  XSS_ONSAVE_ISSUE: "Speichern fehlgeschlagen: XSS-Problem erkannt..."
+  XSS_ISSUE: "<strong>HINWEIS:</strong> Grav hat potenzielle XSS-Probleme in <strong>%s</strong> gefunden"
+  UPLOADS_SECURITY: "Uploads Sicherheit"
+  UPLOADS_DANGEROUS_EXTENSIONS: "Gefährliche Erweiterungen"
+  UPLOADS_DANGEROUS_EXTENSIONS_HELP: "Diese Dateierweiterungen können auch unabhängig der freigegebenen MIME-Types nicht hochgeladen werden"
+  REPORTS: "Berichte"
+  LOGS: "Logs"
+  LOG_VIEWER_FILES: "Log Viewer Dateien"
+  LOG_VIEWER_FILES_HELP: "Dateien in /logs/ welche unter Werkzeuge - Logs angezeigt werden. z.B. 'grav' = /logs/grav.log"
+  BACKUPS_STORAGE_PURGE_TRIGGER: "Auslöser für Löschung alter Sicherungen"
+  BACKUPS_MAX_COUNT: "Maximale Anzahl an Sicherungen"
+  BACKUPS_MAX_COUNT_HELP: "0 = unbegrenzt"
+  BACKUPS_MAX_SPACE: "Maximaler Speicherplatz für Sicherungen"
+  BACKUPS_MAX_RETENTION_TIME: "Maximale Speicherungsdauer"
+  BACKUPS_MAX_RETENTION_TIME_APPEND: "in Tagen"
+  BACKUPS_PROFILE_NAME: "Sicherungsname"
+  BACKUPS_PROFILE_ROOT_FOLDER: "Stammordner"
+  BACKUPS_PROFILE_ROOT_FOLDER_HELP: "Kann ein absoluter Pfad oder ein Stream sein"
+  BACKUPS_PROFILE_EXCLUDE_PATHS: "Pfade ausschließen"
+  BACKUPS_PROFILE_EXCLUDE_PATHS_HELP: "Absolute Pfade zum Ausschließen, einer pro Zeile"
+  BACKUPS_PROFILE_EXCLUDE_FILES: "Dateien ausschließen"
+  BACKUPS_PROFILE_EXCLUDE_FILES_HELP: "Bestimmte Dateien oder Ordner, die ausgeschlossen werden sollen, eine pro Zeile"
+  BACKUPS_PROFILE_SCHEDULE: "Geplanten Job aktivieren"
+  BACKUPS_PROFILE_SCHEDULE_AT: "Geplanten Job ausführen"
+  COMMAND: "Befehl"
+  EXTRA_ARGUMENTS: "Zusätzliche Argumente"
+  DEFAULT_LANG: "Standardsprache überschreiben"
+  DEBUGGER_PROVIDER: "Debugger-Provider"
+  DEBUGGER_DEBUGBAR: "PHP Debug Bar"
+  DEBUGGER_CLOCKWORK: "Clockwork-Browser-Erweiterung"
+  CONTENT_LANGUAGE_FALLBACKS: "Ausweichsprache für Inhalte"
+  CONTENT_LANGUAGE_FALLBACKS_HELP: "Standardmäßig wird Grav Inhalte in der Standardsprache anzeigen, solange der jeweilige Inhalt nicht übersetzt ist. Verwenden Sie diese Einstellung, um das Verhalten pro Sprache zu überschreiben."
+  CONTENT_LANGUAGE_FALLBACK: "Fallback-Sprachen"
+  CONTENT_FALLBACK_LANGUAGE_HELP: "Geben Sie den Sprachcode an, den Sie anpassen möchten."
+  EXPERIMENTAL: "Experimentell"
+  FLEX: "Flex-Objekt (EXPERIMENTELL)"
+  REGULAR: "Standard"
+  FILE: "Datei"
+  SANITIZE_SVG: "SVG säubern"
+  SANITIZE_SVG_HELP: "Entfernt XSS-Code aus SVG"
+  ACCOUNTS: "Benutzerkonten"
+  USER_ACCOUNTS: "Benutzerkonten"
+  USER_GROUPS: "Benutzergruppen"
+  GROUP_NAME: "Gruppenname"
+  DISPLAY_NAME: "Anzeigename"
+  ICON: "Icon"
+  ACCESS: "Zugriff"
+  NO_ACCESS: "Kein Zugriff"
+  ALLOWED: "Erlaubt"
+  DENIED: "Verweigert"
+  MODULE: "Modul"
+  ADD_MODULE: "Modul hinzufügen"
+  MODULE_SETUP: "Modul einrichten"
+  MODULE_TEMPLATE: "Modulvorlage"
+  ADD_MODULE_CONTENT: "Modulinhalt hinzufügen"
+  CHANGELOG: "Änderungsprotokoll"
+  PAGE_ACCESS: "Seitenzugriff"
+  PAGE PERMISSIONS: "Seitenberechtigungen"
+  PAGE_ACCESS_HELP: "Benutzer mit folgenden Zugriffsberechtigungen kann auf die Seite zugreifen."
+  PAGE_VISIBILITY_REQUIRES_ACCESS: "Menüsichtbarkeit erfordert Zugriff"
+  PAGE_VISIBILITY_REQUIRES_ACCESS_HELP: "Auf Ja setzen, wenn die Seite nur im Menü angezeigt werden soll, wenn der Benutzer auf sie zugreifen kann."
+  PAGE_INHERIT_PERMISSIONS: "Berechtigungen erben"
+  PAGE_INHERIT_PERMISSIONS_HELP: "ACL der übergeordneten Seite erben"
+  PAGE_AUTHORS: "Seitenautoren"
+  PAGE_AUTHORS_HELP: "Mitglieder von Seitenautoren haben Zugriff auf die Eigentümerebene auf diese Seite, die in einer speziellen Gruppe \"Autoren\" definiert ist."
+  PAGE_GROUPS: "Seitengruppen"
+  PAGE_GROUPS_HELP: "Mitglieder von Seitengruppen haben besonderen Zugriff auf diese Seite."
+  READ: "Lesen"
+  PUBLISH: "Veröffentlichen"
+  LIST: "Liste"
+  ACCESS_SITE: "Website"
+  ACCESS_SITE_LOGIN: "An der Website anmelden"
+  ACCESS_ADMIN: "Admin"
+  ACCESS_ADMIN_LOGIN: "An der Administrationsoberfläche anmelden"
+  ACCESS_ADMIN_SUPER: "Superbenutzer"
+  ACCESS_ADMIN_CACHE: "Cache leeren"
+  ACCESS_ADMIN_CONFIGURATION: "Konfiguration"
+  ACCESS_ADMIN_CONFIGURATION_SYSTEM: "Systemkonfiguration verwalten"
+  ACCESS_ADMIN_CONFIGURATION_SITE: "Seitenkonfiguration verwalten"
+  ACCESS_ADMIN_CONFIGURATION_INFO: "Serverinformationen anzeigen"
+  ACCESS_ADMIN_SETTINGS: "Einstellungen"
+  ACCESS_ADMIN_PAGES: "Seiten verwalten"
+  ACCESS_ADMIN_STATISTICS: "Seiten-Statistik"
+  ACCESS_ADMIN_PLUGINS: "Plugins verwalten"
+  ACCESS_ADMIN_THEMES: "Themes verwalten"
+  ACCESS_ADMIN_USERS: "Benutzer verwalten"
+  USERS: "Benutzer"
+  ACL: "Zugriffskontrolle"
+  FLEX_OBJECT_CACHE_ENABLED: "Objekt-Cache aktivieren"
+  FLEX_RENDER_CACHE_ENABLED: "Render-Cache aktivieren"
+  DEBUGGER_CENSORED: "Sensible Daten verbergen"
+  LANGUAGE_TRANSLATIONS: "Übersetzungen"
+  RESET: "Zurücksetzen"
+  LOGOS: "Logos"
+  PRESETS: "Vorlagen"
+  COLOR_SCHEME_LABEL: "Farbschema"
+  COLOR_SCHEME_NAME_PLACEHOLDER: "Blautöne"
+  PRIMARY_ACCENT_LABEL: "Primärer Farbton"
+  SECONDARY_ACCENT_LABEL: "Sekundärer Farbton"
+  TERTIARY_ACCENT_LABEL: "Tertiärer Farbton"
+  WEB_FONTS_LABEL: "Web-Schriftarten"
+  HEADER_FONT_LABEL: "Schriftart Kopfzeile"
+  CUSTOM_CSS_LABEL: "Benutzerdefiniertes CSS"
+  CUSTOM_FOOTER: "Benutzerdefinierte Fußzeile"
+  CUSTOM_FOOTER_HELP: "Sie können hier HTML- und/oder Markdown-Syntax verwenden"
+  LOAD_PRESET: "Voreinstellung laden"
+  RECOMPILE: "Neu kompilieren"
+  EXPORT: "Exportieren"
+  CODEMIRROR_THEME_DESC: "**HINWEIS:** Benutze die [CodeMirror Themes Demo](https://codemirror.net/demo/theme.html?target=_blank), um diese in Aktion zu sehen. **_Paper_** ist das Standard Grav Theme."
+  CODEMIRROR_FONTSIZE_SM: "Kleine Schriftgröße"
+  CODEMIRROR_FONTSIZE_MD: "Mittlere Schriftgröße"
+  CODEMIRROR_FONTSIZE_LG: "Große Schriftgröße"
+  CUSTOM_PRESETS: "Benutzerdefinierte Voreinstellung"
+  GENERAL: "Allgemein"
+  BAD_FILENAME: "Ungültiger Dateiname"
+  SHOW_SENSITIVE: "Sensible Daten anzeigen"
+  CONFIGURATION: "Einstellungen"
+  EXTRAS: "Extras"
+  BASICS: "Grundlagen"
+  ADMIN_CACHING: "Aktivere Admin Caching"
+  ADMIN_CACHING_HELP: "Caching für den Admin-Bereich kann unabhängig vom Front-End kontrolliert werden"
+  ADMIN_PATH: "Administratorpfad"
+  ADMIN_PATH_HELP: "Wenn Sie die URL für den Administrator ändern möchten, können Sie hier einen Pfad angeben"
+  LOGO_TEXT: "Logotext"
+  LOGO_TEXT_HELP: "Text, der anstelle des Standard-Grav-Logos angezeigt wird"
+  CONTENT_PADDING: "Abstand um Inhalte"
+  CONTENT_PADDING_HELP: "Abstand um Inhalte aktivieren/deaktivieren um mehr Platz anzubieten"
+  BODY_CLASSES: "Body-CSS-Klassen"
+  BODY_CLASSES_HELP: "Fügen Sie einen durch Leerzeichen getrennten Namen benutzerdefinierter Body-CSS-Klassen hinzu"
+  SIDEBAR_ACTIVATION: "Seitenleistenaktivierung"
+  SIDEBAR_ACTIVATION_HELP: "Legen Sie fest, wie die Seitenleiste aktiviert wird"
+  SIDEBAR_HOVER_DELAY_APPEND: "Millisekunde"
+  SIDEBAR_ACTIVATION_TAB: "Tab"
+  SIDEBAR_SIZE: "Seitenleistengröße"
+  SIDEBAR_SIZE_HELP: "Steuert die Breite der Seitenleiste"
+  SIDEBAR_SIZE_AUTO: "Automatische Breite"
+  SIDEBAR_SIZE_SMALL: "Kleine Breite"
+  EDIT_MODE: "Bearbeitungsmodus"
+  FRONTEND_PREVIEW_TARGET_NEW: "Neuer Tab"
+  FRONTEND_PREVIEW_TARGET_CURRENT: "Aktueller Tab"
+  PARENT_DROPDOWN: "Übergeordnete Dropdown-Liste"
+  PARENT_DROPDOWN_FOLDER: "Ordner anzeigen"
+  PARENT_DROPDOWN_FULLPATH: "Vollständigen Pfad anzeigen"
+  PARENTS_LEVELS: "Übergeordnete Ebenen"
+  PARENTS_LEVELS_HELP: "Anzahl der Ebenen, die in der übergeordneten Auswahlliste angezeigt werden sollen"
+  MODULAR_PARENTS_HELP: "Zeige modulare Seiten in der übergeordneten Auswahlliste"
+  SHOW_GITHUB_LINK: "Zeige GitHub Link"
+  SHOW_GITHUB_LINK_HELP: "Zeige die Nachricht \"Problem gefunden? Bitte melde es auf GitHub\" an."
+  PAGES_LIST_DISPLAY_FIELD: "Seitenlistenanzeigefeld"
+  AUTO_UPDATES: "Automatisch nach Updates suchen"
+  AUTO_UPDATES_HELP: "Zeigt eine informative Nachricht im Admin-Panel, wenn ein Update verfügbar ist."
+  TIMEOUT: "Zeitüberschreitung"
+  TIMEOUT_HELP: "Session Timeout in Sekunden"
+  HIDE_PAGE_TYPES: "Seitentypen im Admin ausblenden"
+  HIDE_MODULAR_PAGE_TYPES: "Modulare Seitentypen im Admin ausblenden"
+  DASHBOARD: "Übersicht"
+  WIDGETS_DISPLAY: "Widget-Anzeigestatus"
+  NOTIFICATIONS: "Benachrichtigungen"
+  FEED_NOTIFICATIONS: "Feed-Benachrichtigungen"
+  BUTTON_BG_HELP: "Schaltflächenhintergrund"
+  BUTTON_TEXT_HELP: "Schaltflächentext"
+  BUTTON_COLORS: "Schaltflächenfarben"
+  LOGO_COLORS: "Logofarben"
+  PAGE_COLORS: "Seitenfarben"
+  POPULARITY: "Beliebtheit"
+  IGNORE_URLS: "Ignorieren"
+  IGNORE_URLS_HELP: "Zu ignorierende URLs"
+  DAILY_HISTORY: "Tagesverlauf"
+  MONTHLY_HISTORY: "Monatsverlauf"
+  VISITORS_HISTORY: "Besucherverlauf"
+  PAGEMEDIA_RESIZER: "> Die folgenden Einstellungen gelten für Bilder, die über die \"Media\" Seite hochgeladen werden. \"Resize Width / Height\" verkleinert ein Bild automatisch und proportional, wenn die voreingestellten Limits überschritten werden. Die Min- und Max-Werte der Auflösung definieren die Größenbereiche für hochgeladene Bilder. Setzen Sie die Felder auf 0, um Manipulationen zu vermeiden."
+  RESIZE_WIDTH: "Breite ändern"
+  RESIZE_HEIGHT: "Höhe ändern"
+  PIXELS: "Pixel"
+  IMAGES_CLS_AUTO_SIZES: "Automatische Größenanpassung aktivieren"

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 24 - 0
plugins/admin/languages/el.yaml


+ 1154 - 0
plugins/admin/languages/en.yaml

@@ -0,0 +1,1154 @@
+PLUGIN_ADMIN:
+  ADMIN_NOSCRIPT_MSG: "Please enable JavaScript in your browser."
+  ADMIN_BETA_MSG: "This is a Beta release! Use this in production at your own risk..."
+  ADMIN_REPORT_ISSUE: "Found an issue? Please report it on GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Powered by Grav</a> - The Modern Flat File CMS"
+  LOGIN_BTN: "Login"
+  LOGIN_BTN_FORGOT: "Forgot"
+  LOGIN_BTN_RESET: "Reset Password"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Send Reset Instructions"
+  LOGIN_BTN_CLEAR: "Clear Form"
+  LOGIN_BTN_CREATE_USER: "Create User"
+  LOGIN_LOGGED_IN: "You have been successfully logged in"
+  LOGIN_FAILED: "Login failed"
+  LOGGED_OUT: "You have been logged out"
+  RESET_NEW_PASSWORD: "Please enter a new password &hellip;"
+  RESET_LINK_EXPIRED: "Reset link has expired, please try again"
+  RESET_PASSWORD_RESET: "Password has been reset"
+  RESET_INVALID_LINK: "Invalid reset link used, please try again"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Instructions to reset your password have been sent to your email address"
+  FORGOT_FAILED_TO_EMAIL: "Failed to email instructions, please try again later"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Cannot reset password for %s, no email address is set"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "User with username <b>%s</b> does not exist"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Cannot reset password. This site is not configured to send emails"
+  FORGOT_EMAIL_SUBJECT: "%s Password Reset Request"
+  FORGOT_EMAIL_BODY: "<h1>Password Reset</h1><p>Dear %1$s,</p><p>A request was made on <b>%4$s</b> to reset your password.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Click this to reset your password</a><br /><br /></p><p>Alternatively, copy the following URL into your browser's address bar:</p> <p>%2$s</p><p><br />Kind regards,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Manage Pages"
+  PAGES: "Pages"
+  PLUGINS: "Plugins"
+  PLUGIN: "Plugin"
+  THEMES: "Themes"
+  LOGOUT: "Logout"
+  BACK: "Back"
+  NEXT: "Next"
+  PREVIOUS: "Previous"
+  ADD_PAGE: "Add Page"
+  MOVE: "Move"
+  DELETE: "Delete"
+  UNSET: "Unset"
+  VIEW: "View"
+  SAVE: "Save"
+  NORMAL: "Normal"
+  EXPERT: "Expert"
+  EXPAND_ALL: "Expand All"
+  COLLAPSE_ALL: "Collapse All"
+  ERROR: "Error"
+  CLOSE: "Close"
+  CANCEL: "Cancel"
+  CONTINUE: "Continue"
+  CONFIRM: "Confirm"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Confirmation Required"
+  MODAL_CHANGED_DETECTED_TITLE: "Changes Detected"
+  MODAL_CHANGED_DETECTED_DESC: "You have unsaved changes.  Are you sure you want to leave without saving?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Confirmation Required"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Are you sure you want to delete this file? This action cannot be undone."
+  MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "You are about to upgrade Grav to the latest version available. Would you like to continue?"
+  ADD_FILTERS: "Add Filters"
+  SEARCH_PAGES: "Search Pages"
+  VERSION: "Version"
+  WAS_MADE_WITH: "Was made with"
+  BY: "By"
+  UPDATE_THEME: "Update Theme"
+  UPDATE_PLUGIN: "Update Plugin"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "of this theme is now available"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "of this plugin is now available"
+  AUTHOR: "Author"
+  HOMEPAGE: "Homepage"
+  DEMO: "Demo"
+  BUG_TRACKER: "Bug Tracker"
+  KEYWORDS: "Keywords"
+  LICENSE: "License"
+  DESCRIPTION: "Description"
+  README: "Readme"
+  DOCS: "Docs"
+  REMOVE_THEME: "Remove Theme"
+  INSTALL_THEME: "Install Theme"
+  THEME: "Theme"
+  BACK_TO_THEMES: "Back to Themes"
+  BACK_TO_PLUGINS: "Back to Plugins"
+  CHECK_FOR_UPDATES: "Check for Updates"
+  ADD: "Add"
+  CLEAR_CACHE: "Clear Cache"
+  CLEAR_CACHE_ALL_CACHE: "All Cache"
+  CLEAR_CACHE_ASSETS_ONLY: "Assets Only"
+  CLEAR_CACHE_IMAGES_ONLY: "Images Only"
+  CLEAR_CACHE_CACHE_ONLY: "Cache Only"
+  CLEAR_CACHE_TMP_ONLY: "Tmp Only"
+  UPDATES_AVAILABLE: "Updates Available"
+  DAYS: "Days"
+  UPDATE: "Update"
+  BACKUP: "Backup"
+  BACKUPS: "Backups"
+  BACKUP_NOW: "Backup Now"
+  BACKUPS_STATS: "Backup Statistics"
+  BACKUPS_HISTORY: "Backup History"
+  BACKUPS_PURGE_CONFIG: "Backup Purge Configuration"
+  BACKUPS_PROFILES: "Backup Profiles"
+  BACKUPS_COUNT: "Number of Backups"
+  BACKUPS_PROFILES_COUNT: "Number of Profiles"
+  BACKUPS_TOTAL_SIZE: "Space Used"
+  BACKUPS_NEWEST: "Newest Backup"
+  BACKUPS_OLDEST: "Oldest Backup"
+  BACKUPS_PURGE: "Purge"
+  BACKUPS_NOT_GENERATED: "No backups have been generated yet..."
+  BACKUPS_PURGE_NUMBER: "Using %s of %s backup slots"
+  BACKUPS_PURGE_TIME: "%s days of backups left"
+  BACKUPS_PURGE_SPACE: "Using %s of %s"
+  BACKUP_DELETED: "Backup Successfully Deleted"
+  BACKUP_NOT_FOUND: "Backup Not Found"
+  BACKUP_DATE: "Backup Date"
+  STATISTICS: "Statistics"
+  VIEWS_STATISTICS: "Page View Statistics"
+  TODAY: "Today"
+  WEEK: "Week"
+  MONTH: "Month"
+  LATEST_PAGE_UPDATES: "Latest Page Updates"
+  MAINTENANCE: "Maintenance"
+  UPDATED: "Updated"
+  MON: "Mon"
+  TUE: "Tue"
+  WED: "Wed"
+  THU: "Thu"
+  FRI: "Fri"
+  SAT: "Sat"
+  SUN: "Sun"
+  COPY: "Copy"
+  EDIT: "Edit"
+  CREATE: "Create"
+  GRAV_ADMIN: "Grav Admin"
+  GRAV_OFFICIAL_PLUGIN: "Grav Official Plugin"
+  GRAV_OFFICIAL_THEME: "Grav Official Theme"
+  PLUGIN_SYMBOLICALLY_LINKED: "This plugin is symbolically linked. Updates won't be detected."
+  THEME_SYMBOLICALLY_LINKED: "This theme is symbolically linked. Updates won't be detected"
+  REMOVE_PLUGIN: "Remove Plugin"
+  INSTALL_PLUGIN: "Install Plugin"
+  AVAILABLE: "Available"
+  INSTALLED: "Installed"
+  INSTALL: "Install"
+  ACTIVE_THEME: "Active Theme"
+  SWITCHING_TO: "Switching to"
+  SWITCHING_TO_DESCRIPTION: "By switching to a different theme, there is no guarantee that all the layout pages are supported, potentially causing errors when trying to load said pages."
+  SWITCHING_TO_CONFIRMATION: "Do you want to continue and switch to the theme"
+  CREATE_NEW_USER: "Create New User"
+  REMOVE_USER: "Remove User"
+  ACCESS_DENIED: "Access denied"
+  ACCOUNT_NOT_ADMIN: "your account does not have administrator permissions"
+  PHP_INFO: "PHP Info"
+  INSTALLER: "Installer"
+  AVAILABLE_THEMES: "Available Themes"
+  AVAILABLE_PLUGINS: "Available Plugins"
+  INSTALLED_THEMES: "Installed Themes"
+  INSTALLED_PLUGINS: "Installed Plugins"
+  BROWSE_ERROR_LOGS: "Browse Error Logs"
+  SITE: "Site"
+  INFO: "Info"
+  SYSTEM: "System"
+  USER: "User"
+  ADD_ACCOUNT: "Add Account"
+  SWITCH_LANGUAGE: "Switch Language"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Successfully enabled plugin"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Successfully disabled plugin"
+  SUCCESSFULLY_CHANGED_THEME: "Successfully changed default theme"
+  INSTALLATION_FAILED: "Installation failed"
+  INSTALLATION_SUCCESSFUL: "Installation successful"
+  UNINSTALL_FAILED: "Uninstall failed"
+  UNINSTALL_SUCCESSFUL: "Uninstall successful"
+  SUCCESSFULLY_SAVED: "Successfully saved"
+  SUCCESSFULLY_COPIED: "Successfully copied"
+  REORDERING_WAS_SUCCESSFUL: "Reordering was successful"
+  SUCCESSFULLY_DELETED: "Successfully deleted"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Successfully switched language"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "You have insufficient permissions for task"
+  CACHE_CLEARED: "Cache cleared"
+  METHOD: "Method"
+  ERROR_CLEARING_CACHE: "Error clearing cache"
+  AN_ERROR_OCCURRED: "An error occurred"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Your backup is ready for download"
+  DOWNLOAD_BACKUP: "Download backup"
+  PAGES_FILTERED: "Pages filtered"
+  NO_PAGE_FOUND: "No Page found"
+  INVALID_PARAMETERS: "Invalid Parameters"
+  NO_FILES_SENT: "No files sent"
+  EXCEEDED_FILESIZE_LIMIT: "Exceeded PHP configuration upload_max_filesize"
+  EXCEEDED_POSTMAX_LIMIT: "Exceeded PHP configuration post_max_size"
+  UNKNOWN_ERRORS: "Unknown errors"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Exceeded Grav configuration file size limit"
+  UNSUPPORTED_FILE_TYPE: "Unsupported file type"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Failed to move uploaded file"
+  FILE_UPLOADED_SUCCESSFULLY: "File uploaded successfully"
+  FILE_DELETED: "File deleted"
+  FILE_COULD_NOT_BE_DELETED: "File could not be deleted"
+  FILE_NOT_FOUND: "File not found"
+  NO_FILE_FOUND: "No file found"
+  FIELD_REORDER_SUCCESSFUL: "Media Order updated for field '%s'"
+  FIELD_REORDER_FAILED: "An error occurred while storing the media order for the field '%s'"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav was successfully updated to"
+  GRAV_UPDATE_FAILED: "Grav update failed"
+  EVERYTHING_UPDATED: "Everything updated"
+  UPDATES_FAILED: "Updates failed"
+  AVATAR_BY: "Avatar by"
+  AVATAR_UPLOAD_OWN: "Or upload your own..."
+  LAST_BACKUP: "Last Backup"
+  FULL_NAME: "Full name"
+  USERNAME: "Username"
+  EMAIL: "Email"
+  USERNAME_EMAIL: "Username or Email"
+  PASSWORD: "Password"
+  PASSWORD_CONFIRM: "Confirm Password"
+  TITLE: "Title"
+  ACCOUNT: "Account"
+  EMAIL_VALIDATION_MESSAGE: "Must be a valid email address"
+  PASSWORD_VALIDATION_MESSAGE: "Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters"
+  LANGUAGE: "Language"
+  LANGUAGE_HELP: "Set the favorite language"
+  LANGUAGE_DEBUG: "Debug language"
+  LANGUAGE_DEBUG_HELP: "Enable the debug of languages which are using the |t twig filter by adding a span around them that can be styled to help diagnose issues"
+  MEDIA: "Media"
+  DEFAULTS: "Defaults"
+  SITE_TITLE: "Site Title"
+  SITE_TITLE_PLACEHOLDER: "Site wide title"
+  SITE_TITLE_HELP: "Default title for your site, often used in themes"
+  SITE_DEFAULT_LANG: "Default language"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Default language to be used by theme's <HTML> tag"
+  SITE_DEFAULT_LANG_HELP: "Default language to be used by theme's <HTML> tag"
+  DEFAULT_AUTHOR: "Default Author"
+  DEFAULT_AUTHOR_HELP: "A default author name, often used in themes or page content"
+  DEFAULT_EMAIL: "Default Email"
+  DEFAULT_EMAIL_HELP: "A default email to reference in themes or pages"
+  TAXONOMY_TYPES: "Taxonomy Types"
+  TAXONOMY_TYPES_HELP: "Taxonomy types must be defined here if you wish to use them in pages"
+  PAGE_SUMMARY: "Page Summary"
+  ENABLED: "Enabled"
+  ENABLED_HELP: "Enable page summary (the summary returns the same as the page content)"
+  'YES': "Yes"
+  'NO': "No"
+  SUMMARY_SIZE: "Summary Size"
+  SUMMARY_SIZE_HELP: "The amount of characters of a page to use as a content summary"
+  FORMAT: "Format"
+  FORMAT_HELP: "short = use the first occurrence of delimiter or size; long = summary delimiter will be ignored"
+  SHORT: "Short"
+  LONG: "Long"
+  DELIMITER: "Delimiter"
+  DELIMITER_HELP: "The summary delimiter (default '===')"
+  METADATA: "Metadata"
+  METADATA_HELP: "Default metadata values that will be displayed on every page unless overridden by the page"
+  NAME: "Name"
+  CONTENT: "Content"
+  SIZE: "Size"
+  ACTION: "Action"
+  REDIRECTS_AND_ROUTES: "Redirects & Routes"
+  CUSTOM_REDIRECTS: "Custom Redirects"
+  CUSTOM_REDIRECTS_HELP: "routes to redirect to other pages. Standard Regex replacement is valid"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/your/redirect"
+  CUSTOM_ROUTES: "Custom Routes"
+  CUSTOM_ROUTES_HELP: "routes to alias to other pages. Standard Regex replacement is valid"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/your/route"
+  FILE_STREAMS: "File Streams"
+  DEFAULT: "Default"
+  PAGE_MEDIA: "Page Media"
+  OPTIONS: "Options"
+  PUBLISHED: "Published"
+  PUBLISHED_HELP: "By default, a page is published unless you explicitly set published: false or via a publish_date being in the future, or unpublish_date in the past"
+  DATE: "Date"
+  DATE_HELP: "The date variable allows you to specifically set a date associated with this page."
+  PUBLISHED_DATE: "Published Date"
+  PUBLISHED_DATE_HELP: "Can provide a date to automatically trigger publication."
+  UNPUBLISHED_DATE: "Unpublished Date"
+  UNPUBLISHED_DATE_HELP: "Can provide a date to automatically trigger un-publication."
+  ROBOTS: "Robots"
+  TAXONOMIES: "Taxonomies"
+  TAXONOMY: "Taxonomy"
+  ADVANCED: "Advanced"
+  SETTINGS: "Settings"
+  FOLDER_NUMERIC_PREFIX: "Folder Numeric Prefix"
+  FOLDER_NUMERIC_PREFIX_HELP: "Numeric prefix that provides manual ordering and implies visibility"
+  FOLDER_NAME: "Folder Name"
+  FOLDER_NAME_HELP: "The folder name that will be stored in the filesystem for this page"
+  PARENT: "Parent"
+  DEFAULT_OPTION_ROOT: "- Root -"
+  DEFAULT_OPTION_SELECT: "- Select -"
+  DISPLAY_TEMPLATE: "Display Template"
+  DISPLAY_TEMPLATE_HELP: "The page type that translates into which twig template renders the page"
+  ORDERING: "Ordering"
+  PAGE_ORDER: "Page Order"
+  OVERRIDES: "Overrides"
+  MENU: "Menu"
+  MENU_HELP: "The string to be used in a menu.  If not set, Title will be used."
+  SLUG: "Slug"
+  SLUG_HELP: "The slug variable allows you to specifically set the page's portion of the URL"
+  SLUG_VALIDATE_MESSAGE: "A slug must contain only lowercase alphanumeric characters and dashes"
+  PROCESS: "Process"
+  PROCESS_HELP: "Control how pages are processed. Can be set per-page rather than globally"
+  DEFAULT_CHILD_TYPE: "Default Child Type"
+  USE_GLOBAL: "Use Global"
+  ROUTABLE: "Routable"
+  ROUTABLE_HELP: "If this page is reachable by a URL"
+  CACHING: "Caching"
+  VISIBLE: "Visible"
+  VISIBLE_HELP: "Determines if a page is visible in the navigation."
+  DISABLED: "Disabled"
+  ITEMS: "Items"
+  ORDER_BY: "Order By"
+  ORDER: "Order"
+  FOLDER: "Folder"
+  ASCENDING: "Ascending"
+  DESCENDING: "Descending"
+  PAGE_TITLE: "Page Title"
+  PAGE_TITLE_HELP: "The title of the page"
+  PAGE: "Page"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Filename"
+  PARENT_PAGE: "Parent Page"
+  HOME_PAGE: "Home page"
+  HOME_PAGE_HELP: "The page that Grav will use as the default landing page"
+  DEFAULT_THEME: "Default theme"
+  DEFAULT_THEME_HELP: "Set the default theme for Grav to use (default is Antimatter)"
+  TIMEZONE: "Timezone"
+  TIMEZONE_HELP: "Override the default timezone the server"
+  SHORT_DATE_FORMAT: "Short display date format"
+  SHORT_DATE_FORMAT_HELP: "Set the short date format that can be used by themes"
+  LONG_DATE_FORMAT: "Long display date format"
+  LONG_DATE_FORMAT_HELP: "Set the long date format that can be used by themes"
+  DEFAULT_ORDERING: "Default ordering"
+  DEFAULT_ORDERING_HELP: "Pages in a list will render using this order unless it is overridden"
+  DEFAULT_ORDERING_DEFAULT: "Default - based on folder name"
+  DEFAULT_ORDERING_FOLDER: "Folder - based on prefix-less folder name"
+  DEFAULT_ORDERING_TITLE: "Title - based on title field in header"
+  DEFAULT_ORDERING_DATE: "Date - based on date field in header"
+  DEFAULT_ORDER_DIRECTION: "Default order direction"
+  DEFAULT_ORDER_DIRECTION_HELP: "The direction of pages in a list"
+  DEFAULT_PAGE_COUNT: "Default page count"
+  DEFAULT_PAGE_COUNT_HELP: "Default maximum pages count in a list"
+  DATE_BASED_PUBLISHING: "Date-based publishing"
+  DATE_BASED_PUBLISHING_HELP: "Automatically (un)publish posts based on their date"
+  EVENTS: "Events"
+  EVENTS_HELP: "Enable or Disable specific events.  Disabling these can break plugins"
+  REDIRECT_DEFAULT_ROUTE: "Redirect default route"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Automatically redirect to a page's default route"
+  LANGUAGES: "Languages"
+  SUPPORTED: "Supported"
+  SUPPORTED_HELP: "Comma separated list of 2 letter language codes (for example 'en,fr,de')"
+  SUPPORTED_PLACEHOLDER: "e.g. en, fr"
+  TRANSLATIONS_FALLBACK: "Translations fallback"
+  TRANSLATIONS_FALLBACK_HELP: "Fallback through supported translations if active language doesn't exist"
+  ACTIVE_LANGUAGE_IN_SESSION: "Active language in session"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Store the active language in the session"
+  HTTP_HEADERS: "HTTP Headers"
+  EXPIRES: "Expires"
+  EXPIRES_HELP: "Sets the expires header. The value is in seconds."
+  CACHE_CONTROL: "HTTP Cache-Control"
+  CACHE_CONTROL_HELP: "Set to a valid cache-control value such as `no-cache, no-store, must-revalidate`"
+  CACHE_CONTROL_PLACEHOLDER: "e.g. public, max-age=31536000"
+  LAST_MODIFIED: "Last modified"
+  LAST_MODIFIED_HELP: "Sets the last modified header that can help optimize proxy and browser caching"
+  ETAG: "ETag"
+  ETAG_HELP: "Sets the etag header to help identify when a page has been modified"
+  VARY_ACCEPT_ENCODING: "Vary accept encoding"
+  VARY_ACCEPT_ENCODING_HELP: "Sets the `Vary: Accept Encoding` header to help with proxy and CDN caching"
+  MARKDOWN: "Markdown"
+  MARKDOWN_EXTRA: "Markdown extra"
+  MARKDOWN_EXTRA_HELP: "Enable default support for Markdown Extra - https://michelf.ca/projects/php-markdown/extra/"
+  MARKDOWN_EXTRA_ESCAPE_FENCES: "Escape HTML elements in markdown extra fences"
+  MARKDOWN_EXTRA_ESCAPE_FENCES_HELP: "Escapes HTML elements in markdown extra fences"
+  AUTO_LINE_BREAKS: "Auto line breaks"
+  AUTO_LINE_BREAKS_HELP: "Enable support for automatic line breaks in markdown"
+  AUTO_URL_LINKS: "Auto URL links"
+  AUTO_URL_LINKS_HELP: "Enable automatic conversion of URLs into HTML hyperlinks"
+  ESCAPE_MARKUP: "Escape markup"
+  ESCAPE_MARKUP_HELP: "Escape markup tags into HTML entities"
+  CACHING_HELP: "Global ON/OFF switch to enable/disable Grav caching"
+  CACHE_CHECK_METHOD: "Cache check method"
+  CACHE_CHECK_METHOD_HELP: "Select the method that Grav uses to check if page files have been modified."
+  CACHE_DRIVER: "Cache driver"
+  CACHE_DRIVER_HELP: "Choose which cache driver Grav should use. 'Auto Detect' attempts to find the best for you"
+  CACHE_PREFIX: "Cache prefix"
+  CACHE_PREFIX_HELP: "An identifier for part of the Grav key.  Don't change unless you know what your doing."
+  CACHE_PREFIX_PLACEHOLDER: "Derived from base URL (override by entering random string)"
+  CACHE_PURGE_JOB: "Run Scheduled Purge Job"
+  CACHE_PURGE_JOB_HELP: "With the scheduler you can periodically clear out old Doctrine file cache folders with this job"
+  CACHE_CLEAR_JOB: "Run Scheduled Clear Job"
+  CACHE_CLEAR_JOB_HELP: "With the scheduler you can periodically clear the Grav Cache"
+  CACHE_JOB_TYPE: "Cache Job Type"
+  CACHE_JOB_TYPE_HELP: "Either clear with the 'standard' folders cache clear, or with 'all' folders"
+  CACHE_PURGE: "Purge Old Cache"
+  LIFETIME: "Lifetime"
+  LIFETIME_HELP: "Sets the cache lifetime in seconds. 0 = infinite"
+  GZIP_COMPRESSION: "Gzip compression"
+  GZIP_COMPRESSION_HELP: "Enable GZip compression of the Grav page for increased performance."
+  TWIG_TEMPLATING: "Twig Templating"
+  TWIG_CACHING: "Twig caching"
+  TWIG_CACHING_HELP: "Control the Twig caching mechanism. Leave this enabled for best performance."
+  TWIG_DEBUG: "Twig debug"
+  TWIG_DEBUG_HELP: "Allows the option of not loading the Twig Debugger extension"
+  DETECT_CHANGES: "Detect changes"
+  DETECT_CHANGES_HELP: "Twig will automatically recompile the Twig cache if it detects any changes in Twig templates"
+  AUTOESCAPE_VARIABLES: "Autoescape variables"
+  AUTOESCAPE_VARIABLES_HELP: "Autoescapes all variables.  This will break your site most likely"
+  ASSETS: "Assets"
+  CSS_ASSETS: "CSS Assets"
+  CSS_PIPELINE: "CSS pipeline"
+  CSS_PIPELINE_HELP: "The CSS pipeline is the unification of multiple CSS resources into one file"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Include externals in CSS pipeline"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "External URLs sometimes have relative file references and shouldn't be pipelined"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "CSS pipeline render first"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Render the CSS pipeline before any other CSS references that are not included"
+  CSS_MINIFY: "CSS minify"
+  CSS_MINIFY_HELP: "Minify the CSS during pipelining"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "CSS minify Windows override"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Minify Override for Windows platforms. False by default due to ThreadStackSize"
+  CSS_REWRITE: "CSS rewrite"
+  CSS_REWRITE_HELP: "Rewrite any CSS relative URLs during pipelining"
+  JS_ASSETS: "JavaScript Assets"
+  JAVASCRIPT_PIPELINE: "JavaScript pipeline"
+  JAVASCRIPT_PIPELINE_HELP: "The JS pipeline is the unification of multiple JS resources into one file"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Include externals in JS pipeline"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "External URLs sometimes have relative file references and shouldn't be pipelined"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "JS pipeline render first"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Render the JS pipeline before any other JS references that are not included"
+  JS_MODULE_ASSETS: "JavaScript Module Assets"
+  JAVASCRIPT_MODULE_PIPELINE: "JavaScript Module pipeline"
+  JAVASCRIPT_MODULE_PIPELINE_HELP: "The JS Module pipeline is the unification of multiple JS resources into one file"
+  JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS: "Include externals in JS Module pipeline"
+  JAVASCRIPT_MODULE_PIPELINE_INCLUDE_EXTERNALS_HELP: "External URLs sometimes have relative file references and shouldn't be pipelined"
+  JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES: "JS Module pipeline render first"
+  JAVASCRIPT_MODULE_PIPELINE_BEFORE_EXCLUDES_HELP: "Render the JS pipeline before any other JS references that are not included"
+  GENERAL_CONFIG: "General Asset Configuration"
+  JAVASCRIPT_MINIFY: "JavaScript minify"
+  JAVASCRIPT_MINIFY_HELP: "Minify the JS during pipelining"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Enable timestamps on assets"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Enable asset timestamps"
+  ENABLED_SRI_ON_ASSETS: "Enable SRI on assets"
+  ENABLED_SRI_ON_ASSETS_HELP: "Enable asset SRI"
+  COLLECTIONS: "Collections"
+  ERROR_HANDLER: "Error handler"
+  DISPLAY_ERRORS: "Display errors"
+  DISPLAY_ERRORS_HELP: "Display full backtrace-style error page"
+  LOG_ERRORS: "Log errors"
+  LOG_ERRORS_HELP: "Log errors to /logs folder"
+  LOG_HANDLER: "Log handler"
+  LOG_HANDLER_HELP: "Where to output the logs"
+  SYSLOG_FACILITY: "Syslog facility"
+  SYSLOG_FACILITY_HELP: "Syslog facility for output"
+  SYSLOG_TAG: "Syslog tag"
+  SYSLOG_TAG_HELP: "Syslog tag for output"
+  DEBUGGER: "Debugger"
+  DEBUGGER_HELP: "Enable Grav debugger and following settings"
+  DEBUG_TWIG: "Debug Twig"
+  DEBUG_TWIG_HELP: "Enable debugging of Twig templates"
+  SHUTDOWN_CLOSE_CONNECTION: "Shutdown close connection"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Close the connection before calling onShutdown(). false for debugging"
+  DEFAULT_IMAGE_QUALITY: "Default image quality"
+  DEFAULT_IMAGE_QUALITY_HELP: "Default image quality to use when resampling or caching images (85%)"
+  CACHE_ALL: "Cache all images"
+  CACHE_ALL_HELP: "Run all images through Grav's cache system even if they have no media manipulations"
+  IMAGES_DEBUG: "Image debug watermark"
+  IMAGES_DEBUG_HELP: "Show an overlay over images indicating the pixel depth of the image when working with retina for example"
+  IMAGES_LOADING: "Image loading behavior"
+  IMAGES_LOADING_HELP: "The loading attribute allows a browser to defer loading offscreen images and iframes until users scroll near them. loading supports three values: auto, lazy, eager"
+
+  # Removed in Grav 1.8
+  IMAGES_SEOFRIENDLY: "SEO-Friendly Image names"
+  IMAGES_SEOFRIENDLY_HELP: "When enabled, the image name is displayed first, then a smaller hash to reflect processed operations"
+
+  UPLOAD_LIMIT: "File upload limit"
+  UPLOAD_LIMIT_HELP: "Set maximum upload size in bytes (0 is unlimited)"
+  ENABLE_MEDIA_TIMESTAMP: "Enable timestamps on media"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Appends a timestamp based on last modified date to each media item"
+  SESSION: "Session"
+  SESSION_ENABLED_HELP: "Enable session support within Grav"
+  SESSION_NAME_HELP: "An identifier used to form the name of the session cookie"
+  SESSION_UNIQUENESS: "Unique string"
+  SESSION_UNIQUENESS_HELP: "MD5 hash of Grav's root path, ie `GRAV_ROOT` (default) or a based on the random `security.salt` string."
+  ABSOLUTE_URLS: "Absolute URLs"
+  ABSOLUTE_URLS_HELP: "Absolute or relative URLs for `base_url`"
+  PARAMETER_SEPARATOR: "Parameter separator"
+  PARAMETER_SEPARATOR_HELP: "Separator for passed parameters that can be changed for Apache on Windows"
+  TASK_COMPLETED: "Task completed"
+  EVERYTHING_UP_TO_DATE: "Everything is up to date"
+  UPDATES_ARE_AVAILABLE: "update(s) are available"
+  IS_AVAILABLE_FOR_UPDATE: "is available for update"
+  IS_NOW_AVAILABLE: "is now available"
+  CURRENT: "Current"
+  UPDATE_GRAV_NOW: "Update Grav Now"
+  GRAV_SYMBOLICALLY_LINKED: "Grav is symbolically linked. Upgrade won't be available"
+  UPDATING_PLEASE_WAIT: "Updating... please wait, downloading"
+  OF_THIS: "of this"
+  OF_YOUR: "of your"
+  HAVE_AN_UPDATE_AVAILABLE: "have an update available"
+  SAVE_AS: "Save as"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Are you sure you want to delete this page and all its children? If the page is translated in other languages, those translations will be kept and must be deleted separately. Otherwise the page folder will be deleted along with its subpages. This action cannot be undone."
+  AND: "and"
+  UPDATE_AVAILABLE: "Update available"
+  METADATA_KEY: "Key (e.g. 'Keywords')"
+  METADATA_VALUE: "Value (e.g. 'Blog, Grav')"
+  USERNAME_HELP: "Username should be between 3 and 16 characters, including lowercase letters, numbers, underscores, and hyphens. Uppercase letters, spaces, and special characters are not allowed"
+  FULLY_UPDATED: "Fully Updated"
+  SAVE_LOCATION: "Save location"
+  PAGE_FILE: "Page Template"
+  PAGE_FILE_HELP: "Page template file name, and by default the display template for this page"
+  NO_USER_ACCOUNTS: "No user accounts found, please create one first..."
+  NO_USER_EXISTS: "No local user exists for this account, cannot save..."
+  REDIRECT_TRAILING_SLASH: "Redirect trailing slash"
+  REDIRECT_TRAILING_SLASH_HELP: "Perform a 301 redirect rather than transparently handling trailing slash URIs."
+  DEFAULT_DATE_FORMAT: "Page date format"
+  DEFAULT_DATE_FORMAT_HELP: "Page date format used by Grav. By default, Grav attempts to guess your date format, however you can specifiy a format using PHP's date syntax (e.g.: Y-m-d H:i)"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Guess automatically"
+  IGNORE_FILES: "Ignore files"
+  IGNORE_FILES_HELP: "Specific files to ignore when processing pages"
+  IGNORE_FOLDERS: "Ignore folders"
+  IGNORE_FOLDERS_HELP: "Specific folders to ignore when processing pages"
+  HIDE_EMPTY_FOLDERS: "Hide empty folders"
+  HIDE_EMPTY_FOLDERS_HELP: "If folder has no .md file, should it be hidden in navigation as well as being unroutable"
+  HTTP_ACCEPT_LANGUAGE: "Set language from browser"
+  HTTP_ACCEPT_LANGUAGE_HELP: "You can opt to try to set the language based on `http_accept_language` header tag in the browser"
+  OVERRIDE_LOCALE: "Override locale"
+  OVERRIDE_LOCALE_HELP: "Override the locale setting in PHP based on the current language"
+  REDIRECT: "Page redirect"
+  REDIRECT_HELP: "Enter a page route or external URL for this page to redirect to. e.g. `/some/route` or `http://somesite.com`"
+  PLUGIN_STATUS: "Plugin status"
+  INCLUDE_DEFAULT_LANG: "Include default language"
+  INCLUDE_DEFAULT_LANG_HELP: "This will prepend all URLs in the default language with the default language.  e.g. `/en/blog/my-post`"
+  INCLUDE_DEFAULT_LANG_FILE_EXTENSION: "Include default language in file extension"
+  INCLUDE_DEFAULT_LANG_HELP_FILE_EXTENSION: "If enabled, it will prepend the default language to the file extension (e.g. `.en.md`). Disable it to keep the default language using `.md` file extension."
+  PAGES_FALLBACK_ONLY: "Pages fallback only"
+  PAGES_FALLBACK_ONLY_HELP: "Only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing"
+  ALLOW_URL_TAXONOMY_FILTERS: "URL Taxonomy Filters"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Page-based collections allow you to filter via `/taxonomy:value`."
+  REDIRECT_DEFAULT_CODE: "Default redirect code"
+  REDIRECT_DEFAULT_CODE_HELP: "The HTTP status code to use for redirects"
+  IGNORE_HIDDEN: "Ignore hidden"
+  IGNORE_HIDDEN_HELP: "Ignore all files and folders that begin with a DOT"
+  WRAPPED_SITE: "Wrapped site"
+  WRAPPED_SITE_HELP: "For themes/plugins to know if Grav is wrapped by another platform"
+  FALLBACK_TYPES: "Allowed fallback types"
+  FALLBACK_TYPES_HELP: "Allowed file types that can be found if accessed via Page route. Defaults to any supported media type."
+  INLINE_TYPES: "Inline fallback types"
+  INLINE_TYPES_HELP: "A list of file types that should be displayed inline rather than downloaded"
+  APPEND_URL_EXT: "Append URL extension"
+  APPEND_URL_EXT_HELP: "Will add a custom extension to the Page's URL. Note, this will mean Grav will look for `<template>.<extension>.twig` template"
+  PAGE_MODES: "Page Modes"
+  PAGE_TYPES: "Page Types"
+  PAGE_TYPES_HELP: "Determines the page types that Grav supports and the order determines which type to fall back to if the request is ambiguous"
+  ACCESS_LEVELS: "Access Levels"
+  GROUPS: "Groups"
+  GROUPS_HELP: "List of groups the user is part of"
+  ADMIN_ACCESS: "Admin Access"
+  SITE_ACCESS: "Site Access"
+  INVALID_SECURITY_TOKEN: "Invalid Security Token"
+  ACTIVATE: "Activate"
+  TWIG_UMASK_FIX: "Umask Fix"
+  TWIG_UMASK_FIX_HELP: "By default Twig creates cached files as 0755, fix switches this to 0775"
+  CACHE_PERMS: "Cache Permissions"
+  CACHE_PERMS_HELP: "Default cache folder perms. Usually 0755 or 0775 depending on setup"
+  REMOVE_SUCCESSFUL: "Remove Successful"
+  REMOVE_FAILED: "Remove Failed"
+  HIDE_HOME_IN_URLS: "Hide home route in URLs"
+  HIDE_HOME_IN_URLS_HELP: "Will ensure the default routes for any pages under home do not reference home's regular route"
+  TWIG_FIRST: "Process Twig First"
+  TWIG_FIRST_HELP: "If you enabled Twig page processing, then you can configure Twig to process before or after markdown"
+  SESSION_SECURE: "Secure"
+  SESSION_SECURE_HELP: "If true, indicates that communication for this cookie must be over an encrypted transmission. WARNING: Enable this only on sites that run exclusively on HTTPS"
+  SESSION_HTTPONLY: "HTTP Only"
+  SESSION_HTTPONLY_HELP: "If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed"
+  REVERSE_PROXY: "Reverse Proxy"
+  REVERSE_PROXY_HELP: "Enable this if you are behind a reverse proxy and you are having trouble with URLs containing incorrect ports"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Invalid frontmatter, could not save"
+  ADD_FOLDER: "Add Folder"
+  COPY_PAGE: "Copy Page"
+  PROXY_URL: "Proxy URL"
+  PROXY_URL_HELP: "Enter the proxy HOST or IP and PORT"
+  PROXY_CERT: "Proxy Certificate Path"
+  PROXY_CERT_HELP: "Local path to folder containing proxy certificate pem file"
+  NOTHING_TO_SAVE: "Nothing to Save"
+  FILE_ERROR_ADD: "An error occurred while trying to add the file"
+  FILE_ERROR_UPLOAD: "An error occurred while trying to upload the file"
+  FILE_UNSUPPORTED: "Unsupported file type"
+  ADD_ITEM: "Add item"
+  FILE_TOO_LARGE: "The file is too large to be uploaded, maximum allowed is %s according <br>to your PHP settings. Increase your `post_max_size` PHP setting"
+  INSTALLING: "Installing"
+  LOADING: "Loading.."
+  DEPENDENCIES_NOT_MET_MESSAGE: "The following dependencies need to be fulfilled first:"
+  ERROR_INSTALLING_PACKAGES: "Error while installing the package(s)"
+  INSTALLING_DEPENDENCIES: "Installing dependencies..."
+  INSTALLING_PACKAGES: "Installing package(s).."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Package(s) successfully installed."
+  READY_TO_INSTALL_PACKAGES: "Ready to install the package(s)"
+  PACKAGES_NOT_INSTALLED: "Packages not installed"
+  PACKAGES_NEED_UPDATE: "Packages already installed, but too old"
+  PACKAGES_SUGGESTED_UPDATE: "Packages already installed, and version is ok, but will be updated to keep you up to date"
+  REMOVE_THE: "Remove the %s"
+  CONFIRM_REMOVAL: "Are you sure you want to delete this %s?"
+  REMOVED_SUCCESSFULLY: "%s removed successfully"
+  ERROR_REMOVING_THE: "Error removing the %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "The %s required the following dependencies, which are not required by other installed packages. If you are not using them, you can remove them directly from here."
+  READY_TO_UPDATE_PACKAGES: "Ready to update the package(s)"
+  ERROR_UPDATING_PACKAGES: "Error while updating the package(s)"
+  UPDATING_PACKAGES: "Updating package(s).."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Package(s) successfully updated."
+  UPDATING: "Updating"
+  GPM_SECTION: "GPM Section"
+  GPM_RELEASES: "GPM Releases"
+  GPM_RELEASES_HELP: "Choose 'Testing' to install beta or testing versions"
+  GPM_METHOD: "Remote Fetch Method"
+  GPM_METHOD_HELP: "When set to Auto, Grav will determine if fopen is available and use it, otherwise fall back to cURL. To force the use of one or the other switch the setting."
+  HTTP_SECTION: "HTTP Section"
+  SSL_ENABLE_PROXY: "Enable Proxy Server"
+  SSL_VERIFY_PEER: "Remote Verify Peer"
+  SSL_VERIFY_PEER_HELP: "Some may fail verifying SSL certificates"
+  SSL_VERIFY_HOST: "Remote Verify Host"
+  SSL_VERIFY_HOST_HELP: "Some may fail verifying SSL certificates"
+  HTTP_CONNECTIONS: "HTTP Connections"
+  HTTP_CONNECTIONS_HELP: "The number of concurrent HTTP connections during multiplexed requests"
+  MISC_SECTION: "Miscellaneous Section"
+  AUTO: "Auto"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stable"
+  TESTING: "Testing"
+  FRONTMATTER_PROCESS_TWIG: "Process frontmatter Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "When enabled you can use Twig config variables in page front matter"
+  FRONTMATTER_IGNORE_FIELDS: "Ignore frontmatter fields"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Certain frontmatter fields may contain Twig but should not be processed, such as 'forms'"
+  FRONTMATTER_IGNORE_FIELDS_PLACEHOLDER: "e.g. forms"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Package %s installed successfully"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Parent setting order, ordering disabled"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Page is not visible, ordering disabled"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Ordering via the admin is unsupported because there are more than 200 siblings"
+  ORDERING_DISABLED_BECAUSE_PAGE_NO_PREFIX: "Page ordering is disabled for this page because <strong>Folder Numeric Prefix</strong> is not enabled"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "NOTE: You cannot add media files until you save the page. Just click 'Save' on top"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "NOTE: Page must be saved before you can upload files to it."
+  DROP_FILES_HERE_TO_UPLOAD: "Drop your files here or <strong>click in this area</strong>"
+  INSERT: "Insert"
+  UNDO: "Undo"
+  REDO: "Redo"
+  HEADERS: "Headers"
+  BOLD: "Bold"
+  ITALIC: "Italic"
+  STRIKETHROUGH: "Strikethrough"
+  SUMMARY_DELIMITER: "Summary Delimiter"
+  LINK: "Link"
+  IMAGE: "Image"
+  BLOCKQUOTE: "Blockquote"
+  UNORDERED_LIST: "Unordered List"
+  ORDERED_LIST: "Ordered List"
+  EDITOR: "Editor"
+  PREVIEW: "Preview"
+  FULLSCREEN: "Fullscreen"
+  NON_ROUTABLE: "Non-Routable"
+  NON_VISIBLE: "Non-Visible"
+  NON_PUBLISHED: "Non-Published"
+  CHARACTERS: "characters"
+  PUBLISHING: "Publishing"
+  MEDIA_TYPES: "Media Types"
+  IMAGE_OPTIONS: "Image options"
+  MIME_TYPE: "Mime Type"
+  THUMB: "Thumb"
+  TYPE: "Type"
+  FILE_EXTENSION: "File Extension"
+  LEGEND: "Page Legend"
+  MEMCACHE_SERVER: "Memcache server"
+  MEMCACHE_SERVER_HELP: "The Memcache server address"
+  MEMCACHE_PORT: "Memcache port"
+  MEMCACHE_PORT_HELP: "The Memcache server port"
+  MEMCACHED_SERVER: "Memcached server"
+  MEMCACHED_SERVER_HELP: "The Memcached server address"
+  MEMCACHED_PORT: "Memcached port"
+  MEMCACHED_PORT_HELP: "The Memcached server port"
+  REDIS_SERVER: "Redis server"
+  REDIS_SERVER_HELP: "The Redis server address"
+  REDIS_PORT: "Redis port"
+  REDIS_PORT_HELP: "The Redis server port"
+  REDIS_PASSWORD: "Redis password/secret"
+  REDIS_DATABASE: "Redis Database ID"
+  REDIS_DATABASE_HELP: "The Redis instance database ID"
+  ALL: "All"
+  FROM: "from"
+  TO: "to"
+  RELEASE_DATE: "Release Date"
+  SORT_BY: "Sort By"
+  RESOURCE_FILTER: "Filter..."
+  FORCE_SSL: "Force SSL"
+  FORCE_SSL_HELP: "Globally force SSL, if enabled when the site is reached through HTTP, Grav sends a redirect to the HTTPS page"
+  NEWS_FEED: "News Feed"
+  EXTERNAL_URL: "External URL"
+  SESSION_SAMESITE: "The session SameSite attribute"
+  SESSION_SAMESITE_HELP: "Lax|Strict|None. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite for more info"
+  CUSTOM_BASE_URL: "Custom base URL"
+  CUSTOM_BASE_URL_HELP: "Use if you want to rewrite the site domain or use a different subfolder than the one used by Grav. Example: http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'Cannot use "%s" outside of pages.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'Unable to upload file %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'Unable to move file %s to "%s"'
+  DROPZONE_CANCEL_UPLOAD: 'Cancel upload'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Are you sure you want to cancel this upload?'
+  DROPZONE_DEFAULT_MESSAGE: 'Drop your files here or <strong>click in this area</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'Your browser does not support drag and drop file uploads.'
+  DROPZONE_FALLBACK_TEXT: 'Please use the fallback form below to upload your files like in the older days.'
+  DROPZONE_FILE_TOO_BIG: 'File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "You can't upload files of this type."
+  DROPZONE_MAX_FILES_EXCEEDED: "You can not upload any more files."
+  DROPZONE_REMOVE_FILE: "Remove file"
+  DROPZONE_RESPONSE_ERROR: "Server responded with {{statusCode}} code."
+  PREMIUM_PRODUCT: "Premium"
+  DESTINATION_NOT_SPECIFIED: "Destination not specified"
+  UPLOAD_ERR_NO_TMP_DIR: "Missing a temporary folder"
+  SESSION_SPLIT: "Session Split"
+  SESSION_SPLIT_HELP: "Independent split sessions between site and other plugins (such as admin)"
+  ERROR_FULL_BACKTRACE: "Full Backtrace Error"
+  ERROR_SIMPLE: "Simple Error"
+  ERROR_SYSTEM: "System Error"
+  IMAGES_AUTO_FIX_ORIENTATION: "Fix orientation automatically"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Automatically fix the image orientation based on the Exif data"
+  REDIS_SOCKET: "Redis socket"
+  REDIS_SOCKET_HELP: "The Redis socket"
+  NOT_SET: "Not set"
+  PERMISSIONS: "Permissions"
+  NEVER_CACHE_TWIG: "Never Cache Twig"
+  NEVER_CACHE_TWIG_HELP: "Only cache content and process Twig every time for pages. Ignores twig_first setting."
+  ALLOW_WEBSERVER_GZIP: "Allow WebServer Gzip"
+  ALLOW_WEBSERVER_GZIP_HELP: "Off by default. When enabled, WebServer-configured Gzip/Deflate compression will work, but http connection will not be closed before onShutDown() event causing slower page loading"
+  OFFLINE_WARNING: "The connection to the GPM cannot be established"
+  CLEAR_IMAGES_BY_DEFAULT: "Clear image cache by default"
+  CLEAR_IMAGES_BY_DEFAULT_HELP: "By default processed images are cleared for all cache clears, this can be disabled"
+  CLI_COMPATIBILITY: "CLI Compatibility"
+  CLI_COMPATIBILITY_HELP: "Ensures that only non-volatile Cache drivers are used (file, redis, memcache, etc.)"
+  REINSTALL_PLUGIN: "Reinstall Plugin"
+  REINSTALL_THEME: "Reinstall Theme"
+  REINSTALL_THE: "Reinstall the %s"
+  CONFIRM_REINSTALL: "Are you sure you want to reinstall this %s?"
+  REINSTALLED_SUCCESSFULLY: "%s reinstalled successfully"
+  ERROR_REINSTALLING_THE: "Error reinstalling the %s"
+  PACKAGE_X_REINSTALLED_SUCCESSFULLY: "Package %s reinstalled successfully"
+  REINSTALLATION_FAILED: "Reinstallation failed"
+  WARNING_REINSTALL_NOT_LATEST_RELEASE: "The installed version is not the latest release. By clicking Continue, you'll remove the current version and install the latest available release"
+  TOOLS: "Tools"
+  DIRECT_INSTALL: "Direct Install"
+  NO_PACKAGE_NAME: "Package name not specified"
+  PACKAGE_EXTRACTION_FAILED: "Package extraction failed"
+  NOT_VALID_GRAV_PACKAGE: "Not a valid Grav package"
+  NAME_COULD_NOT_BE_DETERMINED: "Name could not be determined"
+  CANNOT_OVERWRITE_SYMLINKS: "Cannot overwrite symlinks"
+  ZIP_PACKAGE_NOT_FOUND: "ZIP package could not be found"
+  GPM_OFFICIAL_ONLY: "Official GPM Only"
+  GPM_OFFICIAL_ONLY_HELP: "Only allow direct installs from the official GPM repository only."
+  NO_CHILD_TYPE: "No child type for this rawroute"
+  SORTABLE_PAGES: "Sortable Pages:"
+  UNSORTABLE_PAGES: "Unsortable Pages"
+  ADMIN_SPECIFIC_OVERRIDES: "Admin Specific Overrides"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Children Display Order"
+  ADMIN_CHILDREN_DISPLAY_ORDER_HELP: "The order that children of this page should be displayed in the 'Pages' view of Admin plugin"
+  PWD_PLACEHOLDER: "complex string at least 8 chars long"
+  PWD_REGEX: "Password Regex"
+  PWD_REGEX_HELP: "By default: Password must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters"
+  USERNAME_PLACEHOLDER: "lowercase chars only, e.g. 'admin'"
+  USERNAME_REGEX: "Username Regex"
+  USERNAME_REGEX_HELP: "By default: Only lowercase chars, digits, dashes, and underscores. 3 - 16 chars"
+  ENABLE_AUTO_METADATA: "Auto metadata from Exif"
+  ENABLE_AUTO_METADATA_HELP: "Automatically generate metadata files for images with exif information"
+  2FA_TITLE: "2-Factor Authentication"
+  2FA_INSTRUCTIONS: "##### 2-Factor Authentication\nYou have **2FA** enabled on this account. Please use your **2FA** app to enter the current **6-digit code** to complete the login process."
+  2FA_REGEN_HINT: "Regenerating the secret will require you to update your authenticator app"
+  2FA_LABEL: "Admin Access"
+  2FA_FAILED: "Invalid 2-Factor Authentication code, please try again..."
+  2FA_ENABLED: "2FA Enabled"
+  2FA_CODE_INPUT: "000000"
+  2FA_SECRET: "2FA Secret"
+  2FA_SECRET_HELP: "Scan this QR code into your [Authenticator App](https://learn.getgrav.org/admin-panel/2fa#apps). Also it's a good idea to backup the secret in a safe location, in case you need to reinstall your app. Check the [Grav docs](https://learn.getgrav.org/admin-panel/2fa) for more information "
+  2FA_REGENERATE: "Regenerate"
+  YUBIKEY_ID: "YubiKey ID"
+  YUBIKEY_OTP_INPUT: "YubiKey OTP"
+  YUBIKEY_HELP: "Insert your YubiKey into your computer and click the button to generate an OTP. The first 12 chars are your client ID and will be saved."
+  FORCE_LOWERCASE_URLS: "Force lowercase URLs"
+  FORCE_LOWERCASE_URLS_HELP: "By default Grav will set all slugs and routes to be lowercase. With this set to false, Uppercase slugs and routes can be used"
+  INTL_ENABLED: "Intl module integration"
+  INTL_ENABLED_HELP: "Use Intl PHP module and collate to sort UTF8 based collections"
+  VIEW_SITE_TIP: "View site"
+  TOOLS_DIRECT_INSTALL_TITLE: "Direct Install of Grav Packages"
+  TOOLS_DIRECT_INSTALL_UPLOAD_TITLE: "Install Package via Direct ZIP Upload"
+  TOOLS_DIRECT_INSTALL_UPLOAD_DESC: "You can easily install a valid Grav <strong>theme</strong>, <strong>plugin</strong>, or even <strong>Grav</strong> update Zip package via this method.  This package does not have to be registered via GPM and allows you to easily roll back to a prior version or install for testing."
+  TOOLS_DIRECT_INSTALL_URL_TITLE: "Install Package via Remote URL Reference"
+  TOOLS_DIRECT_INSTALL_URL_DESC: "Alternatively, you can also reference a full URL to the package ZIP file and install it via this remote URL."
+  TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Upload and install"
+  ROUTE_OVERRIDES: "Route Overrides"
+  ROUTE_DEFAULT: "Default Route"
+  ROUTE_CANONICAL: "Canonical Route"
+  ROUTE_ALIASES: "Route Aliases"
+  OPEN_NEW_TAB: "Open in new tab"
+  SESSION_INITIALIZE: "Initialize Session"
+  SESSION_INITIALIZE_HELP: "Makes Grav to start a session. This feature is needed to make any user interaction to work, such as login, forms etc. Admin plugin isn't affected by this setting."
+  STRICT_YAML_COMPAT: "YAML Compatibility"
+  STRICT_YAML_COMPAT_HELP: "Falls back to Symfony 2.4 YAML parser if Native or 3.4 parser fails"
+  STRICT_TWIG_COMPAT: "Twig Compatibility"
+  STRICT_TWIG_COMPAT_HELP: "Enables deprecated Twig autoescape setting.  When disabled, |raw filter is required to output HTML as Twig will autoescape output"
+  SCHEDULER: "Scheduler"
+  SCHEDULER_INSTALL_INSTRUCTIONS: "Install Instructions"
+  SCHEDULER_INSTALLED_READY: "Installed and Ready"
+  SCHEDULER_CRON_NA: "Cron Not Available for user: <b>%s</b>"
+  SCHEDULER_NOT_ENABLED: "Not Enabled for user: <b>%s</b>"
+  SCHEDULER_SETUP: "Scheduler Setup"
+  SCHEDULER_INSTRUCTIONS: "The <b>Grav Scheduler</b> allows you to create and schedule custom jobs.  It also provides a method for Grav plugins to integrate programmatically and dynamically add jobs to be run periodically."
+  SCHEDULER_POST_INSTRUCTIONS: "To enable the Scheduler's functionality, you must add the <b>Grav Scheduler</b> to your system's crontab file for the <b>%s</b> user.  Run the command above from the terminal to add it automatically. Once saved, refresh this page to see the status."
+  SCHEDULER_JOBS: "Custom Scheduler Jobs"
+  SCHEDULER_STATUS: "Scheduler Status"
+  SCHEDULER_RUNAT: "Run At"
+  SCHEDULER_RUNAT_HELP: "Cron formatted 'at' syntax. NOTE: All times are UTC!"
+  SCHEDULER_OUTPUT: "Output File"
+  SCHEDULER_OUTPUT_HELP: "The path/filename of the output file (from the root of the Grav installation)"
+  SCHEDULER_OUTPUT_TYPE: "Output Type"
+  SCHEDULER_OUTPUT_TYPE_HELP: "Either append to the same file each run, or overwrite the file with each run"
+  SCHEDULER_EMAIL: "Email"
+  SCHEDULER_EMAIL_HELP: "Email to send output to. NOTE: requires output file to be set"
+  SCHEDULER_WARNING: "The scheduler uses your system's crontab system to execute commands. You should use this only if you are an advanced user and know what you are doing. Misconfiguration or abuse can lead to security vulnerabilities."
+  SECURITY: "Security"
+  XSS_SECURITY: "XSS Security for Content"
+  XSS_WHITELIST_PERMISSIONS: "Whitelist Permissions"
+  XSS_WHITELIST_PERMISSIONS_HELP: "Users with these permissions will skip the XSS rules when saving content"
+  XSS_ON_EVENTS: "Filter On-events"
+  XSS_INVALID_PROTOCOLS: "Filter Invalid protocols"
+  XSS_INVALID_PROTOCOLS_LIST: "Invalid protocols list"
+  XSS_MOZ_BINDINGS: "Filter Moz bindings"
+  XSS_HTML_INLINE_STYLES: "Filter HTML inline styles"
+  XSS_DANGEROUS_TAGS: "Filter Dangerous HTML tags"
+  XSS_DANGEROUS_TAGS_LIST: "Dangerous HTML tags list"
+  XSS_ONSAVE_ISSUE: "Save failed: XSS issue detected..."
+  XSS_ISSUE: "<strong>NOTICE:</strong> Grav found potential XSS issues in <strong>%s</strong>"
+  UPLOADS_SECURITY: "Uploads Security"
+  UPLOADS_DANGEROUS_EXTENSIONS: "Dangerous Extensions"
+  UPLOADS_DANGEROUS_EXTENSIONS_HELP: "Block these extensions from being uploaded no matter the accepted MIME types"
+  REPORTS: "Reports"
+  LOGS: "Logs"
+  LOG_VIEWER_FILES: "Log Viewer Files"
+  LOG_VIEWER_FILES_HELP: "Files in /logs/ that will be available to view in Tools - Logs. e.g. 'grav' = /logs/grav.log"
+  BACKUPS_STORAGE_PURGE_TRIGGER: "Backup Storage Purge Trigger"
+  BACKUPS_MAX_COUNT: "Maximum Number of Backups"
+  BACKUPS_MAX_COUNT_HELP: "0 is unlimited"
+  BACKUPS_MAX_SPACE: "Maximum Backups Space"
+  BACKUPS_MAX_RETENTION_TIME: "Maximum Retention Time"
+  BACKUPS_MAX_RETENTION_TIME_APPEND: "in Days"
+  BACKUPS_PROFILE_NAME: "Backup Name"
+  BACKUPS_PROFILE_ROOT_FOLDER: "Root Folder"
+  BACKUPS_PROFILE_ROOT_FOLDER_HELP: "Can be an absolute path or a stream"
+  BACKUPS_PROFILE_EXCLUDE_PATHS: "Exclude Paths"
+  BACKUPS_PROFILE_EXCLUDE_PATHS_HELP: "Absolute paths to exclude, one per line"
+  BACKUPS_PROFILE_EXCLUDE_FILES: "Exclude Files"
+  BACKUPS_PROFILE_EXCLUDE_FILES_HELP: "Specific Files or Folders to exclude, one per line"
+  BACKUPS_PROFILE_SCHEDULE: "Enable Scheduled Job"
+  BACKUPS_PROFILE_SCHEDULE_AT: "Run Scheduled Job"
+  COMMAND: "Command"
+  EXTRA_ARGUMENTS: "Extra Arguments"
+  DEFAULT_LANG: "Override Default Language"
+  DEFAULT_LANG_HELP: "Default is the first supported language. This can be overridden by setting this option but it must be one of the supported languages"
+  DEBUGGER_PROVIDER: "Debugger Provider"
+  DEBUGGER_PROVIDER_HELP: "Default is PHP Debug Bar, but Clockwork browser extension provides for a less intrusive approach"
+  DEBUGGER_DEBUGBAR: "PHP Debug Bar"
+  DEBUGGER_CLOCKWORK: "Clockwork Browser Extension"
+  PAGE_ROUTE_NOT_FOUND: "Page route not found"
+  PAGE_ROUTE_FOUND: "Page route found"
+  NO_ROUTE_PROVIDED: "No route provided"
+  CONTENT_LANGUAGE_FALLBACKS: "Content Language Fallback"
+  CONTENT_LANGUAGE_FALLBACKS_HELP: "By default if the content isn't translated, Grav will display content in the default language. Use this setting to override that behavior per language basis."
+  CONTENT_LANGUAGE_FALLBACK: "Fallback Languages"
+  CONTENT_LANGUAGE_FALLBACK_HELP: "Please enter a list of language codes. Note that if you omit default language code, it will not be used."
+  CONTENT_FALLBACK_LANGUAGE_HELP: "Specify language code which you want to customize."
+  EXPERIMENTAL: "Experimental"
+  PAGES_TYPE: "Frontend Page Type"
+  PAGES_TYPE_HELP: "This option enables Flex Object pages on the front-end. Admin Flex Pages requires Flex Objects plugin"
+  ACCOUNTS_TYPE: "Accounts Type"
+  ACCOUNTS_TYPE_HELP: "Flex Object system to store user accounts"
+  ACCOUNTS_STORAGE: "Account Storage"
+  ACCOUNTS_STORAGE_HELP: "The storage mechanism to be used for Flex Object Account type.  Files is the traditional approach where account are stored in a YAML file in a single folder, while Folder creates a new folder for each account"
+  FLEX: "Flex Object (EXPERIMENTAL)"
+  REGULAR: "Regular"
+  FILE: "File"
+  SANITIZE_SVG: "Sanitize SVG"
+  SANITIZE_SVG_HELP: "Removes any XSS code from SVG"
+  ACCOUNTS: "Accounts"
+  USER_ACCOUNTS: "User Accounts"
+  USER_GROUPS: "User Groups"
+  GROUP_NAME: "Group Name"
+  DISPLAY_NAME: "Display Name"
+  ICON: "Icon"
+  ACCESS: "Access"
+  NO_ACCESS: "No Access"
+  SUPER_USER: "Super User"
+  ALLOWED: "Allowed"
+  DENIED: "Denied"
+  MODULE: "Module"
+  NON_MODULE: "Non-Module"
+  ADD_MODULE: "Add Module"
+  MODULE_SETUP: "Module Setup"
+  MODULE_TEMPLATE: "Module Template"
+  ADD_MODULE_CONTENT: "Add Module Content"
+  CHANGELOG: "Changelog"
+  PAGE_ACCESS: "Page Access"
+  PAGE PERMISSIONS: "Page Permissions"
+  PAGE_ACCESS_HELP: "User with following access permissions can access the page."
+  PAGE_VISIBILITY_REQUIRES_ACCESS: "Menu Visibility Requires Access"
+  PAGE_VISIBILITY_REQUIRES_ACCESS_HELP: "Set to Yes if page should be shown in menus only if user can access them."
+  PAGE_INHERIT_PERMISSIONS: "Inherit Permissions"
+  PAGE_INHERIT_PERMISSIONS_HELP: "Inherit ACL from parent page."
+  PAGE_AUTHORS: "Page Authors"
+  PAGE_AUTHORS_HELP: "Members of Page Authors have owner level access to this page defined in special 'Authors' page group."
+  PAGE_GROUPS: "Page Groups"
+  PAGE_GROUPS_HELP: "Members of Page Groups have special access to this page."
+  READ: "Read"
+  PUBLISH: "Publish"
+  LIST: "List"
+  ACCESS_SITE: "Site"
+  ACCESS_SITE_LOGIN: "Login to Site"
+  ACCESS_ADMIN: "Admin"
+  ACCESS_ADMIN_LOGIN: "Login to Admin"
+  ACCESS_ADMIN_SUPER: "Super User"
+  ACCESS_ADMIN_CACHE: "Clear Cache"
+  ACCESS_ADMIN_CONFIGURATION: "Configuration"
+  ACCESS_ADMIN_CONFIGURATION_SYSTEM: "Manage System Configuration"
+  ACCESS_ADMIN_CONFIGURATION_SITE: "Manage Site Configuration"
+  ACCESS_ADMIN_CONFIGURATION_MEDIA: "Manage Media Configuration"
+  ACCESS_ADMIN_CONFIGURATION_INFO: "See Server Information"
+  ACCESS_ADMIN_SETTINGS: "Settings"
+  ACCESS_ADMIN_PAGES: "Manage Pages"
+  ACCESS_ADMIN_MAINTENANCE: "Site Maintenance"
+  ACCESS_ADMIN_STATISTICS: "Site Statistics"
+  ACCESS_ADMIN_PLUGINS: "Manage Plugins"
+  ACCESS_ADMIN_THEMES: "Manage Themes"
+  ACCESS_ADMIN_TOOLS: "Access to Tools"
+  ACCESS_ADMIN_USERS: "Manage Users"
+  USERS: "Users"
+  ACL: "Access Control"
+  FLEX_CACHING: "Flex Caching"
+  FLEX_INDEX_CACHE_ENABLED: "Enable Index Caching"
+  FLEX_INDEX_CACHE_LIFETIME: "Index Cache Lifetime (seconds)"
+  FLEX_OBJECT_CACHE_ENABLED: "Enable Object Caching"
+  FLEX_OBJECT_CACHE_LIFETIME: "Object Cache Lifetime (seconds)"
+  FLEX_RENDER_CACHE_ENABLED: "Enable Render Caching"
+  FLEX_RENDER_CACHE_LIFETIME: "Render Cache Lifetime (seconds)"
+  DEBUGGER_CENSORED: "Censor Sensitive Data"
+  DEBUGGER_CENSORED_HELP: "Clockwork Provider ONLY: If Yes, censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)"
+  LANGUAGE_TRANSLATIONS: "Translations"
+  LANGUAGE_TRANSLATIONS_HELP: "If false, translation keys are used instead of translated strings. This feature can be used to help to fix bad translations or to find hardcoded English strings."
+  STRICT_BLUEPRINT_COMPAT: "Blueprint Compatibility"
+  STRICT_BLUEPRINT_COMPAT_HELP: "Enables backward compatible strict support for blueprints. If turned off, the new behavior makes the form validation to fail if there is extra data which is not defined in the blueprint."
+  RESET: "Reset"
+  LOGOS: "Logos"
+  PRESETS: "Presets"
+  COLOR_SCHEME_LABEL: "Color Scheme"
+  COLOR_SCHEME_HELP: "Choose a color scheme from a list of predefined combinations, or add your own style"
+  COLOR_SCHEME_NAME: "Custom Color Scheme Name"
+  COLOR_SCHEME_NAME_HELP: "Give a name to your custom theme for exporting and sharing"
+  COLOR_SCHEME_NAME_PLACEHOLDER: "Shades of Blue"
+  PRIMARY_ACCENT_LABEL: "Primary Accent"
+  PRIMARY_ACCENT_HELP: "Select which color set the primary accent should use for it's color scheme"
+  SECONDARY_ACCENT_LABEL: "Secondary Accent"
+  SECONDARY_ACCENT_HELP: "Select which color set the secondary accent should use for it's color scheme"
+  TERTIARY_ACCENT_LABEL: "Tertiary Accent"
+  TERTIARY_ACCENT_HELP: "Select which color set the tertiary accent should use for it's color scheme"
+  WEB_FONTS_LABEL: "Web Fonts"
+  WEB_FONTS_HELP: "Use custom web fonts"
+  HEADER_FONT_LABEL: "Header Font"
+  HEADER_FONT_HELP: "Font used for headers, side nav and section titles"
+  BODY_FONT_LABEL: "Body Font"
+  BODY_FONT_HELP: "Primary font used throughout the body of the theme"
+  CUSTOM_CSS_LABEL: "Custom CSS"
+  CUSTOM_CSS_PLACEHOLDER: "Put your custom CSS in here..."
+  CUSTOM_CSS_HELP: "Custom CSS that will be added to every admin page"
+  CUSTOM_FOOTER: "Custom Footer"
+  CUSTOM_FOOTER_HELP: "You can use HTML and/or Markdown syntax here"
+  CUSTOM_FOOTER_PLACEHOLDER: "Enter HTML/Markdown to override default footer"
+  LOGIN_SCREEN_CUSTOM_LOGO_LABEL: "Login Custom Logo"
+  LOGIN_SCREEN_CUSTOM_LOGO_HELP: ""
+  TOP_LEFT_CUSTOM_LOGO_LABEL: "Primary Custom Logo"
+  TOP_LEFT_CUSTOM_LOGO_HELP: ""
+  LOAD_PRESET: "Load Preset"
+  RECOMPILE: "Recompile"
+  EXPORT: "Export"
+  QUICKTRAY_RECOMPILE: "QuickTray Recompile Icon"
+  QUICKTRAY_RECOMPILE_HELP: "Will recompile the preset SCSS to pickup any changes or new plugins"
+  CODEMIRROR: "CodeMirror Editor"
+  CODEMIRROR_THEME: "Editor Theme"
+  CODEMIRROR_THEME_DESC: "**NOTE:** Use the [CodeMirror Themes Demo](https://codemirror.net/demo/theme.html?target=_blank) to see these in action. **_Paper_** is the default Grav theme."
+  CODEMIRROR_FONTSIZE: "Editor Font Size"
+  CODEMIRROR_FONTSIZE_SM: "Small Font"
+  CODEMIRROR_FONTSIZE_MD: "Medium Font"
+  CODEMIRROR_FONTSIZE_LG: "Large Font"
+  CODEMIRROR_MD_FONT: "Markdown Editor Font"
+  CODEMIRROR_MD_FONT_SANS: "Sans Font"
+  CODEMIRROR_MD_FONT_MONO: "Mono/Fixed Width Font"
+  CUSTOM_PRESETS: "Custom Presets"
+  CUSTOM_PRESETS_HELP: "Drag-n-drop a theme .yaml file here, or you can create an array of presets with text based keys"
+  CUSTOM_PRESETS_PLACEHOLDER: "Put your presets here"
+  GENERAL: "General"
+  CONTENT_EDITOR: "Content Editor"
+  CONTENT_EDITOR_HELP: "Custom editors can be preferred for content editing"
+  BAD_FILENAME: "Bad filename"
+  SHOW_SENSITIVE: "Show Sensitive Data"
+  SHOW_SENSITIVE_HELP: "Clockwork Provider ONLY: Censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)"
+  VALID_LINK_ATTRIBUTES: "Valid Link Attributes"
+  VALID_LINK_ATTRIBUTES_HELP: "Attributes that will be automatically added to the media HTML element"
+  CONFIGURATION: "Configuration"
+  CUSTOMIZATION: "Customization"
+  EXTRAS: "Extras"
+  BASICS: "Basics"
+  ADMIN_CACHING: "Enable Admin Caching"
+  ADMIN_CACHING_HELP: "Caching in the admin can be controlled independently from the front-end site"
+  ADMIN_PATH: "Administrator path"
+  ADMIN_PATH_PLACEHOLDER: "Default route for administrator (relative to base)"
+  ADMIN_PATH_HELP: "If you want to change the URL for the administrator, you can provide a path here"
+  LOGO_TEXT: "Logo text"
+  LOGO_TEXT_HELP: "Text to display in place of the default Grav logo"
+  CONTENT_PADDING: "Content padding"
+  CONTENT_PADDING_HELP: "Enable/Disable content padding around content area to provide more space"
+  BODY_CLASSES: "Body classes"
+  BODY_CLASSES_HELP: "Add a space separated name of custom body classes"
+  SIDEBAR_ACTIVATION: "Sidebar Activation"
+  SIDEBAR_ACTIVATION_HELP: "Control how the sidebar is activated"
+  SIDEBAR_HOVER_DELAY: "Hover delay"
+  SIDEBAR_HOVER_DELAY_APPEND: "millseconds"
+  SIDEBAR_ACTIVATION_TAB: "Tab"
+  SIDEBAR_ACTIVATION_HOVER: "Hover"
+  SIDEBAR_SIZE: "Sidebar Size"
+  SIDEBAR_SIZE_HELP: "Control the width of the sidebar"
+  SIDEBAR_SIZE_AUTO: "Automatic width"
+  SIDEBAR_SIZE_SMALL: "Small width"
+  EDIT_MODE: "Edit mode"
+  EDIT_MODE_HELP: "Auto will use blueprint if available, if none found, it will use \"Expert\" mode."
+  FRONTEND_PREVIEW_TARGET: "Preview pages target"
+  FRONTEND_PREVIEW_TARGET_INLINE: "Inline in Admin"
+  FRONTEND_PREVIEW_TARGET_NEW: "New tab"
+  FRONTEND_PREVIEW_TARGET_CURRENT: "Current tab"
+  PARENT_DROPDOWN: "Parent dropdown"
+  PARENT_DROPDOWN_BOTH: "Show slug and folder"
+  PARENT_DROPDOWN_FOLDER: "Show folder"
+  PARENT_DROPDOWN_FULLPATH: "Show fullpath"
+  PARENTS_LEVELS: "Parents levels"
+  PARENTS_LEVELS_HELP: "The number of levels to show in parent select list"
+  MODULAR_PARENTS: "Modular parents"
+  MODULAR_PARENTS_HELP: "Show modular pages in the parent select list"
+  SHOW_GITHUB_LINK: "Show GitHub Link"
+  SHOW_GITHUB_LINK_HELP: "Show the \"Found an issue? Please report it on GitHub.\" message."
+  PAGES_LIST_DISPLAY_FIELD: "Pages List Display Field"
+  PAGES_LIST_DISPLAY_FIELD_HELP: "Field of the page to use in the list of pages if present. Defaults/Fallback to title."
+  AUTO_UPDATES: "Automatically check for updates"
+  AUTO_UPDATES_HELP: "Shows an informative message, in the admin panel, when an update is available."
+  TIMEOUT: "Timeout"
+  TIMEOUT_HELP: "Sets the session timeout in seconds"
+  HIDE_PAGE_TYPES: "Hide page types in Admin"
+  HIDE_MODULAR_PAGE_TYPES: "Hide modular page types in Admin"
+  DASHBOARD: "Dashboard"
+  WIDGETS_DISPLAY: "Widget Display Status"
+  NOTIFICATIONS: "Notifications"
+  FEED_NOTIFICATIONS: "Feed Notifications"
+  FEED_NOTIFICATIONS_HELP: "Display feed-based notifications"
+  DASHBOARD_NOTIFICATIONS: "Dashboard Notifications"
+  DASHBOARD_NOTIFICATIONS_HELP: "Display dashboard-based notifications"
+  PLUGINS_NOTIFICATIONS: "Plugins Notifications"
+  PLUGINS_NOTIFICATIONS_HELP: "Display plugins-targeted notifications"
+  THEMES_NOTIFICATIONS: "Themes Notifications"
+  THEMES_NOTIFICATIONS_HELP: "Display themes-targeted notifications"
+  LOGO_BG_HELP: "Logo bg"
+  LOGO_LINK_HELP: "Logo link"
+  NAV_BG_HELP: "Nav bg"
+  NAV_TEXT_HELP: "Nav text"
+  NAV_LINK_HELP: "Nav link"
+  NAV_SELECTED_BG_HELP: "Nav selected bg"
+  NAV_SELECTED_LINK_HELP: "Nav selected link"
+  NAV_HOVER_BG_HELP: "Nav hover bg"
+  NAV_HOVER_LINK_HELP: "Nav hover link"
+  TOOLBAR_BG_HELP: "Toolbar bg"
+  TOOLBAR_TEXT_HELP: "Toolbar text"
+  PAGE_BG_HELP: "Page bg"
+  PAGE_TEXT_HELP: "Page text"
+  PAGE_LINK_HELP: "Page link"
+  CONTENT_BG_HELP: "Content bg"
+  CONTENT_TEXT_HELP: "Content text"
+  CONTENT_LINK_HELP: "Content link"
+  CONTENT_LINK2_HELP: "Content link 2"
+  CONTENT_HEADER_HELP: "Content header"
+  CONTENT_TABS_BG_HELP: "Content tabs bg"
+  CONTENT_TABS_TEXT_HELP: "Content tabs text"
+  CONTENT_HIGHLIGHT_HELP: "Content highlight"
+  BUTTON_BG_HELP: "Button bg"
+  BUTTON_TEXT_HELP: "Button text"
+  NOTICE_BG_HELP: "Notice bg"
+  NOTICE_TEXT_HELP: "Notice text"
+  UPDATES_BG_HELP: "Updates bg"
+  UPDATES_TEXT_HELP: "Updates text"
+  CRITICAL_BG_HELP: "Critical bg"
+  CRITICAL_TEXT_HELP: "Critical text"
+  BUTTON_COLORS: "Button colors"
+  CONTENT_COLORS: "Content colors"
+  TABS_COLORS: "Tabs colors"
+  CRITICAL_COLORS: "Critical colors"
+  LOGO_COLORS: "Logo colors"
+  NAV_COLORS: "Nav colors"
+  NOTICE_COLORS: "Notice colors"
+  PAGE_COLORS: "Page colors"
+  TOOLBAR_COLORS: "Toolbar colors"
+  UPDATE_COLORS: "Update colors"
+  POPULARITY: "Popularity"
+  VISITOR_TRACKING: "Visitor tracking"
+  VISITOR_TRACKING_HELP: "Enable the visitors stats collecting feature"
+  DAYS_OF_STATS: "Days of stats"
+  DAYS_OF_STATS_HELP: "Keep stats for the specified number of days, then drop them"
+  IGNORE_URLS: "Ignore"
+  IGNORE_URLS_HELP: "URLs to ignore"
+  DAILY_HISTORY: "Daily history"
+  MONTHLY_HISTORY: "Monthly history"
+  VISITORS_HISTORY: "Visitors history"
+  MEDIA_RESIZE: "Page Media Image Resizer"
+  PAGEMEDIA_RESIZER: "> The following settings apply to images uploaded through the page media. Resize Width / Height will automatically resize down and proportionally an image if exceeds the limits set in the inputs. Resolution min and max values define the size ranges for uploaded images. Set the fields to 0 to prevent any manipulation."
+  RESIZE_WIDTH: "Resize Width"
+  RESIZE_WIDTH_HELP: "Resize wide images down to the set value"
+  RESIZE_HEIGHT: "Resize Height"
+  RESIZE_HEIGHT_HELP: "Resize tall images down to the set value"
+  RES_MIN_WIDTH: "Resolution Min Width"
+  RES_MIN_WIDTH_HELP: "The minimum width allowed for an image to be added"
+  RES_MIN_HEIGHT: "Resolution Min Height"
+  RES_MIN_HEIGHT_HELP: "The minimum height allowed for an image to be added"
+  RES_MAX_WIDTH: "Resolution Max Width"
+  RES_MAX_WIDTH_HELP: "The maximum width allowed for an image to be added"
+  RES_MAX_HEIGHT: "Resolution Max Height"
+  RES_MAX_HEIGHT_HELP: "The maximum height allowed for an image to be added"
+  RESIZE_QUALITY: "Resize Quality"
+  RESIZE_QUALITY_HELP: "The quality to use when resizing an image. Between 0 and 1 value."
+  PIXELS: "pixels"
+  ACCESS_ADMIN_CONFIGURATION_SECURITY: "Manage Security Configuration"
+  SESSION_DOMAIN: "Session domain"
+  SESSION_DOMAIN_HELP: "Use only if you you rewrite the site domain for example in a Docker Container."
+  SESSION_PATH: "Session path"
+  SESSION_PATH_HELP: "Use only if you you rewrite the site path for example in a Docker Container."
+  REDIRECT_OPTION_NO_REDIRECT: "No redirect"
+  REDIRECT_OPTION_DEFAULT_REDIRECT: "Use default redirect code"
+  REDIRECT_OPTION_301: "301 - Moved permanently"
+  REDIRECT_OPTION_302: "302 - Moved temporarily"
+  REDIRECT_OPTION_303: "303 - See Other"
+  IMAGES_CLS_TITLE: "Cumulative Layout Shift (CLS)"
+  IMAGES_CLS_AUTO_SIZES: "Enable Auto Sizes"
+  IMAGES_CLS_AUTO_SIZES_HELP: "Automatically add 'width' and 'height' attributes to images to address CLS"
+  IMAGES_CLS_ASPECT_RATIO: "Enable Aspect Ratio"
+  IMAGES_CLS_ASPECT_RATIO_HELP: "Optional CSS variable that gets applied via a 'style' attribute which can be used in CSS for custom styling"
+  IMAGES_CLS_RETINA_SCALE: "Retina Scale Factor"
+  IMAGES_CLS_RETINA_SCALE_HELP: "Will take the calculated size and divide by scale factor to display a higher resolution image at a smaller pixel size for better handling of HiDPI resolutions"
+  AUTOREGENERATE_FOLDER_SLUG: "Auto-regenerate based on page title"
+  ENABLE: Enable
+  PLUGINS_MUST_BE_ENABLED: "Plugin must be enabled to configure"
+  ACTIVATION_REQUIRED: "Activation required to configure"
+  SESSION_SECURE_HTTPS: "Secure (HTTPS)"
+  SESSION_SECURE_HTTPS_HELP: "Set session secure on HTTPS but not on HTTP. Has no effect if you have above Secure setting set to true. Set to false if your site jumps between HTTP and HTTPS."
+  AVATAR: "Avatar Generator"
+  AVATAR_HELP: "Multiavatar is a locally generated avatar.  Gravatar is an external service that uses your email address to pull a preconfigured Avatar remotely"
+  AVATAR_HASH: "NOTE: Optional Avatar custom 'hash' string"
+  IMAGES_TITLE: "Images"
+  LEGACY_MEDIA_MUTATION: "Legacy Media Manipulation Compatibility"
+  LEGACY_MEDIA_MUTATION_HELP: "Enable this setting only if image manipulation broke after Grav update."
+  BACKWARD_COMPATIBILITY: "Backward Compatibility"

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott