kevin tessier 4 years ago
parent
commit
b857814072
100 changed files with 8537 additions and 5357 deletions
  1. 1 2
      .editorconfig
  2. 8 0
      .github/FUNDING.yml
  3. 4 1
      .gitignore
  4. 1 1
      .htaccess
  5. 3 11
      .travis.yml
  6. 373 1
      CHANGELOG.md
  7. 3 2
      CONTRIBUTING.md
  8. 0 0
      README.md
  9. BIN
      bin/composer.phar
  10. 15 16
      bin/gpm
  11. 31 10
      bin/grav
  12. 41 28
      bin/plugin
  13. 55 26
      composer.json
  14. 446 239
      composer.lock
  15. 0 8
      fixperms.sh
  16. 17 15
      index.php
  17. 4 0
      now.json
  18. 1 0
      robots.txt
  19. 16 0
      system/assets/debugger.css
  20. BIN
      system/assets/grav.png
  21. 125 0
      system/blueprints/config/backups.yaml
  22. 76 0
      system/blueprints/config/scheduler.yaml
  23. 8 0
      system/blueprints/config/security.yaml
  24. 1 1
      system/blueprints/config/site.yaml
  25. 1402 1236
      system/blueprints/config/system.yaml
  26. 121 119
      system/blueprints/user/account.yaml
  27. 2 0
      system/blueprints/user/account_new.yaml
  28. 39 0
      system/blueprints/user/accounts.yaml
  29. 15 0
      system/config/backups.yaml
  30. 7 0
      system/config/security.yaml
  31. 5 5
      system/config/site.yaml
  32. 1 1
      system/config/streams.yaml
  33. 19 2
      system/config/system.yaml
  34. 4 4
      system/defines.php
  35. 17 0
      system/install.php
  36. 62 74
      system/languages/ar.yaml
  37. 62 0
      system/languages/bg.yaml
  38. 61 74
      system/languages/ca.yaml
  39. 143 80
      system/languages/cs.yaml
  40. 61 74
      system/languages/da.yaml
  41. 143 88
      system/languages/de.yaml
  42. 62 21
      system/languages/el.yaml
  43. 120 99
      system/languages/en.yaml
  44. 94 89
      system/languages/es.yaml
  45. 85 0
      system/languages/et.yaml
  46. 62 0
      system/languages/eu.yaml
  47. 62 0
      system/languages/fa.yaml
  48. 133 59
      system/languages/fi.yaml
  49. 122 137
      system/languages/fr.yaml
  50. 63 0
      system/languages/he.yaml
  51. 75 74
      system/languages/hr.yaml
  52. 96 137
      system/languages/hu.yaml
  53. 95 0
      system/languages/id.yaml
  54. 80 0
      system/languages/is.yaml
  55. 100 61
      system/languages/it.yaml
  56. 27 18
      system/languages/ja.yaml
  57. 63 0
      system/languages/ko.yaml
  58. 77 68
      system/languages/lt.yaml
  59. 4 2
      system/languages/nb.yaml
  60. 143 63
      system/languages/nl.yaml
  61. 76 92
      system/languages/no.yaml
  62. 61 74
      system/languages/pl.yaml
  63. 45 78
      system/languages/pt.yaml
  64. 95 100
      system/languages/ro.yaml
  65. 100 80
      system/languages/ru.yaml
  66. 143 41
      system/languages/sk.yaml
  67. 62 0
      system/languages/sl.yaml
  68. 71 61
      system/languages/sv.yaml
  69. 55 74
      system/languages/th.yaml
  70. 62 58
      system/languages/tr.yaml
  71. 62 74
      system/languages/uk.yaml
  72. 62 74
      system/languages/vi.yaml
  73. 62 0
      system/languages/zh-tw.yaml
  74. 144 0
      system/languages/zh.yaml
  75. 19 8
      system/router.php
  76. 188 1257
      system/src/Grav/Common/Assets.php
  77. 198 0
      system/src/Grav/Common/Assets/BaseAsset.php
  78. 40 0
      system/src/Grav/Common/Assets/Css.php
  79. 32 0
      system/src/Grav/Common/Assets/InlineCss.php
  80. 32 0
      system/src/Grav/Common/Assets/InlineJs.php
  81. 36 0
      system/src/Grav/Common/Assets/Js.php
  82. 273 0
      system/src/Grav/Common/Assets/Pipeline.php
  83. 182 0
      system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php
  84. 125 0
      system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php
  85. 343 0
      system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php
  86. 261 0
      system/src/Grav/Common/Backup/Backups.php
  87. 0 144
      system/src/Grav/Common/Backup/ZipBackup.php
  88. 15 3
      system/src/Grav/Common/Browser.php
  89. 208 62
      system/src/Grav/Common/Cache.php
  90. 7 6
      system/src/Grav/Common/Composer.php
  91. 9 10
      system/src/Grav/Common/Config/CompiledBase.php
  92. 13 10
      system/src/Grav/Common/Config/CompiledBlueprints.php
  93. 11 13
      system/src/Grav/Common/Config/CompiledConfig.php
  94. 8 10
      system/src/Grav/Common/Config/CompiledLanguages.php
  95. 19 9
      system/src/Grav/Common/Config/Config.php
  96. 3 2
      system/src/Grav/Common/Config/ConfigFileFinder.php
  97. 30 2
      system/src/Grav/Common/Config/Languages.php
  98. 57 19
      system/src/Grav/Common/Config/Setup.php
  99. 200 26
      system/src/Grav/Common/Data/Blueprint.php
  100. 172 24
      system/src/Grav/Common/Data/BlueprintSchema.php

+ 1 - 2
.editorconfig

@@ -13,6 +13,5 @@ indent_style = space
 indent_size = 4
 indent_size = 4
 
 
 # 2 space indentation
 # 2 space indentation
-[*.yaml, *.yml]
-indent_style = space
+[*.{yaml,.yml}]
 indent_size = 2
 indent_size = 2

+ 8 - 0
.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

+ 4 - 1
.gitignore

@@ -23,6 +23,8 @@ user/data/*
 !user/data/.*
 !user/data/.*
 user/plugins/*
 user/plugins/*
 !user/plugins/.*
 !user/plugins/.*
+user/themes/*
+!user/themes/.*
 user/localhost/config/security.yaml
 user/localhost/config/security.yaml
 user/config/security.yaml
 user/config/security.yaml
 
 
@@ -36,8 +38,9 @@ Thumbs.db
 # phpstorm
 # phpstorm
 .idea/*
 .idea/*
 
 
+# testing stuff
 tests/_output/*
 tests/_output/*
 tests/_support/_generated/*
 tests/_support/_generated/*
 tests/cache/*
 tests/cache/*
 tests/error.log
 tests/error.log
-/system/templates/testing
+system/templates/testing/*

+ 1 - 1
.htaccess

@@ -9,7 +9,7 @@ RewriteEngine On
 # along the lines of: RewriteBase /<your_sub_folder>
 # along the lines of: RewriteBase /<your_sub_folder>
 ##
 ##
 
 
-RewriteBase /
+# RewriteBase /
 
 
 ## End - RewriteBase
 ## End - RewriteBase
 
 

+ 3 - 11
.travis.yml

@@ -1,9 +1,8 @@
 language: php
 language: php
 php:
 php:
-  - '5.6'
-  - '7.0.21'
   - '7.1'
   - '7.1'
   - '7.2'
   - '7.2'
+  - '7.3'
 branches:
 branches:
   only:
   only:
     - develop
     - develop
@@ -13,13 +12,6 @@ notifications:
   email:
   email:
     on_success: never
     on_success: never
     on_failure: always
     on_failure: always
-  hipchat:
-  # hipchat_api@grav
-    rooms:
-      - secure: "bqO0wM1B7bJnQw2fuhquSXEqI9gw6WmFytIh9sEWXzbYTzTUP5t0PcKOd3FT2BNMRaDxPJLVl+vG/oqmqDUBkEmOGcG504IQjeNzZqnMz0tXQMIcCc22Las9tFfc4Jf6RVi/qGomFtHGE9Wgii+TAN4zqZaufbNjwd8SyjO0+W8="
-    template:
-      - '%{repository}#%{build_number} (%{branch}): Travis Job Finished [%{duration}] (<a href="%{build_url}">Details</a>)'
-    format: html
   slack:
   slack:
     secure: dowksPsxxCxGKT6nis5hUgkp6+ZDAhoqzQHF9rJnx4hx0iEygPhVBs7pKl9yL2jubYJoLs+EXwE7z1dYgDAEJh4BnfrCokCMLpFGcxVxQC/HeAUdSQ2/RtdBYR5PRT75ScaFpqM/SfXXZVtnwVXAw9Z+JC6BjQ9vmn23m51Jw4k=
     secure: dowksPsxxCxGKT6nis5hUgkp6+ZDAhoqzQHF9rJnx4hx0iEygPhVBs7pKl9yL2jubYJoLs+EXwE7z1dYgDAEJh4BnfrCokCMLpFGcxVxQC/HeAUdSQ2/RtdBYR5PRT75ScaFpqM/SfXXZVtnwVXAw9Z+JC6BjQ9vmn23m51Jw4k=
 env:
 env:
@@ -53,7 +45,7 @@ before_install:
   - if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then
   - if [ $TRAVIS_BRANCH == 'develop' ] || [ $TRAVIS_PULL_REQUEST != 'false' ]; then
         composer install --dev --prefer-dist;
         composer install --dev --prefer-dist;
     fi
     fi
-  - if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
+  - if [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
         export TRAVIS_TAG=$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4);
         export TRAVIS_TAG=$(curl --fail --user "${GH_API_USER}" -s https://api.github.com/repos/getgrav/grav/releases/latest | grep tag_name | head -n 1 | cut -d '"' -f 4);
         eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.8 bash)";
         eval "$(curl -sL https://raw.githubusercontent.com/travis-ci/gimme/master/gimme | GIMME_GO_VERSION=1.8 bash)";
         go get github.com/aktau/github-release;
         go get github.com/aktau/github-release;
@@ -70,7 +62,7 @@ script:
         vendor/bin/codecept run;
         vendor/bin/codecept run;
     fi
     fi
   - echo "Latest Release Tag - ${TRAVIS_TAG}"
   - echo "Latest Release Tag - ${TRAVIS_TAG}"
-  - if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "5.6" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
+  - if [ ! -z "$TRAVIS_TAG" ] && [ $TRAVIS_BRANCH != 'develop' ] && [ $TRAVIS_PHP_VERSION == "7.1" ] && [ $TRAVIS_PULL_REQUEST == "false" ]; then
       FILES="$RT_DEVTOOLS/grav-dist/*.zip";
       FILES="$RT_DEVTOOLS/grav-dist/*.zip";
       for file in ${FILES[@]}; do
       for file in ${FILES[@]}; do
         NAME=${file##*/};
         NAME=${file##*/};

+ 373 - 1
CHANGELOG.md

@@ -1,3 +1,375 @@
+# v1.6.16
+## 09/19/2019
+
+1. [](#bugfix)
+    * Fixed Flex user creation if file storage is being used [#2444](https://github.com/getgrav/grav/issues/2444)
+    * Fixed `Badly encoded JSON data` warning when uploading files [#2663](https://github.com/getgrav/grav/issues/2663)
+
+# v1.6.15
+## 08/20/2019
+
+1. [](#improved)
+    * Improved robots.txt [#2632](https://github.com/getgrav/grav/issues/2632)
+1. [](#bugfix)
+    * Fixed broken markdown Twig tag [#2635](https://github.com/getgrav/grav/issues/2635)
+    * Force Symfony 4.2 in Grav 1.6 to remove a bunch of deprecated messages
+
+# v1.6.14
+## 08/18/2019
+
+1. [](#bugfix)
+    * Actually include fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627)
+
+# v1.6.13
+## 08/16/2019
+
+1. [](#bugfix)
+    * Regression fix for `system\router.php` [#2627](https://github.com/getgrav/grav/issues/2627)
+
+# v1.6.12
+## 08/14/2019
+
+1. [](#new)
+    * Added support for custom `FormFlash` save locations
+    * Added a new `Utils::arrayLower()` method for lowercasing arrays
+    * Support new GRAV_BASEDIR environment variable [#2541](https://github.com/getgrav/grav/pull/2541)
+    * Allow users to override plugin handler priorities [#2165](https://github.com/getgrav/grav/pull/2165)
+1. [](#improved)
+    * Use new `Utils::getSupportedPageTypes()` to enforce `html,htm` at the front of the list [#2531](https://github.com/getgrav/grav/issues/2531)  
+    * Updated vendor libraries
+    * Markdown filter is now page-aware so that it works with modular references [admin#1731](https://github.com/getgrav/grav-plugin-admin/issues/1731)
+    * Check of `GRAV_USER_INSTANCE` constant is already defined [#2621](https://github.com/getgrav/grav/pull/2621)
+1. [](#bugfix)
+    * Fixed some potential issues when `$grav['user']` is not set
+    * Fixed error when calling `Media::add($name, null)`
+    * Fixed `url()` returning wrong path if using stream with grav root path in it, eg: `user-data://shop` when Grav is in `/shop`
+    * Fixed `url()` not returning a path to non-existing file (`user-data://shop` => `/user/data/shop`) if it is set to fail gracefully
+    * Fixed `url()` returning false on unknown streams, such as `ftp://domain.com`, they should be treated as external URL
+    * Fixed Flex User to have permissions to save and delete his own user
+    * Fixed new Flex User creation not being possible because of username could not be given
+    * Fixed fatal error 'Expiration date must be an integer, a DateInterval or null, "double" given' [#2529](https://github.com/getgrav/grav/issues/2529)
+    * Fixed non-existing Flex object having a bad media folder
+    * Fixed collections using `page@.self:` should allow modular pages if requested
+    * Fixed an error when trying to delete a file from non-existing Flex Object
+    * Fixed `FlexObject::exists()` failing sometimes just after the object has been saved
+    * Fixed CSV formatter not encoding strings with `"` and `,` properly
+    * Fixed var order in `Validation.php` [#2610](https://github.com/getgrav/grav/issues/2610)
+    
+# v1.6.11
+## 06/21/2019
+
+1. [](#new)
+    * Added `FormTrait::getAllFlashes()` method to get all the available form flash objects for the form
+    * Added creation and update timestamps to `FormFlash` objects
+1. [](#improved)
+    * Added `FormFlashInterface`, changed constructor to take `$config` array
+1. [](#bugfix)
+    * Fixed error in `ImageMedium::url()` if the image cache folder does not exist
+    * Fixed empty form flash name after file upload or form state update
+    * Fixed a bug in `Route::withParam()` method
+    * Fixed issue with `FormFlash` objects when there is no session initialized
+
+# v1.6.10
+## 06/14/2019
+
+1. [](#improved)
+    * Added **page blueprints** to `YamlLinter` CLI and Admin reports
+    * Removed `Gitter` and `Slack` [#2502](https://github.com/getgrav/grav/issues/2502)
+    * Optimizations for Plugin/Theme loading
+    * Generalized markdown classes so they can be used outside of `Page` scope with a custom `Excerpts` class instance
+    * Change minimal port number to 0 (unix socket) [#2452](https://github.com/getgrav/grav/issues/2452)
+1. [](#bugfix)
+    * Force question to install demo content in theme update [#2493](https://github.com/getgrav/grav/issues/2493)
+    * Fixed GPM errors from blueprints not being logged [#2505](https://github.com/getgrav/grav/issues/2505)
+    * Don't error when IP is invalid [#2507](https://github.com/getgrav/grav/issues/2507)
+    * Fixed regression with `bin/plugin` not listing the plugins available (1c725c0)
+    * Fixed bitwise operator in `TwigExtension::exifFunc()` [#2518](https://github.com/getgrav/grav/issues/2518)
+    * Fixed issue with lang prefix incorrectly identifying as admin [#2511](https://github.com/getgrav/grav/issues/2511)
+    * Fixed issue with `U0ils::pathPrefixedBYLanguageCode()` and trailing slash [#2510](https://github.com/getgrav/grav/issues/2511) 
+    * Fixed regresssion issue of `Utils::Url()` not returning `false` on failure. Added new optional `fail_gracefully` 3rd attribute to return string that caused failure [#2524](https://github.com/getgrav/grav/issues/2524)
+
+# v1.6.9
+## 05/09/2019
+
+1. [](#new)
+    * Added `Route::withoutParams()` methods
+    * Added `Pages::setCheckMethod()` method to override page configuration in Admin Plugin
+    * Added `Cache::clearCache('invalidate')` parameter for just invalidating the cache without deleting any cached files
+    * Made `UserCollectionInderface` to extend `Countable` to get the count of existing users
+1. [](#improved)
+    * Flex admin: added default search options for flex objects
+    * Flex collection and object now fall back to the default template if template file doesn't exist
+    * Updated Vendor libraries including Twig 1.40.1
+    * Updated language files from `https://crowdin.com/project/grav-core`
+1. [](#bugfix)
+    * Fixed `$grav['route']` from being modified when the route instance gets modified
+    * Fixed Assets options array mixed with standalone priority [#2477](https://github.com/getgrav/grav/issues/2477)
+    * Fix for `avatar_url` provided by 3rd party providers
+    * Fixed non standard `lang` code lengths in `Utils` and `Session` detection
+    * Fixed saving a new object in Flex `SimpleStorage`
+    * Fixed exception in `Flex::getDirectories()` if the first parameter is set
+    * Output correct "Last Updated" in `bin/gpm info` command
+    * Checkbox getting interpreted as string, so created new `Validation::filterCheckbox()`
+    * Fixed backwards compatibility to `select` field with `selectize.create` set to true [git-sync#141](https://github.com/trilbymedia/grav-plugin-git-sync/issues/141)
+    * Fixed `YamlFormatter::decode()` to always return array [#2494](https://github.com/getgrav/grav/pull/2494)
+    * Fixed empty `$grav['request']->getAttribute('route')->getExtension()`
+
+# v1.6.8
+## 04/23/2019
+
+1. [](#new)
+    * Added `FlexCollection::filterBy()` method
+1. [](#bugfix)
+    * Revert `Use Null Coalesce Operator` [#2466](https://github.com/getgrav/grav/pull/2466)
+    * Fixed `FormTrait::render()` not providing config variable
+    * Updated `bin/grav clean` to clear `cache/compiled` and `user/config/security.yaml`
+
+# v1.6.7
+## 04/22/2019
+
+1. [](#new)
+    * Added a new `bin/grav yamllinter` CLI command to find YAML Linting issues [#2468](https://github.com/getgrav/grav/issues/2468#issuecomment-485151681)
+1. [](#improved)
+    * Improve `FormTrait` backwards compatibility with existing forms
+    * Added a new `Utils::getSubnet()` function for IPv4/IPv6 parsing [#2465](https://github.com/getgrav/grav/pull/2465)
+1. [](#bugfix)
+    * Remove disabled fields from the form schema
+    * Fix issue when excluding `inlineJs` and `inlineCss` from Assets pipeline [#2468](https://github.com/getgrav/grav/issues/2468)
+    * Fix for manually set position on external URLs [#2470](https://github.com/getgrav/grav/issues/2470)
+
+# v1.6.6
+## 04/17/2019
+
+1. [](#new)
+    * `FormInterface` now implements `RenderInterface`
+    * Added new `FormInterface::getTask()` method which reads the task from `form.task` in the blueprint
+1. [](#improved)
+    * Updated vendor libraries to latest
+1. [](#bugfix)
+    * Rollback `redirect_default_route` logic as it has issues with multi-lang [#2459](https://github.com/getgrav/grav/issues/2459)
+    * Fix potential issue with `|contains` Twig filter on PHP 7.3
+    * Fixed bug in text field filtering: return empty string if value isn't a string or number [#2460](https://github.com/getgrav/grav/issues/2460)
+    * Force Asset `priority` to be an integer and not throw error if invalid string passed [#2461](https://github.com/getgrav/grav/issues/2461)
+    * Fixed bug in text field filtering: return empty string if value isn't a string or number
+    * Fixed `FlexForm` missing getter methods for defining form variables
+
+# v1.6.5
+## 04/15/2019
+
+1. [](#bugfix)
+    * Backwards compatiblity with old `Uri::__toString()` output
+
+# v1.6.4
+## 04/15/2019
+
+1. [](#bugfix)
+    * Improved `redirect_default_route` logic as well as `Uri::toArray()` to take into account `root_path` and `extension`
+    * Rework logic to pull out excluded files from pipeline more reliably [#2445](https://github.com/getgrav/grav/issues/2445)
+    * Better logic in `Utils::normalizePath` to handle externals properly [#2216](https://github.com/getgrav/grav/issues/2216)
+    * Fixed to force all `Page::taxonomy` to be treated as strings [#2446](https://github.com/getgrav/grav/issues/2446)
+    * Fixed issue with `Grav['user']` not being available [form#332](https://github.com/getgrav/grav-plugin-form/issues/332)
+    * Updated rounding logic for `Utils::parseSize()` [#2394](https://github.com/getgrav/grav/issues/2394)
+    * Fixed Flex simple storage not being properly initialized if used with caching
+
+# v1.6.3
+## 04/12/2019
+
+1. [](#new)
+    * Added `Blueprint::addDynamicHandler()` method to allow custom dynamic handlers, for example `custom-options@: getCustomOptions`
+1. [](#bugfix)
+    * Missed a `CacheCommand` reference in `bin/grav` [#2442](https://github.com/getgrav/grav/issues/2442)
+    * Fixed issue with `Utils::normalizePath` messing with external URLs [#2216](https://github.com/getgrav/grav/issues/2216)
+    * Fix for `vUndefined` versions when upgrading
+
+# v1.6.2
+## 04/11/2019
+
+1. [](#bugfix)
+    * Revert renaming of `ClearCacheCommand` to ensure CLI GPM upgrades go smoothly
+
+# v1.6.1
+## 04/11/2019
+
+1. [](#improved)
+    * Improved CSS for the bottom filter bar of DebugBar
+1. [](#bugfix)
+    * Fixed issue with `@import` not being added to top of pipelined css [#2440](https://github.com/getgrav/grav/issues/2440)
+
+# v1.6.0
+## 04/11/2019
+
+1. [](#new)
+    * Set minimum requirements to [PHP 7.1.3](https://getgrav.org/blog/raising-php-requirements-2018)
+    * New `Scheduler` functionality for periodic jobs
+    * New `Backup` functionality with multiple backup profiles and scheduler integration
+    * Refactored `Assets Manager` to be more powerful and flexible
+    * Updated Doctrine Collections to 1.6
+    * Updated Doctrine Cache to 1.8
+    * Updated Symfony Components to 4.2
+    * Added new Cache purge functionality old cache manually via CLI/Admin as well as scheduler integration
+    * Added new `{% throw 404 'Not Found' %}` twig tag (with custom code/message)
+    * Added `Grav\Framework\File` classes for handling YAML, Markdown, JSON, INI and PHP serialized files
+    * Added `Grav\Framework\Collection\AbstractIndexCollection` class
+    * Added `Grav\Framework\Object\ObjectIndex` class
+    * Added `Grav\Framework\Flex` classes
+    * Added support for hiding form fields in blueprints by using dynamic property like `security@: admin.foobar`, `scope@: object` or `scope-ignore@: object` to any field
+    * New experimental **FlexObjects** powered `Users` for increased performance and capability (**disabled** by default)
+    * Added PSR-7 and PSR-15 classes
+    * Added `Grav\Framework\DI\Container` class
+    * Added `Grav\Framework\RequestHandler\RequestHandler` class
+    * Added `Page::httpResponseCode()` and `Page::httpHeaders()` methods
+    * Added `Grav\Framework\Form\Interfaces\FormInterface`
+    * Added `Grav\Framework\Form\Interfaces\FormFactoryInterface`
+    * Added `Grav\Framework\Form\FormTrait`
+    * Added `Page::forms()` method to get normalized list of all form headers defined in the page
+    * Added `onPageAction`, `onPageTask`, `onPageAction.{$action}` and `onPageTask.{$task}` events
+    * Added `Blueprint::processForm()` method to filter form inputs
+    * Move `processMarkdown()` method from `TwigExtension` to more general `Utils` class
+    * Added support to include extra files into `Media` (such as uploaded files)
+    * Added form preview support for `FlexObject`, including a way to render newly uploaded files before saving them
+    * Added `FlexObject::getChanges()` to determine what fields change during an update
+    * Added `arrayDiffMultidimensional`, `arrayIsAssociative`, `arrayCombine` Util functions
+    * New `$grav['users']` service to allow custom user classes implementing `UserInterface`
+    * Added `LogViewer` helper class and CLI command: `bin/grav logviewer`
+    * Added `select()` and `unselect()` methods to `CollectionInterface` and its base classes
+    * Added `orderBy()` and `limit()` methods to `ObjectCollectionInterface` and its base classes
+    * Added `user-data://` which is a writable stream (`user://data` is not and should be avoided)
+    * Added support for `/action:{$action}` (like task but used without nonce when only receiving data)
+    * Added `onAction.{$action}` event
+    * Added `Grav\Framework\Form\FormFlash` class to contain AJAX uploaded files in more reliable way
+    * Added `Grav\Framework\Form\FormFlashFile` class which implements `UploadedFileInterface` from PSR-7
+    * Added `Grav\Framework\Filesystem\Filesystem` class with methods to manipulate stream URLs
+    * Added new `$grav['filesystem']` service using an instance of the new `Filesystem` object
+    * Added `{% render object layout: 'default' with { variable: true } %}` for Flex objects and collections
+    * Added `$grav->setup()` to simplify CLI and custom access points
+    * Added `CsvFormatter` and `CsvFile` classes
+    * Added new system config option to `pages.hide_empty_folders` if a folder has no valid `.md` file available. Default behavior is `false` for compatibility.
+    * Added new system config option for `languages.pages_fallback_only` forcing only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing
+    * Added `Utils::arrayFlattenDotNotation()` and `Utils::arrayUnflattenDotNotation()` helper methods
+1. [](#improved)
+    * Add the page to onMarkdownInitialized event [#2412](https://github.com/getgrav/grav/issues/2412)
+    * Doctrine filecache is now namespaced with prefix to support purging
+    * Register all page types into `blueprint://pages` stream
+    * Removed `apc` and `xcache` support, made `apc` alias of `apcu`
+    * Support admin and regular translations via the `|t` twig filter and `t()` twig function
+    * Improved Grav Core installer/updater to run installer script
+    * Updated vendor libraries including Symfony `4.2.3`
+    * Renamed old `User` class to `Grav\Common\User\DataUser\User` with multiple improvements and small fixes
+    * `User` class now acts as a compatibility layer to older versions of Grav
+    * Deprecated `new User()`, `User::load()`, `User::find()` and `User::delete()` in favor of `$grav['users']` service
+    * `Media` constructor has now support to not to initialize the media objects
+    * Cleanly handle session corruption due to changing Flex object types
+    * Added `FlexObjectInterface::getDefaultValue()` and `FormInterface::getDefaultValue()`
+    * Added new `onPageContent()` event for every call to `Page::content()`
+    * Added phpstan: PHP Static Analysis Tool [#2393](https://github.com/getgrav/grav/pull/2393)
+    * Added `composer test-plugins` to test plugin issues with the current version of Grav
+    * Added `Flex::getObjects()` and `Flex::getMixedCollection()` methods for co-mingled collections
+    * Added support to use single Flex key parameter in `Flex::getObject()` method
+    * Added `FlexObjectInterface::search()` and `FlexCollectionInterface::search()` methods
+    * Override `system.media.upload_limit` with PHP's `post_max_size` or `upload_max_filesize`
+    * Class `Grav\Common\Page\Medium\AbstractMedia` now use array traits instead of extending `Grav\Common\Getters`
+    * Implemented `Grav\Framework\Psr7` classes as `Nyholm/psr7` decorators
+    * Added a new `cache-clear` scheduled job to go along with `cache-purge`
+    * Renamed `Grav\Framework\File\Formatter\FormatterInterface` to `Grav\Framework\File\Interfaces\FileFormatterInterface`
+    * Improved `File::save()` to use a temporary file if file isn't locked
+    * Improved `|t` filter to better support admin `|tu` style filter if in admin
+    * Update all classes to rely on `PageInterface` instead of `Page` class
+    * Better error checking in `bin/plugin` for existence and enabled
+    * Removed `media.upload_limit` references
+    * Twig `nicenumber`: do not use 0 + string casting hack
+    * Converted Twig tags to use namespaced Twig classes
+    * Site shows error on page rather than hard-crash when page has invalid frontmatter [#2343](https://github.com/getgrav/grav/issues/2343)
+    * Added `languages.default_lang` option to override the default lang (usually first supported language)
+    * Added `Content-Type: application/json` body support for PSR-7 `ServerRequest`
+    * Remove PHP time limit in `ZipArchive`
+    * DebugBar: Resolve twig templates in deprecated backtraces in order to help locating Twig issues
+    * Added `$grav['cache']->getSimpleCache()` method for getting PSR-16 compatible cache
+    * MediaTrait: Use PSR-16 cache
+    * Improved `Utils::normalizePath()` to support non-protocol URLs
+    * Added ability to reset `Page::metadata` to allow rebuilding from automatically generated values
+    * Added back missing `page.types` field in system content configuration [admin#1612](https://github.com/getgrav/grav-plugin-admin/issues/1612)
+    * Console commands: add method for invalidating cache
+    * Updated languages
+    * Improved `$page->forms()` call, added `$page->addForms()`
+    * Updated languages from crowdin
+    * Fixed `ImageMedium` constructor warning when file does not exist
+    * Improved `Grav\Common\User` class; added `$user->update()` method
+    * Added trim support for text input fields `validate: trim: true`
+    * Improved `Grav\Framework\File\Formatter` classes to have abstract parent class and some useful methods
+    * Support negotiated content types set via the Request `Accept:` header
+    * Support negotiated language types set via the Request `Accept-Language:` header
+    * Cleaned up and sorted the Service `idMap`
+    * Updated `Grav` container object to implement PSR-11 `ContainerInterface`
+    * Updated Grav `Processor` classes to implement PSR-15 `MiddlewareInterface`
+    * Make `Data` class to extend `JsonSerializable`
+    * Modified debugger icon to use retina space-dude version
+    * Added missing `Video::preload()` method
+    * Set session name based on `security.salt` rather than `GRAV_ROOT` [#2242](https://github.com/getgrav/grav/issues/2242)
+    * Added option to configure list of `xss_invalid_protocols` in `Security` config [#2250](https://github.com/getgrav/grav/issues/2250)
+    * Smarter `security.salt` checking now we use `security.yaml` for other options
+    * Added apcu autoloader optimization
+    * Additional helper methods in `Language`, `Languages`, and `LanguageCodes` classes
+    * Call `onFatalException` event also on internal PHP errors
+    * Built-in PHP Webserver: log requests before handling them
+    * Added support for syslog and syslog facility logging (default: 'file')
+    * Improved usability of `System` configuration blueprint with side-tabs
+ 1. [](#bugfix)
+    * Fixed issue with `Truncator::truncateWords` and `Truncator::truncateLetters` when string not wrapped in tags [#2432](https://github.com/getgrav/grav/issues/2432)
+    * Fixed `Undefined method closure::fields()` when getting avatar for user, thanks @Romarain [#2422](https://github.com/getgrav/grav/issues/2422)
+    * Fixed cached images not being updated when source image is modified
+    * Fixed deleting last list item in the form
+    * Fixed issue with `Utils::url()` method would append extra `base_url` if URL already included it
+    * Fixed `mkdir(...)` race condition
+    * Fixed `Obtaining write lock failed on file...`
+    * Fixed potential undefined property in `onPageNotFound` event handling
+    * Fixed some potential issues/bugs found by phpstan
+    * Fixed regression in GPM packages casted to Array (ref, getgrav/grav-plugin-admin@e3fc4ce)
+    * Fixed session_start(): Setting option 'session.name' failed [#2408](https://github.com/getgrav/grav/issues/2408)
+    * Fixed validation for select field type with selectize
+    * Fixed validation for boolean toggles
+    * Fixed non-namespaced exceptions in scheduler
+    * Fixed trailing slash redirect in multlang environment [#2350](https://github.com/getgrav/grav/issues/2350)
+    * Fixed some issues related to Medium objects losing query string attributes
+    * Broke out Medium timestamp so it's not cleared on `reset()`s
+    * Fixed issue with `redirect_trailing_slash` losing query string [#2269](https://github.com/getgrav/grav/issues/2269)
+    * Fixed failed login if user attempts to log in with upper case non-english letters
+    * Removed extra authenticated/authorized fields when saving existing user from a form
+    * Fixed `Grav\Framework\Route::__toString()` returning relative URL, not relative route
+    * Fixed handling of `append_url_extension` inside of `Page::templateFormat()` [#2264](https://github.com/getgrav/grav/issues/2264)
+    * Fixed a broken language string [#2261](https://github.com/getgrav/grav/issues/2261)
+    * Fixed clearing cache having no effect on Doctrine cache
+    * Fixed `Medium::relativePath()` for streams
+    * Fixed `Object` serialization breaking if overriding `jsonSerialize()` method
+    * Fixed `YamlFormatter::decode()` when calling `init_set()` with integer
+    * Fixed session throwing error in CLI if initialized
+    * Fixed `Uri::hasStandardPort()` to support reverse proxy configurations [#1786](https://github.com/getgrav/grav/issues/1786)
+    * Use `append_url_extension` from page header to set template format if set [#2604](https://github.com/getgrav/grav/pull/2064)
+    * Fixed some bugs in Grav environment selection logic
+    * Use login provider User avatar if set
+    * Fixed `Folder::doDelete($folder, false)` removing symlink when it should not
+    * Fixed asset manager to not add empty assets when they don't exist in the filesystem
+    * Update `script` and `style` Twig tags to use the new `Assets` classes
+    * Fixed asset pipeline to rewrite remote URLs as well as local [#2216](https://github.com/getgrav/grav/issues/2216)
+
+# v1.5.10
+## 03/21/2019
+
+1. [](#new)
+    * Added new `deferred` Twig extension
+
+# v1.5.9
+## 03/20/2019
+
+1. [](#new)
+    * Added new `onPageContent()` event for every call to `Page::content()`
+1. [](#improved)
+    * Fixed phpdoc generation
+    * Updated vendor libraries
+    * Force Toolbox v1.4.2
+1. [](#bugfix)
+    * EXIF fix for streams
+    * Fix for User avatar not working due to uppercase or spaces in email [#2403](https://github.com/getgrav/grav/pull/2403)
+
 # v1.5.8
 # v1.5.8
 ## 02/07/2019
 ## 02/07/2019
 
 
@@ -16,7 +388,7 @@
     * Updated vendor libraries
     * Updated vendor libraries
 1. [](#bugfix)
 1. [](#bugfix)
     * Support spaces with filenames in responsive images [#2300](https://github.com/getgrav/grav/pull/2300)
     * Support spaces with filenames in responsive images [#2300](https://github.com/getgrav/grav/pull/2300)
-    
+
 # v1.5.6
 # v1.5.6
 ## 12/14/2018
 ## 12/14/2018
 
 

+ 3 - 2
CONTRIBUTING.md

@@ -29,7 +29,7 @@ The issue tracker is the preferred channel for [bug reports](#bugs),
 requests](#pull-requests), but please respect the following restrictions:
 requests](#pull-requests), but please respect the following restrictions:
 
 
 * Please **do not** use the issue tracker for support requests. Use
 * Please **do not** use the issue tracker for support requests. Use
-  [the Forum](http://getgrav.org/forum) or [the Gitter chat](https://gitter.im/getgrav/grav).
+  [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/).
 
 
 
 
 <a name="bugs"></a>
 <a name="bugs"></a>
@@ -110,7 +110,8 @@ Good pull requests - patches, improvements, new features - are a fantastic
 help. They should remain focused in scope and avoid containing unrelated
 help. They should remain focused in scope and avoid containing unrelated
 commits.
 commits.
 
 
-**Please ask first** in [Slack](https://getgrav.org/slack) or in the Forum before embarking on any significant pull request (e.g.
+**Please ask first** in [the Forum](http://getgrav.org/forum) or [the Chat](https://chat.getgrav.org/) 
+before embarking on any significant pull request (e.g.
 implementing features, refactoring code..),
 implementing features, refactoring code..),
 otherwise you risk spending a lot of time working on something that the
 otherwise you risk spending a lot of time working on something that the
 project's developers might not want to merge into the project.
 project's developers might not want to merge into the project.

File diff suppressed because it is too large
+ 0 - 0
README.md


BIN
bin/composer.phar


+ 15 - 16
bin/gpm

@@ -1,26 +1,24 @@
 #!/usr/bin/env php
 #!/usr/bin/env php
 <?php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')){
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 
 use Grav\Common\Composer;
 use Grav\Common\Composer;
-use Grav\Common\Config\Setup;
+use Symfony\Component\Console\Application;
+use Grav\Common\Grav;
+
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
 
 
-if (!file_exists(__DIR__ . '/../vendor')){
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
     // Before we can even start, we need to run composer first
     // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
     $composer = Composer::getComposerExecutor();
     $composer = Composer::getComposerExecutor();
     echo "Preparing to install vendor dependencies...\n\n";
     echo "Preparing to install vendor dependencies...\n\n";
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo "\n\n";
     echo "\n\n";
 }
 }
 
 
-use Symfony\Component\Console\Application;
-use Grav\Common\Grav;
-
-$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
@@ -30,7 +28,7 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
     date_default_timezone_set('UTC');
 }
 }
 
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 }
 
 
@@ -48,15 +46,16 @@ $climate->arguments->add([
     ]
     ]
 ]);
 ]);
 $climate->arguments->parse();
 $climate->arguments->parse();
-$environment = $climate->arguments->get('environment');
 
 
 // Set up environment based on params.
 // Set up environment based on params.
-Setup::$environment = $environment;
+$environment = $climate->arguments->get('environment');
 
 
 $grav = Grav::instance(array('loader' => $autoload));
 $grav = Grav::instance(array('loader' => $autoload));
-$grav['uri']->init();
+$grav->setup($environment);
+
 $grav['config']->init();
 $grav['config']->init();
-$grav['streams'];
+$grav['uri']->init();
+$grav['users'];
 
 
 $app = new Application('Grav Package Manager', GRAV_VERSION);
 $app = new Application('Grav Package Manager', GRAV_VERSION);
 $app->addCommands(array(
 $app->addCommands(array(

+ 31 - 10
bin/grav

@@ -1,34 +1,52 @@
 #!/usr/bin/env php
 #!/usr/bin/env php
 <?php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')){
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 
 use Grav\Common\Composer;
 use Grav\Common\Composer;
+use Grav\Common\Grav;
+use League\CLImate\CLImate;
+use Symfony\Component\Console\Application;
+
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
 
 
-if (!file_exists(__DIR__ . '/../vendor')){
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
     // Before we can even start, we need to run composer first
     // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
     $composer = Composer::getComposerExecutor();
     $composer = Composer::getComposerExecutor();
     echo "Preparing to install vendor dependencies...\n\n";
     echo "Preparing to install vendor dependencies...\n\n";
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
     echo "\n\n";
     echo "\n\n";
 }
 }
 
 
-use Symfony\Component\Console\Application;
-
-require_once __DIR__ . '/../vendor/autoload.php';
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
 }
 }
 
 
+$climate = new League\CLImate\CLImate;
+$climate->arguments->add([
+    'environment' => [
+        'prefix'        => 'e',
+        'longPrefix'    => 'env',
+        'description'   => 'Configuration Environment',
+        'defaultValue'  => 'localhost'
+    ]
+]);
+$climate->arguments->parse();
+
+// Set up environment based on params.
+$environment = $climate->arguments->get('environment');
+
+$grav = Grav::instance(array('loader' => $autoload));
+$grav->setup($environment);
+
 if (!ini_get('date.timezone')) {
 if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
     date_default_timezone_set('UTC');
 }
 }
 
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 }
 
 
@@ -41,6 +59,9 @@ $app->addCommands(array(
     new \Grav\Console\Cli\ClearCacheCommand(),
     new \Grav\Console\Cli\ClearCacheCommand(),
     new \Grav\Console\Cli\BackupCommand(),
     new \Grav\Console\Cli\BackupCommand(),
     new \Grav\Console\Cli\NewProjectCommand(),
     new \Grav\Console\Cli\NewProjectCommand(),
+    new \Grav\Console\Cli\SchedulerCommand(),
     new \Grav\Console\Cli\SecurityCommand(),
     new \Grav\Console\Cli\SecurityCommand(),
+    new \Grav\Console\Cli\LogViewerCommand(),
+    new \Grav\Console\Cli\YamlLinterCommand(),
 ));
 ));
 $app->run();
 $app->run();

+ 41 - 28
bin/plugin

@@ -1,30 +1,28 @@
 #!/usr/bin/env php
 #!/usr/bin/env php
 <?php
 <?php
-define('GRAV_CLI', true);
-
-if (!file_exists(__DIR__ . '/../vendor')) {
-    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
-}
 
 
 use Grav\Common\Composer;
 use Grav\Common\Composer;
-
-if (!file_exists(__DIR__ . '/../vendor')) {
-    // Before we can even start, we need to run composer first
-    $composer = Composer::getComposerExecutor();
-    echo "Preparing to install vendor dependencies...\n\n";
-    echo system($composer . ' --working-dir="' . __DIR__ . '/../" --no-interaction --no-dev --prefer-dist -o install');
-    echo "\n\n";
-}
-
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Application;
 use Symfony\Component\Console\Input\ArgvInput;
 use Symfony\Component\Console\Input\ArgvInput;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Console\Output\ConsoleOutput;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Symfony\Component\Console\Formatter\OutputFormatterStyle;
 use Grav\Common\Grav;
 use Grav\Common\Grav;
-use Grav\Common\Config\Setup;
 use Grav\Common\Filesystem\Folder;
 use Grav\Common\Filesystem\Folder;
 
 
-$autoload = require_once(__DIR__ . '/../vendor/autoload.php');
+\define('GRAV_CLI', true);
+\define('GRAV_REQUEST_TIME', microtime(true));
+
+if (!file_exists(__DIR__ . '/../vendor/autoload.php')){
+    // Before we can even start, we need to run composer first
+    require_once __DIR__ . '/../system/src/Grav/Common/Composer.php';
+
+    $composer = Composer::getComposerExecutor();
+    echo "Preparing to install vendor dependencies...\n\n";
+    echo system($composer.' --working-dir="'.__DIR__.'/../" --no-interaction --no-dev --prefer-dist -o install');
+    echo "\n\n";
+}
+
+$autoload = require __DIR__ . '/../vendor/autoload.php';
 
 
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
 if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
     exit(sprintf("You are running PHP %s, but Grav needs at least PHP %s to run.\n", $ver, $req));
@@ -34,7 +32,7 @@ if (!ini_get('date.timezone')) {
     date_default_timezone_set('UTC');
     date_default_timezone_set('UTC');
 }
 }
 
 
-if (!file_exists(ROOT_DIR . 'index.php')) {
+if (!file_exists(GRAV_ROOT . '/index.php')) {
     exit('FATAL: Must be run from ROOT directory of Grav!');
     exit('FATAL: Must be run from ROOT directory of Grav!');
 }
 }
 
 
@@ -48,19 +46,18 @@ $climate->arguments->add([
     ]
     ]
 ]);
 ]);
 $climate->arguments->parse();
 $climate->arguments->parse();
-$environment = $climate->arguments->get('environment');
 
 
-// Set up environment based on params.
-Setup::$environment = $environment;
+$environment = $climate->arguments->get('environment');
 
 
 $grav = Grav::instance(array('loader' => $autoload));
 $grav = Grav::instance(array('loader' => $autoload));
-$grav['uri']->init();
+$grav->setup($environment);
+
 $grav['config']->init();
 $grav['config']->init();
-$grav['streams'];
+$grav['uri']->init();
+$grav['users'];
 $grav['plugins']->init();
 $grav['plugins']->init();
 $grav['themes']->init();
 $grav['themes']->init();
 
 
-
 $app     = new Application('Grav Plugins Commands', GRAV_VERSION);
 $app     = new Application('Grav Plugins Commands', GRAV_VERSION);
 $pattern = '([A-Z]\w+Command\.php)';
 $pattern = '([A-Z]\w+Command\.php)';
 
 
@@ -75,6 +72,7 @@ $argv = array_merge([$bin], $argv);
 
 
 $input = new ArgvInput($argv);
 $input = new ArgvInput($argv);
 
 
+/** @var \Grav\Common\Data\Data $plugin */
 $plugin = $grav['plugins']->get($name);
 $plugin = $grav['plugins']->get($name);
 
 
 $output = new ConsoleOutput();
 $output = new ConsoleOutput();
@@ -83,13 +81,14 @@ $output->getFormatter()->setStyle('white', new OutputFormatterStyle('white', nul
 
 
 if (!$name) {
 if (!$name) {
     $output->writeln('');
     $output->writeln('');
-    $output->writeln("<red>Usage:</red>");
+    $output->writeln('<red>Usage:</red>');
     $output->writeln("  {$bin} [slug] [command] [arguments]");
     $output->writeln("  {$bin} [slug] [command] [arguments]");
     $output->writeln('');
     $output->writeln('');
-    $output->writeln("<red>Example:</red>");
+    $output->writeln('<red>Example:</red>');
     $output->writeln("  {$bin} error log -l 1 --trace");
     $output->writeln("  {$bin} error log -l 1 --trace");
     $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
     $list = Folder::all('plugins://', ['compare' => 'Pathname', 'pattern' => '/\/cli\/' . $pattern . '$/usm', 'levels' => 2]);
 
 
+    $total = 0;
     if (count($list)) {
     if (count($list)) {
         $available = [];
         $available = [];
         $output->writeln('');
         $output->writeln('');
@@ -98,17 +97,31 @@ if (!$name) {
             $split = explode('/', $entry);
             $split = explode('/', $entry);
             $entry = array_shift($split);
             $entry = array_shift($split);
             $index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT);
             $index = str_pad($index++ + 1, 2, '0', STR_PAD_LEFT);
-
-            if (in_array($entry, $available)) {
+            $total = str_pad($total++ + 1, 2, '0', STR_PAD_LEFT);
+            if (\in_array($entry, $available, true)) {
+                $total--;
                 continue;
                 continue;
             }
             }
 
 
             $available[] = $entry;
             $available[] = $entry;
-            $output->writeln('  ' . $index . ". <red>" . str_pad($entry, 15) . "</red> <white>${bin} ${entry} list</white>");
+            $commands_count = $index - $total + 1;
+            $output->writeln('  ' . $total . '. <red>' . str_pad($entry, 15) . "</red> <white>{$bin} {$entry} list</white>");
         }
         }
     }
     }
 
 
     exit;
     exit;
+} else {
+    if (is_null($plugin)) {
+        $output->writeln('');
+        $output->writeln("<red>$name plugin not found</red>");
+        die;
+    }
+
+    if (!$plugin->enabled) {
+        $output->writeln('');
+        $output->writeln("<red>$name not enabled</red>");
+        die;
+    }
 }
 }
 
 
 if ($plugin === null) {
 if ($plugin === null) {

+ 55 - 26
composer.json

@@ -6,47 +6,71 @@
     "homepage": "https://getgrav.org",
     "homepage": "https://getgrav.org",
     "license": "MIT",
     "license": "MIT",
     "require": {
     "require": {
-        "php": ">=5.6.4",
-        "twig/twig": "~1.24",
-        "erusev/parsedown": "1.6.4",
-        "erusev/parsedown-extra": "~0.7",
-        "symfony/yaml": "~3.4",
-        "symfony/console": "~3.4",
-        "symfony/event-dispatcher": "~3.4",
-        "symfony/var-dumper": "~3.4",
-        "symfony/polyfill-iconv": "~1.0",
-        "doctrine/cache": "^1.6",
-        "doctrine/collections": "^1.4",
+        "php": ">=7.1.3",
+        "ext-json": "*",
+        "ext-mbstring": "*",
+        "ext-openssl": "*",
+        "ext-curl": "*",
+        "ext-zip": "*",
+        "ext-dom": "*",
+        "symfony/polyfill-iconv": "^1.9",
+        "symfony/polyfill-php72": "^1.9",
+        "symfony/polyfill-php73": "^1.9",
+
         "psr/simple-cache": "^1.0",
         "psr/simple-cache": "^1.0",
         "psr/http-message": "^1.0",
         "psr/http-message": "^1.0",
+        "psr/http-server-middleware": "^1.0",
+
+        "kodus/psr7-server": "*",
+        "nyholm/psr7": "^1.0",
+
+        "twig/twig": "~1.40",
+        "erusev/parsedown": "1.6.4",
+        "erusev/parsedown-extra": "~0.7",
+        "symfony/yaml": "~4.2.0",
+        "symfony/console": "~4.2.0",
+        "symfony/event-dispatcher": "~4.2.0",
+        "symfony/var-dumper": "~4.2.0",
+        "symfony/process": "~4.2.0",
+        "doctrine/cache": "^1.8",
+        "doctrine/collections": "^1.5",
         "guzzlehttp/psr7": "^1.4",
         "guzzlehttp/psr7": "^1.4",
-        "filp/whoops": "~2.0",
+        "filp/whoops": "~2.2",
+
         "matthiasmullie/minify": "^1.3",
         "matthiasmullie/minify": "^1.3",
         "monolog/monolog": "~1.0",
         "monolog/monolog": "~1.0",
         "gregwar/image": "2.*",
         "gregwar/image": "2.*",
-        "donatj/phpuseragentparser": "~0.3",
+        "donatj/phpuseragentparser": "~0.10",
         "pimple/pimple": "~3.2",
         "pimple/pimple": "~3.2",
         "rockettheme/toolbox": "~1.4",
         "rockettheme/toolbox": "~1.4",
-        "maximebf/debugbar": "~1.10",
-        "ext-mbstring": "*",
-        "ext-openssl": "*",
-        "ext-curl": "*",
-        "ext-zip": "*",
-        "ext-json": "*",
-        "league/climate": "^3.2",
+        "maximebf/debugbar": "~1.15",
+        "league/climate": "^3.4",
         "antoligy/dom-string-iterators": "^1.0",
         "antoligy/dom-string-iterators": "^1.0",
-        "miljar/php-exif": "^0.6.3",
-        "composer/ca-bundle": "^1.0"
+        "miljar/php-exif": "^0.6.4",
+        "composer/ca-bundle": "^1.0",
+        "dragonmantank/cron-expression": "^1.2",
+        "phive/twig-extensions-deferred": "^1.0",
+        "willdurand/negotiation": "^2.3"
     },
     },
     "require-dev": {
     "require-dev": {
-        "codeception/codeception": "^2.1",
-        "phpunit/php-code-coverage": "~2.0",
-        "fzaninotto/faker": "^1.5",
+        "codeception/codeception": "^2.4",
+        "phpstan/phpstan": "^0.11",
+        "phpstan/phpstan-deprecation-rules": "^0.11.0",
+        "phpunit/php-code-coverage": "~6.0",
+        "fzaninotto/faker": "^1.8",
         "victorjonsson/markdowndocs": "dev-master"
         "victorjonsson/markdowndocs": "dev-master"
     },
     },
+    "suggest": {
+        "ext-zend-opcache": "Recommended for better performance",
+        "ext-intl": "Recommended for multi-language sites",
+        "ext-memcache": "Needed to support Memcache servers",
+        "ext-memcached": "Needed to support Memcached servers",
+        "ext-redis": "Needed to support Redis servers"
+    },
     "config": {
     "config": {
+        "apcu-autoloader": true,
         "platform": {
         "platform": {
-            "php": "5.6.4"
+            "php": "7.1.3"
         }
         }
     },
     },
     "repositories": [
     "repositories": [
@@ -65,7 +89,12 @@
         "exclude": ["VERSION"]
         "exclude": ["VERSION"]
     },
     },
     "scripts": {
     "scripts": {
+        "api-16": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.16.md",
+        "api-15": "vendor/bin/phpdoc-md generate system/src > user/pages/14.api/default.md",
         "post-create-project-cmd": "bin/grav install",
         "post-create-project-cmd": "bin/grav install",
+        "phpstan": "vendor/bin/phpstan analyse -l 2 -c ./tests/phpstan/phpstan.neon system/src --memory-limit=256M",
+        "phpstan-framework": "vendor/bin/phpstan analyse -l 5 -c ./tests/phpstan/phpstan.neon system/src/Grav/Framework --memory-limit=256M",
+        "test-plugins": "vendor/bin/phpstan analyse -l 0 -c ./tests/phpstan/plugins.neon user/plugins --memory-limit=256M",
         "test": "vendor/bin/codecept run unit",
         "test": "vendor/bin/codecept run unit",
         "test-windows": "vendor\\bin\\codecept run unit"
         "test-windows": "vendor\\bin\\codecept run unit"
     },
     },

File diff suppressed because it is too large
+ 446 - 239
composer.lock


+ 0 - 8
fixperms.sh

@@ -1,8 +0,0 @@
-#!/bin/sh
-chgrp www-data .
-chgrp -R www-data *
-sh -c "find . -type f | xargs chmod 664"
-sh -c "find ./bin -type f | xargs chmod 775"
-sh -c "find . -type d | xargs chmod 775"
-sh -c "find . -type d | xargs chmod +s"
-sh -c "umask 0002"

+ 17 - 15
index.php

@@ -9,35 +9,34 @@
 
 
 namespace Grav;
 namespace Grav;
 
 
-define('GRAV_PHP_MIN', '5.6.4');
+\define('GRAV_REQUEST_TIME', microtime(true));
+\define('GRAV_PHP_MIN', '7.1.3');
 
 
-// Ensure vendor libraries exist
-$autoload = __DIR__ . '/vendor/autoload.php';
-if (!is_file($autoload)) {
-    die("Please run: <i>bin/grav install</i>");
+if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
+    die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
 }
 }
 
 
-if (PHP_SAPI === 'cli-server') {
-    if (!isset($_SERVER['PHP_CLI_ROUTER'])) {
-       die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
-    }
+if (PHP_SAPI === 'cli-server' && !isset($_SERVER['PHP_CLI_ROUTER'])) {
+    die("PHP webserver requires a router to run Grav, please use: <pre>php -S {$_SERVER['SERVER_NAME']}:{$_SERVER['SERVER_PORT']} system/router.php</pre>");
 }
 }
 
 
-use Grav\Common\Grav;
-use RocketTheme\Toolbox\Event\Event;
-
-if (version_compare($ver = PHP_VERSION, $req = GRAV_PHP_MIN, '<')) {
-    die(sprintf('You are running PHP %s, but Grav needs at least <strong>PHP %s</strong> to run.', $ver, $req));
+// Ensure vendor libraries exist
+$autoload = __DIR__ . '/vendor/autoload.php';
+if (!is_file($autoload)) {
+    die('Please run: <i>bin/grav install</i>');
 }
 }
 
 
 // Register the auto-loader.
 // Register the auto-loader.
 $loader = require $autoload;
 $loader = require $autoload;
 
 
+use Grav\Common\Grav;
+use RocketTheme\Toolbox\Event\Event;
+
 // Set timezone to default, falls back to system if php.ini not set
 // Set timezone to default, falls back to system if php.ini not set
 date_default_timezone_set(@date_default_timezone_get());
 date_default_timezone_set(@date_default_timezone_get());
 
 
 // Set internal encoding if mbstring loaded
 // Set internal encoding if mbstring loaded
-if (!extension_loaded('mbstring')) {
+if (!\extension_loaded('mbstring')) {
     die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
     die("'mbstring' extension is not loaded.  This is required for Grav to run correctly");
 }
 }
 mb_internal_encoding('UTF-8');
 mb_internal_encoding('UTF-8');
@@ -52,6 +51,9 @@ $grav = Grav::instance(
 // Process the page
 // Process the page
 try {
 try {
     $grav->process();
     $grav->process();
+} catch (\Error $e) {
+    $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
+    throw $e;
 } catch (\Exception $e) {
 } catch (\Exception $e) {
     $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
     $grav->fireEvent('onFatalException', new Event(array('exception' => $e)));
     throw $e;
     throw $e;

+ 4 - 0
now.json

@@ -0,0 +1,4 @@
+    {
+  "version": 2,
+  "builds": [{ "src": "*.php", "use": "@now/php" }]
+}

+ 1 - 0
robots.txt

@@ -10,3 +10,4 @@ Disallow: /user/
 Allow: /user/pages/
 Allow: /user/pages/
 Allow: /user/themes/
 Allow: /user/themes/
 Allow: /user/images/
 Allow: /user/images/
+Allow: /

+ 16 - 0
system/assets/debugger.css

@@ -30,9 +30,25 @@ div.phpdebugbar {
 }
 }
 
 
 .phpdebugbar .phpdebugbar-widgets-toolbar {
 .phpdebugbar .phpdebugbar-widgets-toolbar {
+    border-top: 1px solid #ddd;
     padding-left: 5px;
     padding-left: 5px;
+    padding-right: 2px;
+    padding-top: 2px;
+    background-color: #fafafa !important;
+    width: auto !important;
+    left: 0;
+    right: 0;
 }
 }
 
 
+.phpdebugbar .phpdebugbar-widgets-toolbar input {
+    background: transparent !important;
+}
+
+.phpdebugbar .phpdebugbar-widgets-toolbar .phpdebugbar-widgets-filter {
+
+}
+
+
 .phpdebugbar input[type=text] {
 .phpdebugbar input[type=text] {
     padding: 0;
     padding: 0;
     display: inline;
     display: inline;

BIN
system/assets/grav.png


+ 125 - 0
system/blueprints/config/backups.yaml

@@ -0,0 +1,125 @@
+title: PLUGIN_ADMIN.BACKUPS
+
+form:
+    validation: loose
+
+    fields:
+        history_title:
+            type: section
+            title: PLUGIN_ADMIN.BACKUPS_HISTORY
+            underline: true
+
+        history:
+            type: backupshistory
+
+        config_title:
+            type: section
+            title: PLUGIN_ADMIN.BACKUPS_PURGE_CONFIG
+            underline: true
+
+        purge.trigger:
+            type: select
+            label: PLUGIN_ADMIN.BACKUPS_STORAGE_PURGE_TRIGGER
+            size: medium
+            default: space
+            options:
+                space: Maximum Backup Space
+                number: Maximum Number of Backups
+                time: maximum Retention Time
+            validate:
+                required: true
+
+        purge.max_backups_count:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_COUNT
+            default: 25
+            size: x-small
+            help: PLUGIN_ADMIN.BACKUPS_MAX_COUNT
+            validate:
+                min: 0
+                type: number
+                required: true
+                message: Must be a number 0 or greater
+
+        purge.max_backups_space:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_SPACE
+            append: in GB
+            size: x-small
+            default: 5
+            validate:
+                min: 1
+                type: number
+                required: true
+                message: Space must be 1GB or greater
+
+        purge.max_backups_time:
+            type: number
+            label: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME
+            append: PLUGIN_ADMIN.BACKUPS_MAX_RETENTION_TIME_APPEND
+            size: x-small
+            default: 365
+            validate:
+                min: 7
+                type: number
+                required: true
+                message: Rentenion days must be 7 or greater
+
+        profiles_title:
+          type: section
+          title: PLUGIN_ADMIN.BACKUPS_PROFILES
+          underline: true
+
+        profiles:
+          type: list
+          style: vertical
+          label:
+          classes: backups-list compact
+          sort: false
+
+          fields:
+            .name:
+              type: text
+              label: PLUGIN_ADMIN.NAME
+              placeholder: PLUGIN_ADMIN.BACKUPS_PROFILE_NAME
+              validate:
+                  max: 20
+                  message: 'Name must be less than 20 characters'
+                  required: true
+            .root:
+              type: text
+              label: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER
+              help: PLUGIN_ADMIN.BACKUPS_PROFILE_ROOT_FOLDER_HELP
+              placeholder: '/'
+              default: '/'
+              validate:
+                  required: true
+            .exclude_paths:
+              type: textarea
+              label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS
+              rows: 5
+              placeholder: "/backup\r/cache\r/images\r/logs\r/tmp"
+              help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_PATHS_HELP
+            .exclude_files:
+                type: textarea
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES
+                rows: 5
+                placeholder: ".DS_Store\r.git\r.svn\r.hg\r.idea\r.vscode\rnode_modules"
+                help: PLUGIN_ADMIN.BACKUPS_PROFILE_EXCLUDE_FILES_HELP
+            .schedule:
+                type: toggle
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE
+                highlight: 1
+                default: 1
+                options:
+                    1: PLUGIN_ADMIN.YES
+                    0: PLUGIN_ADMIN.NO
+                validate:
+                    type: bool
+            .schedule_at:
+                type: cron
+                label: PLUGIN_ADMIN.BACKUPS_PROFILE_SCHEDULE_AT
+                default: '* 3 * * *'
+                validate:
+                    required: true
+

+ 76 - 0
system/blueprints/config/scheduler.yaml

@@ -0,0 +1,76 @@
+title: PLUGIN_ADMIN.SCHEDULER
+
+form:
+    validation: loose
+
+    fields:
+
+        status_title:
+            type: section
+            title: PLUGIN_ADMIN.SCHEDULER_STATUS
+            underline: true
+
+        status:
+            type: cronstatus
+            validate:
+                type: commalist
+
+        jobs_title:
+            type: section
+            title: PLUGIN_ADMIN.SCHEDULER_JOBS
+            underline: true
+
+        custom_jobs:
+          type: list
+          style: vertical
+          label:
+          classes: cron-job-list compact
+          key: id
+          fields:
+            .id:
+              type: key
+              label: ID
+              placeholder: 'process-name'
+              validate:
+                  required: true
+                  pattern: '[a-zа-я0-9_\-]+'
+                  max: 20
+                  message: 'ID must be lowercase with dashes/underscores only and less than 20 characters'
+            .command:
+              type: text
+              label: PLUGIN_ADMIN.COMMAND
+              placeholder: 'cd ~;ls -lah;'
+              validate:
+                  required: true
+            .args:
+              type: text
+              label: PLUGIN_ADMIN.EXTRA_ARGUMENTS
+            .at:
+              type: cron
+              label: PLUGIN_ADMIN.SCHEDULER_RUNAT
+              help: PLUGIN_ADMIN.SCHEDULER_RUNAT_HELP
+              placeholder: '* * * * *'
+              validate:
+                  required: true
+            .output:
+              type: text
+              label: PLUGIN_ADMIN.SCHEDULER_OUTPUT
+              help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_HELP
+              placeholder: 'logs/ls-cron.out'
+            .output_mode:
+              type: select
+              label: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE
+              help: PLUGIN_ADMIN.SCHEDULER_OUTPUT_TYPE_HELP
+              default: append
+              options:
+                  append: Append
+                  overwrite: Overwrite
+            .email:
+                type: text
+                label: PLUGIN_ADMIN.SCHEDULER_EMAIL
+                help: PLUGIN_ADMIN.SCHEDULER_EMAIL_HELP
+                placeholder: 'notifications@yoursite.com'
+
+
+
+

+ 8 - 0
system/blueprints/config/security.yaml

@@ -41,6 +41,14 @@ form:
             validate:
             validate:
                 type: bool
                 type: bool
 
 
+        xss_invalid_protocols:
+            type: selectize
+            size: large
+            label: PLUGIN_ADMIN.XSS_INVALID_PROTOCOLS_LIST
+            classes: fancy
+            validate:
+                type: commalist
+
         xss_enabled.moz_binding:
         xss_enabled.moz_binding:
             type: toggle
             type: toggle
             label: PLUGIN_ADMIN.XSS_MOZ_BINDINGS
             label: PLUGIN_ADMIN.XSS_MOZ_BINDINGS

+ 1 - 1
system/blueprints/config/site.yaml

@@ -65,7 +65,7 @@ form:
 
 
                 summary.size:
                 summary.size:
                     type: text
                     type: text
-                    size: x-small
+                    size: small
                     append: PLUGIN_ADMIN.CHARACTERS
                     append: PLUGIN_ADMIN.CHARACTERS
                     label: PLUGIN_ADMIN.SUMMARY_SIZE
                     label: PLUGIN_ADMIN.SUMMARY_SIZE
                     help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP
                     help: PLUGIN_ADMIN.SUMMARY_SIZE_HELP

+ 1402 - 1236
system/blueprints/config/system.yaml

@@ -4,1241 +4,1407 @@ form:
     validation: loose
     validation: loose
     fields:
     fields:
 
 
-        content:
-            type: section
-            title: PLUGIN_ADMIN.CONTENT
-            underline: true
+        system_tabs:
+            type: tabs
+            classes: side-tabs
 
 
             fields:
             fields:
-                home.alias:
-                    type: pages
-                    size: large
-                    classes: fancy
-                    label: PLUGIN_ADMIN.HOME_PAGE
-                    show_all: false
-                    show_modular: false
-                    show_root: false
-                    show_slug: true
-                    help: PLUGIN_ADMIN.HOME_PAGE_HELP
-
-                home.hide_in_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS
-                    help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-
-                pages.theme:
-                    type: themeselect
-                    classes: fancy
-                    selectize: true
-                    size: medium
-                    label: PLUGIN_ADMIN.DEFAULT_THEME
-                    help: PLUGIN_ADMIN.DEFAULT_THEME_HELP
-
-                pages.process:
-                    type: checkboxes
-                    label: PLUGIN_ADMIN.PROCESS
-                    help: PLUGIN_ADMIN.PROCESS_HELP
-                    default: [markdown: true, twig: true]
-                    options:
-                        markdown: Markdown
-                        twig: Twig
-                    use: keys
-
-                timezone:
-                    type: select
-                    label: PLUGIN_ADMIN.TIMEZONE
-                    size: medium
-                    classes: fancy
-                    help: PLUGIN_ADMIN.TIMEZONE_HELP
-                    data-options@: '\Grav\Common\Utils::timezones'
-                    default: ''
-                    options:
-                        '': 'Default (Server Timezone)'
-
-                pages.dateformat.default:
-                    type: select
-                    size: medium
-                    selectize:
-                        create: true
-                    label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
-                    help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
-                    placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
-                    data-options@: '\Grav\Common\Utils::dateFormats'
-                    validate:
-                        type: string
-
-                pages.dateformat.short:
-                    type: dateformat
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.SHORT_DATE_FORMAT
-                    help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP
-                    default: "jS M Y"
-                    options:
-                        "F jS \\a\\t g:ia": Date1
-                        "l jS \\of F g:i A": Date2
-                        "D, d M Y G:i:s": Date3
-                        "d-m-y G:i": Date4
-                        "jS M Y": Date5
-
-                pages.dateformat.long:
-                    type: dateformat
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.LONG_DATE_FORMAT
-                    help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP
-                    options:
-                        "F jS \\a\\t g:ia": Date1
-                        "l jS \\of F g:i A": Date2
-                        "D, d M Y G:i:s": Date3
-                        "d-m-y G:i": Date4
-                        "jS M Y": Date5
-
-                pages.order.by:
-                    type: select
-                    size: large
-                    classes: fancy
-                    label: PLUGIN_ADMIN.DEFAULT_ORDERING
-                    help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP
-                    options:
-                        default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT
-                        folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER
-                        title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE
-                        date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE
-
-                pages.order.dir:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION
-                    highlight: asc
-                    default: desc
-                    help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP
-                    options:
-                        asc: PLUGIN_ADMIN.ASCENDING
-                        desc: PLUGIN_ADMIN.DESCENDING
-
-                pages.list.count:
-                    type: text
-                    size: x-small
-                    append: PLUGIN_ADMIN.PAGES
-                    label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT
-                    help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP
-                    validate:
-                        type: number
-                        min: 1
-
-                pages.publish_dates:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING
-                    help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.events:
-                     type: checkboxes
-                     label: PLUGIN_ADMIN.EVENTS
-                     help: PLUGIN_ADMIN.EVENTS_HELP
-                     default: [page: true, twig: true]
-                     options:
-                         page: Page Events
-                         twig: Twig Events
-                     use: keys
-
-                pages.append_url_extension:
-                    type: text
-                    size: x-small
-                    placeholder: "e.g. .html"
-                    label: PLUGIN_ADMIN.APPEND_URL_EXT
-                    help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP
-
-                pages.redirect_default_route:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
-                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.redirect_default_code:
-                    type: select
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
-                    help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
-                    options:
-                        301: 301 - Permanent
-                        302: 302 - Found
-                        303: 303 - Other
-                        304: 304 - Not Modified
-
-                pages.redirect_trailing_slash:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
-                    help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.ignore_hidden:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IGNORE_HIDDEN
-                    help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.ignore_files:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.IGNORE_FILES
-                    help: PLUGIN_ADMIN.IGNORE_FILES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                pages.ignore_folders:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.IGNORE_FOLDERS
-                    help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                pages.url_taxonomy_filters:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS
-                    help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.twig_first:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_FIRST
-                    help: PLUGIN_ADMIN.TWIG_FIRST_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.never_cache_twig:
-                    type: toggle
-                    label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
-                    help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.frontmatter.process_twig:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG
-                    help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                pages.frontmatter.ignore_fields:
-                    type: selectize
-                    size: large
-                    placeholder: "e.g. forms"
-                    label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS
-                    help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-        languages:
-            type: section
-            title: PLUGIN_ADMIN.LANGUAGES
-            underline: true
-
-            fields:
-
-                languages.supported:
-                    type: selectize
-                    size: large
-                    placeholder: "e.g. en, fr"
-                    label: PLUGIN_ADMIN.SUPPORTED
-                    help: PLUGIN_ADMIN.SUPPORTED_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                languages.include_default_lang:
-                    type: toggle
-                    label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG
-                    help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-
-                languages.translations:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED
-                    help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.translations_fallback:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK
-                    help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.session_store_active:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION
-                    help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.http_accept_language:
-                    type: toggle
-                    label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
-                    help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                languages.override_locale:
-                    type: toggle
-                    label: PLUGIN_ADMIN.OVERRIDE_LOCALE
-                    help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        http_headers:
-            type: section
-            title: PLUGIN_ADMIN.HTTP_HEADERS
-            underline: true
-
-            fields:
-                pages.expires:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.EXPIRES
-                    help: PLUGIN_ADMIN.EXPIRES_HELP
-                    validate:
-                        type: number
-                        min: 1
-                pages.cache_control:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.CACHE_CONTROL
-                    help: PLUGIN_ADMIN.CACHE_CONTROL_HELP
-                    placeholder: 'e.g. public, max-age=31536000'
-                pages.last_modified:
-                    type: toggle
-                    label: PLUGIN_ADMIN.LAST_MODIFIED
-                    help: PLUGIN_ADMIN.LAST_MODIFIED_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.etag:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ETAG
-                    help: PLUGIN_ADMIN.ETAG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.vary_accept_encoding:
-                    type: toggle
-                    label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING
-                    help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        markdown:
-            type: section
-            title: Markdown
-            underline: true
-
-            fields:
-                pages.markdown.extra:
-                    type: toggle
-                    label: Markdown extra
-                    help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.auto_line_breaks:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTO_LINE_BREAKS
-                    help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.auto_url_links:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTO_URL_LINKS
-                    help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-                pages.markdown.escape_markup:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ESCAPE_MARKUP
-                    help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        caching:
-            type: section
-            title: PLUGIN_ADMIN.CACHING
-            underline: true
-
-            fields:
-                cache.enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CACHING
-                    help: PLUGIN_ADMIN.CACHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.check.method:
-                    type: select
-                    size: medium
-                    classes: fancy
-                    label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
-                    help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
-                    options:
-                        file: Markdown + Yaml file timestamps
-                        folder: Folder timestamps
-                        hash: All files timestamps
-                        none: No timestamp checking
-
-                cache.driver:
-                    type: select
-                    size: small
-                    classes: fancy
-                    label: PLUGIN_ADMIN.CACHE_DRIVER
-                    help: PLUGIN_ADMIN.CACHE_DRIVER_HELP
-                    options:
-                        auto: Auto detect
-                        file: File
-                        apc: APC
-                        apcu: APCu
-                        xcache: Xcache
-                        memcache: Memcache
-                        memcached: Memcached
-                        wincache: WinCache
-                        redis: Redis
-
-                cache.prefix:
-                    type: text
-                    size: x-small
-                    label: PLUGIN_ADMIN.CACHE_PREFIX
-                    help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
-                    placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
-
-                cache.clear_images_by_default:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
-                    help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.cli_compatibility:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CLI_COMPATIBILITY
-                    help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.lifetime:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.LIFETIME
-                    help: PLUGIN_ADMIN.LIFETIME_HELP
-                    validate:
-                        type: number
-
-                cache.gzip:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GZIP_COMPRESSION
-                    help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.allow_webserver_gzip:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP
-                    help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                cache.memcache.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.MEMCACHE_SERVER
-                    help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.memcache.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.MEMCACHE_PORT
-                    help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP
-                    placeholder: "11211"
-
-                cache.memcached.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.MEMCACHED_SERVER
-                    help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.memcached.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.MEMCACHED_PORT
-                    help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP
-                    placeholder: "11211"
-
-                cache.redis.socket:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.REDIS_SOCKET
-                    help: PLUGIN_ADMIN.REDIS_SOCKET_HELP
-                    placeholder: "/var/run/redis/redis.sock"
-
-                cache.redis.server:
-                    type: text
-                    size: medium
-                    label: PLUGIN_ADMIN.REDIS_SERVER
-                    help: PLUGIN_ADMIN.REDIS_SERVER_HELP
-                    placeholder: "localhost"
-
-                cache.redis.port:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.REDIS_PORT
-                    help: PLUGIN_ADMIN.REDIS_PORT_HELP
-                    placeholder: "6379"
-
-                cache.redis.password:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.REDIS_PASSWORD
-
-
-
-        twig:
-            type: section
-            title: PLUGIN_ADMIN.TWIG_TEMPLATING
-            underline: true
-
-            fields:
-                twig.cache:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_CACHING
-                    help: PLUGIN_ADMIN.TWIG_CACHING_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.debug:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_DEBUG
-                    help: PLUGIN_ADMIN.TWIG_DEBUG_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.auto_reload:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DETECT_CHANGES
-                    help: PLUGIN_ADMIN.DETECT_CHANGES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.autoescape:
-                    type: toggle
-                    label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES
-                    help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                twig.umask_fix:
-                    type: toggle
-                    label: PLUGIN_ADMIN.TWIG_UMASK_FIX
-                    help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        assets:
-            type: section
-            title: PLUGIN_ADMIN.ASSETS
-            underline: true
-
-            fields:
-                assets.css_pipeline:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_pipeline_include_externals:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_pipeline_before_excludes:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES
-                    help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_minify:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_MINIFY
-                    help: PLUGIN_ADMIN.CSS_MINIFY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.css_minify_windows:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE
-                    help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.css_rewrite:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CSS_REWRITE
-                    help: PLUGIN_ADMIN.CSS_REWRITE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.js_pipeline:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.js_pipeline_include_externals:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.js_pipeline_before_excludes:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES
-                    help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                       type: bool
-
-                assets.js_minify:
-                    type: toggle
-                    label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY
-                    help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.enable_asset_timestamp:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
-                    help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                assets.collections:
-                    type: multilevel
-                    label: PLUGIN_ADMIN.COLLECTIONS
-                    placeholder_key: collection_name
-                    placeholder_value: collection_path
-                    validate:
-                        type: array
-
-        errors:
-            type: section
-            title: PLUGIN_ADMIN.ERROR_HANDLER
-            underline: true
-
-            fields:
-                errors.display:
-                    type: select
-                    label: PLUGIN_ADMIN.DISPLAY_ERRORS
-                    help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
-                    size: medium
-                    highlight: 1
-                    options:
-                        -1: PLUGIN_ADMIN.ERROR_SYSTEM
-                        0: PLUGIN_ADMIN.ERROR_SIMPLE
-                        1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
-                    validate:
-                        type: int
-
-
-                errors.log:
-                    type: toggle
-                    label: PLUGIN_ADMIN.LOG_ERRORS
-                    help: PLUGIN_ADMIN.LOG_ERRORS_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        debugger:
-            type: section
-            title: PLUGIN_ADMIN.DEBUGGER
-            underline: true
-
-            fields:
-                debugger.enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.DEBUGGER
-                    help: PLUGIN_ADMIN.DEBUGGER_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                debugger.shutdown.close_connection:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION
-                    help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-        media:
-            type: section
-            title: PLUGIN_ADMIN.MEDIA
-            underline: true
-
-            fields:
-                images.default_image_quality:
-                    type: range
-                    append: '%'
-                    label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY
-                    help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP
-                    validate:
-                        min: 1
-                        max: 100
-
-                images.cache_all:
-                    type: toggle
-                    label: PLUGIN_ADMIN.CACHE_ALL
-                    help: PLUGIN_ADMIN.CACHE_ALL_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                images.cache_perms:
-                    type: select
-                    size: small
-                    label: PLUGIN_ADMIN.CACHE_PERMS
-                    help: PLUGIN_ADMIN.CACHE_PERMS_HELP
-                    highlight: '0755'
-                    options:
-                        '0755': '0755'
-                        '0775': '0775'
-
-                images.debug:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IMAGES_DEBUG
-                    help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                images.auto_fix_orientation:
-                    type: toggle
-                    label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION
-                    help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                media.enable_media_timestamp:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP
-                    help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP
-                    highlight: 0
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                media.auto_metadata_exif:
-                      type: toggle
-                      label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA
-                      help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP
-                      highlight: 0
-                      options:
-                          1: PLUGIN_ADMIN.YES
-                          0: PLUGIN_ADMIN.NO
-                      validate:
-                          type: bool
-
-
-
-                media.allowed_fallback_types:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.FALLBACK_TYPES
-                    help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-                media.unsupported_inline_types:
-                    type: selectize
-                    size: large
-                    label: PLUGIN_ADMIN.INLINE_TYPES
-                    help: PLUGIN_ADMIN.INLINE_TYPES_HELP
-                    classes: fancy
-                    validate:
-                        type: commalist
-
-        session:
-            type: section
-            title: PLUGIN_ADMIN.SESSION
-            underline: true
-
-            fields:
-                session.enabled:
-                    type: hidden
-                    label: PLUGIN_ADMIN.ENABLED
-                    help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.initialize:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_INITIALIZE
-                    help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.timeout:
-                    type: text
-                    size: small
-                    append: NICETIME.SECOND_PLURAL
-                    label: PLUGIN_ADMIN.TIMEOUT
-                    help: PLUGIN_ADMIN.TIMEOUT_HELP
-                    validate:
-                        type: number
-                        min: 0
-
-                session.name:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.NAME
-                    help: PLUGIN_ADMIN.SESSION_NAME_HELP
-
-                session.secure:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_SECURE
-                    help: PLUGIN_ADMIN.SESSION_SECURE_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: false
-                    validate:
-                        type: bool
-
-                session.httponly:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_HTTPONLY
-                    help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                session.path:
-                    type: text
-                    size: small
-                    label: PLUGIN_ADMIN.SESSION_PATH
-                    help: PLUGIN_ADMIN.SESSION_PATH_HELP
-
-                session.split:
-                    type: toggle
-                    label: PLUGIN_ADMIN.SESSION_SPLIT
-                    help: PLUGIN_ADMIN.SESSION_SPLIT_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-        advanced:
-            type: section
-            title: PLUGIN_ADMIN.ADVANCED
-            underline: true
-
-            fields:
-                gpm.releases:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_RELEASES
-                    highlight: stable
-                    help: PLUGIN_ADMIN.GPM_RELEASES_HELP
-                    options:
-                        stable: PLUGIN_ADMIN.STABLE
-                        testing: PLUGIN_ADMIN.TESTING
-
-                gpm.proxy_url:
-                    type: text
-                    size: medium
-                    placeholder: "e.g. 127.0.0.1:3128"
-                    label: PLUGIN_ADMIN.PROXY_URL
-                    help: PLUGIN_ADMIN.PROXY_URL_HELP
-
-                gpm.method:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_METHOD
-                    highlight: auto
-                    help: PLUGIN_ADMIN.GPM_METHOD_HELP
-                    options:
-                        auto: PLUGIN_ADMIN.AUTO
-                        fopen: PLUGIN_ADMIN.FOPEN
-                        curl: PLUGIN_ADMIN.CURL
-
-                gpm.official_gpm_only:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
-                    highlight: auto
-                    help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
-                    highlight: 1
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    default: true
-                    validate:
-                        type: bool
-
-                gpm.verify_peer:
-                    type: toggle
-                    label: PLUGIN_ADMIN.GPM_VERIFY_PEER
-                    highlight: 1
-                    help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                reverse_proxy_setup:
-                    type: toggle
-                    label: PLUGIN_ADMIN.REVERSE_PROXY
-                    highlight: 0
-                    help: PLUGIN_ADMIN.REVERSE_PROXY_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                username_regex:
-                    type: text
-                    size: large
-                    label: PLUGIN_ADMIN.USERNAME_REGEX
-                    help: PLUGIN_ADMIN.USERNAME_REGEX_HELP
-
-                pwd_regex:
-                    type: text
-                    size: large
-                    label: PLUGIN_ADMIN.PWD_REGEX
-                    help: PLUGIN_ADMIN.PWD_REGEX_HELP
-
-                intl_enabled:
-                    type: toggle
-                    label: PLUGIN_ADMIN.INTL_ENABLED
-                    highlight: 1
-                    help: PLUGIN_ADMIN.INTL_ENABLED_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                wrapped_site:
-                    type: toggle
-                    label: PLUGIN_ADMIN.WRAPPED_SITE
-                    highlight: 0
-                    help: PLUGIN_ADMIN.WRAPPED_SITE_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                absolute_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.ABSOLUTE_URLS
-                    highlight: 0
-                    help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                param_sep:
-                    type: select
-                    size: medium
-                    label: PLUGIN_ADMIN.PARAMETER_SEPARATOR
-                    classes: fancy
-                    help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP
-                    default: ''
-                    options:
-                        ':': ': (default)'
-                        ';': '; (for Apache running on Windows)'
-
-                force_ssl:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FORCE_SSL
-                    highlight: 0
-                    help: PLUGIN_ADMIN.FORCE_SSL_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                force_lowercase_urls:
-                    type: toggle
-                    label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                custom_base_url:
-                    type: text
-                    size: medium
-                    placeholder: "e.g. http://yoursite.com/yourpath"
-                    label: PLUGIN_ADMIN.CUSTOM_BASE_URL
-                    help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
-
-                strict_mode.yaml_compat:
-                    type: toggle
-                    label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
-
-                strict_mode.twig_compat:
-                    type: toggle
-                    label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
-                    highlight: 1
-                    default: 1
-                    help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
-                    options:
-                        1: PLUGIN_ADMIN.YES
-                        0: PLUGIN_ADMIN.NO
-                    validate:
-                        type: bool
+                content:
+                    type: tab
+                    title: PLUGIN_ADMIN.CONTENT
+
+                    fields:
+                        content_section:
+                            type: section
+                            title: PLUGIN_ADMIN.CONTENT
+                            underline: true
+
+                        home.alias:
+                            type: pages
+                            size: large
+                            classes: fancy
+                            label: PLUGIN_ADMIN.HOME_PAGE
+                            show_all: false
+                            show_modular: false
+                            show_root: false
+                            show_slug: true
+                            help: PLUGIN_ADMIN.HOME_PAGE_HELP
+
+                        home.hide_in_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.HIDE_HOME_IN_URLS
+                            help: PLUGIN_ADMIN.HIDE_HOME_IN_URLS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.theme:
+                            type: themeselect
+                            classes: fancy
+                            selectize: true
+                            size: medium
+                            label: PLUGIN_ADMIN.DEFAULT_THEME
+                            help: PLUGIN_ADMIN.DEFAULT_THEME_HELP
+
+                        pages.process:
+                            type: checkboxes
+                            label: PLUGIN_ADMIN.PROCESS
+                            help: PLUGIN_ADMIN.PROCESS_HELP
+                            default: [markdown: true, twig: true]
+                            options:
+                                markdown: Markdown
+                                twig: Twig
+                            use: keys
+
+                        pages.types:
+                            type: array
+                            label: PLUGIN_ADMIN.PAGE_TYPES
+                            help: PLUGIN_ADMIN.PAGE_TYPES_HELP
+                            size: small
+                            default: ['html','htm','json','xml','txt','rss','atom']
+                            value_only: true
+
+                        timezone:
+                            type: select
+                            label: PLUGIN_ADMIN.TIMEZONE
+                            size: medium
+                            classes: fancy
+                            help: PLUGIN_ADMIN.TIMEZONE_HELP
+                            data-options@: '\Grav\Common\Utils::timezones'
+                            default: ''
+                            options:
+                                '': 'Default (Server Timezone)'
+
+                        pages.dateformat.default:
+                            type: select
+                            size: medium
+                            selectize:
+                                create: true
+                            label: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT
+                            help: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_HELP
+                            placeholder: PLUGIN_ADMIN.DEFAULT_DATE_FORMAT_PLACEHOLDER
+                            data-options@: '\Grav\Common\Utils::dateFormats'
+                            validate:
+                                type: string
+
+                        pages.dateformat.short:
+                            type: dateformat
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.SHORT_DATE_FORMAT
+                            help: PLUGIN_ADMIN.SHORT_DATE_FORMAT_HELP
+                            default: "jS M Y"
+                            options:
+                                "F jS \\a\\t g:ia": Date1
+                                "l jS \\of F g:i A": Date2
+                                "D, d M Y G:i:s": Date3
+                                "d-m-y G:i": Date4
+                                "jS M Y": Date5
+
+                        pages.dateformat.long:
+                            type: dateformat
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.LONG_DATE_FORMAT
+                            help: PLUGIN_ADMIN.LONG_DATE_FORMAT_HELP
+                            options:
+                                "F jS \\a\\t g:ia": Date1
+                                "l jS \\of F g:i A": Date2
+                                "D, d M Y G:i:s": Date3
+                                "d-m-y G:i": Date4
+                                "jS M Y": Date5
+
+                        pages.order.by:
+                            type: select
+                            size: large
+                            classes: fancy
+                            label: PLUGIN_ADMIN.DEFAULT_ORDERING
+                            help: PLUGIN_ADMIN.DEFAULT_ORDERING_HELP
+                            options:
+                                default: PLUGIN_ADMIN.DEFAULT_ORDERING_DEFAULT
+                                folder: PLUGIN_ADMIN.DEFAULT_ORDERING_FOLDER
+                                title: PLUGIN_ADMIN.DEFAULT_ORDERING_TITLE
+                                date: PLUGIN_ADMIN.DEFAULT_ORDERING_DATE
+
+                        pages.order.dir:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION
+                            highlight: asc
+                            default: desc
+                            help: PLUGIN_ADMIN.DEFAULT_ORDER_DIRECTION_HELP
+                            options:
+                                asc: PLUGIN_ADMIN.ASCENDING
+                                desc: PLUGIN_ADMIN.DESCENDING
+
+                        pages.list.count:
+                            type: text
+                            size: x-small
+                            append: PLUGIN_ADMIN.PAGES
+                            label: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT
+                            help: PLUGIN_ADMIN.DEFAULT_PAGE_COUNT_HELP
+                            validate:
+                                type: number
+                                min: 1
+
+                        pages.publish_dates:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DATE_BASED_PUBLISHING
+                            help: PLUGIN_ADMIN.DATE_BASED_PUBLISHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.events:
+                             type: checkboxes
+                             label: PLUGIN_ADMIN.EVENTS
+                             help: PLUGIN_ADMIN.EVENTS_HELP
+                             default: [page: true, twig: true]
+                             options:
+                                 page: Page Events
+                                 twig: Twig Events
+                             use: keys
+
+                        pages.append_url_extension:
+                            type: text
+                            size: x-small
+                            placeholder: "e.g. .html"
+                            label: PLUGIN_ADMIN.APPEND_URL_EXT
+                            help: PLUGIN_ADMIN.APPEND_URL_EXT_HELP
+
+                        pages.redirect_default_route:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE
+                            help: PLUGIN_ADMIN.REDIRECT_DEFAULT_ROUTE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.redirect_default_code:
+                            type: select
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE
+                            help: PLUGIN_ADMIN.REDIRECT_DEFAULT_CODE_HELP
+                            options:
+                                301: 301 - Permanent
+                                302: 302 - Found
+                                303: 303 - Other
+                                304: 304 - Not Modified
+
+                        pages.redirect_trailing_slash:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH
+                            help: PLUGIN_ADMIN.REDIRECT_TRAILING_SLASH_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.ignore_hidden:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IGNORE_HIDDEN
+                            help: PLUGIN_ADMIN.IGNORE_HIDDEN_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.ignore_files:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.IGNORE_FILES
+                            help: PLUGIN_ADMIN.IGNORE_FILES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.ignore_folders:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.IGNORE_FOLDERS
+                            help: PLUGIN_ADMIN.IGNORE_FOLDERS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.hide_empty_folders:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS
+                            help: PLUGIN_ADMIN.HIDE_EMPTY_FOLDERS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        pages.url_taxonomy_filters:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS
+                            help: PLUGIN_ADMIN.ALLOW_URL_TAXONOMY_FILTERS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.twig_first:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_FIRST
+                            help: PLUGIN_ADMIN.TWIG_FIRST_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.never_cache_twig:
+                            type: toggle
+                            label: PLUGIN_ADMIN.NEVER_CACHE_TWIG
+                            help: PLUGIN_ADMIN.NEVER_CACHE_TWIG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.frontmatter.process_twig:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG
+                            help: PLUGIN_ADMIN.FRONTMATTER_PROCESS_TWIG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        pages.frontmatter.ignore_fields:
+                            type: selectize
+                            size: large
+                            placeholder: "e.g. forms"
+                            label: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS
+                            help: PLUGIN_ADMIN.FRONTMATTER_IGNORE_FIELDS_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                languages:
+                    type: tab
+                    title: PLUGIN_ADMIN.LANGUAGES
+
+                    fields:
+                        languages-section:
+                            type: section
+                            title: PLUGIN_ADMIN.LANGUAGES
+                            underline: true
+
+                        languages.supported:
+                            type: selectize
+                            size: large
+                            placeholder: "e.g. en, fr"
+                            label: PLUGIN_ADMIN.SUPPORTED
+                            help: PLUGIN_ADMIN.SUPPORTED_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        languages.default_lang:
+                            type: text
+                            size: x-small
+                            label: PLUGIN_ADMIN.DEFAULT_LANG
+                            help: PLUGIN_ADMIN.DEFAULT_LANG_HELP
+
+                        languages.include_default_lang:
+                            type: toggle
+                            label: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG
+                            help: PLUGIN_ADMIN.INCLUDE_DEFAULT_LANG_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.pages_fallback_only:
+                            type: toggle
+                            label: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY
+                            help: PLUGIN_ADMIN.PAGES_FALLBACK_ONLY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.translations:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TRANSLATIONS_ENABLED
+                            help: PLUGIN_ADMIN.TRANSLATIONS_ENABLED_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.translations_fallback:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK
+                            help: PLUGIN_ADMIN.TRANSLATIONS_FALLBACK_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.session_store_active:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION
+                            help: PLUGIN_ADMIN.ACTIVE_LANGUAGE_IN_SESSION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.http_accept_language:
+                            type: toggle
+                            label: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE
+                            help: PLUGIN_ADMIN.HTTP_ACCEPT_LANGUAGE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        languages.override_locale:
+                            type: toggle
+                            label: PLUGIN_ADMIN.OVERRIDE_LOCALE
+                            help: PLUGIN_ADMIN.OVERRIDE_LOCALE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                http_headers:
+                    type: tab
+                    title: PLUGIN_ADMIN.HTTP_HEADERS
+
+                    fields:
+                        http_headers_section:
+                            type: section
+                            title: PLUGIN_ADMIN.HTTP_HEADERS
+                            underline: true
+
+                        pages.expires:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.EXPIRES
+                            help: PLUGIN_ADMIN.EXPIRES_HELP
+                            validate:
+                                type: number
+                                min: 1
+                        pages.cache_control:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.CACHE_CONTROL
+                            help: PLUGIN_ADMIN.CACHE_CONTROL_HELP
+                            placeholder: 'e.g. public, max-age=31536000'
+                        pages.last_modified:
+                            type: toggle
+                            label: PLUGIN_ADMIN.LAST_MODIFIED
+                            help: PLUGIN_ADMIN.LAST_MODIFIED_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.etag:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ETAG
+                            help: PLUGIN_ADMIN.ETAG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.vary_accept_encoding:
+                            type: toggle
+                            label: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING
+                            help: PLUGIN_ADMIN.VARY_ACCEPT_ENCODING_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                markdown:
+                    type: tab
+                    title: PLUGIN_ADMIN.MARKDOWN
+
+                    fields:
+                        markdow_section:
+                            type: section
+                            title: PLUGIN_ADMIN.MARKDOWN
+                            underline: true
+
+                        pages.markdown.extra:
+                            type: toggle
+                            label: Markdown extra
+                            help: PLUGIN_ADMIN.MARKDOWN_EXTRA_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.auto_line_breaks:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTO_LINE_BREAKS
+                            help: PLUGIN_ADMIN.AUTO_LINE_BREAKS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.auto_url_links:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTO_URL_LINKS
+                            help: PLUGIN_ADMIN.AUTO_URL_LINKS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+                        pages.markdown.escape_markup:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ESCAPE_MARKUP
+                            help: PLUGIN_ADMIN.ESCAPE_MARKUP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                caching:
+                    type: tab
+                    title: PLUGIN_ADMIN.CACHING
+
+                    fields:
+                        caching_section:
+                            type: section
+                            title: PLUGIN_ADMIN.CACHING
+                            underline: true
+
+                        cache.enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CACHING
+                            help: PLUGIN_ADMIN.CACHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.check.method:
+                            type: select
+                            size: medium
+                            classes: fancy
+                            label: PLUGIN_ADMIN.CACHE_CHECK_METHOD
+                            help: PLUGIN_ADMIN.CACHE_CHECK_METHOD_HELP
+                            options:
+                                file: Markdown + Yaml file timestamps
+                                folder: Folder timestamps
+                                hash: All files timestamps
+                                none: No timestamp checking
+
+                        cache.driver:
+                            type: select
+                            size: small
+                            classes: fancy
+                            label: PLUGIN_ADMIN.CACHE_DRIVER
+                            help: PLUGIN_ADMIN.CACHE_DRIVER_HELP
+                            options:
+                                auto: Auto detect
+                                file: File
+                                apc: APC
+                                apcu: APCu
+                                xcache: Xcache
+                                memcache: Memcache
+                                memcached: Memcached
+                                wincache: WinCache
+                                redis: Redis
+
+                        cache.prefix:
+                            type: text
+                            size: x-small
+                            label: PLUGIN_ADMIN.CACHE_PREFIX
+                            help: PLUGIN_ADMIN.CACHE_PREFIX_HELP
+                            placeholder: PLUGIN_ADMIN.CACHE_PREFIX_PLACEHOLDER
+
+                        cache.purge_at:
+                            type: cron
+                            label: PLUGIN_ADMIN.CACHE_PURGE_JOB
+                            help: PLUGIN_ADMIN.CACHE_PURGE_JOB_HELP
+                            default: '* 4 * * *'
+
+                        cache.clear_at:
+                            type: cron
+                            label: PLUGIN_ADMIN.CACHE_CLEAR_JOB
+                            help: PLUGIN_ADMIN.CACHE_CLEAR_JOB_HELP
+                            default: '* 3 * * *'
+
+                        cache.clear_job_type:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.CACHE_JOB_TYPE
+                            help: PLUGIN_ADMIN.CACHE_JOB_TYPE_HELP
+                            options:
+                                standard: Standard Cache Folders
+                                all: All Cache Folders
+
+                        cache.clear_images_by_default:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT
+                            help: PLUGIN_ADMIN.CLEAR_IMAGES_BY_DEFAULT_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.cli_compatibility:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CLI_COMPATIBILITY
+                            help: PLUGIN_ADMIN.CLI_COMPATIBILITY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.lifetime:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.LIFETIME
+                            help: PLUGIN_ADMIN.LIFETIME_HELP
+                            validate:
+                                type: number
+
+                        cache.gzip:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GZIP_COMPRESSION
+                            help: PLUGIN_ADMIN.GZIP_COMPRESSION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.allow_webserver_gzip:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP
+                            help: PLUGIN_ADMIN.ALLOW_WEBSERVER_GZIP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        cache.memcache.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.MEMCACHE_SERVER
+                            help: PLUGIN_ADMIN.MEMCACHE_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.memcache.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.MEMCACHE_PORT
+                            help: PLUGIN_ADMIN.MEMCACHE_PORT_HELP
+                            placeholder: "11211"
+
+                        cache.memcached.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.MEMCACHED_SERVER
+                            help: PLUGIN_ADMIN.MEMCACHED_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.memcached.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.MEMCACHED_PORT
+                            help: PLUGIN_ADMIN.MEMCACHED_PORT_HELP
+                            placeholder: "11211"
+
+                        cache.redis.socket:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.REDIS_SOCKET
+                            help: PLUGIN_ADMIN.REDIS_SOCKET_HELP
+                            placeholder: "/var/run/redis/redis.sock"
+
+                        cache.redis.server:
+                            type: text
+                            size: medium
+                            label: PLUGIN_ADMIN.REDIS_SERVER
+                            help: PLUGIN_ADMIN.REDIS_SERVER_HELP
+                            placeholder: "localhost"
+
+                        cache.redis.port:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.REDIS_PORT
+                            help: PLUGIN_ADMIN.REDIS_PORT_HELP
+                            placeholder: "6379"
+
+                        cache.redis.password:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.REDIS_PASSWORD
+
+                twig:
+                    type: tab
+                    title: PLUGIN_ADMIN.TWIG_TEMPLATING
+
+                    fields:
+                        twig_section:
+                            type: section
+                            title: PLUGIN_ADMIN.TWIG_TEMPLATING
+                            underline: true
+
+                        twig.cache:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_CACHING
+                            help: PLUGIN_ADMIN.TWIG_CACHING_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.debug:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_DEBUG
+                            help: PLUGIN_ADMIN.TWIG_DEBUG_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.auto_reload:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DETECT_CHANGES
+                            help: PLUGIN_ADMIN.DETECT_CHANGES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.autoescape:
+                            type: toggle
+                            label: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES
+                            help: PLUGIN_ADMIN.AUTOESCAPE_VARIABLES_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        twig.umask_fix:
+                            type: toggle
+                            label: PLUGIN_ADMIN.TWIG_UMASK_FIX
+                            help: PLUGIN_ADMIN.TWIG_UMASK_FIX_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                assets:
+                    type: tab
+                    title: PLUGIN_ADMIN.ASSETS
+
+                    fields:
+                        assets_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ASSETS
+                            underline: true
+
+                        assets.css_pipeline:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_pipeline_include_externals:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_INCLUDE_EXTERNALS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_pipeline_before_excludes:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES
+                            help: PLUGIN_ADMIN.CSS_PIPELINE_BEFORE_EXCLUDES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_minify:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_MINIFY
+                            help: PLUGIN_ADMIN.CSS_MINIFY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.css_minify_windows:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE
+                            help: PLUGIN_ADMIN.CSS_MINIFY_WINDOWS_OVERRIDE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.css_rewrite:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CSS_REWRITE
+                            help: PLUGIN_ADMIN.CSS_REWRITE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.js_pipeline:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.js_pipeline_include_externals:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.js_pipeline_before_excludes:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES
+                            help: PLUGIN_ADMIN.JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                               type: bool
+
+                        assets.js_minify:
+                            type: toggle
+                            label: PLUGIN_ADMIN.JAVASCRIPT_MINIFY
+                            help: PLUGIN_ADMIN.JAVASCRIPT_MINIFY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.enable_asset_timestamp:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS
+                            help: PLUGIN_ADMIN.ENABLED_TIMESTAMPS_ON_ASSETS_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        assets.collections:
+                            type: multilevel
+                            label: PLUGIN_ADMIN.COLLECTIONS
+                            placeholder_key: collection_name
+                            placeholder_value: collection_path
+                            validate:
+                                type: array
+
+                errors:
+                    type: tab
+                    title: PLUGIN_ADMIN.ERROR_HANDLER
+
+                    fields:
+                        errors_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ERROR_HANDLER
+                            underline: true
+
+                        errors.display:
+                            type: select
+                            label: PLUGIN_ADMIN.DISPLAY_ERRORS
+                            help: PLUGIN_ADMIN.DISPLAY_ERRORS_HELP
+                            size: medium
+                            highlight: 1
+                            options:
+                                -1: PLUGIN_ADMIN.ERROR_SYSTEM
+                                0: PLUGIN_ADMIN.ERROR_SIMPLE
+                                1: PLUGIN_ADMIN.ERROR_FULL_BACKTRACE
+                            validate:
+                                type: int
+
+
+                        errors.log:
+                            type: toggle
+                            label: PLUGIN_ADMIN.LOG_ERRORS
+                            help: PLUGIN_ADMIN.LOG_ERRORS_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        log.handler:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.LOG_HANDLER
+                            help: PLUGIN_ADMIN.LOG_HANDLER_HELP
+                            default: 'file'
+                            options:
+                                'file': 'File'
+                                'syslog': 'Syslog'
+
+                        log.syslog.facility:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.SYSLOG_FACILITY
+                            help: PLUGIN_ADMIN.SYSLOG_FACILITY_HELP
+                            default: local6
+                            options:
+                              auth: auth
+                              authpriv: authpriv
+                              cron: cron
+                              daemon: daemon
+                              kern: kern
+                              lpr: lpr
+                              mail: mail
+                              news: news
+                              syslog: syslog
+                              user: user
+                              uucp: uucp
+                              local0: local0
+                              local1: local1
+                              local2: local2
+                              local3: local3
+                              local4: local4
+                              local5: local5
+                              local6: local6
+                              local7: local7
+
+                debugger:
+                    type: tab
+                    title: PLUGIN_ADMIN.DEBUGGER
+
+                    fields:
+                        debugger_section:
+                            type: section
+                            title: PLUGIN_ADMIN.DEBUGGER
+                            underline: true
+
+                        debugger.enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.DEBUGGER
+                            help: PLUGIN_ADMIN.DEBUGGER_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        debugger.shutdown.close_connection:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION
+                            help: PLUGIN_ADMIN.SHUTDOWN_CLOSE_CONNECTION_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                media:
+                    type: tab
+                    title: PLUGIN_ADMIN.MEDIA
+
+                    fields:
+                        media_section:
+                            type: section
+                            title: PLUGIN_ADMIN.MEDIA
+                            underline: true
+
+                        images.default_image_quality:
+                            type: range
+                            append: '%'
+                            label: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY
+                            help: PLUGIN_ADMIN.DEFAULT_IMAGE_QUALITY_HELP
+                            validate:
+                                min: 1
+                                max: 100
+
+                        images.cache_all:
+                            type: toggle
+                            label: PLUGIN_ADMIN.CACHE_ALL
+                            help: PLUGIN_ADMIN.CACHE_ALL_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.cache_perms:
+                            type: select
+                            size: small
+                            label: PLUGIN_ADMIN.CACHE_PERMS
+                            help: PLUGIN_ADMIN.CACHE_PERMS_HELP
+                            highlight: '0755'
+                            options:
+                                '0755': '0755'
+                                '0775': '0775'
+
+                        images.debug:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_DEBUG
+                            help: PLUGIN_ADMIN.IMAGES_DEBUG_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.auto_fix_orientation:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION
+                            help: PLUGIN_ADMIN.IMAGES_AUTO_FIX_ORIENTATION_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        images.seofriendly:
+                            type: toggle
+                            label: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY
+                            help: PLUGIN_ADMIN.IMAGES_SEOFRIENDLY_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        media.enable_media_timestamp:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP
+                            help: PLUGIN_ADMIN.ENABLE_MEDIA_TIMESTAMP_HELP
+                            highlight: 0
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        media.auto_metadata_exif:
+                              type: toggle
+                              label: PLUGIN_ADMIN.ENABLE_AUTO_METADATA
+                              help: PLUGIN_ADMIN.ENABLE_AUTO_METADATA_HELP
+                              highlight: 0
+                              options:
+                                  1: PLUGIN_ADMIN.YES
+                                  0: PLUGIN_ADMIN.NO
+                              validate:
+                                  type: bool
+
+
+                        media.allowed_fallback_types:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.FALLBACK_TYPES
+                            help: PLUGIN_ADMIN.FALLBACK_TYPES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                        media.unsupported_inline_types:
+                            type: selectize
+                            size: large
+                            label: PLUGIN_ADMIN.INLINE_TYPES
+                            help: PLUGIN_ADMIN.INLINE_TYPES_HELP
+                            classes: fancy
+                            validate:
+                                type: commalist
+
+                session:
+                    type: tab
+                    title: PLUGIN_ADMIN.SESSION
+
+                    fields:
+                        session_section:
+                            type: section
+                            title: PLUGIN_ADMIN.SESSION
+                            underline: true
+
+                        session.enabled:
+                            type: hidden
+                            label: PLUGIN_ADMIN.ENABLED
+                            help: PLUGIN_ADMIN.SESSION_ENABLED_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.initialize:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_INITIALIZE
+                            help: PLUGIN_ADMIN.SESSION_INITIALIZE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.timeout:
+                            type: text
+                            size: small
+                            append: GRAV.NICETIME.SECOND_PLURAL
+                            label: PLUGIN_ADMIN.TIMEOUT
+                            help: PLUGIN_ADMIN.TIMEOUT_HELP
+                            validate:
+                                type: number
+                                min: 0
+
+                        session.name:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.NAME
+                            help: PLUGIN_ADMIN.SESSION_NAME_HELP
+
+                        session.uniqueness:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.SESSION_UNIQUENESS
+                            help: PLUGIN_ADMIN.SESSION_UNIQUENESS_HELP
+                            highlight: path
+                            default: path
+                            options:
+                                path: Grav's root file path
+                                salt: Grav's random security salt
+
+                        session.secure:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_SECURE
+                            help: PLUGIN_ADMIN.SESSION_SECURE_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: false
+                            validate:
+                                type: bool
+
+                        session.httponly:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_HTTPONLY
+                            help: PLUGIN_ADMIN.SESSION_HTTPONLY_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        session.path:
+                            type: text
+                            size: small
+                            label: PLUGIN_ADMIN.SESSION_PATH
+                            help: PLUGIN_ADMIN.SESSION_PATH_HELP
+
+                        session.split:
+                            type: toggle
+                            label: PLUGIN_ADMIN.SESSION_SPLIT
+                            help: PLUGIN_ADMIN.SESSION_SPLIT_HELP
+                            highlight: 1
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                advanced:
+                    type: tab
+                    title: PLUGIN_ADMIN.ADVANCED
+
+                    fields:
+                        advanced_section:
+                            type: section
+                            title: PLUGIN_ADMIN.ADVANCED
+                            underline: true
+
+                        gpm.releases:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_RELEASES
+                            highlight: stable
+                            help: PLUGIN_ADMIN.GPM_RELEASES_HELP
+                            options:
+                                stable: PLUGIN_ADMIN.STABLE
+                                testing: PLUGIN_ADMIN.TESTING
+
+                        gpm.proxy_url:
+                            type: text
+                            size: medium
+                            placeholder: "e.g. 127.0.0.1:3128"
+                            label: PLUGIN_ADMIN.PROXY_URL
+                            help: PLUGIN_ADMIN.PROXY_URL_HELP
+
+                        gpm.method:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_METHOD
+                            highlight: auto
+                            help: PLUGIN_ADMIN.GPM_METHOD_HELP
+                            options:
+                                auto: PLUGIN_ADMIN.AUTO
+                                fopen: PLUGIN_ADMIN.FOPEN
+                                curl: PLUGIN_ADMIN.CURL
+
+                        gpm.official_gpm_only:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY
+                            highlight: 1
+                            help: PLUGIN_ADMIN.GPM_OFFICIAL_ONLY_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            default: true
+                            validate:
+                                type: bool
+
+                        gpm.verify_peer:
+                            type: toggle
+                            label: PLUGIN_ADMIN.GPM_VERIFY_PEER
+                            highlight: 1
+                            help: PLUGIN_ADMIN.GPM_VERIFY_PEER_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        reverse_proxy_setup:
+                            type: toggle
+                            label: PLUGIN_ADMIN.REVERSE_PROXY
+                            highlight: 0
+                            help: PLUGIN_ADMIN.REVERSE_PROXY_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        username_regex:
+                            type: text
+                            size: large
+                            label: PLUGIN_ADMIN.USERNAME_REGEX
+                            help: PLUGIN_ADMIN.USERNAME_REGEX_HELP
+
+                        pwd_regex:
+                            type: text
+                            size: large
+                            label: PLUGIN_ADMIN.PWD_REGEX
+                            help: PLUGIN_ADMIN.PWD_REGEX_HELP
+
+                        intl_enabled:
+                            type: toggle
+                            label: PLUGIN_ADMIN.INTL_ENABLED
+                            highlight: 1
+                            help: PLUGIN_ADMIN.INTL_ENABLED_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        wrapped_site:
+                            type: toggle
+                            label: PLUGIN_ADMIN.WRAPPED_SITE
+                            highlight: 0
+                            help: PLUGIN_ADMIN.WRAPPED_SITE_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        absolute_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.ABSOLUTE_URLS
+                            highlight: 0
+                            help: PLUGIN_ADMIN.ABSOLUTE_URLS_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        param_sep:
+                            type: select
+                            size: medium
+                            label: PLUGIN_ADMIN.PARAMETER_SEPARATOR
+                            classes: fancy
+                            help: PLUGIN_ADMIN.PARAMETER_SEPARATOR_HELP
+                            default: ''
+                            options:
+                                ':': ': (default)'
+                                ';': '; (for Apache running on Windows)'
+
+                        force_ssl:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FORCE_SSL
+                            highlight: 0
+                            help: PLUGIN_ADMIN.FORCE_SSL_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        force_lowercase_urls:
+                            type: toggle
+                            label: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.FORCE_LOWERCASE_URLS_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        custom_base_url:
+                            type: text
+                            size: medium
+                            placeholder: "e.g. http://yoursite.com/yourpath"
+                            label: PLUGIN_ADMIN.CUSTOM_BASE_URL
+                            help: PLUGIN_ADMIN.CUSTOM_BASE_URL_HELP
+
+                        accounts.type:
+                            type: hidden
+
+                        accounts.storage:
+                            type: hidden
+
+                        strict_mode.yaml_compat:
+                            type: toggle
+                            label: PLUGIN_ADMIN.STRICT_YAML_COMPAT
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.STRICT_YAML_COMPAT_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool
+
+                        strict_mode.twig_compat:
+                            type: toggle
+                            label: PLUGIN_ADMIN.STRICT_TWIG_COMPAT
+                            highlight: 1
+                            default: 1
+                            help: PLUGIN_ADMIN.STRICT_TWIG_COMPAT_HELP
+                            options:
+                                1: PLUGIN_ADMIN.YES
+                                0: PLUGIN_ADMIN.NO
+                            validate:
+                                type: bool

+ 121 - 119
system/blueprints/user/account.yaml

@@ -4,122 +4,124 @@ form:
 
 
     fields:
     fields:
 
 
-            info:
-                type: userinfo
-                size: large
-
-            avatar:
-                type: file
-                size: large
-                destination: 'user://accounts/avatars'
-                multiple: false
-                random_name: true
-
-            content:
-                type: section
-                title: PLUGIN_ADMIN.ACCOUNT
-                underline: true
-
-            username:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.USERNAME
-                disabled: true
-                readonly: true
-
-            email:
-                type: email
-                size: large
-                label: PLUGIN_ADMIN.EMAIL
-                validate:
-                  type: email
-                  message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
-                  required: true
-
-            password:
-                type: password
-                size: large
-                label: PLUGIN_ADMIN.PASSWORD
-                autocomplete: new-password
-                validate:
-                  required: false
-                  message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
-                  config-pattern@: system.pwd_regex
-
-            fullname:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.FULL_NAME
-                validate:
-                  required: true
-
-            title:
-                type: text
-                size: large
-                label: PLUGIN_ADMIN.TITLE
-
-            language:
-                type: select
-                label: PLUGIN_ADMIN.LANGUAGE
-                size: medium
-                classes: fancy
-                data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages'
-                default: 'en'
-                help: PLUGIN_ADMIN.LANGUAGE_HELP
-
-            twofa_check:
-                type: conditional
-                condition: config.plugins.admin.twofa_enabled
-
-                fields:
-
-                    twofa:
-                        title: PLUGIN_ADMIN.2FA_TITLE
-                        type: section
-                        underline: true
-
-                    twofa_enabled:
-                        type: toggle
-                        label: PLUGIN_ADMIN.2FA_ENABLED
-                        classes: twofa-toggle
-                        highlight: 1
-                        default: 0
-                        options:
-                          1: PLUGIN_ADMIN.YES
-                          0: PLUGIN_ADMIN.NO
-                        validate:
-                          type: bool
-
-
-                    twofa_secret:
-                        type: 2fa_secret
-                        outerclasses: 'twofa-secret'
-                        markdown: true
-                        label: PLUGIN_ADMIN.2FA_SECRET
-                        sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
-
-
-            security:
-                title: PLUGIN_ADMIN.ACCESS_LEVELS
-                type: section
-                security: admin.super
-                underline: true
-
-                fields:
-                    groups:
-                        type: select
-                        multiple: true
-                        size: large
-                        label: PLUGIN_ADMIN.GROUPS
-                        data-options@: '\Grav\Common\User\Group::groupNames'
-                        classes: fancy
-                        help: PLUGIN_ADMIN.GROUPS_HELP
-                        validate:
-                            type: commalist
-
-                    access:
-                        type: permissions
-                        label: PLUGIN_ADMIN.PERMISSIONS
-                        ignore_empty: true
-                        validate:
-                            type: array
+        info:
+            type: userinfo
+            size: large
+
+        avatar:
+            type: file
+            size: large
+            destination: 'user://accounts/avatars'
+            multiple: false
+            random_name: true
+
+        content:
+            type: section
+            title: PLUGIN_ADMIN.ACCOUNT
+            underline: true
+
+        username:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.USERNAME
+            disabled: true
+            readonly: true
+
+        email:
+            type: email
+            size: large
+            label: PLUGIN_ADMIN.EMAIL
+            validate:
+              type: email
+              message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
+              required: true
+
+        password:
+            type: password
+            size: large
+            label: PLUGIN_ADMIN.PASSWORD
+            autocomplete: new-password
+            validate:
+              required: false
+              message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
+              config-pattern@: system.pwd_regex
+
+        fullname:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.FULL_NAME
+            validate:
+              required: true
+
+        title:
+            type: text
+            size: large
+            label: PLUGIN_ADMIN.TITLE
+
+        language:
+            type: select
+            label: PLUGIN_ADMIN.LANGUAGE
+            size: medium
+            classes: fancy
+            data-options@: '\Grav\Plugin\Admin\Admin::adminLanguages'
+            default: 'en'
+            help: PLUGIN_ADMIN.LANGUAGE_HELP
+
+        twofa_check:
+            type: conditional
+            condition: config.plugins.admin.twofa_enabled
+
+            fields:
+
+                twofa:
+                    title: PLUGIN_ADMIN.2FA_TITLE
+                    type: section
+                    underline: true
+
+                twofa_enabled:
+                    type: toggle
+                    label: PLUGIN_ADMIN.2FA_ENABLED
+                    classes: twofa-toggle
+                    highlight: 1
+                    default: 0
+                    options:
+                      1: PLUGIN_ADMIN.YES
+                      0: PLUGIN_ADMIN.NO
+                    validate:
+                      type: bool
+
+
+                twofa_secret:
+                    type: 2fa_secret
+                    outerclasses: 'twofa-secret'
+                    markdown: true
+                    label: PLUGIN_ADMIN.2FA_SECRET
+                    sublabel: PLUGIN_ADMIN.2FA_SECRET_HELP
+
+
+        security:
+            security@: admin.super
+            title: PLUGIN_ADMIN.ACCESS_LEVELS
+            type: section
+            underline: true
+
+            fields:
+                groups:
+                    security@: admin.super
+                    type: select
+                    multiple: true
+                    size: large
+                    label: PLUGIN_ADMIN.GROUPS
+                    data-options@: '\Grav\Common\User\Group::groupNames'
+                    classes: fancy
+                    help: PLUGIN_ADMIN.GROUPS_HELP
+                    validate:
+                        type: commalist
+
+                access:
+                    security@: admin.super
+                    type: permissions
+                    label: PLUGIN_ADMIN.PERMISSIONS
+                    ignore_empty: true
+                    validate:
+                        type: array

+ 2 - 0
system/blueprints/user/account_new.yaml

@@ -12,5 +12,7 @@ form:
       type: text
       type: text
       label: PLUGIN_ADMIN.USERNAME
       label: PLUGIN_ADMIN.USERNAME
       help: PLUGIN_ADMIN.USERNAME_HELP
       help: PLUGIN_ADMIN.USERNAME_HELP
+      unset-disabled@: true
+      unset-readonly@: true
       validate:
       validate:
         required: true
         required: true

+ 39 - 0
system/blueprints/user/accounts.yaml

@@ -0,0 +1,39 @@
+title: User Accounts
+description: User Accounts
+type: flex-objects
+
+extends@: 'user/account'
+
+config:
+  admin:
+    list:
+      fields:
+        username:
+          link: edit
+          search: true
+        email:
+          search: true
+        fullname:
+          search: true
+      options:
+        per_page: 20
+        order:
+          by: username
+          dir: asc
+
+    menu:
+      list:
+        route: '/accounts'
+        title: Accounts
+        icon: fa-users
+        authorize: ['admin.users', 'admin.accounts', 'admin.super']
+
+form:
+  fields:
+    username:
+      flex-disabled@: exists
+      disabled: false
+      flex-readonly@: exists
+      readonly: false
+      validate:
+        required: true

+ 15 - 0
system/config/backups.yaml

@@ -0,0 +1,15 @@
+purge:
+    trigger: space
+    max_backups_count: 25
+    max_backups_space: 5
+    max_backups_time: 365
+
+profiles:
+  -
+    name: 'Default Site Backup'
+    root: '/'
+    schedule: false
+    schedule_at: '0 3 * * *'
+    exclude_paths: "/backup\r\n/cache\r\n/images\r\n/logs\r\n/tmp"
+    exclude_files: ".DS_Store\r\n.git\r\n.svn\r\n.hg\r\n.idea\r\n.vscode\r\nnode_modules"
+

+ 7 - 0
system/config/security.yaml

@@ -5,6 +5,13 @@ xss_enabled:
     moz_binding: true
     moz_binding: true
     html_inline_styles: true
     html_inline_styles: true
     dangerous_tags: true
     dangerous_tags: true
+xss_invalid_protocols:
+    - javascript
+    - livescript
+    - vbscript
+    - mocha
+    - feed
+    - data
 xss_dangerous_tags:
 xss_dangerous_tags:
     - applet
     - applet
     - meta
     - meta

+ 5 - 5
system/config/site.yaml

@@ -17,17 +17,17 @@ summary:
   delimiter: ===                            # The summary delimiter
   delimiter: ===                            # The summary delimiter
 
 
 redirects:
 redirects:
-#  '/redirect-test': '/'                         # Redirect test goes to home page
-#  '/old/(.*)': '/new/$1'                        # Would redirect /old/my-page to /new/my-page
+#  '/redirect-test': '/'                    # Redirect test goes to home page
+#  '/old/(.*)': '/new/$1'                   # Would redirect /old/my-page to /new/my-page
 
 
 routes:
 routes:
-#  '/something/else': '/blog/sample-3'         # Alias for /blog/sample-3
-#  '/new/(.*)': '/blog/$1'                     # Regex any /new/my-page URL to /blog/my-page Route
+#  '/something/else': '/blog/sample-3'      # Alias for /blog/sample-3
+#  '/new/(.*)': '/blog/$1'                  # Regex any /new/my-page URL to /blog/my-page Route
 
 
 blog:
 blog:
   route: '/blog'                            # Custom value added (accessible via system.blog.route)
   route: '/blog'                            # Custom value added (accessible via system.blog.route)
 
 
-#menu:                                      # Sample Menu Example
+#menu:                                      # Menu Example
 #    - text: Source
 #    - text: Source
 #      icon: github
 #      icon: github
 #      url: https://github.com/getgrav/grav
 #      url: https://github.com/getgrav/grav

+ 1 - 1
system/config/streams.yaml

@@ -1,6 +1,6 @@
 schemes:
 schemes:
   image:
   image:
-    type: ReadOnlyStream
+    type: Stream
     paths:
     paths:
       - user://images
       - user://images
       - system://images
       - system://images

+ 19 - 2
system/config/system.yaml

@@ -13,7 +13,9 @@ intl_enabled: true                               # Special logic for PHP Interna
 
 
 languages:
 languages:
   supported: []                                  # List of languages supported. eg: [en, fr, de]
   supported: []                                  # List of languages supported. eg: [en, fr, de]
+  default_lang:                                  # Default is the first supported language. Must be one of the supported languages
   include_default_lang: true                     # Include the default lang prefix in all URLs
   include_default_lang: true                     # Include the default lang prefix in all URLs
+  pages_fallback_only: false                     # Only fallback to find page content through supported languages
   translations: true                             # Enable translations by default
   translations: true                             # Enable translations by default
   translations_fallback: true                    # Fallback through supported translations if active lang doesn't exist
   translations_fallback: true                    # Fallback through supported translations if active lang doesn't exist
   session_store_active: false                    # Store active language in session
   session_store_active: false                    # Store active language in session
@@ -52,7 +54,7 @@ pages:
     special_chars:                               # List of special characters to automatically convert to entities
     special_chars:                               # List of special characters to automatically convert to entities
       '>': 'gt'
       '>': 'gt'
       '<': 'lt'
       '<': 'lt'
-  types: [txt,xml,html,htm,json,rss,atom]        # list of valid page types
+  types: [html,htm,xml,txt,json,rss,atom]        # list of valid page types
   append_url_extension: ''                       # Append page's extension in Page urls (e.g. '.html' results in /path/page.html)
   append_url_extension: ''                       # Append page's extension in Page urls (e.g. '.html' results in /path/page.html)
   expires: 604800                                # Page expires time in seconds (604800 seconds = 7 days)
   expires: 604800                                # Page expires time in seconds (604800 seconds = 7 days)
   cache_control:                                 # Can be blank for no setting, or a valid `cache-control` text value
   cache_control:                                 # Can be blank for no setting, or a valid `cache-control` text value
@@ -65,6 +67,7 @@ pages:
   ignore_files: [.DS_Store]                      # Files to ignore in Pages
   ignore_files: [.DS_Store]                      # Files to ignore in Pages
   ignore_folders: [.git, .idea]                  # Folders to ignore in Pages
   ignore_folders: [.git, .idea]                  # Folders to ignore in Pages
   ignore_hidden: true                            # Ignore all Hidden files and folders
   ignore_hidden: true                            # Ignore all Hidden files and folders
+  hide_empty_folders: false                      # If folder has no .md file, should it be hidden
   url_taxonomy_filters: true                     # Enable auto-magic URL-based taxonomy filters for page collections
   url_taxonomy_filters: true                     # Enable auto-magic URL-based taxonomy filters for page collections
   frontmatter:
   frontmatter:
     process_twig: false                          # Should the frontmatter be processed to replace Twig variables?
     process_twig: false                          # Should the frontmatter be processed to replace Twig variables?
@@ -74,8 +77,11 @@ cache:
   enabled: true                                  # Set to true to enable caching
   enabled: true                                  # Set to true to enable caching
   check:
   check:
     method: file                                 # Method to check for updates in pages: file|folder|hash|none
     method: file                                 # Method to check for updates in pages: file|folder|hash|none
-  driver: auto                                   # One of: auto|file|apc|xcache|memcache|wincache
+  driver: auto                                   # One of: auto|file|apcu|memcache|wincache
   prefix: 'g'                                    # Cache prefix string (prevents cache conflicts)
   prefix: 'g'                                    # Cache prefix string (prevents cache conflicts)
+  purge_at: '0 4 * * *'                          # How often to purge old file cache (using new scheduler)
+  clear_at: '0 3 * * *'                           # How often to clear cache (using new scheduler)
+  clear_job_type: 'standard'                     # Type to clear when processing the scheduled clear job `standard`|`all`
   clear_images_by_default: true                  # By default grav will include processed images in cache clear, this can be disabled
   clear_images_by_default: true                  # By default grav will include processed images in cache clear, this can be disabled
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   cli_compatibility: false                       # Ensures only non-volatile drivers are used (file, redis, memcache, etc.)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
   lifetime: 604800                               # Lifetime of cached data in seconds (0 = infinite)
@@ -112,6 +118,11 @@ errors:
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
   display: 0                                     # Display either (1) Full backtrace | (0) Simple Error | (-1) System Error
   log: true                                      # Log errors to /logs folder
   log: true                                      # Log errors to /logs folder
 
 
+log:
+  handler: file                                 # Log handler. Currently supported: file | syslog
+  syslog:
+    facility: local6                            # Syslog facilities output
+
 debugger:
 debugger:
   enabled: false                                 # Enable Grav debugger and following settings
   enabled: false                                 # Enable Grav debugger and following settings
   shutdown:
   shutdown:
@@ -123,6 +134,7 @@ images:
   cache_perms: '0755'                            # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775'
   cache_perms: '0755'                            # MUST BE IN QUOTES!! Default cache folder perms. Usually '0755' or '0775'
   debug: false                                   # Show an overlay over images indicating the pixel depth of the image when working with retina for example
   debug: false                                   # Show an overlay over images indicating the pixel depth of the image when working with retina for example
   auto_fix_orientation: false                    # Automatically fix the image orientation based on the Exif data
   auto_fix_orientation: false                    # Automatically fix the image orientation based on the Exif data
+  seofriendly: false                             # SEO-friendly processed image names
 
 
 media:
 media:
   enable_media_timestamp: false                  # Enable media timestamps
   enable_media_timestamp: false                  # Enable media timestamps
@@ -135,6 +147,7 @@ session:
   initialize: true                               # Initialize session from Grav (if false, plugin needs to start the session)
   initialize: true                               # Initialize session from Grav (if false, plugin needs to start the session)
   timeout: 1800                                  # Timeout in seconds
   timeout: 1800                                  # Timeout in seconds
   name: grav-site                                # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name
   name: grav-site                                # Name prefix of the session cookie. Use alphanumeric, dashes or underscores only. Do not use dots in the session name
+  uniqueness: path                               # Should sessions be `path` based or `security.salt` based
   secure: false                                  # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
   secure: false                                  # Set session secure. If true, indicates that communication for this cookie must be over an encrypted transmission. Enable this only on sites that run exclusively on HTTPS
   httponly: true                                 # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
   httponly: true                                 # Set session HTTP only. If true, indicates that cookies should be used only over HTTP, and JavaScript modification is not allowed.
   split: true                                    # Sessions should be independent between site and plugins (such as admin)
   split: true                                    # Sessions should be independent between site and plugins (such as admin)
@@ -147,6 +160,10 @@ gpm:
   verify_peer: true                              # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
   verify_peer: true                              # Sometimes on some systems (Windows most commonly) GPM is unable to connect because the SSL certificate cannot be verified. Disabling this setting might help.
   official_gpm_only: true                        # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
   official_gpm_only: true                        # By default GPM direct-install will only allow URLs via the official GPM proxy to ensure security
 
 
+accounts:
+  type: data                                     # Account type: data or flex
+  storage: file                                  # Flex storage type: file or folder
+
 strict_mode:
 strict_mode:
   yaml_compat: true                              # Grav 1.5+: Enables YAML backwards compatibility
   yaml_compat: true                              # Grav 1.5+: Enables YAML backwards compatibility
   twig_compat: true                              # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false)
   twig_compat: true                              # Grav 1.5+: Enables deprecated Twig autoescape setting (autoescape: false)

+ 4 - 4
system/defines.php

@@ -1,19 +1,19 @@
 <?php
 <?php
 /**
 /**
- * @package    Grav.Core
+ * @package    Grav\Core
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
 // Some standard defines
 // Some standard defines
 define('GRAV', true);
 define('GRAV', true);
-define('GRAV_VERSION', '1.5.8');
+define('GRAV_VERSION', '1.6.16');
 define('GRAV_TESTING', false);
 define('GRAV_TESTING', false);
 define('DS', '/');
 define('DS', '/');
 
 
 if (!defined('GRAV_PHP_MIN')) {
 if (!defined('GRAV_PHP_MIN')) {
-    define('GRAV_PHP_MIN', '5.6.4');
+    define('GRAV_PHP_MIN', '7.1.3');
 }
 }
 
 
 // Directories and Paths
 // Directories and Paths

+ 17 - 0
system/install.php

@@ -0,0 +1,17 @@
+<?php
+/**
+ * @package    Grav\Core
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+if (!defined('GRAV_ROOT')) {
+    die();
+}
+
+use Grav\Installer\Install;
+
+require_once __DIR__ . '/src/Grav/Installer/Install.php';
+
+return Install::instance();

+ 62 - 74
system/languages/ar.yaml

@@ -1,75 +1,63 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  العنوان: %1$s
-  ---
-  # خطأ: مادة أمامية غير صحيحة
-  
-  مسار: '%2$s'
-  
-  **%3$s**
-  
-  , , ,
-  
-  %4$s
-  , , ,
-NICETIME:
-  NO_DATE_PROVIDED: لم يتم تقديم التاريخ
-  BAD_DATE: تاريخ خاطئ
-  AGO: من قبل
-  FROM_NOW: من الآن
-  SECOND: ثانية
-  MINUTE: دقيقة
-  HOUR: ساعة
-  DAY: يوم
-  WEEK: أسبوع
-  MONTH: شهر
-  YEAR: سنة
-  DECADE: عقد
-  SEC: ثانية
-  MIN: دقيقة
-  HR: ساعة
-  WK: أسبوع
-  MO: شهر
-  YR: سنة
-  DEC: عقد
-  SECOND_PLURAL: ثواني
-  MINUTE_PLURAL: ‮دقائق
-  HOUR_PLURAL: ساعات
-  DAY_PLURAL: أيام
-  WEEK_PLURAL: أسابيع
-  MONTH_PLURAL: أشهر
-  YEAR_PLURAL: سنوات
-  DECADE_PLURAL: عقود
-  SEC_PLURAL: ثواني
-  MIN_PLURAL: دقائق
-  HR_PLURAL: ساعات
-  WK_PLURAL: أسابيع
-  MO_PLURAL: أشهر
-  YR_PLURAL: سنوات
-  DEC_PLURAL: عقود
-FORM:
-  VALIDATION_FAIL: '<b>فشل التحقق من صحة:</b>'
-  INVALID_INPUT: إدخال غير صحيح في
-  MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
-MONTHS_OF_THE_YEAR:
-  - كانون الثاني
-  - شباط
-  - آذار/ مارس
-  - نيسان
-  - أيار
-  - حزيران
-  - تموز
-  - آب
-  - أيلول
-  - تشرين الأول
-  - تشرين الثاني
-  - كانون الأول
-DAYS_OF_THE_WEEK:
-  - الاثنين
-  - الثلاثاء
-  - الأربعاء
-  - الخميس
-  - الجمعة
-  - السبت
-  - الأحد
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nالعنوان: %1$s\n---\n# خطأ: مادة أمامية غير صحيحة\n\nمسار: '%2$s'\n\n**%3$s**\n\n, , ,\n\n%4$s\n, , ,"
+  NICETIME:
+    NO_DATE_PROVIDED: لم يتم تقديم التاريخ
+    BAD_DATE: تاريخ خاطئ
+    AGO: من قبل
+    FROM_NOW: من الآن
+    SECOND: ثانية
+    MINUTE: دقيقة
+    HOUR: ساعة
+    DAY: يوم
+    WEEK: أسبوع
+    MONTH: شهر
+    YEAR: سنة
+    DECADE: عقد
+    SEC: ثانية
+    MIN: دقيقة
+    HR: ساعة
+    WK: أسبوع
+    MO: شهر
+    YR: سنة
+    DEC: عقد
+    SECOND_PLURAL: ثواني
+    MINUTE_PLURAL: '‮دقائق'
+    HOUR_PLURAL: ساعات
+    DAY_PLURAL: أيام
+    WEEK_PLURAL: أسابيع
+    MONTH_PLURAL: أشهر
+    YEAR_PLURAL: سنوات
+    DECADE_PLURAL: عقود
+    SEC_PLURAL: ثواني
+    MIN_PLURAL: دقائق
+    HR_PLURAL: ساعات
+    WK_PLURAL: أسابيع
+    MO_PLURAL: أشهر
+    YR_PLURAL: سنوات
+    DEC_PLURAL: عقود
+  FORM:
+    VALIDATION_FAIL: <b>فشل التحقق من صحة:</b>
+    INVALID_INPUT: إدخال غير صحيح في
+    MISSING_REQUIRED_FIELD: 'حقل مطلوب مفقود:'
+  MONTHS_OF_THE_YEAR:
+    - 'كانون الثاني'
+    - 'شباط'
+    - 'آذار/ مارس'
+    - 'نيسان'
+    - 'أيار'
+    - 'حزيران'
+    - 'تموز'
+    - 'آب'
+    - 'أيلول'
+    - 'تشرين الأول'
+    - 'تشرين الثاني'
+    - 'كانون الأول'
+  DAYS_OF_THE_WEEK:
+    - 'الاثنين'
+    - 'الثلاثاء'
+    - 'الأربعاء'
+    - 'الخميس'
+    - 'الجمعة'
+    - 'السبت'
+    - 'الأحد'

+ 62 - 0
system/languages/bg.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  NICETIME:
+    NO_DATE_PROVIDED: Не е въведена дата
+    BAD_DATE: Невалидна дата
+    AGO: преди
+    FROM_NOW: от сега
+    SECOND: секунда
+    MINUTE: минута
+    HOUR: час
+    DAY: ден
+    WEEK: седмица
+    MONTH: месец
+    YEAR: година
+    DECADE: десетилетие
+    SEC: сек
+    MIN: мин
+    HR: ч
+    WK: седм
+    MO: мес
+    YR: г
+    DEC: дстлт
+    SECOND_PLURAL: секунди
+    MINUTE_PLURAL: минути
+    HOUR_PLURAL: часа
+    DAY_PLURAL: дена
+    WEEK_PLURAL: седмици
+    MONTH_PLURAL: месеца
+    YEAR_PLURAL: години
+    DECADE_PLURAL: десетилетия
+    SEC_PLURAL: сек
+    MIN_PLURAL: мин
+    HR_PLURAL: ч
+    WK_PLURAL: седм
+    MO_PLURAL: мес
+    YR_PLURAL: г
+    DEC_PLURAL: дстлт
+  FORM:
+    VALIDATION_FAIL: <b>Неуспешна проверка:</b>
+    INVALID_INPUT: Невалидно въвеждане в
+    MISSING_REQUIRED_FIELD: 'Липсва задължително поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'януари'
+    - 'февруари'
+    - 'март'
+    - 'април'
+    - 'май'
+    - 'юни'
+    - 'юли'
+    - 'август'
+    - 'септември'
+    - 'октомври'
+    - 'ноември'
+    - 'декември'
+  DAYS_OF_THE_WEEK:
+    - 'понеделник'
+    - 'вторник'
+    - 'сряда'
+    - 'четвъртък'
+    - 'петък'
+    - 'събота'
+    - 'неделя'

+ 61 - 74
system/languages/ca.yaml

@@ -1,75 +1,62 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # S'ha produït un error: Frontmatter invàlid
-  
-  Ruta: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: "No s'ha proporcionat data"
-  BAD_DATE: Data invàlida
-  AGO: abans
-  FROM_NOW: "des d'ara"
-  SECOND: segon
-  MINUTE: minut
-  HOUR: hora
-  DAY: dia
-  WEEK: setmana
-  MONTH: mes
-  YEAR: any
-  DECADE: dècada
-  SEC: s
-  MIN: min
-  HR: h
-  WK: setm.
-  MO: m.
-  YR: a.
-  DEC: dèc.
-  SECOND_PLURAL: segons
-  MINUTE_PLURAL: minuts
-  HOUR_PLURAL: hores
-  DAY_PLURAL: dies
-  WEEK_PLURAL: setmanes
-  MONTH_PLURAL: mesos
-  YEAR_PLURAL: anys
-  DECADE_PLURAL: dècades
-  SEC_PLURAL: s
-  MIN_PLURAL: min
-  HR_PLURAL: h
-  WK_PLURAL: setm.
-  MO_PLURAL: mesos
-  YR_PLURAL: anys
-  DEC_PLURAL: dèc.
-FORM:
-  VALIDATION_FAIL: '<b>Ha fallat la validació:</b>'
-  INVALID_INPUT: Entrada no vàlida a
-  MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
-MONTHS_OF_THE_YEAR:
-  - Gener
-  - Febrer
-  - Març
-  - Abril
-  - Maig
-  - Juny
-  - Juliol
-  - Agost
-  - Setembre
-  - Octubre
-  - Novembre
-  - Desembre
-DAYS_OF_THE_WEEK:
-  - Dilluns
-  - Dimarts
-  - Dimecres
-  - Dijous
-  - Divendres
-  - Dissabte
-  - Diumenge
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# S'ha produït un error: Frontmatter invàlid\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: No s'ha proporcionat data
+    BAD_DATE: Data invàlida
+    AGO: abans
+    FROM_NOW: des d'ara
+    SECOND: segon
+    MINUTE: minut
+    HOUR: hora
+    DAY: dia
+    WEEK: setmana
+    MONTH: mes
+    YEAR: any
+    DECADE: dècada
+    SEC: s
+    HR: h
+    WK: setm.
+    MO: m.
+    YR: a.
+    DEC: dèc.
+    SECOND_PLURAL: segons
+    MINUTE_PLURAL: minuts
+    HOUR_PLURAL: hores
+    DAY_PLURAL: dies
+    WEEK_PLURAL: setmanes
+    MONTH_PLURAL: mesos
+    YEAR_PLURAL: anys
+    DECADE_PLURAL: dècades
+    SEC_PLURAL: s
+    MIN_PLURAL: min
+    HR_PLURAL: h
+    WK_PLURAL: setm.
+    MO_PLURAL: mesos
+    YR_PLURAL: anys
+    DEC_PLURAL: dèc.
+  FORM:
+    VALIDATION_FAIL: <b>Ha fallat la validació:</b>
+    INVALID_INPUT: Entrada no vàlida a
+    MISSING_REQUIRED_FIELD: 'Falta camp obligatori:'
+  MONTHS_OF_THE_YEAR:
+    - 'Gener'
+    - 'Febrer'
+    - 'Març'
+    - 'Abril'
+    - 'Maig'
+    - 'Juny'
+    - 'Juliol'
+    - 'Agost'
+    - 'Setembre'
+    - 'Octubre'
+    - 'Novembre'
+    - 'Desembre'
+  DAYS_OF_THE_WEEK:
+    - 'Dilluns'
+    - 'Dimarts'
+    - 'Dimecres'
+    - 'Dijous'
+    - 'Divendres'
+    - 'Dissabte'
+    - 'Diumenge'

+ 143 - 80
system/languages/cs.yaml

@@ -1,81 +1,144 @@
 ---
 ---
-INFLECTOR_UNCOUNTABLE:
-  - vybavení
-  - informace
-  - rýže
-  - peníze
-  - druhy
-  - série
-  - ryba
-  - ovce
-INFLECTOR_IRREGULAR:
-  person: lidé
-  man: muži
-  child: děti
-  sex: pohlaví
-  move: pohyby
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Datum nebylo vloženo
-  BAD_DATE: Chybné datum
-  AGO: zpět
-  FROM_NOW: od teď
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: hodina
-  DAY: den
-  WEEK: týden
-  MONTH: měsíc
-  YEAR: rok
-  DECADE: dekáda
-  SEC: sek
-  MIN: min
-  HR: hod
-  WK: t
-  MO: m
-  YR: r
-  DEC: dek
-  SECOND_PLURAL: sekundy
-  MINUTE_PLURAL: minuty
-  HOUR_PLURAL: hodiny
-  DAY_PLURAL: dny
-  WEEK_PLURAL: týdny
-  MONTH_PLURAL: měsíce
-  YEAR_PLURAL: roky
-  DECADE_PLURAL: dekády
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: hod
-  WK_PLURAL: t
-  MO_PLURAL: m
-  YR_PLURAL: r
-  DEC_PLURAL: dek
-FORM:
-  VALIDATION_FAIL: '<b>Ověření se nezdařilo:</b>'
-  INVALID_INPUT: Neplatný vstup v
-  MISSING_REQUIRED_FIELD: 'Chybí požadované pole:'
-MONTHS_OF_THE_YEAR:
-  - ledna
-  - února
-  - března
-  - dubna
-  - května
-  - června
-  - července
-  - srpna
-  - září
-  - října
-  - listopadu
-  - prosince
-DAYS_OF_THE_WEEK:
-  - Pondělí
-  - Úterý
-  - Středa
-  - Čtvrtek
-  - Pátek
-  - Sobota
-  - Neděle
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'vybavení'
+    - 'informace'
+    - 'rýže'
+    - 'peníze'
+    - 'druhy'
+    - 'série'
+    - 'ryba'
+    - 'ovce'
+  INFLECTOR_IRREGULAR:
+    'person': 'lidé'
+    'man': 'muži'
+    'child': 'děti'
+    'sex': 'pohlaví'
+    'move': 'pohyby'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Datum nebylo vloženo
+    BAD_DATE: Chybné datum
+    AGO: zpět
+    FROM_NOW: od teď
+    JUST_NOW: právě teď
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: hodina
+    DAY: den
+    WEEK: týden
+    MONTH: měsíc
+    YEAR: rok
+    DECADE: dekáda
+    SEC: sek
+    MIN: min
+    HR: hod
+    WK: t
+    MO: m
+    YR: r
+    DEC: dek
+    SECOND_PLURAL: sekundy
+    MINUTE_PLURAL: minuty
+    HOUR_PLURAL: hodiny
+    DAY_PLURAL: dny
+    WEEK_PLURAL: týdny
+    MONTH_PLURAL: měsíce
+    YEAR_PLURAL: roky
+    DECADE_PLURAL: dekády
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: hod
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: r
+    DEC_PLURAL: dek
+  FORM:
+    VALIDATION_FAIL: <b>Ověření se nezdařilo:</b>
+    INVALID_INPUT: Neplatný vstup v
+    MISSING_REQUIRED_FIELD: 'Chybí požadované pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'leden'
+    - 'únor'
+    - 'březen'
+    - 'duben'
+    - 'květen'
+    - 'červen'
+    - 'červenec'
+    - 'srpen'
+    - 'září'
+    - 'říjen'
+    - 'listopad'
+    - 'prosinec'
+  DAYS_OF_THE_WEEK:
+    - 'pondělí'
+    - 'úterý'
+    - 'středa'
+    - 'čtvrtek'
+    - 'pátek'
+    - 'sobota'
+    - 'neděle'
+  CRON:
+    EVERY: každý
+    EVERY_HOUR: každou hodinu
+    EVERY_MINUTE: každou minutu
+    EVERY_DAY_OF_WEEK: každý den v týdnu
+    EVERY_DAY_OF_MONTH: každý den v měsíci
+    EVERY_MONTH: každý měsíc
+    TEXT_PERIOD: Every <b />
+    TEXT_MINS: ' at <b /> minute(s) past the hour'
+    TEXT_TIME: ' at <b />:<b />'
+    TEXT_DOW: ' on <b />'
+    TEXT_MONTH: ' of <b />'
+    TEXT_DOM: ' on <b />'
+    ERROR1: Tag %s není podporován!
+    ERROR2: Chybný počet prvků
+    ERROR3: jquery_element musí být nastaven v nastaveních pro jqCron
+    ERROR4: Nerozpoznaný výraz

+ 61 - 74
system/languages/da.yaml

@@ -1,75 +1,62 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Titel: %1$s
-  ---
-  
-  # Fejl: Ugyldigt frontmatter
-  
-  Sti: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Ingen dato angivet
-  BAD_DATE: Ugyldig dato
-  AGO: siden
-  FROM_NOW: fra nu
-  SECOND: sekund
-  MINUTE: minut
-  HOUR: time
-  DAY: dag
-  WEEK: uge
-  MONTH: måned
-  YEAR: år
-  DECADE: årti
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: u
-  MO: md
-  YR: år
-  DEC: årti
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minutter
-  HOUR_PLURAL: timer
-  DAY_PLURAL: dage
-  WEEK_PLURAL: uger
-  MONTH_PLURAL: måneder
-  YEAR_PLURAL: år
-  DECADE_PLURAL: årtier
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: timer
-  WK_PLURAL: uger
-  MO_PLURAL: mdr
-  YR_PLURAL: år
-  DEC_PLURAL: årtier
-FORM:
-  VALIDATION_FAIL: '<b>Validering mislykkedes:</b>'
-  INVALID_INPUT: Ugyldigt input i
-  MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:'
-MONTHS_OF_THE_YEAR:
-  - Januar
-  - Februar
-  - Marts
-  - April
-  - Maj
-  - Juni
-  - Juli
-  - August
-  - September
-  - Oktober
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Mandag
-  - Tirsdag
-  - Onsdag
-  - Torsdag
-  - Fredag
-  - Lørdag
-  - Søndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTitel: %1$s\n---\n\n# Fejl: Ugyldigt frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Ingen dato angivet
+    BAD_DATE: Ugyldig dato
+    AGO: siden
+    FROM_NOW: fra nu
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: time
+    DAY: dag
+    WEEK: uge
+    MONTH: måned
+    YEAR: år
+    DECADE: årti
+    SEC: sek
+    HR: t
+    WK: u
+    MO: md
+    YR: år
+    DEC: årti
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minutter
+    HOUR_PLURAL: timer
+    DAY_PLURAL: dage
+    WEEK_PLURAL: uger
+    MONTH_PLURAL: måneder
+    YEAR_PLURAL: år
+    DECADE_PLURAL: årtier
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: timer
+    WK_PLURAL: uger
+    MO_PLURAL: mdr
+    YR_PLURAL: år
+    DEC_PLURAL: årtier
+  FORM:
+    VALIDATION_FAIL: <b>Validering mislykkedes:</b>
+    INVALID_INPUT: Ugyldigt input i
+    MISSING_REQUIRED_FIELD: 'Mangler obligatorisk felt:'
+  MONTHS_OF_THE_YEAR:
+    - 'januar'
+    - 'februar'
+    - 'mars'
+    - 'april'
+    - 'mai'
+    - 'juni'
+    - 'juli'
+    - 'august'
+    - 'september'
+    - 'oktober'
+    - 'november'
+    - 'desember'
+  DAYS_OF_THE_WEEK:
+    - 'mandag'
+    - 'tirsdag'
+    - 'onsdag'
+    - 'torsdag'
+    - 'fredag'
+    - 'lørdag'
+    - 'søndag'

+ 143 - 88
system/languages/de.yaml

@@ -1,89 +1,144 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  # Fehler: Frontmatter enthält Fehler
-  
-  Pfad: `%2$s`
-  
-  **%3$s ** 
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  1: Informationen
-  2: Reis
-  3: Geld
-INFLECTOR_IRREGULAR:
-  person: Personen
-  man: Menschen
-  child: Kinder
-  sex: Geschlecht
-  move: Züge
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Kein Datum angegeben
-  BAD_DATE: Falsches Datum
-  AGO: her
-  FROM_NOW: ab jetzt
-  SECOND: Sekunde
-  MINUTE: Minute
-  HOUR: Stunde
-  DAY: Tag
-  WEEK: Woche
-  MONTH: Monat
-  YEAR: Jahr
-  DECADE: Jahrzehnt
-  SEC: Sek.
-  MIN: Min.
-  HR: Std.
-  WK: Wo.
-  MO: Mo.
-  YR: J.
-  DEC: Dek.
-  SECOND_PLURAL: Sekunden
-  MINUTE_PLURAL: Minuten
-  HOUR_PLURAL: Stunden
-  DAY_PLURAL: Tage
-  WEEK_PLURAL: Wochen
-  MONTH_PLURAL: Monate
-  YEAR_PLURAL: Jahre
-  DECADE_PLURAL: Jahrzehnte
-  SEC_PLURAL: Sekunden
-  MIN_PLURAL: Minuten
-  HR_PLURAL: Stunden
-  WK_PLURAL: Wochen
-  MO_PLURAL: Monate
-  YR_PLURAL: Jahre
-  DEC_PLURAL: Jahrzehnten
-FORM:
-  VALIDATION_FAIL: '<b>Überprüfung fehlgeschlagen:</b>'
-  INVALID_INPUT: Ungültige Eingabe in
-  MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:'
-MONTHS_OF_THE_YEAR:
-  - Januar
-  - Februar
-  - März
-  - April
-  - Mai
-  - Juni
-  - Juli
-  - August
-  - September
-  - Oktober
-  - November
-  - Dezember
-DAYS_OF_THE_WEEK:
-  - Montag
-  - Dienstag
-  - Mittwoch
-  - Donnerstag
-  - Freitag
-  - Samstag
-  - Sonntag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n# Fehler: Frontmatter enthält Fehler\n\nPfad: `%2$s`\n\n**%3$s ** \n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ice'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1ies'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2ves'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'Ausstattung'
+    - 'Informationen'
+    - 'Reis'
+    - 'Geld'
+    - 'Arten'
+    - 'Serie'
+    - 'Fisch'
+    - 'Schaf'
+  INFLECTOR_IRREGULAR:
+    'person': 'Personen'
+    'man': 'Menschen'
+    'child': 'Kinder'
+    'sex': 'Geschlecht'
+    'move': 'Züge'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Kein Datum angegeben
+    BAD_DATE: Falsches Datum
+    AGO: her
+    FROM_NOW: ab jetzt
+    JUST_NOW: jetzt gerade
+    SECOND: Sekunde
+    MINUTE: Minute
+    HOUR: Stunde
+    DAY: Tag
+    WEEK: Woche
+    MONTH: Monat
+    YEAR: Jahr
+    DECADE: Jahrzehnt
+    SEC: Sek.
+    MIN: Min.
+    HR: Std.
+    WK: Wo.
+    MO: Mo.
+    YR: J.
+    DEC: Dez
+    SECOND_PLURAL: Sekunden
+    MINUTE_PLURAL: Minuten
+    HOUR_PLURAL: Stunden
+    DAY_PLURAL: Tage
+    WEEK_PLURAL: Wochen
+    MONTH_PLURAL: Monate
+    YEAR_PLURAL: Jahre
+    DECADE_PLURAL: Jahrzehnte
+    SEC_PLURAL: Sekunden
+    MIN_PLURAL: Minuten
+    HR_PLURAL: Stunden
+    WK_PLURAL: Wochen
+    MO_PLURAL: Monate
+    YR_PLURAL: Jahre
+    DEC_PLURAL: Jahrzehnten
+  FORM:
+    VALIDATION_FAIL: <b>Überprüfung fehlgeschlagen:</b>
+    INVALID_INPUT: Ungültige Eingabe in
+    MISSING_REQUIRED_FIELD: 'Erforderliches Feld fehlt:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januar'
+    - 'Februar'
+    - 'März'
+    - 'April'
+    - 'Mai'
+    - 'Juni'
+    - 'Juli'
+    - 'August'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'Dezember'
+  DAYS_OF_THE_WEEK:
+    - 'Montag'
+    - 'Dienstag'
+    - 'Mittwoch'
+    - 'Donnerstag'
+    - 'Freitag'
+    - 'Samstag'
+    - 'Sonntag'
+  CRON:
+    EVERY: jede
+    EVERY_HOUR: jede Stunde
+    EVERY_MINUTE: Jede Minute
+    EVERY_DAY_OF_WEEK: jeden Tag der Woche
+    EVERY_DAY_OF_MONTH: jeden Tag des Monats
+    EVERY_MONTH: jeden Monat
+    TEXT_PERIOD: Alle <b />
+    TEXT_MINS: ' bei <b /> Minuten nach der vollen Stunde (n)'
+    TEXT_TIME: ' bei <b />:<b />'
+    TEXT_DOW: ' auf <b />'
+    TEXT_MONTH: ' von <b />'
+    TEXT_DOM: ' auf <b />'
+    ERROR1: Der Tag %s wird nicht unterstützt!
+    ERROR2: Ungültige Anzahl von Elementen
+    ERROR3: jquery_element sollte in den jqCron Einstellungen gesetzt werden
+    ERROR4: Unbekannter Ausdruck

+ 62 - 21
system/languages/el.yaml

@@ -1,22 +1,63 @@
 ---
 ---
-MONTHS_OF_THE_YEAR:
-  - Ιανουάριος
-  - Φεβρουάριος
-  - Μάρτιος
-  - Απρίλιος
-  - Μάιος
-  - Ιούνιος
-  - Ιούλιος
-  - Αύγουστος
-  - Σεπτέμβριος
-  - Οκτώβριος
-  - Νοέμβριος
-  - Δεκέμβριος
-DAYS_OF_THE_WEEK:
-  - Δευτέρα
-  - Τρίτη
-  - Τετάρτη
-  - Πέμπτη
-  - Παρασκευή
-  - Σάββατο
-  - Κυριακή
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nΤίτλος: %1$s\n---\n\n# Σφάλμα: Μη έγκυρη διαδρομή Frontmatter\n\nΔιαδρομή: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Δεν δόθηκε καμία ημερομηνία
+    BAD_DATE: Εσφαλμένη ημερομηνία
+    AGO: πρίν
+    FROM_NOW: από τώρα
+    SECOND: δευτερόλεπτο
+    MINUTE: λεπτό
+    HOUR: ώρα
+    DAY: ημέρα
+    WEEK: εβδομάδα
+    MONTH: μήνας
+    YEAR: έτος
+    DECADE: δεκαετία
+    SEC: δευτερόλεπτο
+    MIN: λεπτό
+    HR: ώρα
+    WK: εβδ
+    MO: μην
+    YR: έτος
+    DEC: δεκαετία
+    SECOND_PLURAL: δευτερόλεπτα
+    MINUTE_PLURAL: λεπτά
+    HOUR_PLURAL: ώρες
+    DAY_PLURAL: ημέρες
+    WEEK_PLURAL: εβδομάδες
+    MONTH_PLURAL: μήνες
+    YEAR_PLURAL: έτη
+    DECADE_PLURAL: δεκαετίες
+    SEC_PLURAL: δευτ.
+    MIN_PLURAL: λεπτά
+    HR_PLURAL: ώρες
+    WK_PLURAL: εβδομάδες
+    MO_PLURAL: μήνες
+    YR_PLURAL: έτη
+    DEC_PLURAL: δεκαετίες
+  FORM:
+    VALIDATION_FAIL: <b>Η επικύρωση απέτυχε:</b>
+    INVALID_INPUT: Μη έγκυρα δεδομένα σε
+    MISSING_REQUIRED_FIELD: 'Λείπει το απαιτούμενο πεδίο:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ιανουάριος'
+    - 'Φεβρουάριος'
+    - 'Μάρτιος'
+    - 'Απρίλιος'
+    - 'Μάιος'
+    - 'Ιούνιος'
+    - 'Ιούλιος'
+    - 'Αύγουστος'
+    - 'Σεπτέμβριος'
+    - 'Οκτώβριος'
+    - 'Νοέμβριος'
+    - 'Δεκέμβριος'
+  DAYS_OF_THE_WEEK:
+    - 'Δευτέρα'
+    - 'Τρίτη'
+    - 'Τετάρτη'
+    - 'Πέμπτη'
+    - 'Παρασκευή'
+    - 'Σάββατο'
+    - 'Κυριακή'

+ 120 - 99
system/languages/en.yaml

@@ -1,99 +1,120 @@
-FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
-INFLECTOR_PLURALS:
-    '/(quiz)$/i': '\1zes'
-    '/^(ox)$/i': '\1en'
-    '/([m|l])ouse$/i': '\1ice'
-    '/(matr|vert|ind)ix|ex$/i': '\1ices'
-    '/(x|ch|ss|sh)$/i': '\1es'
-    '/([^aeiouy]|qu)ies$/i': '\1y'
-    '/([^aeiouy]|qu)y$/i': '\1ies'
-    '/(hive)$/i': '\1s'
-    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
-    '/sis$/i': 'ses'
-    '/([ti])um$/i': '\1a'
-    '/(buffal|tomat)o$/i': '\1oes'
-    '/(bu)s$/i': '\1ses'
-    '/(alias|status)/i': '\1es'
-    '/(octop|vir)us$/i': '\1i'
-    '/(ax|test)is$/i': '\1es'
-    '/s$/i': 's'
-    '/$/': 's'
-INFLECTOR_SINGULAR:
-    '/(quiz)zes$/i': '\1'
-    '/(matr)ices$/i': '\1ix'
-    '/(vert|ind)ices$/i': '\1ex'
-    '/^(ox)en/i': '\1'
-    '/(alias|status)es$/i': '\1'
-    '/([octop|vir])i$/i': '\1us'
-    '/(cris|ax|test)es$/i': '\1is'
-    '/(shoe)s$/i': '\1'
-    '/(o)es$/i': '\1'
-    '/(bus)es$/i': '\1'
-    '/([m|l])ice$/i': '\1ouse'
-    '/(x|ch|ss|sh)es$/i': '\1'
-    '/(m)ovies$/i': '\1ovie'
-    '/(s)eries$/i': '\1eries'
-    '/([^aeiouy]|qu)ies$/i': '\1y'
-    '/([lr])ves$/i': '\1f'
-    '/(tive)s$/i': '\1'
-    '/(hive)s$/i': '\1'
-    '/([^f])ves$/i': '\1fe'
-    '/(^analy)ses$/i': '\1sis'
-    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
-    '/([ti])a$/i': '\1um'
-    '/(n)ews$/i': '\1ews'
-    '/s$/i': ''
-INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
-INFLECTOR_IRREGULAR:
-    'person': 'people'
-    'man': 'men'
-    'child': 'children'
-    'sex': 'sexes'
-    'move': 'moves'
-INFLECTOR_ORDINALS:
-    'default': 'th'
-    'first': 'st'
-    'second': 'nd'
-    'third': 'rd'
-NICETIME:
-    NO_DATE_PROVIDED: No date provided
-    BAD_DATE: Bad date
-    AGO: ago
-    FROM_NOW: from now
-    JUST_NOW: just now
-    SECOND: second
-    MINUTE: minute
-    HOUR: hour
-    DAY: day
-    WEEK: week
-    MONTH: month
-    YEAR: year
-    DECADE: decade
-    SEC: sec
-    MIN: min
-    HR: hr
-    WK: wk
-    MO: mo
-    YR: yr
-    DEC: dec
-    SECOND_PLURAL: seconds
-    MINUTE_PLURAL: minutes
-    HOUR_PLURAL: hours
-    DAY_PLURAL: days
-    WEEK_PLURAL: weeks
-    MONTH_PLURAL: months
-    YEAR_PLURAL: years
-    DECADE_PLURAL: decades
-    SEC_PLURAL: secs
-    MIN_PLURAL: mins
-    HR_PLURAL: hrs
-    WK_PLURAL: wks
-    MO_PLURAL: mos
-    YR_PLURAL: yrs
-    DEC_PLURAL: decs
-FORM:
-    VALIDATION_FAIL: <b>Validation failed:</b>
-    INVALID_INPUT: Invalid input in
-    MISSING_REQUIRED_FIELD: Missing required field:
-MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
-DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+---
+GRAV:
+    FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+    INFLECTOR_PLURALS:
+        '/(quiz)$/i': '\1zes'
+        '/^(ox)$/i': '\1en'
+        '/([m|l])ouse$/i': '\1ice'
+        '/(matr|vert|ind)ix|ex$/i': '\1ices'
+        '/(x|ch|ss|sh)$/i': '\1es'
+        '/([^aeiouy]|qu)ies$/i': '\1y'
+        '/([^aeiouy]|qu)y$/i': '\1ies'
+        '/(hive)$/i': '\1s'
+        '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+        '/sis$/i': 'ses'
+        '/([ti])um$/i': '\1a'
+        '/(buffal|tomat)o$/i': '\1oes'
+        '/(bu)s$/i': '\1ses'
+        '/(alias|status)/i': '\1es'
+        '/(octop|vir)us$/i': '\1i'
+        '/(ax|test)is$/i': '\1es'
+        '/s$/i': 's'
+        '/$/': 's'
+    INFLECTOR_SINGULAR:
+        '/(quiz)zes$/i': '\1'
+        '/(matr)ices$/i': '\1ix'
+        '/(vert|ind)ices$/i': '\1ex'
+        '/^(ox)en/i': '\1'
+        '/(alias|status)es$/i': '\1'
+        '/([octop|vir])i$/i': '\1us'
+        '/(cris|ax|test)es$/i': '\1is'
+        '/(shoe)s$/i': '\1'
+        '/(o)es$/i': '\1'
+        '/(bus)es$/i': '\1'
+        '/([m|l])ice$/i': '\1ouse'
+        '/(x|ch|ss|sh)es$/i': '\1'
+        '/(m)ovies$/i': '\1ovie'
+        '/(s)eries$/i': '\1eries'
+        '/([^aeiouy]|qu)ies$/i': '\1y'
+        '/([lr])ves$/i': '\1f'
+        '/(tive)s$/i': '\1'
+        '/(hive)s$/i': '\1'
+        '/([^f])ves$/i': '\1fe'
+        '/(^analy)ses$/i': '\1sis'
+        '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+        '/([ti])a$/i': '\1um'
+        '/(n)ews$/i': '\1ews'
+        '/s$/i': ''
+    INFLECTOR_UNCOUNTABLE: ['equipment', 'information', 'rice', 'money', 'species', 'series', 'fish', 'sheep']
+    INFLECTOR_IRREGULAR:
+        'person': 'people'
+        'man': 'men'
+        'child': 'children'
+        'sex': 'sexes'
+        'move': 'moves'
+    INFLECTOR_ORDINALS:
+        'default': 'th'
+        'first': 'st'
+        'second': 'nd'
+        'third': 'rd'
+    NICETIME:
+        NO_DATE_PROVIDED: No date provided
+        BAD_DATE: Bad date
+        AGO: ago
+        FROM_NOW: from now
+        JUST_NOW: just now
+        SECOND: second
+        MINUTE: minute
+        HOUR: hour
+        DAY: day
+        WEEK: week
+        MONTH: month
+        YEAR: year
+        DECADE: decade
+        SEC: sec
+        MIN: min
+        HR: hr
+        WK: wk
+        MO: mo
+        YR: yr
+        DEC: dec
+        SECOND_PLURAL: seconds
+        MINUTE_PLURAL: minutes
+        HOUR_PLURAL: hours
+        DAY_PLURAL: days
+        WEEK_PLURAL: weeks
+        MONTH_PLURAL: months
+        YEAR_PLURAL: years
+        DECADE_PLURAL: decades
+        SEC_PLURAL: secs
+        MIN_PLURAL: mins
+        HR_PLURAL: hrs
+        WK_PLURAL: wks
+        MO_PLURAL: mos
+        YR_PLURAL: yrs
+        DEC_PLURAL: decs
+    FORM:
+        VALIDATION_FAIL: <b>Validation failed:</b>
+        INVALID_INPUT: Invalid input in
+        MISSING_REQUIRED_FIELD: Missing required field:
+    MONTHS_OF_THE_YEAR: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
+    DAYS_OF_THE_WEEK: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+    YES: "Yes"
+    NO: "No"
+    CRON:
+        EVERY: every
+        EVERY_HOUR: every hour
+        EVERY_MINUTE: every minute
+        EVERY_DAY_OF_WEEK: every day of the week
+        EVERY_DAY_OF_MONTH: every day of the month
+        EVERY_MONTH: every month
+        TEXT_PERIOD: Every <b />
+        TEXT_MINS: ' at <b /> minute(s) past the hour'
+        TEXT_TIME: ' at <b />:<b />'
+        TEXT_DOW: ' on <b />'
+        TEXT_MONTH: ' of <b />'
+        TEXT_DOM: ' on <b />'
+        ERROR1: The tag %s is not supported!
+        ERROR2: Bad number of elements
+        ERROR3: The jquery_element should be set into jqCron settings
+        ERROR4: Unrecognized expression

+ 94 - 89
system/languages/es.yaml

@@ -1,90 +1,95 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Frontmatter Inválido
-  
-  Ruta: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  0: equipo
-  1: información
-  3: dinero
-  5: series
-  6: pescado
-  7: oveja
-INFLECTOR_IRREGULAR:
-  man: hombres
-  child: niños
-  sex: sexos
-INFLECTOR_ORDINALS:
-  first: ro
-  second: do
-  third: ro
-NICETIME:
-  NO_DATE_PROVIDED: No se proporcionó fecha
-  BAD_DATE: Fecha erronea
-  AGO: antes
-  FROM_NOW: desde ahora
-  SECOND: segundo
-  MINUTE: minuto
-  HOUR: hora
-  DAY: día
-  WEEK: semana
-  MONTH: mes
-  YEAR: año
-  DECADE: década
-  SEC: seg
-  MIN: min
-  HR: h
-  WK: sem
-  MO: mes
-  YR: año
-  DEC: dec
-  SECOND_PLURAL: segundos
-  MINUTE_PLURAL: minutos
-  HOUR_PLURAL: horas
-  DAY_PLURAL: días
-  WEEK_PLURAL: semanas
-  MONTH_PLURAL: meses
-  YEAR_PLURAL: años
-  DECADE_PLURAL: décadas
-  SEC_PLURAL: segs
-  MIN_PLURAL: mins
-  HR_PLURAL: hs
-  WK_PLURAL: sem
-  MO_PLURAL: mes
-  YR_PLURAL: años
-  DEC_PLURAL: décadas
-FORM:
-  VALIDATION_FAIL: '<b>Falló la validación. </b>'
-  INVALID_INPUT: 'Dato inválido en: '
-  MISSING_REQUIRED_FIELD: 'Falta el campo requerido: '
-MONTHS_OF_THE_YEAR:
-  - Enero
-  - Febrero
-  - Marzo
-  - Abril
-  - Mayo
-  - Junio
-  - Julio
-  - Agosto
-  - Septiembre
-  - Octubre
-  - Noviembre
-  - Diciembre
-DAYS_OF_THE_WEEK:
-  - Lunes
-  - Martes
-  - Miércoles
-  - Jueves
-  - Viernes
-  - Sábado
-  - Domingo
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntítulo: %1$s\n---\n\n# Error: Frontmatter no válido\n\nRuta: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipo'
+    - 'información'
+    - 'rice'
+    - 'dinero'
+    - 'species'
+    - 'series'
+    - 'pescado'
+    - 'oveja'
+  INFLECTOR_IRREGULAR:
+    'man': 'hombres'
+    'child': 'niños'
+    'sex': 'sexos'
+  INFLECTOR_ORDINALS:
+    'first': 'ro'
+    'second': 'do'
+    'third': 'ro'
+  NICETIME:
+    NO_DATE_PROVIDED: No se proporcionó fecha
+    BAD_DATE: Fecha errónea
+    AGO: antes
+    FROM_NOW: desde ahora
+    JUST_NOW: hace un momento
+    SECOND: segundo
+    MINUTE: minuto
+    HOUR: hora
+    DAY: día
+    WEEK: semana
+    MONTH: mes
+    YEAR: año
+    DECADE: década
+    SEC: seg
+    HR: h
+    WK: sem
+    MO: mes
+    YR: año
+    SECOND_PLURAL: segundos
+    MINUTE_PLURAL: minutos
+    HOUR_PLURAL: horas
+    DAY_PLURAL: días
+    WEEK_PLURAL: semanas
+    MONTH_PLURAL: meses
+    YEAR_PLURAL: años
+    DECADE_PLURAL: décadas
+    SEC_PLURAL: segs
+    HR_PLURAL: hs
+    WK_PLURAL: sem
+    MO_PLURAL: mes
+    YR_PLURAL: años
+    DEC_PLURAL: décadas
+  FORM:
+    VALIDATION_FAIL: '<b>Falló la validación: </b>'
+    INVALID_INPUT: 'Dato inválido en: '
+    MISSING_REQUIRED_FIELD: 'Falta el campo requerido: '
+  MONTHS_OF_THE_YEAR:
+    - 'Enero'
+    - 'Febrero'
+    - 'Marzo'
+    - 'Abril'
+    - 'Mayo'
+    - 'Junio'
+    - 'Julio'
+    - 'Agosto'
+    - 'Septiembre'
+    - 'Octubre'
+    - 'Noviembre'
+    - 'Diciembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lunes'
+    - 'Martes'
+    - 'Miércoles'
+    - 'Jueves'
+    - 'Viernes'
+    - 'Sábado'
+    - 'Domingo'
+  CRON:
+    EVERY: cada
+    EVERY_HOUR: cada hora
+    EVERY_MINUTE: cada minuto
+    EVERY_DAY_OF_WEEK: cada día de la semana
+    EVERY_DAY_OF_MONTH: cada día del mes
+    EVERY_MONTH: cada mes
+    TEXT_PERIOD: Cada <b />
+    TEXT_MINS: ' a <b /> minuto(s) despues de la hora'
+    TEXT_TIME: ' a <b />:<b />'
+    TEXT_DOW: ' en <b />'
+    TEXT_MONTH: ' de<b />'
+    TEXT_DOM: ' en<b />'
+    ERROR1: La etiqueta %s no está soportada!
+    ERROR2: El número de elementos es erroneo
+    ERROR3: El jquery_element debería establecerse en la configuración del jqCron
+    ERROR4: Expresión no reconocida

+ 85 - 0
system/languages/et.yaml

@@ -0,0 +1,85 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\npealkiri: %1$s\n---\n\n# Viga: vigane Frontmatter'i\n\nasukoht: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'informatsioon'
+    - 'rice'
+    - 'money'
+    - 'species'
+    - 'series'
+    - 'kala'
+    - 'lammas'
+  INFLECTOR_IRREGULAR:
+    'person': 'inimesed'
+    'man': 'mees'
+    'child': 'lapsed'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Kuupäev määramata
+    BAD_DATE: Vigane kuupäev
+    AGO: tagasi
+    FROM_NOW: praegusest
+    JUST_NOW: just nüüd
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: tundi
+    DAY: päev
+    WEEK: nädal
+    MONTH: kuu
+    YEAR: aasta
+    DECADE: 10 aastat
+    SEC: sek
+    HR: t
+    WK: näd
+    MO: k.
+    YR: a.
+    DEC: dekaad
+    SECOND_PLURAL: sekundit
+    MINUTE_PLURAL: minutit
+    HOUR_PLURAL: tundi
+    DAY_PLURAL: päeva
+    WEEK_PLURAL: nädalat
+    MONTH_PLURAL: kuud
+    YEAR_PLURAL: aastat
+    DECADE_PLURAL: dekaadi
+    SEC_PLURAL: sekundit
+    MIN_PLURAL: min
+    HR_PLURAL: t
+    WK_PLURAL: näd
+    MO_PLURAL: kuud
+    YR_PLURAL: aastat
+    DEC_PLURAL: dek.
+  FORM:
+    VALIDATION_FAIL: <b>Kinnitamine nurjus:</b>
+    INVALID_INPUT: 'Vigane sisend:'
+    MISSING_REQUIRED_FIELD: 'Nõutud väli puudub:'
+  MONTHS_OF_THE_YEAR:
+    - 'jaanuar'
+    - 'veebruar'
+    - 'märts'
+    - 'aprill'
+    - 'mai'
+    - 'juuni'
+    - 'juuli'
+    - 'august'
+    - 'september'
+    - 'oktoober'
+    - 'november'
+    - 'detsember'
+  DAYS_OF_THE_WEEK:
+    - 'esmaspäev'
+    - 'teisipäev'
+    - 'kolmapäev'
+    - 'neljapäev'
+    - 'reede'
+    - 'laupäev'
+    - 'pühapäev'
+  CRON:
+    EVERY: iga
+    EVERY_MONTH: iga kuu
+    TEXT_PERIOD: Iga <b />

+ 62 - 0
system/languages/eu.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "--- title: %1$s --- # Errorea: Baliogabeko Frontmatter Bidea: `%2$s` **%3$s** ``` %4$s ```"
+  NICETIME:
+    NO_DATE_PROVIDED: Ez da datarik ezarri
+    BAD_DATE: Okerreko data
+    AGO: ' duela'
+    FROM_NOW: oraindik aurrera
+    SECOND: segundo
+    MINUTE: minutu
+    HOUR: ordua
+    DAY: egun
+    WEEK: astea
+    MONTH: hilabetea
+    YEAR: urtea
+    DECADE: hamarkada
+    SEC: seg
+    HR: h
+    WK: ast
+    MO: hil
+    YR: urt
+    DEC: ham
+    SECOND_PLURAL: segundo
+    MINUTE_PLURAL: minutu
+    HOUR_PLURAL: ordu
+    DAY_PLURAL: egun
+    WEEK_PLURAL: aste
+    MONTH_PLURAL: hilabete
+    YEAR_PLURAL: urte
+    DECADE_PLURAL: hamarkada
+    SEC_PLURAL: segundo
+    MIN_PLURAL: minutu
+    HR_PLURAL: h
+    WK_PLURAL: ast
+    MO_PLURAL: hil
+    YR_PLURAL: urt
+    DEC_PLURAL: ham
+  FORM:
+    VALIDATION_FAIL: <b>Balidazioak huts egin du</b>
+    INVALID_INPUT: Baliogabeko sarrera
+    MISSING_REQUIRED_FIELD: 'Derrigorrezko eremua bete gabe:'
+  MONTHS_OF_THE_YEAR:
+    - 'Urtarrila'
+    - 'Otsaila'
+    - 'Martxoa'
+    - 'Apirila'
+    - 'Maiatza'
+    - 'Ekaina'
+    - 'Uztaila'
+    - 'Abuztua'
+    - 'Iraila'
+    - 'Urria'
+    - 'Azaroa'
+    - 'Abendua'
+  DAYS_OF_THE_WEEK:
+    - 'Astelehena'
+    - 'Asteartea'
+    - 'Azteazkena'
+    - 'Osteguna'
+    - 'Ostirala'
+    - 'Larunbata'
+    - 'Igandea'

+ 62 - 0
system/languages/fa.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nعنوان: %1$s\n---\n\n# خطا: Frontmatter غلط\n\nمسیر: %2$s\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: تاریخی ارائه نشده
+    BAD_DATE: تاریخ اشتباه
+    AGO: قبل
+    FROM_NOW: از حالا
+    SECOND: ثانیه
+    MINUTE: دقیقه
+    HOUR: ساعت
+    DAY: روز
+    WEEK: هفته
+    MONTH: ماه
+    YEAR: سال
+    DECADE: دهه
+    SEC: ثانیه
+    MIN: دقیقه
+    HR: ساعت
+    WK: هفته
+    MO: ماه
+    YR: سال
+    DEC: دهه
+    SECOND_PLURAL: ثانیه
+    MINUTE_PLURAL: دقیقه
+    HOUR_PLURAL: ساعت
+    DAY_PLURAL: روز
+    WEEK_PLURAL: هفته
+    MONTH_PLURAL: ماه
+    YEAR_PLURAL: سال
+    DECADE_PLURAL: دهه
+    SEC_PLURAL: ثانیه
+    MIN_PLURAL: دقیقه
+    HR_PLURAL: ساعت
+    WK_PLURAL: هفته
+    YR_PLURAL: سال
+    DEC_PLURAL: دهه
+  FORM:
+    VALIDATION_FAIL: <b>سنجش اعتبار ناموفق بود</b>
+    INVALID_INPUT: ورودی نامعتبر در
+    MISSING_REQUIRED_FIELD: 'قسمت ضروری جا افتاده:'
+  MONTHS_OF_THE_YEAR:
+    - 'ژانویه'
+    - 'فوریه'
+    - 'مارس'
+    - 'آوریل'
+    - 'می'
+    - 'ژوئن'
+    - 'ژوئیه'
+    - 'اوت'
+    - 'سپتامبر'
+    - 'اکتبر'
+    - 'نوامبر'
+    - 'دسامبر'
+  DAYS_OF_THE_WEEK:
+    - 'دوشنبه'
+    - 'سه‌ شنبه'
+    - 'چهارشنبه'
+    - 'پنج شنبه'
+    - 'جمعه'
+    - 'شنبه'
+    - 'یک‌شنبه'

+ 133 - 59
system/languages/fi.yaml

@@ -1,60 +1,134 @@
 ---
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Päivämäärää ei annettu
-  BAD_DATE: Virheellinen päivämäärä
-  AGO: sitten
-  FROM_NOW: tästä lähtien
-  SECOND: sekunti
-  MINUTE: minuutti
-  HOUR: tunti
-  DAY: päivä
-  WEEK: viikko
-  MONTH: kuukausi
-  YEAR: vuosi
-  DECADE: vuosikymmen
-  SEC: sek
-  MIN: min
-  HR: h
-  WK: vk
-  MO: kk
-  YR: v
-  DEC: vuosikymmen
-  SECOND_PLURAL: sekuntia
-  MINUTE_PLURAL: minuuttia
-  HOUR_PLURAL: tuntia
-  DAY_PLURAL: päivää
-  WEEK_PLURAL: viikkoa
-  MONTH_PLURAL: kuukautta
-  YEAR_PLURAL: vuotta
-  DECADE_PLURAL: vuosikymmentä
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: h
-  WK_PLURAL: v
-  MO_PLURAL: kk
-  YR_PLURAL: v
-  DEC_PLURAL: vuosikymmentä
-FORM:
-  VALIDATION_FAIL: '<b>Vahvistus epäonnistui:</b>'
-  MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:'
-MONTHS_OF_THE_YEAR:
-  - Tammikuu
-  - Helmikuu
-  - Maaliskuu
-  - Huhtikuu
-  - Toukokuu
-  - Kesäkuuta
-  - Heinäkuu
-  - Elokuu
-  - Syyskuu
-  - Lokakuu
-  - Marraskuu
-  - Joulukuu
-DAYS_OF_THE_WEEK:
-  - Maanantai
-  - Tiistai
-  - Keskiviikko
-  - Torstai
-  - Perjantai
-  - Lauantai
-  - Sunnuntai
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\notsikko: %1$s\n---\n\n# Virhe: Virheellinen Frontmatter\n\nPolku: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'information'
+    - 'riisi'
+    - 'raha'
+    - 'lajit'
+    - 'series'
+    - 'kala'
+    - 'lammas'
+  INFLECTOR_IRREGULAR:
+    'person': 'ihmiset'
+    'man': 'miehet'
+    'child': 'lapset'
+    'sex': 'sukupuoli'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Päivämäärää ei annettu
+    BAD_DATE: Virheellinen päivämäärä
+    AGO: sitten
+    FROM_NOW: tästä lähtien
+    JUST_NOW: juuri nyt
+    SECOND: sekuntti
+    MINUTE: minuutti
+    HOUR: tunti
+    DAY: päivä
+    WEEK: viikko
+    MONTH: kuukausi
+    YEAR: vuosi
+    DECADE: vuosikymmen
+    SEC: sek
+    MIN: min
+    HR: h
+    WK: vk
+    MO: kk
+    YR: v
+    DEC: vuosikymmen
+    SECOND_PLURAL: sekuntia
+    MINUTE_PLURAL: minuuttia
+    HOUR_PLURAL: tuntia
+    DAY_PLURAL: päivää
+    WEEK_PLURAL: viikkoa
+    MONTH_PLURAL: kuukautta
+    YEAR_PLURAL: vuotta
+    DECADE_PLURAL: vuosikymmentä
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: h
+    WK_PLURAL: v
+    MO_PLURAL: kk
+    YR_PLURAL: v
+    DEC_PLURAL: vuosikymmentä
+  FORM:
+    VALIDATION_FAIL: <b>Vahvistus epäonnistui:</b>
+    INVALID_INPUT: Syöte ei kelpaa
+    MISSING_REQUIRED_FIELD: 'Puuttuva pakollinen kenttä:'
+  MONTHS_OF_THE_YEAR:
+    - 'Tammikuu'
+    - 'Helmikuu'
+    - 'Maaliskuu'
+    - 'Huhtikuu'
+    - 'Toukokuu'
+    - 'Kesäkuuta'
+    - 'Heinäkuu'
+    - 'Elokuu'
+    - 'Syyskuu'
+    - 'Lokakuu'
+    - 'Marraskuu'
+    - 'Joulukuu'
+  DAYS_OF_THE_WEEK:
+    - 'Maanantai'
+    - 'Tiistai'
+    - 'Keskiviikko'
+    - 'Torstai'
+    - 'Perjantai'
+    - 'Lauantai'
+    - 'Sunnuntai'
+  CRON:
+    EVERY: joka
+    EVERY_HOUR: joka tunti
+    EVERY_MINUTE: joka minuutti
+    EVERY_DAY_OF_WEEK: viikon jokaisena päivänä
+    EVERY_DAY_OF_MONTH: kuukauden jokaisena päivänä
+    EVERY_MONTH: joka kuukausi
+    TEXT_PERIOD: Joka <b />

+ 122 - 137
system/languages/fr.yaml

@@ -1,138 +1,123 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Erreur : Frontmatter invalide
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-  /(bu)s$/i: '\1ses'
-  /(alias|status)/i: '\1es'
-  /(octop|vir)us$/i: '\1i'
-  /(ax|test)is$/i: '\1es'
-  /s$/i: s
-  /$/: s
-INFLECTOR_SINGULAR:
-  /(quiz)zes$/i: '\1'
-  /(matr)ices$/i: '\1ix'
-  /(vert|ind)ices$/i: '\1ex'
-  /^(ox)en/i: '\1'
-  /(alias|status)es$/i: '\1'
-  "/([octop|vir])i$/i": '\1us'
-  /(cris|ax|test)es$/i: '\1is'
-  /(shoe)s$/i: '\1'
-  /(o)es$/i: '\1'
-  /(bus)es$/i: '\1'
-  "/([m|l])ice$/i": '\1ouse'
-  /(x|ch|ss|sh)es$/i: '\1'
-  /(m)ovies$/i: '\1ovie'
-  /(s)eries$/i: '\1eries'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([lr])ves$/i": '\1f'
-  /(tive)s$/i: '\1'
-  /(hive)s$/i: '\1'
-  "/([^f])ves$/i": '\1fe'
-  /(^analy)ses$/i: '\1sis'
-  /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i: '\1\2sis'
-  "/([ti])a$/i": '\1um'
-  /(n)ews$/i: '\1ews'
-INFLECTOR_UNCOUNTABLE:
-  - équipement
-  - informations
-  - riz
-  - argent
-  - espèces
-  - séries
-  - poisson
-  - mouton
-INFLECTOR_IRREGULAR:
-  person: personnes
-  man: hommes
-  child: enfants
-  sex: sexes
-  move: déplacements
-INFLECTOR_ORDINALS:
-  default: ème
-  first: er
-  second: ème
-  third: ème
-NICETIME:
-  NO_DATE_PROVIDED: Aucune date fournie
-  BAD_DATE: Date erronée
-  AGO: plus tôt
-  FROM_NOW: à partir de maintenant
-  SECOND: seconde
-  MINUTE: minute
-  HOUR: heure
-  DAY: jour
-  WEEK: semaine
-  MONTH: mois
-  YEAR: année
-  DECADE: décennie
-  SEC: s
-  MIN: m
-  HR: h
-  WK: sem
-  MO: m
-  YR: an
-  DEC: déc
-  SECOND_PLURAL: secondes
-  MINUTE_PLURAL: minutes
-  HOUR_PLURAL: heures
-  DAY_PLURAL: jours
-  WEEK_PLURAL: semaines
-  MONTH_PLURAL: mois
-  YEAR_PLURAL: années
-  DECADE_PLURAL: décennies
-  SEC_PLURAL: s
-  MIN_PLURAL: m
-  HR_PLURAL: h
-  WK_PLURAL: sem
-  MO_PLURAL: mois
-  YR_PLURAL: a
-  DEC_PLURAL: décs
-FORM:
-  VALIDATION_FAIL: '<b>La validation a échoué :</b>'
-  INVALID_INPUT: Saisie non valide
-  MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
-MONTHS_OF_THE_YEAR:
-  - Janvier
-  - Février
-  - Mars
-  - Avril
-  - Mai
-  - Juin
-  - Juillet
-  - Août
-  - Septembre
-  - Octobre
-  - Novembre
-  - Décembre
-DAYS_OF_THE_WEEK:
-  - Lundi
-  - Mardi
-  - Mercredi
-  - Jeudi
-  - Vendredi
-  - Samedi
-  - Dimanche
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitre: %1$s\n---\n\n# Erreur : Frontmatter invalide\n\nChemin: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1es'
+    '/(bu)s$/i': 'Bus'
+    '/(alias|status)/i': 'alias|status'
+    '/(ax|test)is$/i': '\1s'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+  INFLECTOR_UNCOUNTABLE:
+    - 'équipement'
+    - 'information'
+    - 'riz'
+    - 'argent'
+    - 'espèces'
+    - 'séries'
+    - 'poisson'
+    - 'mouton'
+  INFLECTOR_IRREGULAR:
+    'person': 'personnes'
+    'man': 'hommes'
+    'child': 'enfants'
+    'sex': 'sexes'
+    'move': 'déplacements'
+  INFLECTOR_ORDINALS:
+    'default': 'ème'
+    'first': 'er'
+    'second': 'ème'
+    'third': 'ème'
+  NICETIME:
+    NO_DATE_PROVIDED: Aucune date fournie
+    BAD_DATE: Date erronée
+    AGO: plus tôt
+    FROM_NOW: à partir de maintenant
+    JUST_NOW: à l'instant
+    SECOND: seconde
+    MINUTE: minute
+    HOUR: heure
+    DAY: jour
+    WEEK: semaine
+    MONTH: mois
+    YEAR: année
+    DECADE: décennie
+    SEC: s
+    MIN: m
+    HR: h
+    WK: sem
+    MO: m
+    YR: an
+    DEC: déc
+    SECOND_PLURAL: secondes
+    MINUTE_PLURAL: minutes
+    HOUR_PLURAL: heures
+    DAY_PLURAL: jours
+    WEEK_PLURAL: semaines
+    MONTH_PLURAL: mois
+    YEAR_PLURAL: années
+    DECADE_PLURAL: décennies
+    SEC_PLURAL: s
+    MIN_PLURAL: m
+    HR_PLURAL: h
+    WK_PLURAL: sem
+    MO_PLURAL: mois
+    YR_PLURAL: a
+    DEC_PLURAL: décs
+  FORM:
+    VALIDATION_FAIL: <b>La validation a échoué :</b>
+    INVALID_INPUT: Saisie non valide
+    MISSING_REQUIRED_FIELD: 'Champ obligatoire manquant :'
+  MONTHS_OF_THE_YEAR:
+    - 'Janvier'
+    - 'Février'
+    - 'Mars'
+    - 'Avril'
+    - 'Mai'
+    - 'Juin'
+    - 'Juillet'
+    - 'Août'
+    - 'Septembre'
+    - 'Octobre'
+    - 'Novembre'
+    - 'Décembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lundi'
+    - 'Mardi'
+    - 'Mercredi'
+    - 'Jeudi'
+    - 'Vendredi'
+    - 'Samedi'
+    - 'Dimanche'
+  CRON:
+    EVERY: chaque
+    EVERY_HOUR: toutes les heures
+    EVERY_MINUTE: chaque minute
+    EVERY_DAY_OF_WEEK: tous les jours de la semaine
+    EVERY_DAY_OF_MONTH: tous les jours du mois
+    EVERY_MONTH: chaque mois
+    TEXT_PERIOD: Chaque<b/>
+    TEXT_MINS: ' à <b /> minute(s) après l''heure'
+    TEXT_TIME: ' à<b/>:<b/>'
+    TEXT_DOW: ' sur <b/>'
+    TEXT_MONTH: ' de <b />'
+    TEXT_DOM: ' sur <b/>'
+    ERROR1: La balise %s n'est pas supportée!
+    ERROR2: Nombre invalide d'éléments
+    ERROR3: L'élément jquery_element doit être défini dans les paramètres jqCron
+    ERROR4: Expression non reconnue

+ 63 - 0
system/languages/he.yaml

@@ -0,0 +1,63 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nכותרת: %1$s\n---\n# שגיאה: Fronmatter לא חוקי\nנתיב: `%2$s`\n**%3$s**\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: לא סופק תאריך
+    BAD_DATE: תאריך פגום
+    AGO: לפני
+    FROM_NOW: כרגע
+    SECOND: שנייה
+    MINUTE: דקה
+    HOUR: שעה
+    DAY: יום
+    WEEK: שבוע
+    MONTH: חודש
+    YEAR: שנה
+    DECADE: עשור
+    SEC: שנ'
+    MIN: דק'
+    HR: ש'
+    WK: שב'
+    MO: חו'
+    YR: שני'
+    DEC: עש'
+    SECOND_PLURAL: שניות
+    MINUTE_PLURAL: דקות
+    HOUR_PLURAL: שעות
+    DAY_PLURAL: ימים
+    WEEK_PLURAL: שבועות
+    MONTH_PLURAL: חודשים
+    YEAR_PLURAL: שנים
+    DECADE_PLURAL: עשורים
+    SEC_PLURAL: שנ'
+    MIN_PLURAL: דק'
+    HR_PLURAL: ש'
+    WK_PLURAL: שב'
+    MO_PLURAL: חו'
+    YR_PLURAL: שני'
+    DEC_PLURAL: עש'
+  FORM:
+    VALIDATION_FAIL: <b>האימות נכשל:</b>
+    INVALID_INPUT: קלט לא חוקי
+    MISSING_REQUIRED_FIELD: 'שדות חובה חסרים:'
+  MONTHS_OF_THE_YEAR:
+    - 'ינואר'
+    - 'פברואר'
+    - 'מרץ'
+    - 'אפריל'
+    - 'מאי'
+    - 'יוני'
+    - 'יולי'
+    - 'אוגוסט'
+    - 'ספטמבר'
+    - 'אוקטובר'
+    - 'נובמבר'
+    - 'דצמבר'
+  DAYS_OF_THE_WEEK:
+    - 'שני'
+    - 'שלישי'
+    - 'רביעי'
+    - 'חמישי'
+    - 'שישי'
+    - 'שבת'
+    - 'ראשון'

+ 75 - 74
system/languages/hr.yaml

@@ -1,75 +1,76 @@
 ---
 ---
-INFLECTOR_UNCOUNTABLE:
-  - oprema
-  - informacije
-  - riža
-  - novac
-  - vrsta
-  - serija
-  - riba
-  - ovca
-INFLECTOR_IRREGULAR:
-  person: osobe
-  man: ljudi
-  child: djeca
-  sex: spolovi
-  move: Pomakni
-NICETIME:
-  NO_DATE_PROVIDED: Datum nije upisan
-  BAD_DATE: Pogrešan datum
-  AGO: prije
-  FROM_NOW: od sada
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: sat
-  DAY: dan
-  WEEK: tjedan
-  MONTH: mjesec
-  YEAR: godina
-  DECADE: desetljeće
-  SEC: sek
-  HR: sat
-  WK: t
-  MO: m
-  YR: g
-  DEC: des
-  SECOND_PLURAL: sekundi
-  MINUTE_PLURAL: minuta
-  HOUR_PLURAL: sati
-  DAY_PLURAL: dan
-  WEEK_PLURAL: tjedana
-  MONTH_PLURAL: mjeseci
-  YEAR_PLURAL: godina
-  DECADE_PLURAL: desetljeća
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: sat
-  WK_PLURAL: t
-  MO_PLURAL: m
-  YR_PLURAL: g
-  DEC_PLURAL: des
-FORM:
-  VALIDATION_FAIL: '<b>Validacija nije uspjela:</b>'
-  INVALID_INPUT: Pogrešan unos u
-  MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:'
-MONTHS_OF_THE_YEAR:
-  - Siječanj
-  - Veljača
-  - Ožujak
-  - Travanj
-  - Svibanj
-  - Lipanj
-  - Srpanj
-  - Kolovoz
-  - Rujan
-  - Listopad
-  - Studeni
-  - Prosinac
-DAYS_OF_THE_WEEK:
-  - Ponedjeljak
-  - Utorak
-  - Srijeda
-  - Četvrtak
-  - Petak
-  - Subota
-  - Nedjelja
+GRAV:
+  INFLECTOR_UNCOUNTABLE:
+    - 'oprema'
+    - 'informacije'
+    - 'riža'
+    - 'novac'
+    - 'vrsta'
+    - 'serija'
+    - 'riba'
+    - 'ovca'
+  INFLECTOR_IRREGULAR:
+    'person': 'osobe'
+    'man': 'ljudi'
+    'child': 'djeca'
+    'sex': 'spolovi'
+    'move': 'Pomakni'
+  NICETIME:
+    NO_DATE_PROVIDED: Datum nije upisan
+    BAD_DATE: Pogrešan datum
+    AGO: prije
+    FROM_NOW: od sada
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: sat
+    DAY: dan
+    WEEK: tjedan
+    MONTH: mjesec
+    YEAR: godina
+    DECADE: desetljeće
+    SEC: sek
+    HR: sat
+    WK: t
+    MO: m
+    YR: g
+    DEC: des
+    SECOND_PLURAL: sekundi
+    MINUTE_PLURAL: minuta
+    HOUR_PLURAL: sati
+    DAY_PLURAL: dan
+    WEEK_PLURAL: tjedana
+    MONTH_PLURAL: mjeseci
+    YEAR_PLURAL: godina
+    DECADE_PLURAL: desetljeća
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: sat
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: g
+    DEC_PLURAL: des
+  FORM:
+    VALIDATION_FAIL: <b>Validacija nije uspjela:</b>
+    INVALID_INPUT: Pogrešan unos u
+    MISSING_REQUIRED_FIELD: 'Nedostaje obavezno polje:'
+  MONTHS_OF_THE_YEAR:
+    - 'Siječanj'
+    - 'Veljača'
+    - 'Ožujak'
+    - 'Travanj'
+    - 'Svibanj'
+    - 'Lipanj'
+    - 'Srpanj'
+    - 'Kolovoz'
+    - 'Rujan'
+    - 'Listopad'
+    - 'Studeni'
+    - 'Prosinac'
+  DAYS_OF_THE_WEEK:
+    - 'Ponedjeljak'
+    - 'Utorak'
+    - 'Srijeda'
+    - 'Četvrtak'
+    - 'Petak'
+    - 'Subota'
+    - 'Nedjelja'

+ 96 - 137
system/languages/hu.yaml

@@ -1,138 +1,97 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  cím: %1$s
-  ---
-  
-  # Hiba: Érvénytelen Frontmatter
-  
-  Elérési út: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-  /(bu)s$/i: '\1ses'
-  /(alias|status)/i: '\1es'
-  /(octop|vir)us$/i: '\1i'
-  /(ax|test)is$/i: '\1es'
-  /s$/i: s
-  /$/: s
-INFLECTOR_SINGULAR:
-  /(quiz)zes$/i: '\1'
-  /(matr)ices$/i: '\1ix'
-  /(vert|ind)ices$/i: '\1ex'
-  /^(ox)en/i: '\1'
-  /(alias|status)es$/i: '\1'
-  "/([octop|vir])i$/i": '\1us'
-  /(cris|ax|test)es$/i: '\1is'
-  /(shoe)s$/i: '\1'
-  /(o)es$/i: '\1'
-  /(bus)es$/i: '\1'
-  "/([m|l])ice$/i": '\1ouse'
-  /(x|ch|ss|sh)es$/i: '\1'
-  /(m)ovies$/i: '\1ovie'
-  /(s)eries$/i: '\1eries'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([lr])ves$/i": '\1f'
-  /(tive)s$/i: '\1'
-  /(hive)s$/i: '\1'
-  "/([^f])ves$/i": '\1fe'
-  /(^analy)ses$/i: '\1sis'
-  /((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i: '\1\2sis'
-  "/([ti])a$/i": '\1um'
-  /(n)ews$/i: '\1ews'
-INFLECTOR_UNCOUNTABLE:
-  - felszerelés
-  - információ
-  - rizs
-  - pénz
-  - fajok
-  - sorozat
-  - hal
-  - juh
-INFLECTOR_IRREGULAR:
-  person: személyek
-  man: férfiak
-  child: gyerekek
-  sex: nemek
-  move: lépések
-INFLECTOR_ORDINALS:
-  default: '.'
-  first: '.'
-  second: '.'
-  third: '.'
-NICETIME:
-  NO_DATE_PROVIDED: Nincs dátum megadva
-  BAD_DATE: Hibás dátum
-  AGO: elteltével
-  FROM_NOW: mostantól
-  SECOND: másodperc
-  MINUTE: perc
-  HOUR: óra
-  DAY: nap
-  WEEK: hét
-  MONTH: hónap
-  YEAR: év
-  DECADE: évtized
-  SEC: mp
-  MIN: p
-  HR: ó
-  WK: hét
-  MO: hó
-  YR: év
-  DEC: évt
-  SECOND_PLURAL: másodperc
-  MINUTE_PLURAL: perc
-  HOUR_PLURAL: óra
-  DAY_PLURAL: nap
-  WEEK_PLURAL: hét
-  MONTH_PLURAL: hónap
-  YEAR_PLURAL: év
-  DECADE_PLURAL: évtized
-  SEC_PLURAL: mp
-  MIN_PLURAL: perc
-  HR_PLURAL: ó
-  WK_PLURAL: hét
-  MO_PLURAL: hó
-  YR_PLURAL: év
-  DEC_PLURAL: évt
-FORM:
-  VALIDATION_FAIL: '<b>A validáció hibát talált:</b>'
-  INVALID_INPUT: 'Az itt megadott érték érvénytelen:'
-  MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:'
-MONTHS_OF_THE_YEAR:
-  - január
-  - február
-  - március
-  - április
-  - május
-  - június
-  - július
-  - augusztus
-  - szeptember
-  - október
-  - november
-  - december
-DAYS_OF_THE_WEEK:
-  - hétfő
-  - kedd
-  - szerda
-  - csütörtök
-  - péntek
-  - szombat
-  - vasárnap
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ncím: %1$s\n---\n\n# Hiba: Érvénytelen Frontmatter\n\nElérési út: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'felszerelés'
+    - 'információ'
+    - 'rizs'
+    - 'pénz'
+    - 'fajok'
+    - 'sorozat'
+    - 'hal'
+    - 'juh'
+  INFLECTOR_IRREGULAR:
+    'person': 'személyek'
+    'man': 'férfiak'
+    'child': 'gyerekek'
+    'sex': 'nemek'
+    'move': 'lépések'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Nincs dátum megadva
+    BAD_DATE: Hibás dátum
+    AGO: elteltével
+    FROM_NOW: mostantól
+    JUST_NOW: épp most
+    SECOND: másodperc
+    MINUTE: perc
+    HOUR: óra
+    DAY: nap
+    WEEK: hét
+    MONTH: hónap
+    YEAR: év
+    DECADE: évtized
+    SEC: mp
+    MIN: p
+    HR: ó
+    WK: hét
+    MO: hó
+    YR: év
+    DEC: évt
+    SECOND_PLURAL: másodperc
+    MINUTE_PLURAL: perc
+    HOUR_PLURAL: óra
+    DAY_PLURAL: nap
+    WEEK_PLURAL: hét
+    MONTH_PLURAL: hónap
+    YEAR_PLURAL: év
+    DECADE_PLURAL: évtized
+    SEC_PLURAL: mp
+    MIN_PLURAL: perc
+    HR_PLURAL: ó
+    WK_PLURAL: hét
+    MO_PLURAL: hó
+    YR_PLURAL: év
+    DEC_PLURAL: évt
+  FORM:
+    VALIDATION_FAIL: <b>Érvényesítés nem sikerült:</b>
+    INVALID_INPUT: 'A megadott érték érvénytelen:'
+    MISSING_REQUIRED_FIELD: 'Ez a kötelező mező nincs kitöltve:'
+  MONTHS_OF_THE_YEAR:
+    - 'január'
+    - 'február'
+    - 'március'
+    - 'április'
+    - 'május'
+    - 'június'
+    - 'július'
+    - 'augusztus'
+    - 'szeptember'
+    - 'október'
+    - 'november'
+    - 'december'
+  DAYS_OF_THE_WEEK:
+    - 'hétfő'
+    - 'kedd'
+    - 'szerda'
+    - 'csütörtök'
+    - 'péntek'
+    - 'szombat'
+    - 'vasárnap'
+  CRON:
+    EVERY: minden
+    EVERY_HOUR: óránként
+    EVERY_MINUTE: percenként
+    EVERY_DAY_OF_WEEK: a hét minden napján
+    EVERY_DAY_OF_MONTH: a hónap minden napján
+    EVERY_MONTH: minden hónapban
+    TEXT_PERIOD: Minden <b />
+    TEXT_MINS: '<b /> perccel az óra elteltével'
+    ERROR1: A %s címke nem engedélyezett!
+    ERROR2: Hibás elemszám
+    ERROR3: A jquery_element-et a jqCron beállítsokban kell meghatározni
+    ERROR4: Ismeretlen kifejezés

+ 95 - 0
system/languages/id.yaml

@@ -0,0 +1,95 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Frontmatter tidak valid\n\nLokasi: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'peralatan'
+    - 'informasi'
+    - 'nasi'
+    - 'uang'
+    - 'spesies'
+    - 'rangkaian'
+    - 'ikan'
+    - 'domba'
+  INFLECTOR_IRREGULAR:
+    'person': 'orang-orang'
+    'man': 'laki-laki'
+    'child': 'anak-anak'
+    'sex': 'jenis kelamin'
+    'move': 'pindahkan'
+  NICETIME:
+    NO_DATE_PROVIDED: Tanggal tidak tersedia
+    BAD_DATE: Format tanggal salah
+    AGO: yang lalu
+    FROM_NOW: dari saat ini
+    JUST_NOW: baru saja
+    SECOND: detik
+    MINUTE: menit
+    HOUR: jam
+    DAY: hari
+    WEEK: pekan
+    MONTH: bulan
+    YEAR: tahun
+    DECADE: dekade
+    SEC: dtk
+    MIN: mnt
+    HR: j
+    WK: mng
+    MO: bln
+    YR: thn
+    DEC: desimal
+    SECOND_PLURAL: detik
+    MINUTE_PLURAL: menit
+    HOUR_PLURAL: jam
+    DAY_PLURAL: hari
+    WEEK_PLURAL: pekan
+    MONTH_PLURAL: bulan
+    YEAR_PLURAL: tahun
+    DECADE_PLURAL: dekade
+    SEC_PLURAL: dtk
+    MIN_PLURAL: mnt
+    HR_PLURAL: j
+    WK_PLURAL: mgg
+    MO_PLURAL: bln
+    YR_PLURAL: thn
+    DEC_PLURAL: dekade
+  FORM:
+    VALIDATION_FAIL: <b>Validasi gagal:</b>
+    INVALID_INPUT: Input tidak valid di
+    MISSING_REQUIRED_FIELD: 'Data yang diperlukan belum terisi:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Maret'
+    - 'April'
+    - 'Mei'
+    - 'Juni'
+    - 'Juli'
+    - 'Agustus'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'Desember'
+  DAYS_OF_THE_WEEK:
+    - 'Senin'
+    - 'Selasa'
+    - 'Rabu'
+    - 'Kamis'
+    - 'Jumat'
+    - 'Sabtu'
+    - 'Minggu'
+  CRON:
+    EVERY: Setiap
+    EVERY_HOUR: Setiap jam
+    EVERY_MINUTE: Setiap menit
+    EVERY_DAY_OF_WEEK: Setiap hari selama seminggu
+    EVERY_DAY_OF_MONTH: pada tanggal setiap bulannya
+    EVERY_MONTH: setiap bulan
+    TEXT_PERIOD: Setiap <b />
+    TEXT_TIME: ' pada <b />:<b />'
+    TEXT_DOW: ' pada <b />'
+    TEXT_MONTH: ' pada <b />'
+    TEXT_DOM: ' pada <b />'
+    ERROR1: Tag %s tidak didukung!
+    ERROR2: Jumlah elemen tidak valid
+    ERROR3: jquery_element harus ditetapkan ke pengaturan jqCron
+    ERROR4: Ekspresi tidak dikenali

+ 80 - 0
system/languages/is.yaml

@@ -0,0 +1,80 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitill: %1$s\n---\n\n# Villa: Ógilt efni á forsíðu\n\nSlóð: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'upplýsingar'
+    - 'rice'
+    - 'money'
+    - 'species'
+    - 'series'
+    - 'fish'
+    - 'sheep'
+  NICETIME:
+    NO_DATE_PROVIDED: Engin dagsetning gefin
+    BAD_DATE: Röng dagsetning
+    AGO: síðan
+    JUST_NOW: í þessu
+    SECOND: sekúndu
+    MINUTE: mínútu
+    HOUR: klukkustund
+    DAY: degi
+    WEEK: viku
+    MONTH: mánuði
+    YEAR: ári
+    DECADE: áratug
+    SEC: sek
+    MIN: mín
+    HR: klst
+    WK: vk
+    MO: mán
+    YR: ár
+    DEC: árat
+    SECOND_PLURAL: sekúndum
+    MINUTE_PLURAL: mínútum
+    HOUR_PLURAL: klukkustundum
+    DAY_PLURAL: dögum
+    WEEK_PLURAL: vikum
+    MONTH_PLURAL: mánuðum
+    YEAR_PLURAL: árum
+    DECADE_PLURAL: áratugum
+    SEC_PLURAL: sek
+    MIN_PLURAL: mín
+    HR_PLURAL: klst
+    WK_PLURAL: vik
+    MO_PLURAL: mán
+    YR_PLURAL: árum
+    DEC_PLURAL: árat
+  FORM:
+    VALIDATION_FAIL: <b>Sannvottun mistókst:</b>
+    INVALID_INPUT: Ógilt inntak í
+    MISSING_REQUIRED_FIELD: 'Vantar nauðsynlegan reit:'
+  MONTHS_OF_THE_YEAR:
+    - 'janúar'
+    - 'Febrúar'
+    - 'Mars'
+    - 'Apríl'
+    - 'Maí'
+    - 'Júní'
+    - 'Júlí'
+    - 'Ágúst'
+    - 'September'
+    - 'Október'
+    - 'Nóvember'
+    - 'Desember'
+  DAYS_OF_THE_WEEK:
+    - 'Mánudagur'
+    - 'Þriðjudagur'
+    - 'Miðvikudagur'
+    - 'Fimmtudagur'
+    - 'Föstudagur'
+    - 'Laugardagur'
+    - 'Sunnudagur'
+  CRON:
+    TEXT_TIME: ' á <b />:<b />'
+    TEXT_DOW: ' á <b />'
+    TEXT_MONTH: ' af <b />'
+    TEXT_DOM: ' á <b />'
+    ERROR1: Merkið %s er ekki stutt!
+    ERROR3: Það ætti að setja jquery_element inn í stillingar jqCron
+    ERROR4: Óþekkt segð

+ 100 - 61
system/languages/it.yaml

@@ -1,62 +1,101 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '"
-NICETIME:
-  NO_DATE_PROVIDED: Nessuna data fornita
-  BAD_DATE: Data non valida
-  AGO: fa
-  FROM_NOW: da adesso
-  SECOND: secondo
-  MINUTE: minuto
-  HOUR: ora
-  DAY: giorno
-  WEEK: settimana
-  MONTH: mese
-  YEAR: anno
-  DECADE: decennio
-  SEC: sec
-  MIN: min
-  HR: ora
-  WK: settimana
-  MO: mese
-  YR: anno
-  DEC: decennio
-  SECOND_PLURAL: secondi
-  MINUTE_PLURAL: minuti
-  HOUR_PLURAL: ore
-  DAY_PLURAL: giorni
-  WEEK_PLURAL: settimane
-  MONTH_PLURAL: mesi
-  YEAR_PLURAL: anni
-  DECADE_PLURAL: decadi
-  SEC_PLURAL: secondi
-  MIN_PLURAL: minuti
-  HR_PLURAL: ore
-  WK_PLURAL: settimane
-  MO_PLURAL: mesi
-  YR_PLURAL: anni
-  DEC_PLURAL: decenni
-FORM:
-  VALIDATION_FAIL: '<b>Validazione fallita:</b>'
-  INVALID_INPUT: Input non valido in
-  MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:'
-MONTHS_OF_THE_YEAR:
-  - Gennaio
-  - Febbraio
-  - Marzo
-  - Aprile
-  - Maggio
-  - Giugno
-  - Luglio
-  - Agosto
-  - Settembre
-  - Ottobre
-  - Novembre
-  - Dicembre
-DAYS_OF_THE_WEEK:
-  - Lunedì
-  - Martedì
-  - Mercoledì
-  - Giovedì
-  - Venerdì
-  - Sabato
-  - Domenica
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---Titolo: %1$s---# Errore: Frontmatter non valido: '%2$s' * *%3$s * * ' '%4$s ' '"
+  INFLECTOR_UNCOUNTABLE:
+    - 'dotazione'
+    - 'informazione'
+    - 'riso'
+    - 'denaro'
+    - 'specie'
+    - 'serie'
+    - 'pesce'
+    - 'pecora'
+  INFLECTOR_IRREGULAR:
+    'person': 'persone'
+    'man': 'uomini'
+    'child': 'bambino'
+    'sex': 'sessi'
+    'move': 'sposta'
+  INFLECTOR_ORDINALS:
+    'default': '°'
+    'first': '°'
+    'second': 'o'
+    'third': 'o'
+  NICETIME:
+    NO_DATE_PROVIDED: Nessuna data fornita
+    BAD_DATE: Data non valida
+    AGO: fa
+    FROM_NOW: da adesso
+    JUST_NOW: ora
+    SECOND: secondo
+    MINUTE: minuto
+    HOUR: ora
+    DAY: giorno
+    WEEK: settimana
+    MONTH: mese
+    YEAR: anno
+    DECADE: decennio
+    SEC: sec
+    MIN: min
+    HR: ora
+    WK: settimana
+    MO: mese
+    YR: anno
+    DEC: decennio
+    SECOND_PLURAL: secondi
+    MINUTE_PLURAL: minuti
+    HOUR_PLURAL: ore
+    DAY_PLURAL: giorni
+    WEEK_PLURAL: settimane
+    MONTH_PLURAL: mesi
+    YEAR_PLURAL: anni
+    DECADE_PLURAL: decadi
+    SEC_PLURAL: secondi
+    MIN_PLURAL: minuti
+    HR_PLURAL: ore
+    WK_PLURAL: settimane
+    MO_PLURAL: mesi
+    YR_PLURAL: anni
+    DEC_PLURAL: decenni
+  FORM:
+    VALIDATION_FAIL: <b>Validazione fallita:</b>
+    INVALID_INPUT: Input non valido in
+    MISSING_REQUIRED_FIELD: 'Campo richiesto mancante:'
+  MONTHS_OF_THE_YEAR:
+    - 'Gennaio'
+    - 'Febbraio'
+    - 'Marzo'
+    - 'Aprile'
+    - 'Maggio'
+    - 'Giugno'
+    - 'Luglio'
+    - 'Agosto'
+    - 'Settembre'
+    - 'Ottobre'
+    - 'Novembre'
+    - 'Dicembre'
+  DAYS_OF_THE_WEEK:
+    - 'Lunedì'
+    - 'Martedì'
+    - 'Mercoledì'
+    - 'Giovedì'
+    - 'Venerdì'
+    - 'Sabato'
+    - 'Domenica'
+  CRON:
+    EVERY: ogni
+    EVERY_HOUR: ogni ora
+    EVERY_MINUTE: ogni minuto
+    EVERY_DAY_OF_WEEK: ogni giorno della settimana
+    EVERY_DAY_OF_MONTH: ogni giorno del mese
+    EVERY_MONTH: ogni mese
+    TEXT_PERIOD: Ogni <b />
+    TEXT_MINS: ' a <b /> minuto(i) dall''inizio dell''ora'
+    TEXT_TIME: ' alle <b />:<b />'
+    TEXT_DOW: ' su <b />'
+    TEXT_MONTH: ' di <b />'
+    TEXT_DOM: ' di <b />'
+    ERROR1: Il tag %s non è supportato!
+    ERROR2: Numero di elementi non valido
+    ERROR3: Il jquery_element deve essere impostato nelle impostazioni di jqCron
+    ERROR4: Espressione non riconosciuta

+ 27 - 18
system/languages/ja.yaml

@@ -1,24 +1,15 @@
-FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
-INFLECTOR_PLURALS: []
-
-INFLECTOR_SINGULAR: []
-
-INFLECTOR_UNCOUNTABLE: []
-
-INFLECTOR_IRREGULAR:
+---
+GRAV:
+  INFLECTOR_IRREGULAR:
     'person': 'みんな'
     'person': 'みんな'
     'man': '人'
     'man': '人'
     'child': '子供'
     'child': '子供'
     'sex': '性別'
     'sex': '性別'
     'move': '移動'
     'move': '移動'
-    
-INFLECTOR_ORDINALS: []
-
-NICETIME:
+  NICETIME:
     NO_DATE_PROVIDED: 日付が設定されていません
     NO_DATE_PROVIDED: 日付が設定されていません
     BAD_DATE: 不正な日付
     BAD_DATE: 不正な日付
     AGO: 前
     AGO: 前
-    FROM_NOW: from now
     SECOND: 秒
     SECOND: 秒
     MINUTE: 分
     MINUTE: 分
     HOUR: 時
     HOUR: 時
@@ -33,7 +24,6 @@ NICETIME:
     WK: 週
     WK: 週
     MO: 月
     MO: 月
     YR: 年
     YR: 年
-    DEC: dec
     SECOND_PLURAL: 秒
     SECOND_PLURAL: 秒
     MINUTE_PLURAL: 分
     MINUTE_PLURAL: 分
     HOUR_PLURAL: 時
     HOUR_PLURAL: 時
@@ -49,9 +39,28 @@ NICETIME:
     MO_PLURAL: 月
     MO_PLURAL: 月
     YR_PLURAL: 年
     YR_PLURAL: 年
     DEC_PLURAL: 10年
     DEC_PLURAL: 10年
-FORM:
+  FORM:
     VALIDATION_FAIL: <b>バリデーション失敗 :</b>
     VALIDATION_FAIL: <b>バリデーション失敗 :</b>
     INVALID_INPUT: 不正な入力:
     INVALID_INPUT: 不正な入力:
-    MISSING_REQUIRED_FIELD: 必須項目が入力されていません:
-MONTHS_OF_THE_YEAR: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
-DAYS_OF_THE_WEEK: ['月', '火', '水', '木', '金', '土', '日']
+    MISSING_REQUIRED_FIELD: '必須項目が入力されていません:'
+  MONTHS_OF_THE_YEAR:
+    - '1月'
+    - '2月'
+    - '3月'
+    - '4月'
+    - '5月'
+    - '6月'
+    - '7月'
+    - '8月'
+    - '9月'
+    - '10月'
+    - '11月'
+    - '12月'
+  DAYS_OF_THE_WEEK:
+    - '月'
+    - '火'
+    - '水'
+    - '木'
+    - '金'
+    - '土'
+    - '日'

+ 63 - 0
system/languages/ko.yaml

@@ -0,0 +1,63 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 오류: 무효의 Frontmatter\n\n경로: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: 제공된 날짜가 없습니다
+    BAD_DATE: 잘못된 날짜
+    AGO: 전
+    FROM_NOW: 후
+    SECOND: 초
+    MINUTE: 분
+    HOUR: 시간
+    DAY: 일
+    WEEK: 주
+    MONTH: 개월
+    YEAR: 년
+    DECADE: 년간
+    SEC: 초
+    MIN: 분
+    HR: 시간
+    WK: 주
+    MO: 개월
+    YR: 년
+    DEC: 년간
+    SECOND_PLURAL: 초
+    MINUTE_PLURAL: 분
+    HOUR_PLURAL: 시간
+    DAY_PLURAL: 일
+    WEEK_PLURAL: 주
+    MONTH_PLURAL: 개월
+    YEAR_PLURAL: 년
+    DECADE_PLURAL: 년간
+    SEC_PLURAL: 초
+    MIN_PLURAL: 분
+    HR_PLURAL: 시간
+    WK_PLURAL: 주
+    MO_PLURAL: 개월
+    YR_PLURAL: 년
+    DEC_PLURAL: 년간
+  FORM:
+    VALIDATION_FAIL: <b>유효성 검사 실패:</b>
+    INVALID_INPUT: 잘못된 입력
+    MISSING_REQUIRED_FIELD: '누락 된 필수 필드:'
+  MONTHS_OF_THE_YEAR:
+    - '일월'
+    - '이월'
+    - '삼월'
+    - '사월'
+    - '오월'
+    - '유월'
+    - '칠월'
+    - '팔월'
+    - '구월'
+    - '시월'
+    - '십일월'
+    - '십이월'
+  DAYS_OF_THE_WEEK:
+    - '월요일'
+    - '화요일'
+    - '수요일'
+    - '목요일'
+    - '금요일'
+    - '토요일'
+    - '일요일'

+ 77 - 68
system/languages/lt.yaml

@@ -1,69 +1,78 @@
 ---
 ---
-INFLECTOR_UNCOUNTABLE:
-  2: ryžiai
-  3: pinigai
-  4: prieskoniai
-  5: serijos
-  6: žuvis
-  7: avis
-INFLECTOR_IRREGULAR:
-  person: žmonės
-  man: žmogus
-  child: vaikai
-  sex: lytys
-  move: juda
-NICETIME:
-  NO_DATE_PROVIDED: Nenurodyta data
-  BAD_DATE: Neteisinga data
-  AGO: prieš
-  FROM_NOW: nuo dabar
-  SECOND: sekundė
-  MINUTE: minutė
-  HOUR: valanda
-  DAY: diena
-  WEEK: savaitė
-  MONTH: mėnuo
-  YEAR: metai
-  DECADE: dešimtmetis
-  SEC: sek
-  MIN: min
-  HR: val
-  WK: sav
-  MO: mėn
-  YR: m
-  MINUTE_PLURAL: minutės
-  HOUR_PLURAL: valandos
-  DAY_PLURAL: dienos
-  WEEK_PLURAL: savaitės
-  MONTH_PLURAL: mėnesiai
-  YEAR_PLURAL: metai
-  DECADE_PLURAL: dešimtmečiai
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: val
-  WK_PLURAL: sav
-  MO_PLURAL: mėn
-  YR_PLURAL: m
-FORM:
-  MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:'
-MONTHS_OF_THE_YEAR:
-  - Sausis
-  - Vasaris
-  - Kovas
-  - Balandis
-  - Gegužė
-  - Birželis
-  - Liepa
-  - Rugpjūtis
-  - Rugsėjis
-  - Spalis
-  - Lakpritis
-  - Gruodis
-DAYS_OF_THE_WEEK:
-  - Pirmadienis
-  - Antradienis
-  - Trečiadienis
-  - Ketvirtadienis
-  - Penktadienis
-  - Šeštadienis
-  - Sekmadienis
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Klaida: klaidinga įžanginė konfigūracija\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n %4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'information'
+    - 'ryžiai'
+    - 'pinigai'
+    - 'prieskoniai'
+    - 'serijos'
+    - 'žuvis'
+    - 'avis'
+  INFLECTOR_IRREGULAR:
+    'person': 'žmonės'
+    'man': 'žmogus'
+    'child': 'vaikai'
+    'sex': 'lytys'
+    'move': 'juda'
+  NICETIME:
+    NO_DATE_PROVIDED: Nenurodyta data
+    BAD_DATE: Neteisinga data
+    AGO: prieš
+    FROM_NOW: nuo dabar
+    SECOND: sekundė
+    MINUTE: minutė
+    HOUR: valanda
+    DAY: diena
+    WEEK: savaitė
+    MONTH: mėnuo
+    YEAR: metai
+    DECADE: dešimtmetis
+    SEC: sek.
+    MIN: min.
+    HR: val.
+    WK: sav.
+    MO: mėn.
+    YR: m.
+    DEC: dešimtmetis
+    SECOND_PLURAL: sekundės
+    MINUTE_PLURAL: minutės
+    HOUR_PLURAL: valandos
+    DAY_PLURAL: dienos
+    WEEK_PLURAL: savaitės
+    MONTH_PLURAL: mėnesiai
+    YEAR_PLURAL: metai
+    DECADE_PLURAL: dešimtmečiai
+    SEC_PLURAL: sek.
+    MIN_PLURAL: min.
+    HR_PLURAL: val.
+    WK_PLURAL: sav.
+    MO_PLURAL: mėn.
+    YR_PLURAL: m.
+    DEC_PLURAL: dešimtmečiai
+  FORM:
+    VALIDATION_FAIL: <b>Patvirtinimas nepavyko:</b>
+    INVALID_INPUT: Neteisingai įvesta į
+    MISSING_REQUIRED_FIELD: 'Būtina užpildyti laukelį:'
+  MONTHS_OF_THE_YEAR:
+    - 'Sausis'
+    - 'Vasaris'
+    - 'Kovas'
+    - 'Balandis'
+    - 'Gegužė'
+    - 'Birželis'
+    - 'Liepa'
+    - 'Rugpjūtis'
+    - 'Rugsėjis'
+    - 'Spalis'
+    - 'Lakpritis'
+    - 'Gruodis'
+  DAYS_OF_THE_WEEK:
+    - 'Pirmadienis'
+    - 'Antradienis'
+    - 'Trečiadienis'
+    - 'Ketvirtadienis'
+    - 'Penktadienis'
+    - 'Šeštadienis'
+    - 'Sekmadienis'

+ 4 - 2
system/languages/nb.yaml

@@ -1,2 +1,4 @@
-MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']
-DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag']
+---
+GRAV:
+  MONTHS_OF_THE_YEAR: ['januar', 'februar', 'mars', 'april', 'mai', 'juni', 'juli', 'august', 'september', 'oktober', 'november', 'desember']
+  DAYS_OF_THE_WEEK: ['mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag', 'søndag']

+ 143 - 63
system/languages/nl.yaml

@@ -1,64 +1,144 @@
 ---
 ---
-INFLECTOR_IRREGULAR:
-  person: personen
-  man: mensen
-  child: kinderen
-  sex: geslacht
-  move: verplaatsen
-NICETIME:
-  NO_DATE_PROVIDED: geen datum opgegeven
-  BAD_DATE: Datumformaat onjuist
-  AGO: geleden
-  FROM_NOW: vanaf nu
-  SECOND: seconde
-  MINUTE: minuut
-  HOUR: uur
-  DAY: dag
-  WEEK: week
-  MONTH: maand
-  YEAR: jaar
-  DECADE: decenium
-  SEC: s
-  MIN: min
-  HR: u
-  MO: ma
-  YR: j
-  SECOND_PLURAL: seconden
-  MINUTE_PLURAL: minuten
-  HOUR_PLURAL: uren
-  DAY_PLURAL: dagen
-  WEEK_PLURAL: weken
-  MONTH_PLURAL: maanden
-  YEAR_PLURAL: jaren
-  DECADE_PLURAL: decennia
-  SEC_PLURAL: seconden
-  MIN_PLURAL: minuten
-  HR_PLURAL: uren
-  WK_PLURAL: weken
-  MO_PLURAL: maanden
-  YR_PLURAL: jaren
-FORM:
-  VALIDATION_FAIL: '<b>Validatie mislukt:</b>'
-  INVALID_INPUT: Ongeldige invoer in
-  MISSING_REQUIRED_FIELD: 'Verplicht veld ontbreekt:'
-MONTHS_OF_THE_YEAR:
-  - Januari
-  - Februari
-  - Maart
-  - april
-  - Mei
-  - Juni
-  - Juli
-  - Augustus
-  - september
-  - Oktober
-  - november
-  - december
-DAYS_OF_THE_WEEK:
-  - Maandag
-  - Dinsdag
-  - Woensdag
-  - Donderdag
-  - Vrijdag
-  - Zaterdag
-  - Zondag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitel: %1$s\n---\n\n# Fout: ongeldige frontmatter\n\nPad: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'uitrusting'
+    - 'informatie'
+    - 'rijst'
+    - 'geld'
+    - 'soorten'
+    - 'reeks'
+    - 'vis'
+    - 'schaap'
+  INFLECTOR_IRREGULAR:
+    'person': 'personen'
+    'man': 'mensen'
+    'child': 'kinderen'
+    'sex': 'geslacht'
+    'move': 'verplaatsen'
+  INFLECTOR_ORDINALS:
+    'default': 'th'
+    'first': 'st'
+    'second': 'nd'
+    'third': 'rd'
+  NICETIME:
+    NO_DATE_PROVIDED: geen datum opgegeven
+    BAD_DATE: Datumformaat onjuist
+    AGO: geleden
+    FROM_NOW: vanaf nu
+    JUST_NOW: zojuist
+    SECOND: seconde
+    MINUTE: minuut
+    HOUR: uur
+    DAY: dag
+    WEEK: week
+    MONTH: maand
+    YEAR: jaar
+    DECADE: decennium
+    SEC: s
+    MIN: min
+    HR: u
+    WK: week
+    MO: ma
+    YR: j
+    DEC: decennia
+    SECOND_PLURAL: seconden
+    MINUTE_PLURAL: minuten
+    HOUR_PLURAL: uren
+    DAY_PLURAL: dagen
+    WEEK_PLURAL: weken
+    MONTH_PLURAL: maanden
+    YEAR_PLURAL: jaren
+    DECADE_PLURAL: decennia
+    SEC_PLURAL: seconden
+    MIN_PLURAL: minuten
+    HR_PLURAL: uren
+    WK_PLURAL: weken
+    MO_PLURAL: maanden
+    YR_PLURAL: jaren
+    DEC_PLURAL: decennia
+  FORM:
+    VALIDATION_FAIL: <b>Validatie mislukt:</b>
+    INVALID_INPUT: Ongeldige invoer in
+    MISSING_REQUIRED_FIELD: 'Ontbrekend verplicht veld:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Maart'
+    - 'April'
+    - 'Mei'
+    - 'Juni'
+    - 'Juli'
+    - 'Augustus'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Maandag'
+    - 'Dinsdag'
+    - 'Woensdag'
+    - 'Donderdag'
+    - 'Vrijdag'
+    - 'Zaterdag'
+    - 'Zondag'
+  CRON:
+    EVERY: elke
+    EVERY_HOUR: elk uur
+    EVERY_MINUTE: elke minuut
+    EVERY_DAY_OF_WEEK: elke dag van de week
+    EVERY_DAY_OF_MONTH: elke dag van de maand
+    EVERY_MONTH: elke maand
+    TEXT_PERIOD: Elke <b />
+    TEXT_MINS: ' <b /> minuten te laat'
+    TEXT_TIME: ' op <b />:<b />'
+    TEXT_DOW: ' op <b />'
+    TEXT_MONTH: ' van <b />'
+    TEXT_DOM: ' op <b />'
+    ERROR1: De tag %s wordt niet ondersteund!
+    ERROR2: Slecht aantal elementen
+    ERROR3: Het jquery_element moet ingesteld worden in de jqCron instellingen
+    ERROR4: Onbekende expressie

+ 76 - 92
system/languages/no.yaml

@@ -1,93 +1,77 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Tittel: %1$s
-  ---
-  
-  # Feilmelding: Ugyldig Frontmatter
-  
-  Pane: '%2$s'
-  
-  **%3$s **
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-INFLECTOR_UNCOUNTABLE:
-  - utstyr
-  - informasjon
-  - ris
-  - penger
-  - arter
-  - serier
-  - fisk
-  - sau
-INFLECTOR_IRREGULAR:
-  person: folk
-  man: menn
-  child: barn
-  sex: kjønn
-  move: trekk
-NICETIME:
-  NO_DATE_PROVIDED: Ingen dato gitt
-  BAD_DATE: Dårlig dato
-  AGO: siden
-  FROM_NOW: fra nå
-  SECOND: sekund
-  MINUTE: minutt
-  HOUR: time
-  DAY: dag
-  WEEK: uke
-  MONTH: måned
-  YEAR: år
-  DECADE: tiår
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: uke
-  MO: må
-  YR: år
-  DEC: des
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minutter
-  HOUR_PLURAL: timer
-  DAY_PLURAL: dager
-  WEEK_PLURAL: uker
-  MONTH_PLURAL: måneder
-  YEAR_PLURAL: år
-  DECADE_PLURAL: tiår
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: timer
-  WK_PLURAL: uker
-  MO_PLURAL: mdr
-  YR_PLURAL: år
-  DEC_PLURAL: årtier
-FORM:
-  VALIDATION_FAIL: '<b>Validering mislyktes:</b>'
-  INVALID_INPUT: Ugyldig innhold i
-  MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:'
-MONTHS_OF_THE_YEAR:
-  - januar
-  - februar
-  - mars
-  - april
-  - mai
-  - juni
-  - juli
-  - august
-  - september
-  - oktober
-  - november
-  - desember
-DAYS_OF_THE_WEEK:
-  - mandag
-  - tirsdag
-  - onsdag
-  - torsdag
-  - fredag
-  - lørdag
-  - søndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTittel: %1$s\n---\n\n# Feilmelding: Ugyldig Frontmatter\n\nSti: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'utstyr'
+    - 'informasjon'
+    - 'ris'
+    - 'penger'
+    - 'arter'
+    - 'serier'
+    - 'fisk'
+    - 'sau'
+  INFLECTOR_IRREGULAR:
+    'person': 'folk'
+    'man': 'menn'
+    'child': 'barn'
+    'sex': 'kjønn'
+    'move': 'trekk'
+  NICETIME:
+    NO_DATE_PROVIDED: Ingen dato gitt
+    BAD_DATE: Ugyldig dato
+    AGO: siden
+    FROM_NOW: fra nå
+    SECOND: sekund
+    MINUTE: minutt
+    HOUR: time
+    DAY: dag
+    WEEK: uke
+    MONTH: måned
+    YEAR: år
+    DECADE: tiår
+    SEC: sek
+    HR: t
+    WK: uke
+    MO: må
+    YR: år
+    DEC: tiår
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minutter
+    HOUR_PLURAL: timer
+    DAY_PLURAL: dager
+    WEEK_PLURAL: uker
+    MONTH_PLURAL: måneder
+    YEAR_PLURAL: år
+    DECADE_PLURAL: tiår
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: timer
+    WK_PLURAL: uker
+    MO_PLURAL: md
+    YR_PLURAL: år
+    DEC_PLURAL: årtier
+  FORM:
+    VALIDATION_FAIL: <b>Godkjenning mislyktes:</b>
+    INVALID_INPUT: Ugyldig innhold i
+    MISSING_REQUIRED_FIELD: 'Mangler påkrevd felt:'
+  MONTHS_OF_THE_YEAR:
+    - 'januar'
+    - 'februar'
+    - 'mars'
+    - 'april'
+    - 'mai'
+    - 'juni'
+    - 'juli'
+    - 'august'
+    - 'september'
+    - 'oktober'
+    - 'november'
+    - 'desember'
+  DAYS_OF_THE_WEEK:
+    - 'mandag'
+    - 'tirsdag'
+    - 'onsdag'
+    - 'torsdag'
+    - 'fredag'
+    - 'lørdag'
+    - 'søndag'

+ 61 - 74
system/languages/pl.yaml

@@ -1,75 +1,62 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Nieprawidłowy Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Nie podano daty
-  BAD_DATE: Zła data
-  AGO: temu
-  FROM_NOW: od teraz
-  SECOND: sekunda
-  MINUTE: minuta
-  HOUR: godzina
-  DAY: dzień
-  WEEK: tydzień
-  MONTH: miesiąc
-  YEAR: rok
-  DECADE: dekada
-  SEC: sek
-  MIN: min
-  HR: godz
-  WK: tydz
-  MO: m-c
-  YR: rok
-  DEC: dekada
-  SECOND_PLURAL: sekund
-  MINUTE_PLURAL: minut
-  HOUR_PLURAL: godzin
-  DAY_PLURAL: dni
-  WEEK_PLURAL: tygodnie
-  MONTH_PLURAL: miesięcy
-  YEAR_PLURAL: lat
-  DECADE_PLURAL: dekad
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: godz
-  WK_PLURAL: tyg
-  MO_PLURAL: m-ce
-  YR_PLURAL: lat
-  DEC_PLURAL: dekad
-FORM:
-  VALIDATION_FAIL: '<b>Weryfikacja nie powiodła się:</b>'
-  INVALID_INPUT: Nieprawidłowe dane wejściowe
-  MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:'
-MONTHS_OF_THE_YEAR:
-  - Styczeń
-  - Luty
-  - Marzec
-  - Kwiecień
-  - Maj
-  - Czerwiec
-  - Lipiec
-  - Sierpień
-  - Wrzesień
-  - Październik
-  - Listopad
-  - Grudzień
-DAYS_OF_THE_WEEK:
-  - Poniedziałek
-  - Wtorek
-  - Środa
-  - Czwartek
-  - Piątek
-  - Sobota
-  - Niedziela
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Error: Nieprawidłowy Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Nie podano daty
+    BAD_DATE: Zła data
+    AGO: temu
+    FROM_NOW: od teraz
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: godzina
+    DAY: dzień
+    WEEK: tydzień
+    MONTH: miesiąc
+    YEAR: rok
+    DECADE: dekada
+    SEC: sek
+    HR: godz
+    WK: tydz
+    MO: m-c
+    YR: rok
+    DEC: dekada
+    SECOND_PLURAL: sekund
+    MINUTE_PLURAL: minut
+    HOUR_PLURAL: godzin
+    DAY_PLURAL: dni
+    WEEK_PLURAL: tygodnie
+    MONTH_PLURAL: miesięcy
+    YEAR_PLURAL: lat
+    DECADE_PLURAL: dekad
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: godz
+    WK_PLURAL: tyg
+    MO_PLURAL: m-ce
+    YR_PLURAL: lat
+    DEC_PLURAL: dekad
+  FORM:
+    VALIDATION_FAIL: <b>Weryfikacja nie powiodła się:</b>
+    INVALID_INPUT: Nieprawidłowe dane wejściowe
+    MISSING_REQUIRED_FIELD: 'Opuszczono wymagane pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'Styczeń'
+    - 'Luty'
+    - 'Marzec'
+    - 'Kwiecień'
+    - 'Maj'
+    - 'Czerwiec'
+    - 'Lipiec'
+    - 'Sierpień'
+    - 'Wrzesień'
+    - 'Październik'
+    - 'Listopad'
+    - 'Grudzień'
+  DAYS_OF_THE_WEEK:
+    - 'Poniedziałek'
+    - 'Wtorek'
+    - 'Środa'
+    - 'Czwartek'
+    - 'Piątek'
+    - 'Sobota'
+    - 'Niedziela'

+ 45 - 78
system/languages/pt.yaml

@@ -1,79 +1,46 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  título: %1$s
-  ---
-  
-  # Erro: Frontmatter inválida
-  
-  Caminho: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_UNCOUNTABLE:
-  1: informação
-  2: arroz
-  3: dinheiro
-INFLECTOR_IRREGULAR:
-  man: homens
-  sex: sexos
-NICETIME:
-  NO_DATE_PROVIDED: Não foi fornecida data
-  BAD_DATE: Data inválida
-  AGO: atrás
-  FROM_NOW: a partir de agora
-  SECOND: segundo
-  MINUTE: minuto
-  HOUR: hora
-  DAY: dia
-  WEEK: semana
-  MONTH: mês
-  YEAR: ano
-  DECADE: década
-  SEC: seg
-  MIN: mín
-  HR: h
-  WK: sem
-  MO: m
-  YR: a
-  DEC: dec
-  SECOND_PLURAL: segundos
-  MINUTE_PLURAL: minutos
-  HOUR_PLURAL: horas
-  DAY_PLURAL: dias
-  WEEK_PLURAL: semanas
-  MONTH_PLURAL: meses
-  YEAR_PLURAL: anos
-  DECADE_PLURAL: décadas
-  SEC_PLURAL: seg
-  MIN_PLURAL: mins
-  HR_PLURAL: hrs
-  WK_PLURAL: sems
-  YR_PLURAL: anos
-FORM:
-  VALIDATION_FAIL: '<b>Validação falhada: </b>'
-  MISSING_REQUIRED_FIELD: 'Campo obrigatório ausente:'
-MONTHS_OF_THE_YEAR:
-  - Janeiro
-  - Fevereiro
-  - Março
-  - Abril
-  - Maio
-  - Junho
-  - Julho
-  - Agosto
-  - Setembro
-  - Outubro
-  - Novembro
-  - Dezembro
-DAYS_OF_THE_WEEK:
-  - Segunda
-  - Terça
-  - Quarta
-  - Quinta
-  - Sexta
-  - Sábado
-  - Domingo
+GRAV:
+  NICETIME:
+    NO_DATE_PROVIDED: Nenhuma data fornecida
+    AGO: atrás
+    SECOND: segundo
+    MINUTE: minuto
+    HOUR: hora
+    DAY: dia
+    WEEK: semana
+    MONTH: mês
+    YEAR: ano
+    DECADE: década
+    SEC: segundos
+    MIN: minutos
+    MINUTE_PLURAL: minutos
+    DAY_PLURAL: dias
+    WEEK_PLURAL: semanas
+    MONTH_PLURAL: meses
+    YEAR_PLURAL: anos
+    DECADE_PLURAL: decadas
+  FORM:
+    VALIDATION_FAIL: <b>Falha na validação!</b>
+    MISSING_REQUIRED_FIELD: 'Campo obrigatório requerido:'
+  MONTHS_OF_THE_YEAR:
+    - 'Janeiro'
+    - 'Fevereiro'
+    - 'Março'
+    - 'Abril'
+    - 'Maio'
+    - 'Junho'
+    - 'Julho'
+    - 'Agosto'
+    - 'Setembro'
+    - 'Outubro'
+    - 'Novembro'
+    - 'Dezembro'
+  INFLECTOR_UNCOUNTABLE:
+    - 'equipment'
+    - 'information'
+    - 'arroz'
+    - 'money'
+    - 'species'
+    - 'series'
+    - 'fish'
+    - 'sheep'

+ 95 - 100
system/languages/ro.yaml

@@ -1,101 +1,96 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  Titlu: %1$s
-  ---
-  # Eroare: Frontmatter este invalid
-  
-  Calea: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-INFLECTOR_PLURALS:
-  /(quiz)$/i: '\1zes'
-  /^(ox)$/i: '\1en'
-  "/([m|l])ouse$/i": '\1ice'
-  /(matr|vert|ind)ix|ex$/i: '\1ices'
-  /(x|ch|ss|sh)$/i: '\1es'
-  "/([^aeiouy]|qu)ies$/i": '\1y'
-  "/([^aeiouy]|qu)y$/i": '\1ies'
-  /(hive)$/i: '\1s'
-  "/(?:([^f])fe|([lr])f)$/i": '\1\2ves'
-  /sis$/i: ses
-  "/([ti])um$/i": '\1a'
-  /(buffal|tomat)o$/i: '\1oes'
-INFLECTOR_UNCOUNTABLE:
-  - echipament
-  - informaţie
-  - orez
-  - bani
-  - specii
-  - serii
-  - peşte
-  - oaie
-INFLECTOR_IRREGULAR:
-  person: persoane
-  man: bărbați
-  child: copii
-  sex: sexe
-  move: mutări
-NICETIME:
-  NO_DATE_PROVIDED: Nu există o dată prevăzută
-  BAD_DATE: Dată incorectă
-  AGO: în urmă
-  FROM_NOW: de acum
-  SECOND: secundă
-  MINUTE: minut
-  HOUR: oră
-  DAY: zi
-  WEEK: săptămână
-  MONTH: lună
-  YEAR: an
-  DECADE: decadă
-  SEC: sec
-  MIN: min
-  HR: oră
-  WK: săpt
-  MO: lună
-  YR: an
-  DEC: decadă
-  SECOND_PLURAL: secunde
-  MINUTE_PLURAL: minute
-  HOUR_PLURAL: ore
-  DAY_PLURAL: zile
-  WEEK_PLURAL: săptămâni
-  MONTH_PLURAL: luni
-  YEAR_PLURAL: ani
-  DECADE_PLURAL: decade
-  SEC_PLURAL: sec
-  MIN_PLURAL: min
-  HR_PLURAL: ore
-  WK_PLURAL: săpt
-  MO_PLURAL: luni
-  YR_PLURAL: ani
-  DEC_PLURAL: decenii
-FORM:
-  VALIDATION_FAIL: '<b>Validare nereușită</b>'
-  INVALID_INPUT: Date incorecte în
-  MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:'
-MONTHS_OF_THE_YEAR:
-  - Ianuarie
-  - Februarie
-  - Martie
-  - Aprilie
-  - Mai
-  - Iunie
-  - Iulie
-  - August
-  - Septembrie
-  - Octombrie
-  - Noiembrie
-  - Decembrie
-DAYS_OF_THE_WEEK:
-  - Luni
-  - Marți
-  - Miercuri
-  - Joi
-  - Vineri
-  - Sâmbătă
-  - Duminică
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nTitlu: %1$s\n---\n# Eroare: Frontmatter este invalid\n\nCalea: `%2$s`\n\n**%3$s**\n\n```\n%4$s"
+  INFLECTOR_UNCOUNTABLE:
+    - 'echipament'
+    - 'informaţie'
+    - 'orez'
+    - 'bani'
+    - 'specii'
+    - 'serii'
+    - 'peşte'
+    - 'oaie'
+  INFLECTOR_IRREGULAR:
+    'person': 'persoane'
+    'man': 'bărbați'
+    'child': 'copii'
+    'sex': 'sexe'
+    'move': 'mutări'
+  NICETIME:
+    NO_DATE_PROVIDED: Nu există o dată prevăzută
+    BAD_DATE: Dată incorectă
+    AGO: în urmă
+    FROM_NOW: de acum
+    JUST_NOW: chiar acum
+    SECOND: secundă
+    MINUTE: minut
+    HOUR: oră
+    DAY: zi
+    WEEK: săptămână
+    MONTH: lună
+    YEAR: an
+    DECADE: decadă
+    SEC: secunde
+    MIN: minute
+    HR: oră
+    WK: săpt
+    MO: lună
+    YR: an
+    DEC: decadă
+    SECOND_PLURAL: secunde
+    MINUTE_PLURAL: minute
+    HOUR_PLURAL: ore
+    DAY_PLURAL: zile
+    WEEK_PLURAL: săptămâni
+    MONTH_PLURAL: luni
+    YEAR_PLURAL: ani
+    DECADE_PLURAL: decade
+    SEC_PLURAL: sec
+    MIN_PLURAL: min
+    HR_PLURAL: ore
+    WK_PLURAL: săpt
+    MO_PLURAL: luni
+    YR_PLURAL: ani
+    DEC_PLURAL: decenii
+  FORM:
+    VALIDATION_FAIL: <b>Validare nereușită</b>
+    INVALID_INPUT: Date incorecte în
+    MISSING_REQUIRED_FIELD: 'Câmp obligatoriu lipsă:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ianuarie'
+    - 'Februarie'
+    - 'Martie'
+    - 'Aprilie'
+    - 'Mai'
+    - 'Iunie'
+    - 'Iulie'
+    - 'August'
+    - 'Septembrie'
+    - 'Octombrie'
+    - 'Noiembrie'
+    - 'Decembrie'
+  DAYS_OF_THE_WEEK:
+    - 'Luni'
+    - 'Marți'
+    - 'Miercuri'
+    - 'Joi'
+    - 'Vineri'
+    - 'Sâmbătă'
+    - 'Duminică'
+  CRON:
+    EVERY: la fiecare
+    EVERY_HOUR: la fiecare oră
+    EVERY_MINUTE: la fiecare minut
+    EVERY_DAY_OF_WEEK: fiecare zi a săptămânii
+    EVERY_DAY_OF_MONTH: fiecare zi a lunii
+    EVERY_MONTH: fiecare lună
+    TEXT_PERIOD: Fiecare <b />
+    TEXT_MINS: ' la <b /> minut(e) ale fiecărei ore'
+    TEXT_TIME: ' la <b />:<b />'
+    TEXT_DOW: ' pe <b />'
+    TEXT_MONTH: 'al(e) <b />'
+    TEXT_DOM: ' pe <b />'
+    ERROR1: Eticheta %s nu este acceptată!
+    ERROR2: Număr nevalid de elemente
+    ERROR3: jquery_element ar trebui setat în opțiunile jqCron
+    ERROR4: Expresie necunoscută

+ 100 - 80
system/languages/ru.yaml

@@ -1,81 +1,101 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Ошибка: Недопустимое содержимое
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-INFLECTOR_IRREGULAR:
-  person: люди
-  man: человек
-  child: ребенок
-  sex: пол
-  move: движется
-NICETIME:
-  NO_DATE_PROVIDED: Дата не указана
-  BAD_DATE: Неверная дата
-  AGO: назад
-  FROM_NOW: теперь
-  SECOND: секунда
-  MINUTE: минута
-  HOUR: час
-  DAY: д
-  WEEK: неделя
-  MONTH: месяц
-  YEAR: год
-  DECADE: десятилетие
-  SEC: с
-  MIN: мин
-  HR: ч
-  WK: нед.
-  MO: мес.
-  YR: г.
-  DEC: гг.
-  SECOND_PLURAL: секунды
-  MINUTE_PLURAL: минуты
-  HOUR_PLURAL: часы
-  DAY_PLURAL: д
-  WEEK_PLURAL: недели
-  MONTH_PLURAL: месяцы
-  YEAR_PLURAL: годы
-  DECADE_PLURAL: десятилетия
-  SEC_PLURAL: с
-  MIN_PLURAL: мин
-  HR_PLURAL: ч
-  WK_PLURAL: нед
-  MO_PLURAL: мес
-  YR_PLURAL: г.
-  DEC_PLURAL: гг.
-FORM:
-  VALIDATION_FAIL: '<b>Проверка не удалась:</b>'
-  INVALID_INPUT: Неверный ввод в
-  MISSING_REQUIRED_FIELD: 'Отсутствует необходимое поле:'
-MONTHS_OF_THE_YEAR:
-  - Январь
-  - Февраль
-  - Март
-  - Апрель
-  - Май
-  - Июнь
-  - Июль
-  - Август
-  - Сентябрь
-  - Октябрь
-  - Ноябрь
-  - Декабрь
-DAYS_OF_THE_WEEK:
-  - Понедельник
-  - Вторник
-  - Среда
-  - Четверг
-  - Пятница
-  - Суббота
-  - Воскресенье
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Ошибка: недопустимое содержимое Frontmatter\n\nПуть: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'экипировка'
+    - 'информация'
+    - 'рис'
+    - 'деньги'
+    - 'виды'
+    - 'серии'
+    - 'рыба'
+    - 'овца'
+  INFLECTOR_IRREGULAR:
+    'person': 'люди'
+    'man': 'человек'
+    'child': 'ребенок'
+    'sex': 'пол'
+    'move': 'движется'
+  INFLECTOR_ORDINALS:
+    'default': 'й'
+    'first': 'й'
+    'second': 'й'
+    'third': 'й'
+  NICETIME:
+    NO_DATE_PROVIDED: Дата не указана
+    BAD_DATE: Неверная дата
+    AGO: назад
+    FROM_NOW: теперь
+    JUST_NOW: только что
+    SECOND: секунда
+    MINUTE: минута
+    HOUR: час
+    DAY: день
+    WEEK: неделя
+    MONTH: месяц
+    YEAR: год
+    DECADE: десятилетие
+    SEC: сек
+    MIN: мин
+    HR: ч
+    WK: нед
+    MO: мес
+    YR: г
+    DEC: дстлт
+    SECOND_PLURAL: сек
+    MINUTE_PLURAL: мин
+    HOUR_PLURAL: ч
+    DAY_PLURAL: д
+    WEEK_PLURAL: нед
+    MONTH_PLURAL: мес
+    YEAR_PLURAL: г
+    DECADE_PLURAL: дстлт
+    SEC_PLURAL: сек
+    MIN_PLURAL: мин
+    HR_PLURAL: ч
+    WK_PLURAL: нед
+    MO_PLURAL: мес
+    YR_PLURAL: г
+    DEC_PLURAL: дстлт
+  FORM:
+    VALIDATION_FAIL: <b>Проверка не удалась:</b>
+    INVALID_INPUT: Неверный ввод в
+    MISSING_REQUIRED_FIELD: 'Отсутствует необходимое поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'январь'
+    - 'февраль'
+    - 'март'
+    - 'апрель'
+    - 'май'
+    - 'июнь'
+    - 'Июль'
+    - 'Август'
+    - 'Сентябрь'
+    - 'Октябрь'
+    - 'Ноябрь'
+    - 'Декабрь'
+  DAYS_OF_THE_WEEK:
+    - 'понедельник'
+    - 'вторник'
+    - 'среда'
+    - 'четверг'
+    - 'пятница'
+    - 'суббота'
+    - 'воскресенье'
+  CRON:
+    EVERY: раз в
+    EVERY_HOUR: раз в час
+    EVERY_MINUTE: раз в минуту
+    EVERY_DAY_OF_WEEK: каждый день недели
+    EVERY_DAY_OF_MONTH: каждый день недели
+    EVERY_MONTH: раз в месяц
+    TEXT_PERIOD: Каждый <b />
+    TEXT_MINS: ' в <b /> минуте(ах) за час'
+    TEXT_TIME: ' в <b />:<b />'
+    TEXT_DOW: ' на <b />'
+    TEXT_MONTH: ' из <b />'
+    TEXT_DOM: ' на <b />'
+    ERROR1: Тег %s не поддерживается!
+    ERROR2: Неверное количество элементов
+    ERROR3: jquery_element должен быть установлен в настройки jqCron
+    ERROR4: Выражение не распознано

+ 143 - 41
system/languages/sk.yaml

@@ -1,42 +1,144 @@
 ---
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
-  BAD_DATE: Nesprávny dátum
-  AGO: pred
-  FROM_NOW: odteraz
-  SECOND: sekunda
-  MINUTE: minúta
-  HOUR: hodina
-  DAY: deň
-  WEEK: týždeň
-  MONTH: mesiac
-  YEAR: rok
-  DECADE: desaťročie
-  SEC: sek
-  MIN: min
-  HR: hod
-FORM:
-  VALIDATION_FAIL: '<b>Overenie zlyhalo:</b>'
-  INVALID_INPUT: Neplatný vstup v
-  MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:'
-MONTHS_OF_THE_YEAR:
-  - Január
-  - Február
-  - Marec
-  - Apríl
-  - Máj
-  - Jún
-  - Júl
-  - August
-  - September
-  - Október
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Pondelok
-  - Utorok
-  - Streda
-  - Štvrtok
-  - Piatok
-  - Sobota
-  - Nedeľa
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Chyba: Chybný frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - 'vybavenie'
+    - 'informácie'
+    - 'ryža'
+    - 'peniaze'
+    - 'druhy'
+    - 'séria'
+    - 'ryba'
+    - 'ovce'
+  INFLECTOR_IRREGULAR:
+    'person': 'ľudia'
+    'man': 'muži'
+    'child': 'deti'
+    'sex': 'pohlavia'
+    'move': 'pohyby'
+  INFLECTOR_ORDINALS:
+    'default': '.'
+    'first': '.'
+    'second': '.'
+    'third': '.'
+  NICETIME:
+    NO_DATE_PROVIDED: Neposkytnutý žiaden dátum
+    BAD_DATE: Nesprávny dátum
+    AGO: pred
+    FROM_NOW: odteraz
+    JUST_NOW: práve teraz
+    SECOND: sekunda
+    MINUTE: minúta
+    HOUR: hodina
+    DAY: deň
+    WEEK: týždeň
+    MONTH: mesiac
+    YEAR: rok
+    DECADE: desaťročie
+    SEC: sek
+    MIN: min
+    HR: hod
+    WK: t
+    MO: m
+    YR: r
+    DEC: dec
+    SECOND_PLURAL: sekúnd
+    MINUTE_PLURAL: minút
+    HOUR_PLURAL: hodín
+    DAY_PLURAL: dní
+    WEEK_PLURAL: týždňov
+    MONTH_PLURAL: mesiacov
+    YEAR_PLURAL: rokov
+    DECADE_PLURAL: dekád
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: hod
+    WK_PLURAL: t
+    MO_PLURAL: mes.
+    YR_PLURAL: rokov
+    DEC_PLURAL: dekád
+  FORM:
+    VALIDATION_FAIL: <b>Overenie zlyhalo:</b>
+    INVALID_INPUT: Neplatný vstup v
+    MISSING_REQUIRED_FIELD: 'Chýba vyžadované pole:'
+  MONTHS_OF_THE_YEAR:
+    - 'Január'
+    - 'Február'
+    - 'Marec'
+    - 'Apríl'
+    - 'Máj'
+    - 'Jún'
+    - 'Júl'
+    - 'August'
+    - 'September'
+    - 'Október'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Pondelok'
+    - 'Utorok'
+    - 'Streda'
+    - 'Štvrtok'
+    - 'Piatok'
+    - 'Sobota'
+    - 'Nedeľa'
+  CRON:
+    EVERY: každý
+    EVERY_HOUR: každú hodinu
+    EVERY_MINUTE: každú minútu
+    EVERY_DAY_OF_WEEK: každý deň v týždni
+    EVERY_DAY_OF_MONTH: každý deň v mesiaci
+    EVERY_MONTH: každý mesiac
+    TEXT_PERIOD: Každý <b />
+    TEXT_MINS: ' at <b /> minute(s) past the hour'
+    TEXT_TIME: ' at <b />:<b />'
+    TEXT_DOW: ' on <b />'
+    TEXT_MONTH: ' of <b />'
+    TEXT_DOM: ' on <b />'
+    ERROR1: Tag %s nieje podporovaný!
+    ERROR2: Chybný počet položiek
+    ERROR3: jquery_element musí byť nastavený v nastaveniach pre jqCron
+    ERROR4: Neznámy výraz

+ 62 - 0
system/languages/sl.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Napaka: Neveljavna Frontmatter\n\nPath: `%2$s`\n\n**%3$s ** \n\n```\n%4$s \n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Datum ni na voljo
+    BAD_DATE: Neveljaven datum
+    AGO: pred
+    FROM_NOW: od zdaj
+    SECOND: sekunda
+    MINUTE: minuta
+    HOUR: ura
+    DAY: dan
+    WEEK: teden
+    MONTH: mesec
+    YEAR: leto
+    DECADE: desetletje
+    SEC: sek
+    HR: ur
+    WK: T.
+    MO: m
+    YR: l
+    DEC: des
+    SECOND_PLURAL: sekund
+    MINUTE_PLURAL: minut
+    HOUR_PLURAL: ure
+    DAY_PLURAL: dnevi
+    WEEK_PLURAL: tednov
+    MONTH_PLURAL: mesecev
+    YEAR_PLURAL: leta
+    DECADE_PLURAL: desetletja
+    SEC_PLURAL: s
+    MIN_PLURAL: min
+    HR_PLURAL: ur
+    WK_PLURAL: t
+    MO_PLURAL: m
+    YR_PLURAL: l
+    DEC_PLURAL: des
+  FORM:
+    VALIDATION_FAIL: <b>Preverjanje veljavnosti ni uspelo:</b>
+    INVALID_INPUT: Neveljaven vnos v
+    MISSING_REQUIRED_FIELD: 'Manjka obvezno polje:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januar'
+    - 'Februar'
+    - 'Marec'
+    - 'April'
+    - 'Maj'
+    - 'Junij'
+    - 'Julij'
+    - 'Avgust'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Ponedeljek'
+    - 'Torek'
+    - 'Sreda'
+    - 'Četrtek'
+    - 'Petek'
+    - 'Sobota'
+    - 'Nedelja'

+ 71 - 61
system/languages/sv.yaml

@@ -1,62 +1,72 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: '--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```'
-NICETIME:
-  NO_DATE_PROVIDED: Inget datum har angivits
-  BAD_DATE: Ogiltigt datum
-  AGO: sedan
-  FROM_NOW: från nu
-  SECOND: sekund
-  MINUTE: minut
-  HOUR: timme
-  DAY: dag
-  WEEK: vecka
-  MONTH: månad
-  YEAR: år
-  DECADE: årtionde
-  SEC: sek
-  MIN: min
-  HR: t
-  WK: v
-  MO: m
-  YR: år
-  DEC: dec
-  SECOND_PLURAL: sekunder
-  MINUTE_PLURAL: minuter
-  HOUR_PLURAL: timmar
-  DAY_PLURAL: dagar
-  WEEK_PLURAL: veckor
-  MONTH_PLURAL: månader
-  YEAR_PLURAL: år
-  DECADE_PLURAL: årtionden
-  SEC_PLURAL: sek
-  MIN_PLURAL: min
-  HR_PLURAL: t
-  WK_PLURAL: v
-  MO_PLURAL: må
-  YR_PLURAL: år
-  DEC_PLURAL: dec
-FORM:
-  VALIDATION_FAIL: '<b>Kontrollen misslyckades:</b>'
-  INVALID_INPUT: Ogiltig indata i
-  MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:'
-MONTHS_OF_THE_YEAR:
-  - Januari
-  - Februrari
-  - Mars
-  - April
-  - Maj
-  - Juni
-  - Juli
-  - Augusti
-  - September
-  - Oktober
-  - November
-  - December
-DAYS_OF_THE_WEEK:
-  - Måndag
-  - Tisdag
-  - Onsdag
-  - Torsdag
-  - Fredag
-  - Lördag
-  - Söndag
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "--- titel: %1$s --- # Fel: Ogiltig Frontmatter-sökväg: `%2$s` **%3$s** ``` %4$s ```"
+  INFLECTOR_UNCOUNTABLE:
+    - 'utrustning'
+    - 'information'
+    - 'ris'
+    - 'pengar'
+    - 'arter'
+    - 'serier'
+    - 'fisk'
+    - 'får'
+  INFLECTOR_IRREGULAR:
+    'person': 'personer'
+  NICETIME:
+    NO_DATE_PROVIDED: Inget datum har angivits
+    BAD_DATE: Ogiltigt datum
+    AGO: sedan
+    FROM_NOW: fr.o.m nu
+    SECOND: sekund
+    MINUTE: minut
+    HOUR: timme
+    DAY: dag
+    WEEK: vecka
+    MONTH: månad
+    YEAR: år
+    DECADE: årtionde
+    SEC: sek
+    HR: t
+    WK: v
+    MO: m
+    YR: år
+    SECOND_PLURAL: sekunder
+    MINUTE_PLURAL: minuter
+    HOUR_PLURAL: timmar
+    DAY_PLURAL: dagar
+    WEEK_PLURAL: veckor
+    MONTH_PLURAL: månader
+    YEAR_PLURAL: år
+    DECADE_PLURAL: årtionden
+    SEC_PLURAL: sek
+    MIN_PLURAL: min
+    HR_PLURAL: t
+    WK_PLURAL: v
+    MO_PLURAL: må
+    YR_PLURAL: år
+    DEC_PLURAL: dec
+  FORM:
+    VALIDATION_FAIL: <b>Kontrollen misslyckades:</b>
+    INVALID_INPUT: Ogiltig indata i
+    MISSING_REQUIRED_FIELD: 'Obligatoriskt fält måste fyllas i:'
+  MONTHS_OF_THE_YEAR:
+    - 'Januari'
+    - 'Februari'
+    - 'Mars'
+    - 'April'
+    - 'Maj'
+    - 'Juni'
+    - 'Juli'
+    - 'Augusti'
+    - 'September'
+    - 'Oktober'
+    - 'November'
+    - 'December'
+  DAYS_OF_THE_WEEK:
+    - 'Måndag'
+    - 'Tisdag'
+    - 'Onsdag'
+    - 'Torsdag'
+    - 'Fredag'
+    - 'Lördag'
+    - 'Söndag'

+ 55 - 74
system/languages/th.yaml

@@ -1,75 +1,56 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  ชื่อเรื่อง: %1$s
-  ---
-  
-  # ข้อผิดพลาด: Invalid Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: ไม่มีวันที่ให้
-  BAD_DATE: รูปแบบวันที่ผิด
-  AGO: ที่ผ่านมา
-  FROM_NOW: จากตอนนี้
-  SECOND: วินาที
-  MINUTE: นาที
-  HOUR: ชั่วโมง
-  DAY: วัน
-  WEEK: สัปดาห์
-  MONTH: เดือน
-  YEAR: ปี
-  DECADE: ทศวรรษที่ผ่านมา
-  SEC: วิ
-  MIN: นาที
-  HR: ชม.
-  WK: wk
-  MO: mo
-  YR: yr
-  DEC: dec
-  SECOND_PLURAL: วินาที
-  MINUTE_PLURAL: นาที
-  HOUR_PLURAL: ชั่วโมง
-  DAY_PLURAL: วัน
-  WEEK_PLURAL: สัปดาห์
-  MONTH_PLURAL: เดือน
-  YEAR_PLURAL: ปี
-  DECADE_PLURAL: ทศวรรษที่ผ่านมา
-  SEC_PLURAL: วินาที
-  MIN_PLURAL: นาที
-  HR_PLURAL: ชั่วโมง
-  WK_PLURAL: wks
-  MO_PLURAL: mos
-  YR_PLURAL: ปี
-  DEC_PLURAL: decs
-FORM:
-  VALIDATION_FAIL: '<b>ตรวจสอบล้มเหลว: </b>'
-  INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน
-  MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:'
-MONTHS_OF_THE_YEAR:
-  - มกราคม
-  - กุมภาพันธ์
-  - มีนาคม
-  - เมษายน
-  - พฤษภาคม
-  - มิถุนายน
-  - กรกฏาคม
-  - สิงหาคม
-  - กันยายน
-  - ตุลาคม
-  - พฤศจิกายน
-  - ธันวาคม
-DAYS_OF_THE_WEEK:
-  - จันทร์
-  - อังคาร
-  - พุธ
-  - พฤหัสบดี
-  - ศุกร์
-  - เสาร์
-  - อาทิตย์
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nชื่อเรื่อง: %1$s\n---\n\n# ข้อผิดพลาด: Invalid Frontmatter\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: ไม่มีวันที่ให้
+    BAD_DATE: รูปแบบวันที่ผิด
+    AGO: ที่ผ่านมา
+    FROM_NOW: จากตอนนี้
+    SECOND: วินาที
+    MINUTE: นาที
+    HOUR: ชั่วโมง
+    DAY: วัน
+    WEEK: สัปดาห์
+    MONTH: เดือน
+    YEAR: ปี
+    DECADE: ทศวรรษที่ผ่านมา
+    SEC: วิ
+    MIN: นาที
+    HR: ชม.
+    SECOND_PLURAL: วินาที
+    MINUTE_PLURAL: นาที
+    HOUR_PLURAL: ชั่วโมง
+    DAY_PLURAL: วัน
+    WEEK_PLURAL: สัปดาห์
+    MONTH_PLURAL: เดือน
+    YEAR_PLURAL: ปี
+    DECADE_PLURAL: ทศวรรษที่ผ่านมา
+    SEC_PLURAL: วินาที
+    MIN_PLURAL: นาที
+    HR_PLURAL: ชั่วโมง
+    YR_PLURAL: ปี
+  FORM:
+    VALIDATION_FAIL: '<b>ตรวจสอบล้มเหลว: </b>'
+    INVALID_INPUT: ป้อนข้อมูลไม่ถูกต้องใน
+    MISSING_REQUIRED_FIELD: 'ขาดข้อมูลที่จำเป็น:'
+  MONTHS_OF_THE_YEAR:
+    - 'มกราคม'
+    - 'กุมภาพันธ์'
+    - 'มีนาคม'
+    - 'เมษายน'
+    - 'พฤษภาคม'
+    - 'มิถุนายน'
+    - 'กรกฏาคม'
+    - 'สิงหาคม'
+    - 'กันยายน'
+    - 'ตุลาคม'
+    - 'พฤศจิกายน'
+    - 'ธันวาคม'
+  DAYS_OF_THE_WEEK:
+    - 'จันทร์'
+    - 'อังคาร'
+    - 'พุธ'
+    - 'พฤหัสบดี'
+    - 'ศุกร์'
+    - 'เสาร์'
+    - 'อาทิตย์'

+ 62 - 58
system/languages/tr.yaml

@@ -1,59 +1,63 @@
 ---
 ---
-NICETIME:
-  NO_DATE_PROVIDED: Tarih yok
-  BAD_DATE: Yanlış tarih
-  AGO: önce
-  FROM_NOW: (şimdiden)
-  SECOND: saniye
-  MINUTE: dakika
-  HOUR: saat
-  DAY: gün
-  WEEK: hafta
-  MONTH: ay
-  YEAR: yıl
-  DECADE: onyıl
-  SEC: sn
-  MIN: dk
-  HR: sa
-  WK: hft
-  MO: ay
-  YR: yl
-  DEC: onyl
-  SECOND_PLURAL: saniye
-  MINUTE_PLURAL: dakika
-  HOUR_PLURAL: saat
-  DAY_PLURAL: gün
-  WEEK_PLURAL: hafta
-  MONTH_PLURAL: ay
-  YEAR_PLURAL: yıl
-  DECADE_PLURAL: onyıl
-  SEC_PLURAL: sn
-  MIN_PLURAL: dk
-  HR_PLURAL: sa
-  WK_PLURAL: hft
-  MO_PLURAL: ay
-  YR_PLURAL: yl
-  DEC_PLURAL: onyl
-FORM:
-  VALIDATION_FAIL: '<b>Doğrulama başarısız:</b>'
-MONTHS_OF_THE_YEAR:
-  - Ocak
-  - Şubat
-  - Mart
-  - Nisan
-  - Mayıs
-  - Haziran
-  - Temmuz
-  - Ağustos
-  - Eylül
-  - Ekim
-  - Kasım
-  - Aralık
-DAYS_OF_THE_WEEK:
-  - Pazartesi
-  - Salı
-  - Çarşamba
-  - Perşembe
-  - Cuma
-  - Cumartesi
-  - Pazar
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\nBaşlık: %1$s\n---\n\n# Hata: Geçersiz Önbölüm\n\nYol: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Sağlanan tarih yok
+    BAD_DATE: Yanlış tarih
+    AGO: önce
+    FROM_NOW: şu andan itibaren
+    SECOND: saniye
+    MINUTE: dakika
+    HOUR: saat
+    DAY: gün
+    WEEK: hafta
+    MONTH: ay
+    YEAR: yıl
+    DECADE: onyıl
+    SEC: sn
+    MIN: dk
+    HR: sa
+    WK: hft
+    MO: ay
+    YR: yl
+    DEC: onyl
+    SECOND_PLURAL: saniye
+    MINUTE_PLURAL: dakika
+    HOUR_PLURAL: saat
+    DAY_PLURAL: gün
+    WEEK_PLURAL: hafta
+    MONTH_PLURAL: ay
+    YEAR_PLURAL: yıl
+    DECADE_PLURAL: onyıl
+    SEC_PLURAL: sn
+    MIN_PLURAL: dk
+    HR_PLURAL: sa
+    WK_PLURAL: hft
+    MO_PLURAL: ay
+    YR_PLURAL: yıl
+    DEC_PLURAL: onyl
+  FORM:
+    VALIDATION_FAIL: <b>Doğrulama başarısız:</b>
+    INVALID_INPUT: Geçersiz bilgi girişi
+    MISSING_REQUIRED_FIELD: 'Gerekli alan eksik:'
+  MONTHS_OF_THE_YEAR:
+    - 'Ocak'
+    - 'Şubat'
+    - 'Mart'
+    - 'Nisan'
+    - 'Mayıs'
+    - 'Haziran'
+    - 'Temmuz'
+    - 'Ağustos'
+    - 'Eylül'
+    - 'Ekim'
+    - 'Kasım'
+    - 'Aralık'
+  DAYS_OF_THE_WEEK:
+    - 'Pazartesi'
+    - 'Salı'
+    - 'Çarşamba'
+    - 'Perşembe'
+    - 'Cuma'
+    - 'Cumartesi'
+    - 'Pazar'

+ 62 - 74
system/languages/uk.yaml

@@ -1,75 +1,63 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Помилка: Недопустимий вміст
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Не вказана дата
-  BAD_DATE: Невірна дата
-  AGO: назад
-  FROM_NOW: відтепер
-  SECOND: секунда
-  MINUTE: хвилина
-  HOUR: година
-  DAY: день
-  WEEK: тиждень
-  MONTH: місяць
-  YEAR: рік
-  DECADE: десятиріччя
-  SEC: с
-  MIN: хв
-  HR: год
-  WK: тиж.
-  MO: міс.
-  YR: р.
-  DEC: рр.
-  SECOND_PLURAL: секунди
-  MINUTE_PLURAL: хвилини
-  HOUR_PLURAL: години
-  DAY_PLURAL: дні
-  WEEK_PLURAL: тижні
-  MONTH_PLURAL: місяці
-  YEAR_PLURAL: роки
-  DECADE_PLURAL: десятиріччя
-  SEC_PLURAL: с
-  MIN_PLURAL: хв
-  HR_PLURAL: год
-  WK_PLURAL: тиж.
-  MO_PLURAL: міс.
-  YR_PLURAL: рр.
-  DEC_PLURAL: рр.
-FORM:
-  VALIDATION_FAIL: '<b>Перевірка не вдалася:</b>'
-  INVALID_INPUT: Невірне введення в
-  MISSING_REQUIRED_FIELD: 'Відсутнє необхідне поле:'
-MONTHS_OF_THE_YEAR:
-  - Січень
-  - Лютий
-  - Березень
-  - Квітень
-  - Травень
-  - Червень
-  - Липень
-  - Серпень
-  - Вересень
-  - Жовтень
-  - Листопад
-  - Грудень
-DAYS_OF_THE_WEEK:
-  - Понеділок
-  - Вівторок
-  - Середа
-  - Четвер
-  - "П'ятниця"
-  - Субота
-  - Неділя
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# Помилка: Недопустимий вміст\n\nPath: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Не вказана дата
+    BAD_DATE: Невірна дата
+    AGO: назад
+    FROM_NOW: відтепер
+    SECOND: секунда
+    MINUTE: хвилина
+    HOUR: година
+    DAY: день
+    WEEK: тиждень
+    MONTH: місяць
+    YEAR: рік
+    DECADE: десятиріччя
+    SEC: с
+    MIN: хв
+    HR: год
+    WK: тиж.
+    MO: міс.
+    YR: р.
+    DEC: рр.
+    SECOND_PLURAL: секунди
+    MINUTE_PLURAL: хвилини
+    HOUR_PLURAL: години
+    DAY_PLURAL: дні
+    WEEK_PLURAL: тижні
+    MONTH_PLURAL: місяці
+    YEAR_PLURAL: роки
+    DECADE_PLURAL: десятиріччя
+    SEC_PLURAL: с
+    MIN_PLURAL: хв
+    HR_PLURAL: год
+    WK_PLURAL: тиж.
+    MO_PLURAL: міс.
+    YR_PLURAL: рр.
+    DEC_PLURAL: рр.
+  FORM:
+    VALIDATION_FAIL: <b>Перевірка не вдалася:</b>
+    INVALID_INPUT: Невірне введення в
+    MISSING_REQUIRED_FIELD: 'Відсутнє обов''язкове поле:'
+  MONTHS_OF_THE_YEAR:
+    - 'Січень'
+    - 'Лютий'
+    - 'Березень'
+    - 'Квітень'
+    - 'Травень'
+    - 'Червень'
+    - 'Липень'
+    - 'Серпень'
+    - 'Вересень'
+    - 'Жовтень'
+    - 'Листопад'
+    - 'Грудень'
+  DAYS_OF_THE_WEEK:
+    - 'Понеділок'
+    - 'Вівторок'
+    - 'Середа'
+    - 'Четвер'
+    - 'П''ятниця'
+    - 'Субота'
+    - 'Неділя'

+ 62 - 74
system/languages/vi.yaml

@@ -1,75 +1,63 @@
 ---
 ---
-FRONTMATTER_ERROR_PAGE: |
-  ---
-  title: %1$s
-  ---
-  
-  # Error: Invalid Frontmatter
-  
-  Path: `%2$s`
-  
-  **%3$s**
-  
-  ```
-  %4$s
-  ```
-NICETIME:
-  NO_DATE_PROVIDED: Không có ngày được cung cấp
-  BAD_DATE: Ngày không hợp lệ
-  AGO: cách đây
-  FROM_NOW: từ bây giờ
-  SECOND: giây
-  MINUTE: phút
-  HOUR: giờ
-  DAY: ngày
-  WEEK: tuần
-  MONTH: tháng
-  YEAR: năm
-  DECADE: thập kỷ
-  SEC: giây
-  MIN: phút
-  HR: giờ
-  WK: tuần
-  MO: tháng
-  YR: năm
-  DEC: thập kỷ
-  SECOND_PLURAL: giây
-  MINUTE_PLURAL: phút
-  HOUR_PLURAL: giờ
-  DAY_PLURAL: ngày
-  WEEK_PLURAL: tuần
-  MONTH_PLURAL: tháng
-  YEAR_PLURAL: năm
-  DECADE_PLURAL: thập kỷ
-  SEC_PLURAL: giây
-  MIN_PLURAL: phút
-  HR_PLURAL: giờ
-  WK_PLURAL: tuần
-  MO_PLURAL: tháng
-  YR_PLURAL: năm
-  DEC_PLURAL: thập kỷ
-FORM:
-  VALIDATION_FAIL: '<b>Xác nhận thất bại:</b>'
-  INVALID_INPUT: Dữ liệu nhập không hợp lệ cho
-  MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:'
-MONTHS_OF_THE_YEAR:
-  - Tháng 1
-  - Tháng 2
-  - Tháng 3
-  - Tháng 4
-  - Tháng 5
-  - Tháng 6
-  - Tháng 7
-  - Tháng 8
-  - Tháng 9
-  - Tháng 10
-  - Tháng Mười 11
-  - Tháng 12
-DAYS_OF_THE_WEEK:
-  - Thứ 2
-  - Thứ 3
-  - Thứ 4
-  - Thứ 5
-  - Thứ 6
-  - Thứ 7
-  - Chủ Nhật
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntiêu đề: %1$s\n---\n\n# Error: Trang không hợp lệ\n\nĐường dẫn: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: Không có ngày được cung cấp
+    BAD_DATE: Ngày không hợp lệ
+    AGO: cách đây
+    FROM_NOW: từ bây giờ
+    SECOND: giây
+    MINUTE: phút
+    HOUR: giờ
+    DAY: ngày
+    WEEK: tuần
+    MONTH: tháng
+    YEAR: năm
+    DECADE: thập kỷ
+    SEC: giây
+    MIN: phút
+    HR: giờ
+    WK: tuần
+    MO: tháng
+    YR: năm
+    DEC: thập kỷ
+    SECOND_PLURAL: giây
+    MINUTE_PLURAL: phút
+    HOUR_PLURAL: giờ
+    DAY_PLURAL: ngày
+    WEEK_PLURAL: tuần
+    MONTH_PLURAL: tháng
+    YEAR_PLURAL: năm
+    DECADE_PLURAL: thập kỷ
+    SEC_PLURAL: giây
+    MIN_PLURAL: phút
+    HR_PLURAL: giờ
+    WK_PLURAL: tuần
+    MO_PLURAL: tháng
+    YR_PLURAL: năm
+    DEC_PLURAL: thập kỷ
+  FORM:
+    VALIDATION_FAIL: <b>Xác nhận thất bại:</b>
+    INVALID_INPUT: Dữ liệu nhập không hợp lệ cho
+    MISSING_REQUIRED_FIELD: 'Thiếu trường bắt buộc:'
+  MONTHS_OF_THE_YEAR:
+    - 'Tháng 1'
+    - 'Tháng 2'
+    - 'Tháng 3'
+    - 'Tháng 4'
+    - 'Tháng 5'
+    - 'Tháng 6'
+    - 'Tháng 7'
+    - 'Tháng 8'
+    - 'Tháng 9'
+    - 'Tháng 10'
+    - 'Tháng 11'
+    - 'Tháng 12'
+  DAYS_OF_THE_WEEK:
+    - 'Thứ 2'
+    - 'Thứ 3'
+    - 'Thứ 4'
+    - 'Thứ 5'
+    - 'Thứ 6'
+    - 'Thứ 7'
+    - 'Chủ Nhật'

+ 62 - 0
system/languages/zh-tw.yaml

@@ -0,0 +1,62 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\ntitle: %1$s\n---\n\n# 錯誤: 不正確的 Frontmatter\n\n路徑: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  NICETIME:
+    NO_DATE_PROVIDED: 沒有提供日期
+    BAD_DATE: 錯誤日期
+    AGO: 之前
+    FROM_NOW: 之後
+    JUST_NOW: 剛剛
+    SECOND: 秒
+    MINUTE: 分
+    HOUR: 小時
+    DAY: 天
+    WEEK: 週
+    MONTH: 月
+    YEAR: 年
+    DECADE: 十年
+    SEC: 秒
+    MIN: 分
+    HR: 小時
+    WK: 週
+    MO: 月
+    YR: 年
+    DEC: 十年
+    SECOND_PLURAL: 秒
+    MINUTE_PLURAL: 分
+    HOUR_PLURAL: 小時
+    DAY_PLURAL: 天
+    WEEK_PLURAL: 週
+    MONTH_PLURAL: 月
+    YEAR_PLURAL: 年
+    DECADE_PLURAL: 十年
+    SEC_PLURAL: 秒
+    MIN_PLURAL: 分
+    HR_PLURAL: 時
+    WK_PLURAL: 週
+    MO_PLURAL: 月
+    YR_PLURAL: 年
+    DEC_PLURAL: 十年
+  FORM:
+    MISSING_REQUIRED_FIELD: 遺漏必填欄位:
+  MONTHS_OF_THE_YEAR:
+    - '一月'
+    - '二月'
+    - '三月'
+    - '四月'
+    - '五月'
+    - '六月'
+    - '七月'
+    - '八月'
+    - '九月'
+    - '十月'
+    - '十一月'
+    - '十二月'
+  DAYS_OF_THE_WEEK:
+    - '星期一'
+    - '星期二'
+    - '星期三'
+    - '星期四'
+    - '星期五'
+    - '星期六'
+    - '星期日'

+ 144 - 0
system/languages/zh.yaml

@@ -0,0 +1,144 @@
+---
+GRAV:
+  FRONTMATTER_ERROR_PAGE: "---\n标题: %1$s\n---\n\n# 错误:无效参数\n\n位置: `%2$s`\n\n**%3$s**\n\n```\n%4$s\n```"
+  INFLECTOR_PLURALS:
+    '/(quiz)$/i': '\1zes'
+    '/^(ox)$/i': '\1en'
+    '/([m|l])ouse$/i': '\1ice'
+    '/(matr|vert|ind)ix|ex$/i': '\1ices'
+    '/(x|ch|ss|sh)$/i': '\1es'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([^aeiouy]|qu)y$/i': '\1ies'
+    '/(hive)$/i': '\1s'
+    '/(?:([^f])fe|([lr])f)$/i': '\1\2ves'
+    '/sis$/i': 'ses'
+    '/([ti])um$/i': '\1a'
+    '/(buffal|tomat)o$/i': '\1oes'
+    '/(bu)s$/i': '\1ses'
+    '/(alias|status)/i': '\1es'
+    '/(octop|vir)us$/i': '\1i'
+    '/(ax|test)is$/i': '\1es'
+    '/s$/i': 's'
+    '/$/': 's'
+  INFLECTOR_SINGULAR:
+    '/(quiz)zes$/i': '\1'
+    '/(matr)ices$/i': '\1ix'
+    '/(vert|ind)ices$/i': '\1ex'
+    '/^(ox)en/i': '\1'
+    '/(alias|status)es$/i': '\1'
+    '/([octop|vir])i$/i': '\1us'
+    '/(cris|ax|test)es$/i': '\1is'
+    '/(shoe)s$/i': '\1'
+    '/(o)es$/i': '\1'
+    '/(bus)es$/i': '\1'
+    '/([m|l])ice$/i': '\1ouse'
+    '/(x|ch|ss|sh)es$/i': '\1'
+    '/(m)ovies$/i': '\1ovie'
+    '/(s)eries$/i': '\1eries'
+    '/([^aeiouy]|qu)ies$/i': '\1y'
+    '/([lr])ves$/i': '\1f'
+    '/(tive)s$/i': '\1'
+    '/(hive)s$/i': '\1'
+    '/([^f])ves$/i': '\1fe'
+    '/(^analy)ses$/i': '\1sis'
+    '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i': '\1\2sis'
+    '/([ti])a$/i': '\1um'
+    '/(n)ews$/i': '\1ews'
+  INFLECTOR_UNCOUNTABLE:
+    - '装备'
+    - '信息'
+    - '大米'
+    - '钱'
+    - '物种'
+    - '系列'
+    - '鱼'
+    - '羊'
+  INFLECTOR_IRREGULAR:
+    'person': '人员'
+    'man': '男人'
+    'child': '儿童'
+    'sex': '性别'
+    'move': '移动'
+  INFLECTOR_ORDINALS:
+    'default': 'th'
+    'first': 'st'
+    'second': 'md'
+    'third': 'rd'
+  NICETIME:
+    NO_DATE_PROVIDED: 无日期信息
+    BAD_DATE: 无效日期
+    AGO: 前
+    FROM_NOW: 距今
+    JUST_NOW: 刚刚
+    SECOND: 秒
+    MINUTE: 分钟
+    HOUR: 小时
+    DAY: 天
+    WEEK: 周
+    MONTH: 月
+    YEAR: 年
+    DECADE: 十年
+    SEC: 秒
+    MIN: 分钟
+    HR: 小时
+    WK: 周
+    MO: 月
+    YR: 年
+    DEC: 年代
+    SECOND_PLURAL: 秒
+    MINUTE_PLURAL: 分
+    HOUR_PLURAL: 小时
+    DAY_PLURAL: 天
+    WEEK_PLURAL: 周
+    MONTH_PLURAL: 月
+    YEAR_PLURAL: 年
+    DECADE_PLURAL: 十年
+    SEC_PLURAL: 秒
+    MIN_PLURAL: 分
+    HR_PLURAL: 时
+    WK_PLURAL: 周
+    MO_PLURAL: 月
+    YR_PLURAL: 年
+    DEC_PLURAL: 年代
+  FORM:
+    VALIDATION_FAIL: <b>验证失败:</b>
+    INVALID_INPUT: 无效输入
+    MISSING_REQUIRED_FIELD: 必填字段缺失:
+  MONTHS_OF_THE_YEAR:
+    - '1月'
+    - '2月'
+    - '3月'
+    - '4月'
+    - '5月'
+    - '6月'
+    - '7月'
+    - '8月'
+    - '9月'
+    - '10月'
+    - '11月'
+    - '12月'
+  DAYS_OF_THE_WEEK:
+    - '星期一'
+    - '星期二'
+    - '星期三'
+    - '星期四'
+    - '星期五'
+    - '星期六'
+    - '星期日'
+  CRON:
+    EVERY: 每隔
+    EVERY_HOUR: 每小时
+    EVERY_MINUTE: 每分钟
+    EVERY_DAY_OF_WEEK: 一周中的每一天
+    EVERY_DAY_OF_MONTH: 月份中的每一天
+    EVERY_MONTH: 每月
+    TEXT_PERIOD: 所有 <b />
+    TEXT_MINS: ' 在 <b /> 小时过后的分钟'
+    TEXT_TIME: ' 在 <b />:<b />'
+    TEXT_DOW: ' on <b />'
+    TEXT_MONTH: ' of <b />'
+    TEXT_DOM: ' on <b />'
+    ERROR1: 不支持分享类型 %s
+    ERROR2: 无效数字
+    ERROR3: 请在 jqCron 设置中设定 jquery_element
+    ERROR4: 无法识别表达式

+ 19 - 8
system/router.php

@@ -1,13 +1,14 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Core
+ * @package    Grav\Core
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
 if (PHP_SAPI !== 'cli-server') {
 if (PHP_SAPI !== 'cli-server') {
-    exit('This script cannot be run from browser. Run it from a CLI.');
+    die('This script cannot be run from browser. Run it from a CLI.');
 }
 }
 
 
 $_SERVER['PHP_CLI_ROUTER'] = true;
 $_SERVER['PHP_CLI_ROUTER'] = true;
@@ -16,11 +17,21 @@ if (is_file($_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $_SERVER['SCRIPT_N
     return false;
     return false;
 }
 }
 
 
-$_SERVER = array_merge($_SERVER, $_ENV);
-$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . 'index.php';
-$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR . 'index.php';
-$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR . 'index.php';
+$grav_index = 'index.php';
+
+/* Check the GRAV_BASEDIR environment variable and use if set */
+$grav_basedir = getenv('GRAV_BASEDIR') ?: '';
+if ($grav_basedir) {
+    $grav_index = ltrim($grav_basedir, '/') . DIRECTORY_SEPARATOR . $grav_index;
+    $grav_basedir = DIRECTORY_SEPARATOR . trim($grav_basedir, DIRECTORY_SEPARATOR);
+    define('GRAV_ROOT', str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . $grav_basedir);
+}
 
 
-require 'index.php';
+$_SERVER = array_merge($_SERVER, $_ENV);
+$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'] . $grav_basedir .DIRECTORY_SEPARATOR . 'index.php';
+$_SERVER['SCRIPT_NAME'] = $grav_basedir . DIRECTORY_SEPARATOR . 'index.php';
+$_SERVER['PHP_SELF'] = $grav_basedir . DIRECTORY_SEPARATOR . 'index.php';
 
 
 error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4);
 error_log(sprintf('%s:%d [%d]: %s', $_SERVER['REMOTE_ADDR'], $_SERVER['REMOTE_PORT'], http_response_code(), $_SERVER['REQUEST_URI']), 4);
+
+require $grav_index;

+ 188 - 1257
system/src/Grav/Common/Assets.php

@@ -1,27 +1,33 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
 namespace Grav\Common;
 namespace Grav\Common;
 
 
-use Closure;
-use Exception;
-use FilesystemIterator;
+use Grav\Common\Assets\Pipeline;
+use Grav\Common\Assets\Traits\LegacyAssetsTrait;
+use Grav\Common\Assets\Traits\TestingAssetsTrait;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
-use RecursiveDirectoryIterator;
-use RecursiveIteratorIterator;
-use RegexIterator;
+use Grav\Framework\Object\PropertyObject;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 
-define('CSS_ASSET', true);
-define('JS_ASSET', false);
-
-class Assets
+class Assets extends PropertyObject
 {
 {
+    use TestingAssetsTrait;
+    use LegacyAssetsTrait;
+
+    const CSS_COLLECTION = 'assets_css';
+    const JS_COLLECTION = 'assets_js';
+    const CSS_TYPE = 'Css';
+    const JS_TYPE = 'Js';
+    const INLINE_CSS_TYPE = 'InlineCss';
+    const INLINE_JS_TYPE = 'InlineJs';
+
     /** @const Regex to match CSS and JavaScript files */
     /** @const Regex to match CSS and JavaScript files */
     const DEFAULT_REGEX = '/.\.(css|js)$/i';
     const DEFAULT_REGEX = '/.\.(css|js)$/i';
 
 
@@ -31,79 +37,50 @@ class Assets
     /** @const Regex to match JavaScript files */
     /** @const Regex to match JavaScript files */
     const JS_REGEX = '/.\.js$/i';
     const JS_REGEX = '/.\.js$/i';
 
 
-    /** @const Regex to match CSS urls */
-    const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
+    protected $assets_dir;
+    protected $assets_url;
 
 
-    /** @const Regex to match CSS sourcemap comments */
-    const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
+    protected $assets_css = [];
+    protected $assets_js = [];
 
 
-    /** @const Regex to match CSS import content */
-    const CSS_IMPORT_REGEX = '{@import(.*?);}';
+    // Config Options
+    protected $css_pipeline;
+    protected $css_pipeline_include_externals;
+    protected $css_pipeline_before_excludes;
+    protected $js_pipeline;
+    protected $js_pipeline_include_externals;
+    protected $js_pipeline_before_excludes;
+    protected $pipeline_options = [];
 
 
-    /**
-     * @const Regex to match <script> or <style> tag when adding inline style/script. Note that this only supports a
-     * single tag, so the check is greedy to avoid issues in JS.
-     */
-    const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\2>)#is';
+
+    protected $fetch_command;
+    protected $autoload;
+    protected $enable_asset_timestamp;
+    protected $collections;
+    protected $timestamp;
 
 
 
 
     /**
     /**
-     * Closure used by the pipeline to fetch assets.
-     *
-     * Useful when file_get_contents() function is not available in your PHP
-     * installation or when you want to apply any kind of preprocessing to
-     * your assets before they get pipelined.
-     *
-     * The closure will receive as the only parameter a string with the path/URL of the asset and
-     * it should return the content of the asset file as a string.
-     *
-     * @var Closure
+     * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
      */
      */
-    protected $fetch_command;
+    public function init()
+    {
+        $grav = Grav::instance();
+        /** @var Config $config */
+        $config = $grav['config'];
 
 
-    // Configuration toggles to enable/disable the pipelining feature
-    protected $css_pipeline = false;
-    protected $css_pipeline_include_externals = true;
-    protected $css_pipeline_before_excludes = true;
-    protected $js_pipeline = false;
-    protected $js_pipeline_include_externals = true;
-    protected $js_pipeline_before_excludes = true;
-
-    // The asset holding arrays
-    protected $collections = [];
-    protected $css = [];
-    protected $js = [];
-    protected $inline_css = [];
-    protected $inline_js = [];
-    protected $imports = [];
-
-    // Some configuration variables
-    protected $config;
-    protected $base_url;
-    protected $timestamp = '';
-    protected $assets_dir;
-    protected $assets_url;
+        $asset_config = (array)$config->get('system.assets');
 
 
-    // Default values for pipeline settings
-    protected $css_minify = true;
-    protected $css_minify_windows = false;
-    protected $css_rewrite = true;
-    protected $js_minify = true;
+        /** @var UniformResourceLocator $locator */
+        $locator = $grav['locator'];
+        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_url = $locator->findResource('asset://', false);
 
 
-    // Arrays to hold assets that should NOT be pipelined
-    protected $css_no_pipeline = [];
-    protected $js_no_pipeline = [];
+        $this->config($asset_config);
 
 
-    /**
-     * Assets constructor.
-     *
-     * @param array $options
-     */
-    public function __construct(array $options = [])
-    {
-        // Forward config options
-        if ($options) {
-            $this->config((array)$options);
+        // Register any preconfigured collections
+        foreach ((array) $this->collections as $name => $collection) {
+            $this->registerCollection($name, (array)$collection);
         }
         }
     }
     }
 
 
@@ -117,140 +94,60 @@ class Assets
      * @param  array $config Configurable options.
      * @param  array $config Configurable options.
      *
      *
      * @return $this
      * @return $this
-     * @throws \Exception
      */
      */
     public function config(array $config)
     public function config(array $config)
     {
     {
-        // Set pipeline modes
-        if (isset($config['css_pipeline'])) {
-            $this->css_pipeline = $config['css_pipeline'];
-        }
-
-        if (isset($config['css_pipeline_include_externals'])) {
-            $this->css_pipeline_include_externals = $config['css_pipeline_include_externals'];
-        }
-
-        if (isset($config['css_pipeline_before_excludes'])) {
-            $this->css_pipeline_before_excludes = $config['css_pipeline_before_excludes'];
-        }
-
-        if (isset($config['js_pipeline'])) {
-            $this->js_pipeline = $config['js_pipeline'];
-        }
-
-        if (isset($config['js_pipeline_include_externals'])) {
-            $this->js_pipeline_include_externals = $config['js_pipeline_include_externals'];
-        }
-
-        if (isset($config['js_pipeline_before_excludes'])) {
-            $this->js_pipeline_before_excludes = $config['js_pipeline_before_excludes'];
-        }
-
-        // Pipeline requires public dir
-        if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) {
-            throw new \Exception('Assets: Public dir not found');
-        }
-
-        // Set custom pipeline fetch command
-        if (isset($config['fetch_command']) && ($config['fetch_command'] instanceof Closure)) {
-            $this->fetch_command = $config['fetch_command'];
-        }
-
-        // Set CSS Minify state
-        if (isset($config['css_minify'])) {
-            $this->css_minify = $config['css_minify'];
-        }
-
-        if (isset($config['css_minify_windows'])) {
-            $this->css_minify_windows = $config['css_minify_windows'];
-        }
-
-        if (isset($config['css_rewrite'])) {
-            $this->css_rewrite = $config['css_rewrite'];
-        }
-
-        // Set JS Minify state
-        if (isset($config['js_minify'])) {
-            $this->js_minify = $config['js_minify'];
-        }
-
-        // Set collections
-        if (isset($config['collections']) && is_array($config['collections'])) {
-            $this->collections = $config['collections'];
-        }
-
-        // Autoload assets
-        if (isset($config['autoload']) && is_array($config['autoload'])) {
-            foreach ($config['autoload'] as $asset) {
-                $this->add($asset);
+        foreach ($config as $key => $value) {
+            if ($this->hasProperty($key)) {
+                $this->setProperty($key, $value);
+            } elseif (Utils::startsWith($key, 'css_') || Utils::startsWith($key, 'js_')) {
+                $this->pipeline_options[$key] = $value;
             }
             }
         }
         }
 
 
-        // Set timestamp
-        if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
+        // Add timestamp if it's enabled
+        if ($this->enable_asset_timestamp) {
             $this->timestamp = Grav::instance()['cache']->getKey();
             $this->timestamp = Grav::instance()['cache']->getKey();
         }
         }
 
 
         return $this;
         return $this;
     }
     }
 
 
-    /**
-     * Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
-     */
-    public function init()
-    {
-        $grav = Grav::instance();
-        /** @var Config $config */
-        $config = $grav['config'];
-        $base_url = $grav['base_url'];
-        $asset_config = (array)$config->get('system.assets');
-
-        /** @var UniformResourceLocator $locator */
-        $locator = $grav['locator'];
-        $this->assets_dir = $locator->findResource('asset://') . DS;
-        $this->assets_url = $locator->findResource('asset://', false);
-
-        $this->config($asset_config);
-        $this->base_url = ($config->get('system.absolute_urls') ? '' : '/') . ltrim(ltrim($base_url, '/') . '/', '/');
-
-        // Register any preconfigured collections
-        foreach ($config->get('system.assets.collections', []) as $name => $collection) {
-            $this->registerCollection($name, (array)$collection);
-        }
-    }
-
     /**
     /**
      * Add an asset or a collection of assets.
      * Add an asset or a collection of assets.
      *
      *
      * It automatically detects the asset type (JavaScript, CSS or collection).
      * It automatically detects the asset type (JavaScript, CSS or collection).
      * You may add more than one asset passing an array as argument.
      * You may add more than one asset passing an array as argument.
      *
      *
-     * @param  mixed $asset
-     * @param  int   $priority the priority, bigger comes first
-     * @param  bool  $pipeline false if this should not be pipelined
-     *
+     * @param array|string $asset
      * @return $this
      * @return $this
      */
      */
-    public function add($asset, $priority = null, $pipeline = true)
+    public function add($asset)
     {
     {
+        $args = \func_get_args();
+
         // More than one asset
         // More than one asset
-        if (is_array($asset)) {
+        if (\is_array($asset)) {
             foreach ($asset as $a) {
             foreach ($asset as $a) {
-                $this->add($a, $priority, $pipeline);
+                array_shift($args);
+                $args = array_merge([$a], $args);
+                \call_user_func_array([$this, 'add'], $args);
             }
             }
         } elseif (isset($this->collections[$asset])) {
         } elseif (isset($this->collections[$asset])) {
-            $this->add($this->collections[$asset], $priority, $pipeline);
+            array_shift($args);
+            $args = array_merge([$this->collections[$asset]], $args);
+            \call_user_func_array([$this, 'add'], $args);
         } else {
         } else {
             // Get extension
             // Get extension
             $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
             $extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
 
 
             // JavaScript or CSS
             // JavaScript or CSS
-            if (strlen($extension) > 0) {
+            if (\strlen($extension) > 0) {
                 $extension = strtolower($extension);
                 $extension = strtolower($extension);
                 if ($extension === 'css') {
                 if ($extension === 'css') {
-                    $this->addCss($asset, $priority, $pipeline);
+                    \call_user_func_array([$this, 'addCss'], $args);
                 } elseif ($extension === 'js') {
                 } elseif ($extension === 'js') {
-                    $this->addJs($asset, $priority, $pipeline);
+                    \call_user_func_array([$this, 'addJs'], $args);
                 }
                 }
             }
             }
         }
         }
@@ -258,1187 +155,221 @@ class Assets
         return $this;
         return $this;
     }
     }
 
 
-    /**
-     * Add an asset to its assembly.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The third argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  array   $assembly the array assembling the assets
-     * @param  mixed   $asset
-     * @param  int     $priority the priority, bigger comes first
-     * @param  bool    $pipeline false if this should not be pipelined
-     * @param  string  $loading  how the asset is loaded (async/defer/inline, for CSS: only inline)
-     * @param  string  $group    name of the group
-     *
-     * @return $this
-     */
-    public function addTo(&$assembly, $asset, $priority = null, $pipeline = true, $loading = null, $group = null)
+    protected function addType($collection, $type, $asset, $options)
     {
     {
-        if (is_array($asset)) {
+        if (\is_array($asset)) {
             foreach ($asset as $a) {
             foreach ($asset as $a) {
-                $this->addTo($assembly, $a, $priority, $pipeline, $loading, $group);
+                $this->addType($collection, $type, $a, $options);
             }
             }
-
             return $this;
             return $this;
-        } elseif (isset($this->collections[$asset])) {
-            $this->addTo($assembly, $this->collections[$asset], $priority, $pipeline, $loading, $group);
+        }
 
 
+        if (($type === $this::CSS_TYPE || $type === $this::JS_TYPE) && isset($this->collections[$asset])) {
+            $this->addType($collection, $type, $this->collections[$asset], $options);
             return $this;
             return $this;
         }
         }
 
 
-        $query = [];
-
-        $modified = false;
-        $remote = $this->isRemoteLink($asset);
-        if (!$remote) {
+        // If pipeline disabled, set to position if provided, else after
+        if (isset($options['pipeline'])) {
+            if ($options['pipeline'] === false) {
+                $exclude_type = ($type === $this::JS_TYPE || $type === $this::INLINE_JS_TYPE) ? $this::JS_TYPE : $this::CSS_TYPE;
+                $excludes = strtolower($exclude_type . '_pipeline_before_excludes');
+                if ($this->{$excludes}) {
+                    $default = 'after';
+                } else {
+                    $default = 'before';
+                }
 
 
-            $asset_parts = parse_url($asset);
-            if (isset($asset_parts['query'])) {
-                $query[] = $asset_parts['query'];
-                unset($asset_parts['query']);
-                $asset = Uri::buildUrl($asset_parts);
+                $options['position'] = $options['position'] ?? $default;
             }
             }
 
 
-
-            $modified = $this->getLastModificationTime($asset);
-            $asset = $this->buildLocalLink($asset);
+            unset($options['pipeline']);
         }
         }
 
 
-        // Check for existence
-        if ($asset === false) {
-            return $this;
-        }
+        // Add timestamp
+        $options['timestamp'] = $this->timestamp;
 
 
-        $data = [
-            'asset'    => $asset,
-            'remote'   => $remote,
-            'priority' => intval($priority ?: 10),
-            'order'    => count($assembly),
-            'pipeline' => (bool) $pipeline,
-            'loading'  => $loading ?: '',
-            'group'    => $group ?: 'head',
-            'modified' => $modified,
-            'query'    => implode('&', $query),
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() > 2) {
-            $dynamic_arg = func_get_arg(2);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
+        // Set order
+        $options['order'] = \count($this->$collection);
 
 
-        $key = md5($asset);
-        if ($asset) {
-            $assembly[$key] = $data;
+        // Create asset of correct type
+        $asset_class = "\\Grav\\Common\\Assets\\{$type}";
+        $asset_object = new $asset_class();
+
+        // If exists
+        if ($asset_object->init($asset, $options)) {
+            $this->$collection[md5($asset)] = $asset_object;
         }
         }
 
 
         return $this;
         return $this;
-    }
 
 
-    /**
-     * Add a CSS asset.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The second argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  mixed   $asset
-     * @param  int     $priority the priority, bigger comes first
-     * @param  bool    $pipeline false if this should not be pipelined
-     * @param  string  $group
-     * @param  string  $loading  how the asset is loaded (async/defer/inline, for CSS: only inline)
-     *
-     * @return $this
-     */
-    public function addCss($asset, $priority = null, $pipeline = true, $group = null, $loading = null)
-    {
-        return $this->addTo($this->css, $asset, $priority, $pipeline, $loading, $group);
     }
     }
 
 
     /**
     /**
-     * Add a JavaScript asset.
-     *
-     * It checks for duplicates.
-     * You may add more than one asset passing an array as argument.
-     * The second argument may alternatively contain an array of options which take precedence over positional
-     * arguments.
-     *
-     * @param  mixed  $asset
-     * @param  int    $priority the priority, bigger comes first
-     * @param  bool   $pipeline false if this should not be pipelined
-     * @param  string $loading  how the asset is loaded (async/defer)
-     * @param  string $group    name of the group
+     * Add a CSS asset or a collection of assets.
      *
      *
      * @return $this
      * @return $this
      */
      */
-    public function addJs($asset, $priority = null, $pipeline = true, $loading = null, $group = null)
+    public function addCss($asset)
     {
     {
-        return $this->addTo($this->js, $asset, $priority, $pipeline, $loading, $group);
+        return $this->addType(Assets::CSS_COLLECTION,Assets::CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::CSS_TYPE));
     }
     }
 
 
     /**
     /**
-     * Convenience wrapper for async loading of JavaScript
-     *
-     * @param        $asset
-     * @param int    $priority
-     * @param bool   $pipeline
-     * @param string $group name of the group
+     * Add an Inline CSS asset or a collection of assets.
      *
      *
-     * @deprecated Please use dynamic method with ['loading' => 'async']
-     *
-     * @return \Grav\Common\Assets
+     * @return $this
      */
      */
-    public function addAsyncJs($asset, $priority = null, $pipeline = true, $group = null)
+    public function addInlineCss($asset)
     {
     {
-        return $this->addJs($asset, $priority, $pipeline, 'async', $group);
+        return $this->addType(Assets::CSS_COLLECTION, Assets::INLINE_CSS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_CSS_TYPE));
     }
     }
 
 
     /**
     /**
-     * Convenience wrapper for deferred loading of JavaScript
-     *
-     * @param        $asset
-     * @param int    $priority
-     * @param bool   $pipeline
-     * @param string $group name of the group
-     *
-     * @deprecated Please use dynamic method with ['loading' => 'defer']
+     * Add a JS asset or a collection of assets.
      *
      *
-     * @return \Grav\Common\Assets
+     * @return $this
      */
      */
-    public function addDeferJs($asset, $priority = null, $pipeline = true, $group = null)
+    public function addJs($asset)
     {
     {
-        return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
+        return $this->addType(Assets::JS_COLLECTION, Assets::JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::JS_TYPE));
     }
     }
 
 
     /**
     /**
-     * Add an inline CSS asset.
-     *
-     * It checks for duplicates.
-     * For adding chunks of string-based inline CSS
-     *
-     * @param  mixed $asset
-     * @param  int   $priority the priority, bigger comes first
-     * @param null   $group
+     * Add an Inline JS asset or a collection of assets.
      *
      *
      * @return $this
      * @return $this
      */
      */
-    public function addInlineCss($asset, $priority = null, $group = null)
+    public function addInlineJs($asset)
     {
     {
-        $asset = trim($asset);
-
-        if (is_a($asset, 'Twig_Markup')) {
-            preg_match(self::HTML_TAG_REGEX, $asset, $matches);
-            if (isset($matches[3])) {
-                $asset = $matches[3];
-            }
-        }
-
-        $data = [
-            'priority' => intval($priority ?: 10),
-            'order'    => count($this->inline_css),
-            'asset'    => $asset,
-            'group'    => $group ?: 'head'
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() == 2) {
-            $dynamic_arg = func_get_arg(1);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
-
-        $key = md5($asset);
-        if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_css)) {
-            $this->inline_css[$key] = $data;
-        }
-
-        return $this;
+        return $this->addType(Assets::JS_COLLECTION, Assets::INLINE_JS_TYPE, $asset, $this->unifyLegacyArguments(\func_get_args(), Assets::INLINE_JS_TYPE));
     }
     }
 
 
+
     /**
     /**
-     * Add an inline JS asset.
-     *
-     * It checks for duplicates.
-     * For adding chunks of string-based inline JS
+     * Add/replace collection.
      *
      *
-     * @param  mixed $asset
-     * @param  int $priority the priority, bigger comes first
-     * @param string $group name of the group
-     * @param null $attributes
+     * @param  string $collectionName
+     * @param  array  $assets
+     * @param bool    $overwrite
      *
      *
      * @return $this
      * @return $this
      */
      */
-    public function addInlineJs($asset, $priority = null, $group = null, $attributes = null)
+    public function registerCollection($collectionName, Array $assets, $overwrite = false)
     {
     {
-        $asset = trim($asset);
-
-        if (is_a($asset, 'Twig_Markup')) {
-            preg_match(self::HTML_TAG_REGEX, $asset, $matches);
-            if (isset($matches[3])) {
-                $asset = $matches[3];
-            }
-        }
-
-        $data = [
-            'asset'    => $asset,
-            'priority' => intval($priority ?: 10),
-            'order'    => count($this->js),
-            'group'    => $group ?: 'head',
-            'type'     => $attributes ?: '',
-        ];
-
-        // check for dynamic array and merge with defaults
-        if (func_num_args() == 2) {
-            $dynamic_arg = func_get_arg(1);
-            if (is_array($dynamic_arg)) {
-                $data = array_merge($data, $dynamic_arg);
-            }
-        }
-
-        $key = md5($asset);
-        if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_js)) {
-            $this->inline_js[$key] = $data;
+        if ($overwrite || !isset($this->collections[$collectionName])) {
+            $this->collections[$collectionName] = $assets;
         }
         }
 
 
         return $this;
         return $this;
     }
     }
 
 
-    /**
-     * Build the CSS link tags.
-     *
-     * @param  string $group name of the group
-     * @param  array  $attributes
-     *
-     * @return string
-     */
-    public function css($group = 'head', $attributes = [])
+    protected function filterAssets($assets, $key, $value, $sort = false)
     {
     {
-        if (!$this->css && !$this->inline_css) {
-            return null;
-        }
-
-        // Sort array by priorities (larger priority first)
-        if (Grav::instance()) {
-            uasort($this->css, array($this, 'sortAssetsByPriorityThenOrder'));
-            uasort($this->inline_css, array($this, 'sortAssetsByPriorityThenOrder'));
-        }
-
-        $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
+        $results = array_filter($assets, function($asset) use ($key, $value) {
 
 
-        $attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
+            if ($key === 'position' && $value === 'pipeline') {
 
 
-        $output = '';
-        $inline_css = '';
+                $type = $asset->getType();
 
 
-        if ($this->css_pipeline) {
-            $pipeline_result = $this->pipelineCss($group, !$inlineGroup);
-            $pipeline_html = ($inlineGroup ? '' : '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\n");
-
-            if ($this->css_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_css .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-            foreach ($this->css_no_pipeline as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($file['loading'] === 'inline') {
-                        $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
-                    }
-                    else {
-                        $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
-                        $output .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\n";
-                    }
-                }
-            }
-            if (!$this->css_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_css .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-        } else {
-            foreach ($this->css as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($inlineGroup || $file['loading'] === 'inline') {
-                        $inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
-                    }
-                    else {
-                        $media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
-                        $output .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\n";
+                if ($asset->getRemote() && $this->{$type . '_pipeline_include_externals'} === false && $asset['position'] === 'pipeline' ) {
+                    if ($this->{$type . '_pipeline_before_excludes'}) {
+                        $asset->setPosition('after');
+                    } else {
+                        $asset->setPosition('before');
                     }
                     }
+                    return false;
                 }
                 }
-            }
-        }
 
 
-        // Render Inline CSS
-        foreach ($this->inline_css as $inline) {
-            if ($group && $inline['group'] == $group) {
-                $inline_css .= $inline['asset'] . "\n";
             }
             }
-        }
 
 
-        if ($inline_css) {
-            $output .= "\n<style>\n" . $inline_css . "\n</style>\n";
+            if ($asset[$key] === $value) return true;
+            return false;
+        });
+
+        if ($sort && !empty($results)) {
+            $results = $this->sortAssets($results);
         }
         }
 
 
 
 
-        return $output;
+        return $results;
     }
     }
 
 
-    /**
-     * Build the JavaScript script tags.
-     *
-     * @param  string $group name of the group
-     * @param  array  $attributes
-     *
-     * @return string
-     */
-    public function js($group = 'head', $attributes = [])
+    protected function sortAssets($assets)
     {
     {
-        if (!$this->js && !$this->inline_js) {
-            return null;
-        }
-
-        // Sort array by priorities (larger priority first)
-        uasort($this->js, array($this, 'sortAssetsByPriorityThenOrder'));
-        uasort($this->inline_js, array($this, 'sortAssetsByPriorityThenOrder'));
+        uasort ($assets, function($a, $b) {
+            if ($a['priority'] == $b['priority']) {
+                return $a['order'] - $b['order'];
+            }
+            return $b['priority'] - $a['priority'];
+        });
+        return $assets;
+    }
 
 
-        $inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
+    public function render($type, $group = 'head', $attributes = [])
+    {
+        $before_output = '';
+        $pipeline_output = '';
+        $after_output = '';
 
 
-        $attributes = $this->attributes($attributes);
+        $assets = 'assets_' . $type;
+        $pipeline_enabled = $type . '_pipeline';
+        $render_pipeline = 'render' . ucfirst($type);
 
 
-        $output = '';
-        $inline_js = '';
+        $group_assets = $this->filterAssets($this->$assets, 'group', $group);
+        $pipeline_assets = $this->filterAssets($group_assets, 'position', 'pipeline', true);
+        $before_assets = $this->filterAssets($group_assets, 'position', 'before', true);
+        $after_assets = $this->filterAssets($group_assets, 'position', 'after', true);
 
 
-        if ($this->js_pipeline) {
-            $pipeline_result = $this->pipelineJs($group, !$inlineGroup);
-            $pipeline_html = ($inlineGroup ? '' : '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\n");
+        // Pipeline
+        if ($this->{$pipeline_enabled}) {
+            $options = array_merge($this->pipeline_options, ['timestamp' => $this->timestamp]);
 
 
-            if ($this->js_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_js .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
-            foreach ($this->js_no_pipeline as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($file['loading'] === 'inline') {
-                        $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
-                    }
-                    else {
-                        $output .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
-                    }
-                }
-            }
-            if (!$this->js_pipeline_before_excludes && $pipeline_result) {
-                if ($inlineGroup) {
-                    $inline_js .= $pipeline_result;
-                }
-                else {
-                    $output .= $pipeline_html;
-                }
-            }
+            $pipeline = new Pipeline($options);
+            $pipeline_output = $pipeline->$render_pipeline($pipeline_assets, $group, $attributes);
         } else {
         } else {
-            foreach ($this->js as $file) {
-                if ($group && $file['group'] == $group) {
-                    if ($inlineGroup || $file['loading'] === 'inline') {
-                        $inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
-                    }
-                    else {
-                        $output .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
-                    }
-                }
+            foreach ($pipeline_assets as $asset) {
+                $pipeline_output .= $asset->render();
             }
             }
         }
         }
 
 
-        // Render Inline JS
-        foreach ($this->inline_js as $inline) {
-            if ($group && $inline['group'] == $group) {
-                $inline_js .= $inline['asset'] . "\n";
-            }
+        // Before Pipeline
+        foreach ($before_assets as $asset) {
+            $before_output .= $asset->render();
         }
         }
 
 
-        if ($inline_js) {
-            $attribute_string = isset($inline) && $inline['type'] ? " type=\"" . $inline['type'] . "\"" : '';
-            $output .= "\n<script" . $attribute_string . ">\n" . $inline_js . "\n</script>\n";
+        // After Pipeline
+        foreach ($after_assets as $asset) {
+            $after_output .= $asset->render();
         }
         }
 
 
-        return $output;
+        return $before_output . $pipeline_output . $after_output;
     }
     }
 
 
+
     /**
     /**
-     * Minify and concatenate CSS
+     * Build the CSS link tags.
      *
      *
-     * @param string $group
-     * @param bool $returnURL  true if pipeline should return the URL, otherwise the content
+     * @param  string $group name of the group
+     * @param  array  $attributes
      *
      *
-     * @return bool|string     URL or generated content if available, else false
+     * @return string
      */
      */
-    protected function pipelineCss($group = 'head', $returnURL = true)
+    public function css($group = 'head', $attributes = [])
     {
     {
-        // temporary list of assets to pipeline
-        $temp_css = [];
-
-        // clear no-pipeline assets lists
-        $this->css_no_pipeline = [];
-
-        // Compute uid based on assets and timestamp
-        $uid = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group);
-        $file =  $uid . '.css';
-        $inline_file = $uid . '-inline.css';
-
-        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
-
-        // If inline files exist set them on object
-        if (file_exists($this->assets_dir . $inline_file)) {
-            $this->css_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
-        }
-
-        // If pipeline exist return its URL or content
-        if (file_exists($this->assets_dir . $file)) {
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return file_get_contents($this->assets_dir . $file) . "\n";
-            }
-        }
-
-        // Remove any non-pipeline files
-        foreach ($this->css as $id => $asset) {
-            if ($asset['group'] == $group) {
-                if (!$asset['pipeline'] ||
-                    ($asset['remote'] && $this->css_pipeline_include_externals === false)) {
-                    $this->css_no_pipeline[$id] = $asset;
-                } else {
-                    $temp_css[$id] = $asset;
-                }
-            }
-        }
-
-        //if nothing found get out of here!
-        if (count($temp_css) == 0) {
-            return false;
-        }
-
-        // Write non-pipeline files out
-        if (!empty($this->css_no_pipeline)) {
-            file_put_contents($this->assets_dir . $inline_file, json_encode($this->css_no_pipeline));
-        }
-
-
-        $css_minify = $this->css_minify;
-
-        // If this is a Windows server, and minify_windows is false (default value) skip the
-        // minification process because it will cause Apache to die/crash due to insufficient
-        // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
-        if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) {
-            $css_minify = false;
-        }
-
-        // Concatenate files
-        $buffer = $this->gatherLinks($temp_css, CSS_ASSET);
-        if ($css_minify) {
-            $minifier = new \MatthiasMullie\Minify\CSS();
-            $minifier->add($buffer);
-            $buffer = $minifier->minify();
-        }
-
-        // Write file
-        if (strlen(trim($buffer)) > 0) {
-            file_put_contents($this->assets_dir . $file, $buffer);
-
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return $buffer . "\n";
-            }
-        } else {
-            return false;
-        }
+        return $this->render('css', $group, $attributes);
     }
     }
 
 
     /**
     /**
-     * Minify and concatenate JS files.
+     * Build the JavaScript script tags.
      *
      *
-     * @param string $group
-     * @param bool $returnURL  true if pipeline should return the URL, otherwise the content
+     * @param  string $group name of the group
+     * @param  array  $attributes
      *
      *
-     * @return bool|string     URL or generated content if available, else false
+     * @return string
      */
      */
-    protected function pipelineJs($group = 'head', $returnURL = true)
+    public function js($group = 'head', $attributes = [])
     {
     {
-        // temporary list of assets to pipeline
-        $temp_js = [];
-
-        // clear no-pipeline assets lists
-        $this->js_no_pipeline = [];
-
-        // Compute uid based on assets and timestamp
-        $uid = md5(json_encode($this->js) . $this->js_minify . $group);
-        $file =  $uid . '.js';
-        $inline_file = $uid . '-inline.js';
-
-        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
-
-        // If inline files exist set them on object
-        if (file_exists($this->assets_dir . $inline_file)) {
-            $this->js_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
-        }
-
-        // If pipeline exist return its URL or content
-        if (file_exists($this->assets_dir . $file)) {
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return file_get_contents($this->assets_dir . $file) . "\n";
-            }
-        }
-
-        // Remove any non-pipeline files
-        foreach ($this->js as $id => $asset) {
-            if ($asset['group'] == $group) {
-                if (!$asset['pipeline'] ||
-                    ($asset['remote'] && $this->js_pipeline_include_externals === false)) {
-                    $this->js_no_pipeline[] = $asset;
-                } else {
-                    $temp_js[$id] = $asset;
-                }
-            }
-        }
-
-        //if nothing found get out of here!
-        if (count($temp_js) == 0) {
-            return false;
-        }
-
-        // Write non-pipeline files out
-        if (!empty($this->js_no_pipeline)) {
-            file_put_contents($this->assets_dir . $inline_file, json_encode($this->js_no_pipeline));
-        }
-
-        // Concatenate files
-        $buffer = $this->gatherLinks($temp_js, JS_ASSET);
-        if ($this->js_minify) {
-            $minifier = new \MatthiasMullie\Minify\JS();
-            $minifier->add($buffer);
-            $buffer = $minifier->minify();
-        }
-
-        // Write file
-        if (strlen(trim($buffer)) > 0) {
-            file_put_contents($this->assets_dir . $file, $buffer);
-
-            if ($returnURL) {
-                return $relative_path . $this->getTimestamp();
-            }
-            else {
-                return $buffer . "\n";
-            }
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Return the array of all the registered CSS assets
-     * If a $key is provided, it will try to return only that asset
-     * else it will return null
-     *
-     * @param null|string $key the asset key
-     * @return array
-     */
-    public function getCss($key = null)
-    {
-        if (!empty($key)) {
-            $asset_key = md5($key);
-            if (isset($this->css[$asset_key])) {
-                return $this->css[$asset_key];
-            } else {
-                return null;
-            }
-        }
-
-        return $this->css;
-    }
-
-    /**
-     * Return the array of all the registered JS assets
-     * If a $key is provided, it will try to return only that asset
-     * else it will return null
-     *
-     * @param null|string $key the asset key
-     * @return array
-     */
-    public function getJs($key = null)
-    {
-        if (!empty($key)) {
-            $asset_key = md5($key);
-            if (isset($this->js[$asset_key])) {
-                return $this->js[$asset_key];
-            } else {
-                return null;
-            }
-        }
-
-        return $this->js;
-    }
-
-    /**
-     * Set the whole array of CSS assets
-     *
-     * @param $css
-     */
-    public function setCss($css)
-    {
-        $this->css = $css;
-    }
-
-    /**
-     * Set the whole array of JS assets
-     *
-     * @param $js
-     */
-    public function setJs($js)
-    {
-        $this->js = $js;
-    }
-
-    /**
-     * Removes an item from the CSS array if set
-     *
-     * @param string $key  The asset key
-     */
-    public function removeCss($key)
-    {
-        $asset_key = md5($key);
-        if (isset($this->css[$asset_key])) {
-            unset($this->css[$asset_key]);
-        }
-    }
-
-    /**
-     * Removes an item from the JS array if set
-     *
-     * @param string $key  The asset key
-     */
-    public function removeJs($key)
-    {
-        $asset_key = md5($key);
-        if (isset($this->js[$asset_key])) {
-            unset($this->js[$asset_key]);
-        }
-    }
-
-    /**
-     * Return the array of all the registered collections
-     *
-     * @return array
-     */
-    public function getCollections()
-    {
-        return $this->collections;
-    }
-
-    /**
-     * Set the array of collections explicitly
-     *
-     * @param $collections
-     */
-    public function setCollection($collections)
-    {
-        $this->collections = $collections;
-    }
-
-    /**
-     * Determines if an asset exists as a collection, CSS or JS reference
-     *
-     * @param $asset
-     *
-     * @return bool
-     */
-    public function exists($asset)
-    {
-        if (isset($this->collections[$asset]) || isset($this->css[$asset]) || isset($this->js[$asset])) {
-            return true;
-        } else {
-            return false;
-        }
-    }
-
-    /**
-     * Add/replace collection.
-     *
-     * @param  string $collectionName
-     * @param  array  $assets
-     * @param bool    $overwrite
-     *
-     * @return $this
-     */
-    public function registerCollection($collectionName, Array $assets, $overwrite = false)
-    {
-        if ($overwrite || !isset($this->collections[$collectionName])) {
-            $this->collections[$collectionName] = $assets;
-        }
-
-        return $this;
+        return $this->render('js', $group, $attributes);
     }
     }
-
-    /**
-     * Reset all assets.
-     *
-     * @return $this
-     */
-    public function reset()
-    {
-        return $this->resetCss()->resetJs();
-    }
-
-    /**
-     * Reset JavaScript assets.
-     *
-     * @return $this
-     */
-    public function resetJs()
-    {
-        $this->js = [];
-        $this->inline_js = [];
-
-        return $this;
-    }
-
-    /**
-     * Reset CSS assets.
-     *
-     * @return $this
-     */
-    public function resetCss()
-    {
-        $this->css = [];
-        $this->inline_css = [];
-
-        return $this;
-    }
-
-    /**
-     * Add all JavaScript assets within $directory
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     *
-     * @return $this
-     */
-    public function addDirJs($directory)
-    {
-        return $this->addDir($directory, self::JS_REGEX);
-    }
-
-    /**
-     * Add all CSS assets within $directory
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     *
-     * @return $this
-     */
-    public function addDirCss($directory)
-    {
-        return $this->addDir($directory, self::CSS_REGEX);
-    }
-
-    /**
-     * Add all assets matching $pattern within $directory.
-     *
-     * @param  string $directory Relative to the Grav root path, or a stream identifier
-     * @param  string $pattern   (regex)
-     *
-     * @return $this
-     * @throws Exception
-     */
-    public function addDir($directory, $pattern = self::DEFAULT_REGEX)
-    {
-        $root_dir = rtrim(ROOT_DIR, '/');
-
-        // Check if $directory is a stream.
-        if (strpos($directory, '://')) {
-            $directory = Grav::instance()['locator']->findResource($directory, null);
-        }
-
-        // Get files
-        $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
-
-        // No luck? Nothing to do
-        if (!$files) {
-            return $this;
-        }
-
-        // Add CSS files
-        if ($pattern === self::CSS_REGEX) {
-            foreach ($files as $file) {
-                $this->addCss($file);
-            }
-
-            return $this;
-        }
-
-        // Add JavaScript files
-        if ($pattern === self::JS_REGEX) {
-            foreach ($files as $file) {
-                $this->addJs($file);
-            }
-
-            return $this;
-        }
-
-        // Unknown pattern.
-        foreach ($files as $asset) {
-            $this->add($asset);
-        }
-
-        return $this;
-    }
-
-    /**
-     * Determine whether a link is local or remote.
-     *
-     * Understands both "http://" and "https://" as well as protocol agnostic links "//"
-     *
-     * @param  string $link
-     *
-     * @return bool
-     */
-    protected function isRemoteLink($link)
-    {
-        $base = Grav::instance()['uri']->rootUrl(true);
-
-        // sanity check for local URLs with absolute URL's enabled
-        if (Utils::startsWith($link, $base)) {
-            return false;
-        }
-
-        return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8) || '//' === substr($link, 0,
-                2));
-    }
-
-    /**
-     * Build local links including grav asset shortcodes
-     *
-     * @param  string $asset    the asset string reference
-     * @param  bool   $absolute build absolute asset link
-     *
-     * @return string           the final link url to the asset
-     */
-    protected function buildLocalLink($asset, $absolute = false)
-    {
-        try {
-            $asset = Grav::instance()['locator']->findResource($asset, $absolute);
-        } catch (\Exception $e) {
-        }
-
-        $uri = $absolute ? $asset : $this->base_url . ltrim($asset, '/');
-        return $asset ? $uri : false;
-    }
-
-    /**
-     * Get the last modification time of asset
-     *
-     * @param  string $asset    the asset string reference
-     *
-     * @return string           the last modifcation time or false on error
-     */
-    protected function getLastModificationTime($asset)
-    {
-        $file = GRAV_ROOT . $asset;
-        if (Grav::instance()['locator']->isStream($asset)) {
-            $file = $this->buildLocalLink($asset, true);
-        }
-
-        return file_exists($file) ? filemtime($file) : false;
-    }
-
-    /**
-     * Build an HTML attribute string from an array.
-     *
-     * @param  array $attributes
-     *
-     * @return string
-     */
-    protected function attributes(array $attributes)
-    {
-        $html = '';
-        $no_key = ['loading'];
-
-        foreach ($attributes as $key => $value) {
-            // For numeric keys we will assume that the key and the value are the same
-            // as this will convert HTML attributes such as "required" to a correct
-            // form like required="required" instead of using incorrect numerics.
-            if (is_numeric($key)) {
-                $key = $value;
-            }
-            if (is_array($value)) {
-                $value = implode(' ', $value);
-            }
-
-            if (in_array($key, $no_key)) {
-                $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
-            } else {
-                $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
-            }
-
-            $html .= ' ' . $element;
-        }
-
-        return $html;
-    }
-
-    /**
-     * Download and concatenate the content of several links.
-     *
-     * @param  array $links
-     * @param  bool  $css
-     *
-     * @return string
-     */
-    protected function gatherLinks(array $links, $css = true)
-    {
-        $buffer = '';
-
-
-        foreach ($links as $asset) {
-            $relative_dir = '';
-            $local = true;
-
-            $link = $asset['asset'];
-            $relative_path = $link;
-
-            if ($this->isRemoteLink($link)) {
-                $local = false;
-                if ('//' === substr($link, 0, 2)) {
-                    $link = 'http:' . $link;
-                }
-            } else {
-                // Fix to remove relative dir if grav is in one
-                if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
-                    $base_url = '#' . preg_quote($this->base_url, '#') . '#';
-                    $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
-                }
-
-                $relative_dir = dirname($relative_path);
-                $link = ROOT_DIR . $relative_path;
-            }
-
-            $file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
-
-            // No file found, skip it...
-            if ($file === false) {
-                continue;
-            }
-
-            // Double check last character being
-            if (!$css) {
-                $file = rtrim($file, ' ;') . ';';
-            }
-
-            // If this is CSS + the file is local + rewrite enabled
-            if ($css && $local && $this->css_rewrite) {
-                $file = $this->cssRewrite($file, $relative_dir);
-            }
-
-            $file = rtrim($file) . PHP_EOL;
-            $buffer .= $file;
-        }
-
-        // Pull out @imports and move to top
-        if ($css) {
-            $buffer = $this->moveImports($buffer);
-        }
-
-        return $buffer;
-    }
-
-    /**
-     * Finds relative CSS urls() and rewrites the URL with an absolute one
-     *
-     * @param string $file          the css source file
-     * @param string $relative_path relative path to the css file
-     *
-     * @return mixed
-     */
-    protected function cssRewrite($file, $relative_path)
-    {
-        // Strip any sourcemap comments
-        $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
-
-        // Find any css url() elements, grab the URLs and calculate an absolute path
-        // Then replace the old url with the new one
-        $file = preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($relative_path) {
-
-            $old_url = $matches[2];
-
-            // Ensure link is not rooted to webserver, a data URL, or to a remote host
-            if (Utils::startsWith($old_url, '/') || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
-                return $matches[0];
-            }
-
-            $new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
-
-            return str_replace($old_url, $new_url, $matches[0]);
-        }, $file);
-
-        return $file;
-    }
-
-    /**
-     * Moves @import statements to the top of the file per the CSS specification
-     *
-     * @param  string $file the file containing the combined CSS files
-     *
-     * @return string       the modified file with any @imports at the top of the file
-     */
-    protected function moveImports($file)
-    {
-        $this->imports = [];
-
-        $file = preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) {
-            $this->imports[] = $matches[0];
-
-            return '';
-        }, $file);
-
-        return implode("\n", $this->imports) . "\n\n" . $file;
-    }
-
-    /**
-     * Recursively get files matching $pattern within $directory.
-     *
-     * @param  string $directory
-     * @param  string $pattern (regex)
-     * @param  string $ltrim   Will be trimmed from the left of the file path
-     *
-     * @return array
-     */
-    protected function rglob($directory, $pattern, $ltrim = null)
-    {
-        $iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory,
-            FilesystemIterator::SKIP_DOTS)), $pattern);
-        $offset = strlen($ltrim);
-        $files = [];
-
-        foreach ($iterator as $file) {
-            $files[] = substr($file->getPathname(), $offset);
-        }
-
-        return $files;
-    }
-
-    /**
-     * Sets the state of CSS Pipeline
-     *
-     * @param boolean $value
-     */
-    public function setCssPipeline($value)
-    {
-        $this->css_pipeline = (bool)$value;
-    }
-
-    /**
-     * Sets the state of JS Pipeline
-     *
-     * @param boolean $value
-     */
-    public function setJsPipeline($value)
-    {
-        $this->js_pipeline = (bool)$value;
-    }
-
-    /**
-     * Explicitly set's a timestamp for assets
-     *
-     * @param $value
-     */
-    public function setTimestamp($value)
-    {
-        $this->timestamp = $value;
-    }
-
-    /**
-     * Get the timestamp for assets
-     *
-     * @return string
-     */
-    public function getTimestamp($include_join = true)
-    {
-        if ($this->timestamp) {
-            $timestamp = $include_join ? '?' . $this->timestamp : $this->timestamp;
-            return $timestamp;
-        }
-        return;
-    }
-
-    /**
-     *
-     *
-     * @param $asset
-     * @return string
-     */
-    public function getQuerystring($asset)
-    {
-        $querystring = '';
-
-        if (!empty($asset['query'])) {
-            if (Utils::contains($asset['asset'], '?')) {
-                $querystring .=  '&' . $asset['query'];
-            } else {
-               $querystring .= '?' . $asset['query'];
-            }
-        }
-
-        if ($this->timestamp) {
-            if (Utils::contains($asset['asset'], '?') || $querystring) {
-                $querystring .=  '&' . $this->timestamp;
-            } else {
-                $querystring .= '?' . $this->timestamp;
-            }
-        }
-
-        return $querystring;
-    }
-
-    /**
-     * @return string
-     */
-    public function __toString()
-    {
-        return '';
-    }
-
-    /**
-     * @param $a
-     * @param $b
-     *
-     * @return mixed
-     */
-    protected function sortAssetsByPriorityThenOrder($a, $b)
-    {
-        if ($a['priority'] == $b['priority']) {
-            return $a['order'] - $b['order'];
-        }
-
-        return $b['priority'] - $a['priority'];
-    }
-
 }
 }

+ 198 - 0
system/src/Grav/Common/Assets/BaseAsset.php

@@ -0,0 +1,198 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Assets\Traits\AssetUtilsTrait;
+use Grav\Common\Grav;
+use Grav\Common\Uri;
+use Grav\Common\Utils;
+use Grav\Framework\Object\PropertyObject;
+
+abstract class BaseAsset extends PropertyObject
+{
+    use AssetUtilsTrait;
+
+    protected const CSS_ASSET = true;
+    protected const JS_ASSET = false;
+
+    /** @const Regex to match CSS import content */
+    protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
+
+    protected $asset;
+
+    protected $asset_type;
+    protected $order;
+    protected $group;
+    protected $position;
+    protected $priority;
+    protected $attributes = [];
+
+
+    protected $timestamp;
+    protected $modified;
+    protected $remote;
+    protected $query = '';
+
+    // Private Bits
+    private $base_url;
+    private $fetch_command;
+    private $css_rewrite = false;
+    private $css_minify = false;
+
+    abstract function render();
+
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_config = [
+            'group' => 'head',
+            'position' => 'pipeline',
+            'priority' => 10,
+            'modified' => null,
+            'asset' => null
+        ];
+
+        // Merge base defaults
+        $elements = array_merge($base_config, $elements);
+
+        parent::__construct($elements, $key);
+    }
+
+    public function init($asset, $options)
+    {
+        $config = Grav::instance()['config'];
+        $uri = Grav::instance()['uri'];
+
+        // set attributes
+        foreach ($options as $key => $value) {
+            if ($this->hasProperty($key)) {
+                $this->setProperty($key, $value);
+            } else {
+                $this->attributes[$key] = $value;
+            }
+        }
+
+        // Force priority to be an int
+        $this->priority = (int) $this->priority;
+
+        // Do some special stuff for CSS/JS (not inline)
+        if (!Utils::startsWith($this->getType(), 'inline')) {
+            $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
+            $this->remote = static::isRemoteLink($asset);
+
+            // Move this to render?
+            if (!$this->remote) {
+
+                $asset_parts = parse_url($asset);
+                if (isset($asset_parts['query'])) {
+                    $this->query = $asset_parts['query'];
+                    unset($asset_parts['query']);
+                    $asset = Uri::buildUrl($asset_parts);
+                }
+
+                $locator = Grav::instance()['locator'];
+
+                if ($locator->isStream($asset)) {
+                    $path = $locator->findResource($asset, true);
+                } else {
+                    $path = GRAV_ROOT . $asset;
+                }
+
+                // If local file is missing return
+                if ($path === false) {
+                    return false;
+                }
+
+                $file = new \SplFileInfo($path);
+
+                $asset = $this->buildLocalLink($file->getPathname());
+
+                $this->modified = $file->isFile() ? $file->getMTime() : false;
+            }
+        }
+
+        $this->asset = $asset;
+
+        return $this;
+    }
+
+    public function getAsset()
+    {
+        return $this->asset;
+    }
+
+    public function getRemote()
+    {
+        return $this->remote;
+    }
+
+    public function setPosition($position)
+    {
+        $this->position = $position;
+        return $this;
+    }
+
+
+    /**
+     *
+     * Get the last modification time of asset
+     *
+     * @param  string $asset    the asset string reference
+     *
+     * @return string           the last modifcation time or false on error
+     */
+//    protected function getLastModificationTime($asset)
+//    {
+//        $file = GRAV_ROOT . $asset;
+//        if (Grav::instance()['locator']->isStream($asset)) {
+//            $file = $this->buildLocalLink($asset, true);
+//        }
+//
+//        return file_exists($file) ? filemtime($file) : false;
+//    }
+
+    /**
+     *
+     * Build local links including grav asset shortcodes
+     *
+     * @param  string $asset    the asset string reference
+     *
+     * @return string           the final link url to the asset
+     */
+    protected function buildLocalLink($asset)
+    {
+        if ($asset) {
+            return $this->base_url . ltrim(Utils::replaceFirstOccurrence(GRAV_ROOT, '', $asset), '/');
+        }
+        return false;
+    }
+
+
+    /**
+     * Implements JsonSerializable interface.
+     *
+     * @return array
+     */
+    public function jsonSerialize()
+    {
+        return ['type' => $this->getType(), 'elements' => $this->getElements()];
+    }
+
+    /**
+     * Placeholder for AssetUtilsTrait method
+     *
+     * @param string $file
+     * @param string $dir
+     * @param bool $local
+     */
+    protected function cssRewrite($file, $dir, $local)
+    {
+        return;
+    }
+}

+ 40 - 0
system/src/Grav/Common/Assets/Css.php

@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class Css extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'css',
+            'attributes' => [
+                'type' => 'text/css',
+                'rel' => 'stylesheet'
+            ]
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
+            $buffer = $this->gatherLinks( [$this], self::CSS_ASSET);
+            return "<style>\n" . trim($buffer) . "\n</style>\n";
+        }
+
+        return '<link href="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+    }
+}

+ 32 - 0
system/src/Grav/Common/Assets/InlineCss.php

@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class InlineCss extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'css',
+            'position' => 'after'
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        return '<style' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</style>\n";
+    }
+}

+ 32 - 0
system/src/Grav/Common/Assets/InlineJs.php

@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class InlineJs extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js',
+            'position' => 'after'
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        return '<script' . $this->renderAttributes(). ">\n" . trim($this->asset) . "\n</script>\n";
+    }
+}

+ 36 - 0
system/src/Grav/Common/Assets/Js.php

@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Utils;
+
+class Js extends BaseAsset
+{
+    public function __construct(array $elements = [], $key = null)
+    {
+        $base_options = [
+            'asset_type' => 'js',
+        ];
+
+        $merged_attributes = Utils::arrayMergeRecursiveUnique($base_options, $elements);
+
+        parent::__construct($merged_attributes, $key);
+    }
+
+    public function render()
+    {
+        if (isset($this->attributes['loading']) && $this->attributes['loading'] === 'inline') {
+            $buffer = $this->gatherLinks( [$this], self::JS_ASSET);
+            return '<script' . $this->renderAttributes() . ">\n" . trim($buffer) . "\n</script>\n";
+        }
+
+        return '<script src="' . trim($this->asset) . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+    }
+}

+ 273 - 0
system/src/Grav/Common/Assets/Pipeline.php

@@ -0,0 +1,273 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets;
+
+use Grav\Common\Assets\Traits\AssetUtilsTrait;
+use Grav\Common\Config\Config;
+use Grav\Common\Grav;
+use Grav\Common\Uri;
+use Grav\Common\Utils;
+use Grav\Framework\Object\PropertyObject;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+class Pipeline extends PropertyObject
+{
+    use AssetUtilsTrait;
+
+    protected const CSS_ASSET = true;
+    protected const JS_ASSET = false;
+
+    /** @const Regex to match CSS urls */
+    protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
+
+    /** @const Regex to match CSS sourcemap comments */
+    protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
+
+    /** @const Regex to match CSS import content */
+    protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
+
+    protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
+
+    protected $css_minify;
+    protected $css_minify_windows;
+    protected $css_rewrite;
+
+    protected $js_minify;
+    protected $js_minify_windows;
+
+    protected $base_url;
+    protected $assets_dir;
+    protected $assets_url;
+    protected $timestamp;
+    protected $attributes;
+    protected $query;
+    protected $asset;
+
+    /**
+     * Closure used by the pipeline to fetch assets.
+     *
+     * Useful when file_get_contents() function is not available in your PHP
+     * installation or when you want to apply any kind of preprocessing to
+     * your assets before they get pipelined.
+     *
+     * The closure will receive as the only parameter a string with the path/URL of the asset and
+     * it should return the content of the asset file as a string.
+     *
+     * @var \Closure
+     */
+    protected $fetch_command;
+
+    public function __construct(array $elements = [], ?string $key = null)
+    {
+        parent::__construct($elements, $key);
+
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+
+        /** @var Config $config */
+        $config = Grav::instance()['config'];
+
+        /** @var Uri $uri */
+        $uri = Grav::instance()['uri'];
+
+        $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
+        $this->assets_dir = $locator->findResource('asset://') . DS;
+        $this->assets_url = $locator->findResource('asset://', false);
+    }
+
+    /**
+     * Minify and concatenate CSS
+     *
+     * @param array $assets
+     * @param string $group
+     * @param array $attributes
+     *
+     * @return bool|string     URL or generated content if available, else false
+     */
+    public function renderCss($assets, $group, $attributes = [])
+    {
+        // temporary list of assets to pipeline
+        $inline_group = false;
+
+        if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
+            $inline_group = true;
+            unset($attributes['loading']);
+        }
+
+        // Store Attributes
+        $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes);
+
+        // Compute uid based on assets and timestamp
+        $json_assets = json_encode($assets);
+        $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
+        $file = $uid . '.css';
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+
+        $buffer = null;
+
+        if (file_exists($this->assets_dir . $file)) {
+            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        } else {
+            //if nothing found get out of here!
+            if (empty($assets)) {
+                return false;
+            }
+
+            // Concatenate files
+            $buffer = $this->gatherLinks($assets, self::CSS_ASSET);
+
+            // Minify if required
+            if ($this->shouldMinify('css')) {
+                $minifier = new \MatthiasMullie\Minify\CSS();
+                $minifier->add($buffer);
+                $buffer = $minifier->minify();
+            }
+
+            // Write file
+            if (trim($buffer) !== '') {
+                file_put_contents($this->assets_dir . $file, $buffer);
+            }
+        }
+
+        if ($inline_group) {
+            $output = "<style>\n" . $buffer . "\n</style>\n";
+        } else {
+            $this->asset = $relative_path;
+            $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
+        }
+
+        return $output;
+    }
+
+    /**
+     * Minify and concatenate JS files.
+     *
+     * @param array $assets
+     * @param string $group
+     * @param array $attributes
+     *
+     * @return bool|string     URL or generated content if available, else false
+     */
+    public function renderJs($assets, $group, $attributes = [])
+    {
+        // temporary list of assets to pipeline
+        $inline_group = false;
+
+        if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
+            $inline_group = true;
+            unset($attributes['loading']);
+        }
+
+        // Store Attributes
+        $this->attributes = $attributes;
+
+        // Compute uid based on assets and timestamp
+        $json_assets = json_encode($assets);
+        $uid = md5($json_assets . $this->js_minify . $group);
+        $file = $uid . '.js';
+        $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
+
+        $buffer = null;
+
+        if (file_exists($this->assets_dir . $file)) {
+            $buffer = file_get_contents($this->assets_dir . $file) . "\n";
+        } else {
+            //if nothing found get out of here!
+            if (empty($assets)) {
+                return false;
+            }
+
+            // Concatenate files
+            $buffer = $this->gatherLinks($assets, self::JS_ASSET);
+
+            // Minify if required
+            if ($this->shouldMinify('js')) {
+                $minifier = new \MatthiasMullie\Minify\JS();
+                $minifier->add($buffer);
+                $buffer = $minifier->minify();
+            }
+
+            // Write file
+            if (trim($buffer) !== '') {
+                file_put_contents($this->assets_dir . $file, $buffer);
+            }
+        }
+
+        if ($inline_group) {
+            $output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
+        } else {
+            $this->asset = $relative_path;
+            $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
+        }
+
+        return $output;
+    }
+
+
+    /**
+     * Finds relative CSS urls() and rewrites the URL with an absolute one
+     *
+     * @param string $file the css source file
+     * @param string $dir , $local relative path to the css file
+     * @param bool $local is this a local or remote asset
+     *
+     * @return mixed
+     */
+    protected function cssRewrite($file, $dir, $local)
+    {
+        // Strip any sourcemap comments
+        $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
+
+        // Find any css url() elements, grab the URLs and calculate an absolute path
+        // Then replace the old url with the new one
+        $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) {
+
+            $old_url = $matches[2];
+
+            // Ensure link is not rooted to web server, a data URL, or to a remote host
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
+                return $matches[0];
+            }
+
+            // clean leading /
+            $old_url = Utils::normalizePath($dir . '/' . $old_url);
+            if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
+                $old_url = ltrim($old_url, '/');
+            }
+
+            $new_url = ($local ? $this->base_url: '') . $old_url;
+
+            $fixed = str_replace($matches[2], $new_url, $matches[0]);
+
+            return $fixed;
+        }, $file);
+
+        return $file;
+    }
+
+
+
+    private function shouldMinify($type = 'css')
+    {
+        $check = $type . '_minify';
+        $win_check = $type . '_minify_windows';
+
+        $minify = (bool) $this->$check;
+
+        // If this is a Windows server, and minify_windows is false (default value) skip the
+        // minification process because it will cause Apache to die/crash due to insufficient
+        // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
+        if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) {
+            $minify = false;
+        }
+
+        return $minify;
+    }
+}

+ 182 - 0
system/src/Grav/Common/Assets/Traits/AssetUtilsTrait.php

@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Grav;
+use Grav\Common\Utils;
+
+trait AssetUtilsTrait
+{
+    /**
+     * Determine whether a link is local or remote.
+     * Understands both "http://" and "https://" as well as protocol agnostic links "//"
+     *
+     * @param  string $link
+     * @return bool
+     */
+    public static function isRemoteLink($link)
+    {
+        $base = Grav::instance()['uri']->rootUrl(true);
+
+        // Sanity check for local URLs with absolute URL's enabled
+        if (Utils::startsWith($link, $base)) {
+            return false;
+        }
+
+        return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//'));
+    }
+
+    /**
+     * Download and concatenate the content of several links.
+     *
+     * @param  array $assets
+     * @param  bool $css
+     *
+     * @return string
+     */
+    protected function gatherLinks(array $assets, $css = true)
+    {
+        $buffer = '';
+
+
+        foreach ($assets as $id => $asset) {
+            $local = true;
+
+            $link = $asset->getAsset();
+            $relative_path = $link;
+
+            if (static::isRemoteLink($link)) {
+                $local = false;
+                if (0 === strpos($link, '//')) {
+                    $link = 'http:' . $link;
+                }
+                $relative_dir = \dirname($relative_path);
+            } else {
+                // Fix to remove relative dir if grav is in one
+                if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
+                    $base_url = '#' . preg_quote($this->base_url, '#') . '#';
+                    $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
+                }
+
+                $relative_dir = \dirname($relative_path);
+                $link = ROOT_DIR . $relative_path;
+            }
+
+            $file = ($this->fetch_command instanceof \Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
+
+            // No file found, skip it...
+            if ($file === false) {
+                continue;
+            }
+
+            // Double check last character being
+            if (!$css) {
+                $file = rtrim($file, ' ;') . ';';
+            }
+
+            // If this is CSS + the file is local + rewrite enabled
+            if ($css && $this->css_rewrite) {
+                $file = $this->cssRewrite($file, $relative_dir, $local);
+            }
+
+            $file = rtrim($file) . PHP_EOL;
+            $buffer .= $file;
+        }
+
+        // Pull out @imports and move to top
+        if ($css) {
+            $buffer = $this->moveImports($buffer);
+        }
+
+        return $buffer;
+    }
+
+    /**
+     * Moves @import statements to the top of the file per the CSS specification
+     *
+     * @param  string $file the file containing the combined CSS files
+     *
+     * @return string       the modified file with any @imports at the top of the file
+     */
+    protected function moveImports($file)
+    {
+        $imports = [];
+
+        $file = (string)preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) use (&$imports) {
+            $imports[] = $matches[0];
+
+            return '';
+        }, $file);
+
+        return implode("\n", $imports) . "\n\n" . $file;
+    }
+
+    /**
+     *
+     * Build an HTML attribute string from an array.
+     *
+     * @return string
+     */
+    protected function renderAttributes()
+    {
+        $html = '';
+        $no_key = ['loading'];
+
+        foreach ($this->attributes as $key => $value) {
+            if (is_numeric($key)) {
+                $key = $value;
+            }
+            if (\is_array($value)) {
+                $value = implode(' ', $value);
+            }
+
+            if (\in_array($key, $no_key, true)) {
+                $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
+            } else {
+                $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
+            }
+
+            $html .= ' ' . $element;
+        }
+
+        return $html;
+    }
+
+    /**
+     * Render Querystring
+     *
+     * @param string $asset
+     * @return string
+     */
+    protected function renderQueryString($asset = null)
+    {
+        $querystring = '';
+
+        $asset = $asset ?? $this->asset;
+
+        if (!empty($this->query)) {
+            if (Utils::contains($asset, '?')) {
+                $querystring .=  '&' . $this->query;
+            } else {
+                $querystring .= '?' . $this->query;
+            }
+        }
+
+        if ($this->timestamp) {
+            if (Utils::contains($asset, '?') || $querystring) {
+                $querystring .=  '&' . $this->timestamp;
+            } else {
+                $querystring .= '?' . $this->timestamp;
+            }
+        }
+
+        return $querystring;
+    }
+}

+ 125 - 0
system/src/Grav/Common/Assets/Traits/LegacyAssetsTrait.php

@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Assets;
+
+trait LegacyAssetsTrait
+{
+
+    /**
+     * @param array $args
+     * @param string $type
+     * @return array
+     */
+    protected function unifyLegacyArguments($args, $type = Assets::CSS_TYPE)
+    {
+        // First argument is always the asset
+        array_shift($args);
+
+        if (count($args) === 0) {
+            return [];
+        }
+        // New options array format
+        if (count($args) === 1 && is_array($args[0])) {
+            return $args[0];
+        }
+        // Handle obscure case where options array is mixed with a priority
+        if (count($args) === 2 && is_array($args[0]) && is_int($args[1])) {
+            $arguments = $args[0];
+            $arguments['priority'] = $args[1];
+            return $arguments;
+        }
+
+        switch ($type) {
+            case(Assets::JS_TYPE):
+                $defaults = ['priority' => null, 'pipeline' => true, 'loading' => null, 'group' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+                break;
+
+            case(Assets::INLINE_JS_TYPE):
+                $defaults = ['priority' => null, 'group' => null, 'attributes' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+
+                // special case to handle old attributes being passed in
+                if (isset($arguments['attributes'])) {
+                    $old_attributes = $arguments['attributes'];
+                    $arguments = array_merge($arguments, $old_attributes);
+                }
+                unset($arguments['attributes']);
+
+                break;
+
+            case(Assets::INLINE_CSS_TYPE):
+                $defaults = ['priority' => null, 'group' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+                break;
+
+            default:
+            case(Assets::CSS_TYPE):
+                $defaults = ['priority' => null, 'pipeline' => true, 'group' => null, 'loading' => null];
+                $arguments = $this->createArgumentsFromLegacy($args, $defaults);
+        }
+
+        return $arguments;
+    }
+
+    protected function createArgumentsFromLegacy(array $args, array $defaults)
+    {
+        // Remove arguments with old default values.
+        $arguments = [];
+        foreach ($args as $arg) {
+            $default = current($defaults);
+            if ($arg !== $default) {
+                $arguments[key($defaults)] = $arg;
+            }
+            next($defaults);
+        }
+
+        return $arguments;
+    }
+
+    /**
+     * Convenience wrapper for async loading of JavaScript
+     *
+     * @param string|array  $asset
+     * @param int           $priority
+     * @param bool          $pipeline
+     * @param string        $group name of the group
+     *
+     * @return \Grav\Common\Assets
+     * @deprecated Please use dynamic method with ['loading' => 'async'].
+     */
+    public function addAsyncJs($asset, $priority = 10, $pipeline = true, $group = 'head')
+    {
+        user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'async\']', E_USER_DEPRECATED);
+
+        return $this->addJs($asset, $priority, $pipeline, 'async', $group);
+    }
+
+    /**
+     * Convenience wrapper for deferred loading of JavaScript
+     *
+     * @param string|array  $asset
+     * @param int           $priority
+     * @param bool          $pipeline
+     * @param string        $group name of the group
+     *
+     * @return \Grav\Common\Assets
+     * @deprecated Please use dynamic method with ['loading' => 'defer'].
+     */
+    public function addDeferJs($asset, $priority = 10, $pipeline = true, $group = 'head')
+    {
+        user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use dynamic method with [\'loading\' => \'defer\']', E_USER_DEPRECATED);
+
+        return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
+    }
+
+}

+ 343 - 0
system/src/Grav/Common/Assets/Traits/TestingAssetsTrait.php

@@ -0,0 +1,343 @@
+<?php
+
+/**
+ * @package    Grav\Common\Assets\Traits
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Assets\Traits;
+
+use Grav\Common\Grav;
+
+trait TestingAssetsTrait
+{
+    /**
+     * Determines if an asset exists as a collection, CSS or JS reference
+     *
+     * @param string $asset
+     *
+     * @return bool
+     */
+    public function exists($asset)
+    {
+        return isset($this->collections[$asset]) || isset($this->assets_css[$asset]) || isset($this->assets_js[$asset]);
+    }
+
+    /**
+     * Return the array of all the registered collections
+     *
+     * @return array
+     */
+    public function getCollections()
+    {
+        return $this->collections;
+    }
+
+    /**
+     * Set the array of collections explicitly
+     *
+     * @param array $collections
+     *
+     * @return $this
+     */
+    public function setCollection($collections)
+    {
+        $this->collections = $collections;
+
+        return $this;
+    }
+
+    /**
+     * Return the array of all the registered CSS assets
+     * If a $key is provided, it will try to return only that asset
+     * else it will return null
+     *
+     * @param null|string $key the asset key
+     * @return array
+     */
+    public function getCss($key = null)
+    {
+        if (null !== $key) {
+            $asset_key = md5($key);
+
+            return $this->assets_css[$asset_key] ?? null;
+        }
+
+        return $this->assets_css;
+    }
+
+    /**
+     * Return the array of all the registered JS assets
+     * If a $key is provided, it will try to return only that asset
+     * else it will return null
+     *
+     * @param null|string $key the asset key
+     * @return array
+     */
+    public function getJs($key = null)
+    {
+        if (null !== $key) {
+            $asset_key = md5($key);
+
+            return $this->assets_js[$asset_key] ?? null;
+        }
+
+        return $this->assets_js;
+    }
+
+    /**
+     * Set the whole array of CSS assets
+     *
+     * @param array $css
+     *
+     * @return $this
+     */
+    public function setCss($css)
+    {
+        $this->assets_css = $css;
+
+        return $this;
+    }
+
+    /**
+     * Set the whole array of JS assets
+     *
+     * @param array $js
+     *
+     * @return $this
+     */
+    public function setJs($js)
+    {
+        $this->assets_js = $js;
+
+        return $this;
+    }
+
+    /**
+     * Removes an item from the CSS array if set
+     *
+     * @param string $key  The asset key
+     *
+     * @return $this
+     */
+    public function removeCss($key)
+    {
+        $asset_key = md5($key);
+        if (isset($this->assets_css[$asset_key])) {
+            unset($this->assets_css[$asset_key]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Removes an item from the JS array if set
+     *
+     * @param string $key  The asset key
+     *
+     * @return $this
+     */
+    public function removeJs($key)
+    {
+        $asset_key = md5($key);
+        if (isset($this->assets_js[$asset_key])) {
+            unset($this->assets_js[$asset_key]);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Sets the state of CSS Pipeline
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setCssPipeline($value)
+    {
+        $this->css_pipeline = (bool)$value;
+
+        return $this;
+    }
+
+    /**
+     * Sets the state of JS Pipeline
+     *
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setJsPipeline($value)
+    {
+        $this->js_pipeline = (bool)$value;
+
+        return $this;
+    }
+
+    /**
+     * Reset all assets.
+     *
+     * @return $this
+     */
+    public function reset()
+    {
+        $this->resetCss();
+        $this->resetJs();
+        $this->setCssPipeline(false);
+        $this->setJsPipeline(false);
+
+        return $this;
+    }
+
+    /**
+     * Reset JavaScript assets.
+     *
+     * @return $this
+     */
+    public function resetJs()
+    {
+        $this->assets_js = [];
+
+        return $this;
+    }
+
+    /**
+     * Reset CSS assets.
+     *
+     * @return $this
+     */
+    public function resetCss()
+    {
+        $this->assets_css = [];
+
+        return $this;
+    }
+
+    /**
+     * Explicitly set's a timestamp for assets
+     *
+     * @param string|int $value
+     */
+    public function setTimestamp($value)
+    {
+        $this->timestamp = $value;
+    }
+
+    /**
+     * Get the timestamp for assets
+     *
+     * @param  bool  $include_join
+     * @return string
+     */
+    public function getTimestamp($include_join = true)
+    {
+        if ($this->timestamp) {
+            return $include_join ? '?' . $this->timestamp : $this->timestamp;
+        }
+
+        return null;
+    }
+
+    /**
+     * Add all assets matching $pattern within $directory.
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     * @param  string $pattern   (regex)
+     *
+     * @return $this
+     */
+    public function addDir($directory, $pattern = self::DEFAULT_REGEX)
+    {
+        $root_dir = rtrim(ROOT_DIR, '/');
+
+        // Check if $directory is a stream.
+        if (strpos($directory, '://')) {
+            $directory = Grav::instance()['locator']->findResource($directory, null);
+        }
+
+        // Get files
+        $files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
+
+        // No luck? Nothing to do
+        if (!$files) {
+            return $this;
+        }
+
+        // Add CSS files
+        if ($pattern === self::CSS_REGEX) {
+            foreach ($files as $file) {
+                $this->addCss($file);
+            }
+
+            return $this;
+        }
+
+        // Add JavaScript files
+        if ($pattern === self::JS_REGEX) {
+            foreach ($files as $file) {
+                $this->addJs($file);
+            }
+
+            return $this;
+        }
+
+        // Unknown pattern.
+        foreach ($files as $asset) {
+            $this->add($asset);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Add all JavaScript assets within $directory
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     *
+     * @return $this
+     */
+    public function addDirJs($directory)
+    {
+        return $this->addDir($directory, self::JS_REGEX);
+    }
+
+    /**
+     * Add all CSS assets within $directory
+     *
+     * @param  string $directory Relative to the Grav root path, or a stream identifier
+     *
+     * @return $this
+     */
+    public function addDirCss($directory)
+    {
+        return $this->addDir($directory, self::CSS_REGEX);
+    }
+
+    /**
+     * Recursively get files matching $pattern within $directory.
+     *
+     * @param  string $directory
+     * @param  string $pattern (regex)
+     * @param  string $ltrim   Will be trimmed from the left of the file path
+     *
+     * @return array
+     */
+    protected function rglob($directory, $pattern, $ltrim = null)
+    {
+        $iterator = new \RegexIterator(new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($directory,
+            \FilesystemIterator::SKIP_DOTS)), $pattern);
+        $offset = \strlen($ltrim);
+        $files = [];
+
+        foreach ($iterator as $file) {
+            $files[] = substr($file->getPathname(), $offset);
+        }
+
+        return $files;
+    }
+
+
+}

+ 261 - 0
system/src/Grav/Common/Backup/Backups.php

@@ -0,0 +1,261 @@
+<?php
+
+/**
+ * @package    Grav\Common\Backup
+ *
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
+ * @license    MIT License; see LICENSE file for details.
+ */
+
+namespace Grav\Common\Backup;
+
+use Grav\Common\Filesystem\Archiver;
+use Grav\Common\Filesystem\Folder;
+use Grav\Common\Inflector;
+use Grav\Common\Scheduler\Job;
+use Grav\Common\Scheduler\Scheduler;
+use Grav\Common\Utils;
+use Grav\Common\Grav;
+use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Event\EventDispatcher;
+use RocketTheme\Toolbox\File\JsonFile;
+use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
+
+class Backups
+{
+    protected const BACKUP_FILENAME_REGEXZ = "#(.*)--(\d*).zip#";
+
+    protected const BACKUP_DATE_FORMAT = 'YmdHis';
+
+    protected static $backup_dir;
+
+    protected static $backups = null;
+
+    public function init()
+    {
+        /** @var EventDispatcher $dispatcher */
+        $dispatcher = Grav::instance()['events'];
+        $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
+        Grav::instance()->fireEvent('onBackupsInitialized', new Event(['backups' => $this]));
+    }
+
+    public function setup()
+    {
+        if (null === static::$backup_dir) {
+            static::$backup_dir = Grav::instance()['locator']->findResource('backup://', true, true);
+            Folder::create(static::$backup_dir);
+        }
+    }
+
+    public function onSchedulerInitialized(Event $event)
+    {
+        /** @var Scheduler $scheduler */
+        $scheduler = $event['scheduler'];
+
+        /** @var Inflector $inflector */
+        $inflector = Grav::instance()['inflector'];
+
+        foreach (static::getBackupProfiles() as $id => $profile) {
+            $at = $profile['schedule_at'];
+            $name = $inflector::hyphenize($profile['name']);
+            $logs = 'logs/backup-' . $name . '.out';
+            /** @var Job $job */
+            $job = $scheduler->addFunction('Grav\Common\Backup\Backups::backup', [$id], $name );
+            $job->at($at);
+            $job->output($logs);
+            $job->backlink('/tools/backups');
+        }
+    }
+
+    public function getBackupDownloadUrl($backup, $base_url)
+    {
+        $param_sep = $param_sep = Grav::instance()['config']->get('system.param_sep', ':');
+        $download = urlencode(base64_encode($backup));
+        $url      = rtrim(Grav::instance()['uri']->rootUrl(true), '/') . '/' . trim($base_url,
+                '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
+
+        return $url;
+    }
+
+    public static function getBackupProfiles()
+    {
+        return Grav::instance()['config']->get('backups.profiles');
+    }
+
+    public static function getPurgeConfig()
+    {
+        return Grav::instance()['config']->get('backups.purge');
+    }
+
+    public function getBackupNames()
+    {
+        return array_column(static::getBackupProfiles(), 'name');
+    }
+
+    public static function getTotalBackupsSize()
+    {
+        $backups = static::getAvailableBackups();
+        $size = array_sum(array_column($backups, 'size'));
+
+        return $size ?? 0;
+    }
+
+    public static function getAvailableBackups($force = false)
+    {
+        if ($force || null === static::$backups) {
+            static::$backups = [];
+            $backups_itr = new \GlobIterator(static::$backup_dir . '/*.zip', \FilesystemIterator::KEY_AS_FILENAME);
+            $inflector = Grav::instance()['inflector'];
+            $long_date_format = DATE_RFC2822;
+
+            /**
+             * @var string $name
+             * @var \SplFileInfo $file
+             */
+            foreach ($backups_itr as $name => $file) {
+
+                if (preg_match(static::BACKUP_FILENAME_REGEXZ, $name, $matches)) {
+                    $date = \DateTime::createFromFormat(static::BACKUP_DATE_FORMAT, $matches[2]);
+                    $timestamp = $date->getTimestamp();
+                    $backup = new \stdClass();
+                    $backup->title = $inflector->titleize($matches[1]);
+                    $backup->time = $date;
+                    $backup->date = $date->format($long_date_format);
+                    $backup->filename = $name;
+                    $backup->path = $file->getPathname();
+                    $backup->size = $file->getSize();
+                    static::$backups[$timestamp] = $backup;
+                }
+            }
+            // Reverse Key Sort to get in reverse date order
+            krsort(static::$backups);
+        }
+
+        return static::$backups;
+    }
+
+    /**
+     * Backup
+     *
+     * @param int   $id
+     * @param callable|null $status
+     *
+     * @return null|string
+     */
+    public static function backup($id = 0, callable $status = null)
+    {
+        $profiles = static::getBackupProfiles();
+        /** @var UniformResourceLocator $locator */
+        $locator = Grav::instance()['locator'];
+
+        if (isset($profiles[$id])) {
+            $backup = (object) $profiles[$id];
+        } else {
+            throw new \RuntimeException('No backups defined...');
+        }
+
+        $name = Grav::instance()['inflector']->underscorize($backup->name);
+        $date = date(static::BACKUP_DATE_FORMAT, time());
+        $filename = trim($name, '_') . '--' . $date . '.zip';
+        $destination = static::$backup_dir . DS . $filename;
+        $max_execution_time = ini_set('max_execution_time', 600);
+        $backup_root = $backup->root;
+
+        if ($locator->isStream($backup_root)) {
+            $backup_root = $locator->findResource($backup_root);
+        } else {
+            $backup_root = rtrim(GRAV_ROOT . $backup_root, '/');
+        }
+
+        if (!file_exists($backup_root)) {
+            throw new \RuntimeException("Backup location: {$backup_root} does not exist...");
+        }
+
+        $options = [
+            'exclude_files' => static::convertExclude($backup->exclude_files ?? ''),
+            'exclude_paths' => static::convertExclude($backup->exclude_paths ?? ''),
+        ];
+
+        /** @var Archiver $archiver */
+        $archiver = Archiver::create('zip');
+        $archiver->setArchive($destination)->setOptions($options)->compress($backup_root, $status)->addEmptyFolders($options['exclude_paths'], $status);
+
+        $status && $status([
+            'type' => 'message',
+            'message' => 'Done...',
+        ]);
+
+        $status && $status([
+            'type' => 'progress',
+            'complete' => true
+        ]);
+
+        if ($max_execution_time !== false) {
+            ini_set('max_execution_time', $max_execution_time);
+        }
+
+        // Log the backup
+        Grav::instance()['log']->notice('Backup Created: ' . $destination);
+
+        // Fire Finished event
+        Grav::instance()->fireEvent('onBackupFinished', new Event(['backup' => $destination]));
+
+        // Purge anything required
+        static::purge();
+
+        // Log
+        $log = JsonFile::instance(Grav::instance()['locator']->findResource("log://backup.log", true, true));
+        $log->content([
+            'time'     => time(),
+            'location' => $destination
+        ]);
+        $log->save();
+
+        return $destination;
+    }
+
+    public static function purge()
+    {
+        $purge_config = static::getPurgeConfig();
+        $trigger = $purge_config['trigger'];
+        $backups = static::getAvailableBackups(true);
+
+        switch ($trigger)
+        {
+            case 'number':
+                $backups_count = count($backups);
+                if ($backups_count > $purge_config['max_backups_count']) {
+                    $last = end($backups);
+                    unlink ($last->path);
+                    static::purge();
+                }
+                break;
+
+            case 'time':
+                $last = end($backups);
+                $now = new \DateTime();
+                $interval = $now->diff($last->time);
+                if ($interval->days > $purge_config['max_backups_time']) {
+                    unlink($last->path);
+                    static::purge();
+                }
+                break;
+
+            default:
+                $used_space = static::getTotalBackupsSize();
+                $max_space = $purge_config['max_backups_space'] * 1024 * 1024 *  1024;
+                if ($used_space > $max_space) {
+                    $last = end($backups);
+                    unlink($last->path);
+                    static::purge();
+                }
+                break;
+        }
+    }
+
+    protected static function convertExclude($exclude)
+    {
+        $lines = preg_split("/[\s,]+/", $exclude);
+        return array_map('trim', $lines, array_fill(0, \count($lines), '/'));
+    }
+}

+ 0 - 144
system/src/Grav/Common/Backup/ZipBackup.php

@@ -1,144 +0,0 @@
-<?php
-/**
- * @package    Grav.Common.Backup
- *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
- * @license    MIT License; see LICENSE file for details.
- */
-
-namespace Grav\Common\Backup;
-
-use Grav\Common\Grav;
-use Grav\Common\Inflector;
-
-class ZipBackup
-{
-    protected static $ignorePaths = [
-        'backup',
-        'cache',
-        'images',
-        'logs',
-        'tmp'
-    ];
-
-    protected static $ignoreFolders = [
-        '.git',
-        '.svn',
-        '.hg',
-        '.idea',
-        'node_modules'
-    ];
-
-    /**
-     * Backup
-     *
-     * @param string|null   $destination
-     * @param callable|null $messager
-     *
-     * @return null|string
-     */
-    public static function backup($destination = null, callable $messager = null)
-    {
-        if (!$destination) {
-            $destination = Grav::instance()['locator']->findResource('backup://', true);
-
-            if (!$destination) {
-                throw new \RuntimeException('The backup folder is missing.');
-            }
-        }
-
-        $name = substr(strip_tags(Grav::instance()['config']->get('site.title', basename(GRAV_ROOT))), 0, 20);
-
-        $inflector = new Inflector();
-
-        if (is_dir($destination)) {
-            $date = date('YmdHis', time());
-            $filename = trim($inflector->hyphenize($name), '-') . '-' . $date . '.zip';
-            $destination = rtrim($destination, DS) . DS . $filename;
-        }
-
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => 'Creating new Backup "' . $destination . '"'
-        ]);
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => ''
-        ]);
-
-        $zip = new \ZipArchive();
-        $zip->open($destination, \ZipArchive::CREATE);
-
-        $max_execution_time = ini_set('max_execution_time', 600);
-
-        static::folderToZip(GRAV_ROOT, $zip, strlen(rtrim(GRAV_ROOT, DS) . DS), $messager);
-
-        $messager && $messager([
-            'type' => 'progress',
-            'percentage' => false,
-            'complete' => true
-        ]);
-
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => ''
-        ]);
-        $messager && $messager([
-            'type' => 'message',
-            'level' => 'info',
-            'message' => 'Saving and compressing archive...'
-        ]);
-
-        $zip->close();
-
-        if ($max_execution_time !== false) {
-            ini_set('max_execution_time', $max_execution_time);
-        }
-
-        return $destination;
-    }
-
-    /**
-     * @param $folder
-     * @param $zipFile
-     * @param $exclusiveLength
-     * @param $messager
-     */
-    private static function folderToZip($folder, \ZipArchive $zipFile, $exclusiveLength, callable $messager = null)
-    {
-        $handle = opendir($folder);
-        while (false !== $f = readdir($handle)) {
-            if ($f !== '.' && $f !== '..') {
-                $filePath = "$folder/$f";
-                // Remove prefix from file path before add to zip.
-                $localPath = substr($filePath, $exclusiveLength);
-
-                if (in_array($f, static::$ignoreFolders)) {
-                    continue;
-                }
-                if (in_array($localPath, static::$ignorePaths)) {
-                    $zipFile->addEmptyDir($f);
-                    continue;
-                }
-
-                if (is_file($filePath)) {
-                    $zipFile->addFile($filePath, $localPath);
-
-                    $messager && $messager([
-                        'type' => 'progress',
-                        'percentage' => false,
-                        'complete' => false
-                    ]);
-                } elseif (is_dir($filePath)) {
-                    // Add sub-directory.
-                    $zipFile->addEmptyDir($localPath);
-                    static::folderToZip($filePath, $zipFile, $exclusiveLength, $messager);
-                }
-            }
-        }
-        closedir($handle);
-    }
-}

+ 15 - 3
system/src/Grav/Common/Browser.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -113,7 +114,7 @@ class Browser
     {
     {
         $version = explode('.', $this->getLongVersion());
         $version = explode('.', $this->getLongVersion());
 
 
-        return intval($version[0]);
+        return (int)$version[0];
     }
     }
 
 
     /**
     /**
@@ -134,4 +135,15 @@ class Browser
 
 
         return true;
         return true;
     }
     }
+    
+    /**
+     * Determine if “Do Not Track” is set by browser
+     * @see https://www.w3.org/TR/tracking-dnt/
+     *
+     * @return bool
+     */
+    public function isTrackable(): bool
+    {
+        return !(isset($_SERVER['HTTP_DNT']) && $_SERVER['HTTP_DNT'] === '1');
+    }
 }
 }

+ 208 - 62
system/src/Grav/Common/Cache.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -11,15 +12,16 @@ namespace Grav\Common;
 use \Doctrine\Common\Cache as DoctrineCache;
 use \Doctrine\Common\Cache as DoctrineCache;
 use Grav\Common\Config\Config;
 use Grav\Common\Config\Config;
 use Grav\Common\Filesystem\Folder;
 use Grav\Common\Filesystem\Folder;
+use Grav\Common\Scheduler\Scheduler;
+use Psr\SimpleCache\CacheInterface;
 use RocketTheme\Toolbox\Event\Event;
 use RocketTheme\Toolbox\Event\Event;
+use RocketTheme\Toolbox\Event\EventDispatcher;
 
 
 /**
 /**
  * The GravCache object is used throughout Grav to store and retrieve cached data.
  * The GravCache object is used throughout Grav to store and retrieve cached data.
  * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
  * It uses DoctrineCache library and supports a variety of caching mechanisms. Those include:
  *
  *
  * APCu
  * APCu
- * APC
- * XCache
  * RedisCache
  * RedisCache
  * MemCache
  * MemCache
  * MemCacheD
  * MemCacheD
@@ -43,6 +45,11 @@ class Cache extends Getters
      */
      */
     protected $driver;
     protected $driver;
 
 
+    /**
+     * @var CacheInterface
+     */
+    protected $simpleCache;
+
     protected $driver_name;
     protected $driver_name;
 
 
     protected $driver_setting;
     protected $driver_setting;
@@ -117,37 +124,77 @@ class Cache extends Getters
         $this->config = $grav['config'];
         $this->config = $grav['config'];
         $this->now = time();
         $this->now = time();
 
 
-        $this->cache_dir = $grav['locator']->findResource('cache://doctrine', true, true);
+        if (null === $this->enabled) {
+            $this->enabled = (bool)$this->config->get('system.cache.enabled');
+        }
 
 
         /** @var Uri $uri */
         /** @var Uri $uri */
         $uri = $grav['uri'];
         $uri = $grav['uri'];
 
 
         $prefix = $this->config->get('system.cache.prefix');
         $prefix = $this->config->get('system.cache.prefix');
+        $uniqueness = substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION), 2, 8);
 
 
-        if (is_null($this->enabled)) {
-            $this->enabled = (bool)$this->config->get('system.cache.enabled');
+        // Cache key allows us to invalidate all cache on configuration changes.
+        $this->key = ($prefix ? $prefix : 'g') . '-' . $uniqueness;
+        $this->cache_dir = $grav['locator']->findResource('cache://doctrine/' . $uniqueness, true, true);
+        $this->driver_setting = $this->config->get('system.cache.driver');
+        $this->driver = $this->getCacheDriver();
+        $this->driver->setNamespace($this->key);
+
+        /** @var EventDispatcher $dispatcher */
+        $dispatcher = Grav::instance()['events'];
+        $dispatcher->addListener('onSchedulerInitialized', [$this, 'onSchedulerInitialized']);
+    }
+
+    /**
+     * @return CacheInterface
+     */
+    public function getSimpleCache()
+    {
+        if (null === $this->simpleCache) {
+            $cache = new \Grav\Framework\Cache\Adapter\DoctrineCache($this->driver, '', $this->getLifetime());
+
+            // Disable cache key validation.
+            $cache->setValidation(false);
+
+            $this->simpleCache = $cache;
         }
         }
 
 
-        // Cache key allows us to invalidate all cache on configuration changes.
-        $this->key = ($prefix ? $prefix : 'g') . '-' . substr(md5($uri->rootUrl(true) . $this->config->key() . GRAV_VERSION),
-                2, 8);
+        return $this->simpleCache;
+    }
 
 
-        $this->driver_setting = $this->config->get('system.cache.driver');
+    /**
+     * Deletes the old out of date file-based caches
+     *
+     * @return int
+     */
+    public function purgeOldCache()
+    {
+        $cache_dir = dirname($this->cache_dir);
+        $current = basename($this->cache_dir);
+        $count = 0;
+
+        foreach (new \DirectoryIterator($cache_dir) as $file) {
+            $dir = $file->getBasename();
+            if ($dir === $current || $file->isDot() || $file->isFile()) {
+                continue;
+            }
 
 
-        $this->driver = $this->getCacheDriver();
+            Folder::delete($file->getPathname());
+            $count++;
+        }
 
 
-        // Set the cache namespace to our unique key
-        $this->driver->setNamespace($this->key);
+        return $count;
     }
     }
 
 
     /**
     /**
      * Public accessor to set the enabled state of the cache
      * Public accessor to set the enabled state of the cache
      *
      *
-     * @param $enabled
+     * @param bool|int $enabled
      */
      */
     public function setEnabled($enabled)
     public function setEnabled($enabled)
     {
     {
-        $this->enabled = (bool) $enabled;
+        $this->enabled = (bool)$enabled;
     }
     }
 
 
     /**
     /**
@@ -184,19 +231,15 @@ class Cache extends Getters
 
 
         // CLI compatibility requires a non-volatile cache driver
         // CLI compatibility requires a non-volatile cache driver
         if ($this->config->get('system.cache.cli_compatibility') && (
         if ($this->config->get('system.cache.cli_compatibility') && (
-            $setting == 'auto' || $this->isVolatileDriver($setting))) {
+            $setting === 'auto' || $this->isVolatileDriver($setting))) {
             $setting = $driver_name;
             $setting = $driver_name;
         }
         }
 
 
-        if (!$setting || $setting == 'auto') {
+        if (!$setting || $setting === 'auto') {
             if (extension_loaded('apcu')) {
             if (extension_loaded('apcu')) {
                 $driver_name = 'apcu';
                 $driver_name = 'apcu';
-            } elseif (extension_loaded('apc')) {
-                $driver_name = 'apc';
             } elseif (extension_loaded('wincache')) {
             } elseif (extension_loaded('wincache')) {
                 $driver_name = 'wincache';
                 $driver_name = 'wincache';
-            } elseif (extension_loaded('xcache')) {
-                $driver_name = 'xcache';
             }
             }
         } else {
         } else {
             $driver_name = $setting;
             $driver_name = $setting;
@@ -206,9 +249,6 @@ class Cache extends Getters
 
 
         switch ($driver_name) {
         switch ($driver_name) {
             case 'apc':
             case 'apc':
-                $driver = new DoctrineCache\ApcCache();
-                break;
-
             case 'apcu':
             case 'apcu':
                 $driver = new DoctrineCache\ApcuCache();
                 $driver = new DoctrineCache\ApcuCache();
                 break;
                 break;
@@ -217,45 +257,53 @@ class Cache extends Getters
                 $driver = new DoctrineCache\WinCacheCache();
                 $driver = new DoctrineCache\WinCacheCache();
                 break;
                 break;
 
 
-            case 'xcache':
-                $driver = new DoctrineCache\XcacheCache();
-                break;
-
             case 'memcache':
             case 'memcache':
-                $memcache = new \Memcache();
-                $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
-                    $this->config->get('system.cache.memcache.port', 11211));
-                $driver = new DoctrineCache\MemcacheCache();
-                $driver->setMemcache($memcache);
+                if (extension_loaded('memcache')) {
+                    $memcache = new \Memcache();
+                    $memcache->connect($this->config->get('system.cache.memcache.server', 'localhost'),
+                        $this->config->get('system.cache.memcache.port', 11211));
+                    $driver = new DoctrineCache\MemcacheCache();
+                    $driver->setMemcache($memcache);
+                } else {
+                    throw new \LogicException('Memcache PHP extension has not been installed');
+                }
                 break;
                 break;
 
 
             case 'memcached':
             case 'memcached':
-                $memcached = new \Memcached();
-                $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
-                    $this->config->get('system.cache.memcached.port', 11211));
-                $driver = new DoctrineCache\MemcachedCache();
-                $driver->setMemcached($memcached);
+                if (extension_loaded('memcached')) {
+                    $memcached = new \Memcached();
+                    $memcached->addServer($this->config->get('system.cache.memcached.server', 'localhost'),
+                        $this->config->get('system.cache.memcached.port', 11211));
+                    $driver = new DoctrineCache\MemcachedCache();
+                    $driver->setMemcached($memcached);
+                } else {
+                    throw new \LogicException('Memcached PHP extension has not been installed');
+                }
                 break;
                 break;
 
 
             case 'redis':
             case 'redis':
-                $redis = new \Redis();
-                $socket = $this->config->get('system.cache.redis.socket', false);
-                $password = $this->config->get('system.cache.redis.password', false);
+                if (extension_loaded('redis')) {
+                    $redis = new \Redis();
+                    $socket = $this->config->get('system.cache.redis.socket', false);
+                    $password = $this->config->get('system.cache.redis.password', false);
+
+                    if ($socket) {
+                        $redis->connect($socket);
+                    } else {
+                        $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
+                            $this->config->get('system.cache.redis.port', 6379));
+                    }
 
 
-                if ($socket) {
-                    $redis->connect($socket);
-                } else {
-                    $redis->connect($this->config->get('system.cache.redis.server', 'localhost'),
-                    $this->config->get('system.cache.redis.port', 6379));
-                }
+                    // Authenticate with password if set
+                    if ($password && !$redis->auth($password)) {
+                        throw new \RedisException('Redis authentication failed');
+                    }
 
 
-                // Authenticate with password if set
-                if ($password && !$redis->auth($password)) {
-                    throw new \RedisException('Redis authentication failed');
+                    $driver = new DoctrineCache\RedisCache();
+                    $driver->setRedis($redis);
+                } else {
+                    throw new \LogicException('Redis PHP extension has not been installed');
                 }
                 }
-
-                $driver = new DoctrineCache\RedisCache();
-                $driver->setRedis($redis);
                 break;
                 break;
 
 
             default:
             default:
@@ -277,9 +325,9 @@ class Cache extends Getters
     {
     {
         if ($this->enabled) {
         if ($this->enabled) {
             return $this->driver->fetch($id);
             return $this->driver->fetch($id);
-        } else {
-            return false;
         }
         }
+
+        return false;
     }
     }
 
 
     /**
     /**
@@ -310,6 +358,21 @@ class Cache extends Getters
         if ($this->enabled) {
         if ($this->enabled) {
             return $this->driver->delete($id);
             return $this->driver->delete($id);
         }
         }
+
+        return false;
+    }
+
+    /**
+     * Deletes all cache
+     *
+     * @return bool
+     */
+    public function deleteAll()
+    {
+        if ($this->enabled) {
+            return $this->driver->deleteAll();
+        }
+
         return false;
         return false;
     }
     }
 
 
@@ -324,6 +387,7 @@ class Cache extends Getters
         if ($this->enabled) {
         if ($this->enabled) {
             return $this->driver->contains(($id));
             return $this->driver->contains(($id));
         }
         }
+
         return false;
         return false;
     }
     }
 
 
@@ -373,6 +437,9 @@ class Cache extends Getters
             case 'tmp-only':
             case 'tmp-only':
                 $remove_paths = self::$tmp_remove;
                 $remove_paths = self::$tmp_remove;
                 break;
                 break;
+            case 'invalidate':
+                $remove_paths = [];
+                break;
             default:
             default:
                 if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
                 if (Grav::instance()['config']->get('system.cache.clear_images_by_default')) {
                     $remove_paths = self::$standard_remove;
                     $remove_paths = self::$standard_remove;
@@ -382,6 +449,12 @@ class Cache extends Getters
 
 
         }
         }
 
 
+        // Delete entries in the doctrine cache if required
+        if (in_array($remove, ['all', 'standard'])) {
+            $cache = Grav::instance()['cache'];
+            $cache->driver->deleteAll();
+        }
+
         // Clearing cache event to add paths to clear
         // Clearing cache event to add paths to clear
         Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
         Grav::instance()->fireEvent('onBeforeCacheClear', new Event(['remove' => $remove, 'paths' => &$remove_paths]));
 
 
@@ -422,7 +495,7 @@ class Cache extends Getters
 
 
         $output[] = '';
         $output[] = '';
 
 
-        if (($remove == 'all' || $remove == 'standard') && file_exists($user_config)) {
+        if (($remove === 'all' || $remove === 'standard') && file_exists($user_config)) {
             touch($user_config);
             touch($user_config);
 
 
             $output[] = '<red>Touched: </red>' . $user_config;
             $output[] = '<red>Touched: </red>' . $user_config;
@@ -440,6 +513,23 @@ class Cache extends Getters
         return $output;
         return $output;
     }
     }
 
 
+    public static function invalidateCache()
+    {
+        $user_config = USER_DIR . 'config/system.yaml';
+
+        if (file_exists($user_config)) {
+            touch($user_config);
+        }
+
+        // Clear stat cache
+        @clearstatcache();
+
+        // Clear opcache
+        if (function_exists('opcache_reset')) {
+            @opcache_reset();
+        }
+
+    }
 
 
     /**
     /**
      * Set the cache lifetime programmatically
      * Set the cache lifetime programmatically
@@ -452,7 +542,7 @@ class Cache extends Getters
             return;
             return;
         }
         }
 
 
-        $interval = $future - $this->now;
+        $interval = (int)($future - $this->now);
         if ($interval > 0 && $interval < $this->getLifetime()) {
         if ($interval > 0 && $interval < $this->getLifetime()) {
             $this->lifetime = $interval;
             $this->lifetime = $interval;
         }
         }
@@ -467,7 +557,7 @@ class Cache extends Getters
     public function getLifetime()
     public function getLifetime()
     {
     {
         if ($this->lifetime === null) {
         if ($this->lifetime === null) {
-            $this->lifetime = $this->config->get('system.cache.lifetime') ?: 604800; // 1 week default
+            $this->lifetime = (int)($this->config->get('system.cache.lifetime') ?: 604800); // 1 week default
         }
         }
 
 
         return $this->lifetime;
         return $this->lifetime;
@@ -496,15 +586,71 @@ class Cache extends Getters
     /**
     /**
      * is this driver a volatile driver in that it resides in PHP process memory
      * is this driver a volatile driver in that it resides in PHP process memory
      *
      *
-     * @param $setting
+     * @param string $setting
      * @return bool
      * @return bool
      */
      */
     public function isVolatileDriver($setting)
     public function isVolatileDriver($setting)
     {
     {
         if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
         if (in_array($setting, ['apc', 'apcu', 'xcache', 'wincache'])) {
             return true;
             return true;
-        } else {
-            return false;
         }
         }
+
+        return false;
     }
     }
+
+    /**
+     * Static function to call as a scheduled Job to purge old Doctrine files
+     */
+    public static function purgeJob()
+    {
+        /** @var Cache $cache */
+        $cache = Grav::instance()['cache'];
+        $deleted_folders = $cache->purgeOldCache();
+
+        echo 'Purged ' . $deleted_folders . ' old cache folders...';
+    }
+
+    /**
+     * Static function to call as a scheduled Job to clear Grav cache
+     *
+     * @param string $type
+     */
+    public static function clearJob($type)
+    {
+        $result = static::clearCache($type);
+        static::invalidateCache();
+
+        echo strip_tags(implode("\n", $result));
+    }
+
+    public function onSchedulerInitialized(Event $event)
+    {
+        /** @var Scheduler $scheduler */
+        $scheduler = $event['scheduler'];
+        $config = Grav::instance()['config'];
+
+        // File Cache Purge
+        $at = $config->get('system.cache.purge_at');
+        $name = 'cache-purge';
+        $logs = 'logs/' . $name . '.out';
+
+        $job = $scheduler->addFunction('Grav\Common\Cache::purgeJob', [], $name );
+        $job->at($at);
+        $job->output($logs);
+        $job->backlink('/config/system#caching');
+
+        // Cache Clear
+        $at = $config->get('system.cache.clear_at');
+        $clear_type = $config->get('system.cache.clear_job_type');
+        $name = 'cache-clear';
+        $logs = 'logs/' . $name . '.out';
+
+        $job = $scheduler->addFunction('Grav\Common\Cache::clearJob', [$clear_type], $name );
+        $job->at($at);
+        $job->output($logs);
+        $job->backlink('/config/system#caching');
+
+    }
+
+
 }
 }

+ 7 - 6
system/src/Grav/Common/Composer.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common
+ * @package    Grav\Common
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -11,7 +12,7 @@ namespace Grav\Common;
 class Composer
 class Composer
 {
 {
     /** @const Default composer location */
     /** @const Default composer location */
-    const DEFAULT_PATH = "bin/composer.phar";
+    const DEFAULT_PATH = 'bin/composer.phar';
 
 
     /**
     /**
      * Returns the location of composer.
      * Returns the location of composer.
@@ -20,12 +21,12 @@ class Composer
      */
      */
     public static function getComposerLocation()
     public static function getComposerLocation()
     {
     {
-        if (!function_exists('shell_exec') || strtolower(substr(PHP_OS, 0, 3)) === 'win') {
+        if (!\function_exists('shell_exec') || stripos(PHP_OS, 'win') === 0) {
             return self::DEFAULT_PATH;
             return self::DEFAULT_PATH;
         }
         }
 
 
         // check for global composer install
         // check for global composer install
-        $path = trim(shell_exec("command -v composer"));
+        $path = trim(shell_exec('command -v composer'));
 
 
         // fall back to grav bundled composer
         // fall back to grav bundled composer
         if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
         if (!$path || !preg_match('/(composer|composer\.phar)$/', $path)) {
@@ -46,7 +47,7 @@ class Composer
         $composer = static::getComposerLocation();
         $composer = static::getComposerLocation();
 
 
         if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
         if ($composer !== static::DEFAULT_PATH && is_executable($composer)) {
-            $file = fopen($composer, 'r');
+            $file = fopen($composer, 'rb');
             $firstLine = fgets($file);
             $firstLine = fgets($file);
             fclose($file);
             fclose($file);
 
 

+ 9 - 10
system/src/Grav/Common/Config/CompiledBase.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -128,7 +129,7 @@ abstract class CompiledBase
      */
      */
     public function checksum()
     public function checksum()
     {
     {
-        if (!isset($this->checksum)) {
+        if (null === $this->checksum) {
             $this->checksum = md5(json_encode($this->files) . $this->version);
             $this->checksum = md5(json_encode($this->files) . $this->version);
         }
         }
 
 
@@ -197,11 +198,9 @@ abstract class CompiledBase
 
 
         $cache = include $filename;
         $cache = include $filename;
         if (
         if (
-            !is_array($cache)
-            || !isset($cache['checksum'])
-            || !isset($cache['data'])
-            || !isset($cache['@class'])
-            || $cache['@class'] != get_class($this)
+            !\is_array($cache)
+            || !isset($cache['checksum'], $cache['data'], $cache['@class'])
+            || $cache['@class'] !== \get_class($this)
         ) {
         ) {
             return false;
             return false;
         }
         }
@@ -212,7 +211,7 @@ abstract class CompiledBase
         }
         }
 
 
         $this->createObject($cache['data']);
         $this->createObject($cache['data']);
-        $this->timestamp = isset($cache['timestamp']) ? $cache['timestamp'] : 0;
+        $this->timestamp = $cache['timestamp'] ?? 0;
 
 
         $this->finalizeObject();
         $this->finalizeObject();
 
 
@@ -243,7 +242,7 @@ abstract class CompiledBase
         }
         }
 
 
         $cache = [
         $cache = [
-            '@class' => get_class($this),
+            '@class' => \get_class($this),
             'timestamp' => time(),
             'timestamp' => time(),
             'checksum' => $this->checksum(),
             'checksum' => $this->checksum(),
             'files' => $this->files,
             'files' => $this->files,

+ 13 - 10
system/src/Grav/Common/Config/CompiledBlueprints.php

@@ -1,27 +1,30 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
+
 namespace Grav\Common\Config;
 namespace Grav\Common\Config;
 
 
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\Blueprint;
 use Grav\Common\Data\BlueprintSchema;
 use Grav\Common\Data\BlueprintSchema;
 use Grav\Common\Grav;
 use Grav\Common\Grav;
 
 
+/**
+ * Class CompiledBlueprints
+ * @package Grav\Common\Config
+ */
 class CompiledBlueprints extends CompiledBase
 class CompiledBlueprints extends CompiledBase
 {
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 2;
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
 
 
-    /**
-     * @var BlueprintSchema  Blueprints object.
-     */
-    protected $object;
+        $this->version = 2;
+    }
 
 
     /**
     /**
      * Returns checksum from the configuration files.
      * Returns checksum from the configuration files.

+ 11 - 13
system/src/Grav/Common/Config/CompiledConfig.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -12,16 +13,6 @@ use Grav\Common\File\CompiledYamlFile;
 
 
 class CompiledConfig extends CompiledBase
 class CompiledConfig extends CompiledBase
 {
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 1;
-
-    /**
-     * @var Config  Configuration object.
-     */
-    protected $object;
-
     /**
     /**
      * @var callable  Blueprints loader.
      * @var callable  Blueprints loader.
      */
      */
@@ -32,6 +23,13 @@ class CompiledConfig extends CompiledBase
      */
      */
     protected $withDefaults;
     protected $withDefaults;
 
 
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
+
+        $this->version = 1;
+    }
+
     /**
     /**
      * Set blueprints for the configuration.
      * Set blueprints for the configuration.
      *
      *
@@ -63,7 +61,7 @@ class CompiledConfig extends CompiledBase
      */
      */
     protected function createObject(array $data = [])
     protected function createObject(array $data = [])
     {
     {
-        if ($this->withDefaults && empty($data) && is_callable($this->callable)) {
+        if ($this->withDefaults && empty($data) && \is_callable($this->callable)) {
             $blueprints = $this->callable;
             $blueprints = $this->callable;
             $data = $blueprints()->getDefaults();
             $data = $blueprints()->getDefaults();
         }
         }

+ 8 - 10
system/src/Grav/Common/Config/CompiledLanguages.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -12,15 +13,12 @@ use Grav\Common\File\CompiledYamlFile;
 
 
 class CompiledLanguages extends CompiledBase
 class CompiledLanguages extends CompiledBase
 {
 {
-    /**
-     * @var int Version number for the compiled file.
-     */
-    public $version = 1;
+    public function __construct($cacheFolder, array $files, $path)
+    {
+        parent::__construct($cacheFolder, $files, $path);
 
 
-    /**
-     * @var Languages  Configuration object.
-     */
-    protected $object;
+        $this->version = 1;
+    }
 
 
     /**
     /**
      * Create configuration object.
      * Create configuration object.

+ 19 - 9
system/src/Grav/Common/Config/Config.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -16,14 +17,24 @@ use Grav\Common\Utils;
 
 
 class Config extends Data
 class Config extends Data
 {
 {
+    public $environment;
+
+    /** @var string */
+    protected $key;
     /** @var string */
     /** @var string */
     protected $checksum;
     protected $checksum;
-    protected $modified = false;
+    /** @var int */
     protected $timestamp = 0;
     protected $timestamp = 0;
+    /** @var bool */
+    protected $modified = false;
 
 
     public function key()
     public function key()
     {
     {
-        return $this->checksum();
+        if (null === $this->key) {
+            $this->key = md5($this->checksum . $this->timestamp);
+        }
+
+        return $this->key;
     }
     }
 
 
     public function checksum($checksum = null)
     public function checksum($checksum = null)
@@ -90,7 +101,7 @@ class Config extends Data
     {
     {
         $setup = Grav::instance()['setup']->toArray();
         $setup = Grav::instance()['setup']->toArray();
         foreach ($setup as $key => $value) {
         foreach ($setup as $key => $value) {
-            if ($key === 'streams' || !is_array($value)) {
+            if ($key === 'streams' || !\is_array($value)) {
                 // Optimized as streams and simple values are fully defined in setup.
                 // Optimized as streams and simple values are fully defined in setup.
                 $this->items[$key] = $value;
                 $this->items[$key] = $value;
             } else {
             } else {
@@ -98,14 +109,13 @@ class Config extends Data
             }
             }
         }
         }
 
 
-        // Override the media.upload_limit based on PHP values
-        $upload_limit = Utils::getUploadLimit();
-        $this->items['system']['media']['upload_limit'] = $upload_limit > 0 ? $upload_limit : 1024*1024*1024;
+        // Legacy value - Override the media.upload_limit based on PHP values
+        $this->items['system']['media']['upload_limit'] = Utils::getUploadLimit();
     }
     }
 
 
     /**
     /**
      * @return mixed
      * @return mixed
-     * @deprecated
+     * @deprecated 1.5 Use Grav::instance()['languages'] instead.
      */
      */
     public function getLanguages()
     public function getLanguages()
     {
     {

+ 3 - 2
system/src/Grav/Common/Config/ConfigFileFinder.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 

+ 30 - 2
system/src/Grav/Common/Config/Languages.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -13,6 +14,22 @@ use Grav\Common\Utils;
 
 
 class Languages extends Data
 class Languages extends Data
 {
 {
+    /**
+     * @var string|null
+     */
+    protected $checksum;
+
+    /**
+     * @var string|null
+     */
+    protected $modified;
+
+    /**
+     * @var string|null
+     */
+    protected $timestamp;
+
+
     public function checksum($checksum = null)
     public function checksum($checksum = null)
     {
     {
         if ($checksum !== null) {
         if ($checksum !== null) {
@@ -52,4 +69,15 @@ class Languages extends Data
     {
     {
         $this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
         $this->items = Utils::arrayMergeRecursiveUnique($this->items, $data);
     }
     }
+
+    public function flattenByLang($lang)
+    {
+        $language = $this->items[$lang];
+        return Utils::arrayFlattenDotNotation($language);
+    }
+
+    public function unflatten($array)
+    {
+        return Utils::arrayUnflattenDotNotation($array);
+    }
 }
 }

+ 57 - 19
system/src/Grav/Common/Config/Setup.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Config
+ * @package    Grav\Common\Config
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -12,11 +13,23 @@ use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Data\Data;
 use Grav\Common\Data\Data;
 use Grav\Common\Utils;
 use Grav\Common\Utils;
 use Pimple\Container;
 use Pimple\Container;
-use RocketTheme\Toolbox\File\YamlFile;
+use Psr\Http\Message\ServerRequestInterface;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 
 class Setup extends Data
 class Setup extends Data
 {
 {
+    /**
+     * @var array Environment aliases normalized to lower case.
+     */
+    public static $environments = [
+        '' => 'unknown',
+        '127.0.0.1' => 'localhost',
+        '::1' => 'localhost'
+    ];
+
+    /**
+     * @var string Current environment normalized to lower case.
+     */
     public static $environment;
     public static $environment;
 
 
     protected $streams = [
     protected $streams = [
@@ -38,7 +51,7 @@ class Setup extends Data
             // If not defined, environment will be set up in the constructor.
             // If not defined, environment will be set up in the constructor.
         ],
         ],
         'asset' => [
         'asset' => [
-            'type' => 'ReadOnlyStream',
+            'type' => 'Stream',
             'prefixes' => [
             'prefixes' => [
                 '' => ['assets'],
                 '' => ['assets'],
             ]
             ]
@@ -109,7 +122,7 @@ class Setup extends Data
             ]
             ]
         ],
         ],
         'image' => [
         'image' => [
-            'type' => 'ReadOnlyStream',
+            'type' => 'Stream',
             'prefixes' => [
             'prefixes' => [
                 '' => ['user://images', 'system://images']
                 '' => ['user://images', 'system://images']
             ]
             ]
@@ -120,6 +133,13 @@ class Setup extends Data
                 '' => ['user://pages']
                 '' => ['user://pages']
             ]
             ]
         ],
         ],
+        'user-data' => [
+            'type' => 'Stream',
+            'force' => true,
+            'prefixes' => [
+                '' => ['user://data']
+            ]
+        ],
         'account' => [
         'account' => [
             'type' => 'ReadOnlyStream',
             'type' => 'ReadOnlyStream',
             'prefixes' => [
             'prefixes' => [
@@ -133,12 +153,26 @@ class Setup extends Data
      */
      */
     public function __construct($container)
     public function __construct($container)
     {
     {
-        $environment = null !== static::$environment ? static::$environment : ($container['uri']->environment() ?: 'localhost');
+        // If no environment is set, make sure we get one (CLI or hostname).
+        if (!static::$environment) {
+            if (\defined('GRAV_CLI')) {
+                static::$environment = 'cli';
+            } else {
+                /** @var ServerRequestInterface $request */
+                $request = $container['request'];
+                $host = $request->getUri()->getHost();
+
+                static::$environment = Utils::substrToString($host, ':');
+            }
+        }
+
+        // Resolve server aliases to the proper environment.
+        $environment = $this->environments[static::$environment] ?? static::$environment;
 
 
         // Pre-load setup.php which contains our initial configuration.
         // Pre-load setup.php which contains our initial configuration.
         // Configuration may contain dynamic parts, which is why we need to always load it.
         // Configuration may contain dynamic parts, which is why we need to always load it.
-        // If "GRAVE_SETUP_PATH" has been defined, use it, otherwise use defaults.
-        $file = defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH :  GRAV_ROOT . '/setup.php';
+        // If "GRAV_SETUP_PATH" has been defined, use it, otherwise use defaults.
+        $file = \defined('GRAV_SETUP_PATH') ? GRAV_SETUP_PATH :  GRAV_ROOT . '/setup.php';
         $setup = is_file($file) ? (array) include $file : [];
         $setup = is_file($file) ? (array) include $file : [];
 
 
         // Add default streams defined in beginning of the class.
         // Add default streams defined in beginning of the class.
@@ -151,8 +185,8 @@ class Setup extends Data
         parent::__construct($setup);
         parent::__construct($setup);
 
 
         // Set up environment.
         // Set up environment.
-        $this->def('environment', $environment ?: 'cli');
-        $this->def('streams.schemes.environment.prefixes', ['' => $environment ? ["user://{$this->environment}"] : []]);
+        $this->def('environment', $environment);
+        $this->def('streams.schemes.environment.prefixes', ['' => ["user://{$this->get('environment')}"]]);
     }
     }
 
 
     /**
     /**
@@ -212,8 +246,8 @@ class Setup extends Data
                 $locator->addPath($scheme, '', $config['paths']);
                 $locator->addPath($scheme, '', $config['paths']);
             }
             }
 
 
-            $override = isset($config['override']) ? $config['override'] : false;
-            $force = isset($config['force']) ? $config['force'] : false;
+            $override = $config['override'] ?? false;
+            $force = $config['force'] ?? false;
 
 
             if (isset($config['prefixes'])) {
             if (isset($config['prefixes'])) {
                 foreach ((array)$config['prefixes'] as $prefix => $paths) {
                 foreach ((array)$config['prefixes'] as $prefix => $paths) {
@@ -232,7 +266,7 @@ class Setup extends Data
     {
     {
         $schemes = [];
         $schemes = [];
         foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
         foreach ((array) $this->get('streams.schemes') as $scheme => $config) {
-            $type = !empty($config['type']) ? $config['type'] : 'ReadOnlyStream';
+            $type = $config['type'] ?? 'ReadOnlyStream';
             if ($type[0] !== '\\') {
             if ($type[0] !== '\\') {
                 $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
                 $type = '\\RocketTheme\\Toolbox\\StreamWrapper\\' . $type;
             }
             }
@@ -251,8 +285,8 @@ class Setup extends Data
      */
      */
     protected function check(UniformResourceLocator $locator)
     protected function check(UniformResourceLocator $locator)
     {
     {
-        $streams = isset($this->items['streams']['schemes']) ? $this->items['streams']['schemes'] : null;
-        if (!is_array($streams)) {
+        $streams = $this->items['streams']['schemes'] ?? null;
+        if (!\is_array($streams)) {
             throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
             throw new \InvalidArgumentException('Configuration is missing streams.schemes!');
         }
         }
         $diff = array_keys(array_diff_key($this->streams, $streams));
         $diff = array_keys(array_diff_key($this->streams, $streams));
@@ -271,10 +305,14 @@ class Setup extends Data
 
 
             // Create security.yaml if it doesn't exist.
             // Create security.yaml if it doesn't exist.
             $filename = $locator->findResource('config://security.yaml', true, true);
             $filename = $locator->findResource('config://security.yaml', true, true);
-            $file = YamlFile::instance($filename);
-            if (!$file->exists()) {
-                $file->save(['salt' => Utils::generateRandomString(14)]);
-                $file->free();
+            $security_file = CompiledYamlFile::instance($filename);
+            $security_content = (array)$security_file->content();
+
+            if (!isset($security_content['salt'])) {
+                $security_content = array_merge($security_content, ['salt' => Utils::generateRandomString(14)]);
+                $security_file->content($security_content);
+                $security_file->save();
+                $security_file->free();
             }
             }
         } catch (\RuntimeException $e) {
         } catch (\RuntimeException $e) {
             throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);
             throw new \RuntimeException(sprintf('Grav failed to initialize: %s', $e->getMessage()), 500, $e);

+ 200 - 26
system/src/Grav/Common/Data/Blueprint.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -10,21 +11,37 @@ namespace Grav\Common\Data;
 
 
 use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\File\CompiledYamlFile;
 use Grav\Common\Grav;
 use Grav\Common\Grav;
+use Grav\Common\User\Interfaces\UserInterface;
 use RocketTheme\Toolbox\Blueprints\BlueprintForm;
 use RocketTheme\Toolbox\Blueprints\BlueprintForm;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
 
 
 class Blueprint extends BlueprintForm
 class Blueprint extends BlueprintForm
 {
 {
-    /**
-     * @var string
-     */
+    /** @var string */
     protected $context = 'blueprints://';
     protected $context = 'blueprints://';
 
 
-    /**
-     * @var BlueprintSchema
-     */
+    protected $scope;
+
+    /** @var BlueprintSchema */
     protected $blueprintSchema;
     protected $blueprintSchema;
 
 
+    /** @var array */
+    protected $defaults;
+
+    protected $handlers = [];
+
+    public function __clone()
+    {
+        if ($this->blueprintSchema) {
+            $this->blueprintSchema = clone $this->blueprintSchema;
+        }
+    }
+
+    public function setScope($scope)
+    {
+        $this->scope = $scope;
+    }
+
     /**
     /**
      * Set default values for field types.
      * Set default values for field types.
      *
      *
@@ -51,7 +68,60 @@ class Blueprint extends BlueprintForm
     {
     {
         $this->initInternals();
         $this->initInternals();
 
 
-        return $this->blueprintSchema->getDefaults();
+        if (null === $this->defaults) {
+            $this->defaults = $this->blueprintSchema->getDefaults();
+        }
+
+        return $this->defaults;
+    }
+
+    /**
+     * Initialize blueprints with its dynamic fields.
+     *
+     * @return $this
+     */
+    public function init()
+    {
+        foreach ($this->dynamic as $key => $data) {
+            // Locate field.
+            $path = explode('/', $key);
+            $current = &$this->items;
+
+            foreach ($path as $field) {
+                if (\is_object($current)) {
+                    // Handle objects.
+                    if (!isset($current->{$field})) {
+                        $current->{$field} = [];
+                    }
+
+                    $current = &$current->{$field};
+                } else {
+                    // Handle arrays and scalars.
+                    if (!\is_array($current)) {
+                        $current = [$field => []];
+                    } elseif (!isset($current[$field])) {
+                        $current[$field] = [];
+                    }
+
+                    $current = &$current[$field];
+                }
+            }
+
+            // Set dynamic property.
+            foreach ($data as $property => $call) {
+                $action = $call['action'];
+                $method = 'dynamic' . ucfirst($action);
+
+                if (isset($this->handlers[$action])) {
+                    $callable = $this->handlers[$action];
+                    $callable($current, $property, $call);
+                } elseif (method_exists($this, $method)) {
+                    $this->{$method}($current, $property, $call);
+                }
+            }
+        }
+
+        return $this;
     }
     }
 
 
     /**
     /**
@@ -70,6 +140,20 @@ class Blueprint extends BlueprintForm
         return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
         return $this->blueprintSchema->mergeData($data1, $data2, $name, $separator);
     }
     }
 
 
+    /**
+     * Process data coming from a form.
+     *
+     * @param array $data
+     * @param array $toggles
+     * @return array
+     */
+    public function processForm(array $data, array $toggles = [])
+    {
+        $this->initInternals();
+
+        return $this->blueprintSchema->processForm($data, $toggles);
+    }
+
     /**
     /**
      * Return data fields that do not exist in blueprints.
      * Return data fields that do not exist in blueprints.
      *
      *
@@ -101,15 +185,32 @@ class Blueprint extends BlueprintForm
      * Filter data by using blueprints.
      * Filter data by using blueprints.
      *
      *
      * @param  array $data
      * @param  array $data
+     * @param  bool $missingValuesAsNull
+     * @param  bool $keepEmptyValues
+     * @return array
+     */
+    public function filter(array $data, bool $missingValuesAsNull = false, bool $keepEmptyValues = false)
+    {
+        $this->initInternals();
+
+        return $this->blueprintSchema->filter($data, $missingValuesAsNull, $keepEmptyValues);
+    }
+
+
+    /**
+     * Flatten data by using blueprints.
+     *
+     * @param  array $data
      * @return array
      * @return array
      */
      */
-    public function filter(array $data)
+    public function flattenData(array $data)
     {
     {
         $this->initInternals();
         $this->initInternals();
 
 
-        return $this->blueprintSchema->filter($data);
+        return $this->blueprintSchema->flattenData($data);
     }
     }
 
 
+
     /**
     /**
      * Return blueprint data schema.
      * Return blueprint data schema.
      *
      *
@@ -122,20 +223,28 @@ class Blueprint extends BlueprintForm
         return $this->blueprintSchema;
         return $this->blueprintSchema;
     }
     }
 
 
+    public function addDynamicHandler(string $name, callable $callable): void
+    {
+        $this->handlers[$name] = $callable;
+    }
+
     /**
     /**
      * Initialize validator.
      * Initialize validator.
      */
      */
     protected function initInternals()
     protected function initInternals()
     {
     {
-        if (!isset($this->blueprintSchema)) {
+        if (null === $this->blueprintSchema) {
             $types = Grav::instance()['plugins']->formFieldTypes;
             $types = Grav::instance()['plugins']->formFieldTypes;
 
 
             $this->blueprintSchema = new BlueprintSchema;
             $this->blueprintSchema = new BlueprintSchema;
+
             if ($types) {
             if ($types) {
                 $this->blueprintSchema->setTypes($types);
                 $this->blueprintSchema->setTypes($types);
             }
             }
+
             $this->blueprintSchema->embed('', $this->items);
             $this->blueprintSchema->embed('', $this->items);
             $this->blueprintSchema->init();
             $this->blueprintSchema->init();
+            $this->defaults = null;
         }
         }
     }
     }
 
 
@@ -162,17 +271,19 @@ class Blueprint extends BlueprintForm
         /** @var UniformResourceLocator $locator */
         /** @var UniformResourceLocator $locator */
         $locator = Grav::instance()['locator'];
         $locator = Grav::instance()['locator'];
 
 
-        if (is_string($path) && !$locator->isStream($path)) {
+        if (\is_string($path) && !$locator->isStream($path)) {
             // Find path overrides.
             // Find path overrides.
-            $paths = isset($this->overrides[$path]) ? (array) $this->overrides[$path] : [];
+            $paths = (array) ($this->overrides[$path] ?? null);
 
 
             // Add path pointing to default context.
             // Add path pointing to default context.
             if ($context === null) {
             if ($context === null) {
                 $context = $this->context;
                 $context = $this->context;
             }
             }
-            if ($context && $context[strlen($context)-1] !== '/') {
+
+            if ($context && $context[\strlen($context)-1] !== '/') {
                 $context .= '/';
                 $context .= '/';
             }
             }
+
             $path = $context . $path;
             $path = $context . $path;
 
 
             if (!preg_match('/\.yaml$/', $path)) {
             if (!preg_match('/\.yaml$/', $path)) {
@@ -186,7 +297,7 @@ class Blueprint extends BlueprintForm
 
 
         $files = [];
         $files = [];
         foreach ($paths as $lookup) {
         foreach ($paths as $lookup) {
-            if (is_string($lookup) && strpos($lookup, '://')) {
+            if (\is_string($lookup) && strpos($lookup, '://')) {
                 $files = array_merge($files, $locator->findResources($lookup));
                 $files = array_merge($files, $locator->findResources($lookup));
             } else {
             } else {
                 $files[] = $lookup;
                 $files[] = $lookup;
@@ -205,27 +316,29 @@ class Blueprint extends BlueprintForm
     {
     {
         $params = $call['params'];
         $params = $call['params'];
 
 
-        if (is_array($params)) {
+        if (\is_array($params)) {
             $function = array_shift($params);
             $function = array_shift($params);
         } else {
         } else {
             $function = $params;
             $function = $params;
             $params = [];
             $params = [];
         }
         }
 
 
-        list($o, $f) = preg_split('/::/', $function, 2);
+        [$o, $f] = explode('::', $function, 2);
+
+        $data = null;
         if (!$f) {
         if (!$f) {
-            if (function_exists($o)) {
-                $data = call_user_func_array($o, $params);
+            if (\function_exists($o)) {
+                $data = \call_user_func_array($o, $params);
             }
             }
         } else {
         } else {
             if (method_exists($o, $f)) {
             if (method_exists($o, $f)) {
-                $data = call_user_func_array(array($o, $f), $params);
+                $data = \call_user_func_array([$o, $f], $params);
             }
             }
         }
         }
 
 
         // If function returns a value,
         // If function returns a value,
-        if (isset($data)) {
-            if (isset($field[$property]) && is_array($field[$property]) && is_array($data)) {
+        if (null !== $data) {
+            if (\is_array($data) && isset($field[$property]) && \is_array($field[$property])) {
                 // Combine field and @data-field together.
                 // Combine field and @data-field together.
                 $field[$property] += $data;
                 $field[$property] += $data;
             } else {
             } else {
@@ -243,12 +356,73 @@ class Blueprint extends BlueprintForm
     protected function dynamicConfig(array &$field, $property, array &$call)
     protected function dynamicConfig(array &$field, $property, array &$call)
     {
     {
         $value = $call['params'];
         $value = $call['params'];
-
-        $default = isset($field[$property]) ? $field[$property] : null;
+        $default = $field[$property] ?? null;
         $config = Grav::instance()['config']->get($value, $default);
         $config = Grav::instance()['config']->get($value, $default);
 
 
-        if (!is_null($config)) {
+        if (null !== $config) {
             $field[$property] = $config;
             $field[$property] = $config;
         }
         }
     }
     }
+
+    /**
+     * @param array $field
+     * @param string $property
+     * @param array $call
+     */
+    protected function dynamicSecurity(array &$field, $property, array &$call)
+    {
+        if ($property || !empty($field['validate']['ignore'])) {
+            return;
+        }
+
+        $grav = Grav::instance();
+        $actions = (array)$call['params'];
+
+        /** @var UserInterface|null $user */
+        $user = $grav['user'] ?? null;
+        foreach ($actions as $action) {
+            if (!$user || !$user->authorize($action)) {
+                $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+                return;
+            }
+        }
+    }
+
+    /**
+     * @param array $field
+     * @param string $property
+     * @param array $call
+     */
+    protected function dynamicScope(array &$field, $property, array &$call)
+    {
+        if ($property && $property !== 'ignore') {
+            return;
+        }
+
+        $scopes = (array)$call['params'];
+        $matches = \in_array($this->scope, $scopes, true);
+        if ($this->scope && $property !== 'ignore') {
+            $matches = !$matches;
+        }
+
+        if ($matches) {
+            $this->addPropertyRecursive($field, 'validate', ['ignore' => true]);
+            return;
+        }
+    }
+
+    protected function addPropertyRecursive(array &$field, $property, $value)
+    {
+        if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
+            $field[$property] = array_merge_recursive($field[$property], $value);
+        } else {
+            $field[$property] = $value;
+        }
+
+        if (!empty($field['fields'])) {
+            foreach ($field['fields'] as $key => &$child) {
+                $this->addPropertyRecursive($child, $property, $value);
+            }
+        }
+    }
 }
 }

+ 172 - 24
system/src/Grav/Common/Data/BlueprintSchema.php

@@ -1,8 +1,9 @@
 <?php
 <?php
+
 /**
 /**
- * @package    Grav.Common.Data
+ * @package    Grav\Common\Data
  *
  *
- * @copyright  Copyright (C) 2015 - 2018 Trilby Media, LLC. All rights reserved.
+ * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  * @license    MIT License; see LICENSE file for details.
  * @license    MIT License; see LICENSE file for details.
  */
  */
 
 
@@ -26,6 +27,23 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         'fields' => true
         'fields' => true
     ];
     ];
 
 
+    /**
+     * @return array
+     */
+    public function getTypes()
+    {
+        return $this->types;
+    }
+
+    /**
+     * @param string $name
+     * @return array
+     */
+    public function getType($name)
+    {
+        return $this->types[$name] ?? [];
+    }
+
     /**
     /**
      * Validate data against blueprints.
      * Validate data against blueprints.
      *
      *
@@ -46,36 +64,91 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         }
         }
     }
     }
 
 
+    /**
+     * @param array $data
+     * @param array $toggles
+     * @return array
+     */
+    public function processForm(array $data, array $toggles = [])
+    {
+        return $this->processFormRecursive($data, $toggles, $this->nested);
+    }
+
     /**
     /**
      * Filter data by using blueprints.
      * Filter data by using blueprints.
      *
      *
-     * @param  array $data
+     * @param  array $data                  Incoming data, for example from a form.
+     * @param  bool  $missingValuesAsNull   Include missing values as nulls.
+     * @param bool   $keepEmptyValues       Include empty values.
      * @return array
      * @return array
      */
      */
-    public function filter(array $data)
+    public function filter(array $data, $missingValuesAsNull = false, $keepEmptyValues = false)
     {
     {
-        return $this->filterArray($data, $this->nested);
+        return $this->filterArray($data, $this->nested, $missingValuesAsNull, $keepEmptyValues);
+    }
+
+    /**
+     * Flatten data by using blueprints.
+     *
+     * @param  array $data                  Data to be flattened.
+     * @return array
+     */
+    public function flattenData(array $data)
+    {
+        return $this->flattenArray($data, $this->nested, '');
     }
     }
 
 
     /**
     /**
      * @param array $data
      * @param array $data
      * @param array $rules
      * @param array $rules
-     * @returns array
+     * @param string $prefix
+     * @return array
+     */
+    protected function flattenArray(array $data, array $rules, string $prefix)
+    {
+        $array = [];
+
+        foreach ($data as $key => $field) {
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = is_string($val) ? $this->items[$val] : null;
+
+            if ($rule || isset($val['*'])) {
+                // Item has been defined in blueprints.
+                $array[$prefix.$key] = $field;
+            } elseif (is_array($field) && is_array($val)) {
+                // Array has been defined in blueprints.
+                $array += $this->flattenArray($field, $val, $prefix . $key . '.');
+            } else {
+                // Undefined/extra item.
+                $array[$prefix.$key] = $field;
+            }
+        }
+        return $array;
+    }
+
+    /**
+     * @param array $data
+     * @param array $rules
+     * @return array
      * @throws \RuntimeException
      * @throws \RuntimeException
-     * @internal
      */
      */
     protected function validateArray(array $data, array $rules)
     protected function validateArray(array $data, array $rules)
     {
     {
         $messages = $this->checkRequired($data, $rules);
         $messages = $this->checkRequired($data, $rules);
 
 
         foreach ($data as $key => $field) {
         foreach ($data as $key => $field) {
-            $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
-            $rule = is_string($val) ? $this->items[$val] : null;
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = \is_string($val) ? $this->items[$val] : null;
 
 
             if ($rule) {
             if ($rule) {
                 // Item has been defined in blueprints.
                 // Item has been defined in blueprints.
+                if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
+                    // Skip validation in the ignored field.
+                    continue;
+                }
+
                 $messages += Validation::validate($field, $rule);
                 $messages += Validation::validate($field, $rule);
-            } elseif (is_array($field) && is_array($val)) {
+            } elseif (\is_array($field) && \is_array($val)) {
                 // Array has been defined in blueprints.
                 // Array has been defined in blueprints.
                 $messages += $this->validateArray($field, $val);
                 $messages += $this->validateArray($field, $val);
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
@@ -90,32 +163,99 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     /**
     /**
      * @param array $data
      * @param array $data
      * @param array $rules
      * @param array $rules
+     * @param bool  $missingValuesAsNull
+     * @param bool $keepEmptyValues
      * @return array
      * @return array
-     * @internal
      */
      */
-    protected function filterArray(array $data, array $rules)
+    protected function filterArray(array $data, array $rules, $missingValuesAsNull, $keepEmptyValues)
     {
     {
-        $results = array();
+        $results = [];
+
+        if ($missingValuesAsNull) {
+            // First pass is to fill up all the fields with null. This is done to lock the ordering of the fields.
+            foreach ($rules as $key => $rule) {
+                if ($key && !isset($results[$key])) {
+                    $val = $rules[$key] ?? $rules['*'] ?? null;
+                    $rule = \is_string($val) ? $this->items[$val] : null;
+
+                    if (empty($rule['disabled']) && empty($rule['validate']['ignore'])) {
+                        continue;
+                    }
+                }
+            }
+        }
+
         foreach ($data as $key => $field) {
         foreach ($data as $key => $field) {
-            $val = isset($rules[$key]) ? $rules[$key] : (isset($rules['*']) ? $rules['*'] : null);
-            $rule = is_string($val) ? $this->items[$val] : null;
+            $val = $rules[$key] ?? $rules['*'] ?? null;
+            $rule = \is_string($val) ? $this->items[$val] : null;
 
 
             if ($rule) {
             if ($rule) {
                 // Item has been defined in blueprints.
                 // Item has been defined in blueprints.
+                if (!empty($rule['disabled']) || !empty($rule['validate']['ignore'])) {
+                    // Skip any data in the ignored field.
+                    unset($results[$key]);
+                    continue;
+                }
+
                 $field = Validation::filter($field, $rule);
                 $field = Validation::filter($field, $rule);
-            } elseif (is_array($field) && is_array($val)) {
+            } elseif (\is_array($field) && \is_array($val)) {
                 // Array has been defined in blueprints.
                 // Array has been defined in blueprints.
-                $field = $this->filterArray($field, $val);
+                $field = $this->filterArray($field, $val, $missingValuesAsNull, $keepEmptyValues);
+
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
             } elseif (isset($rules['validation']) && $rules['validation'] === 'strict') {
-                $field = null;
+                // Skip any extra data.
+                continue;
             }
             }
 
 
-            if (isset($field) && (!is_array($field) || !empty($field))) {
+            if ($keepEmptyValues || (null !== $field && (!\is_array($field) || !empty($field)))) {
                 $results[$key] = $field;
                 $results[$key] = $field;
             }
             }
         }
         }
 
 
-        return $results;
+        return $results ?: null;
+    }
+
+    /**
+     * @param array|null $data
+     * @param array $toggles
+     * @param array $nested
+     * @return array|null
+     */
+    protected function processFormRecursive(?array $data, array $toggles, array $nested)
+    {
+        foreach ($nested as $key => $value) {
+            if ($key === '') {
+                continue;
+            }
+            if ($key === '*') {
+                // TODO: Add support to collections.
+                continue;
+            }
+            if (is_array($value)) {
+                // Recursively fetch the items.
+                $data[$key] = $this->processFormRecursive($data[$key] ?? null, $toggles[$key] ?? [], $value);
+            } else {
+                $field = $this->get($value);
+                // Do not add the field if:
+                if (
+                    // Not an input field
+                    !$field
+                    // Field has been disabled
+                    || !empty($field['disabled'])
+                    // Field validation is set to be ignored
+                    || !empty($field['validate']['ignore'])
+                    // Field is toggleable and the toggle is turned off
+                    || (!empty($field['toggleable']) && empty($toggles[$key]))
+                ) {
+                    continue;
+                }
+                if (!isset($data[$key])) {
+                    $data[$key] = null;
+                }
+            }
+        }
+
+        return $data;
     }
     }
 
 
     /**
     /**
@@ -128,10 +268,18 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
         $messages = [];
         $messages = [];
 
 
         foreach ($fields as $name => $field) {
         foreach ($fields as $name => $field) {
-            if (!is_string($field)) {
+            if (!\is_string($field)) {
                 continue;
                 continue;
             }
             }
+
             $field = $this->items[$field];
             $field = $this->items[$field];
+
+            // Skip ignored field, it will not be required.
+            if (!empty($field['disabled']) || !empty($field['validate']['ignore'])) {
+                continue;
+            }
+
+            // Check if required.
             if (isset($field['validate']['required'])
             if (isset($field['validate']['required'])
                 && $field['validate']['required'] === true) {
                 && $field['validate']['required'] === true) {
 
 
@@ -142,9 +290,9 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
                     continue;
                     continue;
                 }
                 }
 
 
-                $value = isset($field['label']) ? $field['label'] : $field['name'];
+                $value = $field['label'] ?? $field['name'];
                 $language = Grav::instance()['language'];
                 $language = Grav::instance()['language'];
-                $message  = sprintf($language->translate('FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
+                $message  = sprintf($language->translate('GRAV.FORM.MISSING_REQUIRED_FIELD', null, true) . ' %s', $language->translate($value));
                 $messages[$field['name']][] = $message;
                 $messages[$field['name']][] = $message;
             }
             }
         }
         }
@@ -161,7 +309,7 @@ class BlueprintSchema extends BlueprintSchemaBase implements ExportInterface
     {
     {
         $value = $call['params'];
         $value = $call['params'];
 
 
-        $default = isset($field[$property]) ? $field[$property] : null;
+        $default = $field[$property] ?? null;
         $config = Grav::instance()['config']->get($value, $default);
         $config = Grav::instance()['config']->get($value, $default);
 
 
         if (null !== $config) {
         if (null !== $config) {

Some files were not shown because too many files changed in this diff