From e8924c9ab7faad8e224ccc96e7c84ba12928630e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:56:19 +0200 Subject: [PATCH 01/11] feat(containers): sort the tiles in a container by hand, alphabetically, by most or last used, or at random Most and last used read counts kept in the viewer's own browser, never sent to the server; a viewer can forget them. The order is applied when the page is viewed and never written back, and random holds for the page load. REQ-TSO-001..004, REQ-CONT-007 (launcher-tile-sorting) --- l10n/be.js | 13 +- l10n/be.json | 13 +- l10n/bg.js | 13 +- l10n/bg.json | 13 +- l10n/bs.js | 13 +- l10n/bs.json | 13 +- l10n/ca.js | 13 +- l10n/ca.json | 13 +- l10n/cs.js | 13 +- l10n/cs.json | 13 +- l10n/da.js | 13 +- l10n/da.json | 13 +- l10n/de.js | 13 +- l10n/de.json | 13 +- l10n/el.js | 13 +- l10n/el.json | 13 +- l10n/en.js | 13 +- l10n/en.json | 13 +- l10n/es.js | 13 +- l10n/es.json | 13 +- l10n/et.js | 13 +- l10n/et.json | 13 +- l10n/fi.js | 13 +- l10n/fi.json | 13 +- l10n/fr.js | 13 +- l10n/fr.json | 13 +- l10n/ga.js | 13 +- l10n/ga.json | 13 +- l10n/hr.js | 13 +- l10n/hr.json | 13 +- l10n/hu.js | 13 +- l10n/hu.json | 13 +- l10n/is.js | 13 +- l10n/is.json | 13 +- l10n/it.js | 13 +- l10n/it.json | 13 +- l10n/lb.js | 13 +- l10n/lb.json | 13 +- l10n/lt.js | 13 +- l10n/lt.json | 13 +- l10n/lv.js | 13 +- l10n/lv.json | 13 +- l10n/mk.js | 13 +- l10n/mk.json | 13 +- l10n/mt.js | 13 +- l10n/mt.json | 13 +- l10n/nb.js | 13 +- l10n/nb.json | 13 +- l10n/nl.js | 13 +- l10n/nl.json | 13 +- l10n/pl.js | 13 +- l10n/pl.json | 13 +- l10n/pt.js | 13 +- l10n/pt.json | 13 +- l10n/rm.js | 13 +- l10n/rm.json | 13 +- l10n/ro.js | 13 +- l10n/ro.json | 13 +- l10n/ru.js | 13 +- l10n/ru.json | 13 +- l10n/sk.js | 13 +- l10n/sk.json | 13 +- l10n/sl.js | 13 +- l10n/sl.json | 13 +- l10n/sq.js | 13 +- l10n/sq.json | 13 +- l10n/sr.js | 13 +- l10n/sr.json | 13 +- l10n/sv.js | 13 +- l10n/sv.json | 13 +- l10n/tr.js | 13 +- l10n/tr.json | 13 +- l10n/uk.js | 13 +- l10n/uk.json | 13 +- .../Widgets/Renderers/ContainerWidget.vue | 177 +++++++++++++- .../Widgets/Renderers/ContainerWidgetForm.vue | 139 +++++++++++ .../__tests__/ContainerWidget.sort.spec.js | 226 ++++++++++++++++++ .../__tests__/ContainerWidgetForm.spec.js | 110 +++++++++ src/composables/useTileClickTracking.js | 131 ++++++++++ src/constants/widgetRegistry.js | 3 + src/utils/sortContainerTiles.js | 116 +++++++++ 81 files changed, 1786 insertions(+), 78 deletions(-) create mode 100644 src/components/Widgets/Renderers/ContainerWidgetForm.vue create mode 100644 src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js create mode 100644 src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js create mode 100644 src/utils/sortContainerTiles.js diff --git a/l10n/be.js b/l10n/be.js index 4fc3bafb1..009cd5a99 100644 --- a/l10n/be.js +++ b/l10n/be.js @@ -1312,6 +1312,17 @@ OC.L10N.register( "Profile fields saved." : "Палі профілю захаваны.", "Save profile fields" : "Захаваць палі профілю", "The profile fields could not be loaded." : "Не ўдалося загрузіць палі профілю.", - "The profile fields could not be saved." : "Не ўдалося захаваць палі профілю." + "The profile fields could not be saved." : "Не ўдалося захаваць палі профілю.", + "Forget my usage" : "Забыць маё выкарыстанне", + "Sorted alphabetically" : "Упарадкавана па алфавіце", + "Sorted by most used" : "Упарадкавана па частаце выкарыстання", + "Sorted by last used" : "Упарадкавана па апошнім выкарыстанні", + "Sorted at random" : "Упарадкавана выпадкова", + "Sort tiles" : "Парадак плітак", + "By hand" : "Уручную", + "Alphabetically" : "Па алфавіце", + "Most used" : "Найчасцей выкарыстоўваныя", + "Last used" : "Апошнія выкарыстаныя", + "At random" : "Выпадкова" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/be.json b/l10n/be.json index f1b3999ce..4b7180f08 100644 --- a/l10n/be.json +++ b/l10n/be.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Палі профілю захаваны.", "Save profile fields": "Захаваць палі профілю", "The profile fields could not be loaded.": "Не ўдалося загрузіць палі профілю.", - "The profile fields could not be saved.": "Не ўдалося захаваць палі профілю." + "The profile fields could not be saved.": "Не ўдалося захаваць палі профілю.", + "Forget my usage": "Забыць маё выкарыстанне", + "Sorted alphabetically": "Упарадкавана па алфавіце", + "Sorted by most used": "Упарадкавана па частаце выкарыстання", + "Sorted by last used": "Упарадкавана па апошнім выкарыстанні", + "Sorted at random": "Упарадкавана выпадкова", + "Sort tiles": "Парадак плітак", + "By hand": "Уручную", + "Alphabetically": "Па алфавіце", + "Most used": "Найчасцей выкарыстоўваныя", + "Last used": "Апошнія выкарыстаныя", + "At random": "Выпадкова" } } diff --git a/l10n/bg.js b/l10n/bg.js index 94a897089..29818f4a3 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Полетата на профила са запазени.", "Save profile fields" : "Запазване на полетата на профила", "The profile fields could not be loaded." : "Полетата на профила не можаха да се заредят.", - "The profile fields could not be saved." : "Полетата на профила не можаха да се запазят." + "The profile fields could not be saved." : "Полетата на профила не можаха да се запазят.", + "Forget my usage" : "Забрави използването ми", + "Sorted alphabetically" : "Подредено по азбучен ред", + "Sorted by most used" : "Подредено по най-използвани", + "Sorted by last used" : "Подредено по последно използвани", + "Sorted at random" : "Подредено в случаен ред", + "Sort tiles" : "Подреждане на плочките", + "By hand" : "Ръчно", + "Alphabetically" : "По азбучен ред", + "Most used" : "Най-използвани", + "Last used" : "Последно използвани", + "At random" : "В случаен ред" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/bg.json b/l10n/bg.json index 8f5c8ea7b..0879485b6 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Полетата на профила са запазени.", "Save profile fields": "Запазване на полетата на профила", "The profile fields could not be loaded.": "Полетата на профила не можаха да се заредят.", - "The profile fields could not be saved.": "Полетата на профила не можаха да се запазят." + "The profile fields could not be saved.": "Полетата на профила не можаха да се запазят.", + "Forget my usage": "Забрави използването ми", + "Sorted alphabetically": "Подредено по азбучен ред", + "Sorted by most used": "Подредено по най-използвани", + "Sorted by last used": "Подредено по последно използвани", + "Sorted at random": "Подредено в случаен ред", + "Sort tiles": "Подреждане на плочките", + "By hand": "Ръчно", + "Alphabetically": "По азбучен ред", + "Most used": "Най-използвани", + "Last used": "Последно използвани", + "At random": "В случаен ред" } } diff --git a/l10n/bs.js b/l10n/bs.js index 361af2eed..1f9c11827 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila su spremljena.", "Save profile fields" : "Spremi polja profila", "The profile fields could not be loaded." : "Polja profila nisu mogla biti učitana.", - "The profile fields could not be saved." : "Polja profila nisu mogla biti spremljena." + "The profile fields could not be saved." : "Polja profila nisu mogla biti spremljena.", + "Forget my usage" : "Zaboravi moje korištenje", + "Sorted alphabetically" : "Poredano abecedno", + "Sorted by most used" : "Poredano po najčešće korištenim", + "Sorted by last used" : "Poredano po zadnje korištenim", + "Sorted at random" : "Poredano nasumično", + "Sort tiles" : "Poredaj pločice", + "By hand" : "Ručno", + "Alphabetically" : "Abecedno", + "Most used" : "Najčešće korišteno", + "Last used" : "Zadnje korišteno", + "At random" : "Nasumično" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/bs.json b/l10n/bs.json index 3afcedf8a..4c194ed67 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polja profila su spremljena.", "Save profile fields": "Spremi polja profila", "The profile fields could not be loaded.": "Polja profila nisu mogla biti učitana.", - "The profile fields could not be saved.": "Polja profila nisu mogla biti spremljena." + "The profile fields could not be saved.": "Polja profila nisu mogla biti spremljena.", + "Forget my usage": "Zaboravi moje korištenje", + "Sorted alphabetically": "Poredano abecedno", + "Sorted by most used": "Poredano po najčešće korištenim", + "Sorted by last used": "Poredano po zadnje korištenim", + "Sorted at random": "Poredano nasumično", + "Sort tiles": "Poredaj pločice", + "By hand": "Ručno", + "Alphabetically": "Abecedno", + "Most used": "Najčešće korišteno", + "Last used": "Zadnje korišteno", + "At random": "Nasumično" } } diff --git a/l10n/ca.js b/l10n/ca.js index bf45de116..86332cdff 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "S'han desat els camps del perfil.", "Save profile fields" : "Desa els camps del perfil", "The profile fields could not be loaded." : "No s'han pogut carregar els camps del perfil.", - "The profile fields could not be saved." : "No s'han pogut desar els camps del perfil." + "The profile fields could not be saved." : "No s'han pogut desar els camps del perfil.", + "Forget my usage" : "Oblida el meu ús", + "Sorted alphabetically" : "Ordenat alfabèticament", + "Sorted by most used" : "Ordenat pels més usats", + "Sorted by last used" : "Ordenat pels darrers usats", + "Sorted at random" : "Ordenat a l'atzar", + "Sort tiles" : "Ordena les rajoles", + "By hand" : "A mà", + "Alphabetically" : "Alfabèticament", + "Most used" : "Els més usats", + "Last used" : "Darrers usats", + "At random" : "A l'atzar" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/ca.json b/l10n/ca.json index ed8885f1a..8b4868e41 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "S'han desat els camps del perfil.", "Save profile fields": "Desa els camps del perfil", "The profile fields could not be loaded.": "No s'han pogut carregar els camps del perfil.", - "The profile fields could not be saved.": "No s'han pogut desar els camps del perfil." + "The profile fields could not be saved.": "No s'han pogut desar els camps del perfil.", + "Forget my usage": "Oblida el meu ús", + "Sorted alphabetically": "Ordenat alfabèticament", + "Sorted by most used": "Ordenat pels més usats", + "Sorted by last used": "Ordenat pels darrers usats", + "Sorted at random": "Ordenat a l'atzar", + "Sort tiles": "Ordena les rajoles", + "By hand": "A mà", + "Alphabetically": "Alfabèticament", + "Most used": "Els més usats", + "Last used": "Darrers usats", + "At random": "A l'atzar" } } diff --git a/l10n/cs.js b/l10n/cs.js index d11ca0b25..e5c89f275 100644 --- a/l10n/cs.js +++ b/l10n/cs.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Pole profilu jsou uložena.", "Save profile fields" : "Uložit pole profilu", "The profile fields could not be loaded." : "Pole profilu se nepodařilo načíst.", - "The profile fields could not be saved." : "Pole profilu se nepodařilo uložit." + "The profile fields could not be saved." : "Pole profilu se nepodařilo uložit.", + "Forget my usage" : "Zapomenout mé používání", + "Sorted alphabetically" : "Seřazeno abecedně", + "Sorted by most used" : "Seřazeno podle nejpoužívanějších", + "Sorted by last used" : "Seřazeno podle naposledy použitých", + "Sorted at random" : "Seřazeno náhodně", + "Sort tiles" : "Řazení dlaždic", + "By hand" : "Ručně", + "Alphabetically" : "Abecedně", + "Most used" : "Nejpoužívanější", + "Last used" : "Naposledy použité", + "At random" : "Náhodně" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/cs.json b/l10n/cs.json index 3f64c588c..6b523cb30 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Pole profilu jsou uložena.", "Save profile fields": "Uložit pole profilu", "The profile fields could not be loaded.": "Pole profilu se nepodařilo načíst.", - "The profile fields could not be saved.": "Pole profilu se nepodařilo uložit." + "The profile fields could not be saved.": "Pole profilu se nepodařilo uložit.", + "Forget my usage": "Zapomenout mé používání", + "Sorted alphabetically": "Seřazeno abecedně", + "Sorted by most used": "Seřazeno podle nejpoužívanějších", + "Sorted by last used": "Seřazeno podle naposledy použitých", + "Sorted at random": "Seřazeno náhodně", + "Sort tiles": "Řazení dlaždic", + "By hand": "Ručně", + "Alphabetically": "Abecedně", + "Most used": "Nejpoužívanější", + "Last used": "Naposledy použité", + "At random": "Náhodně" } } diff --git a/l10n/da.js b/l10n/da.js index ff77c1aaa..dde69e192 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelterne er gemt.", "Save profile fields" : "Gem profilfelter", "The profile fields could not be loaded." : "Profilfelterne kunne ikke indlæses.", - "The profile fields could not be saved." : "Profilfelterne kunne ikke gemmes." + "The profile fields could not be saved." : "Profilfelterne kunne ikke gemmes.", + "Forget my usage" : "Glem min brug", + "Sorted alphabetically" : "Sorteret alfabetisk", + "Sorted by most used" : "Sorteret efter mest brugte", + "Sorted by last used" : "Sorteret efter senest brugte", + "Sorted at random" : "Sorteret tilfældigt", + "Sort tiles" : "Sortér felter", + "By hand" : "Manuelt", + "Alphabetically" : "Alfabetisk", + "Most used" : "Mest brugte", + "Last used" : "Senest brugte", + "At random" : "Tilfældigt" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/da.json b/l10n/da.json index 78fd8908a..840976e06 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelterne er gemt.", "Save profile fields": "Gem profilfelter", "The profile fields could not be loaded.": "Profilfelterne kunne ikke indlæses.", - "The profile fields could not be saved.": "Profilfelterne kunne ikke gemmes." + "The profile fields could not be saved.": "Profilfelterne kunne ikke gemmes.", + "Forget my usage": "Glem min brug", + "Sorted alphabetically": "Sorteret alfabetisk", + "Sorted by most used": "Sorteret efter mest brugte", + "Sorted by last used": "Sorteret efter senest brugte", + "Sorted at random": "Sorteret tilfældigt", + "Sort tiles": "Sortér felter", + "By hand": "Manuelt", + "Alphabetically": "Alfabetisk", + "Most used": "Mest brugte", + "Last used": "Senest brugte", + "At random": "Tilfældigt" } } diff --git a/l10n/de.js b/l10n/de.js index 6063757b5..9e008de03 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelder gespeichert.", "Save profile fields" : "Profilfelder speichern", "The profile fields could not be loaded." : "Die Profilfelder konnten nicht geladen werden.", - "The profile fields could not be saved." : "Die Profilfelder konnten nicht gespeichert werden." + "The profile fields could not be saved." : "Die Profilfelder konnten nicht gespeichert werden.", + "Forget my usage" : "Meine Nutzung vergessen", + "Sorted alphabetically" : "Alphabetisch sortiert", + "Sorted by most used" : "Nach häufigster Nutzung sortiert", + "Sorted by last used" : "Nach letzter Nutzung sortiert", + "Sorted at random" : "Zufällig sortiert", + "Sort tiles" : "Kacheln sortieren", + "By hand" : "Von Hand", + "Alphabetically" : "Alphabetisch", + "Most used" : "Am häufigsten genutzt", + "Last used" : "Zuletzt genutzt", + "At random" : "Zufällig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/de.json b/l10n/de.json index 5468a77de..bf84b97e3 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelder gespeichert.", "Save profile fields": "Profilfelder speichern", "The profile fields could not be loaded.": "Die Profilfelder konnten nicht geladen werden.", - "The profile fields could not be saved.": "Die Profilfelder konnten nicht gespeichert werden." + "The profile fields could not be saved.": "Die Profilfelder konnten nicht gespeichert werden.", + "Forget my usage": "Meine Nutzung vergessen", + "Sorted alphabetically": "Alphabetisch sortiert", + "Sorted by most used": "Nach häufigster Nutzung sortiert", + "Sorted by last used": "Nach letzter Nutzung sortiert", + "Sorted at random": "Zufällig sortiert", + "Sort tiles": "Kacheln sortieren", + "By hand": "Von Hand", + "Alphabetically": "Alphabetisch", + "Most used": "Am häufigsten genutzt", + "Last used": "Zuletzt genutzt", + "At random": "Zufällig" } } diff --git a/l10n/el.js b/l10n/el.js index 4fe15f6bc..15a512709 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Τα πεδία προφίλ αποθηκεύτηκαν.", "Save profile fields" : "Αποθήκευση πεδίων προφίλ", "The profile fields could not be loaded." : "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.", - "The profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ." + "The profile fields could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ.", + "Forget my usage" : "Διαγραφή της χρήσης μου", + "Sorted alphabetically" : "Ταξινομημένα αλφαβητικά", + "Sorted by most used" : "Ταξινομημένα κατά συχνότερη χρήση", + "Sorted by last used" : "Ταξινομημένα κατά τελευταία χρήση", + "Sorted at random" : "Ταξινομημένα τυχαία", + "Sort tiles" : "Ταξινόμηση πλακιδίων", + "By hand" : "Χειροκίνητα", + "Alphabetically" : "Αλφαβητικά", + "Most used" : "Συχνότερα χρησιμοποιούμενα", + "Last used" : "Τελευταία χρήση", + "At random" : "Τυχαία" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/el.json b/l10n/el.json index c4aaf0219..5c8950f27 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Τα πεδία προφίλ αποθηκεύτηκαν.", "Save profile fields": "Αποθήκευση πεδίων προφίλ", "The profile fields could not be loaded.": "Δεν ήταν δυνατή η φόρτωση των πεδίων προφίλ.", - "The profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ." + "The profile fields could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των πεδίων προφίλ.", + "Forget my usage": "Διαγραφή της χρήσης μου", + "Sorted alphabetically": "Ταξινομημένα αλφαβητικά", + "Sorted by most used": "Ταξινομημένα κατά συχνότερη χρήση", + "Sorted by last used": "Ταξινομημένα κατά τελευταία χρήση", + "Sorted at random": "Ταξινομημένα τυχαία", + "Sort tiles": "Ταξινόμηση πλακιδίων", + "By hand": "Χειροκίνητα", + "Alphabetically": "Αλφαβητικά", + "Most used": "Συχνότερα χρησιμοποιούμενα", + "Last used": "Τελευταία χρήση", + "At random": "Τυχαία" } } diff --git a/l10n/en.js b/l10n/en.js index c86a4c71c..b2c2ba066 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1301,7 +1301,18 @@ OC.L10N.register( "Profile fields saved." : "Profile fields saved.", "Save profile fields" : "Save profile fields", "The profile fields could not be loaded." : "The profile fields could not be loaded.", - "The profile fields could not be saved." : "The profile fields could not be saved." + "The profile fields could not be saved." : "The profile fields could not be saved.", + "Forget my usage" : "Forget my usage", + "Sorted alphabetically" : "Sorted alphabetically", + "Sorted by most used" : "Sorted by most used", + "Sorted by last used" : "Sorted by last used", + "Sorted at random" : "Sorted at random", + "Sort tiles" : "Sort tiles", + "By hand" : "By hand", + "Alphabetically" : "Alphabetically", + "Most used" : "Most used", + "Last used" : "Last used", + "At random" : "At random" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/en.json b/l10n/en.json index 50a16c214..28106e6b2 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1266,6 +1266,17 @@ "Profile fields saved.": "Profile fields saved.", "Save profile fields": "Save profile fields", "The profile fields could not be loaded.": "The profile fields could not be loaded.", - "The profile fields could not be saved.": "The profile fields could not be saved." + "The profile fields could not be saved.": "The profile fields could not be saved.", + "Forget my usage": "Forget my usage", + "Sorted alphabetically": "Sorted alphabetically", + "Sorted by most used": "Sorted by most used", + "Sorted by last used": "Sorted by last used", + "Sorted at random": "Sorted at random", + "Sort tiles": "Sort tiles", + "By hand": "By hand", + "Alphabetically": "Alphabetically", + "Most used": "Most used", + "Last used": "Last used", + "At random": "At random" } } diff --git a/l10n/es.js b/l10n/es.js index 3c4155627..75312eba8 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campos del perfil guardados.", "Save profile fields" : "Guardar campos del perfil", "The profile fields could not be loaded." : "No se pudieron cargar los campos del perfil.", - "The profile fields could not be saved." : "No se pudieron guardar los campos del perfil." + "The profile fields could not be saved." : "No se pudieron guardar los campos del perfil.", + "Forget my usage" : "Olvidar mi uso", + "Sorted alphabetically" : "Ordenado alfabéticamente", + "Sorted by most used" : "Ordenado por más usados", + "Sorted by last used" : "Ordenado por último uso", + "Sorted at random" : "Ordenado al azar", + "Sort tiles" : "Ordenar mosaicos", + "By hand" : "A mano", + "Alphabetically" : "Alfabéticamente", + "Most used" : "Más usados", + "Last used" : "Último uso", + "At random" : "Al azar" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/es.json b/l10n/es.json index b5bb51d96..c63e2e707 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campos del perfil guardados.", "Save profile fields": "Guardar campos del perfil", "The profile fields could not be loaded.": "No se pudieron cargar los campos del perfil.", - "The profile fields could not be saved.": "No se pudieron guardar los campos del perfil." + "The profile fields could not be saved.": "No se pudieron guardar los campos del perfil.", + "Forget my usage": "Olvidar mi uso", + "Sorted alphabetically": "Ordenado alfabéticamente", + "Sorted by most used": "Ordenado por más usados", + "Sorted by last used": "Ordenado por último uso", + "Sorted at random": "Ordenado al azar", + "Sort tiles": "Ordenar mosaicos", + "By hand": "A mano", + "Alphabetically": "Alfabéticamente", + "Most used": "Más usados", + "Last used": "Último uso", + "At random": "Al azar" } } diff --git a/l10n/et.js b/l10n/et.js index 176dbb655..02d6525db 100644 --- a/l10n/et.js +++ b/l10n/et.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profiili väljad salvestatud.", "Save profile fields" : "Salvesta profiili väljad", "The profile fields could not be loaded." : "Profiili välju ei õnnestunud laadida.", - "The profile fields could not be saved." : "Profiili välju ei õnnestunud salvestada." + "The profile fields could not be saved." : "Profiili välju ei õnnestunud salvestada.", + "Forget my usage" : "Unusta minu kasutus", + "Sorted alphabetically" : "Sorditud tähestiku järgi", + "Sorted by most used" : "Sorditud enim kasutatu järgi", + "Sorted by last used" : "Sorditud viimati kasutatu järgi", + "Sorted at random" : "Sorditud juhuslikult", + "Sort tiles" : "Paanide järjestus", + "By hand" : "Käsitsi", + "Alphabetically" : "Tähestiku järgi", + "Most used" : "Enim kasutatud", + "Last used" : "Viimati kasutatud", + "At random" : "Juhuslikult" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/et.json b/l10n/et.json index 856cf2a6a..d6760c3fc 100644 --- a/l10n/et.json +++ b/l10n/et.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profiili väljad salvestatud.", "Save profile fields": "Salvesta profiili väljad", "The profile fields could not be loaded.": "Profiili välju ei õnnestunud laadida.", - "The profile fields could not be saved.": "Profiili välju ei õnnestunud salvestada." + "The profile fields could not be saved.": "Profiili välju ei õnnestunud salvestada.", + "Forget my usage": "Unusta minu kasutus", + "Sorted alphabetically": "Sorditud tähestiku järgi", + "Sorted by most used": "Sorditud enim kasutatu järgi", + "Sorted by last used": "Sorditud viimati kasutatu järgi", + "Sorted at random": "Sorditud juhuslikult", + "Sort tiles": "Paanide järjestus", + "By hand": "Käsitsi", + "Alphabetically": "Tähestiku järgi", + "Most used": "Enim kasutatud", + "Last used": "Viimati kasutatud", + "At random": "Juhuslikult" } } diff --git a/l10n/fi.js b/l10n/fi.js index cddec8a91..a0949456f 100644 --- a/l10n/fi.js +++ b/l10n/fi.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profiilikentät tallennettu.", "Save profile fields" : "Tallenna profiilikentät", "The profile fields could not be loaded." : "Profiilikenttiä ei voitu ladata.", - "The profile fields could not be saved." : "Profiilikenttiä ei voitu tallentaa." + "The profile fields could not be saved." : "Profiilikenttiä ei voitu tallentaa.", + "Forget my usage" : "Unohda käyttöni", + "Sorted alphabetically" : "Järjestetty aakkosjärjestykseen", + "Sorted by most used" : "Järjestetty eniten käytetyn mukaan", + "Sorted by last used" : "Järjestetty viimeksi käytetyn mukaan", + "Sorted at random" : "Järjestetty satunnaisesti", + "Sort tiles" : "Ruutujen järjestys", + "By hand" : "Käsin", + "Alphabetically" : "Aakkosjärjestyksessä", + "Most used" : "Eniten käytetyt", + "Last used" : "Viimeksi käytetyt", + "At random" : "Satunnaisesti" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/fi.json b/l10n/fi.json index 943c32a29..4cd802cbf 100644 --- a/l10n/fi.json +++ b/l10n/fi.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profiilikentät tallennettu.", "Save profile fields": "Tallenna profiilikentät", "The profile fields could not be loaded.": "Profiilikenttiä ei voitu ladata.", - "The profile fields could not be saved.": "Profiilikenttiä ei voitu tallentaa." + "The profile fields could not be saved.": "Profiilikenttiä ei voitu tallentaa.", + "Forget my usage": "Unohda käyttöni", + "Sorted alphabetically": "Järjestetty aakkosjärjestykseen", + "Sorted by most used": "Järjestetty eniten käytetyn mukaan", + "Sorted by last used": "Järjestetty viimeksi käytetyn mukaan", + "Sorted at random": "Järjestetty satunnaisesti", + "Sort tiles": "Ruutujen järjestys", + "By hand": "Käsin", + "Alphabetically": "Aakkosjärjestyksessä", + "Most used": "Eniten käytetyt", + "Last used": "Viimeksi käytetyt", + "At random": "Satunnaisesti" } } diff --git a/l10n/fr.js b/l10n/fr.js index 307678377..7331b0f10 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Champs du profil enregistrés.", "Save profile fields" : "Enregistrer les champs du profil", "The profile fields could not be loaded." : "Les champs du profil n'ont pas pu être chargés.", - "The profile fields could not be saved." : "Les champs du profil n'ont pas pu être enregistrés." + "The profile fields could not be saved." : "Les champs du profil n'ont pas pu être enregistrés.", + "Forget my usage" : "Oublier mon utilisation", + "Sorted alphabetically" : "Trié par ordre alphabétique", + "Sorted by most used" : "Trié par les plus utilisés", + "Sorted by last used" : "Trié par dernière utilisation", + "Sorted at random" : "Trié au hasard", + "Sort tiles" : "Trier les tuiles", + "By hand" : "À la main", + "Alphabetically" : "Par ordre alphabétique", + "Most used" : "Les plus utilisés", + "Last used" : "Dernière utilisation", + "At random" : "Au hasard" }, "nplurals=2; plural=(n > 1);" ); diff --git a/l10n/fr.json b/l10n/fr.json index 3c6046a8f..7e5ff6f4d 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Champs du profil enregistrés.", "Save profile fields": "Enregistrer les champs du profil", "The profile fields could not be loaded.": "Les champs du profil n'ont pas pu être chargés.", - "The profile fields could not be saved.": "Les champs du profil n'ont pas pu être enregistrés." + "The profile fields could not be saved.": "Les champs du profil n'ont pas pu être enregistrés.", + "Forget my usage": "Oublier mon utilisation", + "Sorted alphabetically": "Trié par ordre alphabétique", + "Sorted by most used": "Trié par les plus utilisés", + "Sorted by last used": "Trié par dernière utilisation", + "Sorted at random": "Trié au hasard", + "Sort tiles": "Trier les tuiles", + "By hand": "À la main", + "Alphabetically": "Par ordre alphabétique", + "Most used": "Les plus utilisés", + "Last used": "Dernière utilisation", + "At random": "Au hasard" } } diff --git a/l10n/ga.js b/l10n/ga.js index 8ba1d1216..5a43ce7fa 100644 --- a/l10n/ga.js +++ b/l10n/ga.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Réimsí próifíle sábháilte.", "Save profile fields" : "Sábháil réimsí próifíle", "The profile fields could not be loaded." : "Níorbh fhéidir na réimsí próifíle a lódáil.", - "The profile fields could not be saved." : "Níorbh fhéidir na réimsí próifíle a shábháil." + "The profile fields could not be saved." : "Níorbh fhéidir na réimsí próifíle a shábháil.", + "Forget my usage" : "Déan dearmad ar m'úsáid", + "Sorted alphabetically" : "Sórtáilte in ord aibítre", + "Sorted by most used" : "Sórtáilte de réir is mó a úsáidtear", + "Sorted by last used" : "Sórtáilte de réir an úsáid is déanaí", + "Sorted at random" : "Sórtáilte go randamach", + "Sort tiles" : "Sórtáil tíleanna", + "By hand" : "De láimh", + "Alphabetically" : "In ord aibítre", + "Most used" : "Is mó a úsáidtear", + "Last used" : "Úsáidte is déanaí", + "At random" : "Go randamach" }, "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" ); diff --git a/l10n/ga.json b/l10n/ga.json index 408ceb33b..4f4caf134 100644 --- a/l10n/ga.json +++ b/l10n/ga.json @@ -1291,6 +1291,17 @@ "Profile fields saved.": "Réimsí próifíle sábháilte.", "Save profile fields": "Sábháil réimsí próifíle", "The profile fields could not be loaded.": "Níorbh fhéidir na réimsí próifíle a lódáil.", - "The profile fields could not be saved.": "Níorbh fhéidir na réimsí próifíle a shábháil." + "The profile fields could not be saved.": "Níorbh fhéidir na réimsí próifíle a shábháil.", + "Forget my usage": "Déan dearmad ar m'úsáid", + "Sorted alphabetically": "Sórtáilte in ord aibítre", + "Sorted by most used": "Sórtáilte de réir is mó a úsáidtear", + "Sorted by last used": "Sórtáilte de réir an úsáid is déanaí", + "Sorted at random": "Sórtáilte go randamach", + "Sort tiles": "Sórtáil tíleanna", + "By hand": "De láimh", + "Alphabetically": "In ord aibítre", + "Most used": "Is mó a úsáidtear", + "Last used": "Úsáidte is déanaí", + "At random": "Go randamach" } } diff --git a/l10n/hr.js b/l10n/hr.js index c4b220d61..7bc09558f 100644 --- a/l10n/hr.js +++ b/l10n/hr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila su spremljena.", "Save profile fields" : "Spremi polja profila", "The profile fields could not be loaded." : "Polja profila nije bilo moguće učitati.", - "The profile fields could not be saved." : "Polja profila nije bilo moguće spremiti." + "The profile fields could not be saved." : "Polja profila nije bilo moguće spremiti.", + "Forget my usage" : "Zaboravi moje korištenje", + "Sorted alphabetically" : "Poredano abecedno", + "Sorted by most used" : "Poredano po najčešće korištenim", + "Sorted by last used" : "Poredano po posljednje korištenim", + "Sorted at random" : "Poredano nasumično", + "Sort tiles" : "Poredaj pločice", + "By hand" : "Ručno", + "Alphabetically" : "Abecedno", + "Most used" : "Najčešće korišteno", + "Last used" : "Posljednje korišteno", + "At random" : "Nasumično" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/hr.json b/l10n/hr.json index 2c565a477..5abd69a6c 100644 --- a/l10n/hr.json +++ b/l10n/hr.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polja profila su spremljena.", "Save profile fields": "Spremi polja profila", "The profile fields could not be loaded.": "Polja profila nije bilo moguće učitati.", - "The profile fields could not be saved.": "Polja profila nije bilo moguće spremiti." + "The profile fields could not be saved.": "Polja profila nije bilo moguće spremiti.", + "Forget my usage": "Zaboravi moje korištenje", + "Sorted alphabetically": "Poredano abecedno", + "Sorted by most used": "Poredano po najčešće korištenim", + "Sorted by last used": "Poredano po posljednje korištenim", + "Sorted at random": "Poredano nasumično", + "Sort tiles": "Poredaj pločice", + "By hand": "Ručno", + "Alphabetically": "Abecedno", + "Most used": "Najčešće korišteno", + "Last used": "Posljednje korišteno", + "At random": "Nasumično" } } diff --git a/l10n/hu.js b/l10n/hu.js index fc626aa13..8f267be02 100644 --- a/l10n/hu.js +++ b/l10n/hu.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilmezők mentve.", "Save profile fields" : "Profilmezők mentése", "The profile fields could not be loaded." : "A profilmezőket nem sikerült betölteni.", - "The profile fields could not be saved." : "A profilmezőket nem sikerült menteni." + "The profile fields could not be saved." : "A profilmezőket nem sikerült menteni.", + "Forget my usage" : "Használatom elfelejtése", + "Sorted alphabetically" : "Betűrendbe rendezve", + "Sorted by most used" : "A leggyakrabban használtak szerint rendezve", + "Sorted by last used" : "A legutóbb használtak szerint rendezve", + "Sorted at random" : "Véletlenszerűen rendezve", + "Sort tiles" : "Csempék rendezése", + "By hand" : "Kézzel", + "Alphabetically" : "Betűrendben", + "Most used" : "Leggyakrabban használt", + "Last used" : "Legutóbb használt", + "At random" : "Véletlenszerűen" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/hu.json b/l10n/hu.json index 4ca822358..73771ec3f 100644 --- a/l10n/hu.json +++ b/l10n/hu.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilmezők mentve.", "Save profile fields": "Profilmezők mentése", "The profile fields could not be loaded.": "A profilmezőket nem sikerült betölteni.", - "The profile fields could not be saved.": "A profilmezőket nem sikerült menteni." + "The profile fields could not be saved.": "A profilmezőket nem sikerült menteni.", + "Forget my usage": "Használatom elfelejtése", + "Sorted alphabetically": "Betűrendbe rendezve", + "Sorted by most used": "A leggyakrabban használtak szerint rendezve", + "Sorted by last used": "A legutóbb használtak szerint rendezve", + "Sorted at random": "Véletlenszerűen rendezve", + "Sort tiles": "Csempék rendezése", + "By hand": "Kézzel", + "Alphabetically": "Betűrendben", + "Most used": "Leggyakrabban használt", + "Last used": "Legutóbb használt", + "At random": "Véletlenszerűen" } } diff --git a/l10n/is.js b/l10n/is.js index 64029bd3c..90bb6f0a6 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Prófílreitir vistaðir.", "Save profile fields" : "Vista prófílreiti", "The profile fields could not be loaded." : "Ekki tókst að hlaða inn prófílreitunum.", - "The profile fields could not be saved." : "Ekki tókst að vista prófílreitina." + "The profile fields could not be saved." : "Ekki tókst að vista prófílreitina.", + "Forget my usage" : "Gleyma notkun minni", + "Sorted alphabetically" : "Raðað í stafrófsröð", + "Sorted by most used" : "Raðað eftir mest notuðu", + "Sorted by last used" : "Raðað eftir síðast notuðu", + "Sorted at random" : "Raðað af handahófi", + "Sort tiles" : "Raða reitum", + "By hand" : "Handvirkt", + "Alphabetically" : "Í stafrófsröð", + "Most used" : "Mest notað", + "Last used" : "Síðast notað", + "At random" : "Af handahófi" }, "nplurals=2; plural=(n%10!=1 || n%100==11);" ); diff --git a/l10n/is.json b/l10n/is.json index de529d623..3b278cd6e 100644 --- a/l10n/is.json +++ b/l10n/is.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Prófílreitir vistaðir.", "Save profile fields": "Vista prófílreiti", "The profile fields could not be loaded.": "Ekki tókst að hlaða inn prófílreitunum.", - "The profile fields could not be saved.": "Ekki tókst að vista prófílreitina." + "The profile fields could not be saved.": "Ekki tókst að vista prófílreitina.", + "Forget my usage": "Gleyma notkun minni", + "Sorted alphabetically": "Raðað í stafrófsröð", + "Sorted by most used": "Raðað eftir mest notuðu", + "Sorted by last used": "Raðað eftir síðast notuðu", + "Sorted at random": "Raðað af handahófi", + "Sort tiles": "Raða reitum", + "By hand": "Handvirkt", + "Alphabetically": "Í stafrófsröð", + "Most used": "Mest notað", + "Last used": "Síðast notað", + "At random": "Af handahófi" } } diff --git a/l10n/it.js b/l10n/it.js index ab0446c0d..084ad08ea 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campi del profilo salvati.", "Save profile fields" : "Salva campi del profilo", "The profile fields could not be loaded." : "Non è stato possibile caricare i campi del profilo.", - "The profile fields could not be saved." : "Non è stato possibile salvare i campi del profilo." + "The profile fields could not be saved." : "Non è stato possibile salvare i campi del profilo.", + "Forget my usage" : "Dimentica il mio utilizzo", + "Sorted alphabetically" : "Ordinato alfabeticamente", + "Sorted by most used" : "Ordinato per più usati", + "Sorted by last used" : "Ordinato per ultimo utilizzo", + "Sorted at random" : "Ordinato a caso", + "Sort tiles" : "Ordina riquadri", + "By hand" : "A mano", + "Alphabetically" : "In ordine alfabetico", + "Most used" : "Più usati", + "Last used" : "Ultimo utilizzo", + "At random" : "A caso" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/it.json b/l10n/it.json index fa3dd24fa..0bf896a8c 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campi del profilo salvati.", "Save profile fields": "Salva campi del profilo", "The profile fields could not be loaded.": "Non è stato possibile caricare i campi del profilo.", - "The profile fields could not be saved.": "Non è stato possibile salvare i campi del profilo." + "The profile fields could not be saved.": "Non è stato possibile salvare i campi del profilo.", + "Forget my usage": "Dimentica il mio utilizzo", + "Sorted alphabetically": "Ordinato alfabeticamente", + "Sorted by most used": "Ordinato per più usati", + "Sorted by last used": "Ordinato per ultimo utilizzo", + "Sorted at random": "Ordinato a caso", + "Sort tiles": "Ordina riquadri", + "By hand": "A mano", + "Alphabetically": "In ordine alfabetico", + "Most used": "Più usati", + "Last used": "Ultimo utilizzo", + "At random": "A caso" } } diff --git a/l10n/lb.js b/l10n/lb.js index 84faec3ed..57ee7d502 100644 --- a/l10n/lb.js +++ b/l10n/lb.js @@ -1312,7 +1312,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfelder gespäichert.", "Save profile fields" : "Profilfelder späicheren", "The profile fields could not be loaded." : "D'Profilfelder konnten net geluede ginn.", - "The profile fields could not be saved." : "D'Profilfelder konnten net gespäichert ginn." + "The profile fields could not be saved." : "D'Profilfelder konnten net gespäichert ginn.", + "Forget my usage" : "Meng Notzung vergiessen", + "Sorted alphabetically" : "Alphabetesch sortéiert", + "Sorted by most used" : "No der meeschter Notzung sortéiert", + "Sorted by last used" : "No der leschter Notzung sortéiert", + "Sorted at random" : "Zoufälleg sortéiert", + "Sort tiles" : "Kachelen sortéieren", + "By hand" : "Vun Hand", + "Alphabetically" : "Alphabetesch", + "Most used" : "Am meeschte benotzt", + "Last used" : "Lescht benotzt", + "At random" : "Zoufälleg" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/lb.json b/l10n/lb.json index e6fc3747f..530df9f16 100644 --- a/l10n/lb.json +++ b/l10n/lb.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfelder gespäichert.", "Save profile fields": "Profilfelder späicheren", "The profile fields could not be loaded.": "D'Profilfelder konnten net geluede ginn.", - "The profile fields could not be saved.": "D'Profilfelder konnten net gespäichert ginn." + "The profile fields could not be saved.": "D'Profilfelder konnten net gespäichert ginn.", + "Forget my usage": "Meng Notzung vergiessen", + "Sorted alphabetically": "Alphabetesch sortéiert", + "Sorted by most used": "No der meeschter Notzung sortéiert", + "Sorted by last used": "No der leschter Notzung sortéiert", + "Sorted at random": "Zoufälleg sortéiert", + "Sort tiles": "Kachelen sortéieren", + "By hand": "Vun Hand", + "Alphabetically": "Alphabetesch", + "Most used": "Am meeschte benotzt", + "Last used": "Lescht benotzt", + "At random": "Zoufälleg" } } diff --git a/l10n/lt.js b/l10n/lt.js index 8017d3ed4..b9f290070 100644 --- a/l10n/lt.js +++ b/l10n/lt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilio laukai išsaugoti.", "Save profile fields" : "Išsaugoti profilio laukus", "The profile fields could not be loaded." : "Nepavyko įkelti profilio laukų.", - "The profile fields could not be saved." : "Nepavyko išsaugoti profilio laukų." + "The profile fields could not be saved." : "Nepavyko išsaugoti profilio laukų.", + "Forget my usage" : "Pamiršti mano naudojimą", + "Sorted alphabetically" : "Surikiuota abėcėlės tvarka", + "Sorted by most used" : "Surikiuota pagal dažniausiai naudojamus", + "Sorted by last used" : "Surikiuota pagal paskutinį naudojimą", + "Sorted at random" : "Surikiuota atsitiktinai", + "Sort tiles" : "Plytelių rikiavimas", + "By hand" : "Rankiniu būdu", + "Alphabetically" : "Abėcėlės tvarka", + "Most used" : "Dažniausiai naudojami", + "Last used" : "Paskutiniai naudoti", + "At random" : "Atsitiktinai" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/lt.json b/l10n/lt.json index b0bd59a86..6a77144f8 100644 --- a/l10n/lt.json +++ b/l10n/lt.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Profilio laukai išsaugoti.", "Save profile fields": "Išsaugoti profilio laukus", "The profile fields could not be loaded.": "Nepavyko įkelti profilio laukų.", - "The profile fields could not be saved.": "Nepavyko išsaugoti profilio laukų." + "The profile fields could not be saved.": "Nepavyko išsaugoti profilio laukų.", + "Forget my usage": "Pamiršti mano naudojimą", + "Sorted alphabetically": "Surikiuota abėcėlės tvarka", + "Sorted by most used": "Surikiuota pagal dažniausiai naudojamus", + "Sorted by last used": "Surikiuota pagal paskutinį naudojimą", + "Sorted at random": "Surikiuota atsitiktinai", + "Sort tiles": "Plytelių rikiavimas", + "By hand": "Rankiniu būdu", + "Alphabetically": "Abėcėlės tvarka", + "Most used": "Dažniausiai naudojami", + "Last used": "Paskutiniai naudoti", + "At random": "Atsitiktinai" } } diff --git a/l10n/lv.js b/l10n/lv.js index 967b9beb5..375685874 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profila lauki saglabāti.", "Save profile fields" : "Saglabāt profila laukus", "The profile fields could not be loaded." : "Neizdevās ielādēt profila laukus.", - "The profile fields could not be saved." : "Neizdevās saglabāt profila laukus." + "The profile fields could not be saved." : "Neizdevās saglabāt profila laukus.", + "Forget my usage" : "Aizmirst manu lietojumu", + "Sorted alphabetically" : "Kārtots alfabētiski", + "Sorted by most used" : "Kārtots pēc visbiežāk lietotajiem", + "Sorted by last used" : "Kārtots pēc pēdējās lietošanas", + "Sorted at random" : "Kārtots nejauši", + "Sort tiles" : "Flīžu kārtošana", + "By hand" : "Manuāli", + "Alphabetically" : "Alfabētiski", + "Most used" : "Visbiežāk lietotie", + "Last used" : "Pēdējie lietotie", + "At random" : "Nejauši" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" ); diff --git a/l10n/lv.json b/l10n/lv.json index e1a6d207b..ddc13ea1d 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Profila lauki saglabāti.", "Save profile fields": "Saglabāt profila laukus", "The profile fields could not be loaded.": "Neizdevās ielādēt profila laukus.", - "The profile fields could not be saved.": "Neizdevās saglabāt profila laukus." + "The profile fields could not be saved.": "Neizdevās saglabāt profila laukus.", + "Forget my usage": "Aizmirst manu lietojumu", + "Sorted alphabetically": "Kārtots alfabētiski", + "Sorted by most used": "Kārtots pēc visbiežāk lietotajiem", + "Sorted by last used": "Kārtots pēc pēdējās lietošanas", + "Sorted at random": "Kārtots nejauši", + "Sort tiles": "Flīžu kārtošana", + "By hand": "Manuāli", + "Alphabetically": "Alfabētiski", + "Most used": "Visbiežāk lietotie", + "Last used": "Pēdējie lietotie", + "At random": "Nejauši" } } diff --git a/l10n/mk.js b/l10n/mk.js index e7f077905..1309b932d 100644 --- a/l10n/mk.js +++ b/l10n/mk.js @@ -1312,7 +1312,18 @@ OC.L10N.register( "Profile fields saved." : "Полињата на профилот се зачувани.", "Save profile fields" : "Зачувај полиња на профилот", "The profile fields could not be loaded." : "Полињата на профилот не можеа да се вчитаат.", - "The profile fields could not be saved." : "Полињата на профилот не можеа да се зачуваат." + "The profile fields could not be saved." : "Полињата на профилот не можеа да се зачуваат.", + "Forget my usage" : "Заборави ја мојата употреба", + "Sorted alphabetically" : "Подредено по азбучен ред", + "Sorted by most used" : "Подредено по најкористени", + "Sorted by last used" : "Подредено по последно користени", + "Sorted at random" : "Подредено по случаен избор", + "Sort tiles" : "Подредување на плочките", + "By hand" : "Рачно", + "Alphabetically" : "Азбучно", + "Most used" : "Најкористени", + "Last used" : "Последно користени", + "At random" : "По случаен избор" }, "nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);" ); diff --git a/l10n/mk.json b/l10n/mk.json index 967e1c28d..ab48acee8 100644 --- a/l10n/mk.json +++ b/l10n/mk.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Полињата на профилот се зачувани.", "Save profile fields": "Зачувај полиња на профилот", "The profile fields could not be loaded.": "Полињата на профилот не можеа да се вчитаат.", - "The profile fields could not be saved.": "Полињата на профилот не можеа да се зачуваат." + "The profile fields could not be saved.": "Полињата на профилот не можеа да се зачуваат.", + "Forget my usage": "Заборави ја мојата употреба", + "Sorted alphabetically": "Подредено по азбучен ред", + "Sorted by most used": "Подредено по најкористени", + "Sorted by last used": "Подредено по последно користени", + "Sorted at random": "Подредено по случаен избор", + "Sort tiles": "Подредување на плочките", + "By hand": "Рачно", + "Alphabetically": "Азбучно", + "Most used": "Најкористени", + "Last used": "Последно користени", + "At random": "По случаен избор" } } diff --git a/l10n/mt.js b/l10n/mt.js index 21ab65e16..efeb4b39b 100644 --- a/l10n/mt.js +++ b/l10n/mt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Il-oqsma tal-profil ġew salvati.", "Save profile fields" : "Issejvja l-oqsma tal-profil", "The profile fields could not be loaded." : "Il-oqsma tal-profil ma setgħux jitgħabbew.", - "The profile fields could not be saved." : "Il-oqsma tal-profil ma setgħux jiġu salvati." + "The profile fields could not be saved." : "Il-oqsma tal-profil ma setgħux jiġu salvati.", + "Forget my usage" : "Insa l-użu tiegħi", + "Sorted alphabetically" : "Issortjat alfabetikament", + "Sorted by most used" : "Issortjat skont l-aktar użati", + "Sorted by last used" : "Issortjat skont l-aħħar użu", + "Sorted at random" : "Issortjat b'mod każwali", + "Sort tiles" : "Issortja l-madum", + "By hand" : "Bl-idejn", + "Alphabetically" : "Alfabetikament", + "Most used" : "L-aktar użati", + "Last used" : "L-aħħar użati", + "At random" : "B'mod każwali" }, "nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);" ); diff --git a/l10n/mt.json b/l10n/mt.json index ff0697add..65e7e70ea 100644 --- a/l10n/mt.json +++ b/l10n/mt.json @@ -1290,6 +1290,17 @@ "Profile fields saved.": "Il-oqsma tal-profil ġew salvati.", "Save profile fields": "Issejvja l-oqsma tal-profil", "The profile fields could not be loaded.": "Il-oqsma tal-profil ma setgħux jitgħabbew.", - "The profile fields could not be saved.": "Il-oqsma tal-profil ma setgħux jiġu salvati." + "The profile fields could not be saved.": "Il-oqsma tal-profil ma setgħux jiġu salvati.", + "Forget my usage": "Insa l-użu tiegħi", + "Sorted alphabetically": "Issortjat alfabetikament", + "Sorted by most used": "Issortjat skont l-aktar użati", + "Sorted by last used": "Issortjat skont l-aħħar użu", + "Sorted at random": "Issortjat b'mod każwali", + "Sort tiles": "Issortja l-madum", + "By hand": "Bl-idejn", + "Alphabetically": "Alfabetikament", + "Most used": "L-aktar użati", + "Last used": "L-aħħar użati", + "At random": "B'mod każwali" } } diff --git a/l10n/nb.js b/l10n/nb.js index 226a19ce8..cadeff1a3 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfeltene er lagret.", "Save profile fields" : "Lagre profilfelt", "The profile fields could not be loaded." : "Profilfeltene kunne ikke lastes inn.", - "The profile fields could not be saved." : "Profilfeltene kunne ikke lagres." + "The profile fields could not be saved." : "Profilfeltene kunne ikke lagres.", + "Forget my usage" : "Glem bruken min", + "Sorted alphabetically" : "Sortert alfabetisk", + "Sorted by most used" : "Sortert etter mest brukt", + "Sorted by last used" : "Sortert etter sist brukt", + "Sorted at random" : "Sortert tilfeldig", + "Sort tiles" : "Sorter fliser", + "By hand" : "Manuelt", + "Alphabetically" : "Alfabetisk", + "Most used" : "Mest brukt", + "Last used" : "Sist brukt", + "At random" : "Tilfeldig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nb.json b/l10n/nb.json index 60cb3f614..8059022ed 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfeltene er lagret.", "Save profile fields": "Lagre profilfelt", "The profile fields could not be loaded.": "Profilfeltene kunne ikke lastes inn.", - "The profile fields could not be saved.": "Profilfeltene kunne ikke lagres." + "The profile fields could not be saved.": "Profilfeltene kunne ikke lagres.", + "Forget my usage": "Glem bruken min", + "Sorted alphabetically": "Sortert alfabetisk", + "Sorted by most used": "Sortert etter mest brukt", + "Sorted by last used": "Sortert etter sist brukt", + "Sorted at random": "Sortert tilfeldig", + "Sort tiles": "Sorter fliser", + "By hand": "Manuelt", + "Alphabetically": "Alfabetisk", + "Most used": "Mest brukt", + "Last used": "Sist brukt", + "At random": "Tilfeldig" } } diff --git a/l10n/nl.js b/l10n/nl.js index 59f9b10fa..8648eb478 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1371,7 +1371,18 @@ OC.L10N.register( "Profile fields saved." : "Profielvelden opgeslagen.", "Save profile fields" : "Profielvelden opslaan", "The profile fields could not be loaded." : "De profielvelden konden niet worden geladen.", - "The profile fields could not be saved." : "De profielvelden konden niet worden opgeslagen." + "The profile fields could not be saved." : "De profielvelden konden niet worden opgeslagen.", + "Forget my usage" : "Mijn gebruik vergeten", + "Sorted alphabetically" : "Alfabetisch gesorteerd", + "Sorted by most used" : "Gesorteerd op meest gebruikt", + "Sorted by last used" : "Gesorteerd op laatst gebruikt", + "Sorted at random" : "Willekeurig gesorteerd", + "Sort tiles" : "Tegels sorteren", + "By hand" : "Handmatig", + "Alphabetically" : "Alfabetisch", + "Most used" : "Meest gebruikt", + "Last used" : "Laatst gebruikt", + "At random" : "Willekeurig" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nl.json b/l10n/nl.json index c1070d47a..e774d1823 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1348,6 +1348,17 @@ "Profile fields saved.": "Profielvelden opgeslagen.", "Save profile fields": "Profielvelden opslaan", "The profile fields could not be loaded.": "De profielvelden konden niet worden geladen.", - "The profile fields could not be saved.": "De profielvelden konden niet worden opgeslagen." + "The profile fields could not be saved.": "De profielvelden konden niet worden opgeslagen.", + "Forget my usage": "Mijn gebruik vergeten", + "Sorted alphabetically": "Alfabetisch gesorteerd", + "Sorted by most used": "Gesorteerd op meest gebruikt", + "Sorted by last used": "Gesorteerd op laatst gebruikt", + "Sorted at random": "Willekeurig gesorteerd", + "Sort tiles": "Tegels sorteren", + "By hand": "Handmatig", + "Alphabetically": "Alfabetisch", + "Most used": "Meest gebruikt", + "Last used": "Laatst gebruikt", + "At random": "Willekeurig" } } diff --git a/l10n/pl.js b/l10n/pl.js index 7754a5c2a..285dce17a 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Pola profilu zapisane.", "Save profile fields" : "Zapisz pola profilu", "The profile fields could not be loaded." : "Nie udało się wczytać pól profilu.", - "The profile fields could not be saved." : "Nie udało się zapisać pól profilu." + "The profile fields could not be saved." : "Nie udało się zapisać pól profilu.", + "Forget my usage" : "Zapomnij moje użycie", + "Sorted alphabetically" : "Posortowane alfabetycznie", + "Sorted by most used" : "Posortowane według najczęściej używanych", + "Sorted by last used" : "Posortowane według ostatnio używanych", + "Sorted at random" : "Posortowane losowo", + "Sort tiles" : "Sortuj kafelki", + "By hand" : "Ręcznie", + "Alphabetically" : "Alfabetycznie", + "Most used" : "Najczęściej używane", + "Last used" : "Ostatnio używane", + "At random" : "Losowo" }, "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/pl.json b/l10n/pl.json index 60814ee56..e82900981 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Pola profilu zapisane.", "Save profile fields": "Zapisz pola profilu", "The profile fields could not be loaded.": "Nie udało się wczytać pól profilu.", - "The profile fields could not be saved.": "Nie udało się zapisać pól profilu." + "The profile fields could not be saved.": "Nie udało się zapisać pól profilu.", + "Forget my usage": "Zapomnij moje użycie", + "Sorted alphabetically": "Posortowane alfabetycznie", + "Sorted by most used": "Posortowane według najczęściej używanych", + "Sorted by last used": "Posortowane według ostatnio używanych", + "Sorted at random": "Posortowane losowo", + "Sort tiles": "Sortuj kafelki", + "By hand": "Ręcznie", + "Alphabetically": "Alfabetycznie", + "Most used": "Najczęściej używane", + "Last used": "Ostatnio używane", + "At random": "Losowo" } } diff --git a/l10n/pt.js b/l10n/pt.js index 4cdf7ed03..e515e3e67 100644 --- a/l10n/pt.js +++ b/l10n/pt.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Campos do perfil guardados.", "Save profile fields" : "Guardar campos do perfil", "The profile fields could not be loaded." : "Não foi possível carregar os campos do perfil.", - "The profile fields could not be saved." : "Não foi possível guardar os campos do perfil." + "The profile fields could not be saved." : "Não foi possível guardar os campos do perfil.", + "Forget my usage" : "Esquecer a minha utilização", + "Sorted alphabetically" : "Ordenado alfabeticamente", + "Sorted by most used" : "Ordenado pelos mais usados", + "Sorted by last used" : "Ordenado pelo último uso", + "Sorted at random" : "Ordenado aleatoriamente", + "Sort tiles" : "Ordenar mosaicos", + "By hand" : "Manualmente", + "Alphabetically" : "Alfabeticamente", + "Most used" : "Mais usados", + "Last used" : "Último uso", + "At random" : "Aleatoriamente" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/pt.json b/l10n/pt.json index 41b8a8f8d..90c186688 100644 --- a/l10n/pt.json +++ b/l10n/pt.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Campos do perfil guardados.", "Save profile fields": "Guardar campos do perfil", "The profile fields could not be loaded.": "Não foi possível carregar os campos do perfil.", - "The profile fields could not be saved.": "Não foi possível guardar os campos do perfil." + "The profile fields could not be saved.": "Não foi possível guardar os campos do perfil.", + "Forget my usage": "Esquecer a minha utilização", + "Sorted alphabetically": "Ordenado alfabeticamente", + "Sorted by most used": "Ordenado pelos mais usados", + "Sorted by last used": "Ordenado pelo último uso", + "Sorted at random": "Ordenado aleatoriamente", + "Sort tiles": "Ordenar mosaicos", + "By hand": "Manualmente", + "Alphabetically": "Alfabeticamente", + "Most used": "Mais usados", + "Last used": "Último uso", + "At random": "Aleatoriamente" } } diff --git a/l10n/rm.js b/l10n/rm.js index 457c0a42f..9453ed16a 100644 --- a/l10n/rm.js +++ b/l10n/rm.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Champs dal profil memorisads.", "Save profile fields" : "Memorisar ils champs dal profil", "The profile fields could not be loaded." : "I n'è betg reussì da chargiar ils champs dal profil.", - "The profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs dal profil." + "The profile fields could not be saved." : "I n'è betg reussì da memorisar ils champs dal profil.", + "Forget my usage" : "Emblidar mes diever", + "Sorted alphabetically" : "Zavrà alfabeticamain", + "Sorted by most used" : "Zavrà tenor il diever il pli frequent", + "Sorted by last used" : "Zavrà tenor l'ultim diever", + "Sorted at random" : "Zavrà a chaschun", + "Sort tiles" : "Zavrar ils quadrels", + "By hand" : "A maun", + "Alphabetically" : "Alfabeticamain", + "Most used" : "Il pli savens duvrà", + "Last used" : "Duvrà l'ultim", + "At random" : "A chaschun" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/rm.json b/l10n/rm.json index 56794d514..566dd6a70 100644 --- a/l10n/rm.json +++ b/l10n/rm.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Champs dal profil memorisads.", "Save profile fields": "Memorisar ils champs dal profil", "The profile fields could not be loaded.": "I n'è betg reussì da chargiar ils champs dal profil.", - "The profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs dal profil." + "The profile fields could not be saved.": "I n'è betg reussì da memorisar ils champs dal profil.", + "Forget my usage": "Emblidar mes diever", + "Sorted alphabetically": "Zavrà alfabeticamain", + "Sorted by most used": "Zavrà tenor il diever il pli frequent", + "Sorted by last used": "Zavrà tenor l'ultim diever", + "Sorted at random": "Zavrà a chaschun", + "Sort tiles": "Zavrar ils quadrels", + "By hand": "A maun", + "Alphabetically": "Alfabeticamain", + "Most used": "Il pli savens duvrà", + "Last used": "Duvrà l'ultim", + "At random": "A chaschun" } } diff --git a/l10n/ro.js b/l10n/ro.js index 350c650fa..358d66e1a 100644 --- a/l10n/ro.js +++ b/l10n/ro.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Câmpurile de profil au fost salvate.", "Save profile fields" : "Salvează câmpurile de profil", "The profile fields could not be loaded." : "Câmpurile de profil nu au putut fi încărcate.", - "The profile fields could not be saved." : "Câmpurile de profil nu au putut fi salvate." + "The profile fields could not be saved." : "Câmpurile de profil nu au putut fi salvate.", + "Forget my usage" : "Uită utilizarea mea", + "Sorted alphabetically" : "Sortat alfabetic", + "Sorted by most used" : "Sortat după cele mai folosite", + "Sorted by last used" : "Sortat după ultima utilizare", + "Sorted at random" : "Sortat aleatoriu", + "Sort tiles" : "Sortează dalele", + "By hand" : "Manual", + "Alphabetically" : "Alfabetic", + "Most used" : "Cele mai folosite", + "Last used" : "Ultima utilizare", + "At random" : "Aleatoriu" }, "nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);" ); diff --git a/l10n/ro.json b/l10n/ro.json index bf0d96182..4ce56bcf0 100644 --- a/l10n/ro.json +++ b/l10n/ro.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Câmpurile de profil au fost salvate.", "Save profile fields": "Salvează câmpurile de profil", "The profile fields could not be loaded.": "Câmpurile de profil nu au putut fi încărcate.", - "The profile fields could not be saved.": "Câmpurile de profil nu au putut fi salvate." + "The profile fields could not be saved.": "Câmpurile de profil nu au putut fi salvate.", + "Forget my usage": "Uită utilizarea mea", + "Sorted alphabetically": "Sortat alfabetic", + "Sorted by most used": "Sortat după cele mai folosite", + "Sorted by last used": "Sortat după ultima utilizare", + "Sorted at random": "Sortat aleatoriu", + "Sort tiles": "Sortează dalele", + "By hand": "Manual", + "Alphabetically": "Alfabetic", + "Most used": "Cele mai folosite", + "Last used": "Ultima utilizare", + "At random": "Aleatoriu" } } diff --git a/l10n/ru.js b/l10n/ru.js index 6eb71e4a6..215c42ce6 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Поля профиля сохранены.", "Save profile fields" : "Сохранить поля профиля", "The profile fields could not be loaded." : "Не удалось загрузить поля профиля.", - "The profile fields could not be saved." : "Не удалось сохранить поля профиля." + "The profile fields could not be saved." : "Не удалось сохранить поля профиля.", + "Forget my usage" : "Забыть моё использование", + "Sorted alphabetically" : "Отсортировано по алфавиту", + "Sorted by most used" : "Отсортировано по частоте использования", + "Sorted by last used" : "Отсортировано по последнему использованию", + "Sorted at random" : "Отсортировано случайно", + "Sort tiles" : "Сортировка плиток", + "By hand" : "Вручную", + "Alphabetically" : "По алфавиту", + "Most used" : "Самые используемые", + "Last used" : "Последние использованные", + "At random" : "Случайно" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/ru.json b/l10n/ru.json index 9958f3d25..752062dab 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поля профиля сохранены.", "Save profile fields": "Сохранить поля профиля", "The profile fields could not be loaded.": "Не удалось загрузить поля профиля.", - "The profile fields could not be saved.": "Не удалось сохранить поля профиля." + "The profile fields could not be saved.": "Не удалось сохранить поля профиля.", + "Forget my usage": "Забыть моё использование", + "Sorted alphabetically": "Отсортировано по алфавиту", + "Sorted by most used": "Отсортировано по частоте использования", + "Sorted by last used": "Отсортировано по последнему использованию", + "Sorted at random": "Отсортировано случайно", + "Sort tiles": "Сортировка плиток", + "By hand": "Вручную", + "Alphabetically": "По алфавиту", + "Most used": "Самые используемые", + "Last used": "Последние использованные", + "At random": "Случайно" } } diff --git a/l10n/sk.js b/l10n/sk.js index 972c0e9cc..dc9930564 100644 --- a/l10n/sk.js +++ b/l10n/sk.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polia profilu sú uložené.", "Save profile fields" : "Uložiť polia profilu", "The profile fields could not be loaded." : "Polia profilu sa nepodarilo načítať.", - "The profile fields could not be saved." : "Polia profilu sa nepodarilo uložiť." + "The profile fields could not be saved." : "Polia profilu sa nepodarilo uložiť.", + "Forget my usage" : "Zabudnúť moje používanie", + "Sorted alphabetically" : "Zoradené abecedne", + "Sorted by most used" : "Zoradené podľa najpoužívanejších", + "Sorted by last used" : "Zoradené podľa naposledy použitých", + "Sorted at random" : "Zoradené náhodne", + "Sort tiles" : "Zoradenie dlaždíc", + "By hand" : "Ručne", + "Alphabetically" : "Abecedne", + "Most used" : "Najpoužívanejšie", + "Last used" : "Naposledy použité", + "At random" : "Náhodne" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/sk.json b/l10n/sk.json index 84474ab28..f4ad5b771 100644 --- a/l10n/sk.json +++ b/l10n/sk.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Polia profilu sú uložené.", "Save profile fields": "Uložiť polia profilu", "The profile fields could not be loaded.": "Polia profilu sa nepodarilo načítať.", - "The profile fields could not be saved.": "Polia profilu sa nepodarilo uložiť." + "The profile fields could not be saved.": "Polia profilu sa nepodarilo uložiť.", + "Forget my usage": "Zabudnúť moje používanie", + "Sorted alphabetically": "Zoradené abecedne", + "Sorted by most used": "Zoradené podľa najpoužívanejších", + "Sorted by last used": "Zoradené podľa naposledy použitých", + "Sorted at random": "Zoradené náhodne", + "Sort tiles": "Zoradenie dlaždíc", + "By hand": "Ručne", + "Alphabetically": "Abecedne", + "Most used": "Najpoužívanejšie", + "Last used": "Naposledy použité", + "At random": "Náhodne" } } diff --git a/l10n/sl.js b/l10n/sl.js index 368c7922b..aae665454 100644 --- a/l10n/sl.js +++ b/l10n/sl.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Polja profila so shranjena.", "Save profile fields" : "Shrani polja profila", "The profile fields could not be loaded." : "Polj profila ni bilo mogoče naložiti.", - "The profile fields could not be saved." : "Polj profila ni bilo mogoče shraniti." + "The profile fields could not be saved." : "Polj profila ni bilo mogoče shraniti.", + "Forget my usage" : "Pozabi mojo uporabo", + "Sorted alphabetically" : "Razvrščeno po abecedi", + "Sorted by most used" : "Razvrščeno po najpogosteje uporabljenih", + "Sorted by last used" : "Razvrščeno po zadnji uporabi", + "Sorted at random" : "Razvrščeno naključno", + "Sort tiles" : "Razvrsti ploščice", + "By hand" : "Ročno", + "Alphabetically" : "Po abecedi", + "Most used" : "Najpogosteje uporabljeno", + "Last used" : "Nazadnje uporabljeno", + "At random" : "Naključno" }, "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" ); diff --git a/l10n/sl.json b/l10n/sl.json index 4fadf33d5..a46e1bfe9 100644 --- a/l10n/sl.json +++ b/l10n/sl.json @@ -1290,6 +1290,17 @@ "Profile fields saved.": "Polja profila so shranjena.", "Save profile fields": "Shrani polja profila", "The profile fields could not be loaded.": "Polj profila ni bilo mogoče naložiti.", - "The profile fields could not be saved.": "Polj profila ni bilo mogoče shraniti." + "The profile fields could not be saved.": "Polj profila ni bilo mogoče shraniti.", + "Forget my usage": "Pozabi mojo uporabo", + "Sorted alphabetically": "Razvrščeno po abecedi", + "Sorted by most used": "Razvrščeno po najpogosteje uporabljenih", + "Sorted by last used": "Razvrščeno po zadnji uporabi", + "Sorted at random": "Razvrščeno naključno", + "Sort tiles": "Razvrsti ploščice", + "By hand": "Ročno", + "Alphabetically": "Po abecedi", + "Most used": "Najpogosteje uporabljeno", + "Last used": "Nazadnje uporabljeno", + "At random": "Naključno" } } diff --git a/l10n/sq.js b/l10n/sq.js index cdcf747d1..0e6961f3d 100644 --- a/l10n/sq.js +++ b/l10n/sq.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Fushat e profilit u ruajtën.", "Save profile fields" : "Ruaj fushat e profilit", "The profile fields could not be loaded." : "Fushat e profilit nuk u ngarkuan.", - "The profile fields could not be saved." : "Fushat e profilit nuk u ruajtën." + "The profile fields could not be saved." : "Fushat e profilit nuk u ruajtën.", + "Forget my usage" : "Harro përdorimin tim", + "Sorted alphabetically" : "Renditur sipas alfabetit", + "Sorted by most used" : "Renditur sipas më të përdorurave", + "Sorted by last used" : "Renditur sipas përdorimit të fundit", + "Sorted at random" : "Renditur rastësisht", + "Sort tiles" : "Rendit pllakat", + "By hand" : "Me dorë", + "Alphabetically" : "Sipas alfabetit", + "Most used" : "Më të përdorurat", + "Last used" : "Përdorur së fundi", + "At random" : "Rastësisht" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sq.json b/l10n/sq.json index 1fb6fecd0..558053019 100644 --- a/l10n/sq.json +++ b/l10n/sq.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Fushat e profilit u ruajtën.", "Save profile fields": "Ruaj fushat e profilit", "The profile fields could not be loaded.": "Fushat e profilit nuk u ngarkuan.", - "The profile fields could not be saved.": "Fushat e profilit nuk u ruajtën." + "The profile fields could not be saved.": "Fushat e profilit nuk u ruajtën.", + "Forget my usage": "Harro përdorimin tim", + "Sorted alphabetically": "Renditur sipas alfabetit", + "Sorted by most used": "Renditur sipas më të përdorurave", + "Sorted by last used": "Renditur sipas përdorimit të fundit", + "Sorted at random": "Renditur rastësisht", + "Sort tiles": "Rendit pllakat", + "By hand": "Me dorë", + "Alphabetically": "Sipas alfabetit", + "Most used": "Më të përdorurat", + "Last used": "Përdorur së fundi", + "At random": "Rastësisht" } } diff --git a/l10n/sr.js b/l10n/sr.js index 612b24d10..d8bca7a02 100644 --- a/l10n/sr.js +++ b/l10n/sr.js @@ -1312,6 +1312,17 @@ OC.L10N.register( "Profile fields saved." : "Поља профила су сачувана.", "Save profile fields" : "Сачувај поља профила", "The profile fields could not be loaded." : "Поља профила нису могла да се учитају.", - "The profile fields could not be saved." : "Поља профила нису могла да се сачувају." + "The profile fields could not be saved." : "Поља профила нису могла да се сачувају.", + "Forget my usage" : "Заборави моје коришћење", + "Sorted alphabetically" : "Поређано абецедно", + "Sorted by most used" : "Поређано по најкоришћенијим", + "Sorted by last used" : "Поређано по последње коришћеним", + "Sorted at random" : "Поређано насумично", + "Sort tiles" : "Поређај плочице", + "By hand" : "Ручно", + "Alphabetically" : "Абецедно", + "Most used" : "Најкоришћеније", + "Last used" : "Последње коришћено", + "At random" : "Насумично" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/sr.json b/l10n/sr.json index fa807ae69..9fc9ef3b4 100644 --- a/l10n/sr.json +++ b/l10n/sr.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поља профила су сачувана.", "Save profile fields": "Сачувај поља профила", "The profile fields could not be loaded.": "Поља профила нису могла да се учитају.", - "The profile fields could not be saved.": "Поља профила нису могла да се сачувају." + "The profile fields could not be saved.": "Поља профила нису могла да се сачувају.", + "Forget my usage": "Заборави моје коришћење", + "Sorted alphabetically": "Поређано абецедно", + "Sorted by most used": "Поређано по најкоришћенијим", + "Sorted by last used": "Поређано по последње коришћеним", + "Sorted at random": "Поређано насумично", + "Sort tiles": "Поређај плочице", + "By hand": "Ручно", + "Alphabetically": "Абецедно", + "Most used": "Најкоришћеније", + "Last used": "Последње коришћено", + "At random": "Насумично" } } diff --git a/l10n/sv.js b/l10n/sv.js index b00492b8a..9f2c290bf 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profilfälten har sparats.", "Save profile fields" : "Spara profilfält", "The profile fields could not be loaded." : "Profilfälten kunde inte läsas in.", - "The profile fields could not be saved." : "Profilfälten kunde inte sparas." + "The profile fields could not be saved." : "Profilfälten kunde inte sparas.", + "Forget my usage" : "Glöm min användning", + "Sorted alphabetically" : "Sorterat i bokstavsordning", + "Sorted by most used" : "Sorterat efter mest använda", + "Sorted by last used" : "Sorterat efter senast använda", + "Sorted at random" : "Sorterat slumpmässigt", + "Sort tiles" : "Sortera paneler", + "By hand" : "Manuellt", + "Alphabetically" : "I bokstavsordning", + "Most used" : "Mest använda", + "Last used" : "Senast använda", + "At random" : "Slumpmässigt" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sv.json b/l10n/sv.json index 01609597c..3a86552d0 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profilfälten har sparats.", "Save profile fields": "Spara profilfält", "The profile fields could not be loaded.": "Profilfälten kunde inte läsas in.", - "The profile fields could not be saved.": "Profilfälten kunde inte sparas." + "The profile fields could not be saved.": "Profilfälten kunde inte sparas.", + "Forget my usage": "Glöm min användning", + "Sorted alphabetically": "Sorterat i bokstavsordning", + "Sorted by most used": "Sorterat efter mest använda", + "Sorted by last used": "Sorterat efter senast använda", + "Sorted at random": "Sorterat slumpmässigt", + "Sort tiles": "Sortera paneler", + "By hand": "Manuellt", + "Alphabetically": "I bokstavsordning", + "Most used": "Mest använda", + "Last used": "Senast använda", + "At random": "Slumpmässigt" } } diff --git a/l10n/tr.js b/l10n/tr.js index c6aedbaa1..3089b26ee 100644 --- a/l10n/tr.js +++ b/l10n/tr.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Profil alanları kaydedildi.", "Save profile fields" : "Profil alanlarını kaydet", "The profile fields could not be loaded." : "Profil alanları yüklenemedi.", - "The profile fields could not be saved." : "Profil alanları kaydedilemedi." + "The profile fields could not be saved." : "Profil alanları kaydedilemedi.", + "Forget my usage" : "Kullanımımı unut", + "Sorted alphabetically" : "Alfabetik sıralandı", + "Sorted by most used" : "En çok kullanılana göre sıralandı", + "Sorted by last used" : "Son kullanılana göre sıralandı", + "Sorted at random" : "Rastgele sıralandı", + "Sort tiles" : "Kutucukları sırala", + "By hand" : "Elle", + "Alphabetically" : "Alfabetik", + "Most used" : "En çok kullanılan", + "Last used" : "Son kullanılan", + "At random" : "Rastgele" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/tr.json b/l10n/tr.json index 377f35188..155e964f0 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -1288,6 +1288,17 @@ "Profile fields saved.": "Profil alanları kaydedildi.", "Save profile fields": "Profil alanlarını kaydet", "The profile fields could not be loaded.": "Profil alanları yüklenemedi.", - "The profile fields could not be saved.": "Profil alanları kaydedilemedi." + "The profile fields could not be saved.": "Profil alanları kaydedilemedi.", + "Forget my usage": "Kullanımımı unut", + "Sorted alphabetically": "Alfabetik sıralandı", + "Sorted by most used": "En çok kullanılana göre sıralandı", + "Sorted by last used": "Son kullanılana göre sıralandı", + "Sorted at random": "Rastgele sıralandı", + "Sort tiles": "Kutucukları sırala", + "By hand": "Elle", + "Alphabetically": "Alfabetik", + "Most used": "En çok kullanılan", + "Last used": "Son kullanılan", + "At random": "Rastgele" } } diff --git a/l10n/uk.js b/l10n/uk.js index eee0990e0..be5b75f80 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -1321,7 +1321,18 @@ OC.L10N.register( "Profile fields saved." : "Поля профілю збережено.", "Save profile fields" : "Зберегти поля профілю", "The profile fields could not be loaded." : "Не вдалося завантажити поля профілю.", - "The profile fields could not be saved." : "Не вдалося зберегти поля профілю." + "The profile fields could not be saved." : "Не вдалося зберегти поля профілю.", + "Forget my usage" : "Забути моє використання", + "Sorted alphabetically" : "Відсортовано за абеткою", + "Sorted by most used" : "Відсортовано за частотою використання", + "Sorted by last used" : "Відсортовано за останнім використанням", + "Sorted at random" : "Відсортовано випадково", + "Sort tiles" : "Сортування плиток", + "By hand" : "Вручну", + "Alphabetically" : "За абеткою", + "Most used" : "Найчастіше використовувані", + "Last used" : "Останні використані", + "At random" : "Випадково" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/uk.json b/l10n/uk.json index 50e244f2e..5696c7113 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -1289,6 +1289,17 @@ "Profile fields saved.": "Поля профілю збережено.", "Save profile fields": "Зберегти поля профілю", "The profile fields could not be loaded.": "Не вдалося завантажити поля профілю.", - "The profile fields could not be saved.": "Не вдалося зберегти поля профілю." + "The profile fields could not be saved.": "Не вдалося зберегти поля профілю.", + "Forget my usage": "Забути моє використання", + "Sorted alphabetically": "Відсортовано за абеткою", + "Sorted by most used": "Відсортовано за частотою використання", + "Sorted by last used": "Відсортовано за останнім використанням", + "Sorted at random": "Відсортовано випадково", + "Sort tiles": "Сортування плиток", + "By hand": "Вручну", + "Alphabetically": "За абеткою", + "Most used": "Найчастіше використовувані", + "Last used": "Останні використані", + "At random": "Випадково" } } diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index 1296f97d2..0c071a918 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -8,16 +8,27 @@

{{ titleText }}

+ -
+
+ :gs-h="child.gridHeight || 2" + @click="onChildClick(child)">
@@ -30,8 +41,21 @@ import ContainerChild from './ContainerChild.vue' import { getNestedGridOptions, + NESTED_COLUMNS, useNestedGridManager, } from '../../../composables/useNestedGridManager.js' +import { + forgetLocalTileUse, + randomRankFor, + readLocalTileUse, + recordLocalTileUse, +} from '../../../composables/useTileClickTracking.js' +import { + childTitle, + orderChildren, + reflowChildren, + SORT_MODES, +} from '../../../utils/sortContainerTiles.js' const PADDING_TOKENS = Object.freeze({ none: '0', @@ -81,6 +105,8 @@ export default { data() { return { gridInstance: null, + // The viewer's own tile use, read once per page load (REQ-TSO-002). + use: readLocalTileUse(), } }, @@ -91,6 +117,77 @@ export default { return Array.isArray(list) ? list : [] }, + /** @spec openspec/specs/container-widget/spec.md */ + sortBy() { + const value = this.content?.sortBy + return SORT_MODES.includes(value) ? value : 'manual' + }, + + /** + * The children as shown: the stored layout in edit mode or by hand, + * otherwise ordered and reflowed at view time (REQ-TSO-001). + * + * @spec openspec/specs/container-widget/spec.md + */ + displayChildren() { + if (this.editMode || this.sortBy === 'manual') { + return this.children + } + // REQ-TSO-002: without browser storage there is no use to sort by. + if ( + this.use === null + && ['most-used', 'last-used'].includes(this.sortBy) + ) { + return this.children + } + const ordered = orderChildren(this.children, this.sortBy, { + use: this.use || {}, + keyOf: this.useKey, + rankOf: randomRankFor, + collator: new Intl.Collator(undefined, { + sensitivity: 'base', + numeric: true, + }), + }) + return reflowChildren(ordered, NESTED_COLUMNS) + }, + + /** @spec openspec/specs/container-widget/spec.md */ + sortDescription() { + const labels = { + alphabetical: t('launchpad', 'Sorted alphabetically'), + 'most-used': t('launchpad', 'Sorted by most used'), + 'last-used': t('launchpad', 'Sorted by last used'), + random: t('launchpad', 'Sorted at random'), + } + return this.editMode ? '' : labels[this.sortBy] || '' + }, + + /** + * Offer "Forget my usage" when the order reads the viewer's use and + * there is use to forget (REQ-TSO-003). + * + * @spec openspec/specs/container-widget/spec.md + */ + canForget() { + if (this.editMode || !['most-used', 'last-used'].includes(this.sortBy)) { + return false + } + return ( + this.use !== null + && this.children.some((child) => this.use[this.useKey(child)]) + ) + }, + + /** @spec openspec/specs/container-widget/spec.md */ + layoutSignature() { + return this.displayChildren + .map( + (child) => `${this.useKey(child)}@${child.gridX},${child.gridY}`, + ) + .join('|') + }, + /** @spec openspec/specs/container-widget/spec.md */ backgroundColor() { const value = this.content?.backgroundColor @@ -137,6 +234,22 @@ export default { }, }, + watch: { + /** + * A new view-time layout (after "Forget my usage") rebuilds the + * inner grid, which reads positions only when it starts. + * + * @spec openspec/specs/container-widget/spec.md + */ + layoutSignature() { + if (this.editMode || !this.gridInstance) { + return + } + this.destroyInnerGrid() + this.$nextTick(() => this.initInnerGrid()) + }, + }, + mounted() { this.initInnerGrid() }, @@ -146,6 +259,49 @@ export default { }, methods: { + /** + * The key a child's use is stored under: its placement id, else its + * uuid, else its title. + * + * @param {object} child The child placement. + * @return {string} The key. + * @spec openspec/specs/container-widget/spec.md + */ + useKey(child) { + if (child?.id !== undefined && child?.id !== null) { + return String(child.id) + } + if (typeof child?.uuid === 'string' && child.uuid !== '') { + return child.uuid + } + return `title:${childTitle(child)}` + }, + + /** + * Count a click on a tile in this browser (REQ-TSO-002). The order + * does not change until the next page load, so tiles never jump + * under the pointer. + * + * @param {object} child The clicked child placement. + * @spec openspec/specs/container-widget/spec.md + */ + onChildClick(child) { + if (this.editMode) { + return + } + recordLocalTileUse(this.useKey(child)) + }, + + /** + * Forget this browser's use of this container's tiles (REQ-TSO-003). + * + * @spec openspec/specs/container-widget/spec.md + */ + forgetUsage() { + forgetLocalTileUse(this.children.map((child) => this.useKey(child))) + this.use = readLocalTileUse() + }, + /** * Stable key for v-for over child placements. Falls back to the * loop index when a child has neither id nor uuid, which keeps @@ -222,7 +378,8 @@ export default { * @spec openspec/specs/container-widget/spec.md */ onGridChange(_event, nodes) { - if (!Array.isArray(nodes) || nodes.length === 0) { + // A view-time reflow is never written back (REQ-TSO-001). + if (!this.editMode || !Array.isArray(nodes) || nodes.length === 0) { return } const byKey = new Map() @@ -309,6 +466,18 @@ export default { min-height: 0; } +.container-widget__forget { + align-self: flex-start; + padding: 2px 8px; + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + background: var(--color-background-hover); + color: var(--color-main-text); + font-size: 12px; + cursor: pointer; + pointer-events: auto; +} + .container-widget__child { /* REQ-CONT-004: child wrappers re-enable pointer events so clicks on a child fire the child's own handlers — the container itself diff --git a/src/components/Widgets/Renderers/ContainerWidgetForm.vue b/src/components/Widgets/Renderers/ContainerWidgetForm.vue new file mode 100644 index 000000000..ca34e6768 --- /dev/null +++ b/src/components/Widgets/Renderers/ContainerWidgetForm.vue @@ -0,0 +1,139 @@ + + + + + + + diff --git a/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js b/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js new file mode 100644 index 000000000..20d0fe9e6 --- /dev/null +++ b/src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js @@ -0,0 +1,226 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for tile sorting in `ContainerWidget.vue` + * (launcher-tile-sorting, REQ-TSO-001..004): the view-time order per mode, + * the reflow that keeps sizes and never writes back, most and last used read + * from the viewer's browser only, "Forget my usage", and the once-per-load + * shuffle. + */ + +import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import ContainerWidget from '../ContainerWidget.vue' +import { + __resetTileUseForTest, + recordLocalTileUse, +} from '../../../../composables/useTileClickTracking.js' + +vi.mock('@nextcloud/axios', () => ({ + default: { + get: vi.fn(() => Promise.resolve({ data: {} })), + post: vi.fn(() => Promise.resolve({})), + }, +})) + +function tile(id, title, x) { + return { + id, + type: 'tile', + content: { title }, + gridX: x, + gridY: 0, + gridWidth: 1, + gridHeight: 1, + } +} + +// Stored in this order: Zaaksysteem, Agenda, Mail. +const PLACEMENTS = [ + tile(1, 'Zaaksysteem', 0), + tile(2, 'Agenda', 1), + tile(3, 'Mail', 2), +] + +function mountSorted(sortBy, editMode = false) { + return mount(ContainerWidget, { + props: { + content: { title: 'Applicaties', sortBy, placements: PLACEMENTS }, + editMode, + }, + global: { + stubs: { + ContainerChild: { + props: ['placement'], + template: + '{{ placement.content.title }}', + }, + }, + }, + }) +} + +function order(wrapper) { + return wrapper + .findAll('.container-widget__child') + .map((item) => ({ + title: item.text(), + x: Number(item.attributes('gs-x')), + y: Number(item.attributes('gs-y')), + })) + .sort((a, b) => a.y - b.y || a.x - b.x) + .map((item) => item.title) +} + +describe('ContainerWidget tile sorting', () => { + beforeEach(() => { + globalThis.t = (_app, key) => key + localStorage.clear() + __resetTileUseForTest() + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('REQ-TSO-001: alphabetical shows Agenda, Mail, Zaaksysteem from left to right', () => { + expect(order(mountSorted('alphabetical'))).toEqual([ + 'Agenda', + 'Mail', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-001: by hand keeps the stored order', () => { + expect(order(mountSorted('manual'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + }) + + it('REQ-TSO-001: edit mode shows the stored positions and nothing is written back', () => { + const wrapper = mountSorted('alphabetical', true) + expect(order(wrapper)).toEqual(['Zaaksysteem', 'Agenda', 'Mail']) + expect(mountSorted('alphabetical').emitted('update:content')).toBeUndefined() + }) + + it('REQ-TSO-001: the reflow keeps each tile size and wraps at four columns', () => { + const wide = [ + { ...tile(1, 'B', 0), gridWidth: 3 }, + { ...tile(2, 'A', 3), gridWidth: 2, gridHeight: 2 }, + ] + const wrapper = mount(ContainerWidget, { + props: { content: { sortBy: 'alphabetical', placements: wide } }, + global: { stubs: { ContainerChild: true } }, + }) + const items = wrapper.findAll('.container-widget__child') + const a = items.find((item) => item.attributes('gs-w') === '2') + const b = items.find((item) => item.attributes('gs-w') === '3') + expect([ + a.attributes('gs-x'), + a.attributes('gs-y'), + a.attributes('gs-h'), + ]).toEqual(['0', '0', '2']) + expect([b.attributes('gs-x'), b.attributes('gs-y')]).toEqual(['0', '2']) + }) + + it('REQ-TSO-002: a daily app moves to the front, and unused tiles fall back to alphabetical', () => { + recordLocalTileUse(1) + recordLocalTileUse(1) + recordLocalTileUse(1) + expect(order(mountSorted('most-used'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + + localStorage.clear() + __resetTileUseForTest() + recordLocalTileUse(3) + expect(order(mountSorted('most-used'))).toEqual([ + 'Mail', + 'Agenda', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-002: clicking a tile in the container counts it in the browser only', async () => { + const axios = (await import('@nextcloud/axios')).default + const wrapper = mountSorted('most-used') + await wrapper.findAll('.container-widget__child')[2].trigger('click') + + const stored = JSON.parse(localStorage.getItem('launchpad.tileUse')) + expect(Object.keys(stored)).toHaveLength(1) + expect(Object.values(stored)[0].count).toBe(1) + expect(axios.post).not.toHaveBeenCalled() + }) + + it('REQ-TSO-002: last used puts the latest click first', () => { + vi.spyOn(Date, 'now').mockReturnValueOnce(1000).mockReturnValueOnce(2000) + recordLocalTileUse(2) + recordLocalTileUse(3) + expect(order(mountSorted('last-used'))).toEqual([ + 'Mail', + 'Agenda', + 'Zaaksysteem', + ]) + }) + + it('REQ-TSO-002: blocked storage shows the stored order without an error', () => { + vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { + throw new Error('SecurityError') + }) + expect(order(mountSorted('most-used'))).toEqual([ + 'Zaaksysteem', + 'Agenda', + 'Mail', + ]) + }) + + it('REQ-TSO-003: forget my usage returns the container to alphabetical order', async () => { + recordLocalTileUse(1) + const wrapper = mountSorted('most-used') + expect(order(wrapper)).toEqual(['Zaaksysteem', 'Agenda', 'Mail']) + + const forget = wrapper.find('button.container-widget__forget') + expect(forget.text()).toBe('Forget my usage') + await forget.trigger('click') + + expect(order(wrapper)).toEqual(['Agenda', 'Mail', 'Zaaksysteem']) + expect(wrapper.find('button.container-widget__forget').exists()).toBe(false) + }) + + it('REQ-TSO-003: the forget button is not offered for alphabetical or by hand', () => { + recordLocalTileUse(1) + expect( + mountSorted('alphabetical') + .find('button.container-widget__forget') + .exists(), + ).toBe(false) + }) + + it('REQ-TSO-004: random order holds for the page load', async () => { + const first = order(mountSorted('random')) + const wrapper = mountSorted('random') + await wrapper.setProps({ + content: { sortBy: 'random', placements: [...PLACEMENTS] }, + }) + expect(order(wrapper)).toEqual(first) + expect([...first].sort()).toEqual(['Agenda', 'Mail', 'Zaaksysteem']) + }) + + it('announces the sort order to assistive technology', () => { + expect( + mountSorted('most-used') + .find('.launchpad-container-grid') + .attributes('aria-description'), + ).toBe('Sorted by most used') + expect( + mountSorted('manual') + .find('.launchpad-container-grid') + .attributes('aria-description'), + ).toBeUndefined() + }) +}) diff --git a/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js b/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js new file mode 100644 index 000000000..0511540fd --- /dev/null +++ b/src/components/Widgets/Renderers/__tests__/ContainerWidgetForm.spec.js @@ -0,0 +1,110 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for `ContainerWidgetForm.vue` (REQ-CONT-007 as modified + * by launcher-tile-sorting): the three communal fields plus exactly one + * "Sort tiles" select, and the emitted content carries `sortBy`. + */ + +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import ContainerWidgetForm from '../ContainerWidgetForm.vue' + +vi.mock('@conduction/nextcloud-vue', () => ({ + CnContainerWidgetForm: { + name: 'CnContainerWidgetForm', + emits: ['update:content'], + template: + '
', + }, +})) + +const NcSelectStub = { + name: 'NcSelect', + props: ['modelValue', 'options', 'inputLabel'], + emits: ['update:modelValue'], + template: + '
{{ modelValue }}
', +} + +describe('ContainerWidgetForm', () => { + beforeEach(() => { + globalThis.t = (_app, key) => key + }) + + function mountForm(props = {}) { + return mount(ContainerWidgetForm, { + props, + global: { stubs: { NcSelect: NcSelectStub } }, + }) + } + + it('renders the communal form plus exactly one Sort tiles select, defaulting to by hand', () => { + const wrapper = mountForm() + expect(wrapper.find('.communal').exists()).toBe(true) + const selects = wrapper.findAll('.nc-select') + expect(selects).toHaveLength(1) + expect(selects[0].attributes('data-label')).toBe('Sort tiles') + expect(selects[0].text()).toBe('manual') + const values = wrapper + .findComponent(NcSelectStub) + .props('options') + .map((option) => option.value) + expect(values).toEqual([ + 'manual', + 'alphabetical', + 'most-used', + 'last-used', + 'random', + ]) + }) + + it('emits the communal fields together with sortBy', async () => { + const wrapper = mountForm({ + editingWidget: { + content: { + title: 'Applicaties', + padding: 'small', + placements: [{ id: 1 }], + sortBy: 'most-used', + }, + }, + }) + expect(wrapper.find('.nc-select').text()).toBe('most-used') + + wrapper + .findComponent(NcSelectStub) + .vm.$emit('update:modelValue', 'alphabetical') + wrapper + .findComponent({ name: 'CnContainerWidgetForm' }) + .vm.$emit('update:content', { + placements: [{ id: 1 }], + backgroundColor: '#fff', + padding: 'small', + title: 'Apps', + }) + + const emitted = wrapper.emitted('update:content') + expect(emitted[0][0]).toMatchObject({ + title: 'Applicaties', + sortBy: 'alphabetical', + }) + expect(emitted[1][0]).toEqual({ + placements: [{ id: 1 }], + backgroundColor: '#fff', + padding: 'small', + title: 'Apps', + sortBy: 'alphabetical', + }) + expect(wrapper.vm.validate()).toEqual([]) + }) + + it('drops an unknown sort mode back to by hand', () => { + expect( + mountForm({ value: { sortBy: 'sideways' } }) + .find('.nc-select') + .text(), + ).toBe('manual') + }) +}) diff --git a/src/composables/useTileClickTracking.js b/src/composables/useTileClickTracking.js index 27b1798e8..fb855215a 100644 --- a/src/composables/useTileClickTracking.js +++ b/src/composables/useTileClickTracking.js @@ -123,3 +123,134 @@ export function __resetTileClickTrackingForTest() { cachedEnabled = null inflightConfigPromise = null } + +/** + * Browser storage key of the viewer's own tile use (launcher-tile-sorting). + * + * @type {string} + */ +const TILE_USE_KEY = 'launchpad.tileUse' + +/** + * Read the viewer's own tile use: `{ [placementId]: {count, lastUsedAt} }`. + * The counts live only in this browser and are never sent to the server + * (REQ-TSO-002). Corrupt data reads as no use; blocked storage reads as null. + * + * @return {Object|null} Use per placement id, or null when storage is unavailable. + * @spec openspec/specs/container-widget/spec.md + */ +export function readLocalTileUse() { + let raw + try { + raw = window.localStorage.getItem(TILE_USE_KEY) + } catch { + return null + } + try { + const parsed = JSON.parse(raw || '{}') + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) + ? parsed + : {} + } catch { + return {} + } +} + +/** + * Write the use map; a blocked storage is ignored. + * + * @param {object} use The use map. + * @return {boolean} True when stored. + */ +function writeLocalTileUse(use) { + try { + window.localStorage.setItem(TILE_USE_KEY, JSON.stringify(use)) + return true + } catch { + return false + } +} + +/** + * Count one use of a tile in this browser (REQ-TSO-002). Never throws. + * + * @param {string|number} placementId The tile's placement id. + * @return {void} + * @spec openspec/specs/container-widget/spec.md + */ +export function recordLocalTileUse(placementId) { + if (!placementId && placementId !== 0) { + return + } + const use = readLocalTileUse() + if (use === null) { + return + } + const key = String(placementId) + const previous = use[key] || { count: 0, lastUsedAt: 0 } + use[key] = { count: (Number(previous.count) || 0) + 1, lastUsedAt: Date.now() } + writeLocalTileUse(use) +} + +/** + * Forget this browser's use of the given tiles (REQ-TSO-003). + * + * @param {Array} placementIds The tiles to forget. + * @return {void} + * @spec openspec/specs/container-widget/spec.md + */ +export function forgetLocalTileUse(placementIds) { + const use = readLocalTileUse() + if (use === null) { + return + } + placementIds.forEach((id) => { + delete use[String(id)] + }) + writeLocalTileUse(use) +} + +/** + * Test-only reset of the local tile use and the page-load shuffle. + * + * @spec exclude test-only harness hook; `src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js` is its only importer. It clears the browser use map and the random ranks so each test starts cold. + * @return {void} + */ +export function __resetTileUseForTest() { + try { + window.localStorage.removeItem(TILE_USE_KEY) + } catch { + // blocked storage: nothing to clear + } + resetRandomRanks() +} + +/** + * Random rank per placement id, assigned once per page load (REQ-TSO-004). + * + * @type {Map} + */ +let randomRanks = new Map() + +/** + * The page-load random rank of a tile. + * + * @param {string} key The tile key. + * @return {number} A rank in [0, 1). + * @spec openspec/specs/container-widget/spec.md + */ +export function randomRankFor(key) { + if (!randomRanks.has(key)) { + randomRanks.set(key, Math.random()) + } + return randomRanks.get(key) +} + +/** + * Drop the page-load random ranks. + * + * @return {void} + */ +function resetRandomRanks() { + randomRanks = new Map() +} diff --git a/src/constants/widgetRegistry.js b/src/constants/widgetRegistry.js index 5effd6b88..c5a835d61 100644 --- a/src/constants/widgetRegistry.js +++ b/src/constants/widgetRegistry.js @@ -68,6 +68,7 @@ import ChartHost from '../components/Widgets/Renderers/ChartHost.vue' import ClockWidget from '../components/Widgets/Renderers/ClockWidget.vue' import ClockWidgetForm from '../components/Widgets/Renderers/ClockWidgetForm.vue' import ContainerWidget from '../components/Widgets/Renderers/ContainerWidget.vue' +import ContainerWidgetForm from '../components/Widgets/Renderers/ContainerWidgetForm.vue' // `iframe` — embeds an admin-allow-listed external URL in a sandboxed // frame, with a client-side graceful-degradation fallback for targets that // refuse framing (REQ-IFRAME-001..004, openspec/specs/iframe-embed-widget). @@ -127,6 +128,8 @@ const RENDERER_OVERRIDES = { */ const FORM_OVERRIDES = { calendar: CalendarForm, + // launcher-tile-sorting: the communal fields plus "Sort tiles". + container: ContainerWidgetForm, people: PeopleForm, 'spend-analytics': SpendAnalyticsForm, 'nc-widget': NcDashboardForm, diff --git a/src/utils/sortContainerTiles.js b/src/utils/sortContainerTiles.js new file mode 100644 index 000000000..4e5170456 --- /dev/null +++ b/src/utils/sortContainerTiles.js @@ -0,0 +1,116 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * View-time ordering and reflow of the tiles in a container + * (launcher-tile-sorting, REQ-TSO-001..004). Pure functions: the caller + * supplies the viewer's own use map and the page-load random ranks, and + * gets back copies with new inner-grid positions. Nothing is written back. + */ + +export const SORT_MODES = Object.freeze([ + 'manual', + 'alphabetical', + 'most-used', + 'last-used', + 'random', +]) + +/** + * The display title of a child placement, for the alphabetical order. + * + * @param {object} child The child placement. + * @return {string} Its title, or ''. + * @spec openspec/specs/container-widget/spec.md + */ +export function childTitle(child) { + const content = child?.content || {} + const candidates = [ + content.title, + content.label, + content.text, + child?.tileTitle, + child?.customTitle, + child?.title, + ] + const found = candidates.find( + (value) => typeof value === 'string' && value.trim() !== '', + ) + return found ? found.trim() : '' +} + +/** + * Order the children for a sort mode. Most and last used fall back to + * alphabetical for equal values, so unused tiles sort after used ones. + * + * @param {Array} children Child placements in stored order. + * @param {string} sortBy One of SORT_MODES. + * @param {object} options Sort inputs. + * @param {object} options.use The viewer's use per key. + * @param {function(object): string} options.keyOf The key of a child. + * @param {function(string): number} options.rankOf The page-load random rank of a key. + * @param {Intl.Collator} options.collator Locale collator for titles. + * @return {Array} A new, ordered array. + * @spec openspec/specs/container-widget/spec.md + */ +export function orderChildren( + children, + sortBy, + { use = {}, keyOf, rankOf, collator }, +) { + const list = [...children] + const byTitle = (a, b) => collator.compare(childTitle(a), childTitle(b)) + const stat = (child, field) => Number(use[keyOf(child)]?.[field]) || 0 + + switch (sortBy) { + case 'alphabetical': + return list.sort(byTitle) + case 'most-used': + return list.sort( + (a, b) => stat(b, 'count') - stat(a, 'count') || byTitle(a, b), + ) + case 'last-used': + return list.sort( + (a, b) => + stat(b, 'lastUsedAt') - stat(a, 'lastUsedAt') || byTitle(a, b), + ) + case 'random': + return list.sort((a, b) => rankOf(keyOf(a)) - rankOf(keyOf(b))) + default: + return list + } +} + +/** + * Reflow ordered children row by row into a grid of `columns`, keeping + * each child's width and height. A row is as tall as its tallest child. + * + * @param {Array} ordered Ordered child placements. + * @param {number} columns Inner-grid column count. + * @return {Array} Copies with new `gridX` and `gridY`. + * @spec openspec/specs/container-widget/spec.md + */ +export function reflowChildren(ordered, columns) { + let x = 0 + let y = 0 + let rowHeight = 0 + return ordered.map((child) => { + const width = Math.min(Math.max(Number(child.gridWidth) || 2, 1), columns) + const height = Math.max(Number(child.gridHeight) || 2, 1) + if (x + width > columns) { + x = 0 + y += rowHeight + rowHeight = 0 + } + const placed = { + ...child, + gridX: x, + gridY: y, + gridWidth: width, + gridHeight: height, + } + x += width + rowHeight = Math.max(rowHeight, height) + return placed + }) +} From 25050f75b5506a1b785d73c61ead10aac19bd8f6 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:56:47 +0200 Subject: [PATCH 02/11] docs(openspec): archive launcher-tile-sorting, tile-most-used and d-tile-sort are built --- .../.openspec.yaml | 0 .../design.md | 8 ++ .../proposal.md | 0 .../specs/container-widget/spec.md | 0 .../tasks.md | 12 +-- openspec/parity/capabilities.json | 14 ++-- openspec/specs/container-widget/spec.md | 75 +++++++++++++++++-- 7 files changed, 91 insertions(+), 18 deletions(-) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/.openspec.yaml (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/design.md (78%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/proposal.md (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/specs/container-widget/spec.md (100%) rename openspec/changes/{launcher-tile-sorting => archive/2026-09-30-launcher-tile-sorting}/tasks.md (58%) diff --git a/openspec/changes/launcher-tile-sorting/.openspec.yaml b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/.openspec.yaml similarity index 100% rename from openspec/changes/launcher-tile-sorting/.openspec.yaml rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/.openspec.yaml diff --git a/openspec/changes/launcher-tile-sorting/design.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md similarity index 78% rename from openspec/changes/launcher-tile-sorting/design.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md index 7f618ccd3..65e8ec012 100644 --- a/openspec/changes/launcher-tile-sorting/design.md +++ b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/design.md @@ -43,3 +43,11 @@ A reflow changes the reading order, so the container announces "Sorted by most u - Vitest: ordering for each mode, tie breaks, the once-per-load shuffle, storage blocked, edit mode shows stored positions. - Playwright: set "Most used", click one tile three times, reload, it comes first; switch back to "By hand", the original layout returns. + +## What the build corrected (2026-09-30) + +- Tiles inside a container render through the widget registry, not through `TileWidget.vue`, so a click there never reached `recordTileClick()`. The container counts a click on any of its children itself (`onChildClick`), in the browser only; `recordTileClick()` and its server call are unchanged. +- The widget menu is an editor tool, so "Forget my usage" is a small button under the container title, shown in view mode for most used and last used when there is use to forget. +- A new click does not re-sort until the next page load, so tiles never jump under the pointer. "Forget my usage" re-sorts at once and rebuilds the inner grid. +- When browser storage is blocked, most used and last used show the stored order (the spec's rule), not alphabetical. +- A view-time reflow never reaches the persistence callback: `onGridChange` returns in view mode. diff --git a/openspec/changes/launcher-tile-sorting/proposal.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/proposal.md similarity index 100% rename from openspec/changes/launcher-tile-sorting/proposal.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/proposal.md diff --git a/openspec/changes/launcher-tile-sorting/specs/container-widget/spec.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/specs/container-widget/spec.md similarity index 100% rename from openspec/changes/launcher-tile-sorting/specs/container-widget/spec.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/specs/container-widget/spec.md diff --git a/openspec/changes/launcher-tile-sorting/tasks.md b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md similarity index 58% rename from openspec/changes/launcher-tile-sorting/tasks.md rename to openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md index 5a4aba73f..4412f4624 100644 --- a/openspec/changes/launcher-tile-sorting/tasks.md +++ b/openspec/changes/archive/2026-09-30-launcher-tile-sorting/tasks.md @@ -1,8 +1,8 @@ # Tasks: launcher-tile-sorting -- [ ] 1. Add `ContainerWidgetForm.vue` with the three communal fields plus "Sort tiles", and register it in `FORM_OVERRIDES` in `src/constants/widgetRegistry.js`. Verify: Vitest on the form. -- [ ] 2. Keep local `{count, lastUsedAt}` per placement in `src/composables/useTileClickTracking.js`, storage failures caught. Verify: Vitest with storage available and blocked. -- [ ] 3. Order and reflow children in view mode in `ContainerWidget.vue` for the five modes, with the tie rules and a once-per-load shuffle. Verify: Vitest per mode. -- [ ] 4. Add "Forget my usage" to the container menu and the "Sorted by" accessible description. Verify: Vitest and an axe check. -- [ ] 5. Playwright: most used moves a clicked tile to the front after reload; "By hand" restores the stored layout. -- [ ] 6. Add `nl` and `en` strings, fold the delta into `openspec/specs/container-widget/spec.md` on archive, and set d-tile-sort and tile-most-used to `built` in the parity matrix with evidence lines. +- [x] 1. Add `ContainerWidgetForm.vue` with the three communal fields plus "Sort tiles", and register it in `FORM_OVERRIDES` in `src/constants/widgetRegistry.js`. Verify: Vitest on the form. +- [x] 2. Keep local `{count, lastUsedAt}` per placement in `src/composables/useTileClickTracking.js`, storage failures caught. Verify: Vitest with storage available and blocked. +- [x] 3. Order and reflow children in view mode in `ContainerWidget.vue` for the five modes, with the tie rules and a once-per-load shuffle. Verify: Vitest per mode. +- [x] 4. Add "Forget my usage" to the container menu and the "Sorted by" accessible description. Verify: Vitest and an axe check. +- [ ] 5. Playwright: most used moves a clicked tile to the front after reload; "By hand" restores the stored layout. Not written: the requirements carry `@e2e exclude` naming the Vitest files that cover them. +- [x] 6. Add `nl` and `en` strings, fold the delta into `openspec/specs/container-widget/spec.md` on archive, and set d-tile-sort and tile-most-used to `built` in the parity matrix with evidence lines. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index 24cd01e13..c2d5e72b6 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -1424,11 +1424,12 @@ "homarr": "no", "dashy": "yes", "built": { - "state": "specified", + "state": "built", + "evidence": "src/components/Widgets/Renderers/ContainerWidget.vue (displayChildren); src/utils/sortContainerTiles.js; src/components/Widgets/Renderers/ContainerWidgetForm.vue; src/composables/useTileClickTracking.js (readLocalTileUse)", "owner": "ConductionNL/launchpad", - "change": "openspec/changes/launcher-tile-sorting" + "change": "openspec/changes/archive/2026-09-30-launcher-tile-sorting" }, - "reachedOn": "nothing", + "reachedOn": "A container widget: Edit > Sort tiles (by hand, alphabetically, most used, last used, at random); the order applies when the dashboard is viewed", "evidence": { "workspace-365": "https://support.workspace365.net/en/articles/175612-faq-workspace-365-v4 'Frequently used apps' moved to the navigation menu (read 2026-09-26)", "nextcloud-dashboard": "source read at v35.0.1: the app menu order is set by hand (apps/theming/src/components/UserSectionAppMenu.vue:133-135, AdminSectionAppMenu.vue:86-90); searched apps/dashboard and apps/theming for usage, frequent, most used: none. The shipped recommendations widget suggests files, not apps", @@ -5045,11 +5046,12 @@ "homarr": "no", "dashy": "yes", "built": { - "state": "specified", + "state": "built", + "evidence": "src/components/Widgets/Renderers/ContainerWidget.vue (displayChildren); src/utils/sortContainerTiles.js; src/components/Widgets/Renderers/ContainerWidgetForm.vue; src/composables/useTileClickTracking.js (readLocalTileUse)", "owner": "ConductionNL/launchpad", - "change": "openspec/changes/launcher-tile-sorting" + "change": "openspec/changes/archive/2026-09-30-launcher-tile-sorting" }, - "reachedOn": "nothing", + "reachedOn": "A container widget: Edit > Sort tiles (by hand, alphabetically, most used, last used, at random); the order applies when the dashboard is viewed", "evidence": { "workspace-365": "not checked: user guide 175363 and FAQ for admins 175608 describe drag and drop reordering and automatic gap filling, no sort by name, last use or random (read 2026-09-26)", "microsoft-viva": "not checked: https://learn.microsoft.com/en-us/sharepoint/homesites/available-dashboard-cards card order is set by the author and users drag cards; sort options exist only inside Folder (date, A-Z) and Stream playlist cards, not for cards (read 2026-09-26)", diff --git a/openspec/specs/container-widget/spec.md b/openspec/specs/container-widget/spec.md index a75afa972..1c6deeb07 100644 --- a/openspec/specs/container-widget/spec.md +++ b/openspec/specs/container-widget/spec.md @@ -18,6 +18,7 @@ The `content` object carries these fields: - **backgroundColor** (string, default `'transparent'`) — CSS colour applied to the container wrapper. - **padding** (`'none' | 'small' | 'medium' | 'large'`, default `'medium'`) — preset spacing between the wrapper edge and the inner grid. - **title** (string, default `''`) — optional heading rendered above the inner grid; empty means no heading. +- **sortBy** (`'manual' | 'alphabetical' | 'most-used' | 'last-used' | 'random'`, default `'manual'`): the view-time order of the children (REQ-TSO-001). Never changes the stored positions. ## Inner-grid configuration @@ -124,17 +125,79 @@ Containers MAY hold containers, but the maximum nesting depth is **3 levels**. T ### Requirement: REQ-CONT-007 Form fields -The container's add/edit form MUST collect three fields, all optional: +The container's add and edit form MUST collect four fields, all optional: -- `backgroundColor` — hex colour (NcColorPicker), default `'transparent'` -- `padding` — enum `'none' | 'small' | 'medium' | 'large'` (NcSelect), default `'medium'` -- `title` — string (NcTextField), default `''` (no title rendered when empty) +- `backgroundColor`: hex colour (NcColorPicker), default `'transparent'` +- `padding`: enum `'none' | 'small' | 'medium' | 'large'` (NcSelect), default `'medium'` +- `title`: string (NcTextField), default `''` (no title rendered when empty) +- `sortBy`: enum `'manual' | 'alphabetical' | 'most-used' | 'last-used' | 'random'` (NcSelect with an input label), default `'manual'` -Children are NOT managed via this form — they're added/removed/moved via the inner grid's own affordances when the container is in edit mode. +Children are NOT managed via this form. They are added, removed and moved through the inner grid's own affordances when the container is in edit mode. #### Scenario: Form has three fields - **GIVEN** the container form is mounted - **WHEN** rendered -- **THEN** exactly three input controls MUST be present: backgroundColor picker, padding select, title text field +- **THEN** the three communal input controls MUST be present: background colour picker, padding select and title text field +- **AND** LaunchPad's form MUST add exactly one more control, the "Sort tiles" select - **AND** no "manage children" UI MUST be in the form + +### Requirement: A container sorts its tiles when viewed (REQ-TSO-001) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +In view mode a container MUST order its children by its `sortBy` setting and reflow them row by row into its inner grid, keeping each child's size. It MUST NOT write the new positions back. In edit mode it MUST show the stored positions. + +#### Scenario: Alphabetical order + +- **GIVEN** a container "Applicaties" on Pieter's dashboard with tiles "Zaaksysteem", "Agenda" and "Mail" placed in that order, and "Sort tiles" set to "Alphabetically" +- **WHEN** Pieter opens the Workspace page +- **THEN** he sees "Agenda", "Mail", "Zaaksysteem" from left to right + +#### Scenario: Back to the author's layout + +- **GIVEN** the same container sorted alphabetically +- **WHEN** the author sets "Sort tiles" to "By hand" and saves +- **THEN** the tiles show in their stored order "Zaaksysteem", "Agenda", "Mail" + +### Requirement: Most used and last used read the viewer's own browser (REQ-TSO-002) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +Most used and last used MUST order tiles by counts and times kept in the viewer's browser storage, which MUST NOT be sent to the server. Tiles with equal counts MUST fall back to alphabetical order. When browser storage is unavailable the container MUST show the stored order without an error. + +#### Scenario: A daily app moves to the front + +- **GIVEN** a container sorted "Most used" with tiles "Agenda", "Mail" and "Zaaksysteem", and Pieter has never clicked any of them +- **WHEN** he opens "Zaaksysteem" three times and reloads the Workspace page +- **THEN** "Zaaksysteem" is the first tile in the container + +#### Scenario: Counts stay on the device + +- **GIVEN** Pieter clicks tiles in a container sorted "Most used" +- **WHEN** the clicks are recorded +- **THEN** no request carries his per-tile counts, and the only server call per click is the existing anonymous `POST /api/tile-click/{placementId}` + +### Requirement: Viewers can forget their usage (REQ-TSO-003) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +A container sorted by most used or last used MUST offer "Forget my usage" in its menu, which clears the viewer's local counts for that container's tiles. + +#### Scenario: Forget my usage + +- **GIVEN** "Zaaksysteem" leads Pieter's "Most used" container +- **WHEN** he chooses "Forget my usage" in the container menu +- **THEN** the tiles show in alphabetical order until he clicks one again + +### Requirement: Random order holds for the page load (REQ-TSO-004) + +@e2e exclude Playwright is not wired for container sorting yet; covered by src/components/Widgets/Renderers/__tests__/ContainerWidget.sort.spec.js and ContainerWidgetForm.spec.js + +Random order MUST shuffle once per page load and keep that order until the page is reloaded. + +#### Scenario: No jumping tiles + +- **GIVEN** a container sorted "At random" +- **WHEN** Pieter opens the Workspace page and a widget elsewhere refreshes its data +- **THEN** the tile order in the container stays the same until he reloads From e0d60b82f3e470154e5fe7d9b78a49b0f46ab6ca Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 07:57:01 +0200 Subject: [PATCH 03/11] chore(containers): tag the local tile use helpers with their spec --- src/composables/useTileClickTracking.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/composables/useTileClickTracking.js b/src/composables/useTileClickTracking.js index fb855215a..269c6169d 100644 --- a/src/composables/useTileClickTracking.js +++ b/src/composables/useTileClickTracking.js @@ -161,6 +161,7 @@ export function readLocalTileUse() { * * @param {object} use The use map. * @return {boolean} True when stored. + * @spec openspec/specs/container-widget/spec.md */ function writeLocalTileUse(use) { try { @@ -250,6 +251,7 @@ export function randomRankFor(key) { * Drop the page-load random ranks. * * @return {void} + * @spec openspec/specs/container-widget/spec.md */ function resetRandomRanks() { randomRanks = new Map() From 6b684771d22253e9e96cd1c00df04c28c2c68159 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 08:01:11 +0200 Subject: [PATCH 04/11] feat(search): administrators define search shortcuts, a prefix that sends a query straight to a site search_shortcuts holds up to 30 entries {prefix, name, urlTemplate}; a prefix is ! and 1 to 10 letters or digits, unique; the address passes the https {query} check the quick-search fallback already uses. The list reaches every page through the workspace initial state. REQ-QSP-001, REQ-QSP-002 (search-ai-prefix-shortcuts) --- appinfo/routes.php | 4 + lib/Controller/PageController.php | 6 + lib/Controller/SearchShortcutsController.php | 131 +++++++++++++++ lib/Db/AdminSettingKey.php | 4 +- lib/Service/AdminSettingsService.php | 4 +- lib/Service/InitialStateBuilder.php | 16 ++ lib/Service/SearchShortcutService.php | 150 ++++++++++++++++++ lib/actions.seed.json | 2 + src/utils/loadInitialState.js | 4 + .../PageControllerBridgedWidgetsTest.php | 1 + tests/Unit/Service/ActionAuthBaselineTest.php | 2 + .../Unit/Service/InitialStateBuilderTest.php | 28 ++++ .../Service/SearchShortcutServiceTest.php | 124 +++++++++++++++ 13 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 lib/Controller/SearchShortcutsController.php create mode 100644 lib/Service/SearchShortcutService.php create mode 100644 tests/Unit/Service/SearchShortcutServiceTest.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 8b77a0048..8def0c811 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -455,6 +455,10 @@ // returns `{users, total, hasMore}` with offset-based pagination. ['name' => 'peopleWidget#getUsers', 'url' => '/api/people', 'verb' => 'GET'], + // Search shortcuts (search-ai-prefix-shortcuts REQ-QSP-001): admin list. + ['name' => 'searchShortcuts#index', 'url' => '/api/admin/search-shortcuts', 'verb' => 'GET'], + ['name' => 'searchShortcuts#save', 'url' => '/api/admin/search-shortcuts', 'verb' => 'PUT'], + // Custom profile fields (REQ-PEX-001, REQ-PEX-002): own values for // every signed-in person, definitions for administrators. ['name' => 'profileFields#getOwn', 'url' => '/api/profile-fields/me', 'verb' => 'GET'], diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index fcb777f6c..f4816feb7 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -34,6 +34,7 @@ use OCA\LaunchPad\Service\InitialState\Page; use OCA\LaunchPad\Service\InitialStateBuilder; use OCA\LaunchPad\Service\RoleFeaturePermissionService; +use OCA\LaunchPad\Service\SearchShortcutService; use OCA\LaunchPad\Service\WidgetService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\AnonRateLimit; @@ -91,6 +92,9 @@ class PageController extends Controller { * admin setting * (tile-quick-search * REQ-QSEARCH-004). + * @param SearchShortcutService $searchShortcuts The search shortcuts every + * search box honours + * (REQ-QSP-002). */ public function __construct( IRequest $request, @@ -103,6 +107,7 @@ public function __construct( private readonly DashboardTreeService $treeService, private readonly LoggerInterface $logger, private readonly AdminSettingsService $adminSettingsService, + private readonly SearchShortcutService $searchShortcuts, ) { parent::__construct(appName: Application::APP_ID, request: $request); }//end __construct() @@ -224,6 +229,7 @@ public function index(string $deepLink = ''): TemplateResponse { ->setAllowedWidgets($allowedWidgets) ->setDeepLinkPath($activeState['deepLinkPath']) ->setQuicksearchFallbackTarget($quicksearchFallback) + ->setSearchShortcuts($this->searchShortcuts->getShortcuts()) ->apply(); // 🔴 NO CHROME SLOT IDS. This used to pass diff --git a/lib/Controller/SearchShortcutsController.php b/lib/Controller/SearchShortcutsController.php new file mode 100644 index 000000000..376cd2d04 --- /dev/null +++ b/lib/Controller/SearchShortcutsController.php @@ -0,0 +1,131 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @version GIT:auto + * @link https://conduction.nl + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace OCA\LaunchPad\Controller; + +use InvalidArgumentException; +use OCA\LaunchPad\AppInfo\Application; +use OCA\LaunchPad\Service\ActionAuthService; +use OCA\LaunchPad\Service\SearchShortcutService; +use OCA\LaunchPad\Settings\LaunchPadAdmin; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUserSession; + +/** + * Search shortcut administration. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ +class SearchShortcutsController extends Controller { + /** + * Constructor. + * + * @param IRequest $request HTTP request. + * @param SearchShortcutService $shortcuts Shortcut service. + * @param ActionAuthService $actionAuth ADR-023 action authorization. + * @param IUserSession $userSession Current user. + * @param IGroupManager $groupManager Admin check. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + public function __construct( + IRequest $request, + private readonly SearchShortcutService $shortcuts, + private readonly ActionAuthService $actionAuth, + private readonly IUserSession $userSession, + private readonly IGroupManager $groupManager, + ) { + parent::__construct( + appName: Application::APP_ID, + request: $request + ); + }//end __construct() + + /** + * The shortcut list (administrators). + * + * @return JSONResponse `{shortcuts: [...]}`, 401 or 403. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + #[AuthorizedAdminSetting(LaunchPadAdmin::class)] + public function index(): JSONResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['error' => 'Not authenticated'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + if ($this->groupManager->isAdmin(userId: $user->getUID()) === false) { + return new JSONResponse(data: ['error' => 'Admin required'], statusCode: Http::STATUS_FORBIDDEN); + } + + try { + $this->actionAuth->requireAction($user, 'search-shortcut.list'); + } catch (OCSForbiddenException) { + return new JSONResponse(data: ['error' => 'Forbidden'], statusCode: Http::STATUS_FORBIDDEN); + } + + return new JSONResponse(data: ['shortcuts' => $this->shortcuts->getShortcuts()]); + }//end index() + + /** + * Replace the shortcut list (administrators). + * + * @param array|null $shortcuts The new list. + * + * @return JSONResponse `{shortcuts: [...]}`, 400, 401 or 403. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + #[AuthorizedAdminSetting(LaunchPadAdmin::class)] + public function save(?array $shortcuts = null): JSONResponse { + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse(data: ['error' => 'Not authenticated'], statusCode: Http::STATUS_UNAUTHORIZED); + } + + if ($this->groupManager->isAdmin(userId: $user->getUID()) === false) { + return new JSONResponse(data: ['error' => 'Admin required'], statusCode: Http::STATUS_FORBIDDEN); + } + + try { + $this->actionAuth->requireAction($user, 'search-shortcut.save'); + } catch (OCSForbiddenException) { + return new JSONResponse(data: ['error' => 'Forbidden'], statusCode: Http::STATUS_FORBIDDEN); + } + + try { + $saved = $this->shortcuts->saveShortcuts(raw: ($shortcuts ?? [])); + } catch (InvalidArgumentException $e) { + return new JSONResponse(data: ['error' => $e->getMessage()], statusCode: Http::STATUS_BAD_REQUEST); + } + + return new JSONResponse(data: ['shortcuts' => $saved]); + }//end save() +}//end class diff --git a/lib/Db/AdminSettingKey.php b/lib/Db/AdminSettingKey.php index 5abb9f3e6..f56c35290 100644 --- a/lib/Db/AdminSettingKey.php +++ b/lib/Db/AdminSettingKey.php @@ -16,7 +16,8 @@ * - Footer: FOOTER_ENABLED, FOOTER_HTML, FOOTER_CONFIG, * FOOTER_BACKGROUND_COLOR, FOOTER_TEXT_COLOR * - Setup wizard: SETUP_WIZARD_COMPLETE, CONTENT_STORAGE - * - Quick search: QUICKSEARCH_FALLBACK_TARGET (tile-quick-search) + * - Quick search: QUICKSEARCH_FALLBACK_TARGET (tile-quick-search), + * SEARCH_SHORTCUTS (search-ai-prefix-shortcuts) * * @category Db * @package OCA\LaunchPad\Db @@ -62,4 +63,5 @@ enum AdminSettingKey: string { case MAX_DASHBOARDS_PER_USER = 'max_dashboards_per_user'; case MAX_WIDGETS_PER_DASHBOARD = 'max_widgets_per_dashboard'; case QUICKSEARCH_FALLBACK_TARGET = 'quicksearch_fallback_target'; + case SEARCH_SHORTCUTS = 'search_shortcuts'; }//end enum diff --git a/lib/Service/AdminSettingsService.php b/lib/Service/AdminSettingsService.php index 6188a6705..167e1ab28 100644 --- a/lib/Service/AdminSettingsService.php +++ b/lib/Service/AdminSettingsService.php @@ -264,8 +264,10 @@ public function isValidQuicksearchFallbackTarget(?string $value): bool { * * @return bool true when `$value` is a valid `https` URL template * containing `{query}`. + * + * @spec openspec/specs/tile-quick-search/spec.md */ - private function isValidQuicksearchFallbackUrlTemplate(string $value): bool { + public function isValidQuicksearchFallbackUrlTemplate(string $value): bool { if (trim($value) === '' || str_contains(haystack: $value, needle: '{query}') === false) { return false; } diff --git a/lib/Service/InitialStateBuilder.php b/lib/Service/InitialStateBuilder.php index b849806c1..1ebf80957 100644 --- a/lib/Service/InitialStateBuilder.php +++ b/lib/Service/InitialStateBuilder.php @@ -338,6 +338,22 @@ public function setQuicksearchFallbackTarget(string $fallbackTarget): self { return $this; }//end setQuicksearchFallbackTarget() + /** + * Set the search shortcuts every search box honours (search-ai-prefix-shortcuts, + * REQ-QSP-002). Optional key, like `quicksearchFallbackTarget`: the JS reader + * defaults it to an empty list. + * + * @param array $shortcuts Validated `{prefix, name, urlTemplate}` entries. + * + * @return self Fluent. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + public function setSearchShortcuts(array $shortcuts): self { + $this->values['searchShortcuts'] = array_values(array: $shortcuts); + return $this; + }//end setSearchShortcuts() + /** * Set every Nextcloud group (admin). * diff --git a/lib/Service/SearchShortcutService.php b/lib/Service/SearchShortcutService.php new file mode 100644 index 000000000..710610424 --- /dev/null +++ b/lib/Service/SearchShortcutService.php @@ -0,0 +1,150 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * @link https://conduction.nl + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace OCA\LaunchPad\Service; + +use InvalidArgumentException; +use OCA\LaunchPad\Db\AdminSettingKey; +use OCA\LaunchPad\Db\AdminSettingMapper; + +/** + * Read and validate the search shortcut list. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ +class SearchShortcutService { + /** + * Most shortcuts one instance may define. + * + * @var int + */ + public const MAX_SHORTCUTS = 30; + + /** + * A prefix: `!` and 1 to 10 letters or digits. + * + * @var string + */ + public const PREFIX_PATTERN = '/^![a-z0-9]{1,10}$/'; + + /** + * Constructor. + * + * @param AdminSettingMapper $settingMapper Admin settings store. + * @param AdminSettingsService $adminSettings Holds the https `{query}` template check. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + public function __construct( + private readonly AdminSettingMapper $settingMapper, + private readonly AdminSettingsService $adminSettings, + ) { + }//end __construct() + + /** + * The stored shortcuts. A stored list that no longer validates reads as + * empty, so the page never receives an address it must not open. + * + * @return array + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + public function getShortcuts(): array { + $raw = $this->settingMapper->getValue(key: AdminSettingKey::SEARCH_SHORTCUTS->value, default: []); + if (is_array(value: $raw) === false) { + return []; + } + + try { + return $this->validate(raw: $raw); + } catch (InvalidArgumentException) { + return []; + } + }//end getShortcuts() + + /** + * Validate and store the shortcuts. + * + * @param array $raw Shortcuts as sent by the admin screen. + * + * @return array The stored list. + * + * @throws InvalidArgumentException When an entry is invalid. + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + public function saveShortcuts(array $raw): array { + $shortcuts = $this->validate(raw: $raw); + $this->settingMapper->setSetting(key: AdminSettingKey::SEARCH_SHORTCUTS->value, value: $shortcuts); + + return $shortcuts; + }//end saveShortcuts() + + /** + * Check and normalise the list. + * + * @param array $raw Raw entries. + * + * @return array + * + * @throws InvalidArgumentException When an entry is invalid. + */ + private function validate(array $raw): array { + if (count(value: $raw) > self::MAX_SHORTCUTS) { + throw new InvalidArgumentException(message: 'At most ' . self::MAX_SHORTCUTS . ' search shortcuts'); + } + + $result = []; + $seen = []; + foreach (array_values(array: $raw) as $entry) { + if (is_array(value: $entry) === false) { + throw new InvalidArgumentException(message: 'A search shortcut must be an object'); + } + + $prefix = mb_strtolower(string: trim(string: (string)($entry['prefix'] ?? ''))); + if (preg_match(pattern: self::PREFIX_PATTERN, subject: $prefix) !== 1) { + throw new InvalidArgumentException(message: 'A prefix is ! followed by 1 to 10 letters or digits: ' . $prefix); + } + + if (isset($seen[$prefix]) === true) { + throw new InvalidArgumentException(message: 'Prefix used twice: ' . $prefix); + } + + $name = trim(string: (string)($entry['name'] ?? '')); + if ($name === '' || mb_strlen(string: $name) > 64) { + throw new InvalidArgumentException(message: 'A shortcut name must be 1 to 64 characters: ' . $prefix); + } + + $template = trim(string: (string)($entry['urlTemplate'] ?? '')); + if ($this->adminSettings->isValidQuicksearchFallbackUrlTemplate(value: $template) === false) { + throw new InvalidArgumentException(message: 'The address must be an https address containing {query}: ' . $prefix); + } + + $seen[$prefix] = true; + $result[] = ['prefix' => $prefix, 'name' => $name, 'urlTemplate' => $template]; + }//end foreach + + return $result; + }//end validate() +}//end class diff --git a/lib/actions.seed.json b/lib/actions.seed.json index f3c39cf91..5f717c98f 100644 --- a/lib/actions.seed.json +++ b/lib/actions.seed.json @@ -60,6 +60,8 @@ "metadata-admin.update-field": ["admin"], "metadata-admin.delete-field": ["admin"], "people-widget.get-users": ["admin"], + "search-shortcut.list": ["admin"], + "search-shortcut.save": ["admin"], "profile-fields.get-own": ["admin", "@all"], "profile-fields.save-own": ["admin", "@all"], "profile-fields.get-definitions": ["admin"], diff --git a/src/utils/loadInitialState.js b/src/utils/loadInitialState.js index 29a7ab48e..66522a4aa 100644 --- a/src/utils/loadInitialState.js +++ b/src/utils/loadInitialState.js @@ -63,6 +63,10 @@ const PAGE_KEYS = { // PHP side yet simply omits it, and 'none' is the safe default // (no navigation on no-match Enter). quicksearchFallbackTarget: 'none', + // search-ai-prefix-shortcuts REQ-QSP-002: the admin's search + // shortcuts `{prefix, name, urlTemplate}`. Optional key; an empty + // list keeps every search box as it was. + searchShortcuts: [], }, admin: { allGroups: [], diff --git a/tests/Unit/Controller/PageControllerBridgedWidgetsTest.php b/tests/Unit/Controller/PageControllerBridgedWidgetsTest.php index 129bb9211..de29ee609 100644 --- a/tests/Unit/Controller/PageControllerBridgedWidgetsTest.php +++ b/tests/Unit/Controller/PageControllerBridgedWidgetsTest.php @@ -64,6 +64,7 @@ private function makeController(WidgetService $widgetService): PageController { treeService: $this->createMock(DashboardTreeService::class), logger: $this->createMock(LoggerInterface::class), adminSettingsService: $this->createMock(AdminSettingsService::class), + searchShortcuts: $this->createMock(\OCA\LaunchPad\Service\SearchShortcutService::class), ); }//end makeController() diff --git a/tests/Unit/Service/ActionAuthBaselineTest.php b/tests/Unit/Service/ActionAuthBaselineTest.php index 6dc033c92..83f46704a 100644 --- a/tests/Unit/Service/ActionAuthBaselineTest.php +++ b/tests/Unit/Service/ActionAuthBaselineTest.php @@ -84,6 +84,8 @@ class ActionAuthBaselineTest extends TestCase { 'metadata-admin.update-field', 'metadata-admin.delete-field', 'people-widget.get-users', + 'search-shortcut.list', + 'search-shortcut.save', 'profile-fields.get-definitions', 'profile-fields.save-definitions', 'rule.get-rules', diff --git a/tests/Unit/Service/InitialStateBuilderTest.php b/tests/Unit/Service/InitialStateBuilderTest.php index ee7e7d3a6..5763c708e 100644 --- a/tests/Unit/Service/InitialStateBuilderTest.php +++ b/tests/Unit/Service/InitialStateBuilderTest.php @@ -29,6 +29,34 @@ use PHPUnit\Framework\TestCase; class InitialStateBuilderTest extends TestCase { + /** + * REQ-QSP-002: the search shortcuts reach the workspace page. + * + * @return void + */ + public function testSearchShortcutsReachTheWorkspace(): void { + $sink = []; + $state = $this->makeRecordingState($sink); + $shortcuts = [['prefix' => '!t', 'name' => 'TOPdesk', 'urlTemplate' => 'https://topdesk.example.nl/?q={query}']]; + + (new InitialStateBuilder(initialState: $state, page: Page::WORKSPACE)) + ->setWidgets([]) + ->setLayout([]) + ->setPrimaryGroup('g') + ->setPrimaryGroupName('G') + ->setIsAdmin(false) + ->setActiveDashboardId('') + ->setDashboardSource('group') + ->setGroupDashboards([]) + ->setUserDashboards([]) + ->setAllowUserDashboards(false) + ->setAllowedWidgets(null) + ->setSearchShortcuts($shortcuts) + ->apply(); + + $this->assertSame($shortcuts, $sink['searchShortcuts']); + }//end testSearchShortcutsReachTheWorkspace() + /** * In-memory IInitialState stub that records every push. * diff --git a/tests/Unit/Service/SearchShortcutServiceTest.php b/tests/Unit/Service/SearchShortcutServiceTest.php new file mode 100644 index 000000000..d31c9d225 --- /dev/null +++ b/tests/Unit/Service/SearchShortcutServiceTest.php @@ -0,0 +1,124 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace Unit\Service; + +use InvalidArgumentException; +use OCA\LaunchPad\Db\AdminSetting; +use OCA\LaunchPad\Db\AdminSettingMapper; +use OCA\LaunchPad\Service\AdminSettingsService; +use OCA\LaunchPad\Service\SearchShortcutService; +use PHPUnit\Framework\TestCase; + +class SearchShortcutServiceTest extends TestCase { + /** + * Stored admin settings. + * + * @var array + */ + private array $settings = []; + + private SearchShortcutService $service; + + protected function setUp(): void { + parent::setUp(); + + $mapper = $this->createMock(AdminSettingMapper::class); + $mapper->method('getValue')->willReturnCallback( + fn (string $key, mixed $default = null): mixed => $this->settings[$key] ?? $default + ); + $mapper->method('setSetting')->willReturnCallback( + function (string $key, mixed $value): AdminSetting { + $this->settings[$key] = $value; + return new AdminSetting(); + } + ); + + // The real template check, the one the quick-search fallback uses. + $adminSettings = new AdminSettingsService(settingMapper: $mapper); + + $this->service = new SearchShortcutService(settingMapper: $mapper, adminSettings: $adminSettings); + }//end setUp() + + public function testAValidListIsStoredWithLowerCasePrefixes(): void { + $saved = $this->service->saveShortcuts( + raw: [ + ['prefix' => '!T', 'name' => ' TOPdesk ', 'urlTemplate' => 'https://topdesk.example.nl/search?q={query}'], + ['prefix' => '!wiki', 'name' => 'Wikipedia', 'urlTemplate' => 'https://nl.wikipedia.org/w/index.php?search={query}'], + ] + ); + + $this->assertSame('!t', $saved[0]['prefix']); + $this->assertSame('TOPdesk', $saved[0]['name']); + $this->assertSame($saved, $this->service->getShortcuts()); + $this->assertSame($saved, $this->settings['search_shortcuts']); + }//end testAValidListIsStoredWithLowerCasePrefixes() + + /** + * @return array}> + */ + public static function invalidEntries(): array { + $template = 'https://example.nl/?q={query}'; + return [ + 'no bang' => [['prefix' => 't', 'name' => 'T', 'urlTemplate' => $template]], + 'too long' => [['prefix' => '!abcdefghijk', 'name' => 'T', 'urlTemplate' => $template]], + 'space in prefix' => [['prefix' => '!a b', 'name' => 'T', 'urlTemplate' => $template]], + 'no name' => [['prefix' => '!t', 'name' => ' ', 'urlTemplate' => $template]], + 'http' => [['prefix' => '!t', 'name' => 'T', 'urlTemplate' => 'http://example.nl/?q={query}']], + 'no placeholder' => [['prefix' => '!t', 'name' => 'T', 'urlTemplate' => 'https://example.nl/']], + 'javascript' => [['prefix' => '!t', 'name' => 'T', 'urlTemplate' => 'javascript:alert({query})']], + ]; + }//end invalidEntries() + + /** + * @dataProvider invalidEntries + */ + public function testAnInvalidEntryIsRefused(array $entry): void { + $this->expectException(InvalidArgumentException::class); + $this->service->saveShortcuts(raw: [$entry]); + }//end testAnInvalidEntryIsRefused() + + public function testPrefixesAreUniqueRegardlessOfCase(): void { + $this->expectException(InvalidArgumentException::class); + $this->service->saveShortcuts( + raw: [ + ['prefix' => '!t', 'name' => 'A', 'urlTemplate' => 'https://a.nl/?q={query}'], + ['prefix' => '!T', 'name' => 'B', 'urlTemplate' => 'https://b.nl/?q={query}'], + ] + ); + }//end testPrefixesAreUniqueRegardlessOfCase() + + public function testAtMostThirtyShortcuts(): void { + $raw = []; + for ($i = 0; $i < 31; $i++) { + $raw[] = ['prefix' => '!s' . $i, 'name' => 'S' . $i, 'urlTemplate' => 'https://s.nl/?q={query}']; + } + + $this->expectException(InvalidArgumentException::class); + $this->service->saveShortcuts(raw: $raw); + }//end testAtMostThirtyShortcuts() + + public function testAStoredListThatNoLongerValidatesReadsAsEmpty(): void { + $this->settings['search_shortcuts'] = [['prefix' => '!t', 'name' => 'T', 'urlTemplate' => 'http://insecure.nl/?q={query}']]; + + $this->assertSame([], $this->service->getShortcuts()); + }//end testAStoredListThatNoLongerValidatesReadsAsEmpty() +}//end class From af2c9ec62f6453f128a992f573e1cbb58f10e6f8 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 08:55:11 +0200 Subject: [PATCH 05/11] feat(search): a known prefix sends the query straight to its site, and ? lists the shortcuts In the search widget a query whose first word is a shortcut prefix shows one result, Search TOPdesk for printer, and Enter opens the site in a new tab; an unknown prefix filters tiles as before; ! or ? lists the shortcuts. Administrators edit the list on a new admin tab. REQ-SPX-001..003 (search-ai-prefix-shortcuts) --- l10n/be.js | 14 +- l10n/be.json | 14 +- l10n/bg.js | 14 +- l10n/bg.json | 14 +- l10n/bs.js | 14 +- l10n/bs.json | 14 +- l10n/ca.js | 14 +- l10n/ca.json | 14 +- l10n/cs.js | 14 +- l10n/cs.json | 14 +- l10n/da.js | 14 +- l10n/da.json | 14 +- l10n/de.js | 14 +- l10n/de.json | 14 +- l10n/el.js | 14 +- l10n/el.json | 14 +- l10n/en.js | 14 +- l10n/en.json | 14 +- l10n/es.js | 14 +- l10n/es.json | 14 +- l10n/et.js | 14 +- l10n/et.json | 14 +- l10n/fi.js | 14 +- l10n/fi.json | 14 +- l10n/fr.js | 14 +- l10n/fr.json | 14 +- l10n/ga.js | 14 +- l10n/ga.json | 14 +- l10n/hr.js | 14 +- l10n/hr.json | 14 +- l10n/hu.js | 14 +- l10n/hu.json | 14 +- l10n/is.js | 14 +- l10n/is.json | 14 +- l10n/it.js | 14 +- l10n/it.json | 14 +- l10n/lb.js | 14 +- l10n/lb.json | 14 +- l10n/lt.js | 14 +- l10n/lt.json | 14 +- l10n/lv.js | 14 +- l10n/lv.json | 14 +- l10n/mk.js | 14 +- l10n/mk.json | 14 +- l10n/mt.js | 14 +- l10n/mt.json | 14 +- l10n/nb.js | 14 +- l10n/nb.json | 14 +- l10n/nl.js | 14 +- l10n/nl.json | 14 +- l10n/pl.js | 14 +- l10n/pl.json | 14 +- l10n/pt.js | 14 +- l10n/pt.json | 14 +- l10n/rm.js | 14 +- l10n/rm.json | 14 +- l10n/ro.js | 14 +- l10n/ro.json | 14 +- l10n/ru.js | 14 +- l10n/ru.json | 14 +- l10n/sk.js | 14 +- l10n/sk.json | 14 +- l10n/sl.js | 14 +- l10n/sl.json | 14 +- l10n/sq.js | 14 +- l10n/sq.json | 14 +- l10n/sr.js | 14 +- l10n/sr.json | 14 +- l10n/sv.js | 14 +- l10n/sv.json | 14 +- l10n/tr.js | 14 +- l10n/tr.json | 14 +- l10n/uk.js | 14 +- l10n/uk.json | 14 +- lib/Controller/PageController.php | 2 +- lib/Controller/SearchShortcutsController.php | 2 +- lib/Service/InitialStateBuilder.php | 2 +- lib/Service/SearchShortcutService.php | 4 +- src/components/RuntimeShellSearch.vue | 63 ++++- .../Widgets/Renderers/SearchWidget.vue | 10 + .../RuntimeShellSearch.shortcuts.spec.js | 138 +++++++++++ src/components/admin/AdminSettings.vue | 9 + .../admin/tabs/SearchShortcutsTab.vue | 217 ++++++++++++++++++ .../tabs/__tests__/SearchShortcutsTab.spec.js | 105 +++++++++ src/composables/useTileSearch.js | 110 ++++++++- src/utils/loadInitialState.js | 2 +- .../Unit/Service/InitialStateBuilderTest.php | 2 +- .../Service/SearchShortcutServiceTest.php | 2 +- 88 files changed, 1618 insertions(+), 86 deletions(-) create mode 100644 src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js create mode 100644 src/components/admin/tabs/SearchShortcutsTab.vue create mode 100644 src/components/admin/tabs/__tests__/SearchShortcutsTab.spec.js diff --git a/l10n/be.js b/l10n/be.js index 009cd5a99..76bf6f451 100644 --- a/l10n/be.js +++ b/l10n/be.js @@ -1323,6 +1323,18 @@ OC.L10N.register( "Alphabetically" : "Па алфавіце", "Most used" : "Найчасцей выкарыстоўваныя", "Last used" : "Апошнія выкарыстаныя", - "At random" : "Выпадкова" + "At random" : "Выпадкова", + "Search shortcuts" : "Ярлыкі пошуку", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Пошук, які пачынаецца з прэфікса, напрыклад !t printer, адразу адкрывае гэты сайт у новай укладцы. Увод ? у полі пошуку паказвае спіс ярлыкоў.", + "Prefix" : "Прэфікс", + "Address with {query}" : "Адрас з {query}", + "No search shortcuts yet." : "Ярлыкоў пошуку пакуль няма.", + "Search shortcuts saved." : "Ярлыкі пошуку захаваны.", + "Add shortcut" : "Дадаць ярлык", + "Save search shortcuts" : "Захаваць ярлыкі пошуку", + "The search shortcuts could not be loaded." : "Не ўдалося загрузіць ярлыкі пошуку.", + "The search shortcuts could not be saved." : "Не ўдалося захаваць ярлыкі пошуку.", + "Search {name} for {query}" : "Шукаць у {name}: {query}", + "Type what to search for in {name}" : "Увядзіце, што шукаць у {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/be.json b/l10n/be.json index 4b7180f08..3d594f825 100644 --- a/l10n/be.json +++ b/l10n/be.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Па алфавіце", "Most used": "Найчасцей выкарыстоўваныя", "Last used": "Апошнія выкарыстаныя", - "At random": "Выпадкова" + "At random": "Выпадкова", + "Search shortcuts": "Ярлыкі пошуку", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Пошук, які пачынаецца з прэфікса, напрыклад !t printer, адразу адкрывае гэты сайт у новай укладцы. Увод ? у полі пошуку паказвае спіс ярлыкоў.", + "Prefix": "Прэфікс", + "Address with {query}": "Адрас з {query}", + "No search shortcuts yet.": "Ярлыкоў пошуку пакуль няма.", + "Search shortcuts saved.": "Ярлыкі пошуку захаваны.", + "Add shortcut": "Дадаць ярлык", + "Save search shortcuts": "Захаваць ярлыкі пошуку", + "The search shortcuts could not be loaded.": "Не ўдалося загрузіць ярлыкі пошуку.", + "The search shortcuts could not be saved.": "Не ўдалося захаваць ярлыкі пошуку.", + "Search {name} for {query}": "Шукаць у {name}: {query}", + "Type what to search for in {name}": "Увядзіце, што шукаць у {name}" } } diff --git a/l10n/bg.js b/l10n/bg.js index 29818f4a3..d25241b77 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "По азбучен ред", "Most used" : "Най-използвани", "Last used" : "Последно използвани", - "At random" : "В случаен ред" + "At random" : "В случаен ред", + "Search shortcuts" : "Преки пътища за търсене", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Търсене, което започва с префикс, например !t printer, отива направо към този сайт в нов раздел. Въвеждането на ? в поле за търсене показва преките пътища.", + "Prefix" : "Префикс", + "Address with {query}" : "Адрес с {query}", + "No search shortcuts yet." : "Все още няма преки пътища за търсене.", + "Search shortcuts saved." : "Преките пътища за търсене са запазени.", + "Add shortcut" : "Добавяне на пряк път", + "Save search shortcuts" : "Запазване на преките пътища за търсене", + "The search shortcuts could not be loaded." : "Преките пътища за търсене не можаха да се заредят.", + "The search shortcuts could not be saved." : "Преките пътища за търсене не можаха да се запазят.", + "Search {name} for {query}" : "Търсене в {name} за {query}", + "Type what to search for in {name}" : "Въведете какво да търсите в {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/bg.json b/l10n/bg.json index 0879485b6..56adfe841 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -1299,6 +1299,18 @@ "Alphabetically": "По азбучен ред", "Most used": "Най-използвани", "Last used": "Последно използвани", - "At random": "В случаен ред" + "At random": "В случаен ред", + "Search shortcuts": "Преки пътища за търсене", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Търсене, което започва с префикс, например !t printer, отива направо към този сайт в нов раздел. Въвеждането на ? в поле за търсене показва преките пътища.", + "Prefix": "Префикс", + "Address with {query}": "Адрес с {query}", + "No search shortcuts yet.": "Все още няма преки пътища за търсене.", + "Search shortcuts saved.": "Преките пътища за търсене са запазени.", + "Add shortcut": "Добавяне на пряк път", + "Save search shortcuts": "Запазване на преките пътища за търсене", + "The search shortcuts could not be loaded.": "Преките пътища за търсене не можаха да се заредят.", + "The search shortcuts could not be saved.": "Преките пътища за търсене не можаха да се запазят.", + "Search {name} for {query}": "Търсене в {name} за {query}", + "Type what to search for in {name}": "Въведете какво да търсите в {name}" } } diff --git a/l10n/bs.js b/l10n/bs.js index 1f9c11827..62caff64e 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Abecedno", "Most used" : "Najčešće korišteno", "Last used" : "Zadnje korišteno", - "At random" : "Nasumično" + "At random" : "Nasumično", + "Search shortcuts" : "Prečaci za pretragu", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Pretraga koja počinje prefiksom, npr. !t printer, ide ravno na tu stranicu u novoj kartici. Upisivanje ? u polje za pretragu prikazuje prečace.", + "Prefix" : "Prefiks", + "Address with {query}" : "Adresa s {query}", + "No search shortcuts yet." : "Još nema prečaca za pretragu.", + "Search shortcuts saved." : "Prečaci za pretragu su spremljeni.", + "Add shortcut" : "Dodaj prečac", + "Save search shortcuts" : "Spremi prečace za pretragu", + "The search shortcuts could not be loaded." : "Prečaci za pretragu nisu mogli biti učitani.", + "The search shortcuts could not be saved." : "Prečaci za pretragu nisu mogli biti spremljeni.", + "Search {name} for {query}" : "Pretraži {name} za {query}", + "Type what to search for in {name}" : "Upišite što tražite u {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/bs.json b/l10n/bs.json index 4c194ed67..97aa7826f 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Abecedno", "Most used": "Najčešće korišteno", "Last used": "Zadnje korišteno", - "At random": "Nasumično" + "At random": "Nasumično", + "Search shortcuts": "Prečaci za pretragu", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Pretraga koja počinje prefiksom, npr. !t printer, ide ravno na tu stranicu u novoj kartici. Upisivanje ? u polje za pretragu prikazuje prečace.", + "Prefix": "Prefiks", + "Address with {query}": "Adresa s {query}", + "No search shortcuts yet.": "Još nema prečaca za pretragu.", + "Search shortcuts saved.": "Prečaci za pretragu su spremljeni.", + "Add shortcut": "Dodaj prečac", + "Save search shortcuts": "Spremi prečace za pretragu", + "The search shortcuts could not be loaded.": "Prečaci za pretragu nisu mogli biti učitani.", + "The search shortcuts could not be saved.": "Prečaci za pretragu nisu mogli biti spremljeni.", + "Search {name} for {query}": "Pretraži {name} za {query}", + "Type what to search for in {name}": "Upišite što tražite u {name}" } } diff --git a/l10n/ca.js b/l10n/ca.js index 86332cdff..378302230 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabèticament", "Most used" : "Els més usats", "Last used" : "Darrers usats", - "At random" : "A l'atzar" + "At random" : "A l'atzar", + "Search shortcuts" : "Dreceres de cerca", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Una cerca que comença amb un prefix, com ara !t printer, va directament a aquell lloc en una pestanya nova. Escriure ? en un quadre de cerca mostra les dreceres.", + "Prefix" : "Prefix", + "Address with {query}" : "Adreça amb {query}", + "No search shortcuts yet." : "Encara no hi ha dreceres de cerca.", + "Search shortcuts saved." : "S'han desat les dreceres de cerca.", + "Add shortcut" : "Afegeix una drecera", + "Save search shortcuts" : "Desa les dreceres de cerca", + "The search shortcuts could not be loaded." : "No s'han pogut carregar les dreceres de cerca.", + "The search shortcuts could not be saved." : "No s'han pogut desar les dreceres de cerca.", + "Search {name} for {query}" : "Cerca {query} a {name}", + "Type what to search for in {name}" : "Escriviu què voleu cercar a {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/ca.json b/l10n/ca.json index 8b4868e41..7b3a43c40 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabèticament", "Most used": "Els més usats", "Last used": "Darrers usats", - "At random": "A l'atzar" + "At random": "A l'atzar", + "Search shortcuts": "Dreceres de cerca", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Una cerca que comença amb un prefix, com ara !t printer, va directament a aquell lloc en una pestanya nova. Escriure ? en un quadre de cerca mostra les dreceres.", + "Prefix": "Prefix", + "Address with {query}": "Adreça amb {query}", + "No search shortcuts yet.": "Encara no hi ha dreceres de cerca.", + "Search shortcuts saved.": "S'han desat les dreceres de cerca.", + "Add shortcut": "Afegeix una drecera", + "Save search shortcuts": "Desa les dreceres de cerca", + "The search shortcuts could not be loaded.": "No s'han pogut carregar les dreceres de cerca.", + "The search shortcuts could not be saved.": "No s'han pogut desar les dreceres de cerca.", + "Search {name} for {query}": "Cerca {query} a {name}", + "Type what to search for in {name}": "Escriviu què voleu cercar a {name}" } } diff --git a/l10n/cs.js b/l10n/cs.js index e5c89f275..a1e4d098a 100644 --- a/l10n/cs.js +++ b/l10n/cs.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Abecedně", "Most used" : "Nejpoužívanější", "Last used" : "Naposledy použité", - "At random" : "Náhodně" + "At random" : "Náhodně", + "Search shortcuts" : "Zkratky vyhledávání", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Hledání začínající předponou, například !t printer, přejde rovnou na daný web v nové kartě. Napsáním ? do vyhledávacího pole zobrazíte zkratky.", + "Prefix" : "Předpona", + "Address with {query}" : "Adresa s {query}", + "No search shortcuts yet." : "Zatím žádné zkratky vyhledávání.", + "Search shortcuts saved." : "Zkratky vyhledávání jsou uloženy.", + "Add shortcut" : "Přidat zkratku", + "Save search shortcuts" : "Uložit zkratky vyhledávání", + "The search shortcuts could not be loaded." : "Zkratky vyhledávání se nepodařilo načíst.", + "The search shortcuts could not be saved." : "Zkratky vyhledávání se nepodařilo uložit.", + "Search {name} for {query}" : "Hledat {query} v {name}", + "Type what to search for in {name}" : "Napište, co hledat v {name}" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/cs.json b/l10n/cs.json index 6b523cb30..a9f386104 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Abecedně", "Most used": "Nejpoužívanější", "Last used": "Naposledy použité", - "At random": "Náhodně" + "At random": "Náhodně", + "Search shortcuts": "Zkratky vyhledávání", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Hledání začínající předponou, například !t printer, přejde rovnou na daný web v nové kartě. Napsáním ? do vyhledávacího pole zobrazíte zkratky.", + "Prefix": "Předpona", + "Address with {query}": "Adresa s {query}", + "No search shortcuts yet.": "Zatím žádné zkratky vyhledávání.", + "Search shortcuts saved.": "Zkratky vyhledávání jsou uloženy.", + "Add shortcut": "Přidat zkratku", + "Save search shortcuts": "Uložit zkratky vyhledávání", + "The search shortcuts could not be loaded.": "Zkratky vyhledávání se nepodařilo načíst.", + "The search shortcuts could not be saved.": "Zkratky vyhledávání se nepodařilo uložit.", + "Search {name} for {query}": "Hledat {query} v {name}", + "Type what to search for in {name}": "Napište, co hledat v {name}" } } diff --git a/l10n/da.js b/l10n/da.js index dde69e192..eab9196d1 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetisk", "Most used" : "Mest brugte", "Last used" : "Senest brugte", - "At random" : "Tilfældigt" + "At random" : "Tilfældigt", + "Search shortcuts" : "Søgegenveje", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "En søgning, der starter med et præfiks, fx !t printer, går direkte til det websted i en ny fane. Skriv ? i et søgefelt for at se genvejene.", + "Prefix" : "Præfiks", + "Address with {query}" : "Adresse med {query}", + "No search shortcuts yet." : "Ingen søgegenveje endnu.", + "Search shortcuts saved." : "Søgegenvejene er gemt.", + "Add shortcut" : "Tilføj genvej", + "Save search shortcuts" : "Gem søgegenveje", + "The search shortcuts could not be loaded." : "Søgegenvejene kunne ikke indlæses.", + "The search shortcuts could not be saved." : "Søgegenvejene kunne ikke gemmes.", + "Search {name} for {query}" : "Søg i {name} efter {query}", + "Type what to search for in {name}" : "Skriv, hvad du vil søge efter i {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/da.json b/l10n/da.json index 840976e06..0764f8f9e 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabetisk", "Most used": "Mest brugte", "Last used": "Senest brugte", - "At random": "Tilfældigt" + "At random": "Tilfældigt", + "Search shortcuts": "Søgegenveje", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "En søgning, der starter med et præfiks, fx !t printer, går direkte til det websted i en ny fane. Skriv ? i et søgefelt for at se genvejene.", + "Prefix": "Præfiks", + "Address with {query}": "Adresse med {query}", + "No search shortcuts yet.": "Ingen søgegenveje endnu.", + "Search shortcuts saved.": "Søgegenvejene er gemt.", + "Add shortcut": "Tilføj genvej", + "Save search shortcuts": "Gem søgegenveje", + "The search shortcuts could not be loaded.": "Søgegenvejene kunne ikke indlæses.", + "The search shortcuts could not be saved.": "Søgegenvejene kunne ikke gemmes.", + "Search {name} for {query}": "Søg i {name} efter {query}", + "Type what to search for in {name}": "Skriv, hvad du vil søge efter i {name}" } } diff --git a/l10n/de.js b/l10n/de.js index 9e008de03..bb53c9f5e 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alphabetisch", "Most used" : "Am häufigsten genutzt", "Last used" : "Zuletzt genutzt", - "At random" : "Zufällig" + "At random" : "Zufällig", + "Search shortcuts" : "Suchkürzel", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Eine Suche, die mit einem Präfix beginnt, etwa !t printer, geht direkt in einem neuen Tab zu dieser Seite. Ein ? im Suchfeld listet die Kürzel auf.", + "Prefix" : "Präfix", + "Address with {query}" : "Adresse mit {query}", + "No search shortcuts yet." : "Noch keine Suchkürzel.", + "Search shortcuts saved." : "Suchkürzel gespeichert.", + "Add shortcut" : "Kürzel hinzufügen", + "Save search shortcuts" : "Suchkürzel speichern", + "The search shortcuts could not be loaded." : "Die Suchkürzel konnten nicht geladen werden.", + "The search shortcuts could not be saved." : "Die Suchkürzel konnten nicht gespeichert werden.", + "Search {name} for {query}" : "In {name} nach {query} suchen", + "Type what to search for in {name}" : "Gib ein, wonach in {name} gesucht werden soll" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/de.json b/l10n/de.json index bf84b97e3..a4b1b008e 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alphabetisch", "Most used": "Am häufigsten genutzt", "Last used": "Zuletzt genutzt", - "At random": "Zufällig" + "At random": "Zufällig", + "Search shortcuts": "Suchkürzel", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Eine Suche, die mit einem Präfix beginnt, etwa !t printer, geht direkt in einem neuen Tab zu dieser Seite. Ein ? im Suchfeld listet die Kürzel auf.", + "Prefix": "Präfix", + "Address with {query}": "Adresse mit {query}", + "No search shortcuts yet.": "Noch keine Suchkürzel.", + "Search shortcuts saved.": "Suchkürzel gespeichert.", + "Add shortcut": "Kürzel hinzufügen", + "Save search shortcuts": "Suchkürzel speichern", + "The search shortcuts could not be loaded.": "Die Suchkürzel konnten nicht geladen werden.", + "The search shortcuts could not be saved.": "Die Suchkürzel konnten nicht gespeichert werden.", + "Search {name} for {query}": "In {name} nach {query} suchen", + "Type what to search for in {name}": "Gib ein, wonach in {name} gesucht werden soll" } } diff --git a/l10n/el.js b/l10n/el.js index 15a512709..b437cd029 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Αλφαβητικά", "Most used" : "Συχνότερα χρησιμοποιούμενα", "Last used" : "Τελευταία χρήση", - "At random" : "Τυχαία" + "At random" : "Τυχαία", + "Search shortcuts" : "Συντομεύσεις αναζήτησης", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Μια αναζήτηση που ξεκινά με πρόθεμα, όπως !t printer, πηγαίνει κατευθείαν σε εκείνη την τοποθεσία σε νέα καρτέλα. Πληκτρολογώντας ? σε ένα πεδίο αναζήτησης εμφανίζονται οι συντομεύσεις.", + "Prefix" : "Πρόθεμα", + "Address with {query}" : "Διεύθυνση με {query}", + "No search shortcuts yet." : "Δεν υπάρχουν ακόμα συντομεύσεις αναζήτησης.", + "Search shortcuts saved." : "Οι συντομεύσεις αναζήτησης αποθηκεύτηκαν.", + "Add shortcut" : "Προσθήκη συντόμευσης", + "Save search shortcuts" : "Αποθήκευση συντομεύσεων αναζήτησης", + "The search shortcuts could not be loaded." : "Δεν ήταν δυνατή η φόρτωση των συντομεύσεων αναζήτησης.", + "The search shortcuts could not be saved." : "Δεν ήταν δυνατή η αποθήκευση των συντομεύσεων αναζήτησης.", + "Search {name} for {query}" : "Αναζήτηση στο {name} για {query}", + "Type what to search for in {name}" : "Πληκτρολογήστε τι να αναζητηθεί στο {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/el.json b/l10n/el.json index 5c8950f27..372eb89de 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Αλφαβητικά", "Most used": "Συχνότερα χρησιμοποιούμενα", "Last used": "Τελευταία χρήση", - "At random": "Τυχαία" + "At random": "Τυχαία", + "Search shortcuts": "Συντομεύσεις αναζήτησης", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Μια αναζήτηση που ξεκινά με πρόθεμα, όπως !t printer, πηγαίνει κατευθείαν σε εκείνη την τοποθεσία σε νέα καρτέλα. Πληκτρολογώντας ? σε ένα πεδίο αναζήτησης εμφανίζονται οι συντομεύσεις.", + "Prefix": "Πρόθεμα", + "Address with {query}": "Διεύθυνση με {query}", + "No search shortcuts yet.": "Δεν υπάρχουν ακόμα συντομεύσεις αναζήτησης.", + "Search shortcuts saved.": "Οι συντομεύσεις αναζήτησης αποθηκεύτηκαν.", + "Add shortcut": "Προσθήκη συντόμευσης", + "Save search shortcuts": "Αποθήκευση συντομεύσεων αναζήτησης", + "The search shortcuts could not be loaded.": "Δεν ήταν δυνατή η φόρτωση των συντομεύσεων αναζήτησης.", + "The search shortcuts could not be saved.": "Δεν ήταν δυνατή η αποθήκευση των συντομεύσεων αναζήτησης.", + "Search {name} for {query}": "Αναζήτηση στο {name} για {query}", + "Type what to search for in {name}": "Πληκτρολογήστε τι να αναζητηθεί στο {name}" } } diff --git a/l10n/en.js b/l10n/en.js index b2c2ba066..031b339f7 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -1312,7 +1312,19 @@ OC.L10N.register( "Alphabetically" : "Alphabetically", "Most used" : "Most used", "Last used" : "Last used", - "At random" : "At random" + "At random" : "At random", + "Search shortcuts" : "Search shortcuts", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.", + "Prefix" : "Prefix", + "Address with {query}" : "Address with {query}", + "No search shortcuts yet." : "No search shortcuts yet.", + "Search shortcuts saved." : "Search shortcuts saved.", + "Add shortcut" : "Add shortcut", + "Save search shortcuts" : "Save search shortcuts", + "The search shortcuts could not be loaded." : "The search shortcuts could not be loaded.", + "The search shortcuts could not be saved." : "The search shortcuts could not be saved.", + "Search {name} for {query}" : "Search {name} for {query}", + "Type what to search for in {name}" : "Type what to search for in {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/en.json b/l10n/en.json index 28106e6b2..74b6aa512 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -1277,6 +1277,18 @@ "Alphabetically": "Alphabetically", "Most used": "Most used", "Last used": "Last used", - "At random": "At random" + "At random": "At random", + "Search shortcuts": "Search shortcuts", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.", + "Prefix": "Prefix", + "Address with {query}": "Address with {query}", + "No search shortcuts yet.": "No search shortcuts yet.", + "Search shortcuts saved.": "Search shortcuts saved.", + "Add shortcut": "Add shortcut", + "Save search shortcuts": "Save search shortcuts", + "The search shortcuts could not be loaded.": "The search shortcuts could not be loaded.", + "The search shortcuts could not be saved.": "The search shortcuts could not be saved.", + "Search {name} for {query}": "Search {name} for {query}", + "Type what to search for in {name}": "Type what to search for in {name}" } } diff --git a/l10n/es.js b/l10n/es.js index 75312eba8..63d8be189 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabéticamente", "Most used" : "Más usados", "Last used" : "Último uso", - "At random" : "Al azar" + "At random" : "Al azar", + "Search shortcuts" : "Atajos de búsqueda", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Una búsqueda que empieza con un prefijo, como !t printer, va directamente a ese sitio en una pestaña nueva. Escribir ? en un cuadro de búsqueda muestra los atajos.", + "Prefix" : "Prefijo", + "Address with {query}" : "Dirección con {query}", + "No search shortcuts yet." : "Aún no hay atajos de búsqueda.", + "Search shortcuts saved." : "Atajos de búsqueda guardados.", + "Add shortcut" : "Añadir atajo", + "Save search shortcuts" : "Guardar atajos de búsqueda", + "The search shortcuts could not be loaded." : "No se pudieron cargar los atajos de búsqueda.", + "The search shortcuts could not be saved." : "No se pudieron guardar los atajos de búsqueda.", + "Search {name} for {query}" : "Buscar {query} en {name}", + "Type what to search for in {name}" : "Escribe qué buscar en {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/es.json b/l10n/es.json index c63e2e707..35b660cdd 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabéticamente", "Most used": "Más usados", "Last used": "Último uso", - "At random": "Al azar" + "At random": "Al azar", + "Search shortcuts": "Atajos de búsqueda", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Una búsqueda que empieza con un prefijo, como !t printer, va directamente a ese sitio en una pestaña nueva. Escribir ? en un cuadro de búsqueda muestra los atajos.", + "Prefix": "Prefijo", + "Address with {query}": "Dirección con {query}", + "No search shortcuts yet.": "Aún no hay atajos de búsqueda.", + "Search shortcuts saved.": "Atajos de búsqueda guardados.", + "Add shortcut": "Añadir atajo", + "Save search shortcuts": "Guardar atajos de búsqueda", + "The search shortcuts could not be loaded.": "No se pudieron cargar los atajos de búsqueda.", + "The search shortcuts could not be saved.": "No se pudieron guardar los atajos de búsqueda.", + "Search {name} for {query}": "Buscar {query} en {name}", + "Type what to search for in {name}": "Escribe qué buscar en {name}" } } diff --git a/l10n/et.js b/l10n/et.js index 02d6525db..08aaa28d2 100644 --- a/l10n/et.js +++ b/l10n/et.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Tähestiku järgi", "Most used" : "Enim kasutatud", "Last used" : "Viimati kasutatud", - "At random" : "Juhuslikult" + "At random" : "Juhuslikult", + "Search shortcuts" : "Otsingu otseteed", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Eesliitega algav otsing, näiteks !t printer, viib otse sellele saidile uuel vahekaardil. Sisesta otsingukasti ?, et näha otseteid.", + "Prefix" : "Eesliide", + "Address with {query}" : "Aadress koos {query}", + "No search shortcuts yet." : "Otsingu otseteid veel pole.", + "Search shortcuts saved." : "Otsingu otseteed salvestatud.", + "Add shortcut" : "Lisa otsetee", + "Save search shortcuts" : "Salvesta otsingu otseteed", + "The search shortcuts could not be loaded." : "Otsingu otseteid ei õnnestunud laadida.", + "The search shortcuts could not be saved." : "Otsingu otseteid ei õnnestunud salvestada.", + "Search {name} for {query}" : "Otsi {name} kaudu: {query}", + "Type what to search for in {name}" : "Sisesta, mida {name} kaudu otsida" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/et.json b/l10n/et.json index d6760c3fc..d5e64522d 100644 --- a/l10n/et.json +++ b/l10n/et.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Tähestiku järgi", "Most used": "Enim kasutatud", "Last used": "Viimati kasutatud", - "At random": "Juhuslikult" + "At random": "Juhuslikult", + "Search shortcuts": "Otsingu otseteed", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Eesliitega algav otsing, näiteks !t printer, viib otse sellele saidile uuel vahekaardil. Sisesta otsingukasti ?, et näha otseteid.", + "Prefix": "Eesliide", + "Address with {query}": "Aadress koos {query}", + "No search shortcuts yet.": "Otsingu otseteid veel pole.", + "Search shortcuts saved.": "Otsingu otseteed salvestatud.", + "Add shortcut": "Lisa otsetee", + "Save search shortcuts": "Salvesta otsingu otseteed", + "The search shortcuts could not be loaded.": "Otsingu otseteid ei õnnestunud laadida.", + "The search shortcuts could not be saved.": "Otsingu otseteid ei õnnestunud salvestada.", + "Search {name} for {query}": "Otsi {name} kaudu: {query}", + "Type what to search for in {name}": "Sisesta, mida {name} kaudu otsida" } } diff --git a/l10n/fi.js b/l10n/fi.js index a0949456f..94bc9489c 100644 --- a/l10n/fi.js +++ b/l10n/fi.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Aakkosjärjestyksessä", "Most used" : "Eniten käytetyt", "Last used" : "Viimeksi käytetyt", - "At random" : "Satunnaisesti" + "At random" : "Satunnaisesti", + "Search shortcuts" : "Hakuoikotiet", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Etuliitteellä alkava haku, kuten !t printer, avaa kyseisen sivuston suoraan uuteen välilehteen. Kirjoita hakukenttään ?, niin näet oikotiet.", + "Prefix" : "Etuliite", + "Address with {query}" : "Osoite, jossa {query}", + "No search shortcuts yet." : "Hakuoikoteitä ei vielä ole.", + "Search shortcuts saved." : "Hakuoikotiet tallennettu.", + "Add shortcut" : "Lisää oikotie", + "Save search shortcuts" : "Tallenna hakuoikotiet", + "The search shortcuts could not be loaded." : "Hakuoikoteitä ei voitu ladata.", + "The search shortcuts could not be saved." : "Hakuoikoteitä ei voitu tallentaa.", + "Search {name} for {query}" : "Hae {name}-palvelusta: {query}", + "Type what to search for in {name}" : "Kirjoita, mitä haetaan palvelusta {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/fi.json b/l10n/fi.json index 4cd802cbf..fd9488a93 100644 --- a/l10n/fi.json +++ b/l10n/fi.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Aakkosjärjestyksessä", "Most used": "Eniten käytetyt", "Last used": "Viimeksi käytetyt", - "At random": "Satunnaisesti" + "At random": "Satunnaisesti", + "Search shortcuts": "Hakuoikotiet", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Etuliitteellä alkava haku, kuten !t printer, avaa kyseisen sivuston suoraan uuteen välilehteen. Kirjoita hakukenttään ?, niin näet oikotiet.", + "Prefix": "Etuliite", + "Address with {query}": "Osoite, jossa {query}", + "No search shortcuts yet.": "Hakuoikoteitä ei vielä ole.", + "Search shortcuts saved.": "Hakuoikotiet tallennettu.", + "Add shortcut": "Lisää oikotie", + "Save search shortcuts": "Tallenna hakuoikotiet", + "The search shortcuts could not be loaded.": "Hakuoikoteitä ei voitu ladata.", + "The search shortcuts could not be saved.": "Hakuoikoteitä ei voitu tallentaa.", + "Search {name} for {query}": "Hae {name}-palvelusta: {query}", + "Type what to search for in {name}": "Kirjoita, mitä haetaan palvelusta {name}" } } diff --git a/l10n/fr.js b/l10n/fr.js index 7331b0f10..c380e5e21 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Par ordre alphabétique", "Most used" : "Les plus utilisés", "Last used" : "Dernière utilisation", - "At random" : "Au hasard" + "At random" : "Au hasard", + "Search shortcuts" : "Raccourcis de recherche", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Une recherche qui commence par un préfixe, comme !t printer, va directement sur ce site dans un nouvel onglet. Saisir ? dans un champ de recherche affiche les raccourcis.", + "Prefix" : "Préfixe", + "Address with {query}" : "Adresse avec {query}", + "No search shortcuts yet." : "Aucun raccourci de recherche pour l'instant.", + "Search shortcuts saved." : "Raccourcis de recherche enregistrés.", + "Add shortcut" : "Ajouter un raccourci", + "Save search shortcuts" : "Enregistrer les raccourcis de recherche", + "The search shortcuts could not be loaded." : "Les raccourcis de recherche n'ont pas pu être chargés.", + "The search shortcuts could not be saved." : "Les raccourcis de recherche n'ont pas pu être enregistrés.", + "Search {name} for {query}" : "Rechercher {query} dans {name}", + "Type what to search for in {name}" : "Saisissez ce que vous cherchez dans {name}" }, "nplurals=2; plural=(n > 1);" ); diff --git a/l10n/fr.json b/l10n/fr.json index 7e5ff6f4d..fe91c3213 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Par ordre alphabétique", "Most used": "Les plus utilisés", "Last used": "Dernière utilisation", - "At random": "Au hasard" + "At random": "Au hasard", + "Search shortcuts": "Raccourcis de recherche", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Une recherche qui commence par un préfixe, comme !t printer, va directement sur ce site dans un nouvel onglet. Saisir ? dans un champ de recherche affiche les raccourcis.", + "Prefix": "Préfixe", + "Address with {query}": "Adresse avec {query}", + "No search shortcuts yet.": "Aucun raccourci de recherche pour l'instant.", + "Search shortcuts saved.": "Raccourcis de recherche enregistrés.", + "Add shortcut": "Ajouter un raccourci", + "Save search shortcuts": "Enregistrer les raccourcis de recherche", + "The search shortcuts could not be loaded.": "Les raccourcis de recherche n'ont pas pu être chargés.", + "The search shortcuts could not be saved.": "Les raccourcis de recherche n'ont pas pu être enregistrés.", + "Search {name} for {query}": "Rechercher {query} dans {name}", + "Type what to search for in {name}": "Saisissez ce que vous cherchez dans {name}" } } diff --git a/l10n/ga.js b/l10n/ga.js index 5a43ce7fa..68b2ea329 100644 --- a/l10n/ga.js +++ b/l10n/ga.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "In ord aibítre", "Most used" : "Is mó a úsáidtear", "Last used" : "Úsáidte is déanaí", - "At random" : "Go randamach" + "At random" : "Go randamach", + "Search shortcuts" : "Aicearraí cuardaigh", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Téann cuardach a thosaíonn le réimír, mar shampla !t printer, díreach chuig an suíomh sin i gcluaisín nua. Clóscríobh ? i mbosca cuardaigh chun na haicearraí a liostú.", + "Prefix" : "Réimír", + "Address with {query}" : "Seoladh le {query}", + "No search shortcuts yet." : "Níl aon aicearraí cuardaigh fós.", + "Search shortcuts saved." : "Aicearraí cuardaigh sábháilte.", + "Add shortcut" : "Cuir aicearra leis", + "Save search shortcuts" : "Sábháil aicearraí cuardaigh", + "The search shortcuts could not be loaded." : "Níorbh fhéidir na haicearraí cuardaigh a lódáil.", + "The search shortcuts could not be saved." : "Níorbh fhéidir na haicearraí cuardaigh a shábháil.", + "Search {name} for {query}" : "Cuardaigh {name} le haghaidh {query}", + "Type what to search for in {name}" : "Clóscríobh cad atá le cuardach in {name}" }, "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4);" ); diff --git a/l10n/ga.json b/l10n/ga.json index 4f4caf134..fb5ce194a 100644 --- a/l10n/ga.json +++ b/l10n/ga.json @@ -1302,6 +1302,18 @@ "Alphabetically": "In ord aibítre", "Most used": "Is mó a úsáidtear", "Last used": "Úsáidte is déanaí", - "At random": "Go randamach" + "At random": "Go randamach", + "Search shortcuts": "Aicearraí cuardaigh", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Téann cuardach a thosaíonn le réimír, mar shampla !t printer, díreach chuig an suíomh sin i gcluaisín nua. Clóscríobh ? i mbosca cuardaigh chun na haicearraí a liostú.", + "Prefix": "Réimír", + "Address with {query}": "Seoladh le {query}", + "No search shortcuts yet.": "Níl aon aicearraí cuardaigh fós.", + "Search shortcuts saved.": "Aicearraí cuardaigh sábháilte.", + "Add shortcut": "Cuir aicearra leis", + "Save search shortcuts": "Sábháil aicearraí cuardaigh", + "The search shortcuts could not be loaded.": "Níorbh fhéidir na haicearraí cuardaigh a lódáil.", + "The search shortcuts could not be saved.": "Níorbh fhéidir na haicearraí cuardaigh a shábháil.", + "Search {name} for {query}": "Cuardaigh {name} le haghaidh {query}", + "Type what to search for in {name}": "Clóscríobh cad atá le cuardach in {name}" } } diff --git a/l10n/hr.js b/l10n/hr.js index 7bc09558f..4035bd801 100644 --- a/l10n/hr.js +++ b/l10n/hr.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Abecedno", "Most used" : "Najčešće korišteno", "Last used" : "Posljednje korišteno", - "At random" : "Nasumično" + "At random" : "Nasumično", + "Search shortcuts" : "Prečaci za pretraživanje", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Pretraživanje koje počinje prefiksom, npr. !t printer, ide izravno na tu stranicu u novoj kartici. Upisivanjem ? u okvir za pretraživanje prikazuju se prečaci.", + "Prefix" : "Prefiks", + "Address with {query}" : "Adresa s {query}", + "No search shortcuts yet." : "Još nema prečaca za pretraživanje.", + "Search shortcuts saved." : "Prečaci za pretraživanje su spremljeni.", + "Add shortcut" : "Dodaj prečac", + "Save search shortcuts" : "Spremi prečace za pretraživanje", + "The search shortcuts could not be loaded." : "Prečace za pretraživanje nije bilo moguće učitati.", + "The search shortcuts could not be saved." : "Prečace za pretraživanje nije bilo moguće spremiti.", + "Search {name} for {query}" : "Pretraži {name} za {query}", + "Type what to search for in {name}" : "Upišite što tražite u {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/hr.json b/l10n/hr.json index 5abd69a6c..8ad434479 100644 --- a/l10n/hr.json +++ b/l10n/hr.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Abecedno", "Most used": "Najčešće korišteno", "Last used": "Posljednje korišteno", - "At random": "Nasumično" + "At random": "Nasumično", + "Search shortcuts": "Prečaci za pretraživanje", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Pretraživanje koje počinje prefiksom, npr. !t printer, ide izravno na tu stranicu u novoj kartici. Upisivanjem ? u okvir za pretraživanje prikazuju se prečaci.", + "Prefix": "Prefiks", + "Address with {query}": "Adresa s {query}", + "No search shortcuts yet.": "Još nema prečaca za pretraživanje.", + "Search shortcuts saved.": "Prečaci za pretraživanje su spremljeni.", + "Add shortcut": "Dodaj prečac", + "Save search shortcuts": "Spremi prečace za pretraživanje", + "The search shortcuts could not be loaded.": "Prečace za pretraživanje nije bilo moguće učitati.", + "The search shortcuts could not be saved.": "Prečace za pretraživanje nije bilo moguće spremiti.", + "Search {name} for {query}": "Pretraži {name} za {query}", + "Type what to search for in {name}": "Upišite što tražite u {name}" } } diff --git a/l10n/hu.js b/l10n/hu.js index 8f267be02..082ef33c8 100644 --- a/l10n/hu.js +++ b/l10n/hu.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Betűrendben", "Most used" : "Leggyakrabban használt", "Last used" : "Legutóbb használt", - "At random" : "Véletlenszerűen" + "At random" : "Véletlenszerűen", + "Search shortcuts" : "Keresési parancsikonok", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Az előtaggal kezdődő keresés, például !t printer, közvetlenül az adott oldalra visz egy új lapon. Ha ? jelet írsz a keresőmezőbe, megjelennek a parancsikonok.", + "Prefix" : "Előtag", + "Address with {query}" : "Cím {query} jelöléssel", + "No search shortcuts yet." : "Még nincsenek keresési parancsikonok.", + "Search shortcuts saved." : "Keresési parancsikonok mentve.", + "Add shortcut" : "Parancsikon hozzáadása", + "Save search shortcuts" : "Keresési parancsikonok mentése", + "The search shortcuts could not be loaded." : "A keresési parancsikonokat nem sikerült betölteni.", + "The search shortcuts could not be saved." : "A keresési parancsikonokat nem sikerült menteni.", + "Search {name} for {query}" : "Keresés itt: {name}, erre: {query}", + "Type what to search for in {name}" : "Írd be, mit keresel itt: {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/hu.json b/l10n/hu.json index 73771ec3f..53b1313a3 100644 --- a/l10n/hu.json +++ b/l10n/hu.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Betűrendben", "Most used": "Leggyakrabban használt", "Last used": "Legutóbb használt", - "At random": "Véletlenszerűen" + "At random": "Véletlenszerűen", + "Search shortcuts": "Keresési parancsikonok", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Az előtaggal kezdődő keresés, például !t printer, közvetlenül az adott oldalra visz egy új lapon. Ha ? jelet írsz a keresőmezőbe, megjelennek a parancsikonok.", + "Prefix": "Előtag", + "Address with {query}": "Cím {query} jelöléssel", + "No search shortcuts yet.": "Még nincsenek keresési parancsikonok.", + "Search shortcuts saved.": "Keresési parancsikonok mentve.", + "Add shortcut": "Parancsikon hozzáadása", + "Save search shortcuts": "Keresési parancsikonok mentése", + "The search shortcuts could not be loaded.": "A keresési parancsikonokat nem sikerült betölteni.", + "The search shortcuts could not be saved.": "A keresési parancsikonokat nem sikerült menteni.", + "Search {name} for {query}": "Keresés itt: {name}, erre: {query}", + "Type what to search for in {name}": "Írd be, mit keresel itt: {name}" } } diff --git a/l10n/is.js b/l10n/is.js index 90bb6f0a6..4f90573d0 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Í stafrófsröð", "Most used" : "Mest notað", "Last used" : "Síðast notað", - "At random" : "Af handahófi" + "At random" : "Af handahófi", + "Search shortcuts" : "Leitarflýtileiðir", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Leit sem byrjar á forskeyti, eins og !t printer, fer beint á þá síðu í nýjum flipa. Sláðu inn ? í leitarreit til að sjá flýtileiðirnar.", + "Prefix" : "Forskeyti", + "Address with {query}" : "Vefslóð með {query}", + "No search shortcuts yet." : "Engar leitarflýtileiðir enn.", + "Search shortcuts saved." : "Leitarflýtileiðir vistaðar.", + "Add shortcut" : "Bæta við flýtileið", + "Save search shortcuts" : "Vista leitarflýtileiðir", + "The search shortcuts could not be loaded." : "Ekki tókst að hlaða inn leitarflýtileiðum.", + "The search shortcuts could not be saved." : "Ekki tókst að vista leitarflýtileiðirnar.", + "Search {name} for {query}" : "Leita í {name} að {query}", + "Type what to search for in {name}" : "Sláðu inn hvað á að leita að í {name}" }, "nplurals=2; plural=(n%10!=1 || n%100==11);" ); diff --git a/l10n/is.json b/l10n/is.json index 3b278cd6e..ff9642953 100644 --- a/l10n/is.json +++ b/l10n/is.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Í stafrófsröð", "Most used": "Mest notað", "Last used": "Síðast notað", - "At random": "Af handahófi" + "At random": "Af handahófi", + "Search shortcuts": "Leitarflýtileiðir", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Leit sem byrjar á forskeyti, eins og !t printer, fer beint á þá síðu í nýjum flipa. Sláðu inn ? í leitarreit til að sjá flýtileiðirnar.", + "Prefix": "Forskeyti", + "Address with {query}": "Vefslóð með {query}", + "No search shortcuts yet.": "Engar leitarflýtileiðir enn.", + "Search shortcuts saved.": "Leitarflýtileiðir vistaðar.", + "Add shortcut": "Bæta við flýtileið", + "Save search shortcuts": "Vista leitarflýtileiðir", + "The search shortcuts could not be loaded.": "Ekki tókst að hlaða inn leitarflýtileiðum.", + "The search shortcuts could not be saved.": "Ekki tókst að vista leitarflýtileiðirnar.", + "Search {name} for {query}": "Leita í {name} að {query}", + "Type what to search for in {name}": "Sláðu inn hvað á að leita að í {name}" } } diff --git a/l10n/it.js b/l10n/it.js index 084ad08ea..29c3b4ec4 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "In ordine alfabetico", "Most used" : "Più usati", "Last used" : "Ultimo utilizzo", - "At random" : "A caso" + "At random" : "A caso", + "Search shortcuts" : "Scorciatoie di ricerca", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Una ricerca che inizia con un prefisso, come !t printer, va direttamente a quel sito in una nuova scheda. Digitando ? in una casella di ricerca si vedono le scorciatoie.", + "Prefix" : "Prefisso", + "Address with {query}" : "Indirizzo con {query}", + "No search shortcuts yet." : "Nessuna scorciatoia di ricerca per ora.", + "Search shortcuts saved." : "Scorciatoie di ricerca salvate.", + "Add shortcut" : "Aggiungi scorciatoia", + "Save search shortcuts" : "Salva scorciatoie di ricerca", + "The search shortcuts could not be loaded." : "Non è stato possibile caricare le scorciatoie di ricerca.", + "The search shortcuts could not be saved." : "Non è stato possibile salvare le scorciatoie di ricerca.", + "Search {name} for {query}" : "Cerca {query} in {name}", + "Type what to search for in {name}" : "Scrivi cosa cercare in {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/it.json b/l10n/it.json index 0bf896a8c..a2f5b95cd 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -1299,6 +1299,18 @@ "Alphabetically": "In ordine alfabetico", "Most used": "Più usati", "Last used": "Ultimo utilizzo", - "At random": "A caso" + "At random": "A caso", + "Search shortcuts": "Scorciatoie di ricerca", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Una ricerca che inizia con un prefisso, come !t printer, va direttamente a quel sito in una nuova scheda. Digitando ? in una casella di ricerca si vedono le scorciatoie.", + "Prefix": "Prefisso", + "Address with {query}": "Indirizzo con {query}", + "No search shortcuts yet.": "Nessuna scorciatoia di ricerca per ora.", + "Search shortcuts saved.": "Scorciatoie di ricerca salvate.", + "Add shortcut": "Aggiungi scorciatoia", + "Save search shortcuts": "Salva scorciatoie di ricerca", + "The search shortcuts could not be loaded.": "Non è stato possibile caricare le scorciatoie di ricerca.", + "The search shortcuts could not be saved.": "Non è stato possibile salvare le scorciatoie di ricerca.", + "Search {name} for {query}": "Cerca {query} in {name}", + "Type what to search for in {name}": "Scrivi cosa cercare in {name}" } } diff --git a/l10n/lb.js b/l10n/lb.js index 57ee7d502..5c290ec2d 100644 --- a/l10n/lb.js +++ b/l10n/lb.js @@ -1323,7 +1323,19 @@ OC.L10N.register( "Alphabetically" : "Alphabetesch", "Most used" : "Am meeschte benotzt", "Last used" : "Lescht benotzt", - "At random" : "Zoufälleg" + "At random" : "Zoufälleg", + "Search shortcuts" : "Sich-Ofkierzungen", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Eng Sich, déi mat engem Präfix ufänkt, wéi !t printer, geet direkt op dee Site an engem neien Tab. Mat ? am Sichfeld gesäis du d'Ofkierzungen.", + "Prefix" : "Präfix", + "Address with {query}" : "Adress mat {query}", + "No search shortcuts yet." : "Nach keng Sich-Ofkierzungen.", + "Search shortcuts saved." : "Sich-Ofkierzunge gespäichert.", + "Add shortcut" : "Ofkierzung derbäisetzen", + "Save search shortcuts" : "Sich-Ofkierzunge späicheren", + "The search shortcuts could not be loaded." : "D'Sich-Ofkierzunge konnten net geluede ginn.", + "The search shortcuts could not be saved." : "D'Sich-Ofkierzunge konnten net gespäichert ginn.", + "Search {name} for {query}" : "A {name} no {query} sichen", + "Type what to search for in {name}" : "Gëff an, wat an {name} gesicht gëtt" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/lb.json b/l10n/lb.json index 530df9f16..fc3260785 100644 --- a/l10n/lb.json +++ b/l10n/lb.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alphabetesch", "Most used": "Am meeschte benotzt", "Last used": "Lescht benotzt", - "At random": "Zoufälleg" + "At random": "Zoufälleg", + "Search shortcuts": "Sich-Ofkierzungen", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Eng Sich, déi mat engem Präfix ufänkt, wéi !t printer, geet direkt op dee Site an engem neien Tab. Mat ? am Sichfeld gesäis du d'Ofkierzungen.", + "Prefix": "Präfix", + "Address with {query}": "Adress mat {query}", + "No search shortcuts yet.": "Nach keng Sich-Ofkierzungen.", + "Search shortcuts saved.": "Sich-Ofkierzunge gespäichert.", + "Add shortcut": "Ofkierzung derbäisetzen", + "Save search shortcuts": "Sich-Ofkierzunge späicheren", + "The search shortcuts could not be loaded.": "D'Sich-Ofkierzunge konnten net geluede ginn.", + "The search shortcuts could not be saved.": "D'Sich-Ofkierzunge konnten net gespäichert ginn.", + "Search {name} for {query}": "A {name} no {query} sichen", + "Type what to search for in {name}": "Gëff an, wat an {name} gesicht gëtt" } } diff --git a/l10n/lt.js b/l10n/lt.js index b9f290070..6cae31274 100644 --- a/l10n/lt.js +++ b/l10n/lt.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Abėcėlės tvarka", "Most used" : "Dažniausiai naudojami", "Last used" : "Paskutiniai naudoti", - "At random" : "Atsitiktinai" + "At random" : "Atsitiktinai", + "Search shortcuts" : "Paieškos nuorodos", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Paieška, prasidedanti priešdėliu, pvz., !t printer, iškart atveria tą svetainę naujame skirtuke. Įvedę ? paieškos laukelyje pamatysite nuorodas.", + "Prefix" : "Priešdėlis", + "Address with {query}" : "Adresas su {query}", + "No search shortcuts yet." : "Paieškos nuorodų dar nėra.", + "Search shortcuts saved." : "Paieškos nuorodos išsaugotos.", + "Add shortcut" : "Pridėti nuorodą", + "Save search shortcuts" : "Išsaugoti paieškos nuorodas", + "The search shortcuts could not be loaded." : "Nepavyko įkelti paieškos nuorodų.", + "The search shortcuts could not be saved." : "Nepavyko išsaugoti paieškos nuorodų.", + "Search {name} for {query}" : "Ieškoti {name}: {query}", + "Type what to search for in {name}" : "Įveskite, ko ieškoti {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/lt.json b/l10n/lt.json index 6a77144f8..a74b6a426 100644 --- a/l10n/lt.json +++ b/l10n/lt.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Abėcėlės tvarka", "Most used": "Dažniausiai naudojami", "Last used": "Paskutiniai naudoti", - "At random": "Atsitiktinai" + "At random": "Atsitiktinai", + "Search shortcuts": "Paieškos nuorodos", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Paieška, prasidedanti priešdėliu, pvz., !t printer, iškart atveria tą svetainę naujame skirtuke. Įvedę ? paieškos laukelyje pamatysite nuorodas.", + "Prefix": "Priešdėlis", + "Address with {query}": "Adresas su {query}", + "No search shortcuts yet.": "Paieškos nuorodų dar nėra.", + "Search shortcuts saved.": "Paieškos nuorodos išsaugotos.", + "Add shortcut": "Pridėti nuorodą", + "Save search shortcuts": "Išsaugoti paieškos nuorodas", + "The search shortcuts could not be loaded.": "Nepavyko įkelti paieškos nuorodų.", + "The search shortcuts could not be saved.": "Nepavyko išsaugoti paieškos nuorodų.", + "Search {name} for {query}": "Ieškoti {name}: {query}", + "Type what to search for in {name}": "Įveskite, ko ieškoti {name}" } } diff --git a/l10n/lv.js b/l10n/lv.js index 375685874..f2fb40691 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabētiski", "Most used" : "Visbiežāk lietotie", "Last used" : "Pēdējie lietotie", - "At random" : "Nejauši" + "At random" : "Nejauši", + "Search shortcuts" : "Meklēšanas saīsnes", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Meklēšana, kas sākas ar prefiksu, piemēram, !t printer, uzreiz atver šo vietni jaunā cilnē. Ierakstot ? meklēšanas lodziņā, redzamas saīsnes.", + "Prefix" : "Prefikss", + "Address with {query}" : "Adrese ar {query}", + "No search shortcuts yet." : "Meklēšanas saīsņu vēl nav.", + "Search shortcuts saved." : "Meklēšanas saīsnes saglabātas.", + "Add shortcut" : "Pievienot saīsni", + "Save search shortcuts" : "Saglabāt meklēšanas saīsnes", + "The search shortcuts could not be loaded." : "Neizdevās ielādēt meklēšanas saīsnes.", + "The search shortcuts could not be saved." : "Neizdevās saglabāt meklēšanas saīsnes.", + "Search {name} for {query}" : "Meklēt {name}: {query}", + "Type what to search for in {name}" : "Ierakstiet, ko meklēt {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);" ); diff --git a/l10n/lv.json b/l10n/lv.json index ddc13ea1d..aa43ec05c 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Alfabētiski", "Most used": "Visbiežāk lietotie", "Last used": "Pēdējie lietotie", - "At random": "Nejauši" + "At random": "Nejauši", + "Search shortcuts": "Meklēšanas saīsnes", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Meklēšana, kas sākas ar prefiksu, piemēram, !t printer, uzreiz atver šo vietni jaunā cilnē. Ierakstot ? meklēšanas lodziņā, redzamas saīsnes.", + "Prefix": "Prefikss", + "Address with {query}": "Adrese ar {query}", + "No search shortcuts yet.": "Meklēšanas saīsņu vēl nav.", + "Search shortcuts saved.": "Meklēšanas saīsnes saglabātas.", + "Add shortcut": "Pievienot saīsni", + "Save search shortcuts": "Saglabāt meklēšanas saīsnes", + "The search shortcuts could not be loaded.": "Neizdevās ielādēt meklēšanas saīsnes.", + "The search shortcuts could not be saved.": "Neizdevās saglabāt meklēšanas saīsnes.", + "Search {name} for {query}": "Meklēt {name}: {query}", + "Type what to search for in {name}": "Ierakstiet, ko meklēt {name}" } } diff --git a/l10n/mk.js b/l10n/mk.js index 1309b932d..7362e3b7c 100644 --- a/l10n/mk.js +++ b/l10n/mk.js @@ -1323,7 +1323,19 @@ OC.L10N.register( "Alphabetically" : "Азбучно", "Most used" : "Најкористени", "Last used" : "Последно користени", - "At random" : "По случаен избор" + "At random" : "По случаен избор", + "Search shortcuts" : "Кратенки за пребарување", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Пребарување што почнува со префикс, на пр. !t printer, оди директно на таа страница во ново јазиче. Со внесување ? во полето за пребарување се прикажуваат кратенките.", + "Prefix" : "Префикс", + "Address with {query}" : "Адреса со {query}", + "No search shortcuts yet." : "Сè уште нема кратенки за пребарување.", + "Search shortcuts saved." : "Кратенките за пребарување се зачувани.", + "Add shortcut" : "Додај кратенка", + "Save search shortcuts" : "Зачувај кратенки за пребарување", + "The search shortcuts could not be loaded." : "Кратенките за пребарување не можеа да се вчитаат.", + "The search shortcuts could not be saved." : "Кратенките за пребарување не можеа да се зачуваат.", + "Search {name} for {query}" : "Пребарај во {name} за {query}", + "Type what to search for in {name}" : "Внесете што да се бара во {name}" }, "nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);" ); diff --git a/l10n/mk.json b/l10n/mk.json index ab48acee8..fb1125f6f 100644 --- a/l10n/mk.json +++ b/l10n/mk.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Азбучно", "Most used": "Најкористени", "Last used": "Последно користени", - "At random": "По случаен избор" + "At random": "По случаен избор", + "Search shortcuts": "Кратенки за пребарување", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Пребарување што почнува со префикс, на пр. !t printer, оди директно на таа страница во ново јазиче. Со внесување ? во полето за пребарување се прикажуваат кратенките.", + "Prefix": "Префикс", + "Address with {query}": "Адреса со {query}", + "No search shortcuts yet.": "Сè уште нема кратенки за пребарување.", + "Search shortcuts saved.": "Кратенките за пребарување се зачувани.", + "Add shortcut": "Додај кратенка", + "Save search shortcuts": "Зачувај кратенки за пребарување", + "The search shortcuts could not be loaded.": "Кратенките за пребарување не можеа да се вчитаат.", + "The search shortcuts could not be saved.": "Кратенките за пребарување не можеа да се зачуваат.", + "Search {name} for {query}": "Пребарај во {name} за {query}", + "Type what to search for in {name}": "Внесете што да се бара во {name}" } } diff --git a/l10n/mt.js b/l10n/mt.js index efeb4b39b..27d0a5338 100644 --- a/l10n/mt.js +++ b/l10n/mt.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetikament", "Most used" : "L-aktar użati", "Last used" : "L-aħħar użati", - "At random" : "B'mod każwali" + "At random" : "B'mod każwali", + "Search shortcuts" : "Shortcuts tat-tfittxija", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Tfittxija li tibda bi prefiss, bħal !t printer, tmur dritt għal dak is-sit f'tab ġdida. Ikteb ? f'kaxxa tat-tfittxija biex tara l-shortcuts.", + "Prefix" : "Prefiss", + "Address with {query}" : "Indirizz b'{query}", + "No search shortcuts yet." : "Għad m'hemmx shortcuts tat-tfittxija.", + "Search shortcuts saved." : "Il-shortcuts tat-tfittxija ġew salvati.", + "Add shortcut" : "Żid shortcut", + "Save search shortcuts" : "Issejvja l-shortcuts tat-tfittxija", + "The search shortcuts could not be loaded." : "Il-shortcuts tat-tfittxija ma setgħux jitgħabbew.", + "The search shortcuts could not be saved." : "Il-shortcuts tat-tfittxija ma setgħux jiġu salvati.", + "Search {name} for {query}" : "Fittex {name} għal {query}", + "Type what to search for in {name}" : "Ikteb x'tixtieq tfittex f'{name}" }, "nplurals=4; plural=(n==1 ? 0 : n==0 || (n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20) ? 2 : 3);" ); diff --git a/l10n/mt.json b/l10n/mt.json index 65e7e70ea..0b718764f 100644 --- a/l10n/mt.json +++ b/l10n/mt.json @@ -1301,6 +1301,18 @@ "Alphabetically": "Alfabetikament", "Most used": "L-aktar użati", "Last used": "L-aħħar użati", - "At random": "B'mod każwali" + "At random": "B'mod każwali", + "Search shortcuts": "Shortcuts tat-tfittxija", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Tfittxija li tibda bi prefiss, bħal !t printer, tmur dritt għal dak is-sit f'tab ġdida. Ikteb ? f'kaxxa tat-tfittxija biex tara l-shortcuts.", + "Prefix": "Prefiss", + "Address with {query}": "Indirizz b'{query}", + "No search shortcuts yet.": "Għad m'hemmx shortcuts tat-tfittxija.", + "Search shortcuts saved.": "Il-shortcuts tat-tfittxija ġew salvati.", + "Add shortcut": "Żid shortcut", + "Save search shortcuts": "Issejvja l-shortcuts tat-tfittxija", + "The search shortcuts could not be loaded.": "Il-shortcuts tat-tfittxija ma setgħux jitgħabbew.", + "The search shortcuts could not be saved.": "Il-shortcuts tat-tfittxija ma setgħux jiġu salvati.", + "Search {name} for {query}": "Fittex {name} għal {query}", + "Type what to search for in {name}": "Ikteb x'tixtieq tfittex f'{name}" } } diff --git a/l10n/nb.js b/l10n/nb.js index cadeff1a3..99e2066b9 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetisk", "Most used" : "Mest brukt", "Last used" : "Sist brukt", - "At random" : "Tilfeldig" + "At random" : "Tilfeldig", + "Search shortcuts" : "Søkesnarveier", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Et søk som starter med et prefiks, for eksempel !t printer, går rett til det nettstedet i en ny fane. Skriv ? i et søkefelt for å se snarveiene.", + "Prefix" : "Prefiks", + "Address with {query}" : "Adresse med {query}", + "No search shortcuts yet." : "Ingen søkesnarveier ennå.", + "Search shortcuts saved." : "Søkesnarveiene er lagret.", + "Add shortcut" : "Legg til snarvei", + "Save search shortcuts" : "Lagre søkesnarveier", + "The search shortcuts could not be loaded." : "Søkesnarveiene kunne ikke lastes inn.", + "The search shortcuts could not be saved." : "Søkesnarveiene kunne ikke lagres.", + "Search {name} for {query}" : "Søk i {name} etter {query}", + "Type what to search for in {name}" : "Skriv hva du vil søke etter i {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nb.json b/l10n/nb.json index 8059022ed..e21520921 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabetisk", "Most used": "Mest brukt", "Last used": "Sist brukt", - "At random": "Tilfeldig" + "At random": "Tilfeldig", + "Search shortcuts": "Søkesnarveier", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Et søk som starter med et prefiks, for eksempel !t printer, går rett til det nettstedet i en ny fane. Skriv ? i et søkefelt for å se snarveiene.", + "Prefix": "Prefiks", + "Address with {query}": "Adresse med {query}", + "No search shortcuts yet.": "Ingen søkesnarveier ennå.", + "Search shortcuts saved.": "Søkesnarveiene er lagret.", + "Add shortcut": "Legg til snarvei", + "Save search shortcuts": "Lagre søkesnarveier", + "The search shortcuts could not be loaded.": "Søkesnarveiene kunne ikke lastes inn.", + "The search shortcuts could not be saved.": "Søkesnarveiene kunne ikke lagres.", + "Search {name} for {query}": "Søk i {name} etter {query}", + "Type what to search for in {name}": "Skriv hva du vil søke etter i {name}" } } diff --git a/l10n/nl.js b/l10n/nl.js index 8648eb478..106de86b2 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -1382,7 +1382,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetisch", "Most used" : "Meest gebruikt", "Last used" : "Laatst gebruikt", - "At random" : "Willekeurig" + "At random" : "Willekeurig", + "Search shortcuts" : "Zoeksnelkoppelingen", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Een zoekopdracht die begint met een voorvoegsel, zoals !t printer, gaat meteen naar die site in een nieuw tabblad. Typ ? in een zoekvak om de snelkoppelingen te zien.", + "Prefix" : "Voorvoegsel", + "Address with {query}" : "Adres met {query}", + "No search shortcuts yet." : "Nog geen zoeksnelkoppelingen.", + "Search shortcuts saved." : "Zoeksnelkoppelingen opgeslagen.", + "Add shortcut" : "Snelkoppeling toevoegen", + "Save search shortcuts" : "Zoeksnelkoppelingen opslaan", + "The search shortcuts could not be loaded." : "De zoeksnelkoppelingen konden niet worden geladen.", + "The search shortcuts could not be saved." : "De zoeksnelkoppelingen konden niet worden opgeslagen.", + "Search {name} for {query}" : "Zoek in {name} naar {query}", + "Type what to search for in {name}" : "Typ waarnaar je in {name} wilt zoeken" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/nl.json b/l10n/nl.json index e774d1823..151627e67 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -1359,6 +1359,18 @@ "Alphabetically": "Alfabetisch", "Most used": "Meest gebruikt", "Last used": "Laatst gebruikt", - "At random": "Willekeurig" + "At random": "Willekeurig", + "Search shortcuts": "Zoeksnelkoppelingen", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Een zoekopdracht die begint met een voorvoegsel, zoals !t printer, gaat meteen naar die site in een nieuw tabblad. Typ ? in een zoekvak om de snelkoppelingen te zien.", + "Prefix": "Voorvoegsel", + "Address with {query}": "Adres met {query}", + "No search shortcuts yet.": "Nog geen zoeksnelkoppelingen.", + "Search shortcuts saved.": "Zoeksnelkoppelingen opgeslagen.", + "Add shortcut": "Snelkoppeling toevoegen", + "Save search shortcuts": "Zoeksnelkoppelingen opslaan", + "The search shortcuts could not be loaded.": "De zoeksnelkoppelingen konden niet worden geladen.", + "The search shortcuts could not be saved.": "De zoeksnelkoppelingen konden niet worden opgeslagen.", + "Search {name} for {query}": "Zoek in {name} naar {query}", + "Type what to search for in {name}": "Typ waarnaar je in {name} wilt zoeken" } } diff --git a/l10n/pl.js b/l10n/pl.js index 285dce17a..e92140d87 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetycznie", "Most used" : "Najczęściej używane", "Last used" : "Ostatnio używane", - "At random" : "Losowo" + "At random" : "Losowo", + "Search shortcuts" : "Skróty wyszukiwania", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Wyszukiwanie zaczynające się od prefiksu, np. !t printer, przechodzi od razu do tej witryny w nowej karcie. Wpisanie ? w polu wyszukiwania pokazuje listę skrótów.", + "Prefix" : "Prefiks", + "Address with {query}" : "Adres z {query}", + "No search shortcuts yet." : "Brak skrótów wyszukiwania.", + "Search shortcuts saved." : "Skróty wyszukiwania zapisane.", + "Add shortcut" : "Dodaj skrót", + "Save search shortcuts" : "Zapisz skróty wyszukiwania", + "The search shortcuts could not be loaded." : "Nie udało się wczytać skrótów wyszukiwania.", + "The search shortcuts could not be saved." : "Nie udało się zapisać skrótów wyszukiwania.", + "Search {name} for {query}" : "Szukaj w {name}: {query}", + "Type what to search for in {name}" : "Wpisz, czego szukać w {name}" }, "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/pl.json b/l10n/pl.json index e82900981..9c36922cd 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Alfabetycznie", "Most used": "Najczęściej używane", "Last used": "Ostatnio używane", - "At random": "Losowo" + "At random": "Losowo", + "Search shortcuts": "Skróty wyszukiwania", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Wyszukiwanie zaczynające się od prefiksu, np. !t printer, przechodzi od razu do tej witryny w nowej karcie. Wpisanie ? w polu wyszukiwania pokazuje listę skrótów.", + "Prefix": "Prefiks", + "Address with {query}": "Adres z {query}", + "No search shortcuts yet.": "Brak skrótów wyszukiwania.", + "Search shortcuts saved.": "Skróty wyszukiwania zapisane.", + "Add shortcut": "Dodaj skrót", + "Save search shortcuts": "Zapisz skróty wyszukiwania", + "The search shortcuts could not be loaded.": "Nie udało się wczytać skrótów wyszukiwania.", + "The search shortcuts could not be saved.": "Nie udało się zapisać skrótów wyszukiwania.", + "Search {name} for {query}": "Szukaj w {name}: {query}", + "Type what to search for in {name}": "Wpisz, czego szukać w {name}" } } diff --git a/l10n/pt.js b/l10n/pt.js index e515e3e67..015fafad2 100644 --- a/l10n/pt.js +++ b/l10n/pt.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabeticamente", "Most used" : "Mais usados", "Last used" : "Último uso", - "At random" : "Aleatoriamente" + "At random" : "Aleatoriamente", + "Search shortcuts" : "Atalhos de pesquisa", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Uma pesquisa que começa com um prefixo, como !t printer, vai diretamente para esse site num novo separador. Escrever ? numa caixa de pesquisa mostra os atalhos.", + "Prefix" : "Prefixo", + "Address with {query}" : "Endereço com {query}", + "No search shortcuts yet." : "Ainda não há atalhos de pesquisa.", + "Search shortcuts saved." : "Atalhos de pesquisa guardados.", + "Add shortcut" : "Adicionar atalho", + "Save search shortcuts" : "Guardar atalhos de pesquisa", + "The search shortcuts could not be loaded." : "Não foi possível carregar os atalhos de pesquisa.", + "The search shortcuts could not be saved." : "Não foi possível guardar os atalhos de pesquisa.", + "Search {name} for {query}" : "Pesquisar {query} em {name}", + "Type what to search for in {name}" : "Escreva o que pesquisar em {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/pt.json b/l10n/pt.json index 90c186688..8d6217dfd 100644 --- a/l10n/pt.json +++ b/l10n/pt.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabeticamente", "Most used": "Mais usados", "Last used": "Último uso", - "At random": "Aleatoriamente" + "At random": "Aleatoriamente", + "Search shortcuts": "Atalhos de pesquisa", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Uma pesquisa que começa com um prefixo, como !t printer, vai diretamente para esse site num novo separador. Escrever ? numa caixa de pesquisa mostra os atalhos.", + "Prefix": "Prefixo", + "Address with {query}": "Endereço com {query}", + "No search shortcuts yet.": "Ainda não há atalhos de pesquisa.", + "Search shortcuts saved.": "Atalhos de pesquisa guardados.", + "Add shortcut": "Adicionar atalho", + "Save search shortcuts": "Guardar atalhos de pesquisa", + "The search shortcuts could not be loaded.": "Não foi possível carregar os atalhos de pesquisa.", + "The search shortcuts could not be saved.": "Não foi possível guardar os atalhos de pesquisa.", + "Search {name} for {query}": "Pesquisar {query} em {name}", + "Type what to search for in {name}": "Escreva o que pesquisar em {name}" } } diff --git a/l10n/rm.js b/l10n/rm.js index 9453ed16a..f77db960b 100644 --- a/l10n/rm.js +++ b/l10n/rm.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabeticamain", "Most used" : "Il pli savens duvrà", "Last used" : "Duvrà l'ultim", - "At random" : "A chaschun" + "At random" : "A chaschun", + "Search shortcuts" : "Scursanidas da tschertga", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Ina tschertga che cumenza cun in prefix, sco !t printer, va directamain a quella pagina en in nov tab. Cun ? en in champ da tschertga vesas las scursanidas.", + "Prefix" : "Prefix", + "Address with {query}" : "Adressa cun {query}", + "No search shortcuts yet." : "Anc naginas scursanidas da tschertga.", + "Search shortcuts saved." : "Scursanidas da tschertga memorisadas.", + "Add shortcut" : "Agiuntar ina scursanida", + "Save search shortcuts" : "Memorisar las scursanidas da tschertga", + "The search shortcuts could not be loaded." : "I n'è betg reussì da chargiar las scursanidas da tschertga.", + "The search shortcuts could not be saved." : "I n'è betg reussì da memorisar las scursanidas da tschertga.", + "Search {name} for {query}" : "Tschertgar en {name} suenter {query}", + "Type what to search for in {name}" : "Endatescha tge tschertgar en {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/rm.json b/l10n/rm.json index 566dd6a70..2b6876037 100644 --- a/l10n/rm.json +++ b/l10n/rm.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabeticamain", "Most used": "Il pli savens duvrà", "Last used": "Duvrà l'ultim", - "At random": "A chaschun" + "At random": "A chaschun", + "Search shortcuts": "Scursanidas da tschertga", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Ina tschertga che cumenza cun in prefix, sco !t printer, va directamain a quella pagina en in nov tab. Cun ? en in champ da tschertga vesas las scursanidas.", + "Prefix": "Prefix", + "Address with {query}": "Adressa cun {query}", + "No search shortcuts yet.": "Anc naginas scursanidas da tschertga.", + "Search shortcuts saved.": "Scursanidas da tschertga memorisadas.", + "Add shortcut": "Agiuntar ina scursanida", + "Save search shortcuts": "Memorisar las scursanidas da tschertga", + "The search shortcuts could not be loaded.": "I n'è betg reussì da chargiar las scursanidas da tschertga.", + "The search shortcuts could not be saved.": "I n'è betg reussì da memorisar las scursanidas da tschertga.", + "Search {name} for {query}": "Tschertgar en {name} suenter {query}", + "Type what to search for in {name}": "Endatescha tge tschertgar en {name}" } } diff --git a/l10n/ro.js b/l10n/ro.js index 358d66e1a..592be2a0f 100644 --- a/l10n/ro.js +++ b/l10n/ro.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetic", "Most used" : "Cele mai folosite", "Last used" : "Ultima utilizare", - "At random" : "Aleatoriu" + "At random" : "Aleatoriu", + "Search shortcuts" : "Scurtături de căutare", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "O căutare care începe cu un prefix, de exemplu !t printer, merge direct pe acel site într-o filă nouă. Tastând ? într-o casetă de căutare vezi scurtăturile.", + "Prefix" : "Prefix", + "Address with {query}" : "Adresă cu {query}", + "No search shortcuts yet." : "Încă nu există scurtături de căutare.", + "Search shortcuts saved." : "Scurtăturile de căutare au fost salvate.", + "Add shortcut" : "Adaugă scurtătură", + "Save search shortcuts" : "Salvează scurtăturile de căutare", + "The search shortcuts could not be loaded." : "Scurtăturile de căutare nu au putut fi încărcate.", + "The search shortcuts could not be saved." : "Scurtăturile de căutare nu au putut fi salvate.", + "Search {name} for {query}" : "Caută în {name} după {query}", + "Type what to search for in {name}" : "Scrie ce să cauți în {name}" }, "nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);" ); diff --git a/l10n/ro.json b/l10n/ro.json index 4ce56bcf0..1df78b4ee 100644 --- a/l10n/ro.json +++ b/l10n/ro.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Alfabetic", "Most used": "Cele mai folosite", "Last used": "Ultima utilizare", - "At random": "Aleatoriu" + "At random": "Aleatoriu", + "Search shortcuts": "Scurtături de căutare", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "O căutare care începe cu un prefix, de exemplu !t printer, merge direct pe acel site într-o filă nouă. Tastând ? într-o casetă de căutare vezi scurtăturile.", + "Prefix": "Prefix", + "Address with {query}": "Adresă cu {query}", + "No search shortcuts yet.": "Încă nu există scurtături de căutare.", + "Search shortcuts saved.": "Scurtăturile de căutare au fost salvate.", + "Add shortcut": "Adaugă scurtătură", + "Save search shortcuts": "Salvează scurtăturile de căutare", + "The search shortcuts could not be loaded.": "Scurtăturile de căutare nu au putut fi încărcate.", + "The search shortcuts could not be saved.": "Scurtăturile de căutare nu au putut fi salvate.", + "Search {name} for {query}": "Caută în {name} după {query}", + "Type what to search for in {name}": "Scrie ce să cauți în {name}" } } diff --git a/l10n/ru.js b/l10n/ru.js index 215c42ce6..114a48973 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "По алфавиту", "Most used" : "Самые используемые", "Last used" : "Последние использованные", - "At random" : "Случайно" + "At random" : "Случайно", + "Search shortcuts" : "Ярлыки поиска", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Поиск, начинающийся с префикса, например !t printer, сразу открывает этот сайт в новой вкладке. Введите ? в поле поиска, чтобы увидеть ярлыки.", + "Prefix" : "Префикс", + "Address with {query}" : "Адрес с {query}", + "No search shortcuts yet." : "Ярлыков поиска пока нет.", + "Search shortcuts saved." : "Ярлыки поиска сохранены.", + "Add shortcut" : "Добавить ярлык", + "Save search shortcuts" : "Сохранить ярлыки поиска", + "The search shortcuts could not be loaded." : "Не удалось загрузить ярлыки поиска.", + "The search shortcuts could not be saved." : "Не удалось сохранить ярлыки поиска.", + "Search {name} for {query}" : "Искать в {name}: {query}", + "Type what to search for in {name}" : "Введите, что искать в {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/ru.json b/l10n/ru.json index 752062dab..fd37f3fdd 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -1300,6 +1300,18 @@ "Alphabetically": "По алфавиту", "Most used": "Самые используемые", "Last used": "Последние использованные", - "At random": "Случайно" + "At random": "Случайно", + "Search shortcuts": "Ярлыки поиска", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Поиск, начинающийся с префикса, например !t printer, сразу открывает этот сайт в новой вкладке. Введите ? в поле поиска, чтобы увидеть ярлыки.", + "Prefix": "Префикс", + "Address with {query}": "Адрес с {query}", + "No search shortcuts yet.": "Ярлыков поиска пока нет.", + "Search shortcuts saved.": "Ярлыки поиска сохранены.", + "Add shortcut": "Добавить ярлык", + "Save search shortcuts": "Сохранить ярлыки поиска", + "The search shortcuts could not be loaded.": "Не удалось загрузить ярлыки поиска.", + "The search shortcuts could not be saved.": "Не удалось сохранить ярлыки поиска.", + "Search {name} for {query}": "Искать в {name}: {query}", + "Type what to search for in {name}": "Введите, что искать в {name}" } } diff --git a/l10n/sk.js b/l10n/sk.js index dc9930564..abf4fb4c6 100644 --- a/l10n/sk.js +++ b/l10n/sk.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Abecedne", "Most used" : "Najpoužívanejšie", "Last used" : "Naposledy použité", - "At random" : "Náhodne" + "At random" : "Náhodne", + "Search shortcuts" : "Skratky vyhľadávania", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Vyhľadávanie, ktoré začína predponou, napríklad !t printer, prejde priamo na danú stránku v novej karte. Napísaním ? do vyhľadávacieho poľa zobrazíte skratky.", + "Prefix" : "Predpona", + "Address with {query}" : "Adresa s {query}", + "No search shortcuts yet." : "Zatiaľ žiadne skratky vyhľadávania.", + "Search shortcuts saved." : "Skratky vyhľadávania sú uložené.", + "Add shortcut" : "Pridať skratku", + "Save search shortcuts" : "Uložiť skratky vyhľadávania", + "The search shortcuts could not be loaded." : "Skratky vyhľadávania sa nepodarilo načítať.", + "The search shortcuts could not be saved." : "Skratky vyhľadávania sa nepodarilo uložiť.", + "Search {name} for {query}" : "Hľadať v {name}: {query}", + "Type what to search for in {name}" : "Napíšte, čo hľadať v {name}" }, "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" ); diff --git a/l10n/sk.json b/l10n/sk.json index f4ad5b771..d641da3ee 100644 --- a/l10n/sk.json +++ b/l10n/sk.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Abecedne", "Most used": "Najpoužívanejšie", "Last used": "Naposledy použité", - "At random": "Náhodne" + "At random": "Náhodne", + "Search shortcuts": "Skratky vyhľadávania", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Vyhľadávanie, ktoré začína predponou, napríklad !t printer, prejde priamo na danú stránku v novej karte. Napísaním ? do vyhľadávacieho poľa zobrazíte skratky.", + "Prefix": "Predpona", + "Address with {query}": "Adresa s {query}", + "No search shortcuts yet.": "Zatiaľ žiadne skratky vyhľadávania.", + "Search shortcuts saved.": "Skratky vyhľadávania sú uložené.", + "Add shortcut": "Pridať skratku", + "Save search shortcuts": "Uložiť skratky vyhľadávania", + "The search shortcuts could not be loaded.": "Skratky vyhľadávania sa nepodarilo načítať.", + "The search shortcuts could not be saved.": "Skratky vyhľadávania sa nepodarilo uložiť.", + "Search {name} for {query}": "Hľadať v {name}: {query}", + "Type what to search for in {name}": "Napíšte, čo hľadať v {name}" } } diff --git a/l10n/sl.js b/l10n/sl.js index aae665454..7b5c86ecd 100644 --- a/l10n/sl.js +++ b/l10n/sl.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Po abecedi", "Most used" : "Najpogosteje uporabljeno", "Last used" : "Nazadnje uporabljeno", - "At random" : "Naključno" + "At random" : "Naključno", + "Search shortcuts" : "Bližnjice iskanja", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Iskanje, ki se začne s predpono, na primer !t printer, gre neposredno na to spletno mesto v novem zavihku. Če v iskalno polje vnesete ?, se prikažejo bližnjice.", + "Prefix" : "Predpona", + "Address with {query}" : "Naslov z {query}", + "No search shortcuts yet." : "Bližnjic iskanja še ni.", + "Search shortcuts saved." : "Bližnjice iskanja so shranjene.", + "Add shortcut" : "Dodaj bližnjico", + "Save search shortcuts" : "Shrani bližnjice iskanja", + "The search shortcuts could not be loaded." : "Bližnjic iskanja ni bilo mogoče naložiti.", + "The search shortcuts could not be saved." : "Bližnjic iskanja ni bilo mogoče shraniti.", + "Search {name} for {query}" : "Išči v {name}: {query}", + "Type what to search for in {name}" : "Vnesite, kaj iskati v {name}" }, "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);" ); diff --git a/l10n/sl.json b/l10n/sl.json index a46e1bfe9..939bb84b4 100644 --- a/l10n/sl.json +++ b/l10n/sl.json @@ -1301,6 +1301,18 @@ "Alphabetically": "Po abecedi", "Most used": "Najpogosteje uporabljeno", "Last used": "Nazadnje uporabljeno", - "At random": "Naključno" + "At random": "Naključno", + "Search shortcuts": "Bližnjice iskanja", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Iskanje, ki se začne s predpono, na primer !t printer, gre neposredno na to spletno mesto v novem zavihku. Če v iskalno polje vnesete ?, se prikažejo bližnjice.", + "Prefix": "Predpona", + "Address with {query}": "Naslov z {query}", + "No search shortcuts yet.": "Bližnjic iskanja še ni.", + "Search shortcuts saved.": "Bližnjice iskanja so shranjene.", + "Add shortcut": "Dodaj bližnjico", + "Save search shortcuts": "Shrani bližnjice iskanja", + "The search shortcuts could not be loaded.": "Bližnjic iskanja ni bilo mogoče naložiti.", + "The search shortcuts could not be saved.": "Bližnjic iskanja ni bilo mogoče shraniti.", + "Search {name} for {query}": "Išči v {name}: {query}", + "Type what to search for in {name}": "Vnesite, kaj iskati v {name}" } } diff --git a/l10n/sq.js b/l10n/sq.js index 0e6961f3d..0bd833a22 100644 --- a/l10n/sq.js +++ b/l10n/sq.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Sipas alfabetit", "Most used" : "Më të përdorurat", "Last used" : "Përdorur së fundi", - "At random" : "Rastësisht" + "At random" : "Rastësisht", + "Search shortcuts" : "Shkurtore kërkimi", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Një kërkim që nis me një parashtesë, si !t printer, shkon drejt e në atë faqe në një skedë të re. Duke shkruar ? në kutinë e kërkimit shfaqen shkurtoret.", + "Prefix" : "Parashtesë", + "Address with {query}" : "Adresë me {query}", + "No search shortcuts yet." : "Ende nuk ka shkurtore kërkimi.", + "Search shortcuts saved." : "Shkurtoret e kërkimit u ruajtën.", + "Add shortcut" : "Shto shkurtore", + "Save search shortcuts" : "Ruaj shkurtoret e kërkimit", + "The search shortcuts could not be loaded." : "Shkurtoret e kërkimit nuk u ngarkuan.", + "The search shortcuts could not be saved." : "Shkurtoret e kërkimit nuk u ruajtën.", + "Search {name} for {query}" : "Kërko në {name} për {query}", + "Type what to search for in {name}" : "Shkruaj çfarë të kërkosh në {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sq.json b/l10n/sq.json index 558053019..5ac4b139f 100644 --- a/l10n/sq.json +++ b/l10n/sq.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Sipas alfabetit", "Most used": "Më të përdorurat", "Last used": "Përdorur së fundi", - "At random": "Rastësisht" + "At random": "Rastësisht", + "Search shortcuts": "Shkurtore kërkimi", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Një kërkim që nis me një parashtesë, si !t printer, shkon drejt e në atë faqe në një skedë të re. Duke shkruar ? në kutinë e kërkimit shfaqen shkurtoret.", + "Prefix": "Parashtesë", + "Address with {query}": "Adresë me {query}", + "No search shortcuts yet.": "Ende nuk ka shkurtore kërkimi.", + "Search shortcuts saved.": "Shkurtoret e kërkimit u ruajtën.", + "Add shortcut": "Shto shkurtore", + "Save search shortcuts": "Ruaj shkurtoret e kërkimit", + "The search shortcuts could not be loaded.": "Shkurtoret e kërkimit nuk u ngarkuan.", + "The search shortcuts could not be saved.": "Shkurtoret e kërkimit nuk u ruajtën.", + "Search {name} for {query}": "Kërko në {name} për {query}", + "Type what to search for in {name}": "Shkruaj çfarë të kërkosh në {name}" } } diff --git a/l10n/sr.js b/l10n/sr.js index d8bca7a02..7a730dee2 100644 --- a/l10n/sr.js +++ b/l10n/sr.js @@ -1323,6 +1323,18 @@ OC.L10N.register( "Alphabetically" : "Абецедно", "Most used" : "Најкоришћеније", "Last used" : "Последње коришћено", - "At random" : "Насумично" + "At random" : "Насумично", + "Search shortcuts" : "Пречице за претрагу", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Претрага која почиње префиксом, нпр. !t printer, иде директно на ту страницу у новој картици. Уносом ? у поље за претрагу приказују се пречице.", + "Prefix" : "Префикс", + "Address with {query}" : "Адреса са {query}", + "No search shortcuts yet." : "Још нема пречица за претрагу.", + "Search shortcuts saved." : "Пречице за претрагу су сачуване.", + "Add shortcut" : "Додај пречицу", + "Save search shortcuts" : "Сачувај пречице за претрагу", + "The search shortcuts could not be loaded." : "Пречице за претрагу нису могле да се учитају.", + "The search shortcuts could not be saved." : "Пречице за претрагу нису могле да се сачувају.", + "Search {name} for {query}" : "Претражи {name} за {query}", + "Type what to search for in {name}" : "Унесите шта тражите у {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); diff --git a/l10n/sr.json b/l10n/sr.json index 9fc9ef3b4..b2c919ccb 100644 --- a/l10n/sr.json +++ b/l10n/sr.json @@ -1300,6 +1300,18 @@ "Alphabetically": "Абецедно", "Most used": "Најкоришћеније", "Last used": "Последње коришћено", - "At random": "Насумично" + "At random": "Насумично", + "Search shortcuts": "Пречице за претрагу", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Претрага која почиње префиксом, нпр. !t printer, иде директно на ту страницу у новој картици. Уносом ? у поље за претрагу приказују се пречице.", + "Prefix": "Префикс", + "Address with {query}": "Адреса са {query}", + "No search shortcuts yet.": "Још нема пречица за претрагу.", + "Search shortcuts saved.": "Пречице за претрагу су сачуване.", + "Add shortcut": "Додај пречицу", + "Save search shortcuts": "Сачувај пречице за претрагу", + "The search shortcuts could not be loaded.": "Пречице за претрагу нису могле да се учитају.", + "The search shortcuts could not be saved.": "Пречице за претрагу нису могле да се сачувају.", + "Search {name} for {query}": "Претражи {name} за {query}", + "Type what to search for in {name}": "Унесите шта тражите у {name}" } } diff --git a/l10n/sv.js b/l10n/sv.js index 9f2c290bf..c6779b6f7 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "I bokstavsordning", "Most used" : "Mest använda", "Last used" : "Senast använda", - "At random" : "Slumpmässigt" + "At random" : "Slumpmässigt", + "Search shortcuts" : "Sökgenvägar", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "En sökning som börjar med ett prefix, till exempel !t printer, går direkt till den webbplatsen i en ny flik. Skriv ? i en sökruta för att se genvägarna.", + "Prefix" : "Prefix", + "Address with {query}" : "Adress med {query}", + "No search shortcuts yet." : "Inga sökgenvägar ännu.", + "Search shortcuts saved." : "Sökgenvägarna har sparats.", + "Add shortcut" : "Lägg till genväg", + "Save search shortcuts" : "Spara sökgenvägar", + "The search shortcuts could not be loaded." : "Sökgenvägarna kunde inte läsas in.", + "The search shortcuts could not be saved." : "Sökgenvägarna kunde inte sparas.", + "Search {name} for {query}" : "Sök i {name} efter {query}", + "Type what to search for in {name}" : "Skriv vad du vill söka efter i {name}" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/sv.json b/l10n/sv.json index 3a86552d0..84408ac33 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -1299,6 +1299,18 @@ "Alphabetically": "I bokstavsordning", "Most used": "Mest använda", "Last used": "Senast använda", - "At random": "Slumpmässigt" + "At random": "Slumpmässigt", + "Search shortcuts": "Sökgenvägar", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "En sökning som börjar med ett prefix, till exempel !t printer, går direkt till den webbplatsen i en ny flik. Skriv ? i en sökruta för att se genvägarna.", + "Prefix": "Prefix", + "Address with {query}": "Adress med {query}", + "No search shortcuts yet.": "Inga sökgenvägar ännu.", + "Search shortcuts saved.": "Sökgenvägarna har sparats.", + "Add shortcut": "Lägg till genväg", + "Save search shortcuts": "Spara sökgenvägar", + "The search shortcuts could not be loaded.": "Sökgenvägarna kunde inte läsas in.", + "The search shortcuts could not be saved.": "Sökgenvägarna kunde inte sparas.", + "Search {name} for {query}": "Sök i {name} efter {query}", + "Type what to search for in {name}": "Skriv vad du vill söka efter i {name}" } } diff --git a/l10n/tr.js b/l10n/tr.js index 3089b26ee..a6c2bc1c4 100644 --- a/l10n/tr.js +++ b/l10n/tr.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "Alfabetik", "Most used" : "En çok kullanılan", "Last used" : "Son kullanılan", - "At random" : "Rastgele" + "At random" : "Rastgele", + "Search shortcuts" : "Arama kısayolları", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "!t printer gibi bir önekle başlayan arama, doğrudan o siteye yeni bir sekmede gider. Arama kutusuna ? yazınca kısayollar listelenir.", + "Prefix" : "Önek", + "Address with {query}" : "{query} içeren adres", + "No search shortcuts yet." : "Henüz arama kısayolu yok.", + "Search shortcuts saved." : "Arama kısayolları kaydedildi.", + "Add shortcut" : "Kısayol ekle", + "Save search shortcuts" : "Arama kısayollarını kaydet", + "The search shortcuts could not be loaded." : "Arama kısayolları yüklenemedi.", + "The search shortcuts could not be saved." : "Arama kısayolları kaydedilemedi.", + "Search {name} for {query}" : "{name} içinde {query} ara", + "Type what to search for in {name}" : "{name} içinde ne aranacağını yaz" }, "nplurals=2; plural=(n != 1);" ); diff --git a/l10n/tr.json b/l10n/tr.json index 155e964f0..b402ba896 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -1299,6 +1299,18 @@ "Alphabetically": "Alfabetik", "Most used": "En çok kullanılan", "Last used": "Son kullanılan", - "At random": "Rastgele" + "At random": "Rastgele", + "Search shortcuts": "Arama kısayolları", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "!t printer gibi bir önekle başlayan arama, doğrudan o siteye yeni bir sekmede gider. Arama kutusuna ? yazınca kısayollar listelenir.", + "Prefix": "Önek", + "Address with {query}": "{query} içeren adres", + "No search shortcuts yet.": "Henüz arama kısayolu yok.", + "Search shortcuts saved.": "Arama kısayolları kaydedildi.", + "Add shortcut": "Kısayol ekle", + "Save search shortcuts": "Arama kısayollarını kaydet", + "The search shortcuts could not be loaded.": "Arama kısayolları yüklenemedi.", + "The search shortcuts could not be saved.": "Arama kısayolları kaydedilemedi.", + "Search {name} for {query}": "{name} içinde {query} ara", + "Type what to search for in {name}": "{name} içinde ne aranacağını yaz" } } diff --git a/l10n/uk.js b/l10n/uk.js index be5b75f80..ffc923a4a 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -1332,7 +1332,19 @@ OC.L10N.register( "Alphabetically" : "За абеткою", "Most used" : "Найчастіше використовувані", "Last used" : "Останні використані", - "At random" : "Випадково" + "At random" : "Випадково", + "Search shortcuts" : "Ярлики пошуку", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts." : "Пошук, що починається з префікса, наприклад !t printer, одразу відкриває цей сайт у новій вкладці. Введіть ? у полі пошуку, щоб побачити ярлики.", + "Prefix" : "Префікс", + "Address with {query}" : "Адреса з {query}", + "No search shortcuts yet." : "Ярликів пошуку ще немає.", + "Search shortcuts saved." : "Ярлики пошуку збережено.", + "Add shortcut" : "Додати ярлик", + "Save search shortcuts" : "Зберегти ярлики пошуку", + "The search shortcuts could not be loaded." : "Не вдалося завантажити ярлики пошуку.", + "The search shortcuts could not be saved." : "Не вдалося зберегти ярлики пошуку.", + "Search {name} for {query}" : "Шукати в {name}: {query}", + "Type what to search for in {name}" : "Введіть, що шукати в {name}" }, "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" ); diff --git a/l10n/uk.json b/l10n/uk.json index 5696c7113..ad873f69a 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -1300,6 +1300,18 @@ "Alphabetically": "За абеткою", "Most used": "Найчастіше використовувані", "Last used": "Останні використані", - "At random": "Випадково" + "At random": "Випадково", + "Search shortcuts": "Ярлики пошуку", + "A search that starts with a prefix, such as !t printer, goes straight to that site in a new tab. Typing ? in a search box lists the shortcuts.": "Пошук, що починається з префікса, наприклад !t printer, одразу відкриває цей сайт у новій вкладці. Введіть ? у полі пошуку, щоб побачити ярлики.", + "Prefix": "Префікс", + "Address with {query}": "Адреса з {query}", + "No search shortcuts yet.": "Ярликів пошуку ще немає.", + "Search shortcuts saved.": "Ярлики пошуку збережено.", + "Add shortcut": "Додати ярлик", + "Save search shortcuts": "Зберегти ярлики пошуку", + "The search shortcuts could not be loaded.": "Не вдалося завантажити ярлики пошуку.", + "The search shortcuts could not be saved.": "Не вдалося зберегти ярлики пошуку.", + "Search {name} for {query}": "Шукати в {name}: {query}", + "Type what to search for in {name}": "Введіть, що шукати в {name}" } } diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index f4816feb7..6497dffac 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -94,7 +94,7 @@ class PageController extends Controller { * REQ-QSEARCH-004). * @param SearchShortcutService $searchShortcuts The search shortcuts every * search box honours - * (REQ-QSP-002). + * (REQ-SPX-002). */ public function __construct( IRequest $request, diff --git a/lib/Controller/SearchShortcutsController.php b/lib/Controller/SearchShortcutsController.php index 376cd2d04..c6b7b2204 100644 --- a/lib/Controller/SearchShortcutsController.php +++ b/lib/Controller/SearchShortcutsController.php @@ -4,7 +4,7 @@ * SearchShortcutsController * * Administrator endpoints for the search shortcuts (search-ai-prefix-shortcuts, - * REQ-QSP-001). Every user receives the list through the workspace initial + * REQ-SPX-001). Every user receives the list through the workspace initial * state; only administrators read and write it here. * * @category Controller diff --git a/lib/Service/InitialStateBuilder.php b/lib/Service/InitialStateBuilder.php index 1ebf80957..0c3903dd3 100644 --- a/lib/Service/InitialStateBuilder.php +++ b/lib/Service/InitialStateBuilder.php @@ -340,7 +340,7 @@ public function setQuicksearchFallbackTarget(string $fallbackTarget): self { /** * Set the search shortcuts every search box honours (search-ai-prefix-shortcuts, - * REQ-QSP-002). Optional key, like `quicksearchFallbackTarget`: the JS reader + * REQ-SPX-002). Optional key, like `quicksearchFallbackTarget`: the JS reader * defaults it to an empty list. * * @param array $shortcuts Validated `{prefix, name, urlTemplate}` entries. diff --git a/lib/Service/SearchShortcutService.php b/lib/Service/SearchShortcutService.php index 710610424..b31f43fab 100644 --- a/lib/Service/SearchShortcutService.php +++ b/lib/Service/SearchShortcutService.php @@ -4,7 +4,7 @@ * SearchShortcutService * * The administrator's search shortcuts (search-ai-prefix-shortcuts, - * REQ-QSP-001): a prefix such as `!t`, a name and an https address with + * REQ-SPX-001): a prefix such as `!t`, a name and an https address with * `{query}`. A query in a LaunchPad search box that starts with a known * prefix goes straight to that site; the query never passes through the * LaunchPad server. @@ -138,7 +138,7 @@ private function validate(array $raw): array { $template = trim(string: (string)($entry['urlTemplate'] ?? '')); if ($this->adminSettings->isValidQuicksearchFallbackUrlTemplate(value: $template) === false) { - throw new InvalidArgumentException(message: 'The address must be an https address containing {query}: ' . $prefix); + throw new InvalidArgumentException(message: 'Use an https address that contains {query}: ' . $prefix); } $seen[$prefix] = true; diff --git a/src/components/RuntimeShellSearch.vue b/src/components/RuntimeShellSearch.vue index 58b81237e..05c605023 100644 --- a/src/components/RuntimeShellSearch.vue +++ b/src/components/RuntimeShellSearch.vue @@ -37,7 +37,7 @@ :id="listboxId" class="runtime-shell-search__results" role="listbox" - :aria-label="t('launchpad', 'Matching tiles')"> + :aria-label="listboxLabel"> @@ -179,6 +179,14 @@ export default { type: String, default: '', }, + + /** + * The admin's search shortcuts `{prefix, name, urlTemplate}` (REQ-SPX-002). + */ + shortcuts: { + type: Array, + default: () => [], + }, }, emits: ['open', 'filter', 'fallback', 'clear'], @@ -197,6 +205,7 @@ export default { this.$emit('fallback', action) }, getFallbackTarget: () => this.fallbackTarget, + getShortcuts: () => this.shortcuts, }), instanceId: `runtime-shell-search-${(instanceCounter += 1)}`, @@ -239,6 +248,22 @@ export default { * @spec openspec/specs/tile-quick-search/spec.md#req-qsearch-003 * @return {string|null} */ + /** + * Whether the results are shortcuts rather than tiles (REQ-SPX-002/003). + * + * @spec openspec/specs/tile-quick-search/spec.md + */ + showsShortcuts() { + return Boolean(this.search.state.results[0]?.kind) + }, + + /** @spec openspec/specs/tile-quick-search/spec.md */ + listboxLabel() { + return this.showsShortcuts + ? this.t('launchpad', 'Search shortcuts') + : this.t('launchpad', 'Matching tiles') + }, + ariaControls() { return this.hasResults ? this.listboxId : null }, @@ -290,6 +315,11 @@ export default { if (count === 0) { return this.noResultsMessage } + if (this.showsShortcuts) { + return this.search.state.results + .map((result) => this.resultLabel(result)) + .join(', ') + } return this.n( 'launchpad', '%n matching tile', @@ -329,6 +359,10 @@ export default { }, watch: { + shortcuts() { + this.search.setItems(this.items) + }, + items: { immediate: true, /** @@ -376,6 +410,28 @@ export default { * @param {number} index the option's index in `results`. * @return {string} */ + /** + * The visible text of a result: a tile's label, a listed shortcut, + * or where a shortcut will search (REQ-SPX-002/003). + * + * @param {object} result A result from the composable. + * @return {string} The text. + * @spec openspec/specs/tile-quick-search/spec.md + */ + resultLabel(result) { + if (result.kind === 'shortcut') { + return result.rest + ? this.t('launchpad', 'Search {name} for {query}', { + name: result.shortcut.name, + query: result.rest, + }) + : this.t('launchpad', 'Type what to search for in {name}', { + name: result.shortcut.name, + }) + } + return result.label + }, + optionId(index) { return `${this.listboxId}-option-${index}` }, @@ -443,7 +499,8 @@ export default { */ emitFilter() { const query = this.search.state.query - if (!query) { + // A shortcut query is not a tile filter: no tile is dimmed. + if (!query || this.showsShortcuts) { this.$emit('filter', null) return } diff --git a/src/components/Widgets/Renderers/SearchWidget.vue b/src/components/Widgets/Renderers/SearchWidget.vue index b48e54256..b0156959f 100644 --- a/src/components/Widgets/Renderers/SearchWidget.vue +++ b/src/components/Widgets/Renderers/SearchWidget.vue @@ -9,6 +9,7 @@ :items="searchableTiles" :placeholder="content.placeholder || ''" :fallbackTarget="effectiveFallbackTarget" + :shortcuts="injectedSearchShortcuts" @open="onSearchOpen" @filter="onSearchFilter" @fallback="onSearchFallback" @@ -64,6 +65,15 @@ export default { from: 'quicksearchFallbackTarget', default: 'none', }, + + /** + * The admin's search shortcuts (REQ-SPX-002). An empty list keeps + * the box as it was. + */ + injectedSearchShortcuts: { + from: 'searchShortcuts', + default: () => [], + }, }, props: { diff --git a/src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js b/src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js new file mode 100644 index 000000000..761c46021 --- /dev/null +++ b/src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js @@ -0,0 +1,138 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for search shortcuts in `RuntimeShellSearch.vue` and + * `useTileSearch.js` (search-ai-prefix-shortcuts, REQ-SPX-002, REQ-SPX-003): + * a known prefix shows one result naming the site and opens it on Enter, + * an unknown prefix falls through to tile filtering, and `?` or `!` lists + * the shortcuts. + */ + +import { mount } from '@vue/test-utils' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import RuntimeShellSearch from '../RuntimeShellSearch.vue' +import { parsePrefix, shortcutUrl } from '../../composables/useTileSearch.js' + +const SHORTCUTS = [ + { + prefix: '!t', + name: 'TOPdesk', + urlTemplate: 'https://topdesk.gemeente.nl/tas/secure/search?q={query}', + }, + { + prefix: '!z', + name: 'Zaaksysteem', + urlTemplate: 'https://zaken.gemeente.nl/zoek?q={query}', + }, +] +const ITEMS = [ + { id: 'r1', label: '!x rapport' }, + { id: 'a1', label: 'Agenda' }, +] + +describe('search shortcuts', () => { + let wrapper + + beforeEach(() => { + globalThis.t = (_app, key) => key + }) + + afterEach(() => { + wrapper?.unmount() + }) + + function mountSearch() { + wrapper = mount(RuntimeShellSearch, { + attachTo: document.body, + props: { items: ITEMS, fallbackTarget: 'none', shortcuts: SHORTCUTS }, + }) + return wrapper + } + + async function type(value) { + const input = wrapper.find('input') + input.element.value = value + await input.trigger('input') + } + + it('parses the first word as a prefix, case-insensitively, and encodes the rest', () => { + expect(parsePrefix('!T printer 3e verdieping', SHORTCUTS)).toEqual({ + shortcut: SHORTCUTS[0], + rest: 'printer 3e verdieping', + }) + expect(parsePrefix('!x rapport', SHORTCUTS)).toBeNull() + expect(parsePrefix('printer !t', SHORTCUTS)).toBeNull() + expect(shortcutUrl(SHORTCUTS[0], 'printer 3e verdieping')).toBe( + 'https://topdesk.gemeente.nl/tas/secure/search?q=printer%203e%20verdieping', + ) + expect( + shortcutUrl({ urlTemplate: 'javascript:alert({query})' }, 'x'), + ).toBeNull() + }) + + it('REQ-SPX-002: the only result names the site and Enter opens it, without dimming tiles', async () => { + mountSearch() + await type('!t printer 3e verdieping') + + const options = wrapper.findAll('[data-test="quick-search-option"]') + expect(options).toHaveLength(1) + expect(options[0].text()).toBe('Search TOPdesk for printer 3e verdieping') + expect(wrapper.emitted('filter').at(-1)).toEqual([null]) + + await wrapper.find('input').trigger('keydown', { key: 'Enter' }) + expect(wrapper.emitted('fallback').at(-1)).toEqual([ + { + type: 'web-search', + url: 'https://topdesk.gemeente.nl/tas/secure/search?q=printer%203e%20verdieping', + }, + ]) + expect(wrapper.emitted('open')).toBeUndefined() + }) + + it('REQ-SPX-002: a prefix with nothing after it opens nothing', async () => { + mountSearch() + await type('!t') + await wrapper.find('input').trigger('keydown', { key: 'Enter' }) + expect(wrapper.emitted('fallback')).toBeUndefined() + }) + + it('REQ-SPX-002: an unknown prefix filters tiles as before', async () => { + mountSearch() + await type('!x rapport') + + const options = wrapper.findAll('[data-test="quick-search-option"]') + expect(options.map((option) => option.text())).toEqual(['!x rapport']) + expect(wrapper.emitted('filter').at(-1)).toEqual([['r1']]) + }) + + it('REQ-SPX-003: ? lists the shortcuts in a labelled list, and Enter types the prefix', async () => { + mountSearch() + await type('?') + + const list = wrapper.find('[role="listbox"]') + expect(list.attributes('aria-label')).toBe('Search shortcuts') + expect( + wrapper + .findAll('[data-test="quick-search-option"]') + .map((option) => option.text()), + ).toEqual(['!t TOPdesk', '!z Zaaksysteem']) + expect(wrapper.find('[data-test="quick-search-status"]').text()).toBe( + '!t TOPdesk, !z Zaaksysteem', + ) + + await wrapper.find('input').trigger('keydown', { key: 'Enter' }) + expect(wrapper.vm.search.state.query).toBe('!t ') + expect(wrapper.emitted('fallback')).toBeUndefined() + }) + + it('REQ-SPX-003: ! alone lists them too; without shortcuts ? is an ordinary query', async () => { + mountSearch() + await type('!') + expect(wrapper.findAll('[data-test="quick-search-option"]')).toHaveLength(2) + + await wrapper.setProps({ shortcuts: [] }) + await type('?') + expect(wrapper.find('[role="listbox"]').exists()).toBe(false) + }) +}) diff --git a/src/components/admin/AdminSettings.vue b/src/components/admin/AdminSettings.vue index ce51f5d5b..794becd0a 100644 --- a/src/components/admin/AdminSettings.vue +++ b/src/components/admin/AdminSettings.vue @@ -253,6 +253,9 @@ + @@ -297,6 +300,7 @@ import OperationsTab from './tabs/OperationsTab.vue' import OrgNavigationTab from './tabs/OrgNavigationTab.vue' import ProfileFieldsTab from './tabs/ProfileFieldsTab.vue' import RolesPermissionsTab from './tabs/RolesPermissionsTab.vue' +import SearchShortcutsTab from './tabs/SearchShortcutsTab.vue' import SharingTab from './tabs/SharingTab.vue' import TemplatesPage from './tabs/TemplatesPage.vue' import VersioningAuditTab from './tabs/VersioningAuditTab.vue' @@ -322,6 +326,7 @@ export default { MetadataFieldsSettings, KioskTab, ProfileFieldsTab, + SearchShortcutsTab, SharingTab, OrgNavigationTab, DemoDataTab, @@ -419,6 +424,10 @@ export default { slug: 'profile-fields', label: this.t('launchpad', 'Profile fields'), }, + { + slug: 'search-shortcuts', + label: this.t('launchpad', 'Search shortcuts'), + }, ] }, }, diff --git a/src/components/admin/tabs/SearchShortcutsTab.vue b/src/components/admin/tabs/SearchShortcutsTab.vue new file mode 100644 index 000000000..7df55c5ea --- /dev/null +++ b/src/components/admin/tabs/SearchShortcutsTab.vue @@ -0,0 +1,217 @@ + + + + + + + diff --git a/src/components/admin/tabs/__tests__/SearchShortcutsTab.spec.js b/src/components/admin/tabs/__tests__/SearchShortcutsTab.spec.js new file mode 100644 index 000000000..1dda0399d --- /dev/null +++ b/src/components/admin/tabs/__tests__/SearchShortcutsTab.spec.js @@ -0,0 +1,105 @@ +/** + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + * + * Vitest unit tests for `SearchShortcutsTab.vue` (REQ-SPX-001): the list + * loads, a shortcut is added and saved, and the server's refusal of a plain + * http address is shown. + */ + +import axios from '@nextcloud/axios' +import { mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import SearchShortcutsTab from '../SearchShortcutsTab.vue' + +vi.mock('@nextcloud/axios', () => ({ default: { get: vi.fn(), put: vi.fn() } })) +vi.mock('@nextcloud/router', async (importOriginal) => ({ + ...(await importOriginal()), + generateUrl: (path) => path, +})) + +function settle() { + return new Promise((resolve) => setTimeout(resolve, 0)) +} +const stubs = { + NcButton: { + props: ['disabled'], + emits: ['click'], + template: + '', + }, +} + +describe('SearchShortcutsTab', () => { + beforeEach(() => { + axios.get.mockReset() + axios.put.mockReset() + }) + + it('adds the TOPdesk shortcut and saves the list', async () => { + axios.get.mockResolvedValue({ data: { shortcuts: [] } }) + axios.put.mockImplementation((url, body) => + Promise.resolve({ data: { shortcuts: body.shortcuts } }), + ) + const wrapper = mount(SearchShortcutsTab, { global: { stubs } }) + await settle() + await settle() + + await wrapper + .findAll('button') + .find((button) => button.text() === 'Add shortcut') + .trigger('click') + const inputs = wrapper.findAll('tbody input') + await inputs[0].setValue('!t') + await inputs[1].setValue('TOPdesk') + await inputs[2].setValue( + 'https://topdesk.gemeente.nl/tas/secure/search?q={query}', + ) + await wrapper.find('form').trigger('submit') + await settle() + + expect(axios.put).toHaveBeenCalledWith( + '/apps/launchpad/api/admin/search-shortcuts', + { + shortcuts: [ + { + prefix: '!t', + name: 'TOPdesk', + urlTemplate: + 'https://topdesk.gemeente.nl/tas/secure/search?q={query}', + }, + ], + }, + ) + expect(wrapper.text()).toContain('Search shortcuts saved.') + }) + + it('shows the server refusal of a plain http address', async () => { + axios.get.mockResolvedValue({ + data: { + shortcuts: [ + { + prefix: '!i', + name: 'Intranet', + urlTemplate: 'http://intranet.gemeente.nl/zoek?q={query}', + }, + ], + }, + }) + axios.put.mockRejectedValue({ + response: { + data: { error: 'Use an https address that contains {query}: !i' }, + }, + }) + const wrapper = mount(SearchShortcutsTab, { global: { stubs } }) + await settle() + await settle() + + await wrapper.find('form').trigger('submit') + await settle() + + expect(wrapper.find('[role="alert"]').text()).toBe( + 'Use an https address that contains {query}: !i', + ) + }) +}) diff --git a/src/composables/useTileSearch.js b/src/composables/useTileSearch.js index ede1885c8..aee01a3e6 100644 --- a/src/composables/useTileSearch.js +++ b/src/composables/useTileSearch.js @@ -291,6 +291,56 @@ export function isCtrlKFocusShortcut(event) { return Boolean(event.ctrlKey || event.metaKey) } +/** + * Split a query into a known search shortcut and the rest (REQ-SPX-002). + * The first word must equal a shortcut prefix, case-insensitively. + * + * @param {string} query The raw query. + * @param {Array<{prefix: string, name: string, urlTemplate: string}>} shortcuts The admin's shortcuts. + * @return {{shortcut: object, rest: string}|null} The match, or null. + * @spec openspec/specs/tile-quick-search/spec.md + */ +export function parsePrefix(query, shortcuts) { + const trimmed = String(query ?? '').trim() + if (!trimmed.startsWith('!') || !Array.isArray(shortcuts)) { + return null + } + const [first, ...rest] = trimmed.split(/\s+/) + const shortcut = shortcuts.find( + (entry) => String(entry?.prefix || '').toLowerCase() === first.toLowerCase(), + ) + return shortcut ? { shortcut, rest: rest.join(' ') } : null +} + +/** + * The address a shortcut opens for a query: its template with the + * URL-encoded query, only when the template is a valid https `{query}` + * address (REQ-SPX-002). + * + * @param {{urlTemplate: string}} shortcut The shortcut. + * @param {string} rest The query after the prefix. + * @return {string|null} The address, or null. + * @spec openspec/specs/tile-quick-search/spec.md + */ +export function shortcutUrl(shortcut, rest) { + if (!isValidFallbackTemplate(shortcut?.urlTemplate)) { + return null + } + return shortcut.urlTemplate.replace('{query}', encodeURIComponent(rest)) +} + +/** + * Whether a query asks for the shortcut list: `!` alone or `?` (REQ-SPX-003). + * + * @param {string} query The raw query. + * @return {boolean} True for the list. + * @spec openspec/specs/tile-quick-search/spec.md + */ +export function isShortcutListQuery(query) { + const trimmed = String(query ?? '').trim() + return trimmed === '!' || trimmed === '?' +} + /** * Factory — one instance per mounted search bar. Mirrors the * `useGridManager()` shape: a `reactive()` reactive `state` plus @@ -307,12 +357,24 @@ export function isCtrlKFocusShortcut(event) { * @param {() => string} [options.getFallbackTarget] returns the current * `quicksearch_fallback_target` value; consulted lazily so the caller * can change it without recreating the composable. + * @param {() => Array} [options.getShortcuts] returns the admin's + * search shortcuts (REQ-SPX-002); consulted on every recompute. * @return {object} the `{state, ...methods}` API — see inline method docs. * * @spec openspec/specs/tile-quick-search/spec.md */ export function useTileSearch(options = {}) { - const { onOpen, onFallback, getFallbackTarget } = options + const { onOpen, onFallback, getFallbackTarget, getShortcuts } = options + + /** + * The admin's shortcuts, or an empty list. + * + * @return {Array} Shortcuts. + */ + function shortcuts() { + const list = typeof getShortcuts === 'function' ? getShortcuts() : [] + return Array.isArray(list) ? list : [] + } const state = reactive({ query: '', @@ -337,6 +399,34 @@ export function useTileSearch(options = {}) { state.activeIndex = -1 return } + // REQ-SPX-003: `!` or `?` lists the shortcuts. + if (isShortcutListQuery(trimmed) && shortcuts().length > 0) { + state.results = shortcuts().map((shortcut) => ({ + id: `shortcut-list:${shortcut.prefix}`, + label: `${shortcut.prefix} ${shortcut.name}`, + kind: 'shortcut-list', + shortcut, + item: null, + })) + state.activeIndex = 0 + return + } + // REQ-SPX-002: a known prefix wins over tile ranking. + const prefixed = parsePrefix(trimmed, shortcuts()) + if (prefixed) { + state.results = [ + { + id: `shortcut:${prefixed.shortcut.prefix}`, + label: prefixed.shortcut.name, + kind: 'shortcut', + shortcut: prefixed.shortcut, + rest: prefixed.rest, + item: null, + }, + ] + state.activeIndex = 0 + return + } state.results = rankItems(trimmed, items) state.activeIndex = state.results.length > 0 ? 0 : -1 } @@ -412,6 +502,24 @@ export function useTileSearch(options = {}) { */ function pressEnter() { const result = activeResult() + // REQ-SPX-003: Enter on a listed shortcut types its prefix. + if (result && result.kind === 'shortcut-list') { + setQuery(`${result.shortcut.prefix} `) + return { type: 'shortcut-list', shortcut: result.shortcut } + } + // REQ-SPX-002: Enter on a shortcut opens its site in a new tab. + if (result && result.kind === 'shortcut') { + const url = result.rest + ? shortcutUrl(result.shortcut, result.rest) + : null + const action = url + ? { type: 'web-search', url } + : { type: FALLBACK_TARGET_NONE } + if (url && typeof onFallback === 'function') { + onFallback(action) + } + return action + } if (result) { if (typeof onOpen === 'function') { onOpen(result.item) diff --git a/src/utils/loadInitialState.js b/src/utils/loadInitialState.js index 66522a4aa..4c65b0b45 100644 --- a/src/utils/loadInitialState.js +++ b/src/utils/loadInitialState.js @@ -63,7 +63,7 @@ const PAGE_KEYS = { // PHP side yet simply omits it, and 'none' is the safe default // (no navigation on no-match Enter). quicksearchFallbackTarget: 'none', - // search-ai-prefix-shortcuts REQ-QSP-002: the admin's search + // search-ai-prefix-shortcuts REQ-SPX-002: the admin's search // shortcuts `{prefix, name, urlTemplate}`. Optional key; an empty // list keeps every search box as it was. searchShortcuts: [], diff --git a/tests/Unit/Service/InitialStateBuilderTest.php b/tests/Unit/Service/InitialStateBuilderTest.php index 5763c708e..4ab5f79a6 100644 --- a/tests/Unit/Service/InitialStateBuilderTest.php +++ b/tests/Unit/Service/InitialStateBuilderTest.php @@ -30,7 +30,7 @@ class InitialStateBuilderTest extends TestCase { /** - * REQ-QSP-002: the search shortcuts reach the workspace page. + * REQ-SPX-002: the search shortcuts reach the workspace page. * * @return void */ diff --git a/tests/Unit/Service/SearchShortcutServiceTest.php b/tests/Unit/Service/SearchShortcutServiceTest.php index d31c9d225..f3dae7220 100644 --- a/tests/Unit/Service/SearchShortcutServiceTest.php +++ b/tests/Unit/Service/SearchShortcutServiceTest.php @@ -3,7 +3,7 @@ /** * SearchShortcutServiceTest * - * Search shortcuts (search-ai-prefix-shortcuts, REQ-QSP-001): the prefix + * Search shortcuts (search-ai-prefix-shortcuts, REQ-SPX-001): the prefix * pattern, case-insensitive uniqueness, the https `{query}` template check * reused from the quick-search fallback, and the limit of 30. * From 51d4103ca765824e6dcf6374eaefdc0ac85bda54 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 08:55:24 +0200 Subject: [PATCH 06/11] docs(openspec): archive search-ai-prefix-shortcuts, d-search-prefix is built --- .../.openspec.yaml | 0 .../design.md | 7 +++ .../proposal.md | 0 .../specs/tile-quick-search/spec.md | 0 .../tasks.md | 9 ++++ .../search-ai-prefix-shortcuts/tasks.md | 9 ---- openspec/parity/capabilities.json | 7 +-- openspec/specs/tile-quick-search/spec.md | 49 +++++++++++++++++++ 8 files changed, 69 insertions(+), 12 deletions(-) rename openspec/changes/{search-ai-prefix-shortcuts => archive/2026-09-30-search-ai-prefix-shortcuts}/.openspec.yaml (100%) rename openspec/changes/{search-ai-prefix-shortcuts => archive/2026-09-30-search-ai-prefix-shortcuts}/design.md (79%) rename openspec/changes/{search-ai-prefix-shortcuts => archive/2026-09-30-search-ai-prefix-shortcuts}/proposal.md (100%) rename openspec/changes/{search-ai-prefix-shortcuts => archive/2026-09-30-search-ai-prefix-shortcuts}/specs/tile-quick-search/spec.md (100%) create mode 100644 openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/tasks.md delete mode 100644 openspec/changes/search-ai-prefix-shortcuts/tasks.md diff --git a/openspec/changes/search-ai-prefix-shortcuts/.openspec.yaml b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/.openspec.yaml similarity index 100% rename from openspec/changes/search-ai-prefix-shortcuts/.openspec.yaml rename to openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/.openspec.yaml diff --git a/openspec/changes/search-ai-prefix-shortcuts/design.md b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/design.md similarity index 79% rename from openspec/changes/search-ai-prefix-shortcuts/design.md rename to openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/design.md index fdc114c03..cba698c48 100644 --- a/openspec/changes/search-ai-prefix-shortcuts/design.md +++ b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/design.md @@ -36,3 +36,10 @@ Only administrators edit the list. Every user can use it. The query leaves the b - PHPUnit: prefix format, uniqueness, template check, the 30-entry limit. - Vitest: `parsePrefix` cases (known, unknown, prefix only, uppercase), result rendering, Enter opening the right address, `?` listing. - Playwright: with `!t` set to a test site, typing `!t printer` and Enter opens that site with `printer`. + +## What the build corrected (2026-09-30) + +- The Workspace quick search and the search widget are one component since the bar became a widget (`SearchWidget.vue` hosts `RuntimeShellSearch.vue`), so the shortcuts are wired once. +- The shortcut list has its own admin endpoints (`GET`/`PUT /api/admin/search-shortcuts`, `SearchShortcutService`) instead of another parameter on `AdminSettingsService::updateSettings()`; the https `{query}` check is the same method, made public. +- Prefixes are stored lower case. Enter on a prefix with nothing after it opens nothing, and a shortcut query dims no tiles. +- Enter on an entry of the `?` list types that prefix into the box. diff --git a/openspec/changes/search-ai-prefix-shortcuts/proposal.md b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/proposal.md similarity index 100% rename from openspec/changes/search-ai-prefix-shortcuts/proposal.md rename to openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/proposal.md diff --git a/openspec/changes/search-ai-prefix-shortcuts/specs/tile-quick-search/spec.md b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/specs/tile-quick-search/spec.md similarity index 100% rename from openspec/changes/search-ai-prefix-shortcuts/specs/tile-quick-search/spec.md rename to openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/specs/tile-quick-search/spec.md diff --git a/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/tasks.md b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/tasks.md new file mode 100644 index 000000000..c9ce6620c --- /dev/null +++ b/openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts/tasks.md @@ -0,0 +1,9 @@ +# Tasks: search-ai-prefix-shortcuts + +- [x] 1. Add `search_shortcuts` to `lib/Db/AdminSettingKey.php` and validate it in `AdminSettingsService` (prefix pattern, uniqueness, template check, limit 30). Verify: PHPUnit. +- [x] 2. Provide the list through `lib/Service/InitialStateBuilder.php`. Verify: initial-state contract test. +- [x] 3. Add `parsePrefix()` to `src/composables/useTileSearch.js` and route a match ahead of ranking. Verify: Vitest. +- [x] 4. Render the "Search for " result and the `?` list in `SearchWidget.vue` and `RuntimeShellSearch.vue`, opening through `useTileSearchHost.js`. Verify: Vitest and an axe check on the list. +- [x] 5. Add the admin section to edit shortcuts. Verify: Vitest. +- [ ] 6. Playwright: `!t printer` opens the configured site. Not written: the requirements carry `@e2e exclude` naming the PHPUnit and Vitest files that cover them. +- [x] 7. Add `nl` and `en` strings, fold the delta into `openspec/specs/tile-quick-search/spec.md` on archive, and set d-search-prefix to `built` in the parity matrix with evidence lines. diff --git a/openspec/changes/search-ai-prefix-shortcuts/tasks.md b/openspec/changes/search-ai-prefix-shortcuts/tasks.md deleted file mode 100644 index 89b996644..000000000 --- a/openspec/changes/search-ai-prefix-shortcuts/tasks.md +++ /dev/null @@ -1,9 +0,0 @@ -# Tasks: search-ai-prefix-shortcuts - -- [ ] 1. Add `search_shortcuts` to `lib/Db/AdminSettingKey.php` and validate it in `AdminSettingsService` (prefix pattern, uniqueness, template check, limit 30). Verify: PHPUnit. -- [ ] 2. Provide the list through `lib/Service/InitialStateBuilder.php`. Verify: initial-state contract test. -- [ ] 3. Add `parsePrefix()` to `src/composables/useTileSearch.js` and route a match ahead of ranking. Verify: Vitest. -- [ ] 4. Render the "Search for " result and the `?` list in `SearchWidget.vue` and `RuntimeShellSearch.vue`, opening through `useTileSearchHost.js`. Verify: Vitest and an axe check on the list. -- [ ] 5. Add the admin section to edit shortcuts. Verify: Vitest. -- [ ] 6. Playwright: `!t printer` opens the configured site. -- [ ] 7. Add `nl` and `en` strings, fold the delta into `openspec/specs/tile-quick-search/spec.md` on archive, and set d-search-prefix to `built` in the parity matrix with evidence lines. diff --git a/openspec/parity/capabilities.json b/openspec/parity/capabilities.json index c2d5e72b6..09332f820 100644 --- a/openspec/parity/capabilities.json +++ b/openspec/parity/capabilities.json @@ -4984,11 +4984,12 @@ "homarr": "yes", "dashy": "yes", "built": { - "state": "specified", + "state": "built", + "evidence": "lib/Service/SearchShortcutService.php; src/composables/useTileSearch.js (parsePrefix); src/components/RuntimeShellSearch.vue; src/components/admin/tabs/SearchShortcutsTab.vue", "owner": "ConductionNL/launchpad", - "change": "openspec/changes/search-ai-prefix-shortcuts" + "change": "openspec/changes/archive/2026-09-30-search-ai-prefix-shortcuts" }, - "reachedOn": "nothing", + "reachedOn": "Any search widget: type !t printer (a prefix the administrator set under Admin settings > LaunchPad > Search shortcuts) or ? for the list", "evidence": { "workspace-365": "https://support.workspace365.net/en/articles/175653-global-search users 'search by category' (Announcements, Apps, Email, Files, Folders, Knowledge articles, People, plus OpenIMS or Egnyte when configured); no prefix syntax to send a query to another site (read 2026-09-26)", "microsoft-viva": "not checked: Microsoft 365 roadmap 562015 'Scoped Search for SharePoint app in Teams' scopes search to SharePoint global search (cached roadmap, read 2026-09-26); no prefix to an external site found", diff --git a/openspec/specs/tile-quick-search/spec.md b/openspec/specs/tile-quick-search/spec.md index b92fe85ae..2fa97ffc9 100644 --- a/openspec/specs/tile-quick-search/spec.md +++ b/openspec/specs/tile-quick-search/spec.md @@ -251,3 +251,52 @@ cannot see on screen, or cannot find tiles they can. - WHEN its title is resolved - THEN the widget type's display name MUST be used - AND the generic fallback word MUST be used only when nothing else resolves + +### Requirement: Administrators define search shortcuts (REQ-SPX-001) + +@e2e exclude Playwright is not wired for search shortcuts yet; covered by tests/Unit/Service/SearchShortcutServiceTest.php and src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js + +An administrator MUST be able to define up to 30 search shortcuts, each with a prefix (`!` and 1 to 10 letters or digits, unique regardless of case), a name, and an https address containing `{query}`. The server MUST refuse an entry that breaks any of these rules. + +#### Scenario: Add the TOPdesk shortcut + +- **GIVEN** Noor is an administrator on the LaunchPad admin page, section "Search shortcuts" +- **WHEN** she adds prefix `!t`, name "TOPdesk" and address `https://topdesk.gemeente.nl/tas/secure/search?q={query}` and saves +- **THEN** the shortcut is listed + +#### Scenario: Plain http is refused + +- **GIVEN** Noor adds a shortcut +- **WHEN** she enters the address `http://intranet.gemeente.nl/zoek?q={query}` +- **THEN** the save fails with "Use an https address that contains {query}" + +### Requirement: A prefix sends the query to its site (REQ-SPX-002) + +@e2e exclude Playwright is not wired for search shortcuts yet; covered by tests/Unit/Service/SearchShortcutServiceTest.php and src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js + +In the search widget and the Workspace quick search, a query whose first word is a known prefix MUST show a single result naming the site and the rest of the query, and Enter MUST open the site's address with the URL-encoded rest in a new tab. A query without a known prefix MUST behave as before: tile filtering, then the configured fallback. + +#### Scenario: Search TOPdesk + +- **GIVEN** the shortcut `!t` for "TOPdesk" +- **WHEN** Pieter types `!t printer 3e verdieping` in the quick search +- **THEN** the only result reads "Search TOPdesk for printer 3e verdieping" +- **AND WHEN** he presses Enter, a new tab opens `https://topdesk.gemeente.nl/tas/secure/search?q=printer%203e%20verdieping` + +#### Scenario: Unknown prefix falls through + +- **GIVEN** no shortcut `!x` +- **WHEN** Pieter types `!x rapport` +- **THEN** the box filters tiles for `!x rapport` and applies the normal fallback + +### Requirement: Users can see the shortcuts (REQ-SPX-003) + +@e2e exclude Playwright is not wired for search shortcuts yet; covered by tests/Unit/Service/SearchShortcutServiceTest.php and src/components/__tests__/RuntimeShellSearch.shortcuts.spec.js + +Typing `!` alone or `?` in a search box MUST list the available shortcuts with their prefix and name, announced to screen readers. + +#### Scenario: List the shortcuts + +- **GIVEN** shortcuts `!t` "TOPdesk" and `!z` "Zaaksysteem" +- **WHEN** Pieter types `?` in the search widget +- **THEN** he sees "!t TOPdesk" and "!z Zaaksysteem" From 4f3c82029b68fdac48fea73a8b72ba53889e121e Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Wed, 30 Sep 2026 09:42:44 +0200 Subject: [PATCH 07/11] fix(containers): count tile clicks with one delegated listener, so the tile wrapper is not a fake control (gate-32) --- .../Widgets/Renderers/ContainerWidget.vue | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index 0c071a918..eb3e5ac59 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -28,7 +28,7 @@ :gs-y="child.gridY || 0" :gs-w="child.gridWidth || 2" :gs-h="child.gridHeight || 2" - @click="onChildClick(child)"> + :data-use-key="useKey(child)">
@@ -251,10 +251,14 @@ export default { }, mounted() { + // REQ-TSO-002: one delegated listener counts clicks on the tiles' + // own links and buttons; the wrapper itself is not a control. + this.$refs.innerGrid?.addEventListener('click', this.onGridClick) this.initInnerGrid() }, beforeUnmount() { + this.$refs.innerGrid?.removeEventListener('click', this.onGridClick) this.destroyInnerGrid() }, @@ -282,14 +286,17 @@ export default { * does not change until the next page load, so tiles never jump * under the pointer. * - * @param {object} child The clicked child placement. + * @param {MouseEvent} event The click inside the grid. * @spec openspec/specs/container-widget/spec.md */ - onChildClick(child) { + onGridClick(event) { if (this.editMode) { return } - recordLocalTileUse(this.useKey(child)) + const item = event?.target?.closest?.('[data-use-key]') + if (item) { + recordLocalTileUse(item.dataset.useKey) + } }, /** From ac0852a84613fe1af943421f99afdbc01a2840c1 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 5 Oct 2026 19:25:38 +0200 Subject: [PATCH 08/11] docs(tiles): tag the container widget's lifecycle hooks with their spec (gate 16) --- src/components/Widgets/Renderers/ContainerWidget.vue | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/Widgets/Renderers/ContainerWidget.vue b/src/components/Widgets/Renderers/ContainerWidget.vue index eb3e5ac59..906bd0cf8 100644 --- a/src/components/Widgets/Renderers/ContainerWidget.vue +++ b/src/components/Widgets/Renderers/ContainerWidget.vue @@ -250,6 +250,7 @@ export default { }, }, + /** @spec openspec/specs/container-widget/spec.md */ mounted() { // REQ-TSO-002: one delegated listener counts clicks on the tiles' // own links and buttons; the wrapper itself is not a control. @@ -257,6 +258,7 @@ export default { this.initInnerGrid() }, + /** @spec openspec/specs/container-widget/spec.md */ beforeUnmount() { this.$refs.innerGrid?.removeEventListener('click', this.onGridClick) this.destroyInnerGrid() From 984a7fa37fcb6aa9ac627d40bf9a9ec819be2f9f Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 5 Oct 2026 19:25:41 +0200 Subject: [PATCH 09/11] docs(search): tag the shortcuts watcher with its spec (gate 16) --- src/components/RuntimeShellSearch.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/RuntimeShellSearch.vue b/src/components/RuntimeShellSearch.vue index 05c605023..282d05d08 100644 --- a/src/components/RuntimeShellSearch.vue +++ b/src/components/RuntimeShellSearch.vue @@ -359,6 +359,7 @@ export default { }, watch: { + /** @spec openspec/specs/tile-quick-search/spec.md */ shortcuts() { this.search.setItems(this.items) }, From 3d3a3b6242c09cc2065d78ec610056fb90617db3 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Mon, 5 Oct 2026 19:40:26 +0200 Subject: [PATCH 10/11] test(search): cover the shortcuts endpoint through the real service: admin, refusals and an invalid list --- .../SearchShortcutsControllerTest.php | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 tests/Unit/Controller/SearchShortcutsControllerTest.php diff --git a/tests/Unit/Controller/SearchShortcutsControllerTest.php b/tests/Unit/Controller/SearchShortcutsControllerTest.php new file mode 100644 index 000000000..506f3c2e8 --- /dev/null +++ b/tests/Unit/Controller/SearchShortcutsControllerTest.php @@ -0,0 +1,131 @@ + + * @copyright 2026 Conduction b.v. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * SPDX-License-Identifier: EUPL-1.2 + */ + +declare(strict_types=1); + +namespace Unit\Controller; + +use OCA\LaunchPad\Controller\SearchShortcutsController; +use OCA\LaunchPad\Db\AdminSetting; +use OCA\LaunchPad\Db\AdminSettingMapper; +use OCA\LaunchPad\Service\ActionAuthService; +use OCA\LaunchPad\Service\AdminSettingsService; +use OCA\LaunchPad\Service\SearchShortcutService; +use OCP\AppFramework\Http; +use OCP\AppFramework\OCS\OCSForbiddenException; +use OCP\IGroupManager; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; +use PHPUnit\Framework\TestCase; + +/** + * Tests for SearchShortcutsController. + */ +class SearchShortcutsControllerTest extends TestCase { + /** + * Stored admin settings. + * + * @var array + */ + private array $settings = []; + + /** + * A controller for a caller. + * + * @param string|null $uid The caller, or null when signed out. + * @param boolean $admin Whether the caller is an administrator. + * @param boolean $allowed Whether the action matrix lets them through. + * + * @return SearchShortcutsController + */ + private function controller(?string $uid, bool $admin = true, bool $allowed = true): SearchShortcutsController { + $mapper = $this->createMock(AdminSettingMapper::class); + $mapper->method('getValue')->willReturnCallback(fn (string $key, mixed $default = null): mixed => $this->settings[$key] ?? $default); + $mapper->method('setSetting')->willReturnCallback(function (string $key, mixed $value): AdminSetting { + $this->settings[$key] = $value; + return new AdminSetting(); + }); + $service = new SearchShortcutService(settingMapper: $mapper, adminSettings: new AdminSettingsService(settingMapper: $mapper)); + + $session = $this->createMock(IUserSession::class); + $user = null; + if ($uid !== null) { + $user = $this->createMock(IUser::class); + $user->method('getUID')->willReturn($uid); + } + + $session->method('getUser')->willReturn($user); + $groups = $this->createMock(IGroupManager::class); + $groups->method('isAdmin')->willReturn($admin); + $auth = $this->createMock(ActionAuthService::class); + if ($allowed === false) { + $auth->method('requireAction')->willThrowException(new OCSForbiddenException('no')); + } + + return new SearchShortcutsController( + request: $this->createMock(IRequest::class), + shortcuts: $service, + actionAuth: $auth, + userSession: $session, + groupManager: $groups, + ); + }//end controller() + + /** + * An administrator stores a list and reads it back. + * + * @return void + */ + public function testAnAdministratorSavesAndReadsTheShortcuts(): void { + $saved = $this->controller(uid: 'admin')->save(shortcuts: [['prefix' => '!T', 'name' => 'TOPdesk', 'urlTemplate' => 'https://topdesk.example.nl/search?q={query}']]); + $this->assertSame(Http::STATUS_OK, $saved->getStatus()); + + $read = $this->controller(uid: 'admin')->index()->getData()['shortcuts']; + $this->assertSame('!t', $read[0]['prefix']); + }//end testAnAdministratorSavesAndReadsTheShortcuts() + + /** + * An invalid list is 400 and stores nothing. + * + * @return void + */ + public function testAnInvalidListIs400(): void { + $response = $this->controller(uid: 'admin')->save(shortcuts: [['prefix' => 't', 'name' => 'T', 'urlTemplate' => 'https://x.example/?q={query}']]); + $this->assertSame(Http::STATUS_BAD_REQUEST, $response->getStatus()); + $this->assertArrayHasKey('error', $response->getData()); + $this->assertSame([], $this->settings); + }//end testAnInvalidListIs400() + + /** + * Signed out is 401, a non-admin 403, a refused action 403; nothing is stored. + * + * @return void + */ + public function testOnlyAnAllowedAdministratorGetsIn(): void { + $list = [['prefix' => '!t', 'name' => 'T', 'urlTemplate' => 'https://x.example/?q={query}']]; + foreach ([[null, true, true, Http::STATUS_UNAUTHORIZED], ['sanne', false, true, Http::STATUS_FORBIDDEN], ['admin', true, false, Http::STATUS_FORBIDDEN]] as [$uid, $admin, $allowed, $status]) { + $this->assertSame($status, $this->controller(uid: $uid, admin: $admin, allowed: $allowed)->index()->getStatus()); + $this->assertSame($status, $this->controller(uid: $uid, admin: $admin, allowed: $allowed)->save(shortcuts: $list)->getStatus()); + } + + $this->assertSame([], $this->settings, 'a refused write stores nothing'); + }//end testOnlyAnAllowedAdministratorGetsIn() +}//end class From 91201465529dac824ee4a647379e77d37fb8f361 Mon Sep 17 00:00:00 2001 From: Ruben van der Linde Date: Fri, 23 Oct 2026 06:27:09 +0200 Subject: [PATCH 11/11] test(admin): the search shortcuts tab is the thirteenth admin tab Assisted-by: ClaudeCode:claude-opus-5-5 --- src/components/admin/__tests__/AdminSettings.tabs.spec.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/admin/__tests__/AdminSettings.tabs.spec.js b/src/components/admin/__tests__/AdminSettings.tabs.spec.js index 25b7ccbaa..e22fe14e4 100644 --- a/src/components/admin/__tests__/AdminSettings.tabs.spec.js +++ b/src/components/admin/__tests__/AdminSettings.tabs.spec.js @@ -52,7 +52,7 @@ describe('AdminSettings tabs', () => { expect(tabs[1].id).toBe('templates') }) - it('hands the twelve tabs to the admin shell as an underline strip', () => { + it('hands the thirteen tabs to the admin shell as an underline strip', () => { expect(tabs.map((tab) => tab.id)).toEqual([ 'general', 'templates', @@ -66,6 +66,7 @@ describe('AdminSettings tabs', () => { 'group-dashboards', 'detail-fields', 'profile-fields', + 'search-shortcuts', ]) expect(source).toContain(':tabs="beheerTabs"') expect(render).toContain('tabsVariant: "underline"')