armansansd 2 年 前
コミット
4b7c97a02e
100 ファイル変更27982 行追加0 行削除
  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. 2625 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. 307 0
      plugins/admin/languages/ar.yaml
  91. 356 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. 710 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

+ 2625 - 0
plugins/admin/CHANGELOG.md

@@ -0,0 +1,2625 @@
+# v1.10.42
+## 06/14/2023
+
+1. [](#new)
+   * Added a couple of string translations
+
+# v1.10.41.2
+## 05/11/2023
+
+1. [](#improved)
+   * Fixed an issue with `lastBackup()` that caused admin dashboard to fail with an error.
+
+# v1.10.41.1
+## 05/09/2023
+
+1. [](#improved)
+   * Fixed another Toolbox deprecation error for `lastBackup()`
+
+# v1.10.41
+## 05/09/2023
+
+1. [](#new)
+   * Updated to use new `BaconQRCode` version `2.0.8` for new SVG features + PHP 8.2+ fixes
+1. [](#improved)
+   * Require Grav `v1.7.41`
+   * Fixed a deprecated message where `Admin::$routes` was being dynamically defined
+   * Fixes to use non-deprecated methods in `ScssCompiler`
+
+# v1.10.40
+## 03/22/2023
+
+1. [](#new)
+   * Added Github actions for dependabot [#2258](https://github.com/getgrav/grav-plugin-admin/pull/2258)
+1. [](#improved)
+   * Syslog tag fields label added [#2296](https://github.com/getgrav/grav-plugin-admin/pull/2296)
+   * Updated vendor libraries to the latest versions
+1. [](#bugfix)
+   * Fix more than one file upload [#2317](https://github.com/getgrav/grav-plugin-admin/pull/2317)
+
+# v1.10.39
+## 02/19/2023
+
+1. [](#bugfix)
+   * Forked and fixed PicoFeed library to support PHP 8.2
+
+# v1.10.38
+## 01/02/2023
+
+1. [](#new)
+   * Update copyright dates
+   * Keep version number in sync with Grav version
+
+# v1.10.37.1
+## 10/08/2022
+
+1. [](#bugfix)
+   * Removed new GumRoad cart icon + new button styling [getgrav/grav#3631](https://github.com/getgrav/grav/issues/3631)
+
+# v1.10.37
+## 10/05/2022
+
+1. [](#improved)
+   * Updated vendor libraries to latest versions
+   * Removed a reference to `SwiftMailer` library to support new **Email** plugin v4.0
+
+# v1.10.36
+## 09/08/2022
+
+1. [](#bugfix)
+   * Fixed `fieldset.html.twig` not rendering with `markdown: false` [#2313](https://github.com/getgrav/grav-plugin-admin/pull/2313)
+
+# v1.10.35
+## 08/04/2022
+
+1. [](#improved)
+   * Improvements in CodeMirror editor in RTL mode [#359](https://github.com/getgrav/grav-plugin-admin/issues/359), [#2297](https://github.com/getgrav/grav-plugin-admin/pull/2297)
+
+# v1.10.34
+## 06/22/2022
+
+1. [](#improved)
+   * Exposed `UriToMarkdown` util (`Grav.default.Utils.UriToMarkdown`) in admin, to convert links/images
+1. [](#bugfix)
+   * Fixed `Latest Page Updates` permissions [#2294](https://github.com/getgrav/grav-plugin-admin/pull/2294)
+
+# v1.10.33.1
+## 04/25/2022
+
+1. [](#bugfix)
+   * Reverted [PR#2265](https://github.com/getgrav/grav-plugin-admin/pull/2265) as it broke sections output
+
+# v1.10.33
+## 04/25/2022
+
+1. [](#new)
+  * Require **Form 6.0.1**
+2. [](#improved)
+   * Added support for a single `field:` vs `fields:` in element form field to store a single value to the option field
+   * Allow new media collapser logic to configure different cookie storage name location via `data-storage-location`
+1. [](#bugfix)
+   * Fixed nested element form fields
+   * Fixed `columns` and `column` fields with `.dotted` variables inside to ignore columns and column names
+   * Fixed initial elements state not being restored
+
+# v1.10.32
+## 03/28/2022
+
+1. [](#new)
+   * Require **Grav 1.7.32**, **Form 6.0.0**, **Login 3.7.0**, **Email 3.1.6** and **Flex Objects 1.2.0**
+2. [](#improved)
+   * List field: Support for default values other than key/value [#2255](https://github.com/getgrav/grav-plugin-admin/issues/2255)
+   * Added question icon to admin fields with help text [#2261](https://github.com/getgrav/grav-plugin-admin/issues/2261)
+3. [](#bugfix)
+   * Fix nested `toggleable`: originalValue now checks with `??` instead of `is defined`
+
+# v1.10.31
+## 03/14/2022
+
+1. [](#new)
+   * Added new local Multiavatar (local generation). **This will be default in Grav 1.8**
+2. [](#bugfix)
+   * Patch `collection.js` [#2235](https://github.com/getgrav/grav-plugin-admin/issues/2235)
+
+# v1.10.30.2
+## 02/09/2022
+
+2. [](#bugfix)
+   * Fixed regression preventing new `elements` field from saving its state
+
+# v1.10.30.1
+## 02/09/2022
+
+1. [](#improved)
+   * List field items will now require confirmation before getting deleted
+
+# v1.10.30
+## 02/07/2022
+
+1. [](#new)
+   * Require **Grav 1.7.30**
+   * Updated SCSS compiler to v1.10
+   * PageMedia can now be collapsed and thumbnails previewed smaller, in order to save room on the page. Selection will be remembered.
+   * DEPRECATED: Admin field `pages_list_display_field` is no longer available as an option [#2191](https://github.com/getgrav/grav-plugin-admin/issues/2191)
+   * When listing installable themes/plugins, it is now possible to sort them by [Premium](https://getgrav.org/premium)
+2. [](#improved)
+   * Updated JavaScript dependencies
+   * Cleaned up JavaScript unused dependencies and warnings
+   * Removed unused style assets
+   * Plugins list rows now properly highlight on hover, no more guessing when wanting to disable a plugin!
+3. [](#bugfix)
+   * Fixed `elements` field when it's used inside `list` field
+   * Fixed issue uploading non-images media when Resolution setting enabled in Admin [#2172](https://github.com/getgrav/grav-plugin-admin/issues/2172)
+   * Prevent fields from being toggled incorrectly by adding originalValue to childs of fieldset. [#2218](https://github.com/getgrav/grav-plugin-admin/pull/2218)
+   * Fixed persistent focus on Folder field when Adding page (Safari) [#2209](https://github.com/getgrav/grav-plugin-admin/issues/2209)
+   * Fixed performance of Plugins / Themes sort in the installation table
+   * Fixed list field with key/value pairs throwing an exception due to bad value [#2199](https://github.com/getgrav/grav-plugin-admin/issues/2199)
+   * Fixed disabling/enabling plugin from the list breaking the plugin configuration
+
+# v1.10.29
+## 01/28/2022
+
+1. [](#new)
+   * Require **Grav 1.7.29**
+3. [](#improved)
+   * Made path handling unicode-safe, use new `Utils::basename()` and `Utils::pathinfo()` everywhere
+
+# v1.10.28
+## 01/24/2022
+
+1. [](#bugfix)
+   * Clean file names before displaying errors/metadata modals
+   * Recompiled JS for production [#2225](https://github.com/getgrav/grav-plugin-admin/issues/2225)
+
+# v1.10.27
+## 01/12/2022
+
+1. [](#new)
+   * Support for `YubiKey OTP` 2-Factor authenticator
+   * New `elements` container field that shows/hides children fields based on boolean trigger value
+   * Requires Grav `v1.7.27` and Login `v3.6.2`
+2. [](#improved)
+   * Added new asset language strings
+
+# v1.10.26.1
+## 01/03/2022
+
+3. [](#bugfix)
+   * Fixed an issue with missing files reference by cached autoloader
+
+# v1.10.26
+## 01/03/2022
+
+2. [](#improved)
+   * Updated SCSS compiler to v1.9 and other vendor libraries
+   * Fixed various deprecation warnings
+   * Localized dialog buttons and icons [#2207](https://github.com/getgrav/grav-plugin-admin/pull/2207)
+   * Updated copyright year
+
+# v1.10.25
+## 11/16/2021
+
+3. [](#bugfix)
+   * Fixed unescaped messages in JSON responses
+
+# v1.10.24
+## 10/26/2021
+
+1. [](#new)
+   * Require **Grav 1.7.24**
+2. [](#improved)
+   * Use new `Http\Response` rather than deprecated `GPM\Response`
+3. [](#bugfix)
+   * Fixed an issue with invalid HTML throwing errors on HTML security scanning
+   * Clear cache when installing plugins
+
+# v1.10.23
+## 09/29/2021
+
+1. [](#new)
+   * Updated SCSS compiler to v1.8
+2. [](#improved)
+   * Updated with latest language strings from Crowdin.com
+3. [](#bugfix)
+   * Fixed images from plugins/themes disappearing when saving twice
+
+# v1.10.22
+## 09/16/2021
+
+1. [](#new)
+    * Updated SCSS compiler to v1.7
+
+# v1.10.21
+## 09/14/2021
+
+1. [](#new)
+    * Require **Grav 1.7.21**
+2. [](#improved)
+    * Added a note about UTC times in scheduler AT syntax help
+    * Now using a monospaced text-based scheduler AT field in scheduler for simplicity
+    * Improved `Admin:data()` and `Admin::getConfigurationData()` to be more strict
+3. [](#bugfix)
+    * Fixed configuration save location to point to existing config folder [#2176](https://github.com/getgrav/grav-plugin-admin/issues/2176)
+
+# v1.10.20
+## 09/01/2021
+
+1. [](#bugfix)
+    * Fixed regression `Argument 4 passed to Grav\Plugin\Form\TwigExtension::prepareFormField() must be of the type array` [#2177](https://github.com/getgrav/grav-plugin-admin/issues/2177)
+    * Fixed `X-Frame-Options` to be `DENY` in all admin pages to prevent a clickjacking attack
+
+# v1.10.19
+## 08/31/2021
+
+1. [](#new)
+    * Require **Grav 1.7.19** and **Form 5.1.0** and **Login 3.5.0**
+    * Updated SCSS compiler to v1.6
+2. [](#improved)
+    * Updated forms and nested fields to use new form logic
+    * Admin form now use layout `admin`, meaning you can create admin specific field templates by `forms/fields/myfield/admin-field.html.twig`
+    * Stop using `|tu` filter, Grav already has the same logic in `|t` for admin
+    * Remove unneeded escapes
+    * Allow removal of plugin when disabled [#2167](https://github.com/getgrav/grav-plugin-admin/issues/2167)
+3. [](#bugfix)
+    * Fixed missing values in `fieldset` form field
+
+# v1.10.18
+## 07/19/2021
+
+1. [](#improved)
+    * Add logic to allow fieldset form field inside a list field [#2159](https://github.com/getgrav/grav-plugin-admin/pull/2159)
+
+# v1.10.17
+## 06/15/2021
+
+1. [](#improved)
+    * Added timestamp as title in logs date [#2141](https://github.com/getgrav/grav-plugin-admin/issues/2141)
+    * Use `base64_encode` filter rather than function
+    * Composer update
+1. [](#bugfix)
+    * Fixed missing `Remove Theme` button when the theme is inactive
+    * Update taskGetChildTypes() to use Flex Pages (works without the plugin) [#2087](https://github.com/getgrav/grav-plugin-admin/issues/2087)
+
+# v1.10.16
+## 06/02/2021
+
+1. [](#bugfix)
+    * Fixed issue with some elements overflowing closed list items [#2146](https://github.com/getgrav/grav-plugin-admin/issues/2146)
+    * Fixed configuration not fully updating on save [#2149](https://github.com/getgrav/grav-plugin-admin/issues/2149)
+    * Fixed display issue with "+ Add Page" and picking a different route [#2136](https://github.com/getgrav/grav-plugin-admin/issues/2136), [#2145](https://github.com/getgrav/grav-plugin-admin/issues/2145)
+    * Treat WebP as image when inserting / drag & dropping [#2150](https://github.com/getgrav/grav-plugin-admin/issues/2150)
+
+# v1.10.15
+## 05/19/2021
+
+1. [](#new)
+    * Updated SCSS compiler to v1.5
+1. [](#improved)
+    * Updated node modules dev dependencies
+    * Package.json scripts cleanup
+    * Recompiled JS for production
+    * Use `base645_encode` filter rather than function
+    * Editor: Do not assume images URLs are going to be `http://` (wrong assumption plus not SSL) [#2127](https://github.com/getgrav/grav-plugin-admin/issues/2127)
+    * Improved Theme Activation + Plugin Enabled logic to ensure configuration is not displayed unless activation/enabled state. Fixes [#2140](https://github.com/getgrav/grav-plugin-admin/issues/2140)
+1. [](#bugfix)
+    * Fixed issue with slugify where single curly quotes in titles would translate to straight single quote [#2101](https://github.com/getgrav/grav-plugin-admin/issues/2101)
+    * Fix z-index issue with fullscreeen editor (and toolips) [#2143](https://github.com/getgrav/grav-plugin-admin/issues/2143)
+
+# v1.10.14
+## 04/29/2021
+
+1. [](#improved)
+    * Added a `min_height:` option for list field
+1. [](#bugfix)
+    * Fixed z-index issue for tooltips in sidebar
+    * Fixed custom files being overridden during theme update [#2135](https://github.com/getgrav/grav-plugin-admin/issues/2135)
+
+# v1.10.13
+## 04/23/2021
+
+1. [](#new)
+    * Added refresh action button for Folder to ease the regeneration of the slug based on the title. Available also as API entry `Grav.default.Forms.Fields.FolderField.Regenerate()` [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738)
+1. [](#improved)
+    * Removed sourcemaps references from fork-awesome.min.css [#2122](https://github.com/getgrav/grav-plugin-admin/issues/2122)
+    * Support native spell checkers in CodeMirror editor [#1266](https://github.com/getgrav/grav-plugin-admin/issues/1266)
+    * Added new 'Content Highlight' color to presets
+    * Copying Pages now prompts a dedicated modal that allows for picking title, folder name, parent location, page template and visibility [#1738](https://github.com/getgrav/grav-plugin-admin/issues/1738)
+    * Updated with latest language translations from Crowdin.com
+1. [](#bugfix)
+    * Moved preset CSS compile to earlier in the process to ensure compilation happens in time.
+    * Prevent Save actions from Flex Objects to trigger the unsaved unload notice [#2125](https://github.com/getgrav/grav-plugin-admin/issues/2125)
+    * Fixed audit vulnerabilities in module dependencies and house cleanup [#2096](https://github.com/getgrav/grav-plugin-admin/issues/2096)
+    * Fixed issue preventing Drag & Drop of media files while in Expert Mode [#1927](https://github.com/getgrav/grav-plugin-admin/issues/1927)
+    * Fixed broken link colors in `preset.css` which was causing issues with tabs and dropdowns
+    * Fixed permissions for page related tasks and actions
+    * Fixed permission check for configuration save [#2130](https://github.com/getgrav/grav-plugin-admin/issues/2130)
+    * Fixed missing/wrong page categories and tags when multi-language support is enabled [#2107](https://github.com/getgrav/grav-plugin-admin/issues/2107)
+
+# v1.10.12
+## 04/15/2021
+
+1. [](#bugfix)
+    * Regression: Fixed broken plugin/theme installer in admin
+    * Fixed error reporting for AJAX tasks if user has no permissions
+    * Fixed missing slash in password reset URL [#2119](https://github.com/getgrav/grav-plugin-admin/issues/2119)
+
+# v1.10.11
+## 04/13/2021
+
+1. [](#bugfix)
+    * **IMPORTANT** Fixed security vulnerability that allows installation of plugins with minimal admin privileges [GHSA-wg37-cf5x-55hq](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-wg37-cf5x-55hq)
+    * Fixed `You have been logged out` message when entering to 2FA authentication due to `/admin/task:getNotifications` AJAX call
+    * Fixed broken 2FA login when site is not configured to use Flex Users [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109)
+    * Fixed error message when user clicks logout link after the session has been expired
+
+# v1.10.10
+## 04/07/2021
+
+1. [](#bugfix)
+    * Fixed missing `admin-preset.css` in multisite environments
+    * Regression: Fixed broken 2FA form [#2109](https://github.com/getgrav/grav-plugin-admin/issues/2109)
+
+# v1.10.9
+## 04/06/2021
+
+1. [](#new)
+    * Requires **Grav 1.7.10**
+1. [](#improved)
+    * Better isolate admin to prevent session related vulnerabilities
+    * Removed support for custom login redirects for improved security
+    * Shorten forgot password link lifetime from 7 days to 1 hour
+    * Updated with latest language translations from Crowdin.com
+1. [](#bugfix)
+    * Fixed issue where Adding a new page and canceling from within Editing would alter the Parent location of the edited page [#2067](https://github.com/getgrav/grav-plugin-admin/issues/2067)
+    * Fixed and enhanced Range field to be Lists compatible [#2062](https://github.com/getgrav/grav-plugin-admin/issues/2062)
+    * Fixed ERR_TOO_MANY_REDIRECTS with HTTPS = 'On' [#2100](https://github.com/getgrav/grav-plugin-admin/issues/2100)
+    * Prevent expert editing mode from anyone else than super users [#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094)
+    * Fixed login related pages being accessible from admin when user has logged in
+    * Fixed admin user creation and password reset allowing unsafe passwords
+    * Fixed missing validation when registering the first admin user
+    * Fixed reset password email not to have session specific token in it
+    * Fixed admin controller running before setting `$grav['page']`
+
+# v1.10.8
+## 03/19/2021
+
+1. [](#improved)
+    * Include alt text and title for images added to the editor [#2098](https://github.com/getgrav/grav-plugin-admin/issues/2098)
+1. [](#bugfix)
+    * Fixed issue replacing `wildcard` field names in flex collections [#2092](https://github.com/getgrav/grav-plugin-admin/pull/2092)
+    * Fixed legacy Pages having old `modular` reference instead of `module` [#2093](https://github.com/getgrav/grav-plugin-admin/issues/2093)
+    * Fixed issue where Add New modal would close if selecting an item outside of the modal window. It is now necessary go through the Cancel button and clicking the overlay won't trigger the closing of the modal [#2089](https://github.com/getgrav/grav-plugin-admin/issues/2089), [#2065](https://github.com/getgrav/grav-plugin-admin/issues/2065)
+
+# v1.10.7
+## 03/17/2021
+
+1. [](#improved)
+    * Force height of Flex pages admin to fit available space
+    * Updated languages from Crowdin.com
+    * Better field type definitions for file, pagemedia, filepicker and pagemediafield
+1. [](#bugfix)
+    * Fixed error when checking missing log file [#2088](https://github.com/getgrav/grav-plugin-admin/issues/2088)
+
+# v1.10.6
+## 02/23/2021
+
+1. [](#new)
+    * Vastly improved support for RTL languages [#2078](https://github.com/getgrav/grav-plugin-admin/pull/2078)
+1. [](#improved)
+    * Flex pages admin better uses available space [#2075](https://github.com/getgrav/grav/issues/2075)
+1. [](#bugfix)
+    * Regression: Fixed enabling/disabling plugin or theme corrupting configuration
+    * Fixed unnecessary closing bracket causing JS error [#2079](https://github.com/getgrav/grav-plugin-admin/issues/2079)
+    * Fixed wrong language in Admin Tools [#2077](https://github.com/getgrav/grav-plugin-admin/issues/2077)
+
+# v1.10.5
+## 02/18/2021
+
+1. [](#bugfix)
+    * Regression: Fixed fatal error in admin if POST request has `data` in it [#2074](https://github.com/getgrav/grav-plugin-admin/issues/2074)
+    * Fixed Admin creating empty `user/config/info.yaml` file (the file can be safely removed, it is not in use)
+    * Fixed ACL for users with mixed case usernames [#2073](https://github.com/getgrav/grav-plugin-admin/issues/2073)
+
+# v1.10.4
+## 02/17/2021
+
+1. [](#new)
+    * Added support to include new page creation modals in other pages by using `form_action` twig variable [#2024](https://github.com/getgrav/grav-plugin-admin/pull/2024)
+    * Updated all languages from [Crowdin](https://crowdin.com/project/grav-admin) - Please update any translations here
+1. [](#improved)
+    * Removed `noscript` template, because 2021...
+    * List field: added new `placement` property to decide wether to add new items at the top, bottom or based on the *position* of the clicked button [#2055](https://github.com/getgrav/grav-plugin-admin/pull/2055)
+    * Ensure admin default CSS styles load **first**, and presets loads **last**
+    * Tweaked handling of uploaded files [#1429](https://github.com/getgrav/grav-plugin-admin/issues/1429)
+    * Provide media object and filename in `onAdminAfterDelMedia` event [#1905](https://github.com/getgrav/grav-plugin-admin/pull/1905)
+1. [](#bugfix)
+    * Fixed case-sensitive `accept` in `filepicker` field
+    * Fixed HTML Entities in titles [#2028](https://github.com/getgrav/grav-plugin-admin/issues/2028)
+    * Fixed deleting list field options completely, didn't save changes [#2056](https://github.com/getgrav/grav-plugin-admin/issues/2056)
+    * Fixed `onAdminAfterAddMedia` and `onAdminAfterDelMedia` events always pointing to the home page
+    * Fixed ACL for Configuration tabs [#771](https://github.com/getgrav/grav-plugin-admin/issues/771)
+    * Fixed changelog button showing up in Info page even if user cannot access it
+    * Fixed toggleable checkboxes being unchecked in fieldset columns [#2063](https://github.com/getgrav/grav-plugin-admin/issues/2063)
+    * Fixed issue with max backups of zero [#2070](https://github.com/getgrav/grav-plugin-admin/issues/2070)
+
+# v1.10.3
+## 02/01/2021
+
+1. [](#new)
+    * Requires **Grav 1.7.4** (SemVer library moved to Grav)
+    * Added back special fonts (including Gantry)
+2. [](#bugfix)
+    * Fixed field type `range` not taking into account legitimate `0` values
+    * Fixed `Call to a member function trackHit() on null` [#2049](https://github.com/getgrav/grav-plugin-admin/issues/2049)
+
+# v1.10.2
+## 01/21/2021
+
+2. [](#bugfix)
+    * Fixed admin style compilation failing to save CSS if assets folder does not exist
+
+# v1.10.1
+## 01/20/2021
+
+1. [](#improved)
+    * Added `watch.sh` for compiling SCSS with native sass compiler
+2. [](#bugfix)
+    * Fixed issue with overlapping sidebar when using fullscreen editor [#2022](https://github.com/getgrav/grav-plugin-admin/issues/2022)
+
+# v1.10.0
+## 01/19/2021
+
+1. [](#new)
+    * Requires **Grav 1.7 and PHP 7.3.6**
+    * Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post
+    * Read the full list of changes in the [Changelog on GitHub](https://github.com/getgrav/grav-plugin-admin/blob/1.10.0/CHANGELOG.md)
+    * Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading!
+1. [](#improved)
+    * Various notifications improvements
+1. [](#bugfix)
+    * Fixed missed highlight on the selected page in Parents field
+    * Fixed notifications that would not be remembered as hidden
+    * Fixed taxonomy field not listing existing options in Flex Pages
+    * Fixed taxonomy field not working outside pages
+    * Fixed fatal error when moving a page using the old implementation [#2019](https://github.com/getgrav/grav-plugin-admin/issues/2019)
+    * Fixed evaluating default value in `hidden` field (thanks @NicoHood)
+
+# v1.10.0-rc.20
+## 12/14/2020
+
+1. [](#improved)
+    * Cookies now explicitly set `SameSite` to `Lax` unless otherwise specified [#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998)
+    * Exposed **Cookies** class (`Grav.default.Utils.Cookies`) for developers that need it in Admin.
+1. [](#bugfix)
+    * Fixed Plugins references in Themes details page.
+    * Fixed issue preventing purchase of Themes within Admin and redirecting instead.
+    * Regression: Values inside Fieldset do not display [#1995](https://github.com/getgrav/grav-plugin-admin/issues/1995)
+
+# v1.10.0-rc.19
+## 12/02/2020
+
+1. [](#improved)
+    * Just keeping sync with Grav rc.19
+
+# v1.10.0-rc.18
+## 12/02/2020
+
+1. [](#new)
+    * Retired "Secure Delete" and "Warn on page delete". You are now always warned and asked to confirm a deletion.
+1. [](#improved)
+    * Auto-link a plugin/theme license in details if it starts with `http`
+    * Allow to fallback to `docs:` instead of `readme:`
+    * Forward a `sid` to GPM when downloading a premium package
+    * Better support for array field key/value when either key or value is stored empty [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1972)
+    * Remember the open state of the sidebar [#1973](https://github.com/getgrav/grav-plugin-admin/issues/1973)
+    * Upgraded node dependencies to latest version. Improved speed of JS compilation.
+    * Added modal to confirm updating Grav as well as cool down counter before enabling Update button [#1257](https://github.com/getgrav/grav-plugin-admin/issues/1257)
+    * Better handling of offline/intranet mode when the repository index is missing. Faster admin. [#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916)
+    * Statistics is now Page View Statistics [#1885](https://github.com/getgrav/grav-plugin-admin/issues/1885)
+    * It is now possible to use regex as values for "Hide page types in Admin" and "Hide modular page types in Admin" settings [#1828](https://github.com/getgrav/grav-plugin-admin/issues/1828)
+    * Default to `disabled` state for all cron-jobs
+1. [](#bugfix)
+    * Fixed Safari issue with new ACL picker field [#1955](https://github.com/getgrav/grav-plugin-admin/issues/1955)
+    * Stop propagation of ACL add button in ACL picker [flex-objects#83](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/83)
+    * Fixed missing special groups `authors` and `defaults` for pages
+    * Fixed Page Move action and selection highlight in Parents selector modal [flex-objects#80](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/80)
+    * Fixed folder auto-naming in Add Module [#1937](https://github.com/getgrav/grav-plugin-admin/issues/1937)
+    * Fixed remodal issue triggering close when selecting a dropdown item ending outside of scope [#1682](https://github.com/getgrav/grav-plugin-admin/issues/1682)
+    * Reworked how collapsed lists work so the tooltip is not cut off [#1928](https://github.com/getgrav/grav-plugin-admin/issues/1928)
+    * Fixed KeepAlive issue where too large of a session value would fire the keep alive immediately [#1860](https://github.com/getgrav/grav-plugin-admin/issues/1860)
+    * Fixed stringable objects breaking the inputs
+    * Fixed filepicker, pagemediaselect fields with `multiple: true` and `array: true` [#1580](https://github.com/getgrav/grav-plugin-admin/issues/1580)
+
+# v1.10.0-rc.17
+## 10/07/2020
+
+1. [](#new)
+    * Support premium themes
+1. [](#improved)
+    * Improved some error messages for better readability
+    * Strip tags from browser title
+1. [](#bugfix)
+    * More multi-site routing fixes
+    * Fixed issue that would force a page reload when failing to install/update a plugin or theme.
+    * Fixed proxy/browser caching issues in admin pages
+
+# v1.10.0-rc.16
+## 09/01/2020
+
+1. [](#improved)
+    * Made all the `onAdmin*` CRUD events to pass `object` (and backwards compatible `page`) to make them easier to use
+    * Updated vendor libraries including `SCSSPHP` to v1.2
+1. [](#bugfix)
+    * Fixed issue with File field being used in Theme/Plugins
+    * Fixed bad redirection after successful admin login in subdirectory multisite [#1487](https://github.com/getgrav/grav-plugin-admin/issues/1487)
+
+# v1.10.0-rc.15
+## 07/22/2020
+
+1. [](#bugfix)
+    * Disabled the EXIF library for Dropzone for fixing the orientation as it was getting applied twice [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923)
+    * Forked Dropzone fo fix issue with Resize + Exif orientation [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923)
+    * Fixed URI encode for the preview of images names
+
+# v1.10.0-rc.14
+## 07/09/2020
+
+1. [](#improved)
+    * Completely removed old Google font support for upgrade compatibility
+1. [](#bugfix)
+    * Fixed bad `use` reference to `UserObject`
+
+# v1.10.0-rc.13
+## 07/01/2020
+
+1. [](#improved)
+    * Improved color picker field
+    * Trim login route for safety
+    * Composer update to grab latest vendor libs
+
+# v1.10.0-rc.12
+## 06/08/2020
+
+1. [](#new)
+    * Added ability to set a preferred markdown editor in user profile
+    * Added new `onAdminListContentEditors` event to add a custom editor to the list of available
+1. [](#bugfix)
+    * Fixed issue deleting file from a plugin's configuration
+    * Use `Pages::find()` instead of `Pages::dispatch()` as we do not want to redirect out of admin
+    * Fixed broken `parent` field when using the old pages
+    * Fixed broken `file` field preview when using streams in the path
+
+# v1.10.0-rc.11
+## 05/14/2020
+
+1. [](#new)
+    * Major enhancements to "White Label" functionality including ability to export/import presets
+    * New horizontal scroller for theme presets
+    * Codemirror Fontsize / Preset / Font preference options
+1. [](#improved)
+    * Fixed lots of styling issues related to "White Label" presets
+    * Changed out "One Light" theme for new "Firewatch Light" theme
+    * New scrolling system based on `SimpleBar` + native CSS scrollbar styling
+
+# v1.10.0-rc.10
+## 04/30/2020
+
+1. [](#new)
+    * Addd new `taskConvertUrls` method for use with 3rd party editors
+
+# v1.10.0-rc.9
+## 04/27/2020
+
+1. [](#new)
+    * Added new "White Label" functionality to customize admin colors + logos
+    * Added badge count for children in the Parents field
+1. [](#improved)
+    * Added markdown support to `text` in `section` field
+1. [](#bugfix)
+    * Prevent loading Pages in Parents field if they don't have children
+    * Fixed custom folder in `mediapicker` field not working with streams
+    * Fixed language redirect adding extra language prefix in Flex
+    * Fixed `Invalid input in "Parent"` when saving page in raw mode [#1869](https://github.com/getgrav/grav-plugin-admin/issues/1869)
+
+# v1.10.0-rc.8
+## 03/19/2020
+
+1. [](#new)
+    * Added `has-children` flag in parent field data response
+    * Added `RESET` en lang string
+1. [](#bugfix)
+    * Fixed parent field not working with regular pages
+
+# v1.10.0-rc.7
+## 03/05/2020
+
+1. [](#new)
+    * Enable admin cache by default (for existing sites, check `Plugins > Admin Panel > Enable Admin Caching`)
+1. [](#improved)
+    * Removed old `scss.sh` and `watch.sh` scripts, use `gulp watch-css`
+    * Added keysOnly parameter to `AdminPlugin::pagesTypes()` and `AdminPlugin::pagesModularTypes()` methods
+    * Added ignore parameter to `Admin::types()` and `Admin::modularTypes()` methods
+    * Improved configuration fields for hiding page types in Admin
+1. [](#bugfix)
+    * Fixed minor UI padding in Flex pages [#1825](https://github.com/getgrav/grav-plugin-admin/issues/1825)
+    * Fixed `column` and `section` fields loosing user entered value when form submit fails
+    * Fixed `order` field not working with a new Flex Page
+
+# v1.10.0-rc.6
+## 02/11/2020
+
+1. [](#new)
+    * Pass phpstan level 1 tests
+    * Updated semver library to v1.5
+    * Require flex-objects plugin
+1. [](#improved)
+    * Added some debugging messages (turned off by default)
+
+# v1.10.0-rc.5
+## 02/03/2020
+
+1. [](#new)
+    * No changes, just keeping things in sync with Grav RC version
+
+# v1.10.0-rc.4
+## 02/03/2020
+
+1. [](#new)
+    * Added message to dashboard to install Flex Objects plugin if it is missing
+    * Updated `permissions` field to use new `$grav['permissions']`
+    * DEPRECATED `onAdminRegisterPermissions` event, use `PermissionsRegisterEvent::class` event instead
+    * DEPRECATED `Admin::setPermissions()` and `Admin::addPermissions()`, use `PermissionsRegisterEvent::class` event instead
+    * DEPRECATED `Admin::getPermissions()`, use `$grav['permissions']->getInstances()` instead
+1. [](#improved)
+    * Added `field.show_label` and `field.label` display logic from frontend forms
+1. [](#bugfix)
+    * Fixed user profile when using `Flex Users` only in admin
+    * Fixed saving data with empty field, default value (from config, plugin, theme) was used instead
+    * Fixed JS bug is using empty Grav URI param key
+    * Fixed bug in toggleable field being disabled with empty value (`''` `0`, `false`, `[]`...)
+    * Fixed `admin_route()` twig function to work properly with Grav 1.7.0-rc.4, which fixes `Route` base
+    * Fixed misleading 'Show sensitive data' configuration option wording [#1818](https://github.com/getgrav/grav-plugin-admin/issues/1818)
+
+# v1.10.0-rc.3
+## 01/02/2020
+
+1. [](#new)
+    * Added ability to display **Changelogs** for `Grav`, `Plugins` and `Themes`
+    * Added raw root page support for `Flex Pages`
+
+# v1.10.0-rc.2
+## 12/04/2019
+
+1. [](#new)
+    * Added support for hiding parts of admin by `Deny` permissions (`Flex Users` only)
+    * Optimized `parent` field for Flex Pages
+1. [](#improved)
+    * Improved `permissions` field to add support for displaying calculated permissions
+    * Grav 1.7: Updated deprecated `$page->modular()` method calls to `$page->isModule()`
+    * Output the current process user name in Scheduler instructions
+    * Translations: rename MODULAR to MODULE everywhere
+1. [](#bugfix)
+    * Fixed `permissions` field with nested permissions
+    * Fixed Save Shortcut (CTRL + S / CMD + S) not working with new Flex Pages [#1787](https://github.com/getgrav/grav-plugin-admin/issues/1787)
+
+# v1.10.0-rc.1
+## 11/06/2019
+
+1. [](#new)
+    * Added a new `onAdminLogFiles()` event for 3rd party plugins to register log files for log viewer [#1765](https://github.com/getgrav/grav-plugin-admin/issues/1765)
+1. [](#improved)
+    * Improved delete button UI [#1769](https://github.com/getgrav/grav-plugin-admin/issues/1769)
+    * Ability to configure display of 3rd party dashboard widgets [#1766](https://github.com/getgrav/grav-plugin-admin/issues/1766)
+1. [](#bugfix)
+    * Fixed administrator user creation when `Flex Users` is enabled
+    * Fixed minor button alignment in FF [#1760](https://github.com/getgrav/grav-plugin-admin/issues/1760)
+
+# v1.10.0-beta.10
+## 10/03/2019
+
+1. [](#bugfix)
+    * Regression: Fixed language assignments for the pages without set language
+
+# v1.10.0-beta.9
+## 09/26/2019
+
+1. [](#bugfix)
+    * Make pages field to work with Flex Pages
+
+# v1.10.0-beta.8
+## 09/19/2019
+
+1. [](#new)
+    * Add ability to Sanitize SVGs on file upload
+    * Add ability to Sanitize SVGs in Page media
+1. [](#improved)
+    * YAML linter report now supports multi-language
+    * Better colors/placement of toolbar buttons in page edit view
+1. [](#bugfix)
+    * Fixed missing language for AJAX requests
+    * Fixed redirect with absolute language URL
+    * Fixed issue with user avatar reference not being deleted when image removed
+
+# v1.10.0-beta.7
+## 08/30/2019
+
+1. [](#bugfix)
+    * Fixed regression: Do not require Flex Objects plugin [grav#2653](https://github.com/getgrav/grav/issues/2653)
+
+# v1.10.0-beta.6
+## 08/29/2019
+
+1. [](#improved)
+    * Optimized admin for speed (only load frontend pages on demand)
+    * Updated navigation menu to be fully controlled and overrideable by `onAdminMenu` event
+    * Lots of Flex Page speed improvements
+
+# v1.10.0-beta.5
+## 08/11/2019
+
+1. [](#new)
+    * Added `data()` twig function to create data object from an array
+1. [](#improved)
+    * Better support for `array` field into `list` field
+    * Made RAW blueprints (expert mode) to work properly with Flex Form
+    * Better support for `clockwork` logs
+1. [](#bugfix)
+    * Fixed issue with nested `list` fields both utilizing the custom `key` functionality
+    * Regression: Page Preview not working, bad url [#1715](https://github.com/getgrav/grav-plugin-admin/issues/1715)
+    * Fixed '+New Folder' to work with new parent picker
+    * Fixed missing XSS check field when editing modular page as raw
+    * Fixed minor CSS layout issue [#1717](https://github.com/getgrav/grav-plugin-admin/issues/1717)
+
+# v1.10.0-beta.4
+## 07/01/2019
+
+1. [](#new)
+    * Added `Admin::redirect()` method to allow redirects to be used outside of controllers
+    * Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links
+    * Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call
+1. [](#improved)
+    * Much improved multi-language support for pages
+    * Admin redirects should now work better with multiple languages enabled
+1. [](#bugfix)
+    * Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename
+    * Fixed possibility to override already existing translation by `Save As Language`
+    * Fixed missing default translation if page used plain `.md` file extension without language code
+    * Fixed wrong translation showing up as page fallback language
+    * Integrated Admin 1.9.8 bug fixes
+
+# v1.10.0-beta.3
+## 06/24/2019
+
+1. [](#improved)
+    * Smarter handling of symlinks in parent field
+1. [](#bugfix)
+    * Fixed issue with windows paths in `parent` field [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699)
+
+# v1.10.0-beta.2
+## 06/21/2019
+
+1. [](#improved)
+    * Moved Remodal in-house and added support for stackable modals [#1698](https://github.com/getgrav/grav-plugin-admin/issues/1698), [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699)
+1. [](#bugfix)
+    * Fixed missing check for maximum allowed files in `files` field
+
+# v1.10.0-beta.1
+## 06/14/2019
+
+1. [](#new)
+    * New Parent/Move field using Ajax for better performance
+    * Improvements to cache clearing when admin cache is enabled
+    * Require Grav v1.7
+    * Use PSR-4 for plugin classes
+    * Added support for Twig 2.11 (compatible with Twig 1.40+)
+1. [](#improved)
+    * Various admin performance improvements
+1. [](#bugfix)
+    * Fixed admin caching issues
+
+# v1.9.19
+## 12/14/2020
+
+1. [](#bugfix)
+    * Fixed `pages` field escaping issues, needs Grav update, too [#1990](https://github.com/getgrav/grav-plugin-admin/issues/1990)
+    * Fixed Plugins references in Themes details page.
+    * Fixed issue preventing purchase of Themes within Admin and redirecting instead.
+    * Fixed Page Picker not passing admin token
+
+# v1.9.18
+## 12/02/2020
+
+1. [](#new)
+    * Never allow Admin pages to be rendered in `<frame>`, `<iframe>`, `<embed>` or `<object>` for improved security
+1. [](#improved)
+    * Auto-link a plugin/theme license in details if it starts with `http`
+    * Allow to fallback to `docs:` instead of `readme:`
+    * Backported finder/pages navigation from 1.10 (you will still need 1.10 for the fancy Parent Picker)
+    * Forward a `sid` to GPM when downloading a premium package
+    * Add focus states to login buttons [#1839](https://github.com/getgrav/grav-plugin-admin/pull/1839)
+    * Output raw text in paragraph for fieldset [#1956](https://github.com/getgrav/grav-plugin-admin/pull/1956)
+    * Set scheduled items to be 'disabled' by default
+    * Added scheduler warning about potential dangers of use
+1. [](#bugfix)
+    * Escape page title in `pages` field
+    * Fixed unused task RemoveMedia, it cannot be used directly anymore [GHSA-945r-cjfm-642c](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-945r-cjfm-642c)
+    * Tightened checks when removing a media file [GHSA-945r-cjfm-642c](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-945r-cjfm-642c)
+    * Removed unused parameter in file field [GHSA-945r-cjfm-642c](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-945r-cjfm-642c)
+    * Fixed backup download URL [GHSA-vrvq-2pxg-rw5r](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-vrvq-2pxg-rw5r)
+    * Fixed deleting backup [GHSA-85r3-mf4x-qp8f](https://github.com/getgrav/grav-plugin-admin/security/advisories/GHSA-85r3-mf4x-qp8f)
+
+# v1.9.17
+## 10/07/2020
+
+1. [](#new)
+    * Support premium themes
+    * Back-ported functionality from Admin 1.10 required for upcoming WYSIWYM Nextgen Editor
+1. [](#improved)
+    * Improved some error messages for better readability
+1. [](#bugfix)
+    * Fixed issue that would force a page reload when failing to install/update a plugin or theme
+    * Fixed proxy/browser caching issues in admin pages
+
+# v1.9.16
+## 09/01/2020
+
+1. [](#bugfix)
+    * Fixed a glitch which allows user to delete entire pages directory [#1941](https://github.com/getgrav/grav-plugin-admin/issues/1941)
+    * Fixed the hidden login plugin toggle
+
+# v1.9.15
+## 06/08/2020
+
+1. [](#bugfix)
+    * Support markdown in `fieldset.text` [#2934](https://github.com/getgrav/grav/issues/2934)
+    * Fix data URLs in avatar images [#1889](https://github.com/getgrav/grav/issues/1889)
+    * Fix for deleting files in plugin configurations
+
+# v1.9.14
+## 04/27/2020
+
+1. [](#improved)
+    * Added `slug` and `type` to blueprints
+1. [](#bugfix)
+    * Support markdown in `fieldset.text` [#2934](https://github.com/getgrav/grav/issues/2934)
+
+# v1.9.13
+## 03/05/2020
+
+1. [](#improved)
+    * Updated vendor libs
+1. [](#bugfix)
+    * Fixed toggleable buttons no longer holding false state [form#406](ttps://github.com/getgrav/grav-plugin-form/issues/406)
+
+# v1.9.12
+## 12/04/2019
+
+1. [](#bugfix)
+    * Fixed saving configuration in PHP 7.4
+
+# v1.9.11
+## 11/06/2019
+
+1. [](#improved)
+    * Added new "secure delete" functionality [#1752](https://github.com/getgrav/grav-plugin-admin/issues/1752)
+    * Center text logo [#1751](https://github.com/getgrav/grav-plugin-admin/issues/1751)
+    * Added required span to editor field [#1748](https://github.com/getgrav/grav-plugin-admin/issues/1748)
+    * Warn users if JS is disabled [#1722](https://github.com/getgrav/grav-plugin-admin/issues/1722)
+    * Added target rule to quick links [#1518](https://github.com/getgrav/grav-plugin-admin/issues/1518)
+1. [](#bugfix)
+    * Fixed `Badly encoded JSON data` warning when uploading files [grav#2663](https://github.com/getgrav/grav/issues/2663)
+    * Fixed `accept` for SVG in `file` uploaders [#1732](https://github.com/getgrav/grav-plugin-admin/issues/1732)
+
+# v1.9.10
+## 09/19/2019
+
+1. [](#bugfix)
+    * Fixed `Badly encoded JSON data` warning when uploading files [grav#2663](https://github.com/getgrav/grav/issues/2663)
+
+# v1.9.9
+## 08/21/2019
+
+1. [](#bugfix)
+    * Fixed regression with files in admin not allowing types other than images [#1737](https://github.com/getgrav/grav-plugin-admin/issues/1737)
+    * Fixed preview link for non-images files in **Page Media** [#1727](https://github.com/getgrav/grav-plugin-admin/issues/1727)
+
+# v1.9.8
+## 08/11/2019
+
+1. [](#improved)
+    * Better support for `array` field into `list` field
+    * Attach `_list_index` to fields within list items so that the index/key is available
+1. [](#bugfix)
+    * Fixed 2FA regenerate for Flex Users
+    * Added missing closing </li> in language loops
+    * Fixed issue with nested `list` fields both utilizing the custom `key` functionality
+    * Fixed issue with `array` field nested in `list` that were losing their index order when the list reordered
+    * Fixed file form field failing resolution checks in certain circumstances
+    * Fixed issue with deleting files in config based YAML files
+
+# v1.9.7
+## 06/21/2019
+
+1. [](#bugfix)
+    * Fixed issue with charts in dashboard where label would cut off [#1700](https://github.com/getgrav/grav-plugin-admin/issues/1700)
+    * Resetting a user's password clears the user's site access [grav#2528](https://github.com/getgrav/grav/issues/2528)
+    * Fixed issue with permissions toggle [#1702](https://github.com/getgrav/grav-plugin-admin/issues/1702)
+
+# v1.9.6
+## 06/15/2019
+
+1. [](#bugfix)
+    * Fixed regression issue with `parents_levels` defaulting to `2`
+
+# v1.9.5
+## 06/14/2019
+
+1. [](#improved)
+    * Display error message if GPM class fails to initialize
+    * Better append/prepend logic that was breaking some layouts
+    * Default `backups` to an array if used outside of tools
+    * PSR 7 fixes
+
+# v1.9.4
+## 05/09/2019
+
+1. [](#new)
+    * Added support for `field.copy-to-clipboard` on Text input fields
+1. [](#improved)
+    * Only invalidate cache on creating new/deleting page to speed up the recovery
+    * Updated language strings from https://crowdin.com/project/grav-admin
+    * Use `plugins://` stream rather than `user://plugins` [#1674](https://github.com/getgrav/grav-plugin-admin/issues/1674)
+1. [](#bugfix)
+    * Fixed admin cache to detect moved and deleted pages
+    * Fixed avatar URLs with `?` in them being broken
+    * Fixed issue saving page with language that was not exactly `2` or `5` chars long [#1667](https://github.com/getgrav/grav-plugin-admin/issues/1667)
+    * Fixed admin not detecting any existing users when Flex users are being used
+    * Fixed issue with append/prepend not respecting `size:`
+    * Fixed issue with `unset` on file fields [#1427](https://github.com/getgrav/grav/issues/1427), [#1670](https://github.com/getgrav/grav/issues/1670), [#1982](https://github.com/getgrav/grav/issues/1982)
+
+# v1.9.3
+## 04/22/2019
+
+1. [](#new)
+    * Added a new **YAML Linter** report to the `Tools - Reports` section
+1. [](#improved)
+    * Updated package.json scripts to properly use gulp compiler
+
+# v1.9.2
+## 04/15/2019
+
+1. [](#bugfix)
+    * Fix for homepage admin preview [#2426](https://github.com/getgrav/grav/issues/2426)
+    * Uploaded Avatar removed from user's yaml when editing the user [#1647](https://github.com/getgrav/grav-plugin-admin/issues/1647)
+
+# v1.9.1
+## 04/13/2019
+
+1. [](#bugfix)
+    * Fix for Page saving issues [#1648](https://github.com/getgrav/grav-plugin-admin/issues/1648)
+    * Remove status message when picking folder for move [#1650](https://github.com/getgrav/grav-plugin-admin/issues/1650)
+
+# v1.9.0
+## 04/11/2019
+
+1. [](#new)
+    * New `Scheduler` configuration panel in tools
+    * New `Backups` configuration panel in tools
+    * New `Cache::purge()` option in cache drop-down to clear out old cache only
+    * New `Tools - Reports` section with event `onAdminGenerateReports()` for 3rd party plugin support
+    * Added support for the new `Flex User` object
+    * Allow admin forms to use `Form` classes
+    * Added new `Logs` section to tools to allow quick view of Grav log files
+1. [](#improved)
+    * Improved the UI for the Parent Page Route dropdown when adding a new Page / Folder
+    * Use `$grav['accounts']` instead of `$grav['users']`
+    * Improved image background overlay and tools
+    * Better unauthorized user rendering
+    * Update all Form classes to rely on `PageInterface` instead of `Page` class
+    * Removed `media.upload_limit` references
+    * Improve error when upload exceeds `upload_max_filesize`
+    * Delegate Dropzone for checking maximum file size and avoid uploading if not necessary
+    * Low level unauthorized user handling in `base-root.html.twig`
+    * Refactored "NewsFeeds" and "Notifications" for better performance and to address CORS issues
+    * Flex user profile now uses Flex Form
+    * Moved dashboard `notifications` logic to server-side for increased performance (1 request instead of 3)
+    * Refactored feeds logic for better performance
+    * Better logic for delete action to support Ajax. Fixes Flex lists
+    * Cleanly handle session corruption due to changing Flex object types
+    * Implemented [ForkAwesome](https://forkawesome.github.io/Fork-Awesome/) and removed FontAwesome + LineAwesome
+    * Various default admin theme improvements and cleanup
+    * Make new System Config layout responsive [#1579](https://github.com/getgrav/grav-plugin-admin/issues/1579)
+    * Homepage link should be `https://` [#1564](https://github.com/getgrav/grav-plugin-admin/issues/1564)
+    * Improve lang string to describe XSS security settings [#1566](https://github.com/getgrav/grav-plugin-admin/issues/1566)
+    * Take admin setting for 2FA into account when showing user 2FA badge [#1568](https://github.com/getgrav/grav-plugin-admin/issues/1568)
+    * Moved `ignore` and `key` field into form plugin
+    * Improved usability of `System` configuration blueprint with side-tabs
+    * Cleaned up UI in `Scheduler` tools page
+    * Updated languages
+1. [](#bugfix)
+    * Fixed user edit links if Flex Objects plugin is installed but user isn't Flex User
+    * Fixed deprecated `sameas()` Twig test
+    * Regression: Fixed lost user access when saving user profile without super user permissions [#1639](https://github.com/getgrav/grav-plugin-admin/issues/1639)
+    * Fixed `Page.menu` displaying in edit view rather than `Page.title` [#1642](https://github.com/getgrav/grav-plugin-admin/issues/1642)
+    * Regression from beta.8: Deleting files other than from plugins/themes fail on error
+    * Fixed issue with Safari browser and blueprint fields with `toggleable: true` [#1643](https://github.com/getgrav/grav-plugin-admin/issues/1643)
+    * Incorrect 2FA lang code [#1618](https://github.com/getgrav/grav-plugin-admin/issues/1618)
+    * Fixed potential undefined property in `onPageNotFound` event handling
+    * Proper fix for `vUndefined` when updating plugins/themes
+    * Text in Tab Tools/Direct install disappears [#1613](https://github.com/getgrav/grav-plugin-admin/issues/1613)
+    * Fallback to page `slug` in Pages list if title is empty [grav#2267](https://github.com/getgrav/grav/issues/2267)
+    * Fixes backup button issues with `;` param separator [#1602](https://github.com/getgrav/grav-plugin-admin/issues/1602) [#1502](https://github.com/getgrav/grav-plugin-admin/issues/1502)
+    * Set default state for `show_modular` to `true` [#1599](https://github.com/getgrav/grav-plugin-admin/issues/1599)
+    * Removed `tabs`, `tab`, and `toggle` fields as they are now in Form plugin
+    * Fix issue with new page always showing modular page templates [#1573](https://github.com/getgrav/grav-plugin-admin/issues/1573)
+    * Fixed issue deleting files in plugins/themes/config
+    * Fixed array support in admin languages, e.g. `DAYS_OF_THE_WEEK`
+    * Fixed user login / remember me triggering before admin gets initialized
+    * Fixed a bug when deleting files via AJAX
+    * Fixed error page not to be the frontend version
+    * Added `merge_items` option for `field.selectize` to allow storing custom items [#1461](https://github.com/getgrav/grav-plugin-admin/issues/1461)
+    * Better handling of unset in uploaded files [#1427](https://github.com/getgrav/grav-plugin-admin/issues/1427)
+    * Prefix Backup/Scheduler titles with `Tools`
+    * Regression: Media settings have bad layout [#1529](https://github.com/getgrav/grav-plugin-admin/issues/1529)
+    * Fixed Direct Install Uploader, failing to validate the uploaded files
+    * Regression: Editing interface does not keep settings properly without manual intervention on each edit [#1527](https://github.com/getgrav/grav-plugin-admin/issues/1527)
+    * Removed duplicate language strings
+    * Fixed default `job_at` so it does not fail if missing
+    * Minor JS group `bottom` fix
+
+# v1.8.20
+## 03/20/2019
+
+1. [](#improved)
+    * Added security field to column [#1622](https://github.com/getgrav/grav-plugin-admin/pull/1622)
+
+# v1.8.19
+## 02/13/2019
+
+1. [](#bugfix)
+    * Moved `show_modular` to proper place - Doh! [grav#2362](https://github.com/getgrav/grav/issues/2362)
+
+# v1.8.18
+## 02/12/2019
+
+1. [](#bugfix)
+    * Set default value for `show_modular` [grav#2362](https://github.com/getgrav/grav/issues/2362)
+
+# v1.8.17
+## 02/07/2019
+
+1. [](#improved)
+    * Improved Grav Core installer/updater to run installer script (if available)
+    * Added `unauthorized.html.twig` file that was missing [#1609](https://github.com/getgrav/grav-plugin-admin/pull/1609)
+1. [](#bugfix)
+    * Fixed direct install deleting backups and logs if used with full Grav package instead of with update package
+
+# v1.8.16
+## 01/25/2019
+
+1. [](#improved)
+    * IP pseudonymization for rate limiter [#1589](https://github.com/getgrav/grav-plugin-admin/pull/1589)
+    * Add option to hide modular pages in parent select [#1571](https://github.com/getgrav/grav-plugin-admin/pull/1571)
+    * Added `admin.tools` permission [#1550](https://github.com/getgrav/grav-plugin-admin/pull/1550)
+1. [](#bugfix)
+    * Fixed calendar js module not properly loading for datetime field [#1581](https://github.com/getgrav/grav-plugin-admin/issues/1581)
+    * Fixed deleting file when using file field type [#1558](https://github.com/getgrav/grav-plugin-admin/issues/1558)
+    * Unset state from user if not super or user admin
+
+# v1.8.15
+## 12/14/2018
+
+1. [](#improved)
+    * Fire `onAdminSave()` event during `AdminController::taskSaveAs()` [#1544](https://github.com/getgrav/grav-plugin-admin/issues/1544)
+1. [](#bugfix)
+    * Clean user post to ensure dynamically added form fields are not saved
+
+# v1.8.14
+## 11/12/2018
+
+1. [](#bugfix)
+    * Fixed Grav core update potentially spinning forever because of an error which happens after a successful upgrade
+    * Saving in expert mode can cause `undefined index: header` error [#1537](https://github.com/getgrav/grav-plugin-admin/issues/1537)
+
+# v1.8.13
+## 11/05/2018
+
+1. [](#new)
+    * Added new `|nested()` Twig filter to access array objects with dot notation syntax
+1. [](#bugfix)
+    * Fixed issue with complex lists structure and nested dot-notation [admin#2236](https://github.com/getgrav/grav/issues/2236)
+
+# v1.8.12
+## 10/24/2018
+
+1. [](#improved)
+    * Updated various lang strings
+    * Removed duplicate lang strings
+1. [](#bugfix)
+    * Fix XSS checking when empty content [#1533](https://github.com/getgrav/grav-plugin-admin/issues/1533)
+    * Fix DirectInstall not working [#1535](https://github.com/getgrav/grav-plugin-admin/issues/1535)
+
+# v1.8.11
+## 10/08/2018
+
+1. [](#improved)
+    * Change usage of basename where possible [#1480](https://github.com/getgrav/grav-plugin-admin/pull/1480)
+    * Improved filename validation (requires Grav 1.5.3)
+    * Updated various lang codes
+1. [](#bugfix)
+    * File Uploads: Do not trust mimetype sent by the browser
+    * Fixed file extension detection
+    * Fix for HTML entities in page slug [#1524](https://github.com/getgrav/grav-plugin-admin/issues/1524)
+    * Fix for port in backup download links [#1521](https://github.com/getgrav/grav-plugin-admin/issues/1521)
+
+# v1.8.10
+## 10/01/2018
+
+1. [](#new)
+    * IMPORTANT: Non `admin.super` users are now subject to XSS validation in Page content.  Configurable via Configuration / Security
+    * New XSS content warnings and integration into page save
+    * Added new event `onAdminPage()` which allows plugins to customize `Page` object in `$event['page']`
+1. [](#improved)
+    * Use `Url:post()` to get the `$_POST` variable (allows common security checks/filtering for the POST data)
+    * Requires Grav 1.5.2
+1. [](#bugfix)
+    * Fixed redirect to correct URL after failed login
+    * Fixed issue in `filepicker` where missing images would cause a loop to try to load them
+    * Twig 2 compatibility fixes for macros
+    * Updated `composer.json` to better match Grav 1.5
+    * Remove `package-lock.json` as it was referencing an insecure JS package
+
+# v1.8.9
+## 08/23/2018
+
+1. [](#improved)
+    * Make order field to use context, not data
+    * Switched to new Grav Yaml class to support Native + Fallback YAML libraries
+    * Minor fix for `file` thumbnails display
+    * Requires Grav 1.5.1
+
+# v1.8.8
+## 08/17/2018
+
+1. [](#improved)
+    * Support URI Params and Query attributes in Login redirect
+    * Added support for textarea value type in `array` field
+    * Added some new lang strings for Grav 1.5.0
+1. [](#bugfix)
+    * Support params and querystring in login redirect
+    * Added field name nesting with tab field
+
+# v1.8.7
+## 07/31/2018
+
+1. [](#bugfix)
+    * Fix for deleting 'extra' media files [grav#2100](https://githubcom/getgrav/grav/issues/2100)
+
+# v1.8.6
+## 07/13/2018
+
+1. [](#bugfix)
+    * Force `html` for markdown preview [grav#2066](https://github.com/getgrav/grav/issues/2066)
+    * Add missing `authorizeTask()` checks in controller [#1483](https://github.com/getgrav/grav/issues/1483)
+    * Add support for `force_ssl` to admin URLs [#1479](https://github.com/getgrav/grav-plugin-admin/issues/1479)
+
+# v1.8.5
+## 06/20/2018
+
+1. [](#bugfix)
+    * Fixed broken folder attribute on filepicker [#1465](https://github.com/getgrav/grav-plugin-admin/issues/1465)
+    * Added translation for system.session.initialize
+    * Slight updates on new translation strings
+
+# v1.8.4
+## 06/11/2018
+
+1. [](#improved)
+    * Including EXIF JS library in the modules dependencies to fix orientation when uploading images
+1. [](#bugfix)
+    * Initialize session on setup [#1451](https://github.com/getgrav/grav-plugin-admin/issues/1451)
+    * Force a `null` order when empty in the post request
+    * Fixed some 2FA form styling issues
+
+# v1.8.3
+## 05/31/2018
+
+1. [](#new)
+    * Added support for selectize plugins as options in the selectize field
+1. [](#bugfix)
+    * Fixed deep linking in admin after login [#1456](https://github.com/getgrav/grav-plugin-admin/issues/1456)
+    * Fixed Undefined property: `stdClass::$image` in v1.8.2 [#1454](https://github.com/getgrav/grav-plugin-admin/issues/1454)
+    * Pass media order when calling `task:listmedia`
+
+# v1.8.2
+## 05/24/2018
+
+1. [](#new)
+    * Added custom object support for filepicker field
+    * Don't allow saving of a user with no local account file
+    * Controls for `list` field were not in sync between top and bottom
+1. [](#improved)
+    * More subtle `fieldset` styling
+1. [](#bugfix)
+    * Check if `$object->blueprints()` exists in `onAdminAfterSave`
+    * When creating first user, check `admin.login` not `site.login`
+    * Fix admin login redirects for multisite setups
+    * Fixed issue with filepicker field where images wouldn't properly merge with the current value if in a page header
+    * Fixed media delete for streams
+
+# v1.8.1
+## 05/15/2018
+
+1. [](#improved)
+    * use SHA1 hashing of IP addressed to support GDPR rules [#1436](https://github.com/getgrav/grav-plugin-admin/pull/1436)
+1. [](#bugfix)
+    * Fixed 2FA form showing up even if user has not turned on the feature [#1442](https://github.com/getgrav/grav-plugin-admin/issues/1442)
+    * Fixed previews of images in Pagemedia field not properly URI encoded [#1438](https://github.com/getgrav/grav-plugin-admin/issues/1438)
+
+# v1.8.0
+## 05/11/2018
+
+1. [](#new)
+    * Moved 2FA authentication to login plugin
+    * Admin login now uses login plugin events
+    * Added new decoupled `pagemedia` field that is no longer tied to just pages
+    * Updated plugin dependencies (Grav >= 1.4.4, Form >=2.14.0, Login >=2.7.0, Email >=2.7.0)
+1. [](#improved)
+    * Added support for JavaScript `bottom` block [#1425](https://github.com/getgrav/grav-plugin-admin/pull/1425)
+    * Added better typography styling for blockquote and markdown in `display` field
+    * Vendor updates
+1. [](#bugfix)
+    * Added missing MarkdownExtra strings [#1385](https://github.com/getgrav/grav-plugin-admin/pull/1385)
+    * Updated `blueprints.yaml` with missing `step` attribute [#1415](https://github.com/getgrav/grav-plugin-admin/pull/1415)
+    * Fixed preview target setting [#1430](https://github.com/getgrav/grav-plugin-admin/pull/1430)
+    * Added new modular string [#1433](https://github.com/getgrav/grav-plugin-admin/pull/1433)
+    * Fixed Firefox issue with the Regenerate button for 2FA. Forcing the page to reload
+    * Fixed jumpiness behavior for Regenerate button when on active state.
+    * Prevent the prompt for unsaved state when Regenerating a 2FA code and trying to reload/leave the page.
+
+# v1.7.4
+## 04/02/2018
+
+1. [](#bugfix)
+    * Fixed a bug for page copy caused by last release [#1409](https://github.com/getgrav/grav-plugin-admin/pull/1409)
+    * Fixed collapsible `list` option [#1410](https://github.com/getgrav/grav-plugin-admin/pull/1410)
+    * Fixed a minor typo in a label [#1397](https://github.com/getgrav/grav-plugin-admin/pull/1397)
+
+# v1.7.3
+## 04/01/2018
+
+1. [](#new)
+    * Implemented Resize Media and Resolution ('resizeWidth', 'resizeHeight', 'resizeQuality', 'resolution')
+    * Updated Dropzone to latest
+1. [](#bugfix)
+    * Implemented workaround for required text fields [#1390](https://github.com/getgrav/grav-plugin-admin/issues/1390)
+    * Fixed highlight color in Firefox [getgrav/grav#1949](https://github.com/getgrav/grav/issues/1949)
+    * Fix for bad redirect on saving simplesearch (possibly others)
+
+# v1.7.2
+## 03/21/2018
+
+1. [](#improved)
+    * Table CSS improvements for use in 3rd party plugins
+    * Translatable `add_modals` button labels [#1388](https://github.com/getgrav/grav-plugin-admin/issues/1388)
+    * Check for `SHIFT` key on editor save shortcut [#1383](https://github.com/getgrav/grav-plugin-admin/issues/1383)
+    * Fixed User permissions responsive UI [#1379](https://github.com/getgrav/grav-plugin-admin/issues/1379)
+    * Optimization to stop admin for looking for pages in disabled plugins
+    * Added configuration option to choose if you want to use new 'inline' preview or `new tab'
+1. [](#bugfix)
+    * Fix redirect bug when changing admin route to `admin-*`
+    * Changed Twig `|count` to `|length` filter [#1391](https://github.com/getgrav/grav-plugin-admin/issues/1391)
+    * Fix for page preview when `HTTP_REFERRER` is not set [grav#1930](https://github.com/getgrav/grav/issues/1930)
+
+# v1.7.1
+## 03/11/2018
+
+1. [](#new)
+    * New built-in page preview system
+1. [](#improved)
+    * Added `CTRL+K` / `CMD+K` shortcuts for editor links [#1279](https://github.com/getgrav/grav-plugin-admin/issues/1279)
+1. [](#bugfix)
+    * Automatically redirect to new `admin_route` after changing it [#1371](https://github.com/getgrav/grav-plugin-admin/issues/1371)
+    * Remove bad-shadows on alerts
+    * Fixed notifications titles not html escaped [#1272](https://github.com/getgrav/grav-plugin-admin/issues/1272)
+    * Fixed extra horizontal scrollbar with `Editor` field
+    * Fixed `mediapicker` field in lists [#1369](https://github.com/getgrav/grav-plugin-admin/issues/1369)
+
+# v1.7.0
+## 03/09/2018
+
+1. [](#new)
+    * Added styling and lang for **Route Overrides** in the default page blueprint
+    * Added clear cache permanently to quick-tray [#1353](https://github.com/getgrav/grav-plugin-admin/issues/1353)
+1. [](#improved)
+    * Added option to toggle between `line-awesome` and `font-awesome` icon sets [#1334](https://github.com/getgrav/grav-plugin-admin/issues/1334)
+    * Added preview from page list view [#1250](https://github.com/getgrav/grav-plugin-admin/pull/1250)
+    * Added `Add` plugins button to plugins details page [#1352](https://github.com/getgrav/grav-plugin-admin/pull/1352)
+    * Added support for `default` and `options` fields in taxonomy field [#1364](https://github.com/getgrav/grav-plugin-admin/issues/1364)
+    * Added support to limit parent field levels [#1298](https://github.com/getgrav/grav-plugin-admin/issues/1298)
+1. [](#bugfix)
+    * Fixed issue with custom logo text overlapping the sidebar toggle [#1334](https://github.com/getgrav/grav-plugin-admin/issues/1334)
+    * Fixed issues with minimum PHP versions in resource upgrades
+    * Fixed issue with default lang translation in admin [#1361](https://github.com/getgrav/grav-plugin-admin/issues/1361)
+    * Typos in `Tools` -> `Direct Install` page [#1345](https://github.com/getgrav/grav-plugin-admin/issues/1345)
+    * Fixed bug with frontmatter being killed when in `Expert Mode` [#1354](https://github.com/getgrav/grav-plugin-admin/issues/1354)
+
+# v1.7.0-rc.3
+## 02/15/2018
+
+1. [](#improved)
+    * Tab optimization with fixes for 'onpage' tabs
+    * Stopped Chrome from auto-completing admin user profile form [grav#1847](https://github.com/getgrav/grav/issues/1847)
+    * Added a fixed `ga-theme-17x` body class to help styling compatibility
+    * Outputs an iterable field as a string if `yaml: true` or `validate: type: yaml` set in blueprint
+1. [](#bugfix)
+    * Rolled back JS to known working versions [#1323](https://github.com/getgrav/grav-plugin-admin/issues/1323)
+    * Fixed missing translation in order field [#1324](https://github.com/getgrav/grav-plugin-admin/issues/1324)
+    * Fixed UI issue with last drop-down in button group [1325](https://github.com/getgrav/grav-plugin-admin/issues/1325)
+    * Fixed fieldset field outdated rendering [#1313](https://github.com/getgrav/grav-plugin-admin/issues/1313)
+
+# v1.7.0-rc.2
+## 01/24/2018
+
+1. [](#new)
+    * Moved to LineAwesome icons rather than FontAwesome (still compatible w/FA 4.7.0)
+1. [](#improved)
+    * Simplified open/close nav button
+    * Tidied Tools panel and added translations
+    * Tooltip and new icon for site preview
+    * Updated JS library dependencies
+    * Changed CodeMirror editor to use sans-serif font for readability
+1. [](#bugfix)
+    * Fixed z-index issue in fullscreen mode [#1317](https://github.com/getgrav/grav-plugin-admin/issues/1317)
+
+# v1.7.0-rc.1
+## 01/22/2018
+
+1. [](#new)
+    * Added support for markdown in all form fields for `label`, `help`, and `description` when `markdown: true` is set on field
+    * Changed "made by" to Trilby Media from RocketTheme
+1. [](#improved)
+    * Lightened tabs in new theme
+    * Sort languages by key [#1303](https://github.com/getgrav/grav-plugin-admin/issues/1303)
+    * Add limit to Parent Levels [#1298](https://github.com/getgrav/grav-plugin-admin/pull/1298)
+1. [](#bugfix)
+    * Fixed alignment issue with language drop-down
+    * Fixed a z-index issue with fullscreen editor [#1302](https://github.com/getgrav/grav-plugin-admin/issues/1302)
+    * Fixed missing background on register [#1307](https://github.com/getgrav/grav-plugin-admin/issues/1307)
+    * Fixed some style issues with field descriptions
+    * Fixed an issue with `File` field losing download size setting
+    * Fixed distorted thumbnails in `File` field by using `object-fit: cover`
+
+# v1.7.0-beta.1
+## 12/29/2017
+
+1. [](#new)
+    * New lighter-and-tighter admin theme developed
+1. [](#improved)
+    * Added simple value support for list field type
+    * Added checks to automatically hide collapse buttons when there's only single value in list type
+
+# v1.6.7
+## 12/05/2017
+
+1. [](#new)
+    * Logout of admin goes straight to login form with a message (that then fades out)
+    * Added `sl`, `id`, `he`, `eu`, `et` languages
+1. [](#improved)
+    * Added code to use new `GPM::loadRemoteGrav` if it exists in Gav [grav#1746](https://github.com/getgrav/grav/pull/1746)
+    * Add vertical style for order field [#1253](https://github.com/getgrav/grav-plugin-admin/pull/1253)
+    * Added classes to pagemedia field [#1274](https://github.com/getgrav/grav-plugin-admin/issues/1274)
+    * Fixed selectize field not properly updating value when `option` is provided [#1236](https://github.com/getgrav/grav-plugin-admin/pull/1236)
+    * Tab layout tweaks
+    * Updated all language files with latest from [Crowdin](https://crowdin.com/project/grav-admin)
+1. [](#bugfix)
+    * Manual image metadata can now display in pagemedia when auto-generation is disabled [#1275](https://github.com/getgrav/grav-plugin-admin/issues/1275)
+    * Removed broken `home.hide_in_urls` code in `AdminBaseController::save()` that was throwing move errors
+    * Security fix to ensure file uploads are not manipulated mid-post - thnx @FLH!
+
+# v1.6.6
+## 10/27/2017
+
+1. [](#new)
+    * Fixed issue where sortable media in expert mode would reset frontmatter [#1252](https://github.com/getgrav/grav-plugin-admin/issues/1252)
+
+# v1.6.5
+## 10/26/2017
+
+1. [](#new)
+    * Added ability to **order** page media (requires latest Grav update)
+
+# v1.6.4
+## 10/11/2017
+
+1. [](#improved)
+    * Use system PHP size for upload limit rather than `system.media.upload_limit` or `file.filesize` plugin options
+1. [](#bugfix)
+    * Fixed Dropzone timeout to address slow internet connections [#1239](https://github.com/getgrav/grav-plugin-admin/pull/1239)
+
+# v1.6.3
+## 10/02/2017
+
+1. [](#bugfix)
+    * Fixed chart labels not parsing HTML [#1234](https://github.com/getgrav/grav-plugin-admin/issues/1234)
+
+# v1.6.2
+## 09/29/2017
+
+1. [](#improved)
+    * Removed extraneous files in vendor folder for smaller download package
+
+# v1.6.1
+## 09/29/2017
+
+1. [](#improved)
+    * Added support for Latin Extended fonts [#1211](https://github.com/getgrav/grav-plugin-admin/pull/1221)
+    * Added collapsible attribute to lists [#1231](https://github.com/getgrav/grav-plugin-admin/pull/1231)
+1. [](#bugfix)
+    * Fix editor not clickable in list field [#1224](https://github.com/getgrav/grav-plugin-admin/pull/1124)
+    * Updated Google Font URLs to always connect over HTTPS. [#1106](https://github.com/getgrav/grav-plugin-admin/pull/1106)
+    * Fixed fieldset field not allowing to properly save when contained within a list [#1225](https://github.com/getgrav/grav-plugin-admin/issues/1225)
+    * Fixed Video markdown syntax when drag & dropping in the content editor [#1160](https://github.com/getgrav/grav-plugin-admin/issues/1160)
+    * Fixed headers drop-down in editor to properly align
+    * Fixed fields not working in Microsoft Edge with Selectize.js [#1222](https://github.com/getgrav/grav-plugin-admin/pull/1222)
+    * Replaced a left-over "is empty" check [#1232](https://github.com/getgrav/grav-plugin-admin/pull/1232)
+    * Fixed headers drop-down in editor to align properly
+
+
+# v1.6.0
+## 09/07/2017
+
+1. [](#new)
+    * **Added 2-Factor Authentication support to the admin!**
+    * **Added rate-limiting for "failed login attempts" and "forgot password"**
+1. [](#improved)
+    * Revamped the toggle switch CSS so it's more flexible and works better [#1198](https://github.com/getgrav/grav-plugin-admin/issues/1198)
+    * Improved toggle/button alignment on Page edit view
+1. [](#bugfix)
+    * Fixed an issue where icon-picker style was hiding field elements [#1199](https://github.com/getgrav/grav-plugin-admin/issues/1199)
+    * Fixed https -> http redirect issue [#1195](https://github.com/getgrav/grav-plugin-admin/issues/1195)
+    * Also check `/.` for home route [#1191](https://github.com/getgrav/grav-plugin-admin/issues/1191)
+    * Fixed administration being broken in multi-site environments with plugin overrides
+    * Fixed lang-switcher broken in MS Edge browser [#1213](https://github.com/getgrav/grav-plugin-admin/pull/1213)
+    * Added custom `form_id` attribute for modal forms [#1216](https://github.com/getgrav/grav-plugin-admin/issues/1216)
+    * Fixed partially cropped line in Markdown editor for MS Edge/Firefox [#1219](https://github.com/getgrav/grav-plugin-admin/pull/1219)
+    * Downgraded Babel libraries to v6.x for compatibility with webpack [#1218](https://github.com/getgrav/grav-plugin-admin/pull/1218)
+
+# v1.5.2
+## 08/16/2017
+
+1. [](#new)
+    * Added a new icon quick-tray in side navigation that plugins can utilize
+    * Added ability to set and retrieve temporary admin messages
+1. [](#improved)
+    * Allow different field to be used as page label in list of pages [#1122](https://github.com/getgrav/grav-plugin-admin/pull/1122)
+    * Updated `en` language for `cache-control` + `clear_images_by_default` system settings
+    * Allow sorting of page based on custom ordering [#1182](https://github.com/getgrav/grav-plugin-admin/pull/1182)
+    * Search for pages by slug and folder name [#1183](https://github.com/getgrav/grav-plugin-admin/pull/1183)
+    * Allow all page data to be used during `onAdminCreatePageFrontmatter()` event [#1175](https://github.com/getgrav/grav-plugin-admin/pull/1175)
+    * Remove single quotes when slugifying title [#1178](https://github.com/getgrav/grav-plugin-admin/pull/1178)
+1. [](#bugfix)
+    * Ignore missing Twig files [#1169](https://github.com/getgrav/grav-plugin-admin/issues/1169)
+    * If from is already defined, don't override it [#1129](https://github.com/getgrav/grav-plugin-admin/issues/1129)
+    * Fixed SelectUnique field not working with files with spaces
+
+# v1.5.1
+## 07/19/2017
+
+1. [](#bugfix)
+    * Fixes issue when saving pages without a `folder` element [#1163](https://github.com/getgrav/grav-plugin-admin/issues/1163)
+    * Fixed mediapicker field inside lists not properly updating the value on the target input [#1157](https://github.com/getgrav/grav-plugin-admin/issues/1157)
+
+# v1.5.0
+## 07/16/2017
+
+1. [](#new)
+    * Implemented Offline mode. Notifies in the admin when disconnected.
+1. [](#bugfix)
+    * Fixed fetch issue throwing error when request not completed and while unloading the page [#1301](https://github.com/getgrav/grav-plugin-admin/issues/1301)
+    * Fixed ordering when > 100 pages [grav#1564](https://github.com/getgrav/grav/pull/1564)
+    * Fixed Lists issue when reindexing, causing Radio fields to potentially lose their `checked` status ([#1154](https://github.com/getgrav/grav-plugin-admin/issues/1154) | related: [1d55ffc](https://github.com/getgrav/grav-plugin-admin/commit/1d55ffc616125047f245efe9f2180ef2c16b4949))
+
+# v1.5.0-rc.4
+## 07/05/2017
+
+1. [](#new)
+    * New `multilevel` field, useful for defining collections definitions, metadata and other complex YAML data [#1135](https://github.com/getgrav/grav-plugin-admin/pull/1135) - (EXPERIMENTAL)
+    * Fix plugins hooked nav authorize not working with array of permissions [#1148](https://github.com/getgrav/grav-plugin-admin/pull/1148)
+1. [](#improved)
+    * Add badge to plugins hooked into nav [#1147](https://github.com/getgrav/grav-plugin-admin/pull/1147)
+    * Added `field.outerclasses` to default form field [#1124](https://github.com/getgrav/grav-plugin-admin/pull/1124)
+    * Reverted back to textarea/YAML for `media.yaml` image options
+    * Fixed color of textarea fields in admin
+1. [](#bugfix)
+    * Fix for bad referenced to `shouldLoadAdditionalFilesInBackground()` [#1145](https://github.com/getgrav/grav-plugin-admin/pull/1145)
+    * Expose Page Media instance to Grav Admin JS API
+    * Fixed mediapicker issue where newly added list items would not work
+    * Fixed issue with min/max setting of list collections. Removing a list item would not refresh properly the count
+    * If folder is empty/not sent, fallback to page slug [#1146](https://github.com/getgrav/grav-plugin-admin/issues/1146)
+    * Escape the URI basename before using it in Twig
+    * Ignore missing Twig file in the Tools page
+
+# v1.5.0-rc.3
+## 06/22/2017
+
+1. [](#new)
+    * New `Admin::getPageMedia()` static method that can be used in blueprints
+    * Added a new `mediapicker` form field which allows to select a media from any page [#1125](https://github.com/getgrav/grav-plugin-admin/pull/1125)
+    * Added info metadata button for images to view EXIF and other useful details about an image
+1. [](#improved)
+    * Pass original image filename via the `AdminController::taskListedia()` task
+    * Various form styling improvements
+    * Provided an option to control how parent select field displays
+1. [](#bugfix)
+    * Fix referencing DI element when not initialized [#1141](https://github.com/getgrav/grav-plugin-admin/pull/1141)
+
+# v1.5.0-rc.2
+## 05/22/2017
+
+1. [](#improved)
+    * Remove save button and save location notification on Config Info tab [#1116](https://github.com/getgrav/grav-plugin-admin/pull/1116)
+    * Allow taxonomy field to just list one or more specific taxonomies if the `taxonomies` field is filled in the blueprint
+    * `File` field now renders thumbnail previews of the selected value on load
+    * Use new unified `Utils::getPagePathFromToken()` method rather
+1. [](#bugfix)
+    * Fix for undefined `include_metadata` error
+
+
+# v1.5.0-rc.1
+## 05/16/2017
+
+1. [](#new)
+    * Add support for a single array field in forms
+    * Added Prev/Next support on page editing view [#1112](https://github.com/getgrav/grav-plugin-admin/pull/1112)
+1. [](#improved)
+    * Improved full-screen editor for better browser compatibility [#1093](https://github.com/getgrav/grav-plugin-admin/pull/1093)
+    * Added ability to choose how you want the preview button to open [#1096](https://github.com/getgrav/grav-plugin-admin/pull/1096)
+    * `base.html.twig` now extends a `base-root.html.twig` file
+    * Add month+date indication to the stats graph to avoid confusion when there are days without visits
+    * Added `min` and `max` options for `list` form field [#1113](https://github.com/getgrav/grav-plugin-admin/pull/1113)
+    * Remove page metadata file on deletion of media
+    * Improved layout on pages list for pages with long titles [#1102](https://github.com/getgrav/grav-plugin-admin/pull/1102)
+    * Added option to make custom "Add page" dropdown entries [#1104](https://github.com/getgrav/grav-plugin-admin/pull/1104)
+1. [](#bugfix)
+    * Fixed issue with tab widths on Pages overlapping non-english toggle switch [#1089](https://github.com/getgrav/grav-plugin-admin/issues/1089)
+    * Added `vendor` to ignores for direct install of Grav
+    * Translated `field.default` for `editor` form field
+    * Fixed an quote error in `en.yaml`
+    * Resolved z-index issues with mobile nav and pages form elements
+    * Fixed issue with file picker where the selected file preview would not show
+    * Refresh page media on media upload
+    * Default to config file slug if translation is missing, otherwise use translation also in the tab title, not just in the page heading [#1039](https://github.com/getgrav/grav-plugin-admin/issues/1039)
+    * Fix language toggle button in admin top bar visible also in fullscreen mode [#1110](https://github.com/getgrav/grav-plugin-admin/issues/1110)
+    * Fix for editor padding [#1111](https://github.com/getgrav/grav-plugin-admin/issues/1111)
+    * Fix tabs inside blueprint overlapping above content [#1115](https://github.com/getgrav/grav-plugin-admin/pull/1115)
+
+# v1.4.2
+## 04/24/2017
+
+1. [](#new)
+    * Added a new `Content Padding` option to tighten up UI padding space (default `true`)
+1. [](#bugfix)
+    * Added back `Admin::initTheme()` relying on Grav fix [#1069](https://github.com/getgrav/grav-plugin-admin/pull/1069) as it conflicts ith Gantry5
+    * Fix for missing scrollbar when in full-size editor for Firefox [#1077](https://github.com/getgrav/grav-plugin-admin/issues/1077)
+    * Fix for overlay of Add-Page button in full-size editor [#1077](https://github.com/getgrav/grav-plugin-admin/issues/1077)
+    * Better fix for session-based parent overriding root page parents [#1078](https://github.com/getgrav/grav-plugin-admin/issues/1078)
+    * Allow support for `Pages::getList()` with `show_modular` option [#1080](https://github.com/getgrav/grav-plugin-admin/issues/1080)
+    * Added `[tmp,user]` ignores for direct install of Grav [grav#1447](https://github.com/getgrav/grav/issues/1447)
+
+# v1.4.1
+## 04/19/2017
+
+1. [](#bugfix)
+    * Reverted [#1069](https://github.com/getgrav/grav-plugin-admin/pull/1069) as it conflicts ith Gantry5
+
+# v1.4.0
+## 04/19/2017
+
+1. [](#new)
+    * Added ability to add new pages/folders while editing existing page
+1. [](#improved)
+    * Initialize theme in Admin Plugin [#1069](https://github.com/getgrav/grav-plugin-admin/pull/1069)
+    * Use new system configuration entries for username and password format
+    * Reworked Page parent field to use `Pages::getList()` rather than logic in Twig field itself
+    * More robust styling of admin themes page [#1067](https://github.com/getgrav/grav-plugin-admin/pull/1067)
+    * Fix fullscreen editor height [#1065](https://github.com/getgrav/grav-plugin-admin/pull/1065)
+    * Fix small UI issue in the editor with `codemirror.lineNumbers` && `codemirror.styleActiveLine` enabled
+    * Fix UI performance issue in the dashboard [#1064](https://github.com/getgrav/grav-plugin-admin/issues/1064)
+1. [](#bugfix)
+    * Fixed issue with parent not working with custom slug [#1068](https://github.com/getgrav/grav-plugin-admin/issues/1068)
+    * Fixed issue with new page modal not remembering last choice [#1072](https://github.com/getgrav/grav-plugin-admin/issues/1072)
+
+# v1.3.3
+## 04/12/2017
+
+1. [](#bugfix)
+    * Fix for regression introduced in the automatic page template switch when changing page parent [#1059](https://github.com/getgrav/grav-plugin-admin/issues/1059) [grav#1403](https://github.com/getgrav/grav/issues/1403) [#1062](https://github.com/getgrav/grav-plugin-admin/issues/1062)
+    * Fix issue with editor field in lists [#1037](https://github.com/getgrav/grav-plugin-admin/issues/1037)
+
+# v1.3.2
+## 04/10/2017
+
+1. [](#improved)
+    * Added new 'parents' field and switched Page blueprints to use this
+1. [](#bugfix)
+    * Fix for regression in h3 style in the Spacer field [#267](https://github.com/getgrav/grav-plugin-admin/issues/267)
+    * Fix missing preview in page media for SVG images [#1051](https://github.com/getgrav/grav-plugin-admin/issues/1051)
+    * Fix missing check when reordering [#1053](https://github.com/getgrav/grav-plugin-admin/issues/1053)
+    * Fix for editors not getting refreshed when changing tab [#1052](https://github.com/getgrav/grav-plugin-admin/issues/1052)
+    * Fix for mobile tabs in page editing [#1057](https://github.com/getgrav/grav-plugin-admin/issues/1057)
+
+# v1.3.1
+## 03/31/2017
+
+1. [](#bugfix)
+    * Fix for `Undefined index: file_path` error with Direct Install [#1043](https://github.com/getgrav/grav-plugin-admin/issues/1043)
+
+# v1.3.0
+## 03/31/2017
+
+1. [](#new)
+    * User uploadable avatar (still falls back to Gravatar if not provided)
+1. [](#improved)
+    * Improved tabs CSS to handle long titles [#1036](https://github.com/getgrav/grav-plugin-admin/issues/1036)
+    * Fixed `step` in range field [Form#136](https://github.com/getgrav/grav-plugin-form/issues/136)
+1. [](#bugfix)
+    * Fixed issue with exception thrown when `copying` and `moving` a page [#1042](https://github.com/getgrav/grav-plugin-admin/issues/1042)
+    * Automatically calculate the *next* numeric folder prefix [Core#1386](https://github.com/getgrav/grav/issues/1386)
+
+# v1.3.0-rc.3
+## 03/22/2017
+
+1. [](#new)
+    * All new `Page Ordering` implementation.  Completely revamped and will only reorder with folder-prefix enabled.  You can now reorder all siblings at the same time.
+    * Added a new `Advanced - Override` to allow option to display pages by folder name (default) or Collection definition
+    * Improved `range` form field with touch and counter support [#1016](https://github.com/getgrav/grav-plugin-admin/pull/1016)
+1. [](#bugfix)
+    * Cleanup package files via GPM install to make them more windows-friendly [#1361](https://github.com/getgrav/grav/pull/1361)
+
+# v1.3.0-rc.2
+## 03/17/2017
+
+1. [](#improved)
+    * Do not attempt to fetch any notification if settings are disabled [#942](https://github.com/getgrav/grav-plugin-admin/issues/942)
+
+# v1.3.0-rc.1
+## 03/13/2017
+
+1. [](#new)
+    * New flex-based/js Tabs system for better flexibility and improved UX.
+    * Added new **toolbox** with `Direct-Install` option via ZIP or URL.
+    * Added an option to reinstall a plugin/theme already installed [#984](https://github.com/getgrav/grav-plugin-admin/issues/984)
+    * Added a new **range field** [#995](https://github.com/getgrav/grav-plugin-admin/issues/995)
+    * When creating a new page, automatically select the Page Template based on Parent Page Child Type [#1008](https://github.com/getgrav/grav-plugin-admin/issues/1008)
+1. [](#improved)
+    * Page Media field now is available when folder is created, not just markdown file [#1000](https://github.com/getgrav/grav-plugin-admin/issues/1000)
+    * Separated user details and avatar in separate twig to allow more granular overriding in plugins [#989](https://github.com/getgrav/grav-plugin-admin/issues/989)
+    * Nicer layout of themes list on wider screen
+    * Editor full-screen option displays title/save options [#948](https://github.com/getgrav/grav-plugin-admin/issues/948)
+    * Use native OS highlight colors for the editor [#977](https://github.com/getgrav/grav-plugin-admin/issues/977)
+    * Force admin pages to set `Page::expires(0)` so it's not cached [#1009](https://github.com/getgrav/grav-plugin-admin/issues/1009)
+    * Added support for up to 15 tabs (was 10) [#954](https://github.com/getgrav/grav-plugin-admin/issues/954)
+    * Only reorder pages in the admin if collection uses `@self` and `order.by`
+    * Improved configuration tab sizes when you have lots of tabs
+    * Modified default media select size from 150px x 100px to 200px x 150px
+1. [](#bugfix)
+    * Fixed rendering issue with Chrome and sortables collections [#1002](https://github.com/getgrav/grav-plugin-admin/issues/1002)
+    * Fixed issue with removal of file that has been just uploaded and stored in the session
+
+
+# v1.2.14
+## 02/17/2017
+
+1. [](#bugfix)
+    * Fixed bad bug with `GPM::install()` from a change in Admin v1.2.13
+
+# v1.2.13
+## 02/17/2017
+
+1. [](#bugfix)
+    * Fix issue with validating page when switching language [#963](https://github.com/getgrav/grav-plugin-admin/issues/963)
+    * Fix issue with quotes in Admin strings used in JS [#965](https://github.com/getgrav/grav-plugin-admin/issues/965)
+    * Refactored `AdminController::taskGetUpdates` to use standard task/response [#980](https://github.com/getgrav/grav-plugin-admin/issues/980)
+    * Sync Admin pages blueprints with core [core#212d35221a9bbcc242508ba49a551b3f6e62af8e](https://github.com/getgrav/grav/commit/212d35221a9bbcc242508ba49a551b3f6e62af8e)
+
+# v1.2.12
+## 02/12/2017
+
+1. [](#bugfix)
+    * Rebuilt the JS bundle to address various JS-related issues that cropped up in `v1.2.11`
+    * Fixed Firefox Network Error issue when updating multiple plugins/themes at concurrently [#1301](https://github.com/getgrav/grav/issues/1301)
+
+# v1.2.11
+## 02/10/2017
+
+1. [](#new)
+    * Added lang strings for `CLI_COMPATIBILITY` which is new in Grav v1.1.16
+1. [](#improved)
+    * Allow plugin to set custom 'authorize' and 'location' in `onAdminMenu()` event
+    * Updated all language files with latest from [Crowdin](https://crowdin.com/project/grav-admin)
+1. [](#bugfix)
+    * Fixed issue `admin.super` or `admin.users` users changing the account when saving another user [#713](https://github.com/getgrav/grav-plugin-admin/issues/713)
+    * Fix issue where non `admin.super`/`admin.users` users could see other users profiles [#713](https://github.com/getgrav/grav-plugin-admin/issues/713)
+    * Fix removing responsive image from page media [#111](https://github.com/getgrav/grav-plugin-admin/issues/111) [#952](https://github.com/getgrav/grav-plugin-admin/issues/952)
+    * Use @2x & @3x fallback images in the filepicker. [#952](https://github.com/getgrav/grav-plugin-admin/issues/952)
+
+# v1.2.10
+## 1/30/2017
+
+1. [](#improved)
+    * It is now possible to manually specify a format for the `datetime` field [#1261](https://github.com/getgrav/grav/issues/1261)
+    * Allow to see plugins and themes list without internet connection. Also add a more helpful message in the "add" view [grav#1008](https://github.com/getgrav/grav/issues/1008)
+1. [](#bugfix)
+    * Fixed issue with downloaded package when installing a testing release
+    * Allow non admin.super users to change their account information. Allow `admin.super` and `admin.users` to change other users information. [#943](https://github.com/getgrav/grav/issues/943)
+    * Handle removing a media file also if it's not a json request. Was not working after https://github.com/getgrav/grav-plugin-admin/commit/6b343365996ce838759d80fa3917d4d994f1aeb4
+
+# v1.2.9
+## 01/18/2017
+
+1. [](#improved)
+    * Added lang strings for `ALLOW_WEBSERVER_GZIP` in System configuration
+
+# v1.2.8
+## 01/17/2017
+
+1. [](#improved)
+    * Allow the ability to clear the cache if `admin.maintenance`, as stated in the docs [#908](https://github.com/getgrav/grav-plugin-admin/issues/908)
+    * Added lang strings for `DEFAULT_LANG` in Site configuration
+    * Added lang strings for `NEVER_CACHE_TWIG` in System and Page configuration
+1. [](#bugfix)
+    * Fixed saving the configuration if not `admin.super`
+    * Show the clear cache buttons if the user has `admin.cache` permissions [#908](https://github.com/getgrav/grav-plugin-admin/issues/908#issuecomment-270748616)
+    * Fix colorpicker validation when transparency is set to 1.00 [#921](https://github.com/getgrav/grav-plugin-admin/issues/921)
+    * Fix html markup in section twig [#922](https://github.com/getgrav/grav-plugin-admin/pull/922)
+    * Fix bug in deleting a file uploaded with the `file` field [#920](github.com/getgrav/grav-plugin-admin/issues/920)
+    * Fix for plugin throwing event-based errors when plugin is removed and no longer available to process said event
+
+# v1.2.7
+## 12/22/2016
+
+1. [](#improved)
+    * Fixed an issue with non `.html` extensions not setting application type properly when fallback template not found.
+1. [](#bugfix)
+    * Fix plugins and themes json calls after the introduction of [HTML fallback for templates not found](https://github.com/getgrav/grav/commit/364209a27da0f5dfba5fde9c4b07b6d5844cda47)
+
+
+# v1.2.6
+## 12/21/2016
+
+1. [](#improved)
+    * Added a delay before reloading the page when a plugin or theme get installed
+    * Fix prompting to remove Grav itself when removing a package that requires a specific Grav version
+    * Remove cli-server exception since we now have compatibility with a custom router in Grav [#1219](https://github.com/getgrav/grav/pull/1219)
+1. [](#bugfix)
+    * Fix issue with array field and `value_only: true`
+
+# v1.2.5
+## 12/13/2016
+
+1. [](#new)
+    * RC released as stable
+1. [](#bugfix)
+    * YAML syntax fixes
+
+# v1.2.5-rc.4
+## 12/07/2016
+
+1. [](#new)
+    * Added a new `permissions` form field, used in the user profile to simplify editing permissions
+    * Added several new `onAdminAfter...()` events to allow for more 3rd party plugin interaction
+1. [](#bugfix)
+    * Updated admin-user-details to allow longer user names in the sidebar [#879](https://github.com/getgrav/grav-plugin-admin/issues/879)
+    * Redirect to a 404 page when accessing nonexistent plugins and themes [#880](https://github.com/getgrav/grav-plugin-admin/issues/880)
+
+# v1.2.5-rc.3
+## 11/26/2016
+
+1. [](#bugfix)
+    * Update class namespace for Admin class [#874](https://github.com/getgrav/grav-plugin-admin/issues/874)
+    * Fix updating/installing packages from admin
+
+# v1.2.5-rc.2
+## 11/19/2016
+
+1. [](#bugfix)
+    * Make default value work for filepicker [#859](https://github.com/getgrav/grav-plugin-admin/issues/859)
+
+# v1.2.5-rc.1
+## 11/09/2016
+
+1. [](#new)
+    * Updated to FontAwesome 4.7.0 with [Grav icon](http://fontawesome.io/icon/grav/)
+1. [](#improved)
+    * Always delete image alternatives in AdminController#taskDelmedia [#814](https://github.com/getgrav/grav-plugin-admin/issues/814)
+    * Use Media class to retrieve files in AdminController#taskGetFilesInFolder [#842](https://github.com/getgrav/grav-plugin-admin/issues/842)
+    * Increased specificity for Colorpicker field to prevent 3rd party conflicts
+1. [](#bugfix)
+    * Editor link button doesn't prefix links with `http://` anymore [#813](https://github.com/getgrav/grav-plugin-admin/issues/813)
+    * Dashboard Charts now always refresh no matter what [#753](https://github.com/getgrav/grav-plugin-admin/issues/753)
+    * Use rawRoute for parent too when saving [#843](https://github.com/getgrav/grav-plugin-admin/issues/843)
+    * Avoid different output when users exist or not in password recovery [#849](https://github.com/getgrav/grav/issues/849)
+    * Fix login to admin with permission inherited from group [#857](https://github.com/getgrav/grav-plugin-admin/issues/857)
+
+
+# v1.2.4
+## 10/22/2016
+
+1. [](#bugfix)
+    * Fix for accented media files [#833](https://github.com/getgrav/grav-plugin-admin/issues/833)
+    * Fix for `CTRL + s` not saving in editor [#832](https://github.com/getgrav/grav-plugin-admin/pull/832)
+    * Fix for missing REDIS translations in admin [#1123](https://github.com/getgrav/grav/issues/1123)
+
+# v1.2.3
+## 10/19/2016
+
+1. [](#new)
+    * Added new `onAdminCreatePageFrontmatter()` event to support plugins such as `auto-date` by allowing frontmatter to be modified by plugins.
+    * Added a new independent `cache_enabled` option for admin plugin (default is `false`). Should fix various sync issues.
+    * Add an `onAdminData` event to allow plugins to add additional blueprints data
+1. [](#improved)
+    * Handle errors when a resource fails to install
+    * Page media and File field images thumbnail are now properly proportionate and 150x100
+    * Added the Codeception testing suite with an initial test
+1. [](#bugfix)
+    * Fix [#1034](https://github.com/getgrav/grav/issues/1034) redirect of page creation procedure when system.home.hide_in_urls is enabled
+    * Media (Page): Do not extend parent metehod for sending files since Safari and IE API for FormData don’t implement `delete` ([#772](https://github.com/getgrav/grav-plugin-admin/issues/772))
+    * Clean up POST keys containing square brackets, allows for regex ranges in routes ([#776](https://github.com/getgrav/grav-plugin-admin/issues/776))
+    * Fix [#773](https://github.com/getgrav/grav-plugin-admin/issues/773) allow filepicker work inside lists, respond to mutation event
+    * Better error handling for Feed when unable to connect
+    * Fixed UI for Pagemedia note when files cannot yet be uploaded ([#798](https://github.com/getgrav/grav-plugin-admin/issues/798))
+    * Fixed Submit buttons getting disabled in case of form invalidity disallowing to submit again ([#802](https://github.com/getgrav/grav-plugin-admin/issues/802))
+    * Fixed issue when reading the file size setting if set to `0` (in Pagemedia and File fields)
+    * Fixed issue with `file` field in collections that caused unexpected duplication of items ([#775](https://github.com/getgrav/grav-plugin-admin/issues/775))
+    * Dramatically improved `filepicker` performance. Data is only ever loaded when the drop-down is on focus, as it was supposed to be. Image preview of a selected item won't be rendered unless the field gains focus to avoid wasting resources. ([#788](https://github.com/getgrav/grav-plugin-admin/issues/788))
+    * Allow `filepicker` field to peak at the pending uploaded files and optimistically select them ([#792](https://github.com/getgrav/grav-plugin-admin/issues/792))
+    * Fix [#821](https://github.com/getgrav/grav-plugin-admin/issues/821) issue in saving a page to a new language when the filename does not contain the filename yet.
+
+# v1.2.2
+## 09/08/2016
+
+1. [](#bugfix)
+    * Fix [#767](https://github.com/getgrav/grav-plugin-admin/issues/767) Add styling for new HTML5 input field types
+    * Fix issue with checking the package dependencies when more than one package is being inspected
+
+# v1.2.1
+## 09/07/2016
+
+1. [](#bugfix)
+    * Fixed `tmp://` stream issue with Admin updated to 1.2 before Grav updated 1.1.4
+
+# v1.2.0
+## 09/07/2016
+
+1. [](#new)
+    * All new `file` field. All files get uploaded via Ajax and are stored upon Save. This improves the Save task tremendously as now there is no longer the need of waiting for the files to finish uploading. Fully backward compatible, `file` field now includes also a `limit` and `filesize` option in the blueprints. The former determines how many files are allowed to be uploaded when in combination with `multiple: true` (default: 10), the latter determines the file size limit (in MB) allowed for each file (default: 5MB)
+    * Added a new `filepicker` field, which allows to pick any file from an ajax-powered select box. The `pagemediaselect` field now internally uses the `filepicker` field to live-reload the available files, and to show image previews.
+1. [](#improved)
+    * Better error handling for 500 Internal Server Errors, when Fetch fails
+    * Various notifications style and other CSS fixes
+    * More language strings added
+    * Added `clear-tmp` to cache clear drop-down
+    * Unified JSON twig templates
+    * Better error handling for 500 Internal Server Errors, when Fetch fails.
+    * Updated vendor Libraries
+1. [](#bugfix)
+    * Curl fix for invalid cert errors with News Feed
+    * Avoid requiring `admin.super` for ajax calls [#739](https://github.com/getgrav/grav-plugin-admin/issues/739)
+    * Fix showing HTML in notifications, in the feed
+    * Fixed broken page type filtering
+    * Fixed `beforeunload` event not prompting to offer the choice to stay on the page in case of unsaved changes
+    * Fixed click-away detection for preventing loss of changes, that would get ignored in some circumstances (ie, from modal confirmation)
+    * Fixed issue with `_json` elements where nested fields merging would get stored in an unexpected way
+    * Fixed composer dependencies missing error message
+
+# v1.1.4
+## 08/14/2016
+
+1. [](#bugfix)
+    * Fixed Firefox News Feed dashboard widget layout
+
+# v1.1.3
+## 08/10/2016
+
+1. [](#new)
+    * Admin notifications system.  Admin will pull and cache notifications.  This will be used to announce important updates, security vulnerabilities, and general interest news.
+    * Ability to disable widgets in the dashboard
+    * Added news feed widget to the dashboard
+1. [](#improved)
+    * Updated FontAwesome to v4.6.3
+    * Use new List functionality for Media Configuration
+    * Get fresh media list for `Controller::getListMedia()` rather that cache so always latest.
+    * Add translation strings for the new system.force_ssl option
+    * Reworked List UI to better handle drag & drop sort. To sort it is now required to use the left drag handle [#724](https://github.com/getgrav/grav-plugin-admin/issues/724)
+    * Lists now features a new YAML option `controls: [top|bottom|both]` (default: bottom) which will display the "Add Item" button at the Top and/or Bottom position relative to the list. When the Top button is pressed, a new item will be added at the beginning of the list, when the Bottom button is pressed, a new item will be appended to the list.
+    * Lists now features two new YAML options `sortby: [field]` (default: disabled) and `sortby_dir: [asc|desc]` (default: asc) which will display a new Sorting button in the list allowing to automatically reindex the collection based on the given sort field set.
+    * Lists now features a new YAML option `collapsed: [true|false]` (default: false) and a new UI/UX that allows for collapsing / expanding collection items, allowing to better managing long lists of items. It is advised to always put as first field the most significant one, so that when a list is collapsed it can be still easily browsed.
+    * It is now possible to sort Array fields via drag & drop [#950](https://github.com/getgrav/grav/issues/950)
+1. [](#bugfix)
+    * Fixed issue in Admin favicon URL [#704](https://github.com/getgrav/grav-plugin-admin/issues/704)
+    * Fixed issue in `selfupgrade` where the package would get downloaded in the wrong destination
+    * Hide tab when user is not authorized to access it [#712](https://github.com/getgrav/grav-plugin-admin/issues/712)
+    * Fixed Lists issue when reindexing, causing Radio fields to potentially lose their `checked` status
+    * Avoid overwriting a file when uploaded with the same filename through the Admin blueprint `file` field type if `avoid_overwriting` is enabled on the field
+    * Fixed issue with Array field in `value_only` mode, improperly displaying the key when no value was set
+    * Translate the description of a blueprint field [#729](https://github.com/getgrav/grav-plugin-admin/issues/729)
+
+# v1.1.2
+## 07/16/2016
+
+1. [](#improved)
+    * Forcing limit of upload files based on System settings
+1. [](#bugfix)
+    * Definitive fix for multi form submission in Microsoft Edge causing the Save to not work [#694](https://github.com/getgrav/grav-plugin-admin/issues/694)
+    * Fix issue with calculating the `theme_url` with `open_basedir` restrictions [#699](https://github.com/getgrav/grav-plugin-admin/issues/699)
+    * Check for null payload before going on [#526](https://github.com/getgrav/grav-plugin-admin/issues/526)
+    * Redraw Dashboard Charts when collapsing/expanding the sidebar
+    * Fix for `cache/compiled` errors resulting from page media uploads [getgrav/grav#938](https://github.com/getgrav/grav/issues/938)
+
+# v1.1.1
+## 07/14/2016
+
+1. [](#bugfix)
+    * Fixed issue with forms causing creation of new pages not to work [#698](https://github.com/getgrav/grav-plugin-admin/issues/698) and [getgrav/grav#934](https://github.com/getgrav/grav/issues/934)
+
+# v1.1.0
+## 07/14/2016
+
+1. [](#improved)
+    * Added the ability to login with the email in addition to the username. [#674](https://github.com/getgrav/grav-plugin-admin/issues/674)
+    * It is now possible to sort the Plugins and Themes views by 'Name', 'Author', 'GravTeam', 'Release Date', 'Updates Available' and 'Testing' releases (if in Testing Channel), both Ascending and Descending. [#583](https://github.com/getgrav/grav-plugin-admin/issues/583)
+    * Prevent external links (like the Preview button) to trigger the "Changes Detected" notice [#689](https://github.com/getgrav/grav-plugin-admin/issues/689)
+    * Added a filter field in Plugins and Themes list views, to allow for quick search of a particular resource
+    * Added new `Enabled` sorting option for Plugins list view
+1. [](#bugfix)
+    * Fixed an issue that prevented removing more than one page, in the pages listng [#672](https://github.com/getgrav/grav-plugin-admin/issues/672)
+    * Fixed toggleables in lists that were always loading as checked even when not stored [#688](https://github.com/getgrav/grav-plugin-admin/issues/688)
+    * Fixed Fullscreen tooltip in Editor displaying off screen (when in fullscreen mode) [#677](https://github.com/getgrav/grav-plugin-admin/issues/677)
+    * Fixed inconsistency in the way selectized fields would be rendered [#692](https://github.com/getgrav/grav-plugin-admin/issues/692)
+    * Fixed issue with Save in Microsoft Edge [#694](https://github.com/getgrav/grav-plugin-admin/issues/694)
+
+# v1.1.0-rc.4
+## 06/21/2016
+
+1. [](#bugfix)
+    * Fix for 'front-end' shortcut showing in mobile sidebar incorrectly.
+    * Append progressive number to the copied page title. [#394](https://github.com/getgrav/grav-plugin-admin/issues/394)
+    * Add field description to forms [#667](https://github.com/getgrav/grav-plugin-admin/pull/667)
+    * Fix clearing all cache [#658](https://github.com/getgrav/grav-plugin-admin/issues/658)
+    * Assign the correct ordering when saving a page that didn't have ordering set before [#628](https://github.com/getgrav/grav-plugin-admin/issues/628)
+    * Fix issue when saving a modular child folder as 05.somethin and being reset to 01.something upon save [#628](https://github.com/getgrav/grav-plugin-admin/issues/628)
+
+# v1.1.0-rc.3
+## 06/14/2016
+
+1. [](#bugfix)
+    * Fix for Gemini Scrollbar CSS breaking layout in IE 9+ [#644](https://github.com/getgrav/grav-plugin-admin/issues/644)
+    * Fall back to english for UI language if admin's language is not set [#641](https://github.com/getgrav/grav-plugin-admin/issues/641)
+    * List field has the wrong label/field width.  Switched to "1/3 | 2/3" like all other fields.
+    * Correctly set the page slug on page copy. Avoids having two pages with the same slug [#394](https://github.com/getgrav/grav-plugin-admin/issues/394)
+    * When copying a page, if there's a page prefix (used for ordering), update the value to avoid having two pages with the same order number [#429](https://github.com/getgrav/grav-plugin-admin/issues/429)
+    * Fixed size of dropdown text in responsive views to be readable [#647](https://github.com/getgrav/grav-plugin-admin/issues/647)
+    * Fixed issue with checkbox in toggleables getting submitted with the form even when disabled (fixes #646)
+
+# v1.1.0-rc.2
+## 06/02/2016
+
+1. [](#improved)
+    * Cleaned up the Page Preview CSS to make it more 'standard' [#634](https://github.com/getgrav/grav-plugin-admin/issues/634)
+    * Added a legend with the Page colors explained [#637](https://github.com/getgrav/grav-plugin-admin/issues/637)
+    * Hide email output when sending forgot password instructions [#571](https://github.com/getgrav/grav-plugin-admin/issues/571)
+1. [](#bugfix)
+    * Fixed "Data type `System` doesn't exist!" error when activating a theme [#635](https://github.com/getgrav/grav-plugin-admin/issues/635)
+    * Fixed issue with custom media types not deleting on save [#633](https://github.com/getgrav/grav-plugin-admin/issues/633)
+    * Fixed issue when saving `List` field type in plugins + pages
+    * Fixed JS error on login/logout page due to jQuery not being loaded
+
+
+# v1.1.0-rc.1
+## 06/01/2016
+
+1. [](#new)
+    * Major improvements with the **File Upload** (`file`) field type.  Now fully supports themes, plugins, configuration + pages
+1. [](#improved)
+    * Updated with latest languages via [Crowdin](https://crowdin.com/project/grav-admin/)
+    * Provide security options for single tabs [#615](https://github.com/getgrav/grav-plugin-admin/issues/615)
+    * Disable double clicking on Save/Delete/Copy page actions [#611](https://github.com/getgrav/grav-plugin-admin/issues/611)
+    * Tweaked the avatar alignment in sidebar [#592](https://github.com/getgrav/grav-plugin-admin/issues/592)
+    * Added page name to delete dialog [#511](https://github.com/getgrav/grav-plugin-admin/issues/511)
+    * Enabling / Disabling a Plugin doesn't trigger the expand / collapse details anymore [#614](https://github.com/getgrav/grav-plugin-admin/issues/614)
+    * Added hover on plugins list rows to match pages [#619](https://github.com/getgrav/grav-plugin-admin/issues/619)
+    * Translate media configuration [#608](https://github.com/getgrav/grav-plugin-admin/issues/608)
+    * Use raw routes in blueprints to better support multi-language [#798](https://github.com/getgrav/grav-plugin-admin/issues/798)
+    * Updated NPM modules dependencies
+1. [](#bugfix)
+    * Fix double "Removed successfully" appearing when removing a package [#609](https://github.com/getgrav/grav-plugin-admin/issues/609)
+    * Prevent removing required plugins dependencies when removing a package [#613](https://github.com/getgrav/grav-plugin-admin/issues/613)
+    * Show page title in Delete Confirmation modal if this information is available
+    * Don't try to uninstall admin/form/login/email plugins
+    * Only check for updates if not `admin.maintenance` or `admin.super` [#557](https://github.com/getgrav/grav-plugin-admin/issues/557)
+    * Always submit checkboxes that are not checked and force a 0 value [#616](https://github.com/getgrav/grav-plugin-admin/issues/616)
+    * Fix encoding in tooltips again [#622](https://github.com/getgrav/grav-plugin-admin/issues/622)
+    * Do not show `move` cursor for Collections that aren't sortable [#624](https://github.com/getgrav/grav-plugin-admin/issues/624)
+    * Properly handle Collections that specify a custom key, rather than falling back to indexed list [#632](https://github.com/getgrav/grav-plugin-admin/issues/632)
+
+# v1.1.0-beta.5
+## 05/23/2016
+
+1. [](#improved)
+    * Set sidebar navigation defaults back to "Tab Activation" and "Auto Width"
+    * Custom logo text is displayed as first letter in small sidebar view [#829](https://github.com/getgrav/grav/issues/829)
+    * Copied admin-only blueprints from Grav core to the Admin plugin
+    * Allow `field.label` to have HTML in it [#601](https://github.com/getgrav/grav-plugin-admin/issues/601)
+1. [](#bugfix)
+    * Fixed Togggle field with doubled `checked="checked"` when `toggleable: true` [#579](https://github.com/getgrav/grav-plugin-admin/issues/579)
+    * Strip HTML tags and lowercase username from login/reset forms [#577](https://github.com/getgrav/grav-plugin-admin/issues/577)
+    * Fixed issue with version numbers not showing up for dependencies [#581](https://github.com/getgrav/grav-plugin-admin/issues/581)
+    * Fixed editor tooltips in fullscreen mode and tablet devices rendering [#566](https://github.com/getgrav/grav-plugin-admin/issues/566)
+    * Fixed issue with `file` form field not functioning [#838](https://github.com/getgrav/grav/issues/838)
+    * Fixed issue with creating pages [#595](https://github.com/getgrav/grav-plugin-admin/issues/595)
+
+# v1.1.0-beta.4
+## 05/09/2016
+
+1. [](#new)
+    * Implemented Quickopen functionality to automatically open / close the Sidebar when mouseover
+1. [](#improved)
+    * Better error handling when `obj->validate()` fails with exception [#594](https://github.com/getgrav/grav-plugin-admin/issues/564)
+    * Improve markup of update and add package dependencies in update modal [#560](https://github.com/getgrav/grav-plugin-admin/issues/560)
+1. [](#bugfix)
+    * Fix for admin translation filter (`|tu`) not substituting text - [#567](https://github.com/getgrav/grav-plugin-admin/issues/567)
+    * Translated "Publishing" tab text [#561](https://github.com/getgrav/grav-plugin-admin/issues/561)
+    * Fix invalid argument supplied in foreach [#563](https://github.com/getgrav/grav-plugin-admin/issues/563)
+    * CSS fixes for editor button alignment
+    * Fix for forgot password not finding anyone
+    * Fix UI issue with update button on a package page in Firefox
+    * Fix issue with update button when automatic check for updates is disabled
+    * Fix issue caused by clicking "Check for updates" multiple times
+    * Added missing translations
+    * Fix for Themes with an array of keywords [#823](https://github.com/getgrav/grav/issues/823)
+
+# v1.1.0-beta.3
+## 05/04/2016
+
+1. [](#new)
+    * Added a `|adminNicetime` Twig filter to show 'nicetime' in admin user's language
+    * Added a `prepend` and `append` field option for text input type
+    * Added a WIP `onAdminRegisterPermissions` event
+    * Added several new languages: Arabic, Danish, Greek, Farsi, Korean, Romanian, Thai. Huge thanks to the [translation teams](https://crowdin.com/project/grav-admin)
+1. [](#improved)
+    * Fixed UI issue with Backup / Update buttons positioning
+    * Tweaked placeholders color in login/new user panels [#542](https://github.com/getgrav/grav-plugin-admin/issues/542)
+1. [](#bugfix)
+    * Fixed several untranslated strings
+    * Fix the version information after updating Grav from Admin
+    * Fix a Twig autoescape issue on Plugins descriptions
+    * Fix for showing empty drop-down with only one supported language [#522](https://github.com/getgrav/grav-plugin-admin/issues/522)
+    * Fix for visibility toggle on new page not working [#551](https://github.com/getgrav/grav-plugin-admin/issues/551)
+    * Page tooltips usability issue [#496](https://github.com/getgrav/grav-plugin-admin/issues/496)
+    * Fix removed title attribute from editor toolbar buttons [#539](https://github.com/getgrav/grav-plugin-admin/issues/539)
+    * Allow Incognito / Private browsing to still function in Safari [#527](https://github.com/getgrav/grav-plugin-admin/issues/527)
+
+# v1.1.0-beta.2
+## 04/27/2016
+
+1. [](#new)
+    * Added `grav ~1.1` to dependencies
+    * Added a persistent message if you try to run Admin 1.1 on Grav 1.0
+1. [](#improved)
+    * Used locator instead of `CACHE_DIR`
+    * Added a better way to get Admin version
+    * Show account page for users with certain ACL [#524](https://github.com/getgrav/grav-plugin-admin/pull/524)
+1. [](#bugfix)
+    * Fixed Editor Preview using wrong parameters for the ajax call
+    * Fixed toggle for stable/testing channel
+    * Fixed blueprint JSON fields
+    * If not logged in redirect to base path [#445](https://github.com/getgrav/grav-plugin-admin/pull/445)
+    * Various autoescape fixes
+    * ColorPicker CSS fixes
+    * Fix for translation of admin login [#500](https://github.com/getgrav/grav-plugin-admin/issues/500)
+    * Fix list not applying `toggleable: true` and `style: vertical` [#518](https://github.com/getgrav/grav-plugin-admin/pull/518)
+    * Fixed issue with update for wrong plugin displaying on plugin details pages
+    * Fixed error with the **close sidebar** toggle in some browsers (Firefox, iOS Safari)
+
+# v1.1.0-beta.1
+## 04/20/2016
+
+1. [](#new)
+    * JavaScript Rewrite. Admin is now built in ES6
+    * Lists can now be nested and 'fancy fields' (such as editor, datetime picker, selectize, other lists) get automatically initialized so they are always available no matter if you add or remove items from the lists
+    * The Editor has been reworked to be more flexible. In fact you can now pass any CodeMirror setting via blueprints, through the codemirror: attribute. The buttons have also a new API that allow to add or ignore buttons and behaviors into the toolbar from any plugin (see grav-plugin-editor-buttons). We also added the headers buttons (H1-H6) and Undo / Redo buttons, due to popular demand
+    * We introduced a new colorpicker field. You can now add more colors to your admin plugins :)
+    * Along with the versioning support added in the Grav Core for 1.1, the admin plugin can now install dependencies with the same versioning requirements as the GPM CLI commands.
+    * New System configuration field for toggling GPM release version (testing/stable)
+    * Several new system configuration options for new functionality such as `Process frontmatter Twig`
+    * Ability to collapse the sidebar to a smaller icon view if you need more room.
+1. [](#improved)
+    * The default Grav theme has been tweaked and in many places completely rewritten to ensure that it's as flexible as possible. The primary reason for this was to ensure theming and customization compatibility for the upcoming Admin Pro plugin, but a key benefit includes greatly improved mobile compatibility.
+    * We reworked the Datetimepicker, you will notice a new refreshed UI with a much better support for translations
+    * Tabs are now persistent. In views such as Page editing, when switching tab and saving or refreshing, would cause the tab to be reset to the initial one.
+    * When editing a page in Expert mode, the frontmatter editor is now more friendly. You will now get line numbers, undo/redo and YAML linter.
+    * Behind the scenes we have reworked how the form and toggleables work. This added a lot more reliability and consistency across the whole admin.
+    * The Pages view has more persistent states. It will now remember your expanded/collapsed states as well as filtering.
+    * Lists can now accept a custom button label with the 'btnLabel' property
+    * After login to Admin, redirect to the original URL called
+    * Admin now has an unique cache key compared to the 'site' so pages can be cached independently
+    * Improved the layout of the User Profile page.
+    * Set cache key uniquely for admin so cache does not colide with site
+1. [](#bugfix)
+    * Fix for modular preview - [#254](https://github.com/getgrav/grav-plugin-admin/issues/254)
+    * Fix for long content and page tabs - [#441](https://github.com/getgrav/grav-plugin-admin/issues/441)
+    * Fix for clear cache after adding new folder - [#393](https://github.com/getgrav/grav-plugin-admin/issues/393)
+
+# v1.0.9
+## 02/11/2016
+
+1. [](#bugfix)
+    * Fix language translation files
+
+# v1.0.8
+## 02/05/2016
+
+1. [](#new)
+    * Added a logout button when not authorized to access a page in Admin
+    * Added the option to hide a tab from an extended blueprint (https://github.com/getgrav/grav/issues/620)
+    * Many new languages and updates to existing languages from the Translation team.
+1. [](#improved)
+    * Check frontmatter for validity prior to saving
+    * Add noindex, nofollow across the entire admin theme if no other robots headers are set on a page
+    * Allow to hide a configuration blueprint section / tab and still save its values
+    * Allow to show user defined blueprints in configuration
+    * Updated FontAwesome to latest 4.5.0 version
+1. [](#bugfix)
+    * Fixed an issue with user registration on Linux caused by `glob()` possibly returning false.
+    * Fixed an issue preventing Admin to work correctly in a multisite configuration
+    * Fixed preview and insertion of images with non-lowercase extension
+    * Fixed an incorrect number of pages being displayed in the sidebar in some cases
+    * [Security] Don't reveal Grav filesystem path when trying to delete non-existing images
+    * [Security] Fix PHP error happening when uploading file without extension if the JS dropzone uploader is configured to allow empty file extensions
+    * [Security] Ensure correct escaping in various Twig files
+
+# v1.0.7
+## 01/15/2016
+
+1. [](#new)
+    * Added onAdminDashboard event
+    * Added onAdminSave event
+    * New lang strings for reverse proxy toggle
+1. [](#improved)
+    * More robust YAML file checking in config folders
+    * Removed deprecated menu event
+    * Removed old logs code
+    * Used new onAdminDashboard event for current dashboard widgets
+1. [](#bugfix)
+    * Fix for missing access checks on config pages #397
+    * Fix parent not loaded on admin form save #587
+    * When no route field is added to a page blueprint, add it as page root
+    * Fix for wrong page count (will show dynamic added pages in count too - Need to fix this)
+    * Fix for IE/Edge saving forms #391
+
+# v1.0.6
+## 01/07/2016
+
+1. [](#bugfix)
+    * Fix for forms appending `_json` fields on every save
+
+# v1.0.5
+## 01/07/2016
+
+1. [](#new)
+    * Added a pointer to Grav's contributing guide
+    * Handle the optional logic to strip home from Page routes and urls
+    * The Configuration page now shows any blueprint found in the user/blueprints/config/ folder, thus allowing to add custom configurations
+1. [](#improved)
+    * Allow the nonce for a POST action to be set in the query url
+    * Add a fallback twig template to use in case Twig cannot find a template file
+    * Modified update Theme and Plugin buttons to use more reliably markup
+1. [](#bugfix)
+    * Fix additional `on` parameter when saving plugins configs that contain tabs in their blueprint
+    * Fixes for the `pagemediaselect` form field
+    * Fix an untranslated message in the logout form when `system.languages.translations` is disabled
+    * Fixed a hardcoded `http://` reference throwing warnings under HTTPS
+    * Ensure download package has `.zip` extension, just in case
+
+# v1.0.4
+## 12/22/2015
+
+1. [](#improved)
+    * Improved File input field for admin
+    * Restore file inputs functionality and process form via JS if no inputs found
+1. [](#bugfix)
+    * Fix for the image preview in the file field on multi-lang sites
+    * Fix problem in form code introduced by fix to allow file uploads
+    * Fix redirect in deleting page media
+
+# v1.0.3
+## 12/20/2015
+
+1. [](#new)
+    * Added `pagemediaselect` field for use in pages
+1. [](#improved)
+    * Updated various languages
+    * Check for method `meetsRequirements()` prior to using
+    * Enable `file` form field to be used in plugins and theme blueprints
+
+# v1.0.2
+## 12/18/2015
+
+1. [](#bugfix)
+    * Fixed issue with user edit page causing error due to individual language files
+
+# v1.0.1
+## 12/18/2015
+
+1. [](#new)
+    * Moved languages into individual files under `languages/` folder
+    * Added a check for PHP version
+    * Dutch translation added
+1. [](#improved)
+    * Let forms work with file inputs
+    * Various file input improvements
+    * Language updates
+    * Better checks for existence of Popularity JSON data
+    * Add file processing to admin forms
+    * More Admin Pro integration fixes
+1. [](#bugfix)
+    * Set form to multipart if it contains a file field
+    * `cleanFilesData()` now returns just the filename
+
+# v1.0.0
+## 12/11/2015
+
+1. [](#new)
+    * New built-in admin registration process
+    * Added security check to `section` form field
+    * Added new RocketTheme font with various icons
+    * Add `onAdminThemeInitialized()` event to admin `Themes::init()`
+    * Force timestamp on CSS/JS assets based on `GRAV_VERSION`
+    * Additions for Gantry5 support
+1. [](#improved)
+    * Force lowercase `username` when logging in
+    * Hide markdown preview except for pages
+    * Added a notice if you don't have permission to see dashboard
+    * Updated admin login page logic
+    * Return "Invalid Security Token" instead of "Unauthorized"
+    * Throw exception if you used with built-in PHP web server
+    * Updated languages
+    * Removed `noreply@getgrav.org` default email address
+    * Use new methods to disable CSS/JS pipeline if available
+    * Various code cleanups
+1. [](#bugfix)
+    * Handle case when email `from` is not configured
+    * Fix tabs support in plugin/themes settings
+    * Fix param separator in page media Ajax call
+    * Fix favicon base URL
+
+# v1.0.0-rc.7
+## 12/01/2015
+
+1. [](#new)
+    * Display error page if page does not exist in admin
+    * Removed Beta message option and added toggle for GitHub message
+    * Added functionality to support Admin Pro plugin (in development)
+1. [](#improved)
+    * Added support for Markdown editor in lists #239
+    * Better Markdown Editor API with dynamic initialization
+    * Various language updates
+    * Removed some unused variables
+    * Added admin check for pages existence
+    * Prevent the admin to cause an error when an Ajax action is in progress
+    * Force translations to be active even when disabled in site #299
+    * Do not reinitialize `Selectize` if already available
+1. [](#bugfix)
+    * Fixed full-screen markdown Editor
+    * Fix modular preview not working reliably #254
+    * **Nonce fixes** (hopefully the last of them!)
+    * Fix broken plugin enable/disable
+    * Fix issue where `_redirect: /plugins` was getting stored in the plugin configuration
+    * Replace default them service with admin one
+    * Fix saving array fields #304
+    * Fix missing translations when default language is not english
+    * Fix title variables not translated #310
+
+# v1.0.0-rc.6
+## 11/21/2015
+
+1. [](#improved)
+    * Implemented logic to detect when offline and suppress Ajax calls
+    * Added nonce logic to be used by JS
+1. [](#bugfix)
+    * Nonce fix for updating themes
+    * Nonce fix for deleting pages
+
+# v1.0.0-rc.5
+## 11/20/2015
+
+1. [](#new)
+    * Use **Nonce** mechanism for form security
+    * Added Hungarian translation
+    * Add support for Markdown labels #271
+    * Added support for Markdown Editor in all the things
+    * Implemented save keyboard shortcut (Ctrl + S / CMD + S)
+1. [](#improved)
+    * Better error for "Internal Server Error" when accessing GPM
+    * Updated French translation
+    * Updated Russian translation
+    * Load Gravatar image with protocol-less `//:` syntax
+    * Improved header UI in mobile browsers #265
+    * Dropped unused version of JQuery
+    * More visible Preview link icon
+    * Hide **Latest pages** if there are none
+    * Improved toggle to better support different length strings
+1. [](#bugfix)
+    * Force rescanning fields when submitting a form #243
+    * Set default lang for pages on fresh session
+    * Escaped values in `array.html.twig`
+    * Fix saving in IE Edge
+    * Fixed various typos
+    * Fixed JS button issues #370
+    * Fixed JS error in private browsing #272
+    * Fixed date field border
+    * Fixed multiple instance of Markdown Editor #285
+    * Fixed Spacer CSS #267
+
+# v1.0.0-rc.4
+## 10/29/2015
+
+1. [](#improved)
+    * Changed admin menu event hook to `onAdminMenu()`
+    * Minor improvements for admin page location
+    * Additional lang strings for Grav 1.0.0-rc.3
+
+# v1.0.0-rc.3
+## 10/27/2015
+
+1. [](#improved)
+    * Rely on context-language for active language
+    * Improved some Russian translations
+    * Only show login if not already logged in
+1. [](#bugfix)
+    * Disable asset pipeline in admin only
+    * Fix Editor cursor insertion point when text is selected in some actions
+
+# v1.0.0-rc.2
+## 10/23/2015
+
+1. [](#bugfix)
+    * Reverted lang redirect code. Needs to be reworked to be more reliable
+
+# v1.0.0-rc.1
+## 10/23/2015
+
+1. [](#new)
+    * Redirect to non-language URL except for `pages/`
+1. [](#improved)
+    * New language strings for new `system.yaml` fields
+    * Improved Russian translations
+    * Improved compatibility with PECL Yaml parser
+1. [](#bugfix)
+    * Redirect to correct page if you change folder/slug
+    * Fix issue with Asset pipeline not being disabled in admin
+    * Fix for HTML in text input fields
+    * Fixed various icons in headers
+
+# v0.6.2
+## 10/15/2015
+
+1. [](#improved)
+    * Use `title` rather than `menu` in Page listing
+    * Wrapped language strings in double-quotes
+    * New language strings for new fields
+1. [](#bugfix)
+    * Fixed issue with IE not able to save pages
+
+# v0.6.1
+## 10/07/2015
+
+1. [](#new)
+    * Added the ability to render front-end templates in markdown preview
+    * Option to disable Google-based fonts. Useful for Cyrillic languages.
+    * Couple of new static helper methods used by new page blueprints
+    * New `fieldset` form field (thanks @Sommerregen!)
+1. [](#improved)
+    * Hide editor buttons in preview mode
+    * Improved support for admin when offline
+    * Use relative URL in Login form
+    * Added some more missing lang strings
+    * Improved German translation
+    * Compressed CSS files for improved performance
+    * Only get last 7 days in week count calculation
+1. [](#bugfix)
+    * Fix saving pages in local-specific languages
+    * Only track 'human' page hits in statistics
+    * Responsive fixes for 'wordy' languages
+    * Fixed delete issue with array field type
+    * Fixed some hardcoded `admin` references to allow admin path change
+    * Fix for issue with lang code being added twice
+    * Fix language name in admin buttons
+
+# v0.6.0
+## 09/16/2015
+
+1. [](#new)
+    * Support for custom markdown editor buttons!
+    * Added Russian translations
+    * Added Japanese translations
+    * Ajax session keep-alive when editing forms
+1. [](#improved)
+    * Added missing Italian translations
+    * Added additional options field into the pages form field
+1. [](#bugfix)
+    * Fix GPM errors in offline mode
+    * Fix for duplicate status messages
+
+# v0.5.0
+## 09/11/2015
+
+1. [](#new)
+    * Responsive layout for mobile compatibility (thanks @Vivalldi!)
+    * Added page type and many other new filters to Page list view
+    * Added granular ACL requirements to admin pages
+    * Ability to define page date format
+    * Added `onAdminTemplateNavPluginHook` to allow for plugins to hook into sidebar
+    * Added YAML Twig filters (to and from)
+    * Support for nested metadata
+    * Added ability to disable automatic update checks via admin plugin configuration
+    * Initial Spanish translation
+1. [](#improved)
+    * Check for existence of a user account
+    * Various language additions
+    * Refactored form fields to remove duplicates from form plugin
+    * Improved date picker
+    * Improved display field
+    * Add page template type to page list view
+    * Various UI fixes
+    * Added some default field 'focus' to save clicking
+    * Only allow "Add Modular" if the theme has modular templates
+    * Updated `chartist.js` library
+    * Updated 'fontawesome' fonts to the latest v4.4
+1. [](#bugfix)
+    * Fix for "drag-n-drop" of non-image media
+    * Fix a fatal error in GPM when offline
+    * Fix a z-index bug with tooltips
+    * Fix a z-index bug in lang dropdowns
+    * Don't allow deleting of last empty array field
+    * Fix for images with parenthesis in filenames
+    * Fix for page title visualization when not set
+    * Fix for cursor position in folder/array fields
+
+# v0.4.3
+## 08/31/2015
+
+1. [](#new)
+    * Added Japanese translation
+    * Support for independent file name and template override
+1. [](#improved)
+    * Improved slug generation using `slugify.js`
+    * Allow the `title` twig variables to set the page title
+    * Improved Page media handling with several bugfixes
+    * Prevent error when there are no pages on a site
+    * If all updates are applied, show "Fully Updated" text in dashboard
+    * Better preview link (requires `rtrim` filter from Grav 0.9.40)
+    * Order all plugins and themes alphabetically
+    * Removed duplicate language entries
+1. [](#bugfix)
+    * Fix for redirect after saving when multilang not enabled
+    * Fix for deleting responsive media
+    * Fix for HTML encoding in markdown field
+
+# v0.4.2
+## 08/25/2015
+
+1. [](#bugfix)
+    * Fix for current admin lang not showing up in page lang dropdown
+    * Fix for incorrect NAME/CONTENT lang keys
+    * Fix for incorrect site link
+
+# v0.4.1
+## 08/24/2015
+
+1. [](#bugfix)
+    * Fix for broken **Add Page** - Doh!
+    * Fix for empty site link when at root
+
+# v0.4.0
+## 08/24/2015
+
+1. [](#new)
+    * Multi-language Page support!!!
+    * Admin languages configurable per user
+    * Toastr messages for `check updates`
+    * new `tu` filter for admin translations
+    * Italian and German admin translations
+    * Added a save location in system and site configuration
+    * Page metadata now uses flexible array field
+1. [](#improved)
+    * Allow subpages of modular pages to display in pages list
+    * Open external pages in new tabs
+    * Reworked `visibility` of pages
+    * Use `PLUGIN_ADMIN` prefix for translations
+    * Added link to gravatar.com to avoid confusion on avatar
+    * Limit page count to 200 in ordering field
+    * Fixed various Safari _flex_ issues
+    * Use `rawRoute()` for page links
+    * Minor `param separator` fixes
+    * Various CSS fixes
+    * Improved CodeMirror to force spaces
+    * Added **Selectize** dropdowns to various forms and modals
+1. [](#bugfix)
+    * Fix for `Call to a member function path() on non-object` error
+    * Fixed dropdown z-index issues
+    * Correctly set the filename including language if set
+    * Fix for empty taxonomies on page save
+    * Fix for page not redirecting properly on folder change
+    * Fix for table headers styling
+    * Added missing translation strings
+    * Unique page counting in total page counts
+    * Fixed JS warning with page filtering and deleting
+
+
+# v0.3.0
+## 08/11/2015
+
+1. [](#new)
+    * Show current date in form date format fields
+    * Added a new **check for updates** button to flush GPM
+    * Added session timeout configuration for admin
+    * Added `isSymlink` logic for Grav
+    * Added new `phpinfo` page
+1. [](#improved)
+    * Improved toggleables
+    * Support `param_separator` for Apache on windows
+    * Logout now goes to interstitial to provide session messages
+    * Updated hints and improved formatting
+    * Encoding URI for images in editor preview
+    * Create user `system.yaml` and `site.yaml` if they are missing
+    * Open external links in new tab by default
+    * Set edit mode to `normal` by default
+    * Disable CSS/JS pipelining in the admin
+1. [](#bugfix)
+    * Fixed form submission not working in IE
+    * Fix fatal error when deleting homepage
+    * Prevent admin plugin activating when the URL of a page contains partial route
+
+# v0.2.0
+## 08/06/2015
+
+1. [](#new)
+    * Added multiple **clear cache** types
+    * Added back to themes link when adding new themes
+    * Properly handles visibility and ordering and guesses best option on new
+    * Added new templates field with support for custom (unsupported) template type
+    * Added new display field for displaying simple text value
+    * **Update Grav** button now works
+    * Added spanish translation
+    * Added german translation
+1. [](#improved)
+    * Improved page order handling logic
+    * Implemented 2-step theme switching logic with warning
+    * Force `modular` page class for modular template
+    * Clear page cache on page delete (ghost pages still showing)
+    * Clears route on page save so changes such as `slug` are picked up
+    * Fix dashboard layout in Safari
+    * Added tooltips for official 'Team Grav' themes/plugins
+1. [](#bugfix)
+    * Handle modular page templates on create
+    * Fixed Firefox JS error for arrays
+    * Ensure we don't change page type to empty and save (causing page to be deleted)
+    * Fixed some minor CSS issues with editor
+    * Fixed link to RocketTheme.com
+    * Disabled fields now stay properly disabled
+
+# v0.1.1
+## 08/04/2015
+
+1. [](#bugfix)
+    * Fixed GitHub URLs
+    * Hiding toggle for disabling Admin plugin
+    * Removed extra text not needed
+
+# v0.1.0
+## 08/04/2015
+
+1. [](#new)
+    * ChangeLog started...

+ 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"
+               }
+            ]
+         }
+      }
+   }
+}

+ 307 - 0
plugins/admin/languages/ar.yaml

@@ -0,0 +1,307 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "هذا إصدار بيتا! استخدم هذا في الإنتاج على مسؤوليتك الخاصة..."
+  ADMIN_REPORT_ISSUE: "وجدت مشكلة؟ الرجاء الإبلاغ عن GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Powered by Grav</a> - The Modern Flat File CMS"
+  LOGIN_BTN: "تسجل الدخول"
+  LOGIN_BTN_FORGOT: "نسيت"
+  LOGIN_BTN_RESET: "إعادة تعيين كلمة المرور"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "إرسال إرشادات إعادة تعيين"
+  LOGIN_BTN_CLEAR: "مسح النموذج"
+  LOGIN_BTN_CREATE_USER: "أنشاء مستخدم جديد"
+  LOGIN_LOGGED_IN: "لقد تم تسجيل بنجاح"
+  LOGIN_FAILED: "فشل تسجيل الخول"
+  LOGGED_OUT: "لقد قمت بتسجيل الخروج"
+  RESET_NEW_PASSWORD: "إدخال كلمة سر جديدة رجاءً &hellip;"
+  RESET_LINK_EXPIRED: "انتهت مدة صلاحية إعادة الارتباط، الرجاء المحاولة مرة أخرى"
+  RESET_PASSWORD_RESET: "لقد تم إعادة تعيين كلمة المرور"
+  RESET_INVALID_LINK: "اللينك خاطئ ، الرجاء المحاولة مرة أخرى"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "تم إرسال إرشادات إعادة تعيين كلمة المرور الخاصة بك عبر البريد الإلكتروني إلى %s"
+  FORGOT_FAILED_TO_EMAIL: "فشل في تعليمات البريد الإلكتروني، الرجاء المحاولة مرة أخرى لاحقاً"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "لا يمكن إعادة تعيين كلمة المرور ل %s، لم يتم تعيين عنوان البريد الإلكتروني"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "لا يوجد المستخدم مع اسم المستخدم <b>%s</b>"
+  FORGOT_EMAIL_NOT_CONFIGURED: "لا يمكن إعادة تعيين كلمة المرور. لم يتم تكوين هذا الموقع لإرسال رسائل البريد الإلكتروني"
+  FORGOT_EMAIL_SUBJECT: "طلب إعادة تعيين كلمة المرور %s"
+  FORGOT_EMAIL_BODY: "<h1>\"إعادة تعيين كلمة المرور\"</h1><p>عزيزي %1$s،</p><p>طلبا قدم في <b>%4$s</b> لإعادة تعيين كلمة المرور الخاصة بك.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">انقر فوق هذا الخيار لإعادة تعيين الخاص بك كلمة مرور</a><br /><br /></p><p>بدلاً من ذلك، نسخ عنوان URL التالي في شريط العناوين في المستعرض الخاص بك:</p> <p>%2$s</p><p><br />أطيب التحيات،<br /><br />%3$s</p>"
+  MANAGE_PAGES: "إدارة الصفحات"
+  PAGES: "الصفحات"
+  PLUGINS: "الملحقات"
+  PLUGIN: "البرنامج الإضافي"
+  THEMES: "المواضيع"
+  LOGOUT: "تسجيل الخروج"
+  BACK: "الرجوع"
+  NEXT: "التالي"
+  PREVIOUS: "السابق"
+  ADD_PAGE: "إضافة صفحة"
+  MOVE: "انقل"
+  DELETE: "حذف"
+  UNSET: "تراجع عن التعيين"
+  VIEW: "عرض"
+  SAVE: "حفظ"
+  NORMAL: "عادي"
+  EXPERT: "خبير"
+  EXPAND_ALL: "عرض الكل"
+  COLLAPSE_ALL: "طي الكل"
+  ERROR: "خطأ"
+  CLOSE: "أغلق"
+  CANCEL: "إلغاء"
+  CONTINUE: "المتابعة"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "التأكيد مطلوب"
+  MODAL_CHANGED_DETECTED_TITLE: "تم الكشف عن التغييرات"
+  MODAL_CHANGED_DETECTED_DESC: "وقد تغييرات غير محفوظة.  هل أنت متأكد من أنك تريد ترك دون الحفظ؟"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "التأكيد مطلوب"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "هل أنت متأكد من حذف هذا الملف؟ لا يمكن التراجع عن هذا الإجراء."
+  ADD_FILTERS: "إضافة عامل تصفية"
+  SEARCH_PAGES: "صفحات البحث"
+  VERSION: "النسخة"
+  WAS_MADE_WITH: "تم عمله مع"
+  BY: "بواسطة"
+  UPDATE_THEME: "تحديث الموضوع"
+  UPDATE_PLUGIN: "تحديث البرنامج الإضافي"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "من هذه السمة الآن متوفرة"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "من هذه الإضافة متوفرة الآن"
+  AUTHOR: "المؤلّف"
+  HOMEPAGE: "الصفحة الرئيسية"
+  DEMO: "عرض تجريبي"
+  BUG_TRACKER: "متتبع الأخطاء"
+  KEYWORDS: "الكلمات الرئيسية"
+  LICENSE: "الرخصة"
+  DESCRIPTION: "الوصف"
+  README: "الملف التمهيدي"
+  REMOVE_THEME: "إزالة الموضوع"
+  INSTALL_THEME: "تثبيت الموضوع"
+  THEME: "الموضوع"
+  BACK_TO_THEMES: "العودة إلى المواضيع"
+  BACK_TO_PLUGINS: "العودة إلى البرامج الإضافية"
+  CHECK_FOR_UPDATES: "التحقق من وجود تحديثات"
+  ADD: "أَضِف"
+  CLEAR_CACHE: "مسح ذاكرة التخزين المؤقتة"
+  CLEAR_CACHE_ALL_CACHE: "كافة المحفوظات"
+  CLEAR_CACHE_ASSETS_ONLY: "الاصول فقط"
+  CLEAR_CACHE_IMAGES_ONLY: "الصور فقط"
+  CLEAR_CACHE_CACHE_ONLY: "المحفوظات فقط"
+  CLEAR_CACHE_TMP_ONLY: "المؤقتة فقط"
+  UPDATES_AVAILABLE: "تحديثات متوفّرة"
+  DAYS: "أيام"
+  UPDATE: "تحديث"
+  BACKUP: "نسخة احتياطية"
+  BACKUPS: "النسخ الاحتياطية"
+  BACKUP_NOW: "قم بحفض نسخة احتياطية"
+  BACKUPS_STATS: "قم بحفض نسخة احتياطية لاحصائيات"
+  BACKUPS_HISTORY: "قم بحفض نسخة احتياطية للسجل"
+  BACKUPS_PROFILES: "قم بحفض نسخة احتياطي للحسابات"
+  BACKUPS_COUNT: "عدد النسخ الاحتياطية"
+  BACKUPS_PROFILES_COUNT: "عدد الحسابات"
+  BACKUPS_TOTAL_SIZE: "المساحة المستعملة"
+  BACKUPS_NEWEST: "النسخ الاحتياطية الحديثة"
+  BACKUPS_OLDEST: "النسخ الاحتياطية القديمة"
+  BACKUPS_PURGE: "تطهير"
+  BACKUPS_NOT_GENERATED: "لم يتم إنشاء نسخ احتياطية بعد..."
+  BACKUPS_PURGE_NUMBER: "استخدام %s من %s من فتحات النسخ الاحتياطي"
+  BACKUPS_PURGE_TIME: "%s ايام من النسخ الاحتياطية بقية"
+  BACKUPS_PURGE_SPACE: "استعمال %s من %s"
+  BACKUP_DELETED: "تم حذف النسخة الاحتياطية بنجاح"
+  BACKUP_NOT_FOUND: "لم يتم العثور على نسخة احتياطية"
+  BACKUP_DATE: "تاريخ النسخ الاحتياطي"
+  STATISTICS: "إحصائيات"
+  TODAY: "اليوم"
+  WEEK: "اسبوع"
+  MONTH: "شهر"
+  LATEST_PAGE_UPDATES: "آخر التحديثات"
+  MAINTENANCE: "الصيانه"
+  UPDATED: "تم تحديثه"
+  MON: "الإثنين"
+  TUE: "الثلاثاء"
+  WED: "الإربعاء"
+  THU: "الخميس"
+  FRI: "الجمعة"
+  SAT: "السبت"
+  SUN: "الأحد"
+  COPY: "نسخ"
+  EDIT: "تحرير"
+  CREATE: "انشاء"
+  GRAV_ADMIN: "مدير جريف"
+  GRAV_OFFICIAL_PLUGIN: "اضاف رسمية ل جريف"
+  GRAV_OFFICIAL_THEME: "سمة جريف رسمية"
+  PLUGIN_SYMBOLICALLY_LINKED: "هذه الاضافه مرتبطة بمرجع. لن يام اكتشاف التحديثات."
+  THEME_SYMBOLICALLY_LINKED: "هذه السمة مرتبطة بمرجع. لم يتم اكتشاف التحديثات"
+  REMOVE_PLUGIN: "حذف الإضافة"
+  INSTALL_PLUGIN: "تثبيت الإضافة"
+  AVAILABLE: "متوفر"
+  INSTALLED: "مثبت"
+  INSTALL: "تثبيت"
+  ACTIVE_THEME: "سمة نشطة"
+  SWITCHING_TO: "التبديل إلى"
+  SWITCHING_TO_DESCRIPTION: "بالتبديل إلى سمة مختلفة, لايوجد أي ضمانات بأن صفحات الواجهة مدعومة, من المحتمل أن تحدث أخطاء عند محاولة تحميل الصفحات المذكورة."
+  SWITCHING_TO_CONFIRMATION: "هل تريد التبديل الى السمة والمتابعة"
+  CREATE_NEW_USER: "إنشاء مستخدم جديد"
+  REMOVE_USER: "حذف المستخدم"
+  ACCESS_DENIED: "الدخول ممنوع"
+  ACCOUNT_NOT_ADMIN: "لا يملك حسابك أي صلاحيات للإدارة"
+  PHP_INFO: "معلومات بي إش بي"
+  INSTALLER: "المثبت"
+  AVAILABLE_THEMES: "سمات متوفرة"
+  AVAILABLE_PLUGINS: "إضافات متوفرة"
+  INSTALLED_THEMES: "السمات المثبتة"
+  INSTALLED_PLUGINS: "الإضافات المثبتة"
+  BROWSE_ERROR_LOGS: "سجل أخطاء التصفح"
+  SITE: "موقع"
+  INFO: "معلومات"
+  SYSTEM: "النظام"
+  USER: "المستخدم"
+  ADD_ACCOUNT: "إضافة حساب"
+  SWITCH_LANGUAGE: "تبديل اللغة"
+  SUCCESSFULLY_ENABLED_PLUGIN: "تم تفعيل الإضافة بنجاح"
+  SUCCESSFULLY_DISABLED_PLUGIN: "تم إيقاف الإضافة بنجاح"
+  SUCCESSFULLY_CHANGED_THEME: "تم تبديل السمة الإفتراضية بنجاح"
+  INSTALLATION_FAILED: "فشل التثبيت"
+  INSTALLATION_SUCCESSFUL: "تم التثبيت بنجاح"
+  UNINSTALL_FAILED: "فشل الغاء التثبيت"
+  UNINSTALL_SUCCESSFUL: "تم إلغاء التثبيت بنجاح"
+  SUCCESSFULLY_SAVED: "حفظ بنجاح"
+  SUCCESSFULLY_COPIED: "نسخ بنجاح"
+  REORDERING_WAS_SUCCESSFUL: "إعادة ترتيب تم بنجاح"
+  SUCCESSFULLY_DELETED: "تم الحذف بنجاح"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "تم تغيير اللغة بنجاح"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "لديك أذونات غير كافية للقيام بالمهمة"
+  CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
+  METHOD: "الأسلوب"
+  ERROR_CLEARING_CACHE: "خطأ مسح ذاكرة التخزين المؤقت"
+  AN_ERROR_OCCURRED: "حدث خطأ ما"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "النسخة الاحتياطية جاهزة للتحميل"
+  DOWNLOAD_BACKUP: "تنزيل النسخة الاحتياطية"
+  PAGES_FILTERED: "الصفحات التي تمت تصفيتها"
+  NO_PAGE_FOUND: "لم يتم العثور على أي صفحة"
+  INVALID_PARAMETERS: "متغيرات غير صالحة"
+  NO_FILES_SENT: "لا توجد ملفات أرسلت حتى الآن"
+  EXCEEDED_FILESIZE_LIMIT: "تجاوز الحد الأقصى لحجم ملف التكوين بي إتش بي"
+  UNKNOWN_ERRORS: "خطأ غير معروف"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "تجاوز الحد الأقصى لحجم ملف التكوين بي إتش بي"
+  UNSUPPORTED_FILE_TYPE: "نوع ملف غير معتمد"
+  FAILED_TO_MOVE_UPLOADED_FILE: "فشل في تحميل الملف"
+  FILE_UPLOADED_SUCCESSFULLY: "تم رفع الملف بنجاح"
+  FILE_DELETED: "تم حذف الملف"
+  FILE_COULD_NOT_BE_DELETED: "لا يمكن حذف الملف"
+  FILE_NOT_FOUND: "لم يتم العثور على الملف"
+  NO_FILE_FOUND: "لا يوجد ملف"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "تم تحديث النظام بنجاح الى"
+  GRAV_UPDATE_FAILED: "فشل تحديث نظام جراف"
+  EVERYTHING_UPDATED: "تم تحديث كل شيء"
+  UPDATES_FAILED: "فشل التحديث"
+  AVATAR_BY: "الصورة الرمزية التي"
+  AVATAR_UPLOAD_OWN: "أو قم بتحميل الخاصة بك..."
+  LAST_BACKUP: "آخر نسخ احتياطي"
+  FULL_NAME: "الإسم الكامل"
+  USERNAME: "إسم المستخدم"
+  EMAIL: "البريد الإلكتروني"
+  USERNAME_EMAIL: "إسم المستخدم أو البريد الإلكتروني"
+  PASSWORD: "كلمة السر"
+  PASSWORD_CONFIRM: "تأكيد كلمة السر"
+  TITLE: "العنوان"
+  LANGUAGE: "اللّغة"
+  ACCOUNT: "الحساب"
+  EMAIL_VALIDATION_MESSAGE: "يجب أن يكون البريد الإلكتروني صحيحاً"
+  PASSWORD_VALIDATION_MESSAGE: "يجب أن تحتوي كلمة المرور على الأقل على رقم وعلى حرف كبير وعلى حرف صغير، و أن تكون مكونة على الأقل من 8 أحرف أو أكثر"
+  LANGUAGE_HELP: "تعيين اللغة المفضلة"
+  MEDIA: "وسائط"
+  DEFAULTS: "الإعدادات الافتراضية"
+  SITE_TITLE: "عنوان الموقع"
+  SITE_TITLE_PLACEHOLDER: "عنوان الموقع العريض"
+  SITE_TITLE_HELP: "العنوان الإفتراضي لموقعك, غالبا يستخدم في السمات"
+  SITE_DEFAULT_LANG: "اللغة الإفتراضية"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "اللغة الإفتراضية المستخدمة في السمات وسم <HTML>"
+  SITE_DEFAULT_LANG_HELP: "اللغة الإفتراضية المستخدمة في السمات وسم <HTML>"
+  DEFAULT_AUTHOR: "المؤلف الافتراضي"
+  DEFAULT_AUTHOR_HELP: "إسم المؤلف الإفتراضي, يستخدم عادة في السمات او محتوى الصفحة"
+  DEFAULT_EMAIL: "البريد الإلكتروني الإفتراضي"
+  DEFAULT_EMAIL_HELP: "البريد الإلكتروني للإشارة في المواضيع أو صفحات"
+  TAXONOMY_TYPES: "أنواع التصنيف"
+  TAXONOMY_TYPES_HELP: "يجب أن يتم تعريف أنواع التصنيف هنا إذا كنت ترغب في استخدامها في صفحات"
+  PAGE_SUMMARY: "ملخص الصفحة"
+  ENABLED: "فعال"
+  ENABLED_HELP: "تمكين صفحة الموجز (الملخص ترجع نفس محتوى الصفحة)"
+  'YES': "نعم"
+  'NO': "لا"
+  SUMMARY_SIZE: "حجم الملخص"
+  SUMMARY_SIZE_HELP: "مقدار أحرف صفحة لاستخدامها كمحتوى موجز"
+  FORMAT: "الشكل"
+  FORMAT_HELP: "اختصار = الاستخدام الأولى لوقوع محدد أو الحجم؛ = فترة طويلة سيتم تجاهل موجز محدد"
+  SHORT: "قصير"
+  LONG: "طويل"
+  DELIMITER: "الفاصل"
+  DELIMITER_HELP: "موجز محدد (الافتراضي '= = =')"
+  METADATA: "البيانات الوصفية"
+  METADATA_HELP: "قيم بيانات التعريف الافتراضية التي سيتم عرضها في كل صفحة ما لم يتم تجاوز الصفحة"
+  NAME: "الإسم"
+  CONTENT: "المحتوى"
+  SIZE: "حجم"
+  ACTION: "اجراء"
+  REDIRECTS_AND_ROUTES: "اعادة التوجيه و المسارات"
+  CUSTOM_REDIRECTS: "تخصيص اعادة التوجيه"
+  CUSTOM_REDIRECTS_HELP: "مسارات لإعادة التوجيه إلى صفحات أخرى. الاستبدال الاساسي ل Regex صحيح"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/ الخاص / الاسم المستعار"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/ الخاص /إعادة التوجيه"
+  CUSTOM_ROUTES: "تخصيص المسارات"
+  CUSTOM_ROUTES_HELP: "مسارات لاسماء المستعارة إلى صفحات أخرى. الاستبدال الاساسي ل Regex صحيح"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/ الخاص / الاسم المستعار"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/ الخاص / المسار"
+  FILE_STREAMS: "تيارات الملف"
+  DEFAULT: "الإعدادات العامة"
+  PAGE_MEDIA: "صور و فيديوهات الصفحة"
+  OPTIONS: "الخيارات"
+  PUBLISHED: "نُشِرَ"
+  PUBLISHED_HELP: "بشكل افتراضي، يتم نشر صفحة إلا إذا قمت بتعيين المنشور: false أو عن طريق publish_date يجري في المستقبل، أو unpublish_date في الماضي"
+  DATE: "التاريخ"
+  PUBLISHED_DATE: "تاريخ النشر"
+  ROBOTS: "الروبوتات"
+  ADVANCED: "خيارات متقدمة"
+  SETTINGS: "الإعدادات"
+  FOLDER_NAME: "إسم المجلد"
+  MENU: "القائمة"
+  USE_GLOBAL: "الاستخدام العام"
+  ROUTABLE: "قابل للتوجيه"
+  ROUTABLE_HELP: "إذا أمكن ولوج هذه الصفحة عبر عنوان URL"
+  VISIBLE: "مرئي"
+  ASCENDING: "تصاعدي"
+  DESCENDING: "تنازلي"
+  PAGE_TITLE: "موضوع عنوان الصفحة"
+  PAGE_TITLE_HELP: "عنوان الصفحة"
+  PAGE: "صفحة"
+  FILENAME: "اسم الملف"
+  PARENT_PAGE: "الصفحة الأصل"
+  HOME_PAGE: "الصفحة الرئيسية"
+  TIMEZONE: "المنطقة الزمنية"
+  LANGUAGES: "اللغات"
+  EXPIRES: "انتهاء الصلاحية"
+  LAST_MODIFIED: "آخر تعديل"
+  SESSION: "الجلسة"
+  CURRENT: "الحالي"
+  SAVE_AS: "حفظ كـ"
+  AND: "و"
+  FULLY_UPDATED: "تم تحديث النظام بالكامل"
+  GROUPS: "الفِرَق"
+  SESSION_SECURE: "آمن"
+  ADD_FOLDER: "إضافة مجلد"
+  ADD_ITEM: "إضافة عنصر"
+  LOADING: "جار التحميل …"
+  PACKAGES_SUCCESSFULLY_UPDATED: "تم تحديث حزمة أو حزمات بنجاح."
+  INSERT: "إدراج"
+  UNDO: "تراجع"
+  REDO: "إعادة"
+  HEADERS: "العناوين الرأسية"
+  ITALIC: "مائل"
+  LINK: "رابط"
+  IMAGE: "صورة"
+  PREVIEW: "معاينة"
+  PUBLISHING: "النشر"
+  IMAGE_OPTIONS: "خيارات الصورة"
+  ALL: "الكل"
+  FROM: "من"
+  TO: "إلى"
+  RELEASE_DATE: "تاريخ الإصدار"
+  DROPZONE_REMOVE_FILE: "إزالة الملف"
+  TOOLS: "الأدوات"
+  2FA_CODE_INPUT: "000000"
+  2FA_REGENERATE: "إعادة التوليد"
+  CONFIGURATION: "الإعدادات"
+  DASHBOARD: "لوحة المعلومات"

+ 356 - 0
plugins/admin/languages/bg.yaml

@@ -0,0 +1,356 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "Това е Бета версия! Използвате на ваша отговорност..."
+  ADMIN_REPORT_ISSUE: "Открили сте проблем? Моля, съобщете за него в GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\">Задвижван от Grav</a> - Модерният Флат Файл CMS"
+  LOGIN_BTN: "Вход"
+  LOGIN_BTN_FORGOT: "Забравена парола"
+  LOGIN_BTN_RESET: "Промяна на паролата"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Изпращане на инструкциите за възстановяването"
+  LOGIN_BTN_CLEAR: "Изтриване на формуляра"
+  LOGIN_BTN_CREATE_USER: "Създаване на потребител"
+  LOGIN_LOGGED_IN: "Влязохте успешно"
+  LOGIN_FAILED: "Влизането не е успешно"
+  LOGGED_OUT: "Излязохте от системата"
+  RESET_NEW_PASSWORD: "Въведете нова парола &hellip;"
+  RESET_LINK_EXPIRED: "Връзката за нулиране е изтекла, опитайте отново"
+  RESET_PASSWORD_RESET: "Паролата е променена"
+  RESET_INVALID_LINK: "Използвана невалидна връзка за нулиране, моля опитайте отново"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "На Вашия имейл, бяха изпратени инструкции за възстановяване на паролата"
+  FORGOT_FAILED_TO_EMAIL: "Неуспешно изпращане на имейл с инструкции, опитайте по-късно"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Паролата на %s не може да бъде обновена, няма въведен имейл адрес"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Потребител с име <b>%s</b> не съществува"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Не може да обнови паролата. Този сайт не е конфигуриран да изпраща имейли"
+  FORGOT_EMAIL_SUBJECT: "%s - искане за смяна на парола"
+  FORGOT_EMAIL_BODY: "<h1>Смяна на парола</h1><p>Уважаеми %1$s</p><p>На <b>%4$s</b> бе направено искане за смяна на парола.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Натиснете тук за да обновите паролата си</a><br /><br /></p><p>Като алтернатива, можете да копирате линка в адресната лента на браузъра си:</p><p>%2$s</p><p><br />С уважение,<br /><br />%3$s</p>"
+  MANAGE_PAGES: "Управление на страниците"
+  PAGES: "Страници"
+  PLUGINS: "Разширения"
+  PLUGIN: "Разширение"
+  THEMES: "Теми"
+  LOGOUT: "Изход"
+  BACK: "Назад"
+  NEXT: "Напред"
+  PREVIOUS: "Назад"
+  ADD_PAGE: "Добавяне на страница"
+  MOVE: "Преместване"
+  DELETE: "Изтриване"
+  UNSET: "Незададен"
+  VIEW: "Виж"
+  SAVE: "Запазване"
+  NORMAL: "Обикновен"
+  EXPERT: "Експертен"
+  EXPAND_ALL: "Разгъване на всички"
+  COLLAPSE_ALL: "Свиване на всички"
+  ERROR: "Грешка"
+  CLOSE: "Затваряне"
+  CANCEL: "Отказ"
+  CONTINUE: "Продължаване"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Изисква се потвърждение"
+  MODAL_CHANGED_DETECTED_TITLE: "Засечени са промени"
+  MODAL_CHANGED_DETECTED_DESC: "Имате незапазени промени. Наистина ли искате да излезете без да сте ги запазили?"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Изисква се потвърждение"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Наистина ли искате да изтриете този файл? Това действие не може да бъде отменено."
+  ADD_FILTERS: "Добавяне на филтри"
+  SEARCH_PAGES: "Търсене"
+  VERSION: "Версия"
+  WAS_MADE_WITH: "е създаден с"
+  BY: "от"
+  UPDATE_THEME: "Актуализация на тема"
+  UPDATE_PLUGIN: "Актуализация на разширение"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "на тази тема е наличен"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "на този плъгин е вече наличен"
+  AUTHOR: "Автор"
+  HOMEPAGE: "Страница"
+  DEMO: "Демо"
+  BUG_TRACKER: "Докладване за грешки"
+  KEYWORDS: "Ключови думи"
+  LICENSE: "Лиценз"
+  DESCRIPTION: "Описание"
+  README: "Документация"
+  REMOVE_THEME: "Премахване на тема"
+  INSTALL_THEME: "Инсталиране на тема"
+  THEME: "Тема"
+  BACK_TO_THEMES: "Обратно към темите"
+  BACK_TO_PLUGINS: "Обратно към разширенията"
+  CHECK_FOR_UPDATES: "Проверка за актуализации"
+  ADD: "Добавяне"
+  CLEAR_CACHE: "Изтриване на временните файлове"
+  CLEAR_CACHE_ALL_CACHE: "Всички"
+  CLEAR_CACHE_ASSETS_ONLY: "Само Assets"
+  CLEAR_CACHE_IMAGES_ONLY: "Само изображенията"
+  CLEAR_CACHE_CACHE_ONLY: "Само временните файлове"
+  CLEAR_CACHE_TMP_ONLY: "само временен"
+  UPDATES_AVAILABLE: "Налични актуализации"
+  DAYS: "Дни"
+  UPDATE: "Актуализация"
+  BACKUP: "Резервно копие"
+  BACKUPS: "Резервно копие"
+  BACKUP_NOW: "Създай резервно копие"
+  BACKUPS_STATS: "Статистика резервни копия"
+  BACKUPS_HISTORY: "История резервни копия"
+  BACKUPS_PURGE_CONFIG: "Настройка изтриване на резервно копие"
+  BACKUPS_PROFILES: "Профили на резервни копия"
+  BACKUPS_COUNT: "Брой резервни копия"
+  BACKUPS_PROFILES_COUNT: "Брой профили"
+  BACKUPS_TOTAL_SIZE: "Използвано пространство"
+  BACKUPS_NEWEST: "Най-ново резервно копие"
+  BACKUPS_OLDEST: "Най-старо резервно копие"
+  BACKUPS_PURGE: "Изтриване"
+  BACKUPS_NOT_GENERATED: "Все още не са генериране резервни копия..."
+  BACKUPS_PURGE_NUMBER: "Използва %s от %s слота за резервни копия"
+  BACKUPS_PURGE_TIME: "Остават %s дни за резервно копие"
+  BACKUPS_PURGE_SPACE: "Използва %s от %s"
+  BACKUP_DELETED: "Успешно изтрито резервно копие"
+  BACKUP_NOT_FOUND: "Не е намерено резервно копие"
+  BACKUP_DATE: "Данни за резервно копие"
+  STATISTICS: "Статистика"
+  TODAY: "Днес"
+  WEEK: "Седмица"
+  MONTH: "Месец"
+  LATEST_PAGE_UPDATES: "Скоро актуализирани страници"
+  MAINTENANCE: "Техническа поддръжка"
+  UPDATED: "Актуализиран"
+  MON: "пон"
+  TUE: "вт"
+  WED: "ср"
+  THU: "чт"
+  FRI: "пт"
+  SAT: "сб"
+  SUN: "нд"
+  COPY: "Копиране"
+  EDIT: "Редактиране"
+  CREATE: "Създаване"
+  GRAV_ADMIN: "Grav Admin"
+  GRAV_OFFICIAL_PLUGIN: "Официално разширение на Grav"
+  GRAV_OFFICIAL_THEME: "Официална тема на Grav"
+  PLUGIN_SYMBOLICALLY_LINKED: "Този плъгин е символично свързан. Актуализации няма да бъдат отразени."
+  THEME_SYMBOLICALLY_LINKED: "Тази тема е символично свързана. Актуализации няма да бъдат отразени."
+  REMOVE_PLUGIN: "Премахване на разширение"
+  INSTALL_PLUGIN: "Инсталиране на разширение"
+  AVAILABLE: "Налични"
+  INSTALLED: "Инсталирани"
+  INSTALL: "Инсталиране"
+  ACTIVE_THEME: "Активна тема"
+  SWITCHING_TO: "Превключване към"
+  SWITCHING_TO_DESCRIPTION: "При превключването към различна тема няма гаранция, че всички страници са поддържани, което може да доведе до потенциални грешки при опит за зареждане на тези страници."
+  SWITCHING_TO_CONFIRMATION: "Искате ли да продължите и да превключите към темата"
+  CREATE_NEW_USER: "Създаване на нов потребител"
+  REMOVE_USER: "Премахване на потребител"
+  ACCESS_DENIED: "Нямате достъп"
+  ACCOUNT_NOT_ADMIN: "вашият профил няма администраторски права"
+  PHP_INFO: "Информация за PHP"
+  INSTALLER: "Инсталатор"
+  AVAILABLE_THEMES: "Налични теми"
+  AVAILABLE_PLUGINS: "Налични разширения"
+  INSTALLED_THEMES: "Инсталирани теми"
+  INSTALLED_PLUGINS: "Инсталирани разширения"
+  BROWSE_ERROR_LOGS: "Преглед на дневниците за грешки"
+  SITE: "Сайт"
+  INFO: "Информация"
+  SYSTEM: "Система"
+  USER: "Потребител"
+  ADD_ACCOUNT: "Добавяне на профил"
+  SWITCH_LANGUAGE: "Превключване на езика"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Разширението е активирано успешно"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Разширението е спряно"
+  SUCCESSFULLY_CHANGED_THEME: "Промяната на подразбиращата се тема е успешно"
+  INSTALLATION_FAILED: "Неуспешна инсталация"
+  INSTALLATION_SUCCESSFUL: "Инсталацията е успешна"
+  UNINSTALL_FAILED: "Неуспешно деинсталиране"
+  UNINSTALL_SUCCESSFUL: "Деинсталирането е успешно"
+  SUCCESSFULLY_SAVED: "Успешно запазено"
+  SUCCESSFULLY_COPIED: "Успешно копирано"
+  REORDERING_WAS_SUCCESSFUL: "Записът бе успешен"
+  SUCCESSFULLY_DELETED: "Успешно изтрити"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Езикът е променен успешно"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "Нямате достатъчно права за тази задача"
+  CACHE_CLEARED: "Временните файлове са изчистени"
+  METHOD: "Метод"
+  ERROR_CLEARING_CACHE: "Грешка при изтриването на временните файлове"
+  AN_ERROR_OCCURRED: "Възникна грешка"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Резервното копие е готово за изтегляне"
+  DOWNLOAD_BACKUP: "Изтегляне на резервното копие"
+  PAGES_FILTERED: "Филтрирани страници"
+  NO_PAGE_FOUND: "Няма намерени страници"
+  INVALID_PARAMETERS: "Невалидни параметри"
+  NO_FILES_SENT: "Няма изпратени файлове"
+  EXCEEDED_FILESIZE_LIMIT: "Надхвърлен лимит за размер на PHP конфигурационен файл"
+  UNKNOWN_ERRORS: "Неизвестни грешки"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Превишен лимит за размера на конфигурационен GRAV файл"
+  UNSUPPORTED_FILE_TYPE: "Този файлов формат не се поддържа"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Преместването на качения файл не е успешно."
+  FILE_UPLOADED_SUCCESSFULLY: "Файлът е качен успешно"
+  FILE_DELETED: "Файлът е изтрит"
+  FILE_COULD_NOT_BE_DELETED: "Файлът не може да бъде изтрит"
+  FILE_NOT_FOUND: "Файлът не е намерен"
+  NO_FILE_FOUND: "Няма намерени файлове"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav беше успешно актуализиран до"
+  GRAV_UPDATE_FAILED: "Актуализацията на Grav е неуспешна"
+  EVERYTHING_UPDATED: "Всичко е актуализирано"
+  UPDATES_FAILED: "Актуализациите не бяха успшено"
+  AVATAR_BY: "Аватар от"
+  AVATAR_UPLOAD_OWN: "Или качи собствени..."
+  LAST_BACKUP: "Последно резервно копие"
+  FULL_NAME: "Пълно име"
+  USERNAME: "Потребителско име"
+  EMAIL: "Ел. поща"
+  USERNAME_EMAIL: "Потребителско име или инейл"
+  PASSWORD: "Парола"
+  PASSWORD_CONFIRM: "Потвърждение на паролата"
+  TITLE: "Титла"
+  LANGUAGE: "Език"
+  ACCOUNT: "Профил"
+  EMAIL_VALIDATION_MESSAGE: "Ел. поща трябва да бъде валидна"
+  PASSWORD_VALIDATION_MESSAGE: "Паролата трябва да съдържа поне един номер, една главна буква, една малка буква и да съдържа поне 8 или повече знака"
+  LANGUAGE_HELP: "Задаване на любим език"
+  MEDIA: "Медиа"
+  DEFAULTS: "По подразбиране"
+  SITE_TITLE: "Заглавие на сайта"
+  SITE_TITLE_PLACEHOLDER: "Заглавие за всички страници"
+  SITE_TITLE_HELP: "Подразбиращо се заглавие за вашият сайт, често се използва от темите"
+  SITE_DEFAULT_LANG: "Език по подразбиране"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Език по подразбиране, използван от <HTML> тага на темите"
+  SITE_DEFAULT_LANG_HELP: "Език по подразбиране, използван от <HTML> тага на темите"
+  DEFAULT_AUTHOR: "Подразбиращ се автор"
+  DEFAULT_AUTHOR_HELP: "Име на автор по подразбиране, често използвано в теми или страници"
+  DEFAULT_EMAIL: "Имейл по подразбиране"
+  DEFAULT_EMAIL_HELP: "Имейл по подразбиране, използван в теми или страници"
+  TAXONOMY_TYPES: "Видове таксономии"
+  TAXONOMY_TYPES_HELP: "Типовете таксономия трябва да бъдат дефинирани тук, ако искате да ги използвате в страници"
+  PAGE_SUMMARY: "Резюме на страницата"
+  ENABLED: "Включен"
+  ENABLED_HELP: "Разреши извлечение (извлечението връща същото съдържание, като в страницата)"
+  'YES': "Да"
+  'NO': "Не"
+  SUMMARY_SIZE: "Размер на резюмето"
+  SUMMARY_SIZE_HELP: "Брой знаци, които да бъдат използвани при създаването на резюме за страницата"
+  FORMAT: "Формат"
+  SHORT: "Къс"
+  LONG: "Дълъг"
+  DELIMITER: "Делител"
+  DELIMITER_HELP: "Разделител на извлечението (по подразбиране '===')"
+  METADATA: "Мета-данни"
+  METADATA_HELP: "Ще бъдат показани метадата стойностите по подразбиране на всяка страница, освен ако не са отхвърлени от страницата"
+  NAME: "Име"
+  CONTENT: "Съдържание"
+  SIZE: "Размер"
+  ACTION: "Действие"
+  REDIRECTS_AND_ROUTES: "Пренасочвания и пътища"
+  CUSTOM_REDIRECTS: "Потребителски пренасочвания"
+  CUSTOM_REDIRECTS_HELP: "маршрути за пренасочване към сдруга страница. Подмяна със стандартен Regex е валидна"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/your/redirect"
+  CUSTOM_ROUTES: "Потребителски пренасочвания"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/your/route"
+  FILE_STREAMS: "Потоци файлове"
+  DEFAULT: "По подразбиране"
+  PAGE_MEDIA: "Страница с медия"
+  OPTIONS: "Опции"
+  PUBLISHED: "Публикувано"
+  PUBLISHED_HELP: "По подразбиране страницата се публикува, освен ако не е зададео published: false или publish_date е в бъдеще или unpublish_date е в миналото"
+  DATE: "Дата"
+  DATE_HELP: "Променливата за дата позволява да се зададе дата асоциирана със страницата."
+  PUBLISHED_DATE: "Дата на публикуване"
+  PUBLISHED_DATE_HELP: "Дата, на която автоматично ще се публикува."
+  UNPUBLISHED_DATE: "Дата на непубликуване"
+  UNPUBLISHED_DATE_HELP: "Може да зададе дата за автоматично непубликуване."
+  ROBOTS: "Роботи"
+  TAXONOMIES: "Таксономии"
+  TAXONOMY: "Таксономия"
+  ADVANCED: "Разширено"
+  SETTINGS: "Настройки"
+  FOLDER_NUMERIC_PREFIX: "Цифров префикс на папка"
+  FOLDER_NUMERIC_PREFIX_HELP: "Цифров префикс осигуряващ ръчно подреждане и по-добра видимост"
+  FOLDER_NAME: "Име на папка"
+  FOLDER_NAME_HELP: "Името на папката, която ще се съхрани във файлова система за тази страница"
+  PARENT: "Родител"
+  DEFAULT_OPTION_ROOT: "- Коренова папка -"
+  DEFAULT_OPTION_SELECT: "- Избор -"
+  DISPLAY_TEMPLATE: "Показване на шаблон"
+  ORDERING: "Подреждане"
+  PAGE_ORDER: "Подредба на страниците"
+  OVERRIDES: "Замени"
+  MENU: "Меню"
+  MENU_HELP: "Стрингът, който ще се използва в меню. Ако не се зададе, ще се използва Title."
+  SLUG: "Слъг"
+  SLUG_HELP: "Слъг променливата Ви позволява да зададете конкретна порция от URL на страницата"
+  SLUG_VALIDATE_MESSAGE: "Слъговете могат да съдържат само малки букви, цифри и тирета"
+  PROCESS: "Обработване"
+  PROCESS_HELP: "Контролирайте обработката на страниците. Може да бъде за отделна страница или глобално"
+  DEFAULT_CHILD_TYPE: "Тип по подразбиране"
+  USE_GLOBAL: "Използвай глобални"
+  ROUTABLE: "Маршрутизируем"
+  ROUTABLE_HELP: "Ако страницата е достъпна през URL"
+  CACHING: "Създаване на временни файлове"
+  VISIBLE: "Видим"
+  VISIBLE_HELP: "Определя дали една страница е видима в навигацията."
+  DISABLED: "Изключено"
+  ITEMS: "Елементи"
+  ORDER_BY: "Подреждане по"
+  ORDER: "Подреждане"
+  FOLDER: "Папка"
+  ASCENDING: "Възходящо"
+  DESCENDING: "Низходящо"
+  PAGE_TITLE: "Заглавие на страницата"
+  PAGE_TITLE_HELP: "Заглавието на страницата"
+  PAGE: "Страница"
+  FRONTMATTER: "Встъпление"
+  FILENAME: "Име на файла"
+  PARENT_PAGE: "Родителска страница"
+  HOME_PAGE: "Начална страница"
+  HOME_PAGE_HELP: "Страницата, която Grav ще използва по подразбиране за начална страница"
+  DEFAULT_THEME: "Тема по подразбиране"
+  DEFAULT_THEME_HELP: "Задаване на темата по подразбиране, която Grav ще използва (по подразбиране това е Antimatter)"
+  TIMEZONE: "Часова зона"
+  TIMEZONE_HELP: "Презаписване на времевата зона на сървъра"
+  SHORT_DATE_FORMAT: "Кратък формат дата"
+  SHORT_DATE_FORMAT_HELP: "Задай кратък формат дата, който може да се използва от темите"
+  LONG_DATE_FORMAT: "Пълен формат дата"
+  LONG_DATE_FORMAT_HELP: "Задай пълен формат дата, който може да се използва от темите"
+  DEFAULT_ORDERING: "По подразбиране"
+  DEFAULT_ORDERING_DEFAULT: "По подразбиране - според име на папка"
+  DEFAULT_ORDERING_FOLDER: "Папка - според името на папката без префикс"
+  DEFAULT_ORDERING_TITLE: "Заглавие - според заглавно поле в главата"
+  DEFAULT_ORDERING_DATE: "Дата - според поле за дата в главата"
+  DEFAULT_ORDER_DIRECTION: "Подреждане по подразбиране"
+  DEFAULT_ORDER_DIRECTION_HELP: "Посоката на страниците в списък"
+  DEFAULT_PAGE_COUNT: "Брой страници по подразбиране"
+  DEFAULT_PAGE_COUNT_HELP: "Максимален брой страници в списък по подразбиране"
+  DATE_BASED_PUBLISHING: "Публикуване според датата"
+  DATE_BASED_PUBLISHING_HELP: "Автоматично (не)публикувай постове според датата"
+  EVENTS: "Събития"
+  EVENTS_HELP: "Пускане или спиране на специфични събития. Спирането на някои събития може да счупи определени приставки"
+  REDIRECT_DEFAULT_ROUTE: "Пренасочване на пътя по подразбиране"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Автоматично пренасочване към пътя по подразбиране на страницата"
+  LANGUAGES: "Езици"
+  SUPPORTED: "Поддържани"
+  SUPPORTED_HELP: "Списък от двубуквени езикови кодове, отделени със запетая (пример 'bg,en,de')"
+  HTTP_HEADERS: "HTTP заглавки"
+  EXPIRES: "Изтича на"
+  CACHE_CONTROL: "HTTP кеш-контрол"
+  LAST_MODIFIED: "Последна промяна"
+  CACHE_CHECK_METHOD: "Мотод проверка на кеша"
+  CACHE_DRIVER: "Кеш драйвър"
+  CACHE_PREFIX: "Кеш представка"
+  CACHE_PURGE: "Изтриване на стр кеш"
+  LIFETIME: "Продължителност на живот"
+  GZIP_COMPRESSION: "Gzip компресия"
+  GZIP_COMPRESSION_HELP: "Разреши GZip компресия на Grav страницата, за оптимизация."
+  CSS_PIPELINE: "CSS pipeline"
+  JAVASCRIPT_PIPELINE: "JavaScript pipeline"
+  LOG_HANDLER: "Обработка на лога"
+  DEBUGGER: "Дибъгър"
+  SESSION: "Сесия"
+  CURRENT: "Текущ"
+  SAVE_AS: "Запази като"
+  AND: "и"
+  UPDATE_AVAILABLE: "Налична актуализация"
+  FULLY_UPDATED: "Напълно обновен"
+  SAVE_LOCATION: "Местоположение за запис"
+  IGNORE_HIDDEN_HELP: "Игнорирай всички файлове и папки, които започват с точка"
+  WRAPPED_SITE: "Опаковани сайт"
+  CONFIGURATION: "Настройки"
+  TIMEOUT: "Таймаут"
+  DASHBOARD: "Контролен панел"

+ 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"

+ 710 - 0
plugins/admin/languages/el.yaml

@@ -0,0 +1,710 @@
+---
+PLUGIN_ADMIN:
+  ADMIN_BETA_MSG: "Αυτή είναι δοκιμαστική έκδοση (beta)! Χρησιμοποιήστε την στην παραγωγή με δική σας ευθύνη..."
+  ADMIN_REPORT_ISSUE: "Βρήκατε κάποιο πρόβλημα; Παρακαλείστε να το αναφέρετε στο GitHub."
+  EMAIL_FOOTER: "<a href=\"https://getgrav.org\"> με την ισχύ του Grav</a> - Το μοντέρνο CMS χωρίς τη χρήση Βάσης Δεδομένων"
+  LOGIN_BTN: "Σύνδεση"
+  LOGIN_BTN_FORGOT: "Το ξέχασα"
+  LOGIN_BTN_RESET: "Επαναφορά συνθηματικού"
+  LOGIN_BTN_SEND_INSTRUCTIONS: "Αποστολή οδηγιών επαναφοράς"
+  LOGIN_BTN_CLEAR: "Καθαρισμός Φόρμας"
+  LOGIN_BTN_CREATE_USER: "Δημιουργία χρήστη"
+  LOGIN_LOGGED_IN: "Έχετε συνδεθεί με επιτυχία"
+  LOGIN_FAILED: "Αποτυχία Σύνδεσης"
+  LOGGED_OUT: "Έχετε αποσυνδεθεί"
+  RESET_NEW_PASSWORD: "Παρακαλούμε εισαγάγετε νέο συνθηματικό &hellip;"
+  RESET_LINK_EXPIRED: "Ο σύνδεσμος επαναφοράς έχει λήξει, παρακαλώ προσπαθήστε ξανά"
+  RESET_PASSWORD_RESET: "Έχει γίνει επαναφορά του συνθηματικού"
+  RESET_INVALID_LINK: "Ο σύνδεσμος γιά την επαναφορά δεν είναι έγκυρος, παρακαλώ προσπαθήστε πάλι"
+  FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Οι οδηγίες για να επαναφέρετε το συνθηματικό σας έχουν σταλεί στη διεύθυνση ηλεκτρονικού σας ταχυδρομείου"
+  FORGOT_FAILED_TO_EMAIL: "Απέτυχε η αποστολή οδηγιών με email, παρακαλώ προσπαθήστε ξανά αργότερα"
+  FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Δεν είναι δυνατή η επαναφορά του συνθηματικού για %s, δεν έχει καθοριστεί διεύθυνση ηλεκτρονικού ταχυδρομείου"
+  FORGOT_USERNAME_DOES_NOT_EXIST: "Χρήστης με όνομα χρήστη <b>%s</b> δεν υπάρχει"
+  FORGOT_EMAIL_NOT_CONFIGURED: "Δεν είναι δυνατή η επαναφορά του συνθηματικού σας. Αυτός ο ιστότοπος δεν έχει ρυθμιστεί για αποστολή μηνυμάτων ηλεκτρονικού ταχυδρομείου"
+  FORGOT_EMAIL_SUBJECT: "%s Αίτηση Επαναφοράς Κωδικού Πρόσβασης"
+  FORGOT_EMAIL_BODY: "<h1>Επαναφορά συνθηματικού</h1> <p>Αγαπητέ/ή %1$s,</p> <p>Έγινε μία αίτηση στις <b>%4$s</b> για επαναφορά του συνθηματικού σας.</p> <p>< br / > <a href=\"%2$s\" class=\"btn-primary\"> Κάντε κλικ εδώ για να επαναφέρετε το συνθηματικό σας</a> < br / > < br / ></p> <p>Εναλλακτικά, αντιγράψτε την ακόλουθη διεύθυνση URL στη γραμμή διευθύνσεων του προγράμματος περιήγησής σας:</p> <p>%2$s</p> <p>< br / > Με εκτίμηση, < br / > < br / >%3$s</p>"
+  MANAGE_PAGES: "Διαχείριση σελίδων"
+  PAGES: "Σελίδες"
+  PLUGINS: "Πρόσθετα"
+  PLUGIN: "Πρόσθετο"
+  THEMES: "Θέματα"
+  LOGOUT: "Αποσύνδεση"
+  BACK: "Επιστροφή"
+  NEXT: "Επόμενο"
+  PREVIOUS: "Προηγούμενο"
+  ADD_PAGE: "Προσθήκη σελίδας"
+  MOVE: "Μετακίνηση"
+  DELETE: "Διαγραφή"
+  UNSET: "Μη ορισμένο"
+  VIEW: "Προβολή"
+  SAVE: "Αποθήκευση"
+  NORMAL: "Κανονικό"
+  EXPERT: "Ειδικός"
+  EXPAND_ALL: "Ανάπτυξη Όλων"
+  COLLAPSE_ALL: "Σύμπτυξη όλων"
+  ERROR: "Σφάλμα"
+  CLOSE: "Κλείσιμο"
+  CANCEL: "Ακύρωση"
+  CONTINUE: "Συνέχεια"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Απαιτείται Επιβεβαίωση"
+  MODAL_CHANGED_DETECTED_TITLE: "Εντοπίστηκαν Αλλαγές"
+  MODAL_CHANGED_DETECTED_DESC: "Έχετε μη αποθηκευμένες αλλαγές. Είστε βέβαιοι ότι θέλετε να φύγετε χωρίς να γίνει αποθήκευση;"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Απαιτείται Επιβεβαίωση"
+  MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το αρχείο; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."
+  ADD_FILTERS: "Προσθήκη φίλτρων"
+  SEARCH_PAGES: "Αναζήτηση στις σελίδες"
+  VERSION: "Έκδοση"
+  WAS_MADE_WITH: "Δημιουργήθηκε με"
+  BY: "Από"
+  UPDATE_THEME: "Ενημέρωση Θέματος"
+  UPDATE_PLUGIN: "Ενημέρωση Προσθέτου"
+  OF_THIS_THEME_IS_NOW_AVAILABLE: "αυτού του θέματος είναι τώρα διαθέσιμη"
+  OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "αυτού του πρόσθετου είναι τώρα διαθέσιμη"
+  AUTHOR: "Συντάκτης"
+  HOMEPAGE: "Αρχική σελίδα"
+  DEMO: "Επίδειξη"
+  BUG_TRACKER: "Παρακολούθηση Σφαλμάτων"
+  KEYWORDS: "Λέξεις κλειδιά"
+  LICENSE: "Άδεια Χρήσης"
+  DESCRIPTION: "Περιγραφή"
+  README: "Διάβασέ με"
+  REMOVE_THEME: "Κατάργηση Θέματος"
+  INSTALL_THEME: "Εγκατάσταση Θέματος"
+  THEME: "Θέμα"
+  BACK_TO_THEMES: "Πίσω στα Θέματα"
+  BACK_TO_PLUGINS: "Πίσω στα Πρόσθετα"
+  CHECK_FOR_UPDATES: "Ελέγχος για Ενημερώσεις"
+  ADD: "Προσθήκη"
+  CLEAR_CACHE: "Εκκαθάριση προσωρινής μνήμης"
+  CLEAR_CACHE_ALL_CACHE: "Όλης της Cache"
+  CLEAR_CACHE_ASSETS_ONLY: "Μόνον οι διαθέσιμοι πόροι"
+  CLEAR_CACHE_IMAGES_ONLY: "Μόνον των Εικόνων"
+  CLEAR_CACHE_CACHE_ONLY: "Μόνον τη μνήμη cache"
+  CLEAR_CACHE_TMP_ONLY: "Tmp μόνο"
+  UPDATES_AVAILABLE: "Υπάρχουν Διαθέσιμες Ενημερώσεις"
+  DAYS: "Ημέρες"
+  UPDATE: "Ενημέρωση"
+  BACKUP: "Δημιουργία αντιγράφου ασφαλείας"
+  BACKUPS: "Αντίγραφα Ασφαλείας"
+  BACKUP_NOW: "Πάρτε Αντίγραφο Ασφαλείας τώρα"
+  BACKUPS_STATS: "Στατιστικά Αντιγράφων Ασφαλείας"
+  BACKUPS_HISTORY: "Ιστορικό Αντιγράφων Ασφαλείας"
+  BACKUPS_PURGE_CONFIG: "Ρυθμίσεις Καθαρισμού Αντιγράφων Ασφαλείας"
+  BACKUPS_PROFILES: "Προφίλ Αντιγράφων Ασφαλείας"
+  BACKUPS_COUNT: "Αριθμός των Αντιγράφων Ασφαλείας"
+  BACKUPS_PROFILES_COUNT: "Αριθμός των Προφίλ"
+  BACKUPS_TOTAL_SIZE: "Χώρος Χρήσης"
+  STATISTICS: "Στατιστικά"
+  TODAY: "Σήμερα"
+  WEEK: "Εβδομάδα"
+  MONTH: "Μήνας"
+  LATEST_PAGE_UPDATES: "Πιο πρόσφατες ενημερώσεις της σελίδας"
+  MAINTENANCE: "Συντήρηση"
+  UPDATED: "Ενημερωμένο"
+  MON: "Δευ"
+  TUE: "Τρί"
+  WED: "Τετ"
+  THU: "Πέμ"
+  FRI: "Παρ"
+  SAT: "Σάβ"
+  SUN: "Κυρ"
+  COPY: "Αντιγραφή"
+  EDIT: "Επεξεργασία"
+  CREATE: "Δημιουργία"
+  GRAV_ADMIN: "Διαχειριστής του Grav"
+  GRAV_OFFICIAL_PLUGIN: "Επίσημο πρόσθετο του Grav"
+  GRAV_OFFICIAL_THEME: "Επίσημο Θέμα του Grav"
+  PLUGIN_SYMBOLICALLY_LINKED: "Αυτό το πρόσθετο είναι συμβολικά συνδεδεμένο. Οποιεσδήποτε ενημερώσεις δεν θα εντοπιστούν."
+  THEME_SYMBOLICALLY_LINKED: "Αυτό το θέμα είναι συμβολικά συνδεδεμένο. Οποιεσδήποτε ενημερώσεις δεν θα εντοπιστούν"
+  REMOVE_PLUGIN: "Αφαιρέστε το Πρόσθετο"
+  INSTALL_PLUGIN: "Εγκατάσταση Πρόσθετου"
+  AVAILABLE: "Διαθέσιμα"
+  INSTALLED: "Εγκατεστημένα"
+  INSTALL: "Εγκατάσταση"
+  ACTIVE_THEME: "Ενεργό Θέμα"
+  SWITCHING_TO: "Μετάβαση σε"
+  SWITCHING_TO_DESCRIPTION: "Εάν αλλάξετε το θέμα, δεν υπάρχει καμία εγγύηση ότι θα υποστηρίζονται όλα τα layouts σελίδων, ενδεχομένως προκαλώντας σφάλματα όταν θα φορτώσουν οι σελίδες αυτές."
+  SWITCHING_TO_CONFIRMATION: "Θέλετε να συνεχίσετε και να μεταβείτε στο θέμα"
+  CREATE_NEW_USER: "Δημιουργία νέου χρήστη"
+  REMOVE_USER: "Διαγραφή χρήστη"
+  ACCESS_DENIED: "Απαγορεύεται η Πρόσβαση"
+  ACCOUNT_NOT_ADMIN: "ο λογαριασμός σας δεν έχει δικαιώματα διαχειριστή"
+  PHP_INFO: "Πληροφορίες της PHP"
+  INSTALLER: "Πρόγραμμα εγκατάστασης"
+  AVAILABLE_THEMES: "Διαθέσιμα Θέματα"
+  AVAILABLE_PLUGINS: "Διαθέσιμα πρόσθετα"
+  INSTALLED_THEMES: "Εγκατεστημένα Θέματα"
+  INSTALLED_PLUGINS: "Εγκατεστημένα πρόσθετα"
+  BROWSE_ERROR_LOGS: "Περιήγηση στα αρχεία σφαλμάτων"
+  SITE: "Ιστότοπος"
+  INFO: "Πληροφορίες"
+  SYSTEM: "Σύστημα"
+  USER: "Χρήστης"
+  ADD_ACCOUNT: "Προσθήκη λογαριασμού"
+  SWITCH_LANGUAGE: "Εναλλαγή γλώσσας"
+  SUCCESSFULLY_ENABLED_PLUGIN: "Το πρόσθετο ενεργοποιήθηκε με επιτυχία"
+  SUCCESSFULLY_DISABLED_PLUGIN: "Το πρόσθετο απενεργοποιήθηκε με επιτυχία"
+  SUCCESSFULLY_CHANGED_THEME: "Έγινε επιτυχής αλλαγή του προεπιλεγμένου θέματος"
+  INSTALLATION_FAILED: "Η εγκατάσταση απέτυχε"
+  INSTALLATION_SUCCESSFUL: "Επιτυχής εγκατάσταση"
+  UNINSTALL_FAILED: "Η απεγκατάσταση έχει αποτύχει"
+  UNINSTALL_SUCCESSFUL: "Η απεγκατάσταση έγινε με επιτυχία"
+  SUCCESSFULLY_SAVED: "Επιτυχής αποθήκευση"
+  SUCCESSFULLY_COPIED: "Επιτυχής αντιγραφή"
+  REORDERING_WAS_SUCCESSFUL: "Η αναδιάταξη ήταν επιτυχής"
+  SUCCESSFULLY_DELETED: "Επιτυχής διαγραφή"
+  SUCCESSFULLY_SWITCHED_LANGUAGE: "Επιτυχία στην αλλαγή γλώσσας"
+  INSUFFICIENT_PERMISSIONS_FOR_TASK: "Έχετε ανεπαρκή δικαιώματα για την εργασία"
+  CACHE_CLEARED: "Η προσωρινή μνήμη καθάρισε"
+  METHOD: "Μέθοδος"
+  ERROR_CLEARING_CACHE: "Σφάλμα κατά την εκκαθάριση της μνήμης cache"
+  AN_ERROR_OCCURRED: "Παρουσιάστηκε ένα σφάλμα"
+  YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Το αντίγραφο ασφαλείας είναι έτοιμο για λήψη"
+  DOWNLOAD_BACKUP: "Λήψη αντιγράφου ασφαλείας"
+  PAGES_FILTERED: "Σελίδες που φιλτραρίστηκαν"
+  NO_PAGE_FOUND: "Δεν βρέθηκαν σελίδες"
+  INVALID_PARAMETERS: "Μη έγκυρες παράμετροι"
+  NO_FILES_SENT: "Δεν απεστάλη κανένα αρχείο"
+  EXCEEDED_FILESIZE_LIMIT: "Υπέρβαση ορίου μεγέθους αρχείων ρύθμισης παραμέτρων της PHP"
+  UNKNOWN_ERRORS: "Άγνωστο Σφάλμα"
+  EXCEEDED_GRAV_FILESIZE_LIMIT: "Το αρχείο υπερβαίνει το προκαθορισμένο όριο μεγέθους"
+  UNSUPPORTED_FILE_TYPE: "Μη υποστηριζόμενος τύπος αρχείου"
+  FAILED_TO_MOVE_UPLOADED_FILE: "Απέτυχε η μετακίνηση του μεταφορτωμένου αρχείου."
+  FILE_UPLOADED_SUCCESSFULLY: "Το αρχείο μεταφορτώθηκε με επιτυχία"
+  FILE_DELETED: "Το αρχείο διεγράφη"
+  FILE_COULD_NOT_BE_DELETED: "Δεν ήταν δυνατή η διαγραφή του αρχείου"
+  FILE_NOT_FOUND: "Το αρχείο δεν βρέθηκε"
+  NO_FILE_FOUND: "Δεν βρέθηκαν αρχεία"
+  GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Το Grav ενημερώθηκε επιτυχώς σε"
+  GRAV_UPDATE_FAILED: "Η ενημέρωση του Grav απέτυχε"
+  EVERYTHING_UPDATED: "Τα πάντα έχουν ενημερωθεί"
+  UPDATES_FAILED: "Αποτυχία ενημέρωσης"
+  AVATAR_BY: "Avatar από"
+  AVATAR_UPLOAD_OWN: "Ή ανέβασε το δικό σου avatar..."
+  LAST_BACKUP: "Τελευταία δημιουργία αντιγράφων ασφαλείας"
+  FULL_NAME: "Ονοματεπώνυμο"
+  USERNAME: "Όνομα χρήστη"
+  EMAIL: "Διεύθυνση ηλεκτρονικού ταχυδρομείου"
+  USERNAME_EMAIL: "Όνομα χρήστη ή διεύθυνση ηλεκτρονικού ταχυδρομείου"
+  PASSWORD: "Συνθηματικό"
+  PASSWORD_CONFIRM: "Επιβεβαίωση συνθηματικού"
+  TITLE: "Τίτλος"
+  LANGUAGE: "Γλώσσα"
+  ACCOUNT: "Λογαριασμός"
+  EMAIL_VALIDATION_MESSAGE: "Πρέπει να είναι μια έγκυρη διεύθυνση email"
+  PASSWORD_VALIDATION_MESSAGE: "Το συνθηματικό πρέπει να περιέχει τουλάχιστον έναν αριθμό, ένα κεφαλαίο και ένα πεζό γράμμα και τουλάχιστον 8 ή περισσότερους χαρακτήρες"
+  LANGUAGE_HELP: "Ορίστε την αγαπημένη γλώσσα"
+  MEDIA: "Πολυμέσα"
+  DEFAULTS: "Προεπιλογές"
+  SITE_TITLE: "Τίτλος Ιστοχώρου"
+  SITE_TITLE_PLACEHOLDER: "Τίτλος σε όλο τον ιστοχώρο"
+  SITE_TITLE_HELP: "Προεπιλεγμένος τίτλος για το site σας, χρησιμοποιείται συχνά στα θέματα"
+  SITE_DEFAULT_LANG: "Προεπιλεγμένη γλώσσα"
+  SITE_DEFAULT_LANG_PLACEHOLDER: "Προεπιλεγμένη γλώσσα που θα χρησιμοποιείται από <HTML> tag του θέματος"
+  SITE_DEFAULT_LANG_HELP: "Προεπιλεγμένη γλώσσα που θα χρησιμοποιείται από <HTML> tag του θέματος"
+  DEFAULT_AUTHOR: "Προεπιλεγμένος συντάκτης"
+  DEFAULT_AUTHOR_HELP: "Ένα προεπιλεγμένο όνομα συντάκτη, που χρησιμοποιείται συχνά στα θέματα ή στο περιεχόμενο της σελίδας"
+  DEFAULT_EMAIL: "Προεπιλεγμένη διεύθυνση ηλεκτρονικού ταχυδρομείου"
+  DEFAULT_EMAIL_HELP: "Μία ηλεκτρονική διεύθυνση email για να χρησιμοποιηθεί σε θέματα ή σελίδες"
+  TAXONOMY_TYPES: "Τύποι ταξινομίας"
+  TAXONOMY_TYPES_HELP: "Τύποι ταξινόμησης πρέπει να οριστούν εδώ εάν θέλετε να τους χρησιμοποιήσετε σε σελίδες"
+  PAGE_SUMMARY: "Περίληψη Σελίδας"
+  ENABLED: "Ενεργοποιημένη"
+  ENABLED_HELP: "Ενεργοποίηση σύνοψης σελίδας (η σύνοψη επιστρέφει το ίδιο με το περιεχόμενο της σελίδας)"
+  'YES': "Ναι"
+  'NO': "Όχι"
+  SUMMARY_SIZE: "Μέγεθος Περίληψης"
+  SUMMARY_SIZE_HELP: "Το πλήθος των χαρακτήρων μίας σελίδας που θα χρησιμοποιηθεί για τη σύνοψη της σελίδας"
+  FORMAT: "Μορφοποίηση"
+  FORMAT_HELP: "short = χρησιμοποίησε την πρώτη εμφάνιση του delimiter ή την υπέρβαση του πλήθους χαρακτήρων; long = η σύνοψη θα αγνοηθεί"
+  SHORT: "Σύντομη"
+  LONG: "Εκτεταμένη"
+  DELIMITER: "Διαχωριστικό"
+  DELIMITER_HELP: "Ο οριοθέτης σύνοψης (προεπιλογή ' ===')"
+  METADATA: "Μεταδεδομένα"
+  METADATA_HELP: "Προεπιλεγμένες τιμές μεταδεδομένων που θα εμφανίζονται σε κάθε σελίδα εκτός εάν παρακάμπτονται από την εκάστοτε σελίδα"
+  NAME: "Όνομα"
+  CONTENT: "Περιεχόμενο"
+  REDIRECTS_AND_ROUTES: "Ανακατευθύνσεις και Διαδρομές"
+  CUSTOM_REDIRECTS: "Προσαρμοσμένες Ανακατευθύνσεις"
+  CUSTOM_REDIRECTS_HELP: "διαδρομές για την ανακατεύθυνση σε άλλες σελίδες. Η αντικατάσταση με μία κανονική παράσταση είναι έγκυρη"
+  CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/your/redirect"
+  CUSTOM_ROUTES: "Προσαρμοσμένες Διαδρομές"
+  CUSTOM_ROUTES_HELP: "διαδρομές για την ανακατεύθυνση σε άλλες σελίδες. Η αντικατάσταση με μία κανονική παράσταση είναι έγκυρη"
+  CUSTOM_ROUTES_PLACEHOLDER_KEY: "/your/alias"
+  CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/your/route"
+  FILE_STREAMS: "File Streams"
+  DEFAULT: "Προεπιλογή"
+  PAGE_MEDIA: "Σελίδα Media"
+  OPTIONS: "Επιλογές"
+  PUBLISHED: "Δημοσιευμένο"
+  PUBLISHED_HELP: "Από προεπιλογή, μια σελίδα δημοσιεύεται κατευθείαν, εκτός αν ορίσετε την επιλογή published: false, εάν το publish_date αναφέρεται σε μελλοντική στιγμή, ή αν το unpublish_date αναφέρεται στο παρελθόν"
+  DATE: "Ημερομηνία"
+  DATE_HELP: "Η μεταβλητή date σας επιτρέπει να ορίσετε συγκεκριμένα μία ημερομηνία που να σχετίζεται με τη σελίδα αυτή."
+  PUBLISHED_DATE: "Ημερομηνία δημοσίευσης"
+  PUBLISHED_DATE_HELP: "Μπορεί να δοθεί μία ημερομηνία για την αυτόματη δημοσίευση της ανάρτησης."
+  UNPUBLISHED_DATE: "Ημερομηνία παύσης δημοσίευσης"
+  UNPUBLISHED_DATE_HELP: "Μπορεί να δοθεί μία ημερομηνία για την αυτόματη ιδιωτικοποίηση της ανάρτησης."
+  ROBOTS: "Ρομπότ"
+  TAXONOMIES: "Ταξινομίες"
+  TAXONOMY: "Ταξινομία"
+  ADVANCED: "Γιά Προχωρημένους"
+  SETTINGS: "Ρυθμίσεις"
+  FOLDER_NUMERIC_PREFIX: "Αριθμητικό Πρόθεμα Φακέλου"
+  FOLDER_NUMERIC_PREFIX_HELP: "Αριθμητικό πρόθεμα που παρέχει χειροκίνητη ταξινόμηση και υπονοεί την ορατότητα"
+  FOLDER_NAME: "Όνομα φακέλου"
+  FOLDER_NAME_HELP: "Το όνομα του φακέλου που θα χρησιμοποιηθεί στο σύστημα αρχείων γι αυτή τη σελίδα"
+  PARENT: "Γονικό"
+  DEFAULT_OPTION_ROOT: "-Ρίζα-"
+  DEFAULT_OPTION_SELECT: "-Επιλέξτε-"
+  DISPLAY_TEMPLATE: "Εμφάνιση Προτύπου"
+  DISPLAY_TEMPLATE_HELP: "Το τύπος της σελίδας που ορίζει ποιο twig template θα χρησιμοποιηθεί για να αποδοθεί η σελίδα"
+  ORDERING: "Ταξινόμηση"
+  PAGE_ORDER: "Σειρά Σελίδων"
+  OVERRIDES: "Παρακάμψεις"
+  MENU: "Μενού"
+  MENU_HELP: "Η συμβολοσειρά που θα χρησιμοποιηθεί σε ένα μενού.  Εάν δεν έχει καθοριστεί, θα χρησιμοποιηθεί ο τίτλος."
+  SLUG: "Slug"
+  SLUG_HELP: "Η μεταβλητή slug σας επιτρέπει να ορίσετε συγκεκριμένα το sub-URL μίας σελίδας"
+  SLUG_VALIDATE_MESSAGE: "Ένα slug πρέπει να περιέχει μόνο πεζούς αλφαριθμητικούς χαρακτήρες και παύλες"
+  PROCESS: "Διαδικασία"
+  PROCESS_HELP: "Ελέγχει τον τρόπο που οι σελίδες δέχονται επεξεργασία. Μπορεί να ρυθμιστεί για την εκάστοτε σελίδα ή καθολικά"
+  DEFAULT_CHILD_TYPE: "Προεπιλεγμένος τύπος Child"
+  USE_GLOBAL: "Χρησιμοποίησε τη Global τιμή"
+  ROUTABLE: "Μπορεί να δρομολογηθεί"
+  ROUTABLE_HELP: "Εάν η σελίδα είναι προσβάσιμα από μια διεύθυνση URL"
+  CACHING: "Προσωρινή μνήμη αποθήκευσης"
+  VISIBLE: "Ορατή"
+  VISIBLE_HELP: "Προσδιορίζει αν μια σελίδα είναι ορατή στην πλοήγηση."
+  DISABLED: "Ανενεργή"
+  ITEMS: "Αντικείμενα"
+  ORDER_BY: "Ταξινόμιση κατά"
+  ORDER: "Ταξινόμηση"
+  FOLDER: "Φάκελος"
+  ASCENDING: "Αύξουσα"
+  DESCENDING: "Φθίνουσα"
+  PAGE_TITLE: "Τίτλος σελίδας"
+  PAGE_TITLE_HELP: "Ο τίτλος της σελίδας"
+  PAGE: "Σελίδα"
+  FRONTMATTER: "Frontmatter"
+  FILENAME: "Όνομα αρχείου"
+  PARENT_PAGE: "Γονική σελίδα"
+  HOME_PAGE: "Αρχική σελίδα"
+  HOME_PAGE_HELP: "Η σελίδα που το Grav θα χρησιμοποιήσει ως προεπιλεγμένη σελίδα προσγείωσης"
+  DEFAULT_THEME: "Προεπιλεγμένο Θέμα"
+  DEFAULT_THEME_HELP: "Ορίσετε το προεπιλεγμένο θέμα που θα χρησιμοποιεί το Grav (η προεπιλογή είναι το θέμα Antimatter)"
+  TIMEZONE: "Ζώνη ώρας"
+  TIMEZONE_HELP: "Παράκαμψη της προεπιλεγμένης ζώνης ώρας του διακομιστή"
+  SHORT_DATE_FORMAT: "Σύντομη μορφή ημερομηνίας"
+  SHORT_DATE_FORMAT_HELP: "Ρύθμισε τη σύντομη μορφή ημερομηνίας που θα χρησιμοποιείται από τα θέματα"
+  LONG_DATE_FORMAT: "Εκτενής μορφή ημερομηνίας"
+  LONG_DATE_FORMAT_HELP: "Ρύθμισε την εκτενή μορφή ημερομηνίας που θα χρησιμοποιείται από τα θέματα"
+  DEFAULT_ORDERING: "Προεπιλεγμένη Ταξινόμηση"
+  DEFAULT_ORDERING_HELP: "Οι σελίδες μίας λίστας θα εμφανίζονται χρησιμοποιώντας την σειρά εκτός αν αυτό παρακαμφθεί"
+  DEFAULT_ORDERING_DEFAULT: "Default - βάσει του όνομα φακέλου"
+  DEFAULT_ORDERING_FOLDER: "Folder - βάσει του όνομα φακέλου"
+  DEFAULT_ORDERING_TITLE: "Title - βάσει του τίτλου που έχει οριστεί στο header"
+  DEFAULT_ORDERING_DATE: "Date - βάσει της ημερομηνίας που έχει οριστεί στο header"
+  DEFAULT_ORDER_DIRECTION: "Προεπιλεγμένη σειρά εμφάνισης"
+  DEFAULT_ORDER_DIRECTION_HELP: "Η κατεύθυνση των σελίδων σε μια λίστα"
+  DEFAULT_PAGE_COUNT: "Προεπιλεγμένο πλήθος σελίδων"
+  DEFAULT_PAGE_COUNT_HELP: "Προεπιλεγμένο μέγιστο πλήθος σελίδων σε μία λίστα"
+  DATE_BASED_PUBLISHING: "Δημοσίευση βάσει ημερομηνίας"
+  DATE_BASED_PUBLISHING_HELP: "Αυτόματη δημοσίευση ή ιδιωτικοποίηση των αναρτήσεων βασισμένη στην ημερομηνία τους"
+  EVENTS: "Συμβάντα"
+  EVENTS_HELP: "Ενεργοποίησε ή απενεργοποίησε συγκεκριμένα events. Απενεργοποιώντας τα ίσως διακοπεί η λειτουργία κάποιου προσθέτου"
+  REDIRECT_DEFAULT_ROUTE: "Ανακατεύθυνση της προεπιλεγμένης διαδρομής"
+  REDIRECT_DEFAULT_ROUTE_HELP: "Αυτόματη ανακατεύθυνση στην προεπιλεγμένη διαδρομή μιας σελίδας"
+  LANGUAGES: "Γλώσσες"
+  SUPPORTED: "Υποστηρίζεται"
+  SUPPORTED_HELP: "Λίστα χωρισμένη με κόμματα κωδικών γλωσσών 2 γραμμάτων (π.χ. 'en,fr,de')"
+  TRANSLATIONS_FALLBACK: "Εναλλακτική μετάφραση"
+  TRANSLATIONS_FALLBACK_HELP: "Fallback σε κάποια άλλη μετάφραση εάν η επιλεγμένη γλώσσα δεν έχει μεταφραστεί"
+  ACTIVE_LANGUAGE_IN_SESSION: "Ενεργή γλώσσα κατά τη συνεδρία"
+  ACTIVE_LANGUAGE_IN_SESSION_HELP: "Αποθήκευση της ενεργής γλώσσας στη συνεδρία"
+  HTTP_HEADERS: "Κεφαλίδες HTTP"
+  EXPIRES: "Λήξη"
+  EXPIRES_HELP: "Ορίζει την κεφαλίδα λήξης. Η τιμή είναι σε δευτερόλεπτα."
+  CACHE_CONTROL: "HTTP Cache-Control"
+  CACHE_CONTROL_HELP: "Ορίστε μια έγκυρη τιμή cache-ελέγχου όπως 'όχι-cache, όχι-αποθήκευση, να-επανεπικυρωθεί'"
+  LAST_MODIFIED: "Τελευταία τροποποίηση"
+  LAST_MODIFIED_HELP: "Ορίζει την τελευταία τροποποιημένη κεφαλίδα που μπορεί να σας βοηθήσει να βελτιστοποιήσετε τον διακομιστή μεσολάβησης και την προσωρινή αποθήκευση του προγράμματος περιήγησης"
+  ETAG: "ETag"
+  ETAG_HELP: "Καθορίζει την κεφαλίδα etag για να βοηθήσει στον εντοπισμό όταν μια σελίδα έχει τροποποιηθεί"
+  VARY_ACCEPT_ENCODING: "Ποικίλλει η δεκτή κωδικοποίηση"
+  VARY_ACCEPT_ENCODING_HELP: "Ορίζει την κεφαλίδα «Ποικίλλει: Δεκτή Κωδικοποίηση» για να βοηθήσει με το διακομιστή μεσολάβησης και την προσωρινή αποθήκευση CDN"
+  MARKDOWN_EXTRA: "Markdown extra"
+  MARKDOWN_EXTRA_HELP: "Ενεργοποιήσετε την προεπιλεγμένη υποστήριξη για Markdown Extra - https://michelf.ca/projects/php-markdown/extra/"
+  MARKDOWN_EXTRA_ESCAPE_FENCES: "Διάφυγε τα HTML στοιχεία σε markdown extra fences"
+  MARKDOWN_EXTRA_ESCAPE_FENCES_HELP: "Διαφεύγει τα HTML στοιχεία σε markdown extra fences"
+  AUTO_LINE_BREAKS: "Αυτόματες αλλαγές γραμμών"
+  AUTO_LINE_BREAKS_HELP: "Ενεργοποιήσετε την υποστήριξη για αυτόματη αλλαγή γραμμής στη markdown"
+  AUTO_URL_LINKS: "Αυτόματες συνδέσεις URL"
+  AUTO_URL_LINKS_HELP: "Ενεργοποιήσετε την αυτόματη μετατροπή των διευθύνσεων URL σε HTML υπερ-συνδέσεις"
+  ESCAPE_MARKUP: "Σημειοθέτηση αποσπάσματος"
+  ESCAPE_MARKUP_HELP: "Ετικέτες σήμανσης διαφυγής σε HTML οντότητες"
+  CACHING_HELP: "Γενικός διακόπτης ενεργοποίησης/απενεργοποίησης της προσωρινής αποθήκευσης του Grav"
+  CACHE_CHECK_METHOD: "Μέθοδος ελέγχου μνήμης προσωρινής αποθήκευσης (cache)"
+  CACHE_CHECK_METHOD_HELP: "Επιλέξτε τη μέθοδο που χρησιμοποιεί το Grav για να ελέγξει αν έχουν τροποποιηθεί αρχεία σελίδων."
+  CACHE_DRIVER: "Πρόγραμμα οδήγησης της μνήμης cache"
+  CACHE_DRIVER_HELP: "Επιλέξτε ποιο πρόγραμμα οδήγησης της προσωρινής αποθήκευσης να χρησιμοποιήσει το Grav. Ο 'Αυτόματος εντοπισμός' προσπαθεί να βρει το καλύτερο για εσάς"
+  CACHE_PREFIX: "Πρόθεμα cache"
+  CACHE_PREFIX_HELP: "Ένα αναγνωριστικό για το μέρος του κλειδιού Grav.  Μην το αλλάξετε εκτός και αν γνωρίζετε τι κάνετε."
+  CACHE_PREFIX_PLACEHOLDER: "Προέρχεται από τη βασική διεύθυνση URL (παράκαμψη εισάγοντας τυχαία συμβολοσειρά)"
+  LIFETIME: "Διάρκεια ζωής"
+  LIFETIME_HELP: "Ορίζει τη διάρκεια ζωής της cache σε δευτερόλεπτα. 0 = άπειρο"
+  GZIP_COMPRESSION: "Συμπίεση Gzip"
+  GZIP_COMPRESSION_HELP: "Ενεργοποιήστε τη GZip συμπίεση της σελίδας του Grav για αυξημένη απόδοση."
+  TWIG_TEMPLATING: "Πρότυπα Twig"
+  TWIG_CACHING: "Προσωρινή αποθήκευση Twig"
+  TWIG_CACHING_HELP: "Ελέγχει το μηχανισμό προσωρινής αποθήκευσης του Twig. Αφήστε το ενεργοποιημένο για καλύτερη απόδοση."
+  TWIG_DEBUG: "Εντοπισμός σφαλμάτων Twig"
+  TWIG_DEBUG_HELP: "Επιτρέπει την επιλογή της μη φόρτωσης της επέκτασης Αποσφαλμάτωση Twig"
+  DETECT_CHANGES: "Ανίχνευση μεταβολών"
+  DETECT_CHANGES_HELP: "Το Twig θα μεταγλωττίσει αυτόματα την προσωρινή μνήμη του αν εντοπίσει αλλαγές σε πρότυπα Twig"
+  AUTOESCAPE_VARIABLES: "Εφαρμογή Autoescape στις μεταβλητές"
+  AUTOESCAPE_VARIABLES_HELP: "Κάνει autoescape όλες τις μεταβλητές.  Αυτό πιθανότατα θα σπάσει το site σας"
+  ASSETS: "Αντικείμενα"
+  CSS_PIPELINE: "Αγωγός CSS"
+  CSS_PIPELINE_HELP: "Ο αγωγός CSS είναι η ενοποίηση πολλαπλών CSS πόρων σε ένα αρχείο"
+  CSS_PIPELINE_INCLUDE_EXTERNALS: "Συμπεριλάβετε εξωτερικούς αγωγούς CSS"
+  CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "Οι εξωτερικές διευθύνσεις URL μερικές φορές έχουν σχετικές αναφορές αρχείων και δεν θα έπρεπε να διοχετεύονται"
+  CSS_PIPELINE_BEFORE_EXCLUDES: "Πρώτα επεξεργασία CSS αγωγού"
+  CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Επεξεργάζεται τον CSS αγωγό πριν από οποιαδήποτε άλλη αναφορά CSS που δεν περιλαμβάνεται"
+  CSS_MINIFY: "Συμπίεση της CSS"
+  CSS_MINIFY_HELP: "Ελαχιστοποίησε το CSS κατά τη διοχέτευση"
+  CSS_MINIFY_WINDOWS_OVERRIDE: "Παράκαψη ελαχιστοποίησης CSS για Windows"
+  CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Παράκαμψη Ελαχιστοποίησης για Windows. False από προεπιλογή λόγω ThreadStackSize"
+  CSS_REWRITE: "Επανεγγραφή CSS"
+  CSS_REWRITE_HELP: "Επανεγγραφή κάθε αναφορικού CSS URL κατά τη διοχέτευση"
+  JAVASCRIPT_PIPELINE: "Αγωγός JavaScript"
+  JAVASCRIPT_PIPELINE_HELP: "Ο αγωγός JS είναι η ενοποίηση πολλαπλών JS πόρων σε ένα αρχείο"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Συμπεριλάβετε εξωτερικούς αγωγούς JS"
+  JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Οι εξωτερικές διευθύνσεις URL μερικές φορές έχουν σχετικές αναφορές αρχείων και δεν θα έπρεπε να διοχετεύονται"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Πρώτα επεξεργασία JS αγωγού"
+  JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Επεξεργάζεται τον JS αγωγό πριν από οποιαδήποτε άλλη αναφορά JS που δεν περιλαμβάνεται"
+  JAVASCRIPT_MINIFY: "Συμπίεση του κώδικα JavaScript"
+  JAVASCRIPT_MINIFY_HELP: "Ελαχιστοποίησε το JS κατά τη διοχέτευση"
+  ENABLED_TIMESTAMPS_ON_ASSETS: "Ενεργοποιήστε τις χρονοσημάνσεις στους διαθέσιμους πόρους"
+  ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Ενεργοποίηση χρονοσημάνσεων στους διαθέσιμους πόρους"
+  COLLECTIONS: "Συλλογές"
+  ERROR_HANDLER: "Πρόγραμμα χειρισμού σφαλμάτων"
+  DISPLAY_ERRORS: "Εμφάνιση των μηνυμάτων λάθους"
+  DISPLAY_ERRORS_HELP: "Εμφάνιση πλήρους backtrace στιλ σελίδας σφάλματος"
+  LOG_ERRORS: "Καταγραφή σφαλμάτων σε αρχείο"
+  LOG_ERRORS_HELP: "Καταγραφή σφαλμάτων στο φάκελο /logs"
+  DEBUGGER: "Πρόγραμμα εντοπισμού σφαλμάτων"
+  DEBUGGER_HELP: "Ενεργοποιήσετε το πρόγραμμα εντοπισμού σφαλμάτων του Grav και τις ακόλουθες ρυθμίσεις"
+  DEBUG_TWIG: "Εκσφαλμάτωση του Twig"
+  DEBUG_TWIG_HELP: "Ενεργοποίηση του εντοπισμού σφαλμάτων στα πρότυπα του Twig"
+  SHUTDOWN_CLOSE_CONNECTION: "Τερματισμός στενής σύνδεσης"
+  SHUTDOWN_CLOSE_CONNECTION_HELP: "Κλείσε τη σύνδεση πριν από την κλήση onShutdown(). False για εντοπισμό σφαλμάτων"
+  DEFAULT_IMAGE_QUALITY: "Προεπιλεγμένη ποιότητα εικόνας"
+  DEFAULT_IMAGE_QUALITY_HELP: "Η προεπιλεγμένη ποιότητα εικόνας που θα χρησιμοποιείται όταν γίνεται αναδειγματοληψία ή προσωρινή αποθήκευση εικόνων (85%)"
+  CACHE_ALL: "Προσωρινή αποθήκευση όλων των εικόνων"
+  CACHE_ALL_HELP: "Πέρασε όλες τις εικόνες μέσω του Grav cache συστήματος ακόμη και αν δεν έχουν επεξεργασία μέσων"
+  IMAGES_DEBUG: "Υδατογράφημα αποσφαλμάτωσης εικόνας"
+  IMAGES_DEBUG_HELP: "Εμφάνιση μιας επικάλυψης πάνω από τις εικόνες που δείχνει το βάθος pixel της εικόνας, όταν εργάζεστε με retina για παράδειγμα"
+  UPLOAD_LIMIT: "Όριο μεταφόρτωσης αρχείων"
+  UPLOAD_LIMIT_HELP: "Ορίσετε το μέγιστο μέγεθος μεταφόρτωσης σε byte (0 είναι απεριόριστο)"
+  ENABLE_MEDIA_TIMESTAMP: "Ενεργοποιήστε τις χρονοσημάνσεις στα μέσα"
+  ENABLE_MEDIA_TIMESTAMP_HELP: "Προσθέτει μια χρονική σήμανση με βάση την τελευταία ημερομηνία τροποποίησης σε κάθε στοιχείο πολυμέσων"
+  SESSION: "Συνεδρία"
+  SESSION_ENABLED_HELP: "Ενεργοποιήσετε την υποστήριξη συνεδριών λειτουργίας εντός Grav"
+  SESSION_NAME_HELP: "Ένα αναγνωριστικό που χρησιμοποιείται για το σχηματισμό του όνόματος του cookie της συνεδρίας"
+  ABSOLUTE_URLS: "Απόλυτες διευθύνσεις URL"
+  ABSOLUTE_URLS_HELP: "Απόλυτες ή σχετικές διευθύνσεις URL για «base_url»"
+  PARAMETER_SEPARATOR: "Διαχωριστικό παραμέτρων"
+  PARAMETER_SEPARATOR_HELP: "Διαχωριστικό για τις παραμέτρους που διαβιβάστηκαν, που μπορούν να αλλάξουν για τον Apache στα Windows"
+  TASK_COMPLETED: "Η εργασία ολοκληρώθηκε"
+  EVERYTHING_UP_TO_DATE: "Τα πάντα είναι ενημερωμένα"
+  UPDATES_ARE_AVAILABLE: "υπάρχουν διαθέσιμες ενημερώσεις"
+  IS_AVAILABLE_FOR_UPDATE: "είναι διαθέσιμη για ενημέρωση"
+  IS_NOW_AVAILABLE: "είναι τώρα διαθέσιμη"
+  CURRENT: "Τρέχουσα έκδοση"
+  UPDATE_GRAV_NOW: "Αναβαθμίστε το Grav τώρα"
+  GRAV_SYMBOLICALLY_LINKED: "Το Grav είναι symlinked. Δε θα υπάρχει διαθέσιμη αναβάθμιση"
+  UPDATING_PLEASE_WAIT: "Ενημέρωση... παρακαλώ περιμένετε, λήψη"
+  OF_THIS: "αυτό"
+  OF_YOUR: "σας"
+  HAVE_AN_UPDATE_AVAILABLE: "έχει διαθέσιμη ενημέρωση"
+  SAVE_AS: "Αποθήκευση ως"
+  MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη σελίδα και όλα τα τέκνα; Εάν η σελίδα είναι μεταφρασμένη σε άλλες γλώσσες, αυτές οι μεταφράσεις θα κρατηθούν και πρέπει να διαγραφούν ξεχωριστά. Αλλιώς ο φάκελος σελίδας θα διαγραφεί μαζί με τις υποσελίδες. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί."
+  AND: "και"
+  UPDATE_AVAILABLE: "Υπάρχει διαθέσιμη ενημέρωση"
+  METADATA_KEY: "Κλειδί (π.χ. «λέξεις-κλειδιά»)"
+  METADATA_VALUE: "Τιμή (π.χ. «Blog, Grav»)"
+  USERNAME_HELP: "Το όνομα χρήστη πρέπει να είναι μεταξύ 3 και 16 χαρακτήρες, με πεζά, αριθμούς, κάτω παύλες και ενωτικά. Κεφαλαία γράμματα, διαστήματα και ειδικοί χαρακτήρες δεν επιτρέπονται"
+  FULLY_UPDATED: "Πλήρως Ενημερωμένο"
+  SAVE_LOCATION: "Τοποθεσία Αποθήκευσης"
+  PAGE_FILE: "Πρότυπο σελίδας"
+  PAGE_FILE_HELP: "Ονομασία αρχείου προτύπου σελίδας, και από προεπιλογή το πρότυπο εμφάνισης για αυτή τη σελίδα"
+  NO_USER_ACCOUNTS: "Δεν βρέθηκε κανένας λογαριασμός χρήστη, παρακαλώ δημιουργήστε έναν πρώτα..."
+  REDIRECT_TRAILING_SLASH: "Ανακατευθύνει την τελική κάθετο"
+  REDIRECT_TRAILING_SLASH_HELP: "Εκτέλεσε μια ανακατεύθυνση 301 αντί για διαφανή χειρισμό των καταληκτικών καθέτων URIs."
+  DEFAULT_DATE_FORMAT: "Μορφή ημερομηνίας σελίδας"
+  DEFAULT_DATE_FORMAT_HELP: "Μορφή ημερομηνίας σελίδας που χρησιμοποιείται από το Grav. Από προεπιλογή, το Grav επιχειρεί να μαντέψει τη μορφή, εντούτοις μπορείτε να ορίσετε μορφή σύμφωνα με τη σύνταξη PHP"
+  DEFAULT_DATE_FORMAT_PLACEHOLDER: "Μάντεψε αυτόματα"
+  IGNORE_FILES: "Αγνόηση αρχείων"
+  IGNORE_FILES_HELP: "Συγκεκριμένα αρχεία που θα αγνοηθούν κατά την επεξεργασία σελίδων"
+  IGNORE_FOLDERS: "Αγνόηση φακέλων"
+  IGNORE_FOLDERS_HELP: "Συγκεκριμένοι φάκελοι που θα αγνοηθούν κατά την επεξεργασία σελίδων"
+  HTTP_ACCEPT_LANGUAGE: "Όρισε την γλώσσα βάσει του φυλλομετρητή"
+  HTTP_ACCEPT_LANGUAGE_HELP: "Μπορείτε να επιλέξετε να δοκιμάσετε να ορίσετε τη γλώσσα που βασίζεται στην ετικέτα κεφαλίδας «http_accept_language» στο πρόγραμμα περιήγησης"
+  OVERRIDE_LOCALE: "Παράκαμψη των Τοπικών Ρυθμίσεων"
+  OVERRIDE_LOCALE_HELP: "Αντικαθιστά τις ρυθμίσεις τοπικότητας στην PHP με βάση την τρέχουσα γλώσσα"
+  REDIRECT: "Ανακατεύθυνση σελίδας"
+  REDIRECT_HELP: "Εισαγάγετε μια διαδρομή σελίδας ή εξωτερική διεύθυνση URL για αυτήν τη σελίδα για να ανακατευθύνετε σε. π.χ. «/κάποια/διαδρομή» ή «http://somesite.com»"
+  PLUGIN_STATUS: "Κατάσταση πρόσθετου"
+  INCLUDE_DEFAULT_LANG: "Περιλαμβάνει την προεπιλεγμένη γλώσσα"
+  INCLUDE_DEFAULT_LANG_HELP: "Αυτό θα προτάξει όλες τις διευθύνσεις URL στην προεπιλεγμένη γλώσσα με την προεπιλεγμένη γλώσσα.  π.χ. «/en/blog/my-post»"
+  ALLOW_URL_TAXONOMY_FILTERS: "Φίλτρα Ταξινόμησης URL"
+  ALLOW_URL_TAXONOMY_FILTERS_HELP: "Συλλογές βασισμένες σε σελίδα σας επιτρέπουν να φιλτράρετε μέσω «/taxonomy:value»."
+  REDIRECT_DEFAULT_CODE: "Προεπιλεγμένος κωδικός ανακατεύθυνσης"
+  REDIRECT_DEFAULT_CODE_HELP: "Ο κωδικός κατάστασης HTTP που θα χρησιμοποιηθεί για ανακατευθύνσεις"
+  IGNORE_HIDDEN: "Παράβλεψη των κρυφών"
+  IGNORE_HIDDEN_HELP: "Παράβλεψη όλων των αρχείων και των φακέλων που ξεκινούν με μια ΤΕΛΕΙΑ"
+  WRAPPED_SITE: "Wrapped site"
+  WRAPPED_SITE_HELP: "Για να γνωρίζουν τα θέματα/plugins αν το Grav είναι wrapped από άλλη πλατφόρμα"
+  FALLBACK_TYPES: "Επιτρεπόμενοι εναλλακτικοί τύποι"
+  FALLBACK_TYPES_HELP: "Επιτρεπτοί τύποι αρχείων που μπορούν να βρεθούν αν έχετε αποκτήσει πρόσβαση μέσω της οδού Σελίδα. Προεπιλέγει οποιοδήποτε τύπο μέσων που υποστηρίζονται."
+  INLINE_TYPES: "Ένθετοι εναλλακτικοί τύποι"
+  INLINE_TYPES_HELP: "Μια λίστα των τύπων αρχείων που πρέπει να εμφανίζονται ένθετα αντί να γίνεται λήψη αυτών"
+  APPEND_URL_EXT: "Προσάρτηση επέκτασης URL"
+  APPEND_URL_EXT_HELP: "Θα προσθέσει μια προσαρμοσμένη επέκταση στο URL της Σελίδας. Σημείωση, αυτό θα σημαίνει ότι το Grav θα αναζητήσει ' <template>. <extension>. twig ' πρότυπο"
+  PAGE_MODES: "Λειτουργίες Σελίδας"
+  PAGE_TYPES: "Τύποι σελίδας"
+  ACCESS_LEVELS: "Επίπεδα πρόσβασης"
+  GROUPS: "Ομάδες"
+  GROUPS_HELP: "Λίστα των ομάδων που ο χρήστης είναι μέλος"
+  ADMIN_ACCESS: "Πρόσβαση Διαχειριστή"
+  SITE_ACCESS: "Πρόσβαση στον Ιστοχώρο"
+  INVALID_SECURITY_TOKEN: "Το διακριτικό ασφαλείας δεν είναι έγκυρο"
+  ACTIVATE: "Eνεργοποίηση"
+  TWIG_UMASK_FIX: "Unmask Fix"
+  TWIG_UMASK_FIX_HELP: "Από προεπιλογή το Twig δημιουργεί προσωρινά αρχεία ως 0755, η διόρθωση τα αλλάζει σε 0775"
+  CACHE_PERMS: "Δικαιώματα μνήμης cache"
+  CACHE_PERMS_HELP: "Προεπιλεγμένα δικαιώματα φακέλων cache. Συνήθως 0755 ή 0775"
+  REMOVE_SUCCESSFUL: "Επιτυχής Κατάργηση"
+  REMOVE_FAILED: "Η Κατάργηση Απέτυχε"
+  HIDE_HOME_IN_URLS: "Απόκρυψη της αρχικής διαδρομής στις διευθύνσεις URL"
+  HIDE_HOME_IN_URLS_HELP: "Θα εξασφαλίσει ότι οι προεπιλεγμένες διαδρομές για όλες τις σελίδες κάτω από την αρχική δε θα αναφέρουν την κανονική διαδρομή της"
+  TWIG_FIRST: "Επεξεργασία πρώτα των αρχείων του Twig"
+  TWIG_FIRST_HELP: "Εάν έχετε ενεργοποιήσει την επεξεργασία σελίδων Twig, τότε μπορείτε να ρυθμίσετε το Twig να επεξεργαστεί πριν ή μετά από τη markdown"
+  SESSION_SECURE: "Ασφαλής"
+  SESSION_SECURE_HELP: "Εάν είναι αληθές, δείχνει ότι η επικοινωνία για αυτό το cookie πρέπει να γίνει πάνω από μιά κρυπτογραφημένη μετάδοση. Προειδοποίηση: Ενεργοποιήστε την μόνο στις τοποθεσίες που εκτελούνται αποκλειστικά σε HTTPS"
+  SESSION_HTTPONLY: "HTTP μόνο"
+  SESSION_HTTPONLY_HELP: "Εάν είναι αληθές, δείχνει ότι τα cookies πρέπει να χρησιμοποιούνται μόνο μέσω HTTP, και δεν επιτρέπεται η τροποποίηση μέσω JavaScript"
+  REVERSE_PROXY: "Αντίστροφoς Διακομιστής Μεσολάβησης"
+  REVERSE_PROXY_HELP: "Ενεργοποιήστε το άν είστε πίσω από έναν διακομιστή αντίστροφης μεσολάβηση αντιμετωπίζετε δυσκολία με τις διευθύνσεις URL που περιέχουν εσφαλμένες θύρες"
+  INVALID_FRONTMATTER_COULD_NOT_SAVE: "Μη έγκυρος πρόλογος (frontmatter), αδύνατη η αποθήκευση"
+  ADD_FOLDER: "Προσθήκη φακέλου"
+  PROXY_URL: "Διεύθυνση URL του διακομιστή μεσολάβησης"
+  PROXY_URL_HELP: "Εισάγετε το Διακομιστή proxy ή τη διεύθυνση IP και τη ΘΥΡΑ (PORT)"
+  NOTHING_TO_SAVE: "Δεν υπάρχει τίποτα γιά αποθήκευση"
+  FILE_ERROR_ADD: "Παρουσιάστηκε σφάλμα κατά την προσθήκη του αρχείου"
+  FILE_ERROR_UPLOAD: "Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του αρχείου"
+  FILE_UNSUPPORTED: "Μη υποστηριζόμενος τύπος αρχείου"
+  ADD_ITEM: "Προσθήκη στοιχείου"
+  FILE_TOO_LARGE: "Το αρχείο είναι πολύ μεγάλο για να φορτωθεί, το μέγιστο επιτρεπόμενο είναι %s σύμφωνα με <br>τις ρυθμίσεις της PHP. Αυξήστε τη ρύθμιση PHP σας `post_max_size`"
+  INSTALLING: "Γίνεται εγκατάσταση"
+  LOADING: "Φόρτωση…"
+  DEPENDENCIES_NOT_MET_MESSAGE: "Οι ακόλουθες εξαρτήσεις πρέπει να εκπληρωθούν πρώτα:"
+  ERROR_INSTALLING_PACKAGES: "Σφάλμα κατά την εγκατάσταση πακέτων"
+  INSTALLING_DEPENDENCIES: "Εγκατάσταση εξαρτήσεων..."
+  INSTALLING_PACKAGES: "Εγκατάσταση πακέτων..."
+  PACKAGES_SUCCESSFULLY_INSTALLED: "Τα πακέτα εγκαταστάθηκαν με επιτυχία."
+  READY_TO_INSTALL_PACKAGES: "Είστε έτοιμοι να εγκαταστήσετε τα πακέτα"
+  PACKAGES_NOT_INSTALLED: "Τα πακέτα δεν εγκαταστάθηκαν"
+  PACKAGES_NEED_UPDATE: "Πακέτα που έχουν ήδη εγκατασταθεί, αλλά πολύ παλιά"
+  PACKAGES_SUGGESTED_UPDATE: "Πακέτα που είναι ήδη εγκατεστημένα, καθώς και η έκδοση είναι εντάξει, αλλά θα ενημερώνονται"
+  REMOVE_THE: "Αφαιρέστε το %s"
+  CONFIRM_REMOVAL: "Είστε βέβαιοι ότι θέλετε να διαγράψετε το %s; ?"
+  REMOVED_SUCCESSFULLY: "Το %s καταργήθηκε με επιτυχία"
+  ERROR_REMOVING_THE: "Σφάλμα κατά την αφαίρεση του %s"
+  ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "Το %s απαιτεί τις ακόλουθες εξαρτήσεις, που δεν απαιτούνται από άλλα εγκατεστημένα πακέτα. Εάν δεν τις χρησιμοποιείτε, μπορείτε να τις καταργήσετε απευθείας από εδώ."
+  READY_TO_UPDATE_PACKAGES: "Είστε έτοιμοι να ενημερώσετε τα πακέτα"
+  ERROR_UPDATING_PACKAGES: "Σφάλμα κατά την ενημέρωση πακέτων."
+  UPDATING_PACKAGES: "Ενημέρωση πακέτων..."
+  PACKAGES_SUCCESSFULLY_UPDATED: "Τα πακέτα ενημερώθηκαν με επιτυχία."
+  UPDATING: "Ενημέρωση"
+  GPM_RELEASES: "GPM κυκλοφορίες"
+  GPM_RELEASES_HELP: "Επιλέξτε «Testing» για να εγκαταστήσετε beta ή testing εκδόσεις"
+  GPM_METHOD: "Μέθοδος Remote Fetch"
+  GPM_METHOD_HELP: "Όταν οριστεί σε αυτόματο, το Grav θα καθορίσει αν το fopen είναι διαθέσιμο και θα το χρησιμοποιήσει, αλλιώς θα επιστρέψει στο cURL. Για να επιβάλετε τη χρήση του ενός ή του άλλου αλλάξτε τη ρύθμιση."
+  GPM_VERIFY_PEER: "Εξ αποστάσεως επαλήθευση Peer (SSL)"
+  GPM_VERIFY_PEER_HELP: "Ορισμένες υπηρεσίες παροχής φαίνεται να αποτυγχάνουν στην επαλήθευση πιστοποιητικύ SSL του getgrav.org, κάνοντας το GPM να μην λειτουργεί. Αν αυτή είναι η περίπτωση για εσάς, η απενεργοποίηση αυτής της ρύθμισης μπορεί να βοηθήσει"
+  AUTO: "Αυτόματο"
+  FOPEN: "fopen"
+  CURL: "cURL"
+  STABLE: "Stable"
+  TESTING: "Testing"
+  FRONTMATTER_PROCESS_TWIG: "Επεξεργασία frontmatter Twig"
+  FRONTMATTER_PROCESS_TWIG_HELP: "Όταν ενεργοποιηθεί μπορείτε να χρησιμοποιήσετε μεταβλητές Twig config στο frontmatter"
+  FRONTMATTER_IGNORE_FIELDS: "Παράβλεψη frontmatter πεδίων"
+  FRONTMATTER_IGNORE_FIELDS_HELP: "Ορισμένα πεδία frontmatter μπορεί να περιέχουν Twig, αλλά δεν πρέπει να υποστούν επεξεργασία, όπως οι «φόρμες»"
+  PACKAGE_X_INSTALLED_SUCCESSFULLY: "Το πακέτο %s εγκαταστάθηκε με επιτυχία"
+  ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Γονική ρύθμιση ταξινόμησης, η ταξινόμηση απενεργοποιήθηκε"
+  ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Η σελίδα δεν είναι ορατή, η ταξινόμηση απενεργοποιήθηκε"
+  ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "Η ταξινόμηση μέσω διαχειριστή δεν υποστηρίζεται γιατί υπάρχουν περισσότερα από 200 παράγωγα"
+  ORDERING_DISABLED_BECAUSE_PAGE_NO_PREFIX: "Η ταξινόμηση σελίδων είναι απενεργοποιημένη για αυτήν τη σελίδα, επειδή το <strong>Αριθμητικό Πρόθεμα Φακέλου</strong> δεν είναι ενεργοποιημένο"
+  CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "ΣΗΜΕΙΩΣΗ: Δεν μπορείτε να προσθέσετε αρχεία πολυμέσων, μέχρι να αποθηκεύσετε τη σελίδα. Απλά κάντε κλικ «Αποθήκευση» στην κορυφή"
+  CANNOT_ADD_FILES_PAGE_NOT_SAVED: "ΣΗΜΕΙΩΣΗ: Η σελίδα πρέπει να αποθηκευτεί πριν να μπορείτε να ανεβάσετε αρχεία σε αυτή."
+  DROP_FILES_HERE_TO_UPLOAD: "Ρίξτε τα αρχεία σας εδώ ή <strong>κάντε κλικ σε αυτόν τον τομέα</strong>"
+  INSERT: "Εισαγωγή"
+  UNDO: "Αναίρεση"
+  REDO: "Ακύρωση Αναίρεσης"
+  HEADERS: "Επικεφαλίδες"
+  BOLD: "Έντονα"
+  ITALIC: "Πλάγια"
+  STRIKETHROUGH: "Διακριτή διαγραφή"
+  SUMMARY_DELIMITER: "Περίληψη οριοθέτη"
+  LINK: "Σύνδεσμος"
+  IMAGE: "Εικόνα"
+  BLOCKQUOTE: "Μπλοκ κειμένου παράθεσης"
+  UNORDERED_LIST: "Μη ταξινομημένη λίστα"
+  ORDERED_LIST: "Ταξινομημένη λίστα"
+  EDITOR: "Επεξεργαστής"
+  PREVIEW: "Προεπισκόπιση"
+  FULLSCREEN: "Πλήρης οθόνη"
+  NON_ROUTABLE: "Απροσπέλαστη"
+  NON_VISIBLE: "Αόρατη"
+  NON_PUBLISHED: "Μη δημοσιευμένες"
+  CHARACTERS: "χαρακτήρες"
+  PUBLISHING: "Δημοσίευση"
+  MEDIA_TYPES: "Τύποι μέσων"
+  IMAGE_OPTIONS: "Επιλογές εικόνας"
+  MIME_TYPE: "Τύπος MIME"
+  THUMB: "Μικρογραφία"
+  TYPE: "Τύπος"
+  FILE_EXTENSION: "Επέκταση αρχείου"
+  LEGEND: "Υπόμνημα σελίδων"
+  MEMCACHE_SERVER: "Διακομιστής Memcache"
+  MEMCACHE_SERVER_HELP: "Η διεύθυνση του διακομιστή Memcache"
+  MEMCACHE_PORT: "Θύρα Memcache"
+  MEMCACHE_PORT_HELP: "Η θύρα του διακομιστή Memcache"
+  MEMCACHED_SERVER: "Διακομιστής Memcached"
+  MEMCACHED_SERVER_HELP: "Η διεύθυνση του διακομιστή Memcached"
+  MEMCACHED_PORT: "Θύρα Memcached"
+  MEMCACHED_PORT_HELP: "Η θύρα του διακομιστή Memcached"
+  REDIS_SERVER: "Διακομιστής Redis"
+  REDIS_SERVER_HELP: "Η διεύθυνση του διακομιστή Redis"
+  REDIS_PORT: "Θύρα Redis"
+  REDIS_PORT_HELP: "Η θύρα του διακομιστή Redis"
+  REDIS_PASSWORD: "Συνθηματικό Redis"
+  ALL: "Όλα"
+  FROM: "από"
+  TO: "έως"
+  RELEASE_DATE: "Ημερομηνία κυκλοφορίας"
+  SORT_BY: "Ταξινόμηση κατά"
+  RESOURCE_FILTER: "Φίλτρο..."
+  FORCE_SSL: "Εξαναγκασμός SSL"
+  FORCE_SSL_HELP: "Γενικός εξαναγκασμός SSL, αν ενεργοποιηθεί όταν η σελίδα είναι προσβάσιμη μέσω HTTP, το Grav αποστέλλει ανακατεύθυνση στη σελίδα HTTPS"
+  NEWS_FEED: "Ροή ειδήσεων"
+  EXTERNAL_URL: "Εξωτερική διεύθυνση URL"
+  CUSTOM_BASE_URL: "Προσαρμοσμένο URL βάσης"
+  CUSTOM_BASE_URL_HELP: "Χρησιμοποιήστε το αν θέλετε να ξαναγράψετε τον τομέα του ιστότοπου ή να χρησιμοποιήσετε κάποιον υποφάκελο, διαφορετικό από εκείνο που χρησιμοποιείται από το Grav. Παράδειγμα: http://localhost"
+  FILEUPLOAD_PREVENT_SELF: 'Δεν είναι δυνατό να χρησιμοποιήσετε «%s» εκτός σελίδων.'
+  FILEUPLOAD_UNABLE_TO_UPLOAD: 'Δεν μπορείτε να ανεβάσετε το αρχείο %s: %s'
+  FILEUPLOAD_UNABLE_TO_MOVE: 'Δεν μπορείτε να μετακινήσετε το αρχείο %s σε «%s»'
+  DROPZONE_CANCEL_UPLOAD: 'Ακύρωση ανεβάσματος'
+  DROPZONE_CANCEL_UPLOAD_CONFIRMATION: 'Είστε βέβαιοι ότι θέλετε να ακυρώσετε αυτό το ανέβασμα;'
+  DROPZONE_DEFAULT_MESSAGE: 'Ρίξτε τα αρχεία σας εδώ ή <strong>κάντε κλικ σε αυτόν τον τομέα</strong>'
+  DROPZONE_FALLBACK_MESSAGE: 'Το πρόγραμμα περιήγησής σας δεν υποστηρίζει drag and drop ανέβασμα αρχείων.'
+  DROPZONE_FALLBACK_TEXT: 'Παρακαλούμε χρησιμοποιήστε την παρακάτω φόρμα για να φορτώσετε τα αρχεία σας όπως γινόταν παλαιότερα.'
+  DROPZONE_FILE_TOO_BIG: 'Το αρχείο είναι πολύ μεγάλο ({{filesize}}MiB). Μέγιστο μέγεθος αρχείου: {{maxFilesize}}MiB.'
+  DROPZONE_INVALID_FILE_TYPE: "Δεν μπορείτε να ανεβάσετε αρχεία αυτού του τύπου."
+  DROPZONE_MAX_FILES_EXCEEDED: "Δεν μπορείτε να ανεβάσετε άλλα αρχεία."
+  DROPZONE_REMOVE_FILE: "Αφαίρεση αρχείου"
+  DROPZONE_RESPONSE_ERROR: "Ο διακομιστής ανταποκρίθηκε με κωδικό {{statusCode}}."
+  PREMIUM_PRODUCT: "Premium"
+  DESTINATION_NOT_SPECIFIED: "Δεν ορίστηκε προορισμός"
+  UPLOAD_ERR_NO_TMP_DIR: "Λείπει ένας προσωρινός φάκελος"
+  SESSION_SPLIT: "Συνεδρία Σπλιτ"
+  SESSION_SPLIT_HELP: "Ανεξάρτητες split συνεδρίες μεταξύ σελίδας και άλλων plugins (όπως admin)"
+  ERROR_FULL_BACKTRACE: "Πλήρες Backtrace σφάλμα"
+  ERROR_SIMPLE: "Απλό σφάλμα"
+  ERROR_SYSTEM: "Σφάλμα συστήματος"
+  IMAGES_AUTO_FIX_ORIENTATION: "Αυτόματη επιδιόρθωση του προσανατολισμού"
+  IMAGES_AUTO_FIX_ORIENTATION_HELP: "Διορθώσετε αυτόματα τον προσανατολισμό της εικόνας που βασίζεται στα δεδομένα Exif"
+  REDIS_SOCKET: "Υποδοχή Redis"
+  REDIS_SOCKET_HELP: "Η υποδοχή Redis"
+  NOT_SET: "Μη ορισμένο"
+  PERMISSIONS: "Δικαιώματα"
+  NEVER_CACHE_TWIG: "Ποτέ Twig Cache"
+  NEVER_CACHE_TWIG_HELP: "Μόνο cache περιεχόμενου και επεξεργασια του Twig κάθε φορά για τις σελίδες. Παραβλέπει τη ρύθμιση twig_first."
+  ALLOW_WEBSERVER_GZIP: "Επίτρεψε WebServer Gzip"
+  ALLOW_WEBSERVER_GZIP_HELP: "Απενεργοποιημένη από προεπιλογή. Όταν είναι ενεργοποιημένη, η WebServer-configured Gzip/Deflate συμπίεση θα λειτουργήσει, αλλά δεν θα κλείσει πριν από το onShutDown() προκαλώντας αργή φόρτωση της σελίδας"
+  OFFLINE_WARNING: "Αδύνατη η σύνδεση με το GPM"
+  CLEAR_IMAGES_BY_DEFAULT: "Καθαρισμός cache εικόνων από προεπιλογή"
+  CLEAR_IMAGES_BY_DEFAULT_HELP: "Από προεπιλογή, οι επεξεργασμένες εικόνες καθαρίζονται για κάθε καθαρισμό cache, αυτό μπορεί να απενεργοποιηθεί"
+  CLI_COMPATIBILITY: "CLI Συμβατότητα"
+  CLI_COMPATIBILITY_HELP: "Εξασφαλίζει ότι χρησιμοποιούνται μόνο non-volatile Cache οδηγοί (αρχείο, redis, memcache, κλπ.)"
+  REINSTALL_PLUGIN: "Επανεγκατάσταση Πρόσθετου"
+  REINSTALL_THEME: "Εγκατάσταση Θέματος"
+  REINSTALL_THE: "Εγκαταστήστε ξανά το %s"
+  CONFIRM_REINSTALL: "Είστε βέβαιοι ότι θέλετε να επανεγκαταστήσετε το %s;"
+  REINSTALLED_SUCCESSFULLY: "Το %s επανεγκαταστάθηκε με επιτυχία"
+  ERROR_REINSTALLING_THE: "Σφάλμα κατά την επανεγκατάσταση του %s"
+  PACKAGE_X_REINSTALLED_SUCCESSFULLY: "Το πακέτο %s επανεγκαταστάθηκε με επιτυχία"
+  REINSTALLATION_FAILED: "Η επανεγκατάσταση απέτυχε"
+  WARNING_REINSTALL_NOT_LATEST_RELEASE: "Η εγκατεστημένη έκδοση δεν είναι η τελευταία έκδοση. Κάνοντας κλικ στο κουμπί Συνέχεια, θα αφαιρέσετε την τρέχουσα έκδοση και θα εγκαταστήσετε την πιο πρόσφατη διαθέσιμη έκδοση"
+  TOOLS: "Εργαλεία"
+  DIRECT_INSTALL: "Άμεση Εγκατάσταση"
+  NO_PACKAGE_NAME: "Δεν επιλέχθηκε όνομα πακέτου"
+  PACKAGE_EXTRACTION_FAILED: "Η εξαγωγή πακέτου απέτυχε"
+  NOT_VALID_GRAV_PACKAGE: "Δεν είναι ένα έγκυρο πακέτο Grav"
+  NAME_COULD_NOT_BE_DETERMINED: "Δεν μπορούσε να προσδιοριστεί το όνομα"
+  CANNOT_OVERWRITE_SYMLINKS: "Δεν μπορεί να αντικαταστήσει symlinks"
+  ZIP_PACKAGE_NOT_FOUND: "Δεν βρέθηκε ZIP"
+  GPM_OFFICIAL_ONLY: "Επίσημo GPM μόνο"
+  GPM_OFFICIAL_ONLY_HELP: "Επιτρέπει μόνο άμεση εγκατάσταση από το επίσημο αποθετήριο GPM."
+  NO_CHILD_TYPE: "Κανένα θυγατρικό τύπο για αυτό το rawroute"
+  SORTABLE_PAGES: "Ταξινομήσιμες σελίδες:"
+  UNSORTABLE_PAGES: "Μη Ταξινομήσιμες Σελίδες"
+  ADMIN_SPECIFIC_OVERRIDES: "Συγκεκριμένες Παρακάμψεις Διαχειριστή"
+  ADMIN_CHILDREN_DISPLAY_ORDER: "Σειρά Εμφάνισης Θυγατρικών"
+  ADMIN_CHILDREN_DISPLAY_ORDER_HELP: "Η σειρά με την οποία οι θυγατρικές αυτής της σελίδας θα πρέπει να εμφανίζονται στην προβολή 'Σελίδες' του Admin plugin"
+  PWD_PLACEHOLDER: "πολύπλοκη συμβολοσειρά μήκους τουλάχιστον 8 χαρακτήρων"
+  PWD_REGEX: "Κανονική έκφραση (regex) συνθηματικού"
+  PWD_REGEX_HELP: "Από προεπιλογή: Το συνθηματικό πρέπει να περιέχει τουλάχιστον έναν αριθμό, ένα κεφαλαίο και ένα πεζό γράμμα και τουλάχιστον 8 ή περισσότερους χαρακτήρες"
+  USERNAME_PLACEHOLDER: "πεζούς χαρακτήρες μόνο, π.χ. «admin»"
+  USERNAME_REGEX: "Όνομα χρήστη Regex"
+  USERNAME_REGEX_HELP: "Από προεπιλογή: πεζούς μόνο χαρακτήρες, αριθμούς, παύλες και κάτω παύλες. 3 - 16 χαρακτήρες"
+  ENABLE_AUTO_METADATA: "Αυτόματα μεταδεδομένα από Exif"
+  ENABLE_AUTO_METADATA_HELP: "Δημιουργεί αυτόματα αρχεία μεταδεδομένων για εικόνες με exif πληροφορίες"
+  2FA_TITLE: "Έλεγχος ταυτότητας δύο παραγόντων"
+  2FA_INSTRUCTIONS: "##### Έλεγχος Ταυτότητας Δύο Παραγόντων\nΈχετε ενεργοποιημένο τον **ΕΤΔΠ** σε αυτόν το λογαριασμό. Παρακαλώ χρησιμοποιήστε την **ΕΤΔΠ** εφαρμογή σας για να εισαγάγετε τον τρέχοντα **6-ψήφιο κωδικό** ώστε να ολοκληρώσετε τη διαδικασία σύνδεσης."
+  2FA_REGEN_HINT: "Η αναδημιουργία του κωδικού θα απαιτήσει να ενημερώσετε την εφαρμογή επαλήθευσής σας"
+  2FA_LABEL: "Πρόσβαση Διαχειριστή"
+  2FA_FAILED: "Μη έγκυρος κωδικός ελέγχου ταυτότητας δύο παραγόντων, παρακαλούμε προσπαθήστε ξανά..."
+  2FA_ENABLED: "ΕΤΔΠ Ενεργοποιημένος"
+  2FA_CODE_INPUT: "000000"
+  2FA_SECRET: "Μυστικό ΕΤΔΠ"
+  2FA_SECRET_HELP: "Σαρώστε αυτόν τον κωδικό QR με την [εφαρμογή ελέγχου ταυτότητας] (https://learn.getgrav.org/admin-panel/2fa#apps) της επιλογής σας. Επίσης είναι καλή ιδέα να κρατήσετε αντίγραφο ασφαλείας του μυστικού κωδικού σε μια ασφαλή τοποθεσία, σε περίπτωση που χρειαστεί να επανεγκαταστήσετε την εφαρμογή σας. Ελέγξτε τα [εγχειρίδια του Grav] (https://learn.getgrav.org/admin-panel/2fa) για περισσότερες πληροφορίες "
+  2FA_REGENERATE: "Επαναδημιουργία"
+  FORCE_LOWERCASE_URLS: "Εξαναγκασμός πεζών URLs"
+  FORCE_LOWERCASE_URLS_HELP: "Ως προεπιλογή το Grav θα ορίσει τις διαδρομές με πεζά. Αν αυτό είναι απενεργοποιημένο, μπορούν να χρησιμοποιηθούν κεφαλαία"
+  INTL_ENABLED: "Ενσωμάτωση intl ενότητας"
+  INTL_ENABLED_HELP: "Χρησιμοποιήστε την Intl PHP ενότητα και διαταξινόμηση για να ταξινομήσετε τις συλλογές που βασίζονται στο UTF8"
+  VIEW_SITE_TIP: "Δείτε τη σελίδα"
+  TOOLS_DIRECT_INSTALL_TITLE: "Απευθείας Εγκατάσταση των Grav Πακέτων"
+  TOOLS_DIRECT_INSTALL_UPLOAD_TITLE: "Εγκατάσταση Πακέτου με Απευθείας Μεταφόρτωση ZIP"
+  TOOLS_DIRECT_INSTALL_UPLOAD_DESC: "Μπορείτε να εγκαταστήσετε εύκολα ένα έγκυρο <strong>θέμα</strong>, <strong>πρόσθετο</strong>, ή ακόμα και πακέτο zip αναβάθμισης του <strong>Grav</strong> με αυτή τη μέθοδο. Αυτό το πακέτο δε χρειάζεται να είναι καταχωρημένο στο GPM και σας επιτρέπει να επιστρέψετε εύκολα σε προηγούμενη έκδοση ή να εγκαταστήσετε για δοκιμή."
+  TOOLS_DIRECT_INSTALL_URL_TITLE: "Εγκατάσταση Πακέτου μέσω Απομακρυσμένης Αναφοράς URL"
+  TOOLS_DIRECT_INSTALL_URL_DESC: "Εναλλακτικά, μπορείτε επίσης να αναφέρετε το πλήρες URL για το αρχείο ZIP του πακέτου και να το εγκαταστήσετε μέσω απομακρυσμένου URL."
+  TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "Μεταφόρτωση και εγκατάσταση"
+  ROUTE_OVERRIDES: "Παρακάμψεις Διαδρομών"
+  ROUTE_DEFAULT: "Προεπιλεγμένη Διαδρομή"
+  ROUTE_CANONICAL: "Κανονική Διαδρομή"
+  ROUTE_ALIASES: "Ψευδώνυμα Διαδρομών"
+  CONFIGURATION: "Διαμόρφωση"
+  ADMIN_CACHING: "Ενεργοποιήσετε την προσωρινή αποθήκευση Admin"
+  ADMIN_CACHING_HELP: "Η προσωρινή αποθήκευση στο admin μπορεί να ελέγχεται αυτόνομα από τη front-end ιστοσελίδα"
+  CONTENT_PADDING: "Περιθώριο περιεχομένου"
+  CONTENT_PADDING_HELP: "Ενεργοποίηση/απενεργοποίηση του περιθωρίου περιεχομένου γύρω από την περιοχή περιεχομένου για να παρέχει περισσότερο χώρο"
+  TIMEOUT: "Λήξη χρονικού ορίου"
+  TIMEOUT_HELP: "Ορίζει το χρονικό όριο της συνεδρίας σε δευτερόλεπτα"
+  DASHBOARD: "Πίνακας Ελέγχου"
+  NOTIFICATIONS: "Ειδοποιήσεις"

+ 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"

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません