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

Implement sort order per feed (#8234)

* added local feed sorting

Addresses https://github.com/FreshRSS/FreshRSS/issues/4761

- Added number of sorted feeds and associative array for feed sorting option in Context.
- Number of sorted feeds and local sorting option by its index saved into Minz Request Parameters.
- Number of sorted feeds and local sorting options deleted when choosing another Option Of Global Sorting.
- Added option of allowing sorting by feed in configuration.
- Added variable for allowing local sorting in userConf.
- Added function to get feeds by current get in context.
- Added menu button for all individual feed sorting.
- New database options for individual feed sorting in EntryDAO.
- Considered choosing new entries based on chosen load limit.
- Local sorting parameter saved into continuation value in Index Controller.

How to test the feature manually:

1. At the bottom of Reading Configuration menu turn on individual sorting option menu 
2. Choose Sorting by feed option
3. Choose feed at next sorting menu and choose sorting option for that feed

* added feed sorting option

* added sort feeds display

* added template for sort feed name

* added title to feed sorting button

* added comments

* added local sorting option

* Added Docs

* css reset

* added getter and seter for local sort

* added getter and seter for local sort

* allowed sorting per feed

* allowed sorting per feed

* added sorting option for category

* deleted changes from NetryDAO

* add setting up sorting for category

* docs reset

* i18 reset

* updated i18 for category

* added i18 for categories

* added i18 for category

* added setting sorting for feeds and category

* removing userConf.allow-local-sort

* removing userConf.allow-local-sort

* removing white space

* added credits

* removed feeds_by_get

* removed whitespace

* changed escaping for values

* added escaping to user set values

* added in_array

* added secondary sort and order

* added secondary sort and order

* fixed readme

* removed whitespace change

* reseted i18n

* added translations

* added feed setting translations

* fixed i18n

* fixed i18n

* changes in sort order per feed

* changes in sort order per feed

* added secondary sort order

* primary sort

* changed to preferred sort order

* i18n

* Revert wrong whitespace changes

* Re-order new options

* added blank option

* fixed escaping

* fixed default sort in feed

* fixed default sort recovery

* siplyfied option

* added rand option

* Revert unrelated change

* Minor plaintext

* Whitespace and formatting fixes

* Avoid unneeded SQL requests and processing

* Improve syntax

* Improve logic

* Reuse existing translations as much as possible

* i18n

* Remove some options that make little sense

* Separators

* Fix old transation key

* Add help messages

* Progress on secondary sort

* raw name

* Pass parameters. Add TODO

* Progress

* Minor ordering

* Fix parenthesis

---------

Co-authored-by: root <root@LAPTOP-C8TCHHPN.localdomain>
Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
PeterVavercak 2 месяцев назад
Родитель
Сommit
ee7eb67f3c
74 измененных файлов с 645 добавлено и 290 удалено
  1. 1 0
      CREDITS.md
  2. 8 8
      README.fr.md
  3. 8 8
      README.md
  4. 16 0
      app/Controllers/categoryController.php
  5. 30 2
      app/Controllers/configureController.php
  6. 15 5
      app/Controllers/indexController.php
  7. 16 0
      app/Controllers/subscriptionController.php
  8. 7 0
      app/Models/Category.php
  9. 44 4
      app/Models/Context.php
  10. 96 30
      app/Models/EntryDAO.php
  11. 7 0
      app/Models/Feed.php
  12. 3 1
      app/Models/UserConfiguration.php
  13. 0 5
      app/i18n/cs/conf.php
  14. 10 3
      app/i18n/cs/index.php
  15. 0 5
      app/i18n/de/conf.php
  16. 10 3
      app/i18n/de/index.php
  17. 0 5
      app/i18n/el/conf.php
  18. 10 3
      app/i18n/el/index.php
  19. 0 5
      app/i18n/en-US/conf.php
  20. 10 3
      app/i18n/en-US/index.php
  21. 1 1
      app/i18n/en/admin.php
  22. 4 4
      app/i18n/en/api.php
  23. 0 5
      app/i18n/en/conf.php
  24. 10 3
      app/i18n/en/index.php
  25. 1 1
      app/i18n/en/sub.php
  26. 0 5
      app/i18n/es/conf.php
  27. 10 3
      app/i18n/es/index.php
  28. 0 5
      app/i18n/fa/conf.php
  29. 10 3
      app/i18n/fa/index.php
  30. 0 5
      app/i18n/fi/conf.php
  31. 10 3
      app/i18n/fi/index.php
  32. 0 5
      app/i18n/fr/conf.php
  33. 10 3
      app/i18n/fr/index.php
  34. 0 5
      app/i18n/he/conf.php
  35. 10 3
      app/i18n/he/index.php
  36. 0 5
      app/i18n/hu/conf.php
  37. 10 3
      app/i18n/hu/index.php
  38. 0 5
      app/i18n/id/conf.php
  39. 10 3
      app/i18n/id/index.php
  40. 0 5
      app/i18n/it/conf.php
  41. 10 3
      app/i18n/it/index.php
  42. 0 5
      app/i18n/ja/conf.php
  43. 10 3
      app/i18n/ja/index.php
  44. 0 5
      app/i18n/ko/conf.php
  45. 10 3
      app/i18n/ko/index.php
  46. 0 5
      app/i18n/lv/conf.php
  47. 10 3
      app/i18n/lv/index.php
  48. 0 5
      app/i18n/nl/conf.php
  49. 10 3
      app/i18n/nl/index.php
  50. 0 5
      app/i18n/oc/conf.php
  51. 10 3
      app/i18n/oc/index.php
  52. 0 5
      app/i18n/pl/conf.php
  53. 10 3
      app/i18n/pl/index.php
  54. 0 5
      app/i18n/pt-BR/conf.php
  55. 10 3
      app/i18n/pt-BR/index.php
  56. 0 5
      app/i18n/pt-PT/conf.php
  57. 10 3
      app/i18n/pt-PT/index.php
  58. 0 5
      app/i18n/ru/conf.php
  59. 10 3
      app/i18n/ru/index.php
  60. 0 5
      app/i18n/sk/conf.php
  61. 10 3
      app/i18n/sk/index.php
  62. 0 5
      app/i18n/tr/conf.php
  63. 10 3
      app/i18n/tr/index.php
  64. 0 5
      app/i18n/uk/conf.php
  65. 10 3
      app/i18n/uk/index.php
  66. 0 5
      app/i18n/zh-CN/conf.php
  67. 10 3
      app/i18n/zh-CN/index.php
  68. 0 5
      app/i18n/zh-TW/conf.php
  69. 10 3
      app/i18n/zh-TW/index.php
  70. 3 3
      app/layout/nav_menu.phtml
  71. 48 5
      app/views/configure/reading.phtml
  72. 31 0
      app/views/helpers/category/update.phtml
  73. 30 0
      app/views/helpers/feed/update.phtml
  74. 6 2
      config-user.default.php

+ 1 - 0
CREDITS.md

@@ -226,6 +226,7 @@ People are sorted by name so please keep this order.
 * [PedroPMS](https://github.com/PedroPMS): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:PedroPMS)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [Peter Stoinov](https://github.com/stoinov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:stoinov), [Web](https://stoinov.com)
+* [Peter Vaverčák](https://github.com/PeterVavercak): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is%3Apr+author%3APeterVavercak)
 * [Petra Lamborn](https://github.com/petraoleum): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:petraoleum), [Web](https://petras.space)
 * [Pim Snel](https://github.com/mipmip): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is%3Apr+author%3Amipmip), [Web](https://www.pimsnel.com)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:plopoyop)

+ 8 - 8
README.fr.md

@@ -228,18 +228,18 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 | Langage | Progression | |
 | - | - | - |
 | Čeština (cs) | ■■■■■■■■・・ 83% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Deutsch (de) | ■■■■■■■■■・ 95% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Deutsch (de) | ■■■■■■■■■・ 94% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Ελληνικά (el) | ■■■・・・・・・・ 38% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
 | English (en) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) |
 | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Español (es) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
 | فارسی (fa) | ■■■■■■■■■・ 92% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Suomi (fi) | ■■■■■■■■■・ 95% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Suomi (fi) | ■■■■■■■■■・ 94% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Français (fr) | ■■■■■■■■■■ 100% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
 | עברית (he) | ■■■■・・・・・・ 42% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Magyar (hu) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Bahasa Indonesia (id) | ■■■■■■■■■・ 92% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Italiano (it) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Magyar (hu) | ■■■■■■■■■・ 98% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Bahasa Indonesia (id) | ■■■■■■■■■・ 91% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Italiano (it) | ■■■■■■■■■・ 98% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 日本語 (ja) | ■■■■■■■■■・ 90% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 한국어 (ko) | ■■■■■■■■・・ 83% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Latviešu (lv) | ■■■■■■■・・・ 77% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
@@ -247,11 +247,11 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 | Occitan (oc) | ■■■■■■■・・・ 76% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Polski (pl) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Português (Brasil) (pt-BR) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 83% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Русский (ru) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 82% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Русский (ru) | ■■■■■■■■■・ 98% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Slovenčina (sk) | ■■■■■■■■・・ 83% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Türkçe (tr) | ■■■■■■■■■・ 91% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Українська (uk) | ■■■■■■■■■・ 94% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Українська (uk) | ■■■■■■■■■・ 93% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 简体中文 (zh-CN) | ■■■■■■■■■・ 99% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 正體中文 (zh-TW) | ■■■■■■■■・・ 83% | [contribuer](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-TW+%2F%28TODO%7CDIRTY%29%24%2F) |
 

+ 8 - 8
README.md

@@ -124,18 +124,18 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 | Language | Progress | |
 | - | - | - |
 | Čeština (cs) | ■■■■■■■■・・ 83% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fcs+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Deutsch (de) | ■■■■■■■■■・ 95% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Deutsch (de) | ■■■■■■■■■・ 94% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fde+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Ελληνικά (el) | ■■■・・・・・・・ 38% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fel+%2F%28TODO%7CDIRTY%29%24%2F) |
 | English (en) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen+%2F%28TODO%7CDIRTY%29%24%2F) |
 | English (United States) (en-US) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fen-US+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Español (es) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fes+%2F%28TODO%7CDIRTY%29%24%2F) |
 | فارسی (fa) | ■■■■■■■■■・ 92% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffa+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Suomi (fi) | ■■■■■■■■■・ 95% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Suomi (fi) | ■■■■■■■■■・ 94% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffi+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Français (fr) | ■■■■■■■■■■ 100% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ffr+%2F%28TODO%7CDIRTY%29%24%2F) |
 | עברית (he) | ■■■■・・・・・・ 42% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhe+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Magyar (hu) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Bahasa Indonesia (id) | ■■■■■■■■■・ 92% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Italiano (it) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Magyar (hu) | ■■■■■■■■■・ 98% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fhu+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Bahasa Indonesia (id) | ■■■■■■■■■・ 91% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fid+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Italiano (it) | ■■■■■■■■■・ 98% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fit+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 日本語 (ja) | ■■■■■■■■■・ 90% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fja+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 한국어 (ko) | ■■■■■■■■・・ 83% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fko+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Latviešu (lv) | ■■■■■■■・・・ 77% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Flv+%2F%28TODO%7CDIRTY%29%24%2F) |
@@ -143,11 +143,11 @@ See the [repository dedicated to those extensions](https://github.com/FreshRSS/E
 | Occitan (oc) | ■■■■■■■・・・ 76% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Foc+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Polski (pl) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpl+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Português (Brasil) (pt-BR) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-BR+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 83% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Русский (ru) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Português (Portugal) (pt-PT) | ■■■■■■■■・・ 82% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fpt-PT+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Русский (ru) | ■■■■■■■■■・ 98% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fru+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Slovenčina (sk) | ■■■■■■■■・・ 83% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fsk+%2F%28TODO%7CDIRTY%29%24%2F) |
 | Türkçe (tr) | ■■■■■■■■■・ 91% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Ftr+%2F%28TODO%7CDIRTY%29%24%2F) |
-| Українська (uk) | ■■■■■■■■■・ 94% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) |
+| Українська (uk) | ■■■■■■■■■・ 93% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fuk+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 简体中文 (zh-CN) | ■■■■■■■■■・ 99% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-CN+%2F%28TODO%7CDIRTY%29%24%2F) |
 | 正體中文 (zh-TW) | ■■■■■■■■・・ 83% | [contribute](https://github.com/search?q=repo%3AFreshRSS%2FFreshRSS+path%3Aapp%2Fi18n%2Fzh-TW+%2F%28TODO%7CDIRTY%29%24%2F) |
 

+ 16 - 0
app/Controllers/categoryController.php

@@ -150,6 +150,22 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 				$category->_attribute('opml_url', null);
 			}
 
+			$defaultSortOrder = Minz_Request::paramString('defaultSortOrder', plaintext: true);
+			if (str_ends_with($defaultSortOrder, '_asc')) {
+				$category->_attribute('defaultOrder', 'ASC');
+				$defaultSortOrder = substr($defaultSortOrder, 0, -strlen('_asc'));
+			} elseif (str_ends_with($defaultSortOrder, '_desc')) {
+				$category->_attribute('defaultOrder', 'DESC');
+				$defaultSortOrder = substr($defaultSortOrder, 0, -strlen('_desc'));
+			} else {
+				$category->_attribute('defaultOrder');
+			}
+			if (in_array($defaultSortOrder, ['id', 'date', 'link', 'title', 'length', 'f.name', 'rand'], true)) {
+				$category->_attribute('defaultSort', $defaultSortOrder);
+			} else {
+				$category->_attribute('defaultSort');
+			}
+
 			$values = [
 				'kind' => $category->kind(),
 				'name' => Minz_Request::paramString('name'),

+ 30 - 2
app/Controllers/configureController.php

@@ -149,11 +149,39 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
 			FreshRSS_Context::userConf()->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
 			FreshRSS_Context::userConf()->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
-			if (in_array(Minz_Request::paramString('sort_order'), ['ASC', 'DESC'], true)) {
-				FreshRSS_Context::userConf()->sort_order = Minz_Request::paramString('sort_order');
+
+			$sorting = Minz_Request::paramString('primary_sort', plaintext: true);
+			if (str_ends_with($sorting, '_asc')) {
+				FreshRSS_Context::userConf()->sort_order = 'ASC';
+				$sorting = substr($sorting, 0, -strlen('_asc'));
+			} elseif (str_ends_with($sorting, '_desc')) {
+				FreshRSS_Context::userConf()->sort_order = 'DESC';
+				$sorting = substr($sorting, 0, -strlen('_desc'));
 			} else {
 				FreshRSS_Context::userConf()->sort_order = 'DESC';
 			}
+			if (in_array($sorting, ['id', 'c.name', 'date', 'f.name', 'length', 'link', 'title', 'rand'], true)) {
+				FreshRSS_Context::userConf()->sort = $sorting;
+			} else {
+				FreshRSS_Context::userConf()->sort = 'id';
+			}
+
+			$sorting = Minz_Request::paramString('secondary_sort', plaintext: true);
+			if (str_ends_with($sorting, '_asc')) {
+				FreshRSS_Context::userConf()->secondary_sort_order = 'ASC';
+				$sorting = substr($sorting, 0, -strlen('_asc'));
+			} elseif (str_ends_with($sorting, '_desc')) {
+				FreshRSS_Context::userConf()->secondary_sort_order = 'DESC';
+				$sorting = substr($sorting, 0, -strlen('_desc'));
+			} else {
+				FreshRSS_Context::userConf()->secondary_sort_order = 'DESC';
+			}
+			if (in_array($sorting, ['id', 'date', 'link', 'title'], true)) {
+				FreshRSS_Context::userConf()->secondary_sort = $sorting;
+			} else {
+				FreshRSS_Context::userConf()->secondary_sort = 'id';
+			}
+
 			FreshRSS_Context::userConf()->mark_when = [
 				'article' => Minz_Request::paramBoolean('mark_open_article'),
 				'gone' => Minz_Request::paramBoolean('read_upon_gone'),

+ 15 - 5
app/Controllers/indexController.php

@@ -367,15 +367,24 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 					'c.name' => $pagingEntry->feed()?->categoryId() === FreshRSS_CategoryDAO::DEFAULTCATEGORYID ?
 						FreshRSS_CategoryDAO::DEFAULT_CATEGORY_NAME : $pagingEntry->feed()?->category()?->name() ?? '',
 					'date' => $pagingEntry->date(raw: true),
-					'f.name' => $pagingEntry->feed()?->name() ?? '',
+					'f.name' => $pagingEntry->feed()?->name(raw: true) ?? '',
 					'link' => $pagingEntry->link(raw: true),
 					'title' => $pagingEntry->title(),
 					'lastUserModified' => $pagingEntry->lastUserModified(),
 					'length' => $pagingEntry->sqlContentLength() ?? 0,
 				};
-				if ($pagingEntry !== null && FreshRSS_Context::$sort === 'c.name') {
-					// Secondary sort criterion
-					$continuation_values[] = $pagingEntry->feed()?->name() ?? '';
+				if (FreshRSS_Context::$sort === 'c.name') {
+					// Internal secondary sort criterion for category name
+					$continuation_values[] = $pagingEntry?->feed()?->name(raw: true) ?? '';
+				}
+				if (in_array(FreshRSS_Context::$sort, ['c.name', 'f.name'], true)) {
+					// User secondary sort criterion
+					$continuation_values[] = $pagingEntry === null ? 0 : match (FreshRSS_Context::$secondary_sort) {
+						'id' => $pagingEntry->id(),
+						'date' => $pagingEntry->date(raw: true),
+						'link' => $pagingEntry->link(raw: true),
+						'title' => $pagingEntry->title(),
+					};
 				}
 			} elseif (FreshRSS_Context::$sort === 'rand') {
 				FreshRSS_Context::$continuation_id = '0';
@@ -386,7 +395,8 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
 					$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$search,
 					id_min: $id_min, id_max: FreshRSS_Context::$id_max, sort: FreshRSS_Context::$sort, order: FreshRSS_Context::$order,
 					continuation_id: FreshRSS_Context::$continuation_id, continuation_values: $continuation_values,
-					limit: $postsPerPage ?? FreshRSS_Context::$number, offset: FreshRSS_Context::$offset) as $entry) {
+					limit: $postsPerPage ?? FreshRSS_Context::$number, offset: FreshRSS_Context::$offset,
+					secondary_sort: FreshRSS_Context::$secondary_sort, secondary_sort_order: FreshRSS_Context::$secondary_sort_order) as $entry) {
 			yield $entry;
 		}
 	}

+ 16 - 0
app/Controllers/subscriptionController.php

@@ -334,6 +334,22 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				$feed->resetCustomFavicon();
 			}
 
+			$defaultSortOrder = Minz_Request::paramString('defaultSortOrder', plaintext: true);
+			if (str_ends_with($defaultSortOrder, '_asc')) {
+				$feed->_attribute('defaultOrder', 'ASC');
+				$defaultSortOrder = substr($defaultSortOrder, 0, -strlen('_asc'));
+			} elseif (str_ends_with($defaultSortOrder, '_desc')) {
+				$feed->_attribute('defaultOrder', 'DESC');
+				$defaultSortOrder = substr($defaultSortOrder, 0, -strlen('_desc'));
+			} else {
+				$feed->_attribute('defaultOrder');
+			}
+			if (in_array($defaultSortOrder, ['id', 'date', 'link', 'title', 'length', 'rand'], true)) {
+				$feed->_attribute('defaultSort', $defaultSortOrder);
+			} else {
+				$feed->_attribute('defaultSort');
+			}
+
 			$values = [
 				'name' => Minz_Request::paramString('name'),
 				'kind' => $feed->kind(),

+ 7 - 0
app/Models/Category.php

@@ -171,6 +171,13 @@ class FreshRSS_Category extends Minz_Model {
 		$this->sortFeeds();
 	}
 
+	public function defaultSort(): ?string {
+		return $this->attributeString('defaultSort');
+	}
+	public function defaultOrder(): ?string {
+		return $this->attributeString('defaultOrder');
+	}
+
 	/**
 	 * To manually add feeds to this category (not committing to database).
 	 */

+ 44 - 4
app/Models/Context.php

@@ -42,8 +42,12 @@ final class FreshRSS_Context {
 	public static int $state = 0;
 	/** @var 'ASC'|'DESC' */
 	public static string $order = 'DESC';
-	/** @var 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' */
+	/** @var 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' */
 	public static string $sort = 'id';
+	/** @var 'ASC'|'DESC' */
+	public static string $secondary_sort_order = 'DESC';
+	/** @var 'id'|'date'|'link'|'title' */
+	public static string $secondary_sort = 'id';
 	public static int $number = 0;
 	public static int $offset = 0;
 	public static FreshRSS_BooleanSearch $search;
@@ -258,10 +262,46 @@ final class FreshRSS_Context {
 		}
 
 		self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search', plaintext: true));
-		$order = Minz_Request::paramString('order', plaintext: true) ?: FreshRSS_Context::userConf()->sort_order;
+
+		$default_order = null;
+		$default_sort = null;
+		if (Minz_Request::paramString('order', plaintext: true) === '' || Minz_Request::paramString('sort', plaintext: true) === '') {
+			if (!empty(self::$current_get['feed'])) {
+				$id = self::$current_get['feed'];
+				// We most likely already have the feed object in cache
+				$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
+				if ($feed === null) {
+					$feedDAO = FreshRSS_Factory::createFeedDao();
+					$feed = $feedDAO->searchById($id);
+				}
+				$default_order = $feed?->defaultOrder();
+				$default_sort = $feed?->defaultSort();
+			} elseif (!empty(self::$current_get['category'])) {
+				$id = self::$current_get['category'];
+				// We most likely already have the category object in cache
+				$category = FreshRSS_Context::categories()[$id] ?? null;
+				if ($category === null) {
+					$categoryDAO = FreshRSS_Factory::createCategoryDao();
+					$category = $categoryDAO->searchById($id);
+				}
+				$default_order = $category?->defaultOrder();
+				$default_sort = $category?->defaultSort();
+			}
+		}
+		$order = Minz_Request::paramString('order', plaintext: true) ?: $default_order ?: FreshRSS_Context::userConf()->sort_order;
 		self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
-		$sort = Minz_Request::paramString('sort', plaintext: true) ?: FreshRSS_Context::userConf()->sort;
-		self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+		$sort = Minz_Request::paramString('sort', plaintext: true) ?: $default_sort ?: FreshRSS_Context::userConf()->sort;
+		self::$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'lastUserModified', 'length', 'link', 'title', 'rand'], true) ? $sort : 'id';
+
+		if (in_array(self::$sort, ['c.name', 'f.name'], true)) {
+			self::$secondary_sort = FreshRSS_Context::userConf()->secondary_sort;
+			self::$secondary_sort_order = FreshRSS_Context::userConf()->secondary_sort_order;
+			if ($order !== ($default_order ?: FreshRSS_Context::userConf()->sort_order)) {
+				// User swapped order so swap secondary order as well
+				self::$secondary_sort_order = self::$secondary_sort_order === 'DESC' ? 'ASC' : 'DESC';
+			}
+		}
+
 		self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page;
 		if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) {
 			self::$number = max(

+ 96 - 30
app/Models/EntryDAO.php

@@ -1270,15 +1270,18 @@ SQL;
 	/**
 	 * @param numeric-string $id_min
 	 * @param numeric-string $id_max
-	 * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+	 * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
 	 * @param 'ASC'|'DESC' $order
 	 * @param numeric-string $continuation_id
 	 * @param list<string|int> $continuation_values
+	 * @param 'id'|'date'|'link'|'title' $secondary_sort
+	 * @param 'ASC'|'DESC' $secondary_sort_order
 	 * @return array{0:list<int|string>,1:string}
 	 */
 	protected function sqlListEntriesWhere(string $alias = '', int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null,
 		string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
-		string $continuation_id = '0', array $continuation_values = []): array {
+		string $continuation_id = '0', array $continuation_values = [],
+		string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): array {
 		$search = ' ';
 		$values = [];
 		if ($state & FreshRSS_Entry::STATE_ANDS) {
@@ -1338,29 +1341,53 @@ SQL;
 			$values[] = $id_min;
 		}
 
-		if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'link', 'title', 'lastUserModified', 'length'], true)) {
+		if ($continuation_id !== '0' && in_array($sort, ['c.name', 'date', 'f.name', 'lastUserModified', 'length', 'link', 'title'], true)) {
 			$sign = $order === 'ASC' ? '>' : '<';
+			$sign2 = $secondary_sort_order === 'ASC' ? '>' : '<';
 			$orderBy = match ($sort) {
 				'c.name' => 'c.name',
+				'date' => $alias . 'date',
 				'f.name' => 'f.name',
 				'lastUserModified' => $alias . '`lastUserModified`',
 				'length' => 'LENGTH(' . $alias . (static::isCompressed() ? 'content_bin' : 'content') . ')',
-				default => $alias . $sort,
+				'link' => $alias . 'link',
+				'title' => $alias . 'title',
+			};
+			$orderBy2 = match ($secondary_sort) {
+				'id' => $alias . 'id',
+				'date' => $alias . 'date',
+				'link' => $alias . 'link',
+				'title' => $alias . 'title',
 			};
 			// Keyset pagination (Compatibility syntax due to poor performance of tuple syntax in MySQL https://bugs.mysql.com/bug.php?id=104128)
 			if ($sort === 'c.name') {
-				// Includes a secondary sort by feed name
-				$search .= "AND ((c.name {$sign} ?) OR (c.name = ? AND f.name {$sign} ?) OR (c.name = ? AND f.name = ? AND {$alias}id {$sign}= ?)) ";
-				$values[] = $continuation_values[0];
-				$values[] = $continuation_values[0];
-				$values[] = $continuation_values[1];
-				$values[] = $continuation_values[0];
-				$values[] = $continuation_values[1];
+				// Includes the feed-name sort and a user secondary sort
+				$search .= "AND ((c.name {$sign} ?) OR (c.name = ? AND f.name {$sign} ?) OR (c.name = ? AND f.name = ? AND {$orderBy2} {$sign2}= ?) " .
+					"OR (c.name = ? AND f.name = ? AND {$orderBy2} = ? AND {$alias}id {$sign}= ?)) ";
+				$values[] = $continuation_values[0];	// c.name (primary sort)
+				$values[] = $continuation_values[0];	// c.name (primary sort)
+				$values[] = $continuation_values[1];	// f.name (internal secondary sort)
+				$values[] = $continuation_values[0];	// c.name (primary sort)
+				$values[] = $continuation_values[1];	// f.name (internal secondary sort)
+				$values[] = $continuation_values[2];	// secondary sort
+				$values[] = $continuation_values[0];	// c.name (primary sort)
+				$values[] = $continuation_values[1];	// f.name (internal secondary sort)
+				$values[] = $continuation_values[2];	// secondary sort
+				$values[] = $continuation_id;
+			} elseif ($sort === 'f.name') {
+				// Includes the user secondary sort
+				$search .= "AND ((f.name {$sign} ?) OR (f.name = ? AND {$orderBy2} {$sign2} ?) " .
+					"OR (f.name = ? AND {$orderBy2} = ? AND {$alias}id {$sign}= ?)) ";
+				$values[] = $continuation_values[0];	// f.name (primary sort)
+				$values[] = $continuation_values[0];	// f.name (primary sort)
+				$values[] = $continuation_values[1];	// secondary sort
+				$values[] = $continuation_values[0];	// f.name (primary sort)
+				$values[] = $continuation_values[1];	// secondary sort
 				$values[] = $continuation_id;
 			} else {
 				$search .= "AND ({$orderBy} {$sign} ? OR ({$orderBy} = ? AND {$alias}id {$sign}= ?)) ";
-				$values[] = $continuation_values[0];
-				$values[] = $continuation_values[0];
+				$values[] = $continuation_values[0];	// primary sort
+				$values[] = $continuation_values[0];	// primary sort
 				$values[] = $continuation_id;
 			}
 		}
@@ -1382,16 +1409,19 @@ SQL;
 	 * @param int $id category/feed/tag ID
 	 * @param numeric-string $id_min
 	 * @param numeric-string $id_max
-	 * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+	 * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
 	 * @param 'ASC'|'DESC' $order
 	 * @param numeric-string $continuation_id
 	 * @param list<string|int> $continuation_values
+	 * @param 'id'|'date'|'link'|'title' $secondary_sort
+	 * @param 'ASC'|'DESC' $secondary_sort_order
 	 * @return array{0:list<int|string>,1:string}
 	 * @throws FreshRSS_EntriesGetter_Exception
 	 */
 	private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null,
 			string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
-			string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): array {
+			string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+			string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): array {
 		if (!$state) {
 			$state = FreshRSS_Entry::STATE_ALL;
 		}
@@ -1441,17 +1471,27 @@ SQL;
 		}
 
 		$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
-		$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+		$order2 = in_array($secondary_sort_order, ['ASC', 'DESC'], true) ? $secondary_sort_order : 'DESC';
 		$orderBy = match ($sort) {
+			'id' => 'e.id',
 			'c.name' => 'c.name',
+			'date' => 'e.date',
 			'f.name' => 'f.name',
 			'lastUserModified' => 'e.`lastUserModified`',
 			'length' => 'LENGTH(e.' . (static::isCompressed() ? 'content_bin' : 'content') . ')',
+			'link' => 'e.link',
+			'title' => 'e.title',
 			'rand' => static::sqlRandom(),
-			default => 'e.' . $sort,
+		};
+		$orderBy2 = match ($secondary_sort) {
+			'id' => 'e.id',
+			'date' => 'e.date',
+			'link' => 'e.link',
+			'title' => 'e.title',
 		};
 		[$searchValues, $search] = $this->sqlListEntriesWhere(alias: 'e.', state: $state, filters: $filters, id_min: $id_min, id_max: $id_max,
-			sort: $sort, order: $order, continuation_id: $continuation_id, continuation_values: $continuation_values);
+			sort: $sort, order: $order, continuation_id: $continuation_id, continuation_values: $continuation_values,
+			secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
 
 		// Help MySQL/MariaDB's optimizer with the query plan:
 		$useEntryIndex = ($this->pdo->dbType() === 'mysql' &&	// Only relevant for MySQL/MariaDB,
@@ -1470,7 +1510,8 @@ SQL;
 			. 'WHERE ' . $where
 			. $search
 			. 'ORDER BY ' . $orderBy . ' ' . $order
-			. ($sort === 'c.name' ? ', f.name ' . $order : '')	// Secondary sort
+			. ($sort === 'c.name' ? ', f.name ' . $order : '')	// Internal secondary sort
+			. (in_array($sort, ['c.name', 'f.name'], true) ? ', ' . $orderBy2 . ' ' . $order2 : '')	// User secondary sort
 			. ($sort === 'id' ? '' : ', e.id ' . $order)	// For keyset pagination
 			. ($limit > 0 ? ' LIMIT ' . $limit : '')	// http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
 			. ($offset > 0 ? ' OFFSET ' . $offset : '')
@@ -1482,28 +1523,41 @@ SQL;
 	 * @param int $id category/feed/tag ID
 	 * @param numeric-string $id_min
 	 * @param numeric-string $id_max
-	 * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+	 * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
 	 * @param 'ASC'|'DESC' $order
 	 * @param numeric-string $continuation_id
 	 * @param list<string|int> $continuation_values
+	 * @param 'id'|'date'|'link'|'title' $secondary_sort
+	 * @param 'ASC'|'DESC' $secondary_sort_order
 	 * @throws FreshRSS_EntriesGetter_Exception
 	 */
 	private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null,
 		string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
-		string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): PDOStatement|false {
+		string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+		string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): PDOStatement|false {
 		$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
-		$sort = in_array($sort, ['id', 'c.name', 'date', 'f.name', 'link', 'title', 'rand', 'lastUserModified', 'length'], true) ? $sort : 'id';
+		$secondary_sort_order = in_array($secondary_sort_order, ['ASC', 'DESC'], true) ? $secondary_sort_order : 'DESC';
 
 		[$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order,
-			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
+			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+			secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
 
 		$orderBy = match ($sort) {
+			'id' => 'e0.id',
 			'c.name' => 'c0.name',
+			'date' => 'e0.date',
 			'f.name' => 'f0.name',
 			'lastUserModified' => 'e0.`lastUserModified`',
 			'length' => 'LENGTH(e0.' . (static::isCompressed() ? 'content_bin' : 'content') . ')',
+			'link' => 'e0.link',
+			'title' => 'e0.title',
 			'rand' => static::sqlRandom(),
-			default => 'e0.' . $sort,
+		};
+		$orderBy2 = match ($secondary_sort) {
+			'id' => 'e0.id',
+			'date' => 'e0.date',
+			'link' => 'e0.link',
+			'title' => 'e0.title',
 		};
 		$content = static::isCompressed() ? 'UNCOMPRESS(e0.content_bin) AS content' : 'e0.content';
 		$hash = static::sqlHexEncode('e0.hash');
@@ -1520,7 +1574,10 @@ SQL;
 		}
 		$sql .= ' ORDER BY ' . $orderBy . ' ' . $order;
 		if ($sort === 'c.name') {
-			$sql .= ', f0.name ' . $order;	// Secondary sort
+			$sql .= ', f0.name ' . $order;	// Internal secondary sort
+		}
+		if (in_array($sort, ['c.name', 'f.name'], true)) {
+			$sql .= ', ' . $orderBy2 . ' ' . $secondary_sort_order;	// User secondary sort
 		}
 		if ($sort !== 'id') {
 			// For keyset pagination
@@ -1537,6 +1594,7 @@ SQL;
 					continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
 			}
 			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
+			Minz_Log::error('SQL error ' . __METHOD__ . json_encode($sql));
 			return false;
 		}
 	}
@@ -1546,18 +1604,22 @@ SQL;
 	 * @param int $id category/feed/tag ID
 	 * @param numeric-string $id_min
 	 * @param numeric-string $id_max
-	 * @param 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length' $sort
+	 * @param 'id'|'c.name'|'date'|'f.name'|'lastUserModified'|'length'|'link'|'rand'|'title' $sort
 	 * @param 'ASC'|'DESC' $order
 	 * @param numeric-string $continuation_id
 	 * @param list<string|int> $continuation_values
+	 * @param 'id'|'date'|'link'|'title' $secondary_sort
+	 * @param 'ASC'|'DESC' $secondary_sort_order
 	 * @return Traversable<FreshRSS_Entry>
 	 * @throws FreshRSS_EntriesGetter_Exception
 	 */
 	public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null,
 			string $id_min = '0', string $id_max = '0', string $sort = 'id', string $order = 'DESC',
-			string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): Traversable {
+			string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+			string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): Traversable {
 		$stm = $this->listWhereRaw($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, sort: $sort, order: $order,
-			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
+			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+			secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
 		if ($stm !== false) {
 			while (is_array($row = $stm->fetch(PDO::FETCH_ASSOC))) {
 				/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
@@ -1618,14 +1680,18 @@ SQL;
 	 * @param 'ASC'|'DESC' $order
 	 * @param numeric-string $continuation_id
 	 * @param list<string|int> $continuation_values
+	 * @param 'id'|'date'|'link'|'title' $secondary_sort
+	 * @param 'ASC'|'DESC' $secondary_sort_order
 	 * @return list<numeric-string>|null
 	 * @throws FreshRSS_EntriesGetter_Exception
 	 */
 	public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL, ?FreshRSS_BooleanSearch $filters = null,
 		string $id_min = '0', string $id_max = '0', string $order = 'DESC',
-		string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0): ?array {
+		string $continuation_id = '0', array $continuation_values = [], int $limit = 1, int $offset = 0,
+		string $secondary_sort = 'id', string $secondary_sort_order = 'DESC'): ?array {
 		[$values, $sql] = $this->sqlListWhere($type, $id, $state, $filters, id_min: $id_min, id_max: $id_max, order: $order,
-			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset);
+			continuation_id: $continuation_id, continuation_values: $continuation_values, limit: $limit, offset: $offset,
+			secondary_sort: $secondary_sort, secondary_sort_order: $secondary_sort_order);
 		$stm = $this->pdo->prepare($sql);
 		if ($stm !== false && $stm->execute($values)) {
 			/** @var list<int|numeric-string> $res */

+ 7 - 0
app/Models/Feed.php

@@ -537,6 +537,13 @@ class FreshRSS_Feed extends Minz_Model {
 		$this->nbEntries = $value;
 	}
 
+	public function defaultSort(): ?string {
+		return $this->attributeString('defaultSort');
+	}
+	public function defaultOrder(): ?string {
+		return $this->attributeString('defaultOrder');
+	}
+
 	/**
 	 * @throws Minz_FileNotExistException
 	 * @throws FreshRSS_Feed_Exception

+ 3 - 1
app/Models/UserConfiguration.php

@@ -56,7 +56,9 @@ declare(strict_types=1);
  * @property bool $show_nav_buttons
  * @property 'big'|'small'|'none' $mark_read_button
  * @property 'ASC'|'DESC' $sort_order
- * @property 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'length' $sort
+ * @property 'id'|'c.name'|'date'|'f.name'|'length'|'link'|'rand'|'title' $sort
+ * @property 'ASC'|'DESC' $secondary_sort_order
+ * @property 'id'|'date'|'link'|'title' $secondary_sort
  * @property array<int,array<string,string>> $sharing
  * @property array<string,string> $shortcuts
  * @property bool $sides_close_article

+ 0 - 5
app/i18n/cs/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Použije se také na popisky',
 		'sides_close_article' => 'Kliknutí mimo oblast textu článku zavře článek',
-		'sort' => array(
-			'_' => 'Pořadí řazení',
-			'newer_first' => 'Nejdříve nejnovější',
-			'older_first' => 'Nejdříve nejstarší',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/cs/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Označit kanál jako přečtený',
 		'mark_selection_unread' => 'Označit výběr jako nepřečtený',
 		'mylabels' => 'Mé popisky',
-		'newer_first' => 'Nejdříve novější',
 		'non-starred' => 'Zobrazit neoblíbené',
 		'normal_view' => 'Normální zobrazení',
-		'older_first' => 'Nejdříve nejstarší',
 		'queries' => 'Uživatelské dotazy',
 		'read' => 'Zobrazit přečtené',
 		'reader_view' => 'Zobrazení pro čtení',
 		'rss_view' => 'Kanál RSS',
 		'search_short' => 'Hledat',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/de/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Auch auf Labels anwenden',
 		'sides_close_article' => 'Klick außerhalb des Artikel-Textes schließt den Artikel',
-		'sort' => array(
-			'_' => 'Sortierreihenfolge',
-			'newer_first' => 'Neuere zuerst',
-			'older_first' => 'Ältere zuerst',
-		),
 		'star' => array(
 			'when' => 'Markiere einen Artikel als Favoriten…',
 		),

+ 10 - 3
app/i18n/de/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Feed als gelesen markieren',
 		'mark_selection_unread' => 'Auswahl als ungelesen markieren',
 		'mylabels' => 'Meine Labels',
-		'newer_first' => 'Neuere zuerst',
 		'non-starred' => 'Nicht-Favoriten zeigen',
 		'normal_view' => 'Normale Ansicht',
-		'older_first' => 'Ältere zuerst',
 		'queries' => 'Benutzerabfragen',
 		'read' => 'Gelesene zeigen',
 		'reader_view' => 'Lese-Ansicht',
 		'rss_view' => 'RSS-Feed',
 		'search_short' => 'Suchen',
 		'sort' => array(
-			'_' => 'Sortierkriterien',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Kategorie, Feed-Titel A→Z',
 				'name_desc' => 'Kategorie, Feed-Titel Z→A',
 			),
 			'date_asc' => 'Veröffentlichungsdatum 1→9',
 			'date_desc' => 'Veröffentlichungsdatum 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed-Titel A→Z',
 				'name_desc' => 'Feed-Titel Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Zufällige Reihenfolge',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Titel A→Z',
 			'title_desc' => 'Titel Z→A',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/el/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Applies also on labels',	// TODO
 		'sides_close_article' => 'Clicking outside of article text area closes the article',	// TODO
-		'sort' => array(
-			'_' => 'Sort order',	// TODO
-			'newer_first' => 'Newest first',	// TODO
-			'older_first' => 'Oldest first',	// TODO
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/el/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Mark feed as read',	// TODO
 		'mark_selection_unread' => 'Mark selection as unread',	// TODO
 		'mylabels' => 'My labels',	// TODO
-		'newer_first' => 'Newer first',	// TODO
 		'non-starred' => 'Show non-favourites',	// TODO
 		'normal_view' => 'Normal view',	// TODO
-		'older_first' => 'Oldest first',	// TODO
 		'queries' => 'User queries',	// TODO
 		'read' => 'Show read',	// TODO
 		'reader_view' => 'Reading view',	// TODO
 		'rss_view' => 'RSS feed',	// TODO
 		'search_short' => 'Search',	// TODO
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/en-US/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Applies also on labels',	// IGNORE
 		'sides_close_article' => 'Clicking outside of article text area closes the article',	// IGNORE
-		'sort' => array(
-			'_' => 'Sort order',	// IGNORE
-			'newer_first' => 'Newest first',	// IGNORE
-			'older_first' => 'Oldest first',	// IGNORE
-		),
 		'star' => array(
 			'when' => 'Mark an article as favorite…',
 		),

+ 10 - 3
app/i18n/en-US/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Mark feed as read',	// IGNORE
 		'mark_selection_unread' => 'Mark selection as unread',	// IGNORE
 		'mylabels' => 'My labels',	// IGNORE
-		'newer_first' => 'Newer first',	// IGNORE
 		'non-starred' => 'Show non-favorites',
 		'normal_view' => 'Normal view',	// IGNORE
-		'older_first' => 'Oldest first',	// IGNORE
 		'queries' => 'User queries',	// IGNORE
 		'read' => 'Show read',	// IGNORE
 		'reader_view' => 'Reading view',	// IGNORE
 		'rss_view' => 'RSS feed',	// IGNORE
 		'search_short' => 'Search',	// IGNORE
 		'sort' => array(
-			'_' => 'Sorting criteria',	// IGNORE
+			'asc' => 'Ascending',	// IGNORE
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// IGNORE
 				'name_desc' => 'Category, feed titles Z→A',	// IGNORE
 			),
 			'date_asc' => 'Publication date 1→9',	// IGNORE
 			'date_desc' => 'Publication date 9→1',	// IGNORE
+			'desc' => 'Descending',	// IGNORE
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// IGNORE
 				'name_desc' => 'Feed title Z→A',	// IGNORE
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// IGNORE
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// IGNORE
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// IGNORE
+			),
 			'rand' => 'Random order',	// IGNORE
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// IGNORE
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// IGNORE
+			),
 			'title_asc' => 'Title A→Z',	// IGNORE
 			'title_desc' => 'Title Z→A',	// IGNORE
 			'user_modified_asc' => 'User modified 1→9',	// IGNORE

+ 1 - 1
app/i18n/en/admin.php

@@ -31,7 +31,7 @@ return array(
 		'empty_list' => 'There are no installed extensions',
 		'empty_list_help' => 'Check the logs to determine the reason behind the empty extension list.',
 		'enabled' => 'Enabled',
-		'is_compatible' => 'Is compatible',	// TODO
+		'is_compatible' => 'Is compatible',
 		'latest' => 'Installed',
 		'name' => 'Name',
 		'no_configure_view' => 'This extension cannot be configured.',

+ 4 - 4
app/i18n/en/api.php

@@ -14,10 +14,10 @@ return array(
 	'information' => array(
 		'address' => 'Your API address:',
 		'output' => array(
-			'encoding-support' => '⚠️ WARN: no <code>%2F</code> support, some clients might not work!',	// TODO
-			'invalid-configuration' => '⚠️ WARN: Probable invalid base URL in ./data/config.php',	// TODO
-			'pass' => '✔️ PASS',	// TODO
-			'unknown-error' => '❌ ',	// TODO
+			'encoding-support' => '⚠️ WARN: no <code>%2F</code> support, some clients might not work!',
+			'invalid-configuration' => '⚠️ WARN: Probable invalid base URL in ./data/config.php',
+			'pass' => '✔️ PASS',
+			'unknown-error' => '❌ ',
 		),
 		'test' => array(
 			'fever' => 'Fever API configuration test:',

+ 0 - 5
app/i18n/en/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Applies also on labels',
 		'sides_close_article' => 'Clicking outside of article text area closes the article',
-		'sort' => array(
-			'_' => 'Sort order',
-			'newer_first' => 'Newest first',
-			'older_first' => 'Oldest first',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',
 		),

+ 10 - 3
app/i18n/en/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Mark feed as read',
 		'mark_selection_unread' => 'Mark selection as unread',
 		'mylabels' => 'My labels',
-		'newer_first' => 'Newer first',
 		'non-starred' => 'Show non-favourites',
 		'normal_view' => 'Normal view',
-		'older_first' => 'Oldest first',
 		'queries' => 'User queries',
 		'read' => 'Show read',
 		'reader_view' => 'Reading view',
 		'rss_view' => 'RSS feed',
 		'search_short' => 'Search',
 		'sort' => array(
-			'_' => 'Sorting criteria',
+			'asc' => 'Ascending',
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',
 				'name_desc' => 'Category, feed titles Z→A',
 			),
 			'date_asc' => 'Publication date 1→9',
 			'date_desc' => 'Publication date 9→1',
+			'desc' => 'Descending',
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',
 				'name_desc' => 'Feed title Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',
 			'link_asc' => 'Link A→Z',
 			'link_desc' => 'Link Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',
+			),
 			'rand' => 'Random order',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',
+			),
 			'title_asc' => 'Title A→Z',
 			'title_desc' => 'Title Z→A',
 			'user_modified_asc' => 'User modified 1→9',

+ 1 - 1
app/i18n/en/sub.php

@@ -211,7 +211,7 @@ return array(
 		'priority' => array(
 			'_' => 'Visibility',
 			'category' => 'Show in its category',
-			'feed' => 'Show in its feed',	// TODO
+			'feed' => 'Show in its feed',
 			'hidden' => 'Do not show',
 			'important' => 'Show in important feeds',
 			'main_stream' => 'Show in main stream',

+ 0 - 5
app/i18n/es/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Se aplica también en las etiquetas',
 		'sides_close_article' => 'Pinchar fuera del área de texto del artículo lo cerrará',
-		'sort' => array(
-			'_' => 'Orden',
-			'newer_first' => 'Nuevos primero',
-			'older_first' => 'Antiguos primero',
-		),
 		'star' => array(
 			'when' => 'Marca un artículo como favorito…',
 		),

+ 10 - 3
app/i18n/es/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Marcar fuente como leída',
 		'mark_selection_unread' => 'Marcar la selección como no leída',
 		'mylabels' => 'Mis etiquetas',
-		'newer_first' => 'Nuevos primero',
 		'non-starred' => 'Mostrar todos menos los favoritos',
 		'normal_view' => 'Vista normal',
-		'older_first' => 'Más antiguos primero',
 		'queries' => 'Búsquedas de usuario',
 		'read' => 'Mostrar solo los leídos',
 		'reader_view' => 'Vista de lectura',
 		'rss_view' => 'Fuente RSS',
 		'search_short' => 'Buscar',
 		'sort' => array(
-			'_' => 'Criterios de ordenación',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Categoría, títulos de fuentes A→Z',
 				'name_desc' => 'Categoría, títulos de fuentes Z→A',
 			),
 			'date_asc' => 'Fecha de publicación 1→9',
 			'date_desc' => 'Fecha de publicación 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Título de fuente A→Z',
 				'name_desc' => 'Título de fuente Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Longitud de contenido 9→1',
 			'link_asc' => 'Enlace A→Z',
 			'link_desc' => 'Enlace Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Orden aleatorio',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Título A→Z',
 			'title_desc' => 'Título Z→A',
 			'user_modified_asc' => 'Modificado por usuario 1→9',

+ 0 - 5
app/i18n/fa/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => ' روی برچسب ها نیز اعمال می شود',
 		'sides_close_article' => ' با کلیک کردن خارج از ناحیه متن مقاله',
-		'sort' => array(
-			'_' => ' ترتیب مرتب سازی',
-			'newer_first' => ' ابتدا جدیدترین',
-			'older_first' => ' اول قدیمی ترین',
-		),
 		'star' => array(
 			'when' => 'یک مطلب را به عنوان مورد علاقه علامت‌گذاری کن...',
 		),

+ 10 - 3
app/i18n/fa/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => ' فید را به عنوان خوانده شده علامت گذاری کنید',
 		'mark_selection_unread' => ' انتخاب را به عنوان خوانده نشده علامت گذاری کنید',
 		'mylabels' => ' برچسب های من',
-		'newer_first' => ' ابتدا جدیدتر',
 		'non-starred' => ' موارد غیر مورد علاقه را نشان دهید',
 		'normal_view' => ' نمای عادی',
-		'older_first' => ' اول مسن ترین',
 		'queries' => ' پرس و جوهای کاربر',
 		'read' => ' نمایش خوانده شده',
 		'reader_view' => ' مشاهده خواندن',
 		'rss_view' => ' خوراک RSS',
 		'search_short' => ' جستجو',
 		'sort' => array(
-			'_' => 'معیارهای مرتب‌سازی',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'دسته بندی، عناوین فید A→Z',
 				'name_desc' => 'دسته بندی، عناوین فید Z→A',
 			),
 			'date_asc' => 'تاریخ انتشار ۱→۹',
 			'date_desc' => 'تاریخ انتشار ۹→۱',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'عنوان فید A→Z',
 				'name_desc' => 'عنوان فید Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'لینک A→Z',
 			'link_desc' => 'لینک Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'ترتیب تصادفی',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'عنوانA→Z',
 			'title_desc' => 'عنوان Z→A',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/fi/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Koskee myös merkintöjä',
 		'sides_close_article' => 'Artikkeli sulkeutuu napsauttamalla sen ulkopuolelle',
-		'sort' => array(
-			'_' => 'Lajittelujärjestys',
-			'newer_first' => 'Uusimmat ensin',
-			'older_first' => 'Vanhimmat ensin',
-		),
 		'star' => array(
 			'when' => 'Merkitse artikkeli suosikiksi…',
 		),

+ 10 - 3
app/i18n/fi/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Merkitse syöte luetuksi',
 		'mark_selection_unread' => 'Merkitse valitut lukemattomiksi',
 		'mylabels' => 'Omat tunnisteet',
-		'newer_first' => 'Uusin ensin',
 		'non-starred' => 'Näytä muut kuin suosikit',
 		'normal_view' => 'Tavallinen näkymä',
-		'older_first' => 'Vanhin ensin',
 		'queries' => 'Käyttäjän tekemät kyselyt',
 		'read' => 'Näytä luetut',
 		'reader_view' => 'Lukunäkymä',
 		'rss_view' => 'RSS-syöte',
 		'search_short' => 'Haku',
 		'sort' => array(
-			'_' => 'Lajitteluehdot',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Luokka, syötteiden otsikot A→Ö',
 				'name_desc' => 'Luokka, syötteiden otsikot Ö→A',
 			),
 			'date_asc' => 'Julkaisupäivä 1→9',
 			'date_desc' => 'Julkaisupäivä 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Syötteen otsikko A→Ö',
 				'name_desc' => 'Syötteen otsikko Ö→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Linkki A→Ö',
 			'link_desc' => 'Linkki Ö→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Satunnainen järjestys',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Otsikko A→Ö',
 			'title_desc' => 'Otsikko Ö→A',
 			'user_modified_asc' => 'Käyttäjä muokannut 1→9',

+ 0 - 5
app/i18n/fr/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'S’applique aussi aux étiquettes',
 		'sides_close_article' => 'Cliquer hors de la zone de texte ferme l’article',
-		'sort' => array(
-			'_' => 'Ordre de tri',
-			'newer_first' => 'Plus récents en premier',
-			'older_first' => 'Plus anciens en premier',
-		),
 		'star' => array(
 			'when' => 'Marquer un article comme favori…',
 		),

+ 10 - 3
app/i18n/fr/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Marquer le flux comme lu',
 		'mark_selection_unread' => 'Marquer la sélection comme non-lue',
 		'mylabels' => 'Mes étiquettes',
-		'newer_first' => 'Plus récents en premier',
 		'non-starred' => 'Afficher les non-favoris',
 		'normal_view' => 'Vue normale',
-		'older_first' => 'Plus anciens en premier',
 		'queries' => 'Filtres utilisateurs',
 		'read' => 'Afficher les lus',
 		'reader_view' => 'Vue lecture',
 		'rss_view' => 'Flux RSS',
 		'search_short' => 'Rechercher',
 		'sort' => array(
-			'_' => 'Critère de tri',
+			'asc' => 'Ascendant',
 			'c' => array(
 				'name_asc' => 'Catégorie, flux (titres) A→Z',
 				'name_desc' => 'Catégorie, flux (titres) Z→A',
 			),
 			'date_asc' => 'Date de publication 1→9',
 			'date_desc' => 'Date de publication 9→1',
+			'desc' => 'Descendant',
 			'f' => array(
 				'name_asc' => 'Flux (titre) A→Z',
 				'name_desc' => 'Flux (titre) Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Longueur du contenu 9→1',
 			'link_asc' => 'Lien A→Z',
 			'link_desc' => 'Lien Z→A',
+			'primary' => array(
+				'_' => 'Critère de tri',
+				'help' => 'Le tri par date de <em>réception</em> est recommandé dans la plupart des cas, pour une meilleure cohérence et performance',
+			),
 			'rand' => 'Ordre aléatoire',
+			'secondary' => array(
+				'_' => 'Critère de tri secondaire',
+				'help' => 'Seulemement pertinent lorsque le critère de tri principal est sur les titres des catégories ou des flux',
+			),
 			'title_asc' => 'Titre A→Z',
 			'title_desc' => 'Titre Z→A',
 			'user_modified_asc' => 'Modifié par l’utilisateur 1→9',

+ 0 - 5
app/i18n/he/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Applies also on labels',	// TODO
 		'sides_close_article' => 'Clicking outside of article text area closes the article',	// TODO
-		'sort' => array(
-			'_' => 'סדר המיון',
-			'newer_first' => 'חדשים בראש',
-			'older_first' => 'ישנים יותר בראש',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/he/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'סימון הזנה כנקראה',
 		'mark_selection_unread' => 'Mark selection as unread',	// TODO
 		'mylabels' => 'My labels',	// TODO
-		'newer_first' => 'חדשים בראש',
 		'non-starred' => 'הצגת הכל פרט למועדפים',
 		'normal_view' => 'תצוגה רגילה',
-		'older_first' => 'ישנים יותר בראש',
 		'queries' => 'שאילתות',
 		'read' => 'הצגת נקראו בלבד',
 		'reader_view' => 'תצוגת קריאה',
 		'rss_view' => 'הזנת RSS',
 		'search_short' => 'חיפוש',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/hu/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'A címkékre is vonatkozik',
 		'sides_close_article' => 'A cikk szövegrészén kívüli kattintás bezárja a cikket',
-		'sort' => array(
-			'_' => 'Rendezési sorrend',
-			'newer_first' => 'Újabb elöl',
-			'older_first' => 'Régebbi elöl',
-		),
 		'star' => array(
 			'when' => 'Cikk megjelölése kedvencnek…',
 		),

+ 10 - 3
app/i18n/hu/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Hírforrás megjelölése olvasottként',
 		'mark_selection_unread' => 'Kijelöltek olvasatlanná tétele',
 		'mylabels' => 'Címkék',
-		'newer_first' => 'Újabbak elöl',
 		'non-starred' => 'Nem kedvencek megjelenítése',
 		'normal_view' => 'Normál nézet',
-		'older_first' => 'Régebbiek elöl',
 		'queries' => 'Felhasználói lekérdezések',
 		'read' => 'Olvasottak megjelenítése',
 		'reader_view' => 'Olvasó nézet',
 		'rss_view' => 'RSS hírforrás',
 		'search_short' => 'Keresés',
 		'sort' => array(
-			'_' => 'Rendezési sorrend',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Kategória, feed címek A→Z',
 				'name_desc' => 'Kategória, feed címek Z→A',
 			),
 			'date_asc' => 'Kiadás dátuma 1→9',
 			'date_desc' => 'Kiadás dátuma 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed cím A→Z',
 				'name_desc' => 'Feed cím Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Tartalom hossza 9→1',
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Véletlen sorrend',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Cím A→Z',
 			'title_desc' => 'Cím Z→A',
 			'user_modified_asc' => 'Felhasználói módosítás 1→9',

+ 0 - 5
app/i18n/id/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Berlaku juga pada label',
 		'sides_close_article' => 'Klik di luar area teks artikel untuk menutup artikel',
-		'sort' => array(
-			'_' => 'Kriteria pengurutan',
-			'newer_first' => 'Terbaru dulu',
-			'older_first' => 'Terlama dulu',
-		),
 		'star' => array(
 			'when' => 'Tandai artikel sebagai favorit…',
 		),

+ 10 - 3
app/i18n/id/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Tandai umpan sebagai sudah dibaca',
 		'mark_selection_unread' => 'Tandai yang dipilih sebagai belum dibaca',
 		'mylabels' => 'Label Saya',
-		'newer_first' => 'Yang terbaru dulu',
 		'non-starred' => 'Tampilkan yang tidak difavoritkan',
 		'normal_view' => 'Tampilan Normal',
-		'older_first' => 'Yang terlama dulu',
 		'queries' => 'Pencarian pengguna',
 		'read' => 'Tampilkan yang sudah dibaca',
 		'reader_view' => 'Tampilan Membaca',
 		'rss_view' => 'Umpan RSS',
 		'search_short' => 'Cari',
 		'sort' => array(
-			'_' => 'Kriteria pengurutan',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Tanggal publikasi 1→9',
 			'date_desc' => 'Tanggal publikasi 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Tautan A→Z',
 			'link_desc' => 'Tautan Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Acak',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Judul A→Z',
 			'title_desc' => 'Judul Z→A',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/it/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Si applica anche alle etichette',
 		'sides_close_article' => 'Cliccare fuori dall’area di testo dell’articolo chiude l’articolo',
-		'sort' => array(
-			'_' => 'Ordinamento',
-			'newer_first' => 'Prima i più recenti',
-			'older_first' => 'Prima i più vecchi',
-		),
 		'star' => array(
 			'when' => 'Segna un articolo come preferito…',
 		),

+ 10 - 3
app/i18n/it/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Segna il feed come letto',
 		'mark_selection_unread' => 'Segna i selezionati come non letti',
 		'mylabels' => 'Le mie etichette',
-		'newer_first' => 'Mostra prima i recenti',
 		'non-starred' => 'Escludi preferiti',
 		'normal_view' => 'Vista elenco',
-		'older_first' => 'Ordina per meno recenti',
 		'queries' => 'Chiavi di ricerca',
 		'read' => 'Mostra solo letti',
 		'reader_view' => 'Modalità di lettura',
 		'rss_view' => 'Feed RSS',
 		'search_short' => 'Cerca',
 		'sort' => array(
-			'_' => 'Ordina per',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Categoria, titolo del feed A→Z',
 				'name_desc' => 'Categoria, titolo del feed Z→A',
 			),
 			'date_asc' => 'Data di pubblicazione 1→9',
 			'date_desc' => 'Data di pubblicazione 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Titolo del feed A→Z',
 				'name_desc' => 'Titolo del feed Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Lunghezza contenuto 9→1',
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Ordine casuale',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Titolo A→Z',
 			'title_desc' => 'Titolo Z→A',
 			'user_modified_asc' => 'Modificato dall’utente 1→9',

+ 0 - 5
app/i18n/ja/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'ラベルも適用する',
 		'sides_close_article' => '記事の外をクリックすると記事を閉じるようにする',
-		'sort' => array(
-			'_' => '順序',
-			'newer_first' => '最新のものを先頭にする',
-			'older_first' => '最古のものを先頭にする',
-		),
 		'star' => array(
 			'when' => '記事をお気に入りに登録する。',
 		),

+ 10 - 3
app/i18n/ja/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'フィードを既読にする',
 		'mark_selection_unread' => '選択した記事を未読にする',
 		'mylabels' => 'ラベル',
-		'newer_first' => '最新の記事を先頭にする',
 		'non-starred' => 'お気に入りに登録されてない記事を表示する',
 		'normal_view' => 'ノーマルビュー',
-		'older_first' => '最古の記事を先頭にする',
 		'queries' => 'ユーザークエリ',
 		'read' => '既読の記事を表示する',
 		'reader_view' => 'リーディングビュー',
 		'rss_view' => 'RSSフィード',
 		'search_short' => '検索',
 		'sort' => array(
-			'_' => '並べ替え',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => '公開日順 1→9',
 			'date_desc' => '公開日順 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'リンクURL順 A→Z',
 			'link_desc' => 'リンクURL順 Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'ランダムに並べる',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'タイトル順 A→Z',
 			'title_desc' => 'タイトル順 Z→A',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/ko/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => '라벨에도 적용하기',
 		'sides_close_article' => '글 영역 바깥을 클릭하면 글 접기',
-		'sort' => array(
-			'_' => '정렬 순서',
-			'newer_first' => '최근 글 먼저',
-			'older_first' => '오래된 글 먼저',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/ko/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => '피드를 읽음으로 표시',
 		'mark_selection_unread' => '선택된 글을 읽지 않음으로 표시',
 		'mylabels' => '내 라벨',
-		'newer_first' => '최근 글 먼저',
 		'non-starred' => '즐겨찾기를 제외하고 표시',
 		'normal_view' => '일반 모드',
-		'older_first' => '오래된 글 먼저',
 		'queries' => '사용자 쿼리',
 		'read' => '읽은 글만 표시',
 		'reader_view' => '읽기 모드',
 		'rss_view' => 'RSS 피드',
 		'search_short' => '검색',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/lv/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Attiecas arī uz birkām',
 		'sides_close_article' => 'Spiežot ārpus raksta teksta apgabala, raksts tiek aizvērts',
-		'sort' => array(
-			'_' => 'Kārtošanas kārtība',
-			'newer_first' => 'Sākumā jaunākos',
-			'older_first' => 'Sākumā vecākos',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/lv/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Atzīmēt barotni kā izlasītu',
 		'mark_selection_unread' => 'Atzīmēt izvēlni kā izlasītu',
 		'mylabels' => 'Manas birkas',
-		'newer_first' => 'Sākumā jaunākos',
 		'non-starred' => 'Rādīt neiecienītākos',
 		'normal_view' => 'Parastais skats',
-		'older_first' => 'Sākumā vecākos',
 		'queries' => 'Lietotāja pieprasījumi',
 		'read' => 'Rādīt izlasītos',
 		'reader_view' => 'Lasīšanas skats',
 		'rss_view' => 'RSS barotne',
 		'search_short' => 'Meklēt',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/nl/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Ook toepassen op labels',
 		'sides_close_article' => 'Sluit het artikel door buiten de artikeltekst te klikken',
-		'sort' => array(
-			'_' => 'Sorteer volgorde',
-			'newer_first' => 'Nieuwste eerst',
-			'older_first' => 'Oudste eerst',
-		),
 		'star' => array(
 			'when' => 'Markeer een artikel als favoriet…',
 		),

+ 10 - 3
app/i18n/nl/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Markeer feed als gelezen',
 		'mark_selection_unread' => 'Markeer selectie als ongelezen',
 		'mylabels' => 'Mijn labels',
-		'newer_first' => 'Nieuwste eerst',
 		'non-starred' => 'Niet-favorieten tonen',
 		'normal_view' => 'Normale weergave',
-		'older_first' => 'Oudste eerst',
 		'queries' => 'Gebruikers queries',
 		'read' => 'Gelezen tonen',
 		'reader_view' => 'Leesmodus',
 		'rss_view' => 'RSS-feed',
 		'search_short' => 'Zoeken',
 		'sort' => array(
-			'_' => 'Sorteercriteria',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Categorie, feedtitels A→Z',
 				'name_desc' => 'Categorie, feedtitels Z→A',
 			),
 			'date_asc' => 'Publicatiedatum 1→9',
 			'date_desc' => 'Publicatiedatum 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feedtitel A→Z',
 				'name_desc' => 'Feedtitel Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Lengte van inhoud 9→1',
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Willekeurige volgorde',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Titel A→Z',
 			'title_desc' => 'Titel Z→A',
 			'user_modified_asc' => 'Aangepast door gebruiker 1→9',

+ 0 - 5
app/i18n/oc/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Aplicar tanben a las etiquetas',
 		'sides_close_article' => 'Clicar fòra de la zòna de tèxte tampa l’article',
-		'sort' => array(
-			'_' => 'Òrdre de tria',
-			'newer_first' => 'Mai recents en primièr',
-			'older_first' => 'Mai ancians en primièr',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/oc/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Marcar lo flux coma legit',
 		'mark_selection_unread' => 'Marcar la seleccion coma pas legida',
 		'mylabels' => 'Mas etiquetas',
-		'newer_first' => 'Mai recents en primièr',
 		'non-starred' => 'Mostrar los pas favorits',
 		'normal_view' => 'Vista normala',
-		'older_first' => 'Mai ancians en primièr',
 		'queries' => 'Filtres utilizaire',
 		'read' => 'Mostrar los legits',
 		'reader_view' => 'Vista lectura',
 		'rss_view' => 'Flux RSS',
 		'search_short' => 'Recercar',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/pl/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Stosuje się również do etykiet',
 		'sides_close_article' => 'Kliknięcie poza zawartością wiadomości zamyka widok wiadomości',
-		'sort' => array(
-			'_' => 'Porządek sortowania',
-			'newer_first' => 'Najpierw najnowsze',
-			'older_first' => 'Najpierw najstarsze',
-		),
 		'star' => array(
 			'when' => 'Oznacz artykuł jako ulubiony…',
 		),

+ 10 - 3
app/i18n/pl/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Oznacz kanał jako przeczytany',
 		'mark_selection_unread' => 'Oznacz wiadomości jako nieprzeczytane',
 		'mylabels' => 'Własne etykiety',
-		'newer_first' => 'Najpierw najnowsze',
 		'non-starred' => 'Pokaż wiadomości, które nie są ulubione',
 		'normal_view' => 'Widok normalny',
-		'older_first' => 'Najpierw najstarsze',
 		'queries' => 'Zapisane zapytania',
 		'read' => 'Pokaż przeczytane',
 		'reader_view' => 'Widok czytania',
 		'rss_view' => 'Kanał RSS',
 		'search_short' => 'Szukaj',
 		'sort' => array(
-			'_' => 'Kryteria sortowania',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Tytuł kategorii i kanału A→Z',
 				'name_desc' => 'Tytuł kategorii i kanału Z→A',
 			),
 			'date_asc' => 'Data publikacji 1→9',
 			'date_desc' => 'Data publikacji 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Tytuł kanału A→Z',
 				'name_desc' => 'Tytuł kanału Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Długość zawartości 9→1',
 			'link_asc' => 'Odnośnik A→Z',
 			'link_desc' => 'Odnośnik Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Losowa kolejność',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Tytuł A→Z',
 			'title_desc' => 'Tytuł Z→A',
 			'user_modified_asc' => 'Zmodyfikowane przez użytkownika 1→9',

+ 0 - 5
app/i18n/pt-BR/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Aplicar também nas tags',
 		'sides_close_article' => 'Clicando fora da área do texto do artigo fecha o mesmo',
-		'sort' => array(
-			'_' => 'Ordem de visualização',
-			'newer_first' => 'Novos primeiro',
-			'older_first' => 'Antigos primeiro',
-		),
 		'star' => array(
 			'when' => 'Marque um artigo como favorito…',
 		),

+ 10 - 3
app/i18n/pt-BR/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Marcar feed com lido',
 		'mark_selection_unread' => 'Marcar seleção como não lida',
 		'mylabels' => 'Minhas etiquetas',
-		'newer_first' => 'Novos primeiro',
 		'non-starred' => 'Mostrar itens que não são favoritos',
 		'normal_view' => 'visualização normal',
-		'older_first' => 'Antigos primeiro',
 		'queries' => 'Queries do usuário',
 		'read' => 'Mostrar leitura',
 		'reader_view' => 'Visualização de leitura',
 		'rss_view' => 'Feed RSS',
 		'search_short' => 'Buscar',
 		'sort' => array(
-			'_' => 'Critérios de ordenação',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Categoria, títulos dos feeds A→Z',
 				'name_desc' => 'Categoria, títulos dos feeds Z→A',
 			),
 			'date_asc' => 'Data de publicação 1→9',
 			'date_desc' => 'Data de publicação 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Título do feed A→Z',
 				'name_desc' => 'Título do feed Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Comprimento do conteúdo 9→1',
 			'link_asc' => 'Link A→Z',	// IGNORE
 			'link_desc' => 'Link Z→A',	// IGNORE
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Ordem aleatória',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Título A→Z',
 			'title_desc' => 'Título Z→A',
 			'user_modified_asc' => 'Modificado pelo usuário 1→9',

+ 0 - 5
app/i18n/pt-PT/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Aplicar também nas tags',
 		'sides_close_article' => 'Clicando fora da área do texto do artigo fecha o mesmo',
-		'sort' => array(
-			'_' => 'Ordem de visualização',
-			'newer_first' => 'Novos primeiro',
-			'older_first' => 'Antigos primeiro',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/pt-PT/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Marcar feed com lido',
 		'mark_selection_unread' => 'Marcar seleção como não lida',
 		'mylabels' => 'Minhas etiquetas',
-		'newer_first' => 'Novos primeiro',
 		'non-starred' => 'Mostrar todos, exceto favoritos',
 		'normal_view' => 'visualização normal',
-		'older_first' => 'Antigos primeiro',
 		'queries' => 'Queries do utilizador',
 		'read' => 'Mostrar apenas lidos',
 		'reader_view' => 'Visualização de leitura',
 		'rss_view' => 'Feed RSS',
 		'search_short' => 'Pesquisar',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/ru/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Также относится к меткам',
 		'sides_close_article' => 'Нажатия мышью за пределами текста статьи закрывают статью',
-		'sort' => array(
-			'_' => 'Порядок сортировки',
-			'newer_first' => 'Сначала новые',
-			'older_first' => 'Сначала старые',
-		),
 		'star' => array(
 			'when' => 'Отмечать статью избранной…',
 		),

+ 10 - 3
app/i18n/ru/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Отметить ленту прочитанной',
 		'mark_selection_unread' => 'Отметить выделение прочитанным',
 		'mylabels' => 'Мои метки',
-		'newer_first' => 'Сначала новые',
 		'non-starred' => 'Показать неизбранное',
 		'normal_view' => 'Обычный вид',
-		'older_first' => 'Сначала старые',
 		'queries' => 'Запросы',
 		'read' => 'Показать прочитанное',
 		'reader_view' => 'Вид для чтения',
 		'rss_view' => 'RSS-лента',
 		'search_short' => 'Поиск',
 		'sort' => array(
-			'_' => 'Критерии сортировки',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Категории, названия лент А→Я',
 				'name_desc' => 'Категории, названия лент Я→А',
 			),
 			'date_asc' => 'Дата публикации 1→9',
 			'date_desc' => 'Дата публикации 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Названия лент А→Я',
 				'name_desc' => 'Названия лент Я→А',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Длина контента 9→1',
 			'link_asc' => 'Ссылка А→Я',
 			'link_desc' => 'Ссылка Я→А',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Случайный порядок',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Заголовок А→Я',
 			'title_desc' => 'Заголовок Я→А',
 			'user_modified_asc' => 'Изменено пользователем 1→9',

+ 0 - 5
app/i18n/sk/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Týka sa aj štítkov',
 		'sides_close_article' => 'Po kliknutí mimo textu článku sa článok zatvorí',
-		'sort' => array(
-			'_' => 'Poradie',
-			'newer_first' => 'Novšie hore',
-			'older_first' => 'Staršie hore',
-		),
 		'star' => array(
 			'when' => 'Mark an article as favourite…',	// TODO
 		),

+ 10 - 3
app/i18n/sk/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Označiť kanál ako prečítaný',
 		'mark_selection_unread' => 'Označiť označené ako prečítané',
 		'mylabels' => 'Moje nálepky',
-		'newer_first' => 'Novšie hore',
 		'non-starred' => 'Zobraziť všetko okrem obľúbených',
 		'normal_view' => 'Základné zobrazenie',
-		'older_first' => 'Staršie hore',
 		'queries' => 'Používateľské dopyty',
 		'read' => 'Zobraziť prečítané',
 		'reader_view' => 'Zobrazenie na čítanie',
 		'rss_view' => 'RSS kanál',
 		'search_short' => 'Hľadať',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/tr/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Etiketler için de geçerlidir',
 		'sides_close_article' => 'Makale metin alanının dışına tıklayınca makaleyi kapat',
-		'sort' => array(
-			'_' => 'Sıralama düzeni',
-			'newer_first' => 'Önce yeniler',
-			'older_first' => 'Önce eskiler',
-		),
 		'star' => array(
 			'when' => 'Bir makaleyi favori olarak işaretle…',
 		),

+ 10 - 3
app/i18n/tr/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Beslemeyi okundu olarak işaretle',
 		'mark_selection_unread' => 'Seçimi okunmadı olarak işaretle',
 		'mylabels' => 'Etiketlerim',
-		'newer_first' => 'Önce yeniler',
 		'non-starred' => 'Favori olmayanları göster',
 		'normal_view' => 'Normal görünüm',
-		'older_first' => 'Önce eskiler',
 		'queries' => 'Kullanıcı sorguları',
 		'read' => 'Okunanları göster',
 		'reader_view' => 'Okuma görünümü',
 		'rss_view' => 'RSS beslemesi',
 		'search_short' => 'Ara',
 		'sort' => array(
-			'_' => 'Sıralama kriteri',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Yayın tarihi 1→9',
 			'date_desc' => 'Yayın tarihi 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Bağlantı A→Z',
 			'link_desc' => 'Bağlantı Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Rastgele sıralama',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Başlık A→Z',
 			'title_desc' => 'Başlık Z→A',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/uk/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => 'Впливає також на мітки',
 		'sides_close_article' => 'Натиск за межами тексту статті закриває статтю',
-		'sort' => array(
-			'_' => 'Порядок',
-			'newer_first' => 'Спершу новіші',
-			'older_first' => 'Спершу старіші',
-		),
 		'star' => array(
 			'when' => 'Вподобати статтю…',
 		),

+ 10 - 3
app/i18n/uk/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => 'Позначити стрічку прочитаною',
 		'mark_selection_unread' => 'Позначити вибрані непрочитаними',
 		'mylabels' => 'Мої мітки',
-		'newer_first' => 'Спершу новіші',
 		'non-starred' => 'Показати невподобані',
 		'normal_view' => 'Звичайний показ',
-		'older_first' => 'Спершу старіші',
 		'queries' => 'Користувацькі запити',
 		'read' => 'Показати прочитані',
 		'reader_view' => 'Читацький показ',
 		'rss_view' => 'RSS-стрічка',
 		'search_short' => 'Пошук',
 		'sort' => array(
-			'_' => 'Критерії впорядкування',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Заголовки категорії та стрічки А→Я',
 				'name_desc' => 'Заголовки категорії та стрічки Я→А',
 			),
 			'date_asc' => 'Дата оприлюднення 1→9',
 			'date_desc' => 'Дата оприлюднення 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Назва стрічки A→Z',
 				'name_desc' => 'Назва стрічки Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Посилання А→Я',
 			'link_desc' => 'Посилання Я→А',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Довільний порядок',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Заголовок А→Я',
 			'title_desc' => 'Заголовок Я→А',
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 0 - 5
app/i18n/zh-CN/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => '同样适用于标签',
 		'sides_close_article' => '点击文章文本区域外关闭文章',
-		'sort' => array(
-			'_' => '排列顺序',
-			'newer_first' => '由新至旧',
-			'older_first' => '由旧至新',
-		),
 		'star' => array(
 			'when' => '将文章标记为收藏时…',
 		),

+ 10 - 3
app/i18n/zh-CN/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => '此订阅源设为已读',
 		'mark_selection_unread' => '将筛选结果标记为未读',
 		'mylabels' => '我的标签',
-		'newer_first' => '由新至旧',
 		'non-starred' => '显示未收藏',
 		'normal_view' => '普通视图',
-		'older_first' => '由旧至新',
 		'queries' => '自定义查询',
 		'read' => '显示已读',
 		'reader_view' => '阅读视图',
 		'rss_view' => '订阅源',
 		'search_short' => '搜索',
 		'sort' => array(
-			'_' => '排序标准',
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => '分类、订阅源标题 A→Z',
 				'name_desc' => '分类、订阅源标题 Z→A',
 			),
 			'date_asc' => '发布日期 1→9',
 			'date_desc' => '发布日期 9→1',
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => '订阅源标题 A→Z',
 				'name_desc' => '订阅源标题 Z→A',
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => '内容长度 9→1',
 			'link_asc' => '链接 A→Z',
 			'link_desc' => '链接 Z→A',
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => '随机顺序',
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => '标题 A→Z',
 			'title_desc' => '标题 Z→A',
 			'user_modified_asc' => '用户修改 1→9',

+ 0 - 5
app/i18n/zh-TW/conf.php

@@ -294,11 +294,6 @@ return array(
 		),
 		'show_fav_unread_help' => '同樣適用於標籤',
 		'sides_close_article' => '點擊文章區域外以關閉',
-		'sort' => array(
-			'_' => '排列順序',
-			'newer_first' => '由新至舊',
-			'older_first' => '由舊至新',
-		),
 		'star' => array(
 			'when' => '標記一篇文章為最愛…',
 		),

+ 10 - 3
app/i18n/zh-TW/index.php

@@ -77,23 +77,22 @@ return array(
 		'mark_feed_read' => '此訂閱源設為已讀',
 		'mark_selection_unread' => '選中設為已讀',
 		'mylabels' => '我的標籤',
-		'newer_first' => '由新至舊',
 		'non-starred' => '顯示未收藏',
 		'normal_view' => '普通視圖',
-		'older_first' => '由舊至新',
 		'queries' => '自定義查詢',
 		'read' => '顯示已讀',
 		'reader_view' => '閱讀視圖',
 		'rss_view' => '訂閱源',
 		'search_short' => '搜尋',
 		'sort' => array(
-			'_' => 'Sorting criteria',	// TODO
+			'asc' => 'Ascending',	// TODO
 			'c' => array(
 				'name_asc' => 'Category, feed titles A→Z',	// TODO
 				'name_desc' => 'Category, feed titles Z→A',	// TODO
 			),
 			'date_asc' => 'Publication date 1→9',	// TODO
 			'date_desc' => 'Publication date 9→1',	// TODO
+			'desc' => 'Descending',	// TODO
 			'f' => array(
 				'name_asc' => 'Feed title A→Z',	// TODO
 				'name_desc' => 'Feed title Z→A',	// TODO
@@ -104,7 +103,15 @@ return array(
 			'length_desc' => 'Content length 9→1',	// TODO
 			'link_asc' => 'Link A→Z',	// TODO
 			'link_desc' => 'Link Z→A',	// TODO
+			'primary' => array(
+				'_' => 'Sorting criterion',	// TODO
+				'help' => 'Sorting by <em>received</em> date is recommended in most cases, for consistency and performance',	// TODO
+			),
 			'rand' => 'Random order',	// TODO
+			'secondary' => array(
+				'_' => 'Secondary sorting criterion',	// TODO
+				'help' => 'Only relevant when the primary sorting criterion is categories or feeds titles',	// TODO
+			),
 			'title_asc' => 'Title A→Z',	// TODO
 			'title_desc' => 'Title Z→A',	// TODO
 			'user_modified_asc' => 'User modified 1→9',	// TODO

+ 3 - 3
app/layout/nav_menu.phtml

@@ -227,17 +227,17 @@
 	<?php
 		if (FreshRSS_Context::$order === 'ASC') {
 			$icon = 'sort-up';
-			$title = _t('index.menu.older_first');
+			$title = _t('index.menu.sort.asc');
 		} else {
 			$icon = 'sort-down';
-			$title = _t('index.menu.newer_first');
+			$title = _t('index.menu.sort.desc');
 		}
 		$url_order = Minz_Request::currentRequest();
 	?>
 	<div id="nav_menu_sort" class="group">
 		<div class="dropdown">
 			<div id="dropdown-sort" class="dropdown-target"></div>
-			<a id="toggle-order" class="dropdown-toggle btn" href="#dropdown-sort" title="<?= _t('index.menu.sort') ?>"><?= _i($icon) ?></a>
+			<a id="toggle-order" class="dropdown-toggle btn" href="#dropdown-sort" title="<?= _t('index.menu.sort.primary') ?>"><?= _i($icon) ?></a>
 			<ul class="dropdown-menu" role="radiogroup">
 				<li class="item" role="radio" aria-checked="<?= FreshRSS_Context::$order === 'DESC' && FreshRSS_Context::$sort === 'id' ? 'true' : 'false' ?>">
 					<a href="<?= Minz_Url::display($url_order, amend: ['params' => ['sort' => 'id', 'order' => 'DESC']]) ?>"><?= _t('index.menu.sort.id_desc') ?></a></li>

+ 48 - 5
app/views/configure/reading.phtml

@@ -74,16 +74,59 @@
 				</div>
 			</div>
 
+			<?php
+				$userSort = FreshRSS_Context::userConf()->sort;
+				$userSortOrder = FreshRSS_Context::userConf()->sort_order;
+				$userSortCombined = $userSort === 'rand' ? 'rand' : $userSort . '_' . strtolower($userSortOrder ?? 'desc');
+			?>
 			<div class="form-group">
-				<label class="group-name" for="sort_order"><?= _t('conf.reading.sort') ?></label>
-				<div class="group-controls">
-					<select name="sort_order" id="sort_order">
-						<option value="DESC"<?= FreshRSS_Context::userConf()->sort_order === 'DESC' ? ' selected="selected"' : '' ?>><?= _t('conf.reading.sort.newer_first') ?></option>
-						<option value="ASC"<?= FreshRSS_Context::userConf()->sort_order === 'ASC' ? ' selected="selected"' : '' ?>><?= _t('conf.reading.sort.older_first') ?></option>
+				<label class="group-name" for="primary_sort"><?= _t('index.menu.sort.primary') ?></label>
+				<div class="group-controls">
+					<select name="primary_sort" id="primary_sort">
+						<option value="id_desc" <?= $userSortCombined === 'id_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_desc') ?></option>
+						<option value="date_desc" <?= $userSortCombined === 'date_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_desc') ?></option>
+						<option value="length_desc" <?= $userSortCombined === 'length_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_desc') ?></option>
+						<option value="link_desc" <?= $userSortCombined === 'link_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_desc') ?></option>
+						<option value="title_desc" <?= $userSortCombined === 'title_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_desc') ?></option>
+						<option value="f.name_desc" <?= $userSortCombined === 'f.name_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.f.name_desc') ?></option>
+						<option value="c.name_desc" <?= $userSortCombined === 'c.name_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.c.name_desc') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="rand" <?= $userSortCombined === 'rand' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.rand') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="id_asc" <?= $userSortCombined === 'id_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_asc') ?></option>
+						<option value="date_asc" <?= $userSortCombined === 'date_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_asc') ?></option>
+						<option value="length_asc" <?= $userSortCombined === 'length_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_asc') ?></option>
+						<option value="link_asc" <?= $userSortCombined === 'link_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_asc') ?></option>
+						<option value="title_asc" <?= $userSortCombined === 'title_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_asc') ?></option>
+						<option value="f.name_asc" <?= $userSortCombined === 'f.name_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.f.name_asc') ?></option>
+						<option value="c.name_asc" <?= $userSortCombined === 'c.name_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.c.name_asc') ?></option>
 					</select>
+					<p class="help"><?= _i('help') ?> <?= _t('index.menu.sort.primary.help') ?></p>
 				</div>
 			</div>
 
+			<?php
+				$userSecondarySort = FreshRSS_Context::userConf()->secondary_sort;
+				$userSecondarySortOrder = FreshRSS_Context::userConf()->secondary_sort_order;
+				$userSecondarySortCombined = $userSecondarySort === 'rand' ? 'rand' : $userSecondarySort . '_' . strtolower($userSecondarySortOrder ?? 'desc');
+			?>
+			<div class="form-group">
+				<label class="group-name" for="secondary_sort"><?= _t('index.menu.sort.secondary') ?></label>
+				<div class="group-controls">
+					<select name="secondary_sort" id="secondary_sort">
+						<option value="id_desc" <?= $userSecondarySortCombined === 'id_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_desc') ?></option>
+						<option value="date_desc" <?= $userSecondarySortCombined === 'date_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_desc') ?></option>
+						<option value="link_desc" <?= $userSecondarySortCombined === 'link_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_desc') ?></option>
+						<option value="title_desc" <?= $userSecondarySortCombined === 'title_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_desc') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="id_asc" <?= $userSecondarySortCombined === 'id_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_asc') ?></option>
+						<option value="date_asc" <?= $userSecondarySortCombined === 'date_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_asc') ?></option>
+						<option value="link_asc" <?= $userSecondarySortCombined === 'link_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_asc') ?></option>
+						<option value="title_asc" <?= $userSecondarySortCombined === 'title_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_asc') ?></option>
+					</select>
+					<p class="help"><?= _i('help') ?> <?= _t('index.menu.sort.secondary.help') ?></p>
+				</div>
+			</div>
 		</fieldset>
 
 		<fieldset>

+ 31 - 0
app/views/helpers/category/update.phtml

@@ -36,6 +36,37 @@
 				</div>
 			</div>
 
+			<?php
+				$categoryDefaultSort = $this->category->defaultSort();
+				$categoryDefaultOrder = $this->category->defaultOrder();
+				$categoryDefaultSortOrder = $categoryDefaultSort !== null
+					? ($categoryDefaultSort === 'rand' ? 'rand' : $categoryDefaultSort . '_' . strtolower($categoryDefaultOrder ?? 'desc'))
+					: '';
+			?>
+			<div class="form-group">
+				<label class="group-name" for="defaultSortOrder"><?= _t('index.menu.sort.primary') ?></label>
+				<div class="group-controls">
+					<select name="defaultSortOrder" id="defaultSortOrderCategory" class="w50">
+						<option value=""></option>
+						<option value="id_desc" <?= $categoryDefaultSortOrder === 'id_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_desc') ?></option>
+						<option value="date_desc" <?= $categoryDefaultSortOrder === 'date_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_desc') ?></option>
+						<option value="length_desc" <?= $categoryDefaultSortOrder === 'length_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_desc') ?></option>
+						<option value="link_desc" <?= $categoryDefaultSortOrder === 'link_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_desc') ?></option>
+						<option value="title_desc" <?= $categoryDefaultSortOrder === 'title_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_desc') ?></option>
+						<option value="f.name_desc" <?= $categoryDefaultSortOrder === 'f.name_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.f.name_desc') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="rand" <?= $categoryDefaultSortOrder === 'rand' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.rand') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="id_asc" <?= $categoryDefaultSortOrder === 'id_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_asc') ?></option>
+						<option value="date_asc" <?= $categoryDefaultSortOrder === 'date_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_asc') ?></option>
+						<option value="length_asc" <?= $categoryDefaultSortOrder === 'length_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_asc') ?></option>
+						<option value="link_asc" <?= $categoryDefaultSortOrder === 'link_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_asc') ?></option>
+						<option value="title_asc" <?= $categoryDefaultSortOrder === 'title_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_asc') ?></option>
+						<option value="f.name_asc" <?= $categoryDefaultSortOrder === 'f.name_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.f.name_asc') ?></option>
+					</select>
+				</div>
+			</div>
+
 			<div class="form-group form-actions">
 				<div class="group-controls">
 					<button type="submit" class="btn btn-important"><?= _t('gen.action.submit') ?></button>

+ 30 - 0
app/views/helpers/feed/update.phtml

@@ -130,6 +130,36 @@
 				</div>
 			</div>
 
+
+			<?php
+				$feedDefaultSort = $this->feed->defaultSort();
+				$feedDefaultOrder = $this->feed->defaultOrder();
+				$feedDefaultSortOrder = $feedDefaultSort !== null
+					? ($feedDefaultSort === 'rand' ? 'rand' : $feedDefaultSort . '_' . strtolower($feedDefaultOrder ?? 'desc'))
+					: '';
+			?>
+			<div class="form-group">
+				<label class="group-name" for="defaultSortOrder"><?= _t('index.menu.sort.primary') ?></label>
+				<div class="group-controls">
+					<select name="defaultSortOrder" id="defaultSortOrderFeed" class="w50">
+						<option value=""></option>
+						<option value="id_desc" <?= $feedDefaultSortOrder === 'id_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_desc') ?></option>
+						<option value="date_desc" <?= $feedDefaultSortOrder === 'date_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_desc') ?></option>
+						<option value="length_desc" <?= $feedDefaultSortOrder === 'length_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_desc') ?></option>
+						<option value="link_desc" <?= $feedDefaultSortOrder === 'link_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_desc') ?></option>
+						<option value="title_desc" <?= $feedDefaultSortOrder === 'title_desc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_desc') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="rand" <?= $feedDefaultSortOrder === 'rand' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.rand') ?></option>
+						<option disabled="disabled">────────────────</option>
+						<option value="id_asc" <?= $feedDefaultSortOrder === 'id_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.id_asc') ?></option>
+						<option value="date_asc" <?= $feedDefaultSortOrder === 'date_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.date_asc') ?></option>
+						<option value="length_asc" <?= $feedDefaultSortOrder === 'length_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.length_asc') ?></option>
+						<option value="link_asc" <?= $feedDefaultSortOrder === 'link_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.link_asc') ?></option>
+						<option value="title_asc" <?= $feedDefaultSortOrder === 'title_asc' ? 'selected="selected"' : '' ?>><?= _t('index.menu.sort.title_asc') ?></option>
+					</select>
+				</div>
+			</div>
+
 			<div class="form-group">
 				<label class="group-name" for="unicityCriteria"><?= _t('sub.feed.unicityCriteria') ?></label>
 				<?php

+ 6 - 2
config-user.default.php

@@ -53,10 +53,13 @@ return array (
 	#	Set to `true` to mark it unread, or `false` to leave it as-is.
 	'mark_updated_article_unread' => false, //TODO: -1 => ignore, 0 => update, 1 => update and mark as unread
 
-	# 'id'|'c.name'|'date'|'f.name'|'link'|'title'|'rand'|'lastUserModified'|'length'
+	# 'id'|'c.name'|'date'|'f.name'|'length'|'link'|'rand'|'title'
 	'sort' => 'id',
-	'mark_read_button' => 'big',
 	'sort_order' => 'DESC',
+	# 'id'|'date'|'link'|'title'
+	'secondary_sort' => 'id',
+	'secondary_sort_order' => 'DESC',
+
 	'anon_access' => false,
 	'mark_when' => array (
 		'article' => true,
@@ -105,6 +108,7 @@ return array (
 	# Hide the dropdown configuration menu and favicon in the aside list in case of many feeds, for UI performance
 	'simplify_over_n_feeds' => 1000,
 
+	'mark_read_button' => 'big',
 	'topline_read' => true,
 	'topline_favorite' => true,
 	'topline_myLabels' => false,