diff options
Diffstat (limited to 'apps')
68 files changed, 931 insertions, 222 deletions
diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index 9021ba98a0f..baf1021d3e6 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -10,7 +10,7 @@ <name>WebDAV</name> <summary>WebDAV endpoint</summary> <description>WebDAV endpoint</description> - <version>1.34.0</version> + <version>1.34.1</version> <licence>agpl</licence> <author>owncloud.org</author> <namespace>DAV</namespace> diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index b9708ea5589..9eab0456159 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -217,6 +217,7 @@ return array( 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => $baseDir . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => $baseDir . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PropFindPreloadNotifyPlugin' => $baseDir . '/../lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => $baseDir . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => $baseDir . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => $baseDir . '/../lib/Connector/Sabre/QuotaPlugin.php', @@ -354,6 +355,7 @@ return array( 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => $baseDir . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => $baseDir . '/../lib/Migration/Version1030Date20240205103243.php', 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => $baseDir . '/../lib/Migration/Version1031Date20240610134258.php', + 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php', 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php', 'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 75ac3350160..e9a0ef01c07 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -232,6 +232,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Connector\\Sabre\\ObjectTree' => __DIR__ . '/..' . '/../lib/Connector/Sabre/ObjectTree.php', 'OCA\\DAV\\Connector\\Sabre\\Principal' => __DIR__ . '/..' . '/../lib/Connector/Sabre/Principal.php', 'OCA\\DAV\\Connector\\Sabre\\PropFindMonitorPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindMonitorPlugin.php', + 'OCA\\DAV\\Connector\\Sabre\\PropFindPreloadNotifyPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PropfindCompressionPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PropfindCompressionPlugin.php', 'OCA\\DAV\\Connector\\Sabre\\PublicAuth' => __DIR__ . '/..' . '/../lib/Connector/Sabre/PublicAuth.php', 'OCA\\DAV\\Connector\\Sabre\\QuotaPlugin' => __DIR__ . '/..' . '/../lib/Connector/Sabre/QuotaPlugin.php', @@ -369,6 +370,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1029Date20231004091403' => __DIR__ . '/..' . '/../lib/Migration/Version1029Date20231004091403.php', 'OCA\\DAV\\Migration\\Version1030Date20240205103243' => __DIR__ . '/..' . '/../lib/Migration/Version1030Date20240205103243.php', 'OCA\\DAV\\Migration\\Version1031Date20240610134258' => __DIR__ . '/..' . '/../lib/Migration/Version1031Date20240610134258.php', + 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php', 'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php', 'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php', diff --git a/apps/dav/l10n/pl.js b/apps/dav/l10n/pl.js index 3c9929cc943..8d72b6d7632 100644 --- a/apps/dav/l10n/pl.js +++ b/apps/dav/l10n/pl.js @@ -79,6 +79,9 @@ OC.L10N.register( "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"], "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"], + "In the past on %1$s between %2$s - %3$s" : "W przeszłości dnia %1$s między %2$s - %3$s", + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutę dnia %1$s między %2$s - %3$s","Za %n minuty dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s"], + "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za godzinę dnia %1$s między %2$s - %3$s","Za %n godziny dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"], "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"], @@ -87,18 +90,38 @@ OC.L10N.register( "Every Day for the entire day" : "Codziennie przez cały dzień", "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s", "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s", + "Every Day between %1$s - %2$s until %3$s" : "Codziennie między %1$s - %2$s do %3$s", "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień", "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s", "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s", "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s", "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia", "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień", + "Every Week on %1$s for the entire day until %2$s" : "Co tydzień w %1$s przez cały dzień do %2$s", + "Every Week on %1$s between %2$s - %3$s" : "Co tydzień w %1$s między %2$s - %3$s", + "Every Week on %1$s between %2$s - %3$s until %4$s" : "Co tydzień w %1$s między %2$s - %3$s do %4$s", + "Every %1$d Weeks on %2$s for the entire day" : "Co %1$d tygodni w %2$s przez cały dzień", + "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d tygodnie w %2$s przez cały dzień do %3$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s do %5$s", + "Every Month on the %1$s for the entire day" : "Co miesiąc dnia %1$s przez cały dzień", + "Every Month on the %1$s for the entire day until %2$s" : "Co miesiąc dnia %1$s przez cały dzień do %2$s", + "Every Month on the %1$s between %2$s - %3$s" : "Co miesiąc dnia %1$s między %2$s - %3$s", + "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Co miesiąc dnia %1$s między %2$s - %3$s do %4$s", + "Every %1$d Months on the %2$s for the entire day" : "Co %1$d miesiący dnia %2$s przez cały dzień", + "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d miesięcy dnia %2$s przez cały dzień do %3$s", + "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d miesięcy dnia %2$s między %3$s - %4$s", "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s", "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień", "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s", "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s", "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s", "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień", + "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d lat w %2$s dnia %3$s przez cały dzień do %4$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s do %6$s", + "On specific dates for the entire day until %1$s" : "W określonych datach przez cały dzień do %1$s", + "On specific dates between %1$s - %2$s until %3$s" : "W określonych datach między %1$s - %2$s do %3$s", "In the past on %1$s" : "W przeszłości dnia %1$s", "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"], "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"], @@ -108,6 +131,18 @@ OC.L10N.register( "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"], "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s", "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"], + "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s","Za %n godziny dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s"], + "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Jutro dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s"], + "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s"], + "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s","Za %n miesiące dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s"], + "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok dnia %1$s, następnie dnia %2$s","Za %n lata dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s"], + "In the past on %1$s then on %2$s and %3$s" : "W przeszłości dnia %1$s, następnie dnia %2$s i %3$s", + "_In a minute on %1$s then on %2$s and %3$s_::_In %n minutes on %1$s then on %2$s and %3$s_" : ["Za minutę dnia %1$s, następnie dnia %2$s i %3$s","Za %n minuty dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a hour on %1$s then on %2$s and %3$s_::_In %n hours on %1$s then on %2$s and %3$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s i %3$s","Za %%ngodziny dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a day on %1$s then on %2$s and %3$s_::_In %n days on %1$s then on %2$s and %3$s_" : ["Jutro, dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a week on %1$s then on %2$s and %3$s_::_In %n weeks on %1$s then on %2$s and %3$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a month on %1$s then on %2$s and %3$s_::_In %n months on %1$s then on %2$s and %3$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesiące dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a year on %1$s then on %2$s and %3$s_::_In %n years on %1$s then on %2$s and %3$s_" : ["Za rok dnia %1$s, następnie dnia %2$s i %3$s","Za %n lata dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s"], "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania", "Cancelled: %1$s" : "Anulowane: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", diff --git a/apps/dav/l10n/pl.json b/apps/dav/l10n/pl.json index 4b2ffc40dc0..242c36b857a 100644 --- a/apps/dav/l10n/pl.json +++ b/apps/dav/l10n/pl.json @@ -77,6 +77,9 @@ "_In a week on %1$s for the entire day_::_In %n weeks on %1$s for the entire day_" : ["Za tydzień, dnia %1$s, przez cały dzień","Za %n tygodnie, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień","Za %n tygodni, dnia %1$s, przez cały dzień"], "_In a month on %1$s for the entire day_::_In %n months on %1$s for the entire day_" : ["W ciągu miesiąca, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień","W ciągu %n miesięcy, dnia %1$s przez cały dzień"], "_In a year on %1$s for the entire day_::_In %n years on %1$s for the entire day_" : ["Za rok dnia %1$s przez cały dzień","Za %n lata dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień","Za %n lat dnia %1$s przez cały dzień"], + "In the past on %1$s between %2$s - %3$s" : "W przeszłości dnia %1$s między %2$s - %3$s", + "_In a minute on %1$s between %2$s - %3$s_::_In %n minutes on %1$s between %2$s - %3$s_" : ["Za minutę dnia %1$s między %2$s - %3$s","Za %n minuty dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s","Za %n minut dnia %1$s między %2$s - %3$s"], + "_In a hour on %1$s between %2$s - %3$s_::_In %n hours on %1$s between %2$s - %3$s_" : ["Za godzinę dnia %1$s między %2$s - %3$s","Za %n godziny dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s","Za %n godzin dnia %1$s między %2$s - %3$s"], "_In a day on %1$s between %2$s - %3$s_::_In %n days on %1$s between %2$s - %3$s_" : ["Za dzień, dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s","Za %n dni dnia %1$s między %2$s - %3$s"], "_In a week on %1$s between %2$s - %3$s_::_In %n weeks on %1$s between %2$s - %3$s_" : ["Za tydzień dnia %1$s między %2$s - %3$s","Za %n tygodnie dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s","Za %n tygodni dnia %1$s między %2$s - %3$s"], "_In a month on %1$s between %2$s - %3$s_::_In %n months on %1$s between %2$s - %3$s_" : ["Za miesiąc dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s","Za %n miesięcy dnia %1$s między %2$s - %3$s"], @@ -85,18 +88,38 @@ "Every Day for the entire day" : "Codziennie przez cały dzień", "Every Day for the entire day until %1$s" : "Codziennie przez cały dzień do %1$s", "Every Day between %1$s - %2$s" : "Codziennie między %1$s – %2$s", + "Every Day between %1$s - %2$s until %3$s" : "Codziennie między %1$s - %2$s do %3$s", "Every %1$d Days for the entire day" : "Co %1$d dni przez cały dzień", "Every %1$d Days for the entire day until %2$s" : "Co %1$d dni przez cały dzień aż do %2$s", "Every %1$d Days between %2$s - %3$s" : "Co %1$d dni pomiędzy %2$s - %3$s", "Every %1$d Days between %2$s - %3$s until %4$s" : "Co %1$d dni, pomiędzy %2$s - %3$s aż do %4$s", "Could not generate event recurrence statement" : "Nie można wygenerować zestawienia powtórzeń zdarzenia", "Every Week on %1$s for the entire day" : "Każdego tygodnia w %1$s przez cały dzień", + "Every Week on %1$s for the entire day until %2$s" : "Co tydzień w %1$s przez cały dzień do %2$s", + "Every Week on %1$s between %2$s - %3$s" : "Co tydzień w %1$s między %2$s - %3$s", + "Every Week on %1$s between %2$s - %3$s until %4$s" : "Co tydzień w %1$s między %2$s - %3$s do %4$s", + "Every %1$d Weeks on %2$s for the entire day" : "Co %1$d tygodni w %2$s przez cały dzień", + "Every %1$d Weeks on %2$s for the entire day until %3$s" : "Co %1$d tygodnie w %2$s przez cały dzień do %3$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s", + "Every %1$d Weeks on %2$s between %3$s - %4$s until %5$s" : "Co %1$d tygodnie w %2$s między %3$s - %4$s do %5$s", + "Every Month on the %1$s for the entire day" : "Co miesiąc dnia %1$s przez cały dzień", + "Every Month on the %1$s for the entire day until %2$s" : "Co miesiąc dnia %1$s przez cały dzień do %2$s", + "Every Month on the %1$s between %2$s - %3$s" : "Co miesiąc dnia %1$s między %2$s - %3$s", + "Every Month on the %1$s between %2$s - %3$s until %4$s" : "Co miesiąc dnia %1$s między %2$s - %3$s do %4$s", + "Every %1$d Months on the %2$s for the entire day" : "Co %1$d miesiący dnia %2$s przez cały dzień", + "Every %1$d Months on the %2$s for the entire day until %3$s" : "Co %1$d miesięcy dnia %2$s przez cały dzień do %3$s", + "Every %1$d Months on the %2$s between %3$s - %4$s" : "Co %1$d miesięcy dnia %2$s między %3$s - %4$s", "Every %1$d Months on the %2$s between %3$s - %4$s until %5$s" : "Każdego %1$d miesiąca dnia %2$s między %3$s - %4$s do %5$s", "Every Year in %1$s on the %2$s for the entire day" : "Co rok w %1$s dnia %2$s przez cały dzień", "Every Year in %1$s on the %2$s for the entire day until %3$s" : "Co rok w %1$s dnia %2$s przez cały dzień do %3$s", "Every Year in %1$s on the %2$s between %3$s - %4$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s", "Every Year in %1$s on the %2$s between %3$s - %4$s until %5$s" : "Co roku za %1$s dnia %2$s między %3$s - %4$s do %5$s", "Every %1$d Years in %2$s on the %3$s for the entire day" : "Co %1$d lat dnia %2$s o %3$s przez cały dzień", + "Every %1$d Years in %2$s on the %3$s for the entire day until %4$s" : "Co %1$d lat w %2$s dnia %3$s przez cały dzień do %4$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s", + "Every %1$d Years in %2$s on the %3$s between %4$s - %5$s until %6$s" : "Co %1$d lat w %2$s dnia %3$s między %4$s - %5$s do %6$s", + "On specific dates for the entire day until %1$s" : "W określonych datach przez cały dzień do %1$s", + "On specific dates between %1$s - %2$s until %3$s" : "W określonych datach między %1$s - %2$s do %3$s", "In the past on %1$s" : "W przeszłości dnia %1$s", "_In a minute on %1$s_::_In %n minutes on %1$s_" : ["Za minutę dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s","Za %n minut dnia %1$s"], "_In a hour on %1$s_::_In %n hours on %1$s_" : ["Za godzinę dnia %1$s","Za %n godziny dnia %1$s","Za %n godzin dnia %1$s","Za %n godzin dnia %1$s"], @@ -106,6 +129,18 @@ "_In a year on %1$s_::_In %n years on %1$s_" : ["Za rok dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s","Za %n lat dnia %1$s"], "In the past on %1$s then on %2$s" : "W przeszłości dnia %1$s, a następnie %2$s", "_In a minute on %1$s then on %2$s_::_In %n minutes on %1$s then on %2$s_" : ["Za minutę dnia %1$s, a następnie %2$s","Za %n minuty dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s","Za %n minut dnia %1$s, a następnie %2$s"], + "_In a hour on %1$s then on %2$s_::_In %n hours on %1$s then on %2$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s","Za %n godziny dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s","Za %n godzin dnia %1$s, następnie dnia %2$s"], + "_In a day on %1$s then on %2$s_::_In %n days on %1$s then on %2$s_" : ["Jutro dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s","Za %n dni dnia %1$s, następnie dnia %2$s"], + "_In a week on %1$s then on %2$s_::_In %n weeks on %1$s then on %2$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s","Za %n tygodni dnia %1$s, następnie dnia %2$s"], + "_In a month on %1$s then on %2$s_::_In %n months on %1$s then on %2$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s","Za %n miesiące dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s"], + "_In a year on %1$s then on %2$s_::_In %n years on %1$s then on %2$s_" : ["Za rok dnia %1$s, następnie dnia %2$s","Za %n lata dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s","Za %n lat dnia %1$s, następnie dnia %2$s"], + "In the past on %1$s then on %2$s and %3$s" : "W przeszłości dnia %1$s, następnie dnia %2$s i %3$s", + "_In a minute on %1$s then on %2$s and %3$s_::_In %n minutes on %1$s then on %2$s and %3$s_" : ["Za minutę dnia %1$s, następnie dnia %2$s i %3$s","Za %n minuty dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s","Za %n minut dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a hour on %1$s then on %2$s and %3$s_::_In %n hours on %1$s then on %2$s and %3$s_" : ["Za godzinę dnia %1$s, następnie dnia %2$s i %3$s","Za %%ngodziny dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s","Za %n godzin dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a day on %1$s then on %2$s and %3$s_::_In %n days on %1$s then on %2$s and %3$s_" : ["Jutro, dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s","Za %n dni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a week on %1$s then on %2$s and %3$s_::_In %n weeks on %1$s then on %2$s and %3$s_" : ["Za tydzień dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodnie dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s","Za %n tygodni dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a month on %1$s then on %2$s and %3$s_::_In %n months on %1$s then on %2$s and %3$s_" : ["Za miesiąc dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesiące dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s","Za %n miesięcy dnia %1$s, następnie dnia %2$s i %3$s"], + "_In a year on %1$s then on %2$s and %3$s_::_In %n years on %1$s then on %2$s and %3$s_" : ["Za rok dnia %1$s, następnie dnia %2$s i %3$s","Za %n lata dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s","Za %n lat dnia %1$s, następnie dnia %2$s i %3$s"], "Could not generate next recurrence statement" : "Nie można wygenerować następnej instrukcji powtarzania", "Cancelled: %1$s" : "Anulowane: %1$s", "\"%1$s\" has been canceled" : "\"%1$s\" zostało anulowane", diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index dd3a4cf3f69..deb00caa93d 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -36,7 +36,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable public function __construct( BackendInterface $caldavBackend, - $calendarInfo, + array $calendarInfo, IL10N $l10n, private IConfig $config, private LoggerInterface $logger, @@ -60,6 +60,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable $this->l10n = $l10n; } + public function getUri(): string { + return $this->calendarInfo['uri']; + } + /** * {@inheritdoc} * @throws Forbidden diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php index 3cc4039ed36..a8b818e59aa 100644 --- a/apps/dav/lib/CalDAV/CalendarProvider.php +++ b/apps/dav/lib/CalDAV/CalendarProvider.php @@ -36,9 +36,14 @@ class CalendarProvider implements ICalendarProvider { }); } + $additionalProperties = $this->getAdditionalPropertiesForCalendars($calendarInfos); $iCalendars = []; foreach ($calendarInfos as $calendarInfo) { - $calendarInfo = array_merge($calendarInfo, $this->getAdditionalProperties($calendarInfo['principaluri'], $calendarInfo['uri'])); + $user = str_replace('principals/users/', '', $calendarInfo['principaluri']); + $path = 'calendars/' . $user . '/' . $calendarInfo['uri']; + + $calendarInfo = array_merge($calendarInfo, $additionalProperties[$path] ?? []); + $calendar = new Calendar($this->calDavBackend, $calendarInfo, $this->l10n, $this->config, $this->logger); $iCalendars[] = new CalendarImpl( $calendar, @@ -49,16 +54,34 @@ class CalendarProvider implements ICalendarProvider { return $iCalendars; } - public function getAdditionalProperties(string $principalUri, string $calendarUri): array { - $user = str_replace('principals/users/', '', $principalUri); - $path = 'calendars/' . $user . '/' . $calendarUri; + /** + * @param array{ + * principaluri: string, + * uri: string, + * }[] $uris + * @return array<string, array<string, string|bool>> + */ + private function getAdditionalPropertiesForCalendars(array $uris): array { + $calendars = []; + foreach ($uris as $uri) { + /** @var string $user */ + $user = str_replace('principals/users/', '', $uri['principaluri']); + if (!array_key_exists($user, $calendars)) { + $calendars[$user] = []; + } + $calendars[$user][] = 'calendars/' . $user . '/' . $uri['uri']; + } - $properties = $this->propertyMapper->findPropertiesByPath($user, $path); + $properties = $this->propertyMapper->findPropertiesByPathsAndUsers($calendars); $list = []; foreach ($properties as $property) { if ($property instanceof Property) { - $list[$property->getPropertyname()] = match ($property->getPropertyname()) { + if (!isset($list[$property->getPropertypath()])) { + $list[$property->getPropertypath()] = []; + } + + $list[$property->getPropertypath()][$property->getPropertyname()] = match ($property->getPropertyname()) { '{http://owncloud.org/ns}calendar-enabled' => (bool)$property->getPropertyvalue(), default => $property->getPropertyvalue() }; diff --git a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php index 21d8c06fa99..d9d6d840c5e 100644 --- a/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php +++ b/apps/dav/lib/CalDAV/EmbeddedCalDavServer.php @@ -20,6 +20,7 @@ use OCA\DAV\Connector\Sabre\DavAclPlugin; use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin; use OCA\DAV\Connector\Sabre\LockPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\RootCollection; use OCA\Theming\ThemingDefaults; @@ -96,6 +97,9 @@ class EmbeddedCalDavServer { $this->server->addPlugin(Server::get(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); } + // collection preload plugin + $this->server->addPlugin(new PropFindPreloadNotifyPlugin()); + // wait with registering these until auth is handled and the filesystem is setup $this->server->on('beforeMethod:*', function () use ($root): void { // register plugins from apps diff --git a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php index e4b6c2636da..ef9bd1ae472 100644 --- a/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/CommentPropertiesPlugin.php @@ -10,6 +10,7 @@ namespace OCA\DAV\Connector\Sabre; use OCP\Comments\ICommentsManager; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\ServerPlugin; @@ -21,6 +22,7 @@ class CommentPropertiesPlugin extends ServerPlugin { protected ?Server $server = null; private array $cachedUnreadCount = []; + private array $cachedDirectories = []; public function __construct( private ICommentsManager $commentsManager, @@ -41,6 +43,8 @@ class CommentPropertiesPlugin extends ServerPlugin { */ public function initialize(\Sabre\DAV\Server $server) { $this->server = $server; + + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); } @@ -69,6 +73,21 @@ class CommentPropertiesPlugin extends ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): + void { + if (!($collection instanceof Directory)) { + return; + } + + $collectionPath = $collection->getPath(); + if (!isset($this->cachedDirectories[$collectionPath]) && $propFind->getStatus( + self::PROPERTY_NAME_UNREAD + ) !== null) { + $this->cacheDirectory($collection); + $this->cachedDirectories[$collectionPath] = true; + } + } + /** * Adds tags and favorites properties to the response, * if requested. @@ -85,14 +104,6 @@ class CommentPropertiesPlugin extends ServerPlugin { return; } - // need prefetch ? - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && !is_null($propFind->getStatus(self::PROPERTY_NAME_UNREAD)) - ) { - $this->cacheDirectory($node); - } - $propFind->handle(self::PROPERTY_NAME_COUNT, function () use ($node): int { return $this->commentsManager->getNumberOfCommentsForObject('files', (string)$node->getId()); }); diff --git a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php index 130d4562146..38538fdcff0 100644 --- a/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php +++ b/apps/dav/lib/Connector/Sabre/PropFindMonitorPlugin.php @@ -48,30 +48,34 @@ class PropFindMonitorPlugin extends ServerPlugin { if (empty($pluginQueries)) { return; } - $maxDepth = max(0, ...array_keys($pluginQueries)); - // entries at the top are usually not interesting - unset($pluginQueries[$maxDepth]); $logger = $this->server->getLogger(); - foreach ($pluginQueries as $depth => $propFinds) { - foreach ($propFinds as $pluginName => $propFind) { - [ - 'queries' => $queries, - 'nodes' => $nodes - ] = $propFind; - if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES - || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { - continue; + foreach ($pluginQueries as $eventName => $eventQueries) { + $maxDepth = max(0, ...array_keys($eventQueries)); + // entries at the top are usually not interesting + unset($eventQueries[$maxDepth]); + foreach ($eventQueries as $depth => $propFinds) { + foreach ($propFinds as $pluginName => $propFind) { + [ + 'queries' => $queries, + 'nodes' => $nodes + ] = $propFind; + if ($queries === 0 || $nodes > $queries || $nodes < self::THRESHOLD_NODES + || $queries < $nodes * self::THRESHOLD_QUERY_FACTOR) { + continue; + } + $logger->error( + '{name}:{event} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', + [ + 'name' => $pluginName, + 'scans' => $nodes, + 'count' => $queries, + 'depth' => $depth, + 'maxDepth' => $maxDepth, + 'event' => $eventName, + ] + ); } - $logger->error( - '{name} scanned {scans} nodes with {count} queries in depth {depth}/{maxDepth}. This is bad for performance, please report to the plugin developer!', [ - 'name' => $pluginName, - 'scans' => $nodes, - 'count' => $queries, - 'depth' => $depth, - 'maxDepth' => $maxDepth, - ] - ); } } } diff --git a/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php b/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php new file mode 100644 index 00000000000..c7b0c64132c --- /dev/null +++ b/apps/dav/lib/Connector/Sabre/PropFindPreloadNotifyPlugin.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types = 1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Connector\Sabre; + +use Sabre\DAV\ICollection; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; + +/** + * This plugin asks other plugins to preload data for a collection, so that + * subsequent PROPFIND handlers for children do not query the DB on a per-node + * basis. + */ +class PropFindPreloadNotifyPlugin extends ServerPlugin { + + private Server $server; + + public function initialize(Server $server): void { + $this->server = $server; + $this->server->on('propFind', [$this, 'collectionPreloadNotifier' ], 1); + } + + /** + * Uses the server instance to emit a `preloadCollection` event to signal + * to interested plugins that a collection can be preloaded. + * + * NOTE: this can be emitted several times, so ideally every plugin + * should cache what they need and check if a cache exists before + * re-fetching. + */ + public function collectionPreloadNotifier(PropFind $propFind, INode $node): bool { + if (!$this->shouldPreload($propFind, $node)) { + return true; + } + + return $this->server->emit('preloadCollection', [$propFind, $node]); + } + + private function shouldPreload( + PropFind $propFind, + INode $node, + ): bool { + $depth = $propFind->getDepth(); + return $node instanceof ICollection + && ($depth === Server::DEPTH_INFINITY || $depth > 0); + } +} diff --git a/apps/dav/lib/Connector/Sabre/Server.php b/apps/dav/lib/Connector/Sabre/Server.php index dda9c29b763..eef65000131 100644 --- a/apps/dav/lib/Connector/Sabre/Server.php +++ b/apps/dav/lib/Connector/Sabre/Server.php @@ -27,7 +27,8 @@ class Server extends \Sabre\DAV\Server { /** * Tracks queries done by plugins. - * @var array<int, array<string, array{nodes:int, queries:int}>> + * @var array<string, array<int, array<string, array{nodes:int, + * queries:int}>>> The keys represent: event name, depth and plugin name */ private array $pluginQueries = []; @@ -50,8 +51,8 @@ class Server extends \Sabre\DAV\Server { ): void { $this->debugEnabled ? $this->monitorPropfindQueries( parent::once(...), - ...func_get_args(), - ) : parent::once(...func_get_args()); + ...\func_get_args(), + ) : parent::once(...\func_get_args()); } #[Override] @@ -62,8 +63,8 @@ class Server extends \Sabre\DAV\Server { ): void { $this->debugEnabled ? $this->monitorPropfindQueries( parent::on(...), - ...func_get_args(), - ) : parent::on(...func_get_args()); + ...\func_get_args(), + ) : parent::on(...\func_get_args()); } /** @@ -76,13 +77,17 @@ class Server extends \Sabre\DAV\Server { callable $callBack, int $priority = 100, ): void { - if ($eventName !== 'propFind') { + $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown'; + // The NotifyPlugin needs to be excluded as it emits the + // `preloadCollection` event, which causes many plugins run queries. + /** @psalm-suppress TypeDoesNotContainType */ + if ($pluginName === PropFindPreloadNotifyPlugin::class || ($eventName !== 'propFind' + && $eventName !== 'preloadCollection')) { $parentFn($eventName, $callBack, $priority); return; } - $pluginName = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2]['class'] ?? 'unknown'; - $callback = $this->getMonitoredCallback($callBack, $pluginName); + $callback = $this->getMonitoredCallback($callBack, $pluginName, $eventName); $parentFn($eventName, $callback, $priority); } @@ -94,22 +99,26 @@ class Server extends \Sabre\DAV\Server { private function getMonitoredCallback( callable $callBack, string $pluginName, + string $eventName, ): callable { return function (PropFind $propFind, INode $node) use ( $callBack, $pluginName, - ) { + $eventName, + ): bool { $connection = \OCP\Server::get(Connection::class); $queriesBefore = $connection->getStats()['executed']; $result = $callBack($propFind, $node); $queriesAfter = $connection->getStats()['executed']; $this->trackPluginQueries( $pluginName, + $eventName, $queriesAfter - $queriesBefore, $propFind->getDepth() ); - return $result; + // many callbacks don't care about returning a bool + return $result ?? true; }; } @@ -118,6 +127,7 @@ class Server extends \Sabre\DAV\Server { */ private function trackPluginQueries( string $pluginName, + string $eventName, int $queriesExecuted, int $depth, ): void { @@ -126,11 +136,11 @@ class Server extends \Sabre\DAV\Server { return; } - $this->pluginQueries[$depth][$pluginName]['nodes'] - = ($this->pluginQueries[$depth][$pluginName]['nodes'] ?? 0) + 1; + $this->pluginQueries[$eventName][$depth][$pluginName]['nodes'] + = ($this->pluginQueries[$eventName][$depth][$pluginName]['nodes'] ?? 0) + 1; - $this->pluginQueries[$depth][$pluginName]['queries'] - = ($this->pluginQueries[$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted; + $this->pluginQueries[$eventName][$depth][$pluginName]['queries'] + = ($this->pluginQueries[$eventName][$depth][$pluginName]['queries'] ?? 0) + $queriesExecuted; } /** @@ -221,8 +231,8 @@ class Server extends \Sabre\DAV\Server { /** * Returns queries executed by registered plugins. - * - * @return array<int, array<string, array{nodes:int, queries:int}>> + * @return array<string, array<int, array<string, array{nodes:int, + * queries:int}>>> The keys represent: event name, depth and plugin name */ public function getPluginQueries(): array { return $this->pluginQueries; diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index a6a27057177..1b4de841ec6 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -14,6 +14,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\ViewOnlyPlugin; +use OCA\DAV\Db\PropertyMapper; use OCA\DAV\Files\BrowserErrorPagePlugin; use OCA\DAV\Files\Sharing\RootCollection; use OCA\DAV\Upload\CleanupService; @@ -94,6 +95,8 @@ class ServerFactory { $server->debugEnabled = $debugEnabled; $server->addPlugin(new PropFindMonitorPlugin()); } + + $server->addPlugin(new PropFindPreloadNotifyPlugin()); // FIXME: The following line is a workaround for legacy components relying on being able to send a GET to / $server->addPlugin(new DummyGetResponsePlugin()); $server->addPlugin(new ExceptionLoggerPlugin('webdav', $this->logger)); @@ -226,6 +229,7 @@ class ServerFactory { $tree, $this->databaseConnection, $this->userSession->getUser(), + \OCP\Server::get(PropertyMapper::class), \OCP\Server::get(DefaultCalendarValidator::class), ) ) diff --git a/apps/dav/lib/Connector/Sabre/SharesPlugin.php b/apps/dav/lib/Connector/Sabre/SharesPlugin.php index f49e85333f3..11e50362dc2 100644 --- a/apps/dav/lib/Connector/Sabre/SharesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/SharesPlugin.php @@ -15,6 +15,7 @@ use OCP\Files\NotFoundException; use OCP\IUserSession; use OCP\Share\IManager; use OCP\Share\IShare; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\Server; use Sabre\DAV\Tree; @@ -38,7 +39,14 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { /** @var IShare[][] */ private array $cachedShares = []; - /** @var string[] */ + + /** + * Tracks which folders have been cached. + * When a folder is cached, it will appear with its path as key and true + * as value. + * + * @var bool[] + */ private array $cachedFolders = []; public function __construct( @@ -67,6 +75,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { $server->protectedProperties[] = self::SHAREES_PROPERTYNAME; $this->server = $server; + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); } @@ -89,28 +98,28 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { ]; foreach ($requestedShareTypes as $requestedShareType) { - $result = array_merge($result, $this->shareManager->getSharesBy( + $result[] = $this->shareManager->getSharesBy( $this->userId, $requestedShareType, $node, false, -1 - )); + ); // Also check for shares where the user is the recipient try { - $result = array_merge($result, $this->shareManager->getSharedWith( + $result[] = $this->shareManager->getSharedWith( $this->userId, $requestedShareType, $node, -1 - )); + ); } catch (BackendError $e) { // ignore } } - return $result; + return array_merge(...$result); } /** @@ -141,7 +150,7 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { // if we already cached the folder containing this file // then we already know there are no shares here. - if (array_search($parentPath, $this->cachedFolders) === false) { + if (!isset($this->cachedFolders[$parentPath])) { try { $node = $sabreNode->getNode(); } catch (NotFoundException $e) { @@ -156,6 +165,27 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { return []; } + private function preloadCollection(PropFind $propFind, ICollection $collection): void { + if (!$collection instanceof Directory + || isset($this->cachedFolders[$collection->getPath()]) + || ( + $propFind->getStatus(self::SHARETYPES_PROPERTYNAME) === null + && $propFind->getStatus(self::SHAREES_PROPERTYNAME) === null + ) + ) { + return; + } + + // If the node is a directory and we are requesting share types or sharees + // then we get all the shares in the folder and cache them. + // This is more performant than iterating each files afterwards. + $folderNode = $collection->getNode(); + $this->cachedFolders[$collection->getPath()] = true; + foreach ($this->getSharesFolder($folderNode) as $id => $shares) { + $this->cachedShares[$id] = $shares; + } + } + /** * Adds shares to propfind response * @@ -170,24 +200,6 @@ class SharesPlugin extends \Sabre\DAV\ServerPlugin { return; } - // If the node is a directory and we are requesting share types or sharees - // then we get all the shares in the folder and cache them. - // This is more performant than iterating each files afterwards. - if ($sabreNode instanceof Directory - && $propFind->getDepth() !== 0 - && ( - !is_null($propFind->getStatus(self::SHARETYPES_PROPERTYNAME)) - || !is_null($propFind->getStatus(self::SHAREES_PROPERTYNAME)) - ) - ) { - $folderNode = $sabreNode->getNode(); - $this->cachedFolders[] = $sabreNode->getPath(); - $childShares = $this->getSharesFolder($folderNode); - foreach ($childShares as $id => $shares) { - $this->cachedShares[$id] = $shares; - } - } - $propFind->handle(self::SHARETYPES_PROPERTYNAME, function () use ($sabreNode): ShareTypeList { $shares = $this->getShares($sabreNode); diff --git a/apps/dav/lib/Connector/Sabre/TagsPlugin.php b/apps/dav/lib/Connector/Sabre/TagsPlugin.php index 25c1633df36..ec3e6fc5320 100644 --- a/apps/dav/lib/Connector/Sabre/TagsPlugin.php +++ b/apps/dav/lib/Connector/Sabre/TagsPlugin.php @@ -31,6 +31,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\ITagManager; use OCP\ITags; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -61,6 +62,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { * @var array */ private $cachedTags; + private array $cachedDirectories; /** * @param \Sabre\DAV\Tree $tree tree @@ -92,6 +94,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { $server->xml->elementMap[self::TAGS_PROPERTYNAME] = TagList::class; $this->server = $server; + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'handleGetProperties']); $this->server->on('propPatch', [$this, 'handleUpdateProperties']); $this->server->on('preloadProperties', [$this, 'handlePreloadProperties']); @@ -194,6 +197,29 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): + void { + if (!($collection instanceof Node)) { + return; + } + + // need prefetch ? + if ($collection instanceof Directory + && !isset($this->cachedDirectories[$collection->getPath()]) + && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) + || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) + )) { + // note: pre-fetching only supported for depth <= 1 + $folderContent = $collection->getChildren(); + $fileIds = [(int)$collection->getId()]; + foreach ($folderContent as $info) { + $fileIds[] = (int)$info->getId(); + } + $this->prefetchTagsForFileIds($fileIds); + $this->cachedDirectories[$collection->getPath()] = true; + } + } + /** * Adds tags and favorites properties to the response, * if requested. @@ -210,21 +236,6 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin { return; } - // need prefetch ? - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && (!is_null($propFind->getStatus(self::TAGS_PROPERTYNAME)) - || !is_null($propFind->getStatus(self::FAVORITE_PROPERTYNAME)) - )) { - // note: pre-fetching only supported for depth <= 1 - $folderContent = $node->getChildren(); - $fileIds = [(int)$node->getId()]; - foreach ($folderContent as $info) { - $fileIds[] = (int)$info->getId(); - } - $this->prefetchTagsForFileIds($fileIds); - } - $isFav = null; $propFind->handle(self::TAGS_PROPERTYNAME, function () use (&$isFav, $node) { diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index f9a4f8ee986..be7345f25df 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -9,13 +9,20 @@ namespace OCA\DAV\DAV; use Exception; +use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\CalendarHome; use OCA\DAV\CalDAV\CalendarObject; use OCA\DAV\CalDAV\DefaultCalendarValidator; +use OCA\DAV\CalDAV\Integration\ExternalCalendar; +use OCA\DAV\CalDAV\Outbox; +use OCA\DAV\CalDAV\Trashbin\TrashbinHome; use OCA\DAV\Connector\Sabre\Directory; +use OCA\DAV\Db\PropertyMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; +use Sabre\CalDAV\Schedule\Inbox; use Sabre\DAV\Exception as DavException; use Sabre\DAV\PropertyStorage\Backend\BackendInterface; use Sabre\DAV\PropFind; @@ -97,11 +104,17 @@ class CustomPropertiesBackend implements BackendInterface { ]; /** + * Map of well-known property names to default values + */ + private const PROPERTY_DEFAULT_VALUES = [ + '{http://owncloud.org/ns}calendar-enabled' => '1', + ]; + + /** * Properties cache - * - * @var array */ - private $userCache = []; + private array $userCache = []; + private array $publishedCache = []; private XmlService $xmlService; /** @@ -114,6 +127,7 @@ class CustomPropertiesBackend implements BackendInterface { private Tree $tree, private IDBConnection $connection, private IUser $user, + private PropertyMapper $propertyMapper, private DefaultCalendarValidator $defaultCalendarValidator, ) { $this->xmlService = new XmlService(); @@ -197,6 +211,13 @@ class CustomPropertiesBackend implements BackendInterface { $this->cacheDirectory($path, $node); } + if ($node instanceof CalendarHome && $propFind->getDepth() !== 0) { + $backend = $node->getCalDAVBackend(); + if ($backend instanceof CalDavBackend) { + $this->cacheCalendars($node, $requestedProps); + } + } + if ($node instanceof CalendarObject) { // No custom properties supported on individual events return; @@ -316,6 +337,10 @@ class CustomPropertiesBackend implements BackendInterface { return []; } + if (isset($this->publishedCache[$path])) { + return $this->publishedCache[$path]; + } + $qb = $this->connection->getQueryBuilder(); $qb->select('*') ->from(self::TABLE_NAME) @@ -326,6 +351,7 @@ class CustomPropertiesBackend implements BackendInterface { $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']); } $result->closeCursor(); + $this->publishedCache[$path] = $props; return $props; } @@ -364,6 +390,62 @@ class CustomPropertiesBackend implements BackendInterface { $this->userCache = array_merge($this->userCache, $propsByPath); } + private function cacheCalendars(CalendarHome $node, array $requestedProperties): void { + $calendars = $node->getChildren(); + + $users = []; + foreach ($calendars as $calendar) { + if ($calendar instanceof Calendar) { + $user = str_replace('principals/users/', '', $calendar->getPrincipalURI()); + if (!isset($users[$user])) { + $users[$user] = ['calendars/' . $user]; + } + $users[$user][] = 'calendars/' . $user . '/' . $calendar->getUri(); + } elseif ($calendar instanceof Inbox || $calendar instanceof Outbox || $calendar instanceof TrashbinHome || $calendar instanceof ExternalCalendar) { + if ($calendar->getOwner()) { + $user = str_replace('principals/users/', '', $calendar->getOwner()); + if (!isset($users[$user])) { + $users[$user] = ['calendars/' . $user]; + } + $users[$user][] = 'calendars/' . $user . '/' . $calendar->getName(); + } + } + } + + // user properties + $properties = $this->propertyMapper->findPropertiesByPathsAndUsers($users); + + $propsByPath = []; + foreach ($users as $paths) { + foreach ($paths as $path) { + $propsByPath[$path] = []; + } + } + + foreach ($properties as $property) { + $propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype()); + } + $this->userCache = array_merge($this->userCache, $propsByPath); + + // published properties + $allowedProps = array_intersect(self::PUBLISHED_READ_ONLY_PROPERTIES, $requestedProperties); + if (empty($allowedProps)) { + return; + } + $paths = []; + foreach ($users as $nestedPaths) { + $paths = array_merge($paths, $nestedPaths); + } + $paths = array_unique($paths); + + $propsByPath = array_fill_keys(array_values($paths), []); + $properties = $this->propertyMapper->findPropertiesByPaths($paths, $allowedProps); + foreach ($properties as $property) { + $propsByPath[$property->getPropertypath()][$property->getPropertyname()] = $this->decodeValueFromDatabase($property->getPropertyvalue(), $property->getValuetype()); + } + $this->publishedCache = array_merge($this->publishedCache, $propsByPath); + } + /** * Returns a list of properties for the given path and current user * @@ -410,6 +492,14 @@ class CustomPropertiesBackend implements BackendInterface { return $props; } + private function isPropertyDefaultValue(string $name, mixed $value): bool { + if (!isset(self::PROPERTY_DEFAULT_VALUES[$name])) { + return false; + } + + return self::PROPERTY_DEFAULT_VALUES[$name] === $value; + } + /** * @throws Exception */ @@ -426,8 +516,8 @@ class CustomPropertiesBackend implements BackendInterface { 'propertyName' => $propertyName, ]; - // If it was null, we need to delete the property - if (is_null($propertyValue)) { + // If it was null or set to the default value, we need to delete the property + if (is_null($propertyValue) || $this->isPropertyDefaultValue($propertyName, $propertyValue)) { if (array_key_exists($propertyName, $existing)) { $deleteQuery = $deleteQuery ?? $this->createDeleteQuery(); $deleteQuery diff --git a/apps/dav/lib/DAV/Sharing/Plugin.php b/apps/dav/lib/DAV/Sharing/Plugin.php index 03e63813bab..82b000bc8ce 100644 --- a/apps/dav/lib/DAV/Sharing/Plugin.php +++ b/apps/dav/lib/DAV/Sharing/Plugin.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Http; use OCP\IConfig; use OCP\IRequest; use Sabre\DAV\Exception\NotFound; +use Sabre\DAV\ICollection; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -89,6 +90,7 @@ class Plugin extends ServerPlugin { $this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = Invite::class; $this->server->on('method:POST', [$this, 'httpPost']); + $this->server->on('preloadCollection', $this->preloadCollection(...)); $this->server->on('propFind', [$this, 'propFind']); } @@ -168,6 +170,24 @@ class Plugin extends ServerPlugin { } } + private function preloadCollection(PropFind $propFind, ICollection $collection): void { + if (!$collection instanceof CalendarHome || $propFind->getDepth() !== 1) { + return; + } + + $backend = $collection->getCalDAVBackend(); + if (!$backend instanceof CalDavBackend) { + return; + } + + $calendars = $collection->getChildren(); + $calendars = array_filter($calendars, static fn (INode $node) => $node instanceof IShareable); + /** @var int[] $resourceIds */ + $resourceIds = array_map( + static fn (IShareable $node) => $node->getResourceId(), $calendars); + $backend->preloadShares($resourceIds); + } + /** * This event is triggered when properties are requested for a certain * node. @@ -179,20 +199,6 @@ class Plugin extends ServerPlugin { * @return void */ public function propFind(PropFind $propFind, INode $node) { - if ($node instanceof CalendarHome && $propFind->getDepth() === 1) { - $backend = $node->getCalDAVBackend(); - if ($backend instanceof CalDavBackend) { - $calendars = $node->getChildren(); - $calendars = array_filter($calendars, function (INode $node) { - return $node instanceof IShareable; - }); - /** @var int[] $resourceIds */ - $resourceIds = array_map(function (IShareable $node) { - return $node->getResourceId(); - }, $calendars); - $backend->preloadShares($resourceIds); - } - } if ($node instanceof IShareable) { $propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) { return new Invite( diff --git a/apps/dav/lib/Db/Property.php b/apps/dav/lib/Db/Property.php index 96c5f75ef4f..6c1e249ac47 100644 --- a/apps/dav/lib/Db/Property.php +++ b/apps/dav/lib/Db/Property.php @@ -16,6 +16,7 @@ use OCP\AppFramework\Db\Entity; * @method string getPropertypath() * @method string getPropertyname() * @method string getPropertyvalue() + * @method int getValuetype() */ class Property extends Entity { diff --git a/apps/dav/lib/Db/PropertyMapper.php b/apps/dav/lib/Db/PropertyMapper.php index 1789194ee7a..a3dbdaa7d98 100644 --- a/apps/dav/lib/Db/PropertyMapper.php +++ b/apps/dav/lib/Db/PropertyMapper.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace OCA\DAV\Db; use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; /** @@ -39,17 +40,43 @@ class PropertyMapper extends QBMapper { } /** + * @param array<string, string[]> $calendars * @return Property[] + * @throws \OCP\DB\Exception */ - public function findPropertiesByPath(string $userId, string $path): array { + public function findPropertiesByPathsAndUsers(array $calendars): array { $selectQb = $this->db->getQueryBuilder(); $selectQb->select('*') - ->from(self::TABLE_NAME) - ->where( - $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($userId)), - $selectQb->expr()->eq('propertypath', $selectQb->createNamedParameter($path)), + ->from(self::TABLE_NAME); + + foreach ($calendars as $user => $paths) { + $selectQb->orWhere( + $selectQb->expr()->andX( + $selectQb->expr()->eq('userid', $selectQb->createNamedParameter($user)), + $selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($paths, IQueryBuilder::PARAM_STR_ARRAY)), + ) ); + } + return $this->findEntities($selectQb); } + /** + * @param string[] $calendars + * @param string[] $allowedProperties + * @return Property[] + * @throws \OCP\DB\Exception + */ + public function findPropertiesByPaths(array $calendars, array $allowedProperties = []): array { + $selectQb = $this->db->getQueryBuilder(); + $selectQb->select('*') + ->from(self::TABLE_NAME) + ->where($selectQb->expr()->in('propertypath', $selectQb->createNamedParameter($calendars, IQueryBuilder::PARAM_STR_ARRAY))); + + if ($allowedProperties) { + $selectQb->andWhere($selectQb->expr()->in('propertyname', $selectQb->createNamedParameter($allowedProperties, IQueryBuilder::PARAM_STR_ARRAY))); + } + + return $this->findEntities($selectQb); + } } diff --git a/apps/dav/lib/Migration/Version1034Date20250813093701.php b/apps/dav/lib/Migration/Version1034Date20250813093701.php new file mode 100644 index 00000000000..10be71f067b --- /dev/null +++ b/apps/dav/lib/Migration/Version1034Date20250813093701.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\DAV\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; +use Override; + +class Version1034Date20250813093701 extends SimpleMigrationStep { + public function __construct( + private IDBConnection $db, + ) { + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + #[Override] + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete('properties') + ->where($qb->expr()->eq( + 'propertyname', + $qb->createNamedParameter( + '{http://owncloud.org/ns}calendar-enabled', + IQueryBuilder::PARAM_STR, + ), + IQueryBuilder::PARAM_STR, + )) + ->andWhere($qb->expr()->eq( + 'propertyvalue', + $qb->createNamedParameter( + '1', + IQueryBuilder::PARAM_STR, + ), + IQueryBuilder::PARAM_STR, + )) + ->executeStatement(); + } +} diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index a92e162f1b0..9b4a1b3d33c 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -46,6 +46,7 @@ use OCA\DAV\Connector\Sabre\LockPlugin; use OCA\DAV\Connector\Sabre\MaintenancePlugin; use OCA\DAV\Connector\Sabre\PropfindCompressionPlugin; use OCA\DAV\Connector\Sabre\PropFindMonitorPlugin; +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; use OCA\DAV\Connector\Sabre\QuotaPlugin; use OCA\DAV\Connector\Sabre\RequestIdHeaderPlugin; use OCA\DAV\Connector\Sabre\SharesPlugin; @@ -54,6 +55,7 @@ use OCA\DAV\Connector\Sabre\ZipFolderPlugin; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\PublicAuth; use OCA\DAV\DAV\ViewOnlyPlugin; +use OCA\DAV\Db\PropertyMapper; use OCA\DAV\Events\SabrePluginAddEvent; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\Files\BrowserErrorPagePlugin; @@ -237,6 +239,7 @@ class Server { \OCP\Server::get(IUserSession::class) )); + // performance improvement plugins $this->server->addPlugin(new CopyEtagHeaderPlugin()); $this->server->addPlugin(new RequestIdHeaderPlugin(\OCP\Server::get(IRequest::class))); $this->server->addPlugin(new UploadAutoMkcolPlugin()); @@ -248,6 +251,7 @@ class Server { $eventDispatcher, )); $this->server->addPlugin(\OCP\Server::get(PaginatePlugin::class)); + $this->server->addPlugin(new PropFindPreloadNotifyPlugin()); // allow setup of additional plugins $eventDispatcher->dispatch('OCA\DAV\Connector\Sabre::addPlugin', $event); @@ -306,6 +310,7 @@ class Server { $this->server->tree, \OCP\Server::get(IDBConnection::class), \OCP\Server::get(IUserSession::class)->getUser(), + \OCP\Server::get(PropertyMapper::class), \OCP\Server::get(DefaultCalendarValidator::class), ) ) diff --git a/apps/dav/lib/SystemTag/SystemTagPlugin.php b/apps/dav/lib/SystemTag/SystemTagPlugin.php index 4d4499c7559..6be3e8bd1a2 100644 --- a/apps/dav/lib/SystemTag/SystemTagPlugin.php +++ b/apps/dav/lib/SystemTag/SystemTagPlugin.php @@ -27,6 +27,7 @@ use Sabre\DAV\Exception\BadRequest; use Sabre\DAV\Exception\Conflict; use Sabre\DAV\Exception\Forbidden; use Sabre\DAV\Exception\UnsupportedMediaType; +use Sabre\DAV\ICollection; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; use Sabre\HTTP\RequestInterface; @@ -94,6 +95,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { $server->protectedProperties[] = self::ID_PROPERTYNAME; + $server->on('preloadCollection', $this->preloadCollection(...)); $server->on('propFind', [$this, 'handleGetProperties']); $server->on('propPatch', [$this, 'handleUpdateProperties']); $server->on('method:POST', [$this, 'httpPost']); @@ -199,6 +201,40 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { } } + private function preloadCollection( + PropFind $propFind, + ICollection $collection, + ): void { + if (!$collection instanceof Node) { + return; + } + + if ($collection instanceof Directory + && !isset($this->cachedTagMappings[$collection->getId()]) + && $propFind->getStatus( + self::SYSTEM_TAGS_PROPERTYNAME + ) !== null) { + $fileIds = [$collection->getId()]; + + // note: pre-fetching only supported for depth <= 1 + $folderContent = $collection->getChildren(); + foreach ($folderContent as $info) { + if ($info instanceof Node) { + $fileIds[] = $info->getId(); + } + } + + $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files'); + + $this->cachedTagMappings += $tags; + $emptyFileIds = array_diff($fileIds, array_keys($tags)); + + // also cache the ones that were not found + foreach ($emptyFileIds as $fileId) { + $this->cachedTagMappings[$fileId] = []; + } + } + } /** * Retrieves system tag properties @@ -297,29 +333,6 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin { } private function propfindForFile(PropFind $propFind, Node $node): void { - if ($node instanceof Directory - && $propFind->getDepth() !== 0 - && !is_null($propFind->getStatus(self::SYSTEM_TAGS_PROPERTYNAME))) { - $fileIds = [$node->getId()]; - - // note: pre-fetching only supported for depth <= 1 - $folderContent = $node->getChildren(); - foreach ($folderContent as $info) { - if ($info instanceof Node) { - $fileIds[] = $info->getId(); - } - } - - $tags = $this->tagMapper->getTagIdsForObjects($fileIds, 'files'); - - $this->cachedTagMappings = $this->cachedTagMappings + $tags; - $emptyFileIds = array_diff($fileIds, array_keys($tags)); - - // also cache the ones that were not found - foreach ($emptyFileIds as $fileId) { - $this->cachedTagMappings[$fileId] = []; - } - } $propFind->handle(self::SYSTEM_TAGS_PROPERTYNAME, function () use ($node) { $user = $this->userSession->getUser(); diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php index d4021a66299..cafbdd3ca40 100644 --- a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php @@ -12,6 +12,7 @@ use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\File; use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; use OCP\IDBConnection; use OCP\IUser; use OCP\Server; @@ -52,6 +53,7 @@ class CustomPropertiesBackendTest extends \Test\TestCase { $this->tree, Server::get(IDBConnection::class), $this->user, + Server::get(PropertyMapper::class), $this->defaultCalendarValidator, ); } diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php index b528c3d731c..9d22befa201 100644 --- a/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindMonitorPluginTest.php @@ -29,66 +29,76 @@ class PropFindMonitorPluginTest extends TestCase { 'No queries logged' => [[], 0], 'Plugins with queries in less than threshold nodes should not be logged' => [ [ - [ - 'PluginName' => ['queries' => 100, 'nodes' - => PropFindMonitorPlugin::THRESHOLD_NODES - 1] - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => 100, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1] + ], + [], + ] ], 0 ], 'Plugins with query-to-node ratio less than threshold should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger - 1, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger - 1, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES ], + ], + [], + ] ], 0 ], 'Plugins with more nodes scanned than queries executed should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], - ], - [], + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES * 2], + ], + [],] ], 0 ], 'Plugins with queries only in highest depth level should not be logged' => [ [ - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 - ] - ], - [ - 'PluginName' => [ - 'queries' => $minQueriesTrigger * 2, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - ] + 'propFind' => [ + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES - 1 + ] + ], + [ + 'PluginName' => [ + 'queries' => $minQueriesTrigger * 2, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES + ] + ], ] ], 0 ], 'Plugins with too many queries should be logged' => [ [ - [ - 'FirstPlugin' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + 'propFind' => [ + [ + 'FirstPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ], + 'SecondPlugin' => [ + 'queries' => $minQueriesTrigger, + 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, + ] ], - 'SecondPlugin' => [ - 'queries' => $minQueriesTrigger, - 'nodes' => PropFindMonitorPlugin::THRESHOLD_NODES, - ] - ], - [] + [], + ] ], 2 ] diff --git a/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php new file mode 100644 index 00000000000..52fe3eba5bf --- /dev/null +++ b/apps/dav/tests/unit/Connector/Sabre/PropFindPreloadNotifyPluginTest.php @@ -0,0 +1,92 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\DAV\Tests\unit\Connector\Sabre; + +use OCA\DAV\Connector\Sabre\PropFindPreloadNotifyPlugin; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\MockObject\MockObject; +use Sabre\DAV\ICollection; +use Sabre\DAV\IFile; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Test\TestCase; + +class PropFindPreloadNotifyPluginTest extends TestCase { + + private Server&MockObject $server; + private PropFindPreloadNotifyPlugin $plugin; + + protected function setUp(): void { + parent::setUp(); + + $this->server = $this->createMock(Server::class); + $this->plugin = new PropFindPreloadNotifyPlugin(); + } + + public function testInitialize(): void { + $this->server + ->expects(self::once()) + ->method('on') + ->with('propFind', + $this->anything(), 1); + $this->plugin->initialize($this->server); + } + + public static function dataTestCollectionPreloadNotifier(): array { + return [ + 'When node is not a collection, should not emit' => [ + IFile::class, + 1, + false, + true + ], + 'When node is a collection but depth is zero, should not emit' => [ + ICollection::class, + 0, + false, + true + ], + 'When node is a collection, and depth > 0, should emit' => [ + ICollection::class, + 1, + true, + true + ], + 'When node is a collection, and depth is infinite, should emit' + => [ + ICollection::class, + Server::DEPTH_INFINITY, + true, + true + ], + 'When called called handler returns false, it should be returned' + => [ + ICollection::class, + 1, + true, + false + ] + ]; + } + + #[DataProvider(methodName: 'dataTestCollectionPreloadNotifier')] + public function testCollectionPreloadNotifier(string $nodeType, int $depth, bool $shouldEmit, bool $emitReturns): + void { + $this->plugin->initialize($this->server); + $propFind = $this->createMock(PropFind::class); + $propFind->expects(self::any())->method('getDepth')->willReturn($depth); + $node = $this->createMock($nodeType); + + $expectation = $shouldEmit ? self::once() : self::never(); + $this->server->expects($expectation)->method('emit')->with('preloadCollection', + [$propFind, $node])->willReturn($emitReturns); + $return = $this->plugin->collectionPreloadNotifier($propFind, $node); + $this->assertEquals($emitReturns, $return); + } +} diff --git a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php index 1c8e29dab38..33f579eb913 100644 --- a/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/SharesPluginTest.php @@ -223,6 +223,7 @@ class SharesPluginTest extends \Test\TestCase { 0 ); + $this->server->emit('preloadCollection', [$propFindRoot, $sabreNode]); $this->plugin->handleGetProperties( $propFindRoot, $sabreNode diff --git a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php index 5003280bfdc..554a4a1424e 100644 --- a/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/TagsPluginTest.php @@ -147,6 +147,8 @@ class TagsPluginTest extends \Test\TestCase { 0 ); + $this->server->emit('preloadCollection', [$propFindRoot, $node]); + $this->plugin->handleGetProperties( $propFindRoot, $node diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php index 2a85c0cbecd..517969fc9a3 100644 --- a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php @@ -10,6 +10,7 @@ namespace OCA\DAV\Tests\unit\DAV; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\DAV\CustomPropertiesBackend; +use OCA\DAV\Db\PropertyMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; @@ -36,6 +37,7 @@ class CustomPropertiesBackendTest extends TestCase { private IUser&MockObject $user; private DefaultCalendarValidator&MockObject $defaultCalendarValidator; private CustomPropertiesBackend $backend; + private PropertyMapper $propertyMapper; protected function setUp(): void { parent::setUp(); @@ -49,6 +51,7 @@ class CustomPropertiesBackendTest extends TestCase { ->with() ->willReturn('dummy_user_42'); $this->dbConnection = \OCP\Server::get(IDBConnection::class); + $this->propertyMapper = \OCP\Server::get(PropertyMapper::class); $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); $this->backend = new CustomPropertiesBackend( @@ -56,6 +59,7 @@ class CustomPropertiesBackendTest extends TestCase { $this->tree, $this->dbConnection, $this->user, + $this->propertyMapper, $this->defaultCalendarValidator, ); } @@ -129,6 +133,7 @@ class CustomPropertiesBackendTest extends TestCase { $this->tree, $db, $this->user, + $this->propertyMapper, $this->defaultCalendarValidator, ); diff --git a/apps/federatedfilesharing/l10n/pt_BR.js b/apps/federatedfilesharing/l10n/pt_BR.js index b6af476dffe..4d09230d39e 100644 --- a/apps/federatedfilesharing/l10n/pt_BR.js +++ b/apps/federatedfilesharing/l10n/pt_BR.js @@ -47,6 +47,7 @@ OC.L10N.register( "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Compartilhe comigo por meio do meu ID de Nuvem Federada #Nextcloud, consulte {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartilhe comigo através do meu ID de Nuvem Federada #Nextcloud", "Share with me via Nextcloud" : "Compartilhe comigo via Nextcloud", + "Cloud ID copied" : "ID da nuvem copiada", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Área de transferência indisponível. Copie o ID de Nuvem manualmente.", "Copied!" : "Copiado!", diff --git a/apps/federatedfilesharing/l10n/pt_BR.json b/apps/federatedfilesharing/l10n/pt_BR.json index b668cdc5513..7d7c89d5329 100644 --- a/apps/federatedfilesharing/l10n/pt_BR.json +++ b/apps/federatedfilesharing/l10n/pt_BR.json @@ -45,6 +45,7 @@ "Share with me through my #Nextcloud Federated Cloud ID, see {url}" : "Compartilhe comigo por meio do meu ID de Nuvem Federada #Nextcloud, consulte {url}", "Share with me through my #Nextcloud Federated Cloud ID" : "Compartilhe comigo através do meu ID de Nuvem Federada #Nextcloud", "Share with me via Nextcloud" : "Compartilhe comigo via Nextcloud", + "Cloud ID copied" : "ID da nuvem copiada", "Copy" : "Copiar", "Clipboard not available. Please copy the cloud ID manually." : "Área de transferência indisponível. Copie o ID de Nuvem manualmente.", "Copied!" : "Copiado!", diff --git a/apps/federatedfilesharing/l10n/sr.js b/apps/federatedfilesharing/l10n/sr.js index d64d4e59d19..2200c42b9ee 100644 --- a/apps/federatedfilesharing/l10n/sr.js +++ b/apps/federatedfilesharing/l10n/sr.js @@ -58,6 +58,7 @@ OC.L10N.register( "X (formerly Twitter)" : "X (бивши Twitter)", "formerly Twitter" : "бивши Twitter", "Mastodon" : "Mastodon", + "Bluesky" : "Bluesky", "Add to your website" : "Додај на свој веб сајт", "HTML Code:" : "ХТМЛ кôд:", "Cancel" : "Одустани", diff --git a/apps/federatedfilesharing/l10n/sr.json b/apps/federatedfilesharing/l10n/sr.json index c8816b63785..85e3a9ca73f 100644 --- a/apps/federatedfilesharing/l10n/sr.json +++ b/apps/federatedfilesharing/l10n/sr.json @@ -56,6 +56,7 @@ "X (formerly Twitter)" : "X (бивши Twitter)", "formerly Twitter" : "бивши Twitter", "Mastodon" : "Mastodon", + "Bluesky" : "Bluesky", "Add to your website" : "Додај на свој веб сајт", "HTML Code:" : "ХТМЛ кôд:", "Cancel" : "Одустани", diff --git a/apps/files/l10n/de_DE.js b/apps/files/l10n/de_DE.js index 4b897553f31..f5b58fcde18 100644 --- a/apps/files/l10n/de_DE.js +++ b/apps/files/l10n/de_DE.js @@ -115,7 +115,7 @@ OC.L10N.register( "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -126,8 +126,8 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Diese Liste ist aus Performance-Gründen nicht vollständig gerendert. Die Dateien werden gerendert, wenn Sie durch die Liste navigieren.", "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], - "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search everywhere …" : "Überall suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -224,7 +224,7 @@ OC.L10N.register( "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/de_DE.json b/apps/files/l10n/de_DE.json index 26b5abdbd75..403987f92f0 100644 --- a/apps/files/l10n/de_DE.json +++ b/apps/files/l10n/de_DE.json @@ -113,7 +113,7 @@ "Name" : "Name", "File type" : "Dateityp", "Size" : "Größe", - "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", + "{displayName}: failed on some elements" : "{displayName}: Ist bei einigen Elementen fehlgeschlagen", "{displayName}: done" : "{displayName}: Abgeschlossen", "{displayName}: failed" : "{displayName}: Fehlgeschlagen", "Actions" : "Aktionen", @@ -124,8 +124,8 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Diese Liste ist aus Performance-Gründen nicht vollständig gerendert. Die Dateien werden gerendert, wenn Sie durch die Liste navigieren.", "File not found" : "Datei nicht gefunden", "_{count} selected_::_{count} selected_" : ["{count} ausgewählt","{count} ausgewählt"], - "Search everywhere …" : "Überall suchen …", - "Search here …" : "Hier suchen …", + "Search everywhere …" : "Überall suchen …", + "Search here …" : "Hier suchen …", "Search scope options" : "Suchbereichsoptionen", "Search here" : "Hier suchen", "{usedQuotaByte} used" : "{usedQuotaByte} verwendet", @@ -222,7 +222,7 @@ "Selection" : "Auswahl", "Select all files" : "Alle Dateien auswählen", "Deselect all" : "Auswahl aufheben", - "Select or deselect" : "Aus- oder Abwählen", + "Select or deselect" : "Aus- oder abwählen", "Select a range" : "Einen Bereich auswählen", "Navigation" : "Navigation", "Go to parent folder" : "Zum übergeordneten Ordner wechseln", diff --git a/apps/files/l10n/pt_BR.js b/apps/files/l10n/pt_BR.js index 6b64267ec74..2b19b2bf39c 100644 --- a/apps/files/l10n/pt_BR.js +++ b/apps/files/l10n/pt_BR.js @@ -115,6 +115,9 @@ OC.L10N.register( "Name" : "Nome", "File type" : "Tipo de arquivo", "Size" : "Tamanho", + "{displayName}: failed on some elements" : "{displayName}: falhou em alguns elementos", + "{displayName}: done" : "{displayName}: feito", + "{displayName}: failed" : "{displayName}: fracassado", "Actions" : "Ações", "(selected)" : "(selecionados)", "List of files and folders." : "Lista de arquivos e pastas.", @@ -123,7 +126,10 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Esta lista não é totalmente renderizada por motivos de desempenho. Os arquivos serão renderizados à medida que você navegar pela lista.", "File not found" : "Arquivo não encontrado", "_{count} selected_::_{count} selected_" : ["{count} selecionado","{count} selecionados","{count} selecionados"], + "Search everywhere …" : "Pesquise em todos os lugares…", + "Search here …" : "Pesquise aqui…", "Search scope options" : "Opções de escopo da pesquisa", + "Search here" : "Pesquise aqui", "{usedQuotaByte} used" : "{usedQuotaByte} usado", "{used} of {quota} used" : "{used} de {quota} usados", "{relative}% used" : "{relative}% usado", @@ -187,6 +193,7 @@ OC.L10N.register( "No search results for “{query}”" : "Sem resultados de pesquisa para \"{query}\"", "Search for files" : "Pesquisar arquivos", "Clipboard is not available" : "A área de transferência não está disponível", + "WebDAV URL copied" : "URL do WebDAV copiada", "General" : "Geral", "Default view" : "Visualização padrão", "All files" : "Todos os arquivos", @@ -197,24 +204,37 @@ OC.L10N.register( "Appearance" : "Aparência", "Show hidden files" : "Mostrar arquivos ocultos", "Show file type column" : "Mostrar coluna de tipo de arquivo", + "Show file extensions" : "Mostrar extensões de arquivo", "Crop image previews" : "Cortar visualizações de imagem", "Additional settings" : "Configurações adicionais", "WebDAV" : "WebDAV", "WebDAV URL" : "URL WebDAV", "Copy" : "Copiar", + "How to access files using WebDAV" : "Como acessar arquivos usando WebDAV", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "A Autenticação de Dois Fatores está ativada para sua conta e, portanto, você precisa usar uma senha de aplicativo para conectar um cliente WebDAV externo.", "Warnings" : "Avisos", + "Warn before changing a file extension" : "Avisar antes de alterar uma extensão de arquivo", + "Warn before deleting files" : "Avisar antes de excluir arquivos", "Keyboard shortcuts" : "Atalhos do teclado", "File actions" : "Ações de arquivos", "Rename" : "Renomear", "Delete" : "Excluir", + "Add or remove favorite" : "Adicionar ou remover favoritos", "Manage tags" : "Gerenciar etiquetas", "Selection" : "Seleção", "Select all files" : "Selecionar todos os arquivos", "Deselect all" : "Desselecionar todos", + "Select or deselect" : "Selecionar ou desmarcar", + "Select a range" : "Selecione um intervalo", "Navigation" : "Navegação", + "Go to parent folder" : "Ir para a pasta mãe", + "Go to file above" : "Vá para o arquivo acima", + "Go to file below" : "Vá para o arquivo abaixo", + "Go left in grid" : "Vá para a esquerda na grade", + "Go right in grid" : "Vá para a direita na grade", "View" : "Visualização", "Toggle grid view" : "Alternar a visão em grade", + "Open file sidebar" : "Abrir barra lateral de arquivo", "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", @@ -322,6 +342,7 @@ OC.L10N.register( "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "O nome \"{newName}\" já é utilizado na pasta \"{dir}\". Escolha um nome diferente.", "Could not rename \"{oldName}\"" : "Não foi possível renomear \"{oldName}\"", "This operation is forbidden" : "Esta operação é proibida", + "This folder is unavailable, please try again later or contact the administration" : "Esta pasta não está disponível, tente novamente mais tarde ou entre em contato com a administração", "Storage is temporarily not available" : "O armazenamento está temporariamente indisponível", "Unexpected error: {error}" : "Erro inesperado: {error}", "_%n file_::_%n files_" : ["%n arquivo","%n de arquivos","%n arquivos"], diff --git a/apps/files/l10n/pt_BR.json b/apps/files/l10n/pt_BR.json index dd50c32b179..55a603b6107 100644 --- a/apps/files/l10n/pt_BR.json +++ b/apps/files/l10n/pt_BR.json @@ -113,6 +113,9 @@ "Name" : "Nome", "File type" : "Tipo de arquivo", "Size" : "Tamanho", + "{displayName}: failed on some elements" : "{displayName}: falhou em alguns elementos", + "{displayName}: done" : "{displayName}: feito", + "{displayName}: failed" : "{displayName}: fracassado", "Actions" : "Ações", "(selected)" : "(selecionados)", "List of files and folders." : "Lista de arquivos e pastas.", @@ -121,7 +124,10 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Esta lista não é totalmente renderizada por motivos de desempenho. Os arquivos serão renderizados à medida que você navegar pela lista.", "File not found" : "Arquivo não encontrado", "_{count} selected_::_{count} selected_" : ["{count} selecionado","{count} selecionados","{count} selecionados"], + "Search everywhere …" : "Pesquise em todos os lugares…", + "Search here …" : "Pesquise aqui…", "Search scope options" : "Opções de escopo da pesquisa", + "Search here" : "Pesquise aqui", "{usedQuotaByte} used" : "{usedQuotaByte} usado", "{used} of {quota} used" : "{used} de {quota} usados", "{relative}% used" : "{relative}% usado", @@ -185,6 +191,7 @@ "No search results for “{query}”" : "Sem resultados de pesquisa para \"{query}\"", "Search for files" : "Pesquisar arquivos", "Clipboard is not available" : "A área de transferência não está disponível", + "WebDAV URL copied" : "URL do WebDAV copiada", "General" : "Geral", "Default view" : "Visualização padrão", "All files" : "Todos os arquivos", @@ -195,24 +202,37 @@ "Appearance" : "Aparência", "Show hidden files" : "Mostrar arquivos ocultos", "Show file type column" : "Mostrar coluna de tipo de arquivo", + "Show file extensions" : "Mostrar extensões de arquivo", "Crop image previews" : "Cortar visualizações de imagem", "Additional settings" : "Configurações adicionais", "WebDAV" : "WebDAV", "WebDAV URL" : "URL WebDAV", "Copy" : "Copiar", + "How to access files using WebDAV" : "Como acessar arquivos usando WebDAV", "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "A Autenticação de Dois Fatores está ativada para sua conta e, portanto, você precisa usar uma senha de aplicativo para conectar um cliente WebDAV externo.", "Warnings" : "Avisos", + "Warn before changing a file extension" : "Avisar antes de alterar uma extensão de arquivo", + "Warn before deleting files" : "Avisar antes de excluir arquivos", "Keyboard shortcuts" : "Atalhos do teclado", "File actions" : "Ações de arquivos", "Rename" : "Renomear", "Delete" : "Excluir", + "Add or remove favorite" : "Adicionar ou remover favoritos", "Manage tags" : "Gerenciar etiquetas", "Selection" : "Seleção", "Select all files" : "Selecionar todos os arquivos", "Deselect all" : "Desselecionar todos", + "Select or deselect" : "Selecionar ou desmarcar", + "Select a range" : "Selecione um intervalo", "Navigation" : "Navegação", + "Go to parent folder" : "Ir para a pasta mãe", + "Go to file above" : "Vá para o arquivo acima", + "Go to file below" : "Vá para o arquivo abaixo", + "Go left in grid" : "Vá para a esquerda na grade", + "Go right in grid" : "Vá para a direita na grade", "View" : "Visualização", "Toggle grid view" : "Alternar a visão em grade", + "Open file sidebar" : "Abrir barra lateral de arquivo", "Show those shortcuts" : "Mostrar esses atalhos", "You" : "Você", "Shared multiple times with different people" : "Compartilhado várias vezes com pessoas diferentes", @@ -320,6 +340,7 @@ "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "O nome \"{newName}\" já é utilizado na pasta \"{dir}\". Escolha um nome diferente.", "Could not rename \"{oldName}\"" : "Não foi possível renomear \"{oldName}\"", "This operation is forbidden" : "Esta operação é proibida", + "This folder is unavailable, please try again later or contact the administration" : "Esta pasta não está disponível, tente novamente mais tarde ou entre em contato com a administração", "Storage is temporarily not available" : "O armazenamento está temporariamente indisponível", "Unexpected error: {error}" : "Erro inesperado: {error}", "_%n file_::_%n files_" : ["%n arquivo","%n de arquivos","%n arquivos"], diff --git a/apps/files/l10n/sr.js b/apps/files/l10n/sr.js index da03fc7ccc4..50b07bbe6ca 100644 --- a/apps/files/l10n/sr.js +++ b/apps/files/l10n/sr.js @@ -115,6 +115,9 @@ OC.L10N.register( "Name" : "Назив", "File type" : "Тип фајла", "Size" : "Величина", + "{displayName}: failed on some elements" : "{displayName}: није успело на неким елементима", + "{displayName}: done" : "{displayName}: завршено", + "{displayName}: failed" : "{displayName}: није успело", "Actions" : "Радње", "(selected)" : "(изабрано)", "List of files and folders." : "Листа фајлова и фолдера.", @@ -123,7 +126,10 @@ OC.L10N.register( "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Ова листа није у потпуности приказана из разлога перформанси. Фајлови ће се приказивати како се крећете кроз листу.", "File not found" : "Фајл није нађен", "_{count} selected_::_{count} selected_" : ["изабран је {count}","изабрана су {count}","изабрано је {count}"], + "Search everywhere …" : "Претражи свуда", + "Search here …" : "Претражи овде…", "Search scope options" : "Опције опсега претраге", + "Search here" : "Претражи овде", "{usedQuotaByte} used" : "{usedQuotaByte} искоришћено", "{used} of {quota} used" : "{used} од {quota} искоришћено", "{relative}% used" : "{relative}% искоришћено", @@ -135,6 +141,7 @@ OC.L10N.register( "Create new folder" : "Направи нову фасциклу", "This name is already in use." : "Ово име се већ користи.", "Create" : "Направи", + "Files starting with a dot are hidden by default" : "Фајлови који почињу тачком су подразумевано скривени", "Fill template fields" : "Попуните поља шаблона", "Submitting fields …" : "Поља се подносе…", "Submit" : "Пошаљи", @@ -186,32 +193,48 @@ OC.L10N.register( "No search results for “{query}”" : "Није нађен ниједан резултат за „{query}”", "Search for files" : "Претражи фајлове", "Clipboard is not available" : "Клипборд није доступан", + "WebDAV URL copied" : "WebDAV URL је копиран", "General" : "Опште", "Default view" : "Подразумевани поглед", "All files" : "Сви фајлови", "Personal files" : "Лични фајлови", "Sort favorites first" : "Сортирај прво омиљене", "Sort folders before files" : "Поређај фолдере испред фајлова", + "Folder tree" : "Стабло фолдера", "Appearance" : "Изглед", "Show hidden files" : "Прикажи скривене фајлове", "Show file type column" : "Прикажи колону са типом фајла", + "Show file extensions" : "Прикажи екстензије фајла", "Crop image previews" : "Опсецање прегледа слика", "Additional settings" : "Додатне поставке", "WebDAV" : "ВебДАВ", "WebDAV URL" : "WebDAV URL", "Copy" : "Копирај", + "How to access files using WebDAV" : "Како да приступите фајловима преко WebDAV протокола", + "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "Двофакторска потврда идентитета је укључена за ваш налог, тако да морате користити лозинку апликације да повежете спољни WebDAV клијент.", "Warnings" : "Упозорења", + "Warn before changing a file extension" : "Упозори пре измене екстензије фајла", + "Warn before deleting files" : "Упозори пре брисања фајлова", "Keyboard shortcuts" : "Пречице на тастатури", "File actions" : "Фајл акције", "Rename" : "Преименуј", "Delete" : "Обриши", + "Add or remove favorite" : "Додај или уклони омиљене", "Manage tags" : "Управљање ознакама", "Selection" : "Избор", "Select all files" : "Изабери све фајлове", "Deselect all" : "Поништи цео избор", + "Select or deselect" : "Изабери или уклони избор", + "Select a range" : "Изабери опсег", "Navigation" : "Навигација", + "Go to parent folder" : "Иди на фолдер родитељ", + "Go to file above" : "Иди на фајл изнад", + "Go to file below" : "Иди на фајл испод", + "Go left in grid" : "Иди на лево у мрежи", + "Go right in grid" : "Иди на десно у мреж", "View" : "Погледај", "Toggle grid view" : "Укључи/искључи приказ мреже", + "Open file sidebar" : "Отвори бочну траку за фајл", "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", @@ -300,6 +323,7 @@ OC.L10N.register( "Templates" : "Шаблони", "New template folder" : "Нови фолдер шаблона", "In folder" : "У фолдеру", + "Search in all files" : "Претражи у свим фајловима", "Search in folder: {folder}" : "Претрага у фолдеру: {folder}", "One of the dropped files could not be processed" : "Један од упуштених фајлова не може да се обради", "Your browser does not support the Filesystem API. Directories will not be uploaded" : "Ваш интернет прегледач не подржава Filesystem API. Нећете моћи да отпремате директоријуме", @@ -318,6 +342,7 @@ OC.L10N.register( "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Назив „{newName}” се већ користи у директоријуму „{dir}”. Молимо вас да изаберете неко друго име.", "Could not rename \"{oldName}\"" : "Не може да се промени име фајла „{oldName}”", "This operation is forbidden" : "Ова радња је забрањена", + "This folder is unavailable, please try again later or contact the administration" : "Овај фолдер није доступан, молимо вас да покушате касније или да контактирате администрацију", "Storage is temporarily not available" : "Складиште привремено није доступно", "Unexpected error: {error}" : "Неочекивана грешка: {error}", "_%n file_::_%n files_" : ["%n фајл","%n фајла","%n фајлова"], diff --git a/apps/files/l10n/sr.json b/apps/files/l10n/sr.json index b97d3ffc9b0..8746aa285a7 100644 --- a/apps/files/l10n/sr.json +++ b/apps/files/l10n/sr.json @@ -113,6 +113,9 @@ "Name" : "Назив", "File type" : "Тип фајла", "Size" : "Величина", + "{displayName}: failed on some elements" : "{displayName}: није успело на неким елементима", + "{displayName}: done" : "{displayName}: завршено", + "{displayName}: failed" : "{displayName}: није успело", "Actions" : "Радње", "(selected)" : "(изабрано)", "List of files and folders." : "Листа фајлова и фолдера.", @@ -121,7 +124,10 @@ "This list is not fully rendered for performance reasons. The files will be rendered as you navigate through the list." : "Ова листа није у потпуности приказана из разлога перформанси. Фајлови ће се приказивати како се крећете кроз листу.", "File not found" : "Фајл није нађен", "_{count} selected_::_{count} selected_" : ["изабран је {count}","изабрана су {count}","изабрано је {count}"], + "Search everywhere …" : "Претражи свуда", + "Search here …" : "Претражи овде…", "Search scope options" : "Опције опсега претраге", + "Search here" : "Претражи овде", "{usedQuotaByte} used" : "{usedQuotaByte} искоришћено", "{used} of {quota} used" : "{used} од {quota} искоришћено", "{relative}% used" : "{relative}% искоришћено", @@ -133,6 +139,7 @@ "Create new folder" : "Направи нову фасциклу", "This name is already in use." : "Ово име се већ користи.", "Create" : "Направи", + "Files starting with a dot are hidden by default" : "Фајлови који почињу тачком су подразумевано скривени", "Fill template fields" : "Попуните поља шаблона", "Submitting fields …" : "Поља се подносе…", "Submit" : "Пошаљи", @@ -184,32 +191,48 @@ "No search results for “{query}”" : "Није нађен ниједан резултат за „{query}”", "Search for files" : "Претражи фајлове", "Clipboard is not available" : "Клипборд није доступан", + "WebDAV URL copied" : "WebDAV URL је копиран", "General" : "Опште", "Default view" : "Подразумевани поглед", "All files" : "Сви фајлови", "Personal files" : "Лични фајлови", "Sort favorites first" : "Сортирај прво омиљене", "Sort folders before files" : "Поређај фолдере испред фајлова", + "Folder tree" : "Стабло фолдера", "Appearance" : "Изглед", "Show hidden files" : "Прикажи скривене фајлове", "Show file type column" : "Прикажи колону са типом фајла", + "Show file extensions" : "Прикажи екстензије фајла", "Crop image previews" : "Опсецање прегледа слика", "Additional settings" : "Додатне поставке", "WebDAV" : "ВебДАВ", "WebDAV URL" : "WebDAV URL", "Copy" : "Копирај", + "How to access files using WebDAV" : "Како да приступите фајловима преко WebDAV протокола", + "Two-Factor Authentication is enabled for your account, and therefore you need to use an app password to connect an external WebDAV client." : "Двофакторска потврда идентитета је укључена за ваш налог, тако да морате користити лозинку апликације да повежете спољни WebDAV клијент.", "Warnings" : "Упозорења", + "Warn before changing a file extension" : "Упозори пре измене екстензије фајла", + "Warn before deleting files" : "Упозори пре брисања фајлова", "Keyboard shortcuts" : "Пречице на тастатури", "File actions" : "Фајл акције", "Rename" : "Преименуј", "Delete" : "Обриши", + "Add or remove favorite" : "Додај или уклони омиљене", "Manage tags" : "Управљање ознакама", "Selection" : "Избор", "Select all files" : "Изабери све фајлове", "Deselect all" : "Поништи цео избор", + "Select or deselect" : "Изабери или уклони избор", + "Select a range" : "Изабери опсег", "Navigation" : "Навигација", + "Go to parent folder" : "Иди на фолдер родитељ", + "Go to file above" : "Иди на фајл изнад", + "Go to file below" : "Иди на фајл испод", + "Go left in grid" : "Иди на лево у мрежи", + "Go right in grid" : "Иди на десно у мреж", "View" : "Погледај", "Toggle grid view" : "Укључи/искључи приказ мреже", + "Open file sidebar" : "Отвори бочну траку за фајл", "Show those shortcuts" : "Прикажи те пречице", "You" : "Ви", "Shared multiple times with different people" : "Дељено више пута са разним људима", @@ -298,6 +321,7 @@ "Templates" : "Шаблони", "New template folder" : "Нови фолдер шаблона", "In folder" : "У фолдеру", + "Search in all files" : "Претражи у свим фајловима", "Search in folder: {folder}" : "Претрага у фолдеру: {folder}", "One of the dropped files could not be processed" : "Један од упуштених фајлова не може да се обради", "Your browser does not support the Filesystem API. Directories will not be uploaded" : "Ваш интернет прегледач не подржава Filesystem API. Нећете моћи да отпремате директоријуме", @@ -316,6 +340,7 @@ "The name \"{newName}\" is already used in the folder \"{dir}\". Please choose a different name." : "Назив „{newName}” се већ користи у директоријуму „{dir}”. Молимо вас да изаберете неко друго име.", "Could not rename \"{oldName}\"" : "Не може да се промени име фајла „{oldName}”", "This operation is forbidden" : "Ова радња је забрањена", + "This folder is unavailable, please try again later or contact the administration" : "Овај фолдер није доступан, молимо вас да покушате касније или да контактирате администрацију", "Storage is temporarily not available" : "Складиште привремено није доступно", "Unexpected error: {error}" : "Неочекивана грешка: {error}", "_%n file_::_%n files_" : ["%n фајл","%n фајла","%n фајлова"], diff --git a/apps/files_reminders/l10n/pt_BR.js b/apps/files_reminders/l10n/pt_BR.js index ac662d677af..3ed3e3e2949 100644 --- a/apps/files_reminders/l10n/pt_BR.js +++ b/apps/files_reminders/l10n/pt_BR.js @@ -11,6 +11,7 @@ OC.L10N.register( "Set file reminders" : "Defina lembretes de arquivo", "**📣 File reminders**\n\nSet file reminders.\n\nNote: to use the `File reminders` app, ensure that the `Notifications` app is installed and enabled. The `Notifications` app provides the necessary APIs for the `File reminders` app to work correctly." : "**📣 Lembretes de arquivos** \n\nDefina lembretes de arquivos. \n\nObservação: para usar o aplicativo `Lembretes de arquivos`, certifique-se de que o aplicativo `Notificações` esteja instalado e habilitado. O aplicativo `Notificações` fornece as APIs necessárias para que o aplicativo `Lembretes de arquivos` funcione corretamente.", "Set reminder for \"{fileName}\"" : "Definir lembrete para \"{fileName}\"", + "Reminder at custom date & time" : "Lembrete em data e hora personalizadas", "Clear reminder" : "Limpar lembrete", "Please choose a valid date & time" : "Por favor escolha uma data & hora válida.", "Reminder set for \"{fileName}\"" : "Lembrete definido para \"{fileName}\"", @@ -21,6 +22,7 @@ OC.L10N.register( "Cancel" : "Cancelar", "Set reminder" : "Definir lembrete", "Reminder set" : "Lembrete definido", + "Custom reminder" : "Lembrete personalizado", "Later today" : "Hoje mais tarde", "Set reminder for later today" : "Definir lembrete para hoje mais tarde", "Tomorrow" : "Amanhã", diff --git a/apps/files_reminders/l10n/pt_BR.json b/apps/files_reminders/l10n/pt_BR.json index d4a04ec5eec..e5be7701fae 100644 --- a/apps/files_reminders/l10n/pt_BR.json +++ b/apps/files_reminders/l10n/pt_BR.json @@ -9,6 +9,7 @@ "Set file reminders" : "Defina lembretes de arquivo", "**📣 File reminders**\n\nSet file reminders.\n\nNote: to use the `File reminders` app, ensure that the `Notifications` app is installed and enabled. The `Notifications` app provides the necessary APIs for the `File reminders` app to work correctly." : "**📣 Lembretes de arquivos** \n\nDefina lembretes de arquivos. \n\nObservação: para usar o aplicativo `Lembretes de arquivos`, certifique-se de que o aplicativo `Notificações` esteja instalado e habilitado. O aplicativo `Notificações` fornece as APIs necessárias para que o aplicativo `Lembretes de arquivos` funcione corretamente.", "Set reminder for \"{fileName}\"" : "Definir lembrete para \"{fileName}\"", + "Reminder at custom date & time" : "Lembrete em data e hora personalizadas", "Clear reminder" : "Limpar lembrete", "Please choose a valid date & time" : "Por favor escolha uma data & hora válida.", "Reminder set for \"{fileName}\"" : "Lembrete definido para \"{fileName}\"", @@ -19,6 +20,7 @@ "Cancel" : "Cancelar", "Set reminder" : "Definir lembrete", "Reminder set" : "Lembrete definido", + "Custom reminder" : "Lembrete personalizado", "Later today" : "Hoje mais tarde", "Set reminder for later today" : "Definir lembrete para hoje mais tarde", "Tomorrow" : "Amanhã", diff --git a/apps/files_reminders/lib/Dav/PropFindPlugin.php b/apps/files_reminders/lib/Dav/PropFindPlugin.php index 014e636eb2d..7fa45a4b854 100644 --- a/apps/files_reminders/lib/Dav/PropFindPlugin.php +++ b/apps/files_reminders/lib/Dav/PropFindPlugin.php @@ -16,6 +16,7 @@ use OCA\FilesReminders\Service\ReminderService; use OCP\Files\Folder; use OCP\IUser; use OCP\IUserSession; +use Sabre\DAV\ICollection; use Sabre\DAV\INode; use Sabre\DAV\PropFind; use Sabre\DAV\Server; @@ -32,9 +33,22 @@ class PropFindPlugin extends ServerPlugin { } public function initialize(Server $server): void { + $server->on('preloadCollection', $this->preloadCollection(...)); $server->on('propFind', [$this, 'propFind']); } + private function preloadCollection( + PropFind $propFind, + ICollection $collection, + ): void { + if ($collection instanceof Directory && $propFind->getStatus( + static::REMINDER_DUE_DATE_PROPERTY + ) !== null) { + $folder = $collection->getNode(); + $this->cacheFolder($folder); + } + } + public function propFind(PropFind $propFind, INode $node) { if (!in_array(static::REMINDER_DUE_DATE_PROPERTY, $propFind->getRequestedProperties())) { return; @@ -44,15 +58,6 @@ class PropFindPlugin extends ServerPlugin { return; } - if ( - $node instanceof Directory - && $propFind->getDepth() > 0 - && $propFind->getStatus(static::REMINDER_DUE_DATE_PROPERTY) !== null - ) { - $folder = $node->getNode(); - $this->cacheFolder($folder); - } - $propFind->handle( static::REMINDER_DUE_DATE_PROPERTY, function () use ($node) { diff --git a/apps/files_sharing/l10n/de_DE.js b/apps/files_sharing/l10n/de_DE.js index a3507239899..a7dc879a8f0 100644 --- a/apps/files_sharing/l10n/de_DE.js +++ b/apps/files_sharing/l10n/de_DE.js @@ -304,13 +304,13 @@ OC.L10N.register( "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Teams eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben kann nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/de_DE.json b/apps/files_sharing/l10n/de_DE.json index 41d191e8166..7e4298f986a 100644 --- a/apps/files_sharing/l10n/de_DE.json +++ b/apps/files_sharing/l10n/de_DE.json @@ -302,13 +302,13 @@ "Link shares" : "Freigaben teilen", "Shares" : "Freigaben", "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Dateien innerhalb Ihrer Organisation teilen. Auch Empfänger, die auf die Datei bereits zugreifen können, können diesen Link für einen einfachen Zugriff nutzen.", - "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer föderierten Cloud-ID teilen.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Dateien über öffentliche Links und E-Mail-Adressen mit anderen außerhalb Ihrer Organisation teilen. Sie können Nextcloud-Konten auch auf anderen Instanzen mithilfe ihrer Federated-Cloud-ID teilen.", "Shares from apps or other sources which are not included in internal or external shares." : "Freigaben aus Apps oder anderen Quellen, die nicht in internen oder externen Freigaben enthalten sind.", - "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federierte Cloud-IDs eingeben", - "Type names or teams" : "Namen oder Federierte Cloud-IDs eingeben", - "Type a federated cloud ID" : "Eine Federierte Cloud-ID eingeben", - "Type an email" : "Eine E-Mailadresse eingeben", - "Type an email or federated cloud ID" : "Eine E-Mailadresse oder eine Federierte Cloud-ID eingeben", + "Type names, teams, federated cloud IDs" : "Namen, Teams oder Federated-Cloud-IDs eingeben", + "Type names or teams" : "Namen oder Teams eingeben", + "Type a federated cloud ID" : "Eine Federated-Cloud-ID eingeben", + "Type an email" : "Eine E-Mail-Adresse eingeben", + "Type an email or federated cloud ID" : "Eine E-Mail-Adresse oder eine Federated-Cloud-ID eingeben", "Unable to load the shares list" : "Liste der Freigaben kann nicht geladen werden", "Expires {relativetime}" : "Läuft {relativetime} ab", "this share just expired." : "Diese Freigabe ist gerade abgelaufen.", diff --git a/apps/files_sharing/l10n/pt_BR.js b/apps/files_sharing/l10n/pt_BR.js index 4f9db865df5..c308cfdb122 100644 --- a/apps/files_sharing/l10n/pt_BR.js +++ b/apps/files_sharing/l10n/pt_BR.js @@ -202,6 +202,7 @@ OC.L10N.register( "Unshare" : "Descompartilhar", "Cannot copy, please copy the link manually" : "Não é possível copiar, copie o link manualmente", "Copy internal link" : "Copiar link interno", + "For people who already have access" : "Para pessoas que já têm acesso", "Internal link" : "Link interno", "{shareWith} by {initiator}" : "{shareWith} por {initiator}", "Shared via link by {initiator}" : "Compartilhado via link por {initiator}", @@ -212,6 +213,7 @@ OC.L10N.register( "Share link ({index})" : "Link de compartilhamento ({index})", "Create public link" : "Criar link público", "Actions for \"{title}\"" : "Ações para \"{title}\"", + "Copy public link of \"{title}\"" : "Copiar link público de \"{title}\"", "Error, please enter proper password and/or expiration date" : "Erro, digite a senha correta e/ou a data de validade", "Link share created" : "Compartilhamento por link criado", "Error while creating the share" : "Erro ao criar o compartilhamento", @@ -256,6 +258,7 @@ OC.L10N.register( "Successfully uploaded files" : "Arquivos carregados com sucesso", "View terms of service" : "Ver os termos de serviço", "Terms of service" : "Termos de serviço", + "Share with {user}" : "Compartilhe com {user}", "Share with email {email}" : "Compartilhar com e-mail {email}", "Share with group" : "Compartilhar com grupo", "Share in conversation" : "Compartilhar na conversa", @@ -300,6 +303,14 @@ OC.L10N.register( "Unable to fetch inherited shares" : "Não foi possível buscar compartilhamentos herdados", "Link shares" : "Compartilhamentos por link", "Shares" : "Compartilhamentos", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Compartilhe arquivos dentro da sua organização. Os destinatários que já conseguem visualizar o arquivo também podem usar este link para facilitar o acesso.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Compartilhe arquivos com outras pessoas fora da sua organização por meio de links públicos e endereços de e-mail. Você também pode compartilhar com contas Nextcloud em outras instâncias usando o ID de nuvem federado delas.", + "Shares from apps or other sources which are not included in internal or external shares." : "Compartilhamentos de aplicativos ou outras fontes que não estão incluídos em compartilhamentos internos ou externos.", + "Type names, teams, federated cloud IDs" : "Digite nomes, equipes, IDs de nuvem federada", + "Type names or teams" : "Digite nomes ou equipes", + "Type a federated cloud ID" : "Digite um ID de nuvem federada", + "Type an email" : "Digite um e-mail", + "Type an email or federated cloud ID" : "Digite um e-mail ou ID de nuvem federada", "Unable to load the shares list" : "Não foi possível carregar a lista de compartilhamentos", "Expires {relativetime}" : "Expira {relativetime}", "this share just expired." : "esse compartilhamento acabou de expirar.", diff --git a/apps/files_sharing/l10n/pt_BR.json b/apps/files_sharing/l10n/pt_BR.json index 1d6f17a194f..27b631d6137 100644 --- a/apps/files_sharing/l10n/pt_BR.json +++ b/apps/files_sharing/l10n/pt_BR.json @@ -200,6 +200,7 @@ "Unshare" : "Descompartilhar", "Cannot copy, please copy the link manually" : "Não é possível copiar, copie o link manualmente", "Copy internal link" : "Copiar link interno", + "For people who already have access" : "Para pessoas que já têm acesso", "Internal link" : "Link interno", "{shareWith} by {initiator}" : "{shareWith} por {initiator}", "Shared via link by {initiator}" : "Compartilhado via link por {initiator}", @@ -210,6 +211,7 @@ "Share link ({index})" : "Link de compartilhamento ({index})", "Create public link" : "Criar link público", "Actions for \"{title}\"" : "Ações para \"{title}\"", + "Copy public link of \"{title}\"" : "Copiar link público de \"{title}\"", "Error, please enter proper password and/or expiration date" : "Erro, digite a senha correta e/ou a data de validade", "Link share created" : "Compartilhamento por link criado", "Error while creating the share" : "Erro ao criar o compartilhamento", @@ -254,6 +256,7 @@ "Successfully uploaded files" : "Arquivos carregados com sucesso", "View terms of service" : "Ver os termos de serviço", "Terms of service" : "Termos de serviço", + "Share with {user}" : "Compartilhe com {user}", "Share with email {email}" : "Compartilhar com e-mail {email}", "Share with group" : "Compartilhar com grupo", "Share in conversation" : "Compartilhar na conversa", @@ -298,6 +301,14 @@ "Unable to fetch inherited shares" : "Não foi possível buscar compartilhamentos herdados", "Link shares" : "Compartilhamentos por link", "Shares" : "Compartilhamentos", + "Share files within your organization. Recipients who can already view the file can also use this link for easy access." : "Compartilhe arquivos dentro da sua organização. Os destinatários que já conseguem visualizar o arquivo também podem usar este link para facilitar o acesso.", + "Share files with others outside your organization via public links and email addresses. You can also share to Nextcloud accounts on other instances using their federated cloud ID." : "Compartilhe arquivos com outras pessoas fora da sua organização por meio de links públicos e endereços de e-mail. Você também pode compartilhar com contas Nextcloud em outras instâncias usando o ID de nuvem federado delas.", + "Shares from apps or other sources which are not included in internal or external shares." : "Compartilhamentos de aplicativos ou outras fontes que não estão incluídos em compartilhamentos internos ou externos.", + "Type names, teams, federated cloud IDs" : "Digite nomes, equipes, IDs de nuvem federada", + "Type names or teams" : "Digite nomes ou equipes", + "Type a federated cloud ID" : "Digite um ID de nuvem federada", + "Type an email" : "Digite um e-mail", + "Type an email or federated cloud ID" : "Digite um e-mail ou ID de nuvem federada", "Unable to load the shares list" : "Não foi possível carregar a lista de compartilhamentos", "Expires {relativetime}" : "Expira {relativetime}", "this share just expired." : "esse compartilhamento acabou de expirar.", diff --git a/apps/files_sharing/l10n/sr.js b/apps/files_sharing/l10n/sr.js index 894bb17b920..a32961e5ab2 100644 --- a/apps/files_sharing/l10n/sr.js +++ b/apps/files_sharing/l10n/sr.js @@ -202,6 +202,7 @@ OC.L10N.register( "Unshare" : "Укини дељење", "Cannot copy, please copy the link manually" : "Не могу да копирам, копирајте везу ручно", "Copy internal link" : "Копирај интерну везу", + "For people who already have access" : "Особе које већ имају приступ", "Internal link" : "Интерна веза", "{shareWith} by {initiator}" : "{shareWith} od {initiator}", "Shared via link by {initiator}" : "{initiator} поделио преко везе", diff --git a/apps/files_sharing/l10n/sr.json b/apps/files_sharing/l10n/sr.json index 4a59406e5ac..af0e916dc89 100644 --- a/apps/files_sharing/l10n/sr.json +++ b/apps/files_sharing/l10n/sr.json @@ -200,6 +200,7 @@ "Unshare" : "Укини дељење", "Cannot copy, please copy the link manually" : "Не могу да копирам, копирајте везу ручно", "Copy internal link" : "Копирај интерну везу", + "For people who already have access" : "Особе које већ имају приступ", "Internal link" : "Интерна веза", "{shareWith} by {initiator}" : "{shareWith} od {initiator}", "Shared via link by {initiator}" : "{initiator} поделио преко везе", diff --git a/apps/files_sharing/lib/MountProvider.php b/apps/files_sharing/lib/MountProvider.php index 7a0b1f135a6..b7b0582493e 100644 --- a/apps/files_sharing/lib/MountProvider.php +++ b/apps/files_sharing/lib/MountProvider.php @@ -46,13 +46,14 @@ class MountProvider implements IMountProvider { * @return IMountPoint[] */ public function getMountsForUser(IUser $user, IStorageFactory $loader) { - $shares = $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1)); - $shares = array_merge($shares, $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1)); - + $shares = array_merge( + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_USER, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_GROUP, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_CIRCLE, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_ROOM, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_DECK, null, -1), + $this->shareManager->getSharedWith($user->getUID(), IShare::TYPE_SCIENCEMESH, null, -1), + ); // filter out excluded shares and group shares that includes self $shares = array_filter($shares, function (IShare $share) use ($user) { diff --git a/apps/settings/l10n/de.js b/apps/settings/l10n/de.js index 0f6c53e01fc..9853b70efa7 100644 --- a/apps/settings/l10n/de.js +++ b/apps/settings/l10n/de.js @@ -110,7 +110,7 @@ OC.L10N.register( "Administration" : "Administration", "Users" : "Konten", "Additional settings" : "Zusätzliche Einstellungen", - "Assistant" : "Benötigt keine Übersetzung. Hier wird nur die formelle Übersetzung verwendet (de_DE).", + "Assistant" : "Assistent", "Administration privileges" : "Administrationsrechte", "Groupware" : "Groupware", "Overview" : "Übersicht", @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für solche der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Kontendieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registriere eine benutzerdefinierte Vorlage oder richte sie aus verfügbaren Vorlagen ein", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details der App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de.json b/apps/settings/l10n/de.json index 81fe3298433..549b86f45d2 100644 --- a/apps/settings/l10n/de.json +++ b/apps/settings/l10n/de.json @@ -108,7 +108,7 @@ "Administration" : "Administration", "Users" : "Konten", "Additional settings" : "Zusätzliche Einstellungen", - "Assistant" : "Benötigt keine Übersetzung. Hier wird nur die formelle Übersetzung verwendet (de_DE).", + "Assistant" : "Assistent", "Administration privileges" : "Administrationsrechte", "Groupware" : "Groupware", "Overview" : "Übersicht", @@ -418,10 +418,12 @@ "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für solche der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Kontendieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registriere eine benutzerdefinierte Vorlage oder richte sie aus verfügbaren Vorlagen ein", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details der App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de_DE.js b/apps/settings/l10n/de_DE.js index 8372aec845d..4b20c111333 100644 --- a/apps/settings/l10n/de_DE.js +++ b/apps/settings/l10n/de_DE.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für Konten der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Konten dieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern ", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", - "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registrieren Sie eine benutzerdefinierte Vorlage oder richten Sie sie aus verfügbaren Vorlagen ein", + "No Deploy daemons configured" : "Keine Bereitstellungs-Daemons konfiguriert", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer der verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details für die App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/de_DE.json b/apps/settings/l10n/de_DE.json index 3a9fd905c36..8b67f6c8169 100644 --- a/apps/settings/l10n/de_DE.json +++ b/apps/settings/l10n/de_DE.json @@ -418,10 +418,12 @@ "Excluded groups" : "Ausgeschlossene Gruppen", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Bei der Auswahl/Abwahl von Gruppen wird folgende Logik verwendet, um festzustellen, ob ein Konto 2FA verwenden muss: Wenn keine Gruppe ausgewählt ist, dann wird 2FA für alle Konten aktiviert, außer für Konten der ausgenommenen Gruppen. Sind Gruppen ausgewählt, so wird 2FA für alle Konten dieser Gruppen aktiviert. Ist ein Konto sowohl in einer ausgewählten als auch in einer ausgenommenen Gruppe, so hat die Auswahl Vorrang und 2FA wird aktiviert.", "Save changes" : "Änderungen speichern ", + "Choose Deploy Daemon for {appName}" : "Bereitstellungs-Daemon für {appName} wählen", "Default" : "Standard", "Registered Deploy daemons list" : "Liste von registrierten Bereitstellungsdaemons", - "No Deploy daemons configured" : "Keine Bereitstellungsdämonen konfiguriert", - "Register a custom one or setup from available templates" : "Registrieren Sie eine benutzerdefinierte Vorlage oder richten Sie sie aus verfügbaren Vorlagen ein", + "No Deploy daemons configured" : "Keine Bereitstellungs-Daemons konfiguriert", + "Register a custom one or setup from available templates" : "Eine benutzerdefinierte Vorlage registrieren oder aus einer der verfügbaren Vorlagen einrichten", + "Manage Deploy daemons" : "Bereitstellungs-Daemone verwalten", "Show details for {appName} app" : "Details für die App {appName} anzeigen", "Update to {update}" : "Aktualisieren auf {update}", "Remove" : "Entfernen", diff --git a/apps/settings/l10n/en_GB.js b/apps/settings/l10n/en_GB.js index d3657944451..72ce4dd2a2d 100644 --- a/apps/settings/l10n/en_GB.js +++ b/apps/settings/l10n/en_GB.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Excluded groups", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced.", "Save changes" : "Save changes", + "Choose Deploy Daemon for {appName}" : "Choose Deploy Daemon for {appName}", "Default" : "Default", "Registered Deploy daemons list" : "Registered Deploy daemons list", "No Deploy daemons configured" : "No Deploy daemons configured", "Register a custom one or setup from available templates" : "Register a custom one or setup from available templates", + "Manage Deploy daemons" : "Manage Deploy daemons", "Show details for {appName} app" : "Show details for {appName} app", "Update to {update}" : "Update to {update}", "Remove" : "Remove", diff --git a/apps/settings/l10n/en_GB.json b/apps/settings/l10n/en_GB.json index cc5761aef9a..7032d0834cf 100644 --- a/apps/settings/l10n/en_GB.json +++ b/apps/settings/l10n/en_GB.json @@ -418,10 +418,12 @@ "Excluded groups" : "Excluded groups", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced.", "Save changes" : "Save changes", + "Choose Deploy Daemon for {appName}" : "Choose Deploy Daemon for {appName}", "Default" : "Default", "Registered Deploy daemons list" : "Registered Deploy daemons list", "No Deploy daemons configured" : "No Deploy daemons configured", "Register a custom one or setup from available templates" : "Register a custom one or setup from available templates", + "Manage Deploy daemons" : "Manage Deploy daemons", "Show details for {appName} app" : "Show details for {appName} app", "Update to {update}" : "Update to {update}", "Remove" : "Remove", diff --git a/apps/settings/l10n/pl.js b/apps/settings/l10n/pl.js index 97798f4033a..fa8e66874c1 100644 --- a/apps/settings/l10n/pl.js +++ b/apps/settings/l10n/pl.js @@ -420,7 +420,12 @@ OC.L10N.register( "Excluded groups" : "Wyłączone grupy", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Gdy grupy są zaznaczone lub wykluczone, stosowana jest następująca logika w celu określenia, czy dla konta ma być wymuszona 2FA:. Jeśli nie wybrano żadnych grup, 2FA jest włączone dla wszystkich, z wyjątkiem członków wykluczonych grup. Jeśli grupy są wybrane, 2FA jest włączone dla wszystkich ich członków. Jeśli konto należy zarówno do grupy wybranej, jak i wykluczonej, priorytet ma grupa wybrana i 2FA jest wymuszane.", "Save changes" : "Zapisz zmiany", + "Choose Deploy Daemon for {appName}" : "Wybierz demona wdrożeniowego dla {appName}", "Default" : "Domyślny", + "Registered Deploy daemons list" : "Lista zarejestrowanych demonów wdrożeniowych", + "No Deploy daemons configured" : "Brak skonfigurowanych demonów wdrożeniowych", + "Register a custom one or setup from available templates" : "Zarejestruj własnego lub skonfiguruj z dostępnych szablonów", + "Manage Deploy daemons" : "Zarządzaj demonami wdrożeniowymi", "Show details for {appName} app" : "Pokaż szczegóły aplikacji {appName}", "Update to {update}" : "Zaktualizuj do {update}", "Remove" : "Usuń", diff --git a/apps/settings/l10n/pl.json b/apps/settings/l10n/pl.json index d22c9232ce9..db6334e6d5d 100644 --- a/apps/settings/l10n/pl.json +++ b/apps/settings/l10n/pl.json @@ -418,7 +418,12 @@ "Excluded groups" : "Wyłączone grupy", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Gdy grupy są zaznaczone lub wykluczone, stosowana jest następująca logika w celu określenia, czy dla konta ma być wymuszona 2FA:. Jeśli nie wybrano żadnych grup, 2FA jest włączone dla wszystkich, z wyjątkiem członków wykluczonych grup. Jeśli grupy są wybrane, 2FA jest włączone dla wszystkich ich członków. Jeśli konto należy zarówno do grupy wybranej, jak i wykluczonej, priorytet ma grupa wybrana i 2FA jest wymuszane.", "Save changes" : "Zapisz zmiany", + "Choose Deploy Daemon for {appName}" : "Wybierz demona wdrożeniowego dla {appName}", "Default" : "Domyślny", + "Registered Deploy daemons list" : "Lista zarejestrowanych demonów wdrożeniowych", + "No Deploy daemons configured" : "Brak skonfigurowanych demonów wdrożeniowych", + "Register a custom one or setup from available templates" : "Zarejestruj własnego lub skonfiguruj z dostępnych szablonów", + "Manage Deploy daemons" : "Zarządzaj demonami wdrożeniowymi", "Show details for {appName} app" : "Pokaż szczegóły aplikacji {appName}", "Update to {update}" : "Zaktualizuj do {update}", "Remove" : "Usuń", diff --git a/apps/settings/l10n/pt_BR.js b/apps/settings/l10n/pt_BR.js index 0484828ab7d..b6bd95b29da 100644 --- a/apps/settings/l10n/pt_BR.js +++ b/apps/settings/l10n/pt_BR.js @@ -345,6 +345,7 @@ OC.L10N.register( "Unified task processing" : "Processamento unificado de tarefas", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "As tarefas de IA podem ser implementadas por aplicativos diferentes. Aqui você pode definir qual aplicativo deve ser usado para qual tarefa.", "Allow AI usage for guest users" : "Permitir o uso de IA para usuários convidados", + "Provider for Task types" : "Provedor para tipos de tarefas", "Enable" : "Ativar", "None of your currently installed apps provide Task processing functionality" : "Nenhum dos seus aplicativos instalados atualmente oferece funcionalidade de processamento de Tarefas", "Machine translation" : "Traduções automáticas", @@ -419,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Grupos excluídos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Quando os grupos são selecionados/excluídos, eles usam a seguinte lógica para determinar se uma conta tem a 2FA aplicada: Se nenhum grupo for selecionado, a 2FA será habilitada para todos, exceto os membros dos grupos excluídos. Se grupos forem selecionados, a 2FA será habilitada para todos os membros deles. Se uma conta estiver em um grupo selecionado e excluído, o selecionado terá precedência e a 2FA será aplicada.", "Save changes" : "Salvar alterações", + "Choose Deploy Daemon for {appName}" : "Escolha Implantar Daemon para{appName}", "Default" : "Padrão", "Registered Deploy daemons list" : "Lista de Deploy daemons registrados", "No Deploy daemons configured" : "Nenhum Deploy daemon configurado", "Register a custom one or setup from available templates" : "Registre um personalizado ou configure a partir dos modelos disponíveis", + "Manage Deploy daemons" : "Gerenciar daemons de implantação", "Show details for {appName} app" : "Mostrar detalhes do aplicativo {appName}", "Update to {update}" : "Atualizar para {update}", "Remove" : "Excluir", @@ -624,6 +627,7 @@ OC.L10N.register( "Your biography. Markdown is supported." : "Sua biografia. Markdown é suportado.", "Unable to update date of birth" : "Não foi possível atualizar a data de nascimento", "Enter your date of birth" : "Digite sua data de nascimento", + "Bluesky handle" : "Alça Bluesky", "You are using {s}{usage}{/s}" : "Você está usando {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Você está usando {s}{usage}{/s} de {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Você é membro dos seguintes grupos:", diff --git a/apps/settings/l10n/pt_BR.json b/apps/settings/l10n/pt_BR.json index ba161dd1c8f..9742ef9df72 100644 --- a/apps/settings/l10n/pt_BR.json +++ b/apps/settings/l10n/pt_BR.json @@ -343,6 +343,7 @@ "Unified task processing" : "Processamento unificado de tarefas", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "As tarefas de IA podem ser implementadas por aplicativos diferentes. Aqui você pode definir qual aplicativo deve ser usado para qual tarefa.", "Allow AI usage for guest users" : "Permitir o uso de IA para usuários convidados", + "Provider for Task types" : "Provedor para tipos de tarefas", "Enable" : "Ativar", "None of your currently installed apps provide Task processing functionality" : "Nenhum dos seus aplicativos instalados atualmente oferece funcionalidade de processamento de Tarefas", "Machine translation" : "Traduções automáticas", @@ -417,10 +418,12 @@ "Excluded groups" : "Grupos excluídos", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Quando os grupos são selecionados/excluídos, eles usam a seguinte lógica para determinar se uma conta tem a 2FA aplicada: Se nenhum grupo for selecionado, a 2FA será habilitada para todos, exceto os membros dos grupos excluídos. Se grupos forem selecionados, a 2FA será habilitada para todos os membros deles. Se uma conta estiver em um grupo selecionado e excluído, o selecionado terá precedência e a 2FA será aplicada.", "Save changes" : "Salvar alterações", + "Choose Deploy Daemon for {appName}" : "Escolha Implantar Daemon para{appName}", "Default" : "Padrão", "Registered Deploy daemons list" : "Lista de Deploy daemons registrados", "No Deploy daemons configured" : "Nenhum Deploy daemon configurado", "Register a custom one or setup from available templates" : "Registre um personalizado ou configure a partir dos modelos disponíveis", + "Manage Deploy daemons" : "Gerenciar daemons de implantação", "Show details for {appName} app" : "Mostrar detalhes do aplicativo {appName}", "Update to {update}" : "Atualizar para {update}", "Remove" : "Excluir", @@ -622,6 +625,7 @@ "Your biography. Markdown is supported." : "Sua biografia. Markdown é suportado.", "Unable to update date of birth" : "Não foi possível atualizar a data de nascimento", "Enter your date of birth" : "Digite sua data de nascimento", + "Bluesky handle" : "Alça Bluesky", "You are using {s}{usage}{/s}" : "Você está usando {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Você está usando {s}{usage}{/s} de {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Você é membro dos seguintes grupos:", diff --git a/apps/settings/l10n/sr.js b/apps/settings/l10n/sr.js index 99361f3eefe..19a35ecb06f 100644 --- a/apps/settings/l10n/sr.js +++ b/apps/settings/l10n/sr.js @@ -345,6 +345,7 @@ OC.L10N.register( "Unified task processing" : "Обједињена обрада задатака", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI задатке могу да имплементирају разне апликације. Овде можете да подесите која ће се користити за који задатак.", "Allow AI usage for guest users" : "Дозволи да корисници гости користе AI", + "Provider for Task types" : "Пружалац услуге за Типове задатака", "Enable" : "Укључи", "None of your currently installed apps provide Task processing functionality" : "Ниједна од ваших тренутно инсталираних апликација не пружа функционалност обраде задатака", "Machine translation" : "Машинско превођење", @@ -371,6 +372,8 @@ OC.L10N.register( "Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Корисници ће ипак моћи да направе слику екрана или да сниме екран. Ово не представља дефинитивну заштиту.", "Allow users to share via link and emails" : "Дозволи корисницима да деле путем линка и и-мејлова", "Allow public uploads" : "Дозволи јавна отпремања", + "Allow public shares to be added to other clouds by federation." : "Дозволи да се јавна дељења здруживањем додају на остале облаке.", + "This will add share permissions to all newly created link shares." : "Ово ће додати дозволе дељења свим новокреираним дељењима линком.", "Always ask for a password" : "Увек питај за лозинку", "Enforce password protection" : "Захтевај заштиту лозинком", "Exclude groups from password requirements" : "Изузми групе из захтева за лозинком", @@ -394,6 +397,7 @@ OC.L10N.register( "Default expiration time of remote shares in days" : "Подразумевано време трајања удаљених дељења у данима", "Expire remote shares after x days" : "Нека удаљена дељења истекну након х дана", "Set default expiration date for shares via link or mail" : "Постави подразумевано време истека за дељења путем линка или мејла", + "Enforce expiration date for link or mail shares" : "Форсирај датум истека за дељења линком или мејлом", "Default expiration time of shares in days" : "Подразумевано време трајања дељења у данима", "Privacy settings for sharing" : "Поставке приватности за дељење", "Allow account name autocompletion in share dialog and allow access to the system address book" : "Омогући аутоматско довршавање имена налога у дијалогу дељења и дозволи приступ системском адресару", @@ -416,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Искључене групе", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Када се групе означе/искључе, користи се следећа логика да се закључи да ли се за налог захтева 2FA: ако није одабрана ниједна група, 2FA је укључен свима осим члановима искључених група. Ако има одабраних група, 2FA је укључен само њиховим члановима. Ако је налог у исто време у одабраној и у искљученој групи, одабрана група има предност и 2FA се захтева.", "Save changes" : "Сними измене", + "Choose Deploy Daemon for {appName}" : "Изабери Даемон за постављање за {appName}", "Default" : "Подразумевано", "Registered Deploy daemons list" : "Листа регистрованих Даемона за постављање", "No Deploy daemons configured" : "Није подешен ниједан Даемон за постављање", "Register a custom one or setup from available templates" : "Региструјте произвољни или подесите неки према доступном шаблону", + "Manage Deploy daemons" : "Управљај Даемонима за постављање", "Show details for {appName} app" : "Прикажи детаље апликације {appName}", "Update to {update}" : "Ажурирај на {update}", "Remove" : "Уклони", @@ -621,6 +627,7 @@ OC.L10N.register( "Your biography. Markdown is supported." : "Ваша биографија. Подржава се употреба Markdown означавања.", "Unable to update date of birth" : "Није успело ажурирање датума рођења", "Enter your date of birth" : "Унесите датум свог рођења", + "Bluesky handle" : "Bluesky ручка", "You are using {s}{usage}{/s}" : "Користите {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Користите {s}{usage}{/s} од {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Члан сте следећих група:", @@ -812,6 +819,7 @@ OC.L10N.register( "Pronouns" : "Заменице", "Role" : "Улога", "X (formerly Twitter)" : "X (бивши Twitter)", + "Bluesky" : "Bluesky", "Website" : "Веб сајт", "Profile visibility" : "Видљивост профила", "Locale" : "Локалитет", diff --git a/apps/settings/l10n/sr.json b/apps/settings/l10n/sr.json index a19dc3c091b..507cd5e5c41 100644 --- a/apps/settings/l10n/sr.json +++ b/apps/settings/l10n/sr.json @@ -343,6 +343,7 @@ "Unified task processing" : "Обједињена обрада задатака", "AI tasks can be implemented by different apps. Here you can set which app should be used for which task." : "AI задатке могу да имплементирају разне апликације. Овде можете да подесите која ће се користити за који задатак.", "Allow AI usage for guest users" : "Дозволи да корисници гости користе AI", + "Provider for Task types" : "Пружалац услуге за Типове задатака", "Enable" : "Укључи", "None of your currently installed apps provide Task processing functionality" : "Ниједна од ваших тренутно инсталираних апликација не пружа функционалност обраде задатака", "Machine translation" : "Машинско превођење", @@ -369,6 +370,8 @@ "Users will still be able to screenshot or record the screen. This does not provide any definitive protection." : "Корисници ће ипак моћи да направе слику екрана или да сниме екран. Ово не представља дефинитивну заштиту.", "Allow users to share via link and emails" : "Дозволи корисницима да деле путем линка и и-мејлова", "Allow public uploads" : "Дозволи јавна отпремања", + "Allow public shares to be added to other clouds by federation." : "Дозволи да се јавна дељења здруживањем додају на остале облаке.", + "This will add share permissions to all newly created link shares." : "Ово ће додати дозволе дељења свим новокреираним дељењима линком.", "Always ask for a password" : "Увек питај за лозинку", "Enforce password protection" : "Захтевај заштиту лозинком", "Exclude groups from password requirements" : "Изузми групе из захтева за лозинком", @@ -392,6 +395,7 @@ "Default expiration time of remote shares in days" : "Подразумевано време трајања удаљених дељења у данима", "Expire remote shares after x days" : "Нека удаљена дељења истекну након х дана", "Set default expiration date for shares via link or mail" : "Постави подразумевано време истека за дељења путем линка или мејла", + "Enforce expiration date for link or mail shares" : "Форсирај датум истека за дељења линком или мејлом", "Default expiration time of shares in days" : "Подразумевано време трајања дељења у данима", "Privacy settings for sharing" : "Поставке приватности за дељење", "Allow account name autocompletion in share dialog and allow access to the system address book" : "Омогући аутоматско довршавање имена налога у дијалогу дељења и дозволи приступ системском адресару", @@ -414,10 +418,12 @@ "Excluded groups" : "Искључене групе", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Када се групе означе/искључе, користи се следећа логика да се закључи да ли се за налог захтева 2FA: ако није одабрана ниједна група, 2FA је укључен свима осим члановима искључених група. Ако има одабраних група, 2FA је укључен само њиховим члановима. Ако је налог у исто време у одабраној и у искљученој групи, одабрана група има предност и 2FA се захтева.", "Save changes" : "Сними измене", + "Choose Deploy Daemon for {appName}" : "Изабери Даемон за постављање за {appName}", "Default" : "Подразумевано", "Registered Deploy daemons list" : "Листа регистрованих Даемона за постављање", "No Deploy daemons configured" : "Није подешен ниједан Даемон за постављање", "Register a custom one or setup from available templates" : "Региструјте произвољни или подесите неки према доступном шаблону", + "Manage Deploy daemons" : "Управљај Даемонима за постављање", "Show details for {appName} app" : "Прикажи детаље апликације {appName}", "Update to {update}" : "Ажурирај на {update}", "Remove" : "Уклони", @@ -619,6 +625,7 @@ "Your biography. Markdown is supported." : "Ваша биографија. Подржава се употреба Markdown означавања.", "Unable to update date of birth" : "Није успело ажурирање датума рођења", "Enter your date of birth" : "Унесите датум свог рођења", + "Bluesky handle" : "Bluesky ручка", "You are using {s}{usage}{/s}" : "Користите {s}{usage}{/s}", "You are using {s}{usage}{/s} of {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})" : "Користите {s}{usage}{/s} од {s}{totalSpace}{/s} ({s}{usageRelative}%{/s})", "You are a member of the following groups:" : "Члан сте следећих група:", @@ -810,6 +817,7 @@ "Pronouns" : "Заменице", "Role" : "Улога", "X (formerly Twitter)" : "X (бивши Twitter)", + "Bluesky" : "Bluesky", "Website" : "Веб сајт", "Profile visibility" : "Видљивост профила", "Locale" : "Локалитет", diff --git a/apps/settings/l10n/uk.js b/apps/settings/l10n/uk.js index 46eae517b63..d3888dd835a 100644 --- a/apps/settings/l10n/uk.js +++ b/apps/settings/l10n/uk.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "Виключені групи", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Якщо вибрано/виключено групи, то застосовується така логіка визначення, чи до користувача застосовується двофакторна авторизація: якщо не вибрано жодної групи, двофакторну авторизацію ввімкнено для всіх, крім учасників виключених груп. Якщо вибрано групи, для всіх учасників увімкнено двофакторну авторизацію. Якщо користувач одночасно входить до вибраної групи та виключеної групи, то вибрана група має пріоритет і в такому випадку до цього користувача застосовується двофакторна авторизація.", "Save changes" : "Зберегти зміни", + "Choose Deploy Daemon for {appName}" : "Виберіть Deploy Daemon для {appName}", "Default" : "Типово", "Registered Deploy daemons list" : "Список зареєстрованих демонів розгортання", "No Deploy daemons configured" : "Не налаштовано демонів розгортання", "Register a custom one or setup from available templates" : "Зареєструвати власний або налаштувати з доступних шаблонів", + "Manage Deploy daemons" : "Керувати демонами розгортання", "Show details for {appName} app" : "Показати деталі для застосунку {appName}", "Update to {update}" : "Оновити до {update}", "Remove" : "Вилучити", diff --git a/apps/settings/l10n/uk.json b/apps/settings/l10n/uk.json index a818cf9def4..bbdbde52932 100644 --- a/apps/settings/l10n/uk.json +++ b/apps/settings/l10n/uk.json @@ -418,10 +418,12 @@ "Excluded groups" : "Виключені групи", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "Якщо вибрано/виключено групи, то застосовується така логіка визначення, чи до користувача застосовується двофакторна авторизація: якщо не вибрано жодної групи, двофакторну авторизацію ввімкнено для всіх, крім учасників виключених груп. Якщо вибрано групи, для всіх учасників увімкнено двофакторну авторизацію. Якщо користувач одночасно входить до вибраної групи та виключеної групи, то вибрана група має пріоритет і в такому випадку до цього користувача застосовується двофакторна авторизація.", "Save changes" : "Зберегти зміни", + "Choose Deploy Daemon for {appName}" : "Виберіть Deploy Daemon для {appName}", "Default" : "Типово", "Registered Deploy daemons list" : "Список зареєстрованих демонів розгортання", "No Deploy daemons configured" : "Не налаштовано демонів розгортання", "Register a custom one or setup from available templates" : "Зареєструвати власний або налаштувати з доступних шаблонів", + "Manage Deploy daemons" : "Керувати демонами розгортання", "Show details for {appName} app" : "Показати деталі для застосунку {appName}", "Update to {update}" : "Оновити до {update}", "Remove" : "Вилучити", diff --git a/apps/settings/l10n/zh_HK.js b/apps/settings/l10n/zh_HK.js index 775b486d6d6..1d19804ea48 100644 --- a/apps/settings/l10n/zh_HK.js +++ b/apps/settings/l10n/zh_HK.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選擇/排除群組時,其使用以下邏輯來確定帳戶是否強制啟用雙因素驗證:如果沒有選擇群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選擇了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳戶同時位在選擇與排除的群組,則被選擇的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "默認", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_HK.json b/apps/settings/l10n/zh_HK.json index 9c9dbb6f271..286202d71ff 100644 --- a/apps/settings/l10n/zh_HK.json +++ b/apps/settings/l10n/zh_HK.json @@ -418,10 +418,12 @@ "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選擇/排除群組時,其使用以下邏輯來確定帳戶是否強制啟用雙因素驗證:如果沒有選擇群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選擇了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳戶同時位在選擇與排除的群組,則被選擇的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "默認", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_TW.js b/apps/settings/l10n/zh_TW.js index 9806745beea..18a6b553e05 100644 --- a/apps/settings/l10n/zh_TW.js +++ b/apps/settings/l10n/zh_TW.js @@ -420,10 +420,12 @@ OC.L10N.register( "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選取/排除群組時,其使用以下邏輯來確定帳號是否強制啟用雙因素驗證:如果沒有選取群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選取了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳號同時位在選取與排除的群組,則被選取的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "預設", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/settings/l10n/zh_TW.json b/apps/settings/l10n/zh_TW.json index 9f4cbb94344..4e1ee7c15e5 100644 --- a/apps/settings/l10n/zh_TW.json +++ b/apps/settings/l10n/zh_TW.json @@ -418,10 +418,12 @@ "Excluded groups" : "排除的群組", "When groups are selected/excluded, they use the following logic to determine if an account has 2FA enforced: If no groups are selected, 2FA is enabled for everyone except members of the excluded groups. If groups are selected, 2FA is enabled for all members of these. If an account is both in a selected and excluded group, the selected takes precedence and 2FA is enforced." : "選取/排除群組時,其使用以下邏輯來確定帳號是否強制啟用雙因素驗證:如果沒有選取群組,雙因素驗證對所有除了排除群組以外的成員啟用。若選取了群組,則雙因素驗證會對所有該群組的成員啟用。如果某個帳號同時位在選取與排除的群組,則被選取的群組優先程度較高,因此會強制啟用雙因素驗證。", "Save changes" : "儲存變更", + "Choose Deploy Daemon for {appName}" : "選擇 {appName} 的部署幕後程式", "Default" : "預設", "Registered Deploy daemons list" : "已註冊的部署幕後程式清單", "No Deploy daemons configured" : "未設定部署幕後程式", "Register a custom one or setup from available templates" : "從可用範本註冊自訂範本或設定", + "Manage Deploy daemons" : "管理部署幕後程式", "Show details for {appName} app" : "顯示 {appName} 應用程式的詳細資訊", "Update to {update}" : "更新到 {update}", "Remove" : "移除", diff --git a/apps/systemtags/l10n/pt_BR.js b/apps/systemtags/l10n/pt_BR.js index 6f931085ed1..4b8ad122508 100644 --- a/apps/systemtags/l10n/pt_BR.js +++ b/apps/systemtags/l10n/pt_BR.js @@ -80,6 +80,7 @@ OC.L10N.register( "Search tag" : "Pesquisar etiqueta", "Change tag color" : "Alterar cor da etiqueta", "Create new tag" : "Criar etiqueta nova", + "Choose tags for the selected files" : "Escolha tags para os arquivos selecionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Erro ao carregar etiquetas", diff --git a/apps/systemtags/l10n/pt_BR.json b/apps/systemtags/l10n/pt_BR.json index c5bc3eeedd5..a2a6ae74c6a 100644 --- a/apps/systemtags/l10n/pt_BR.json +++ b/apps/systemtags/l10n/pt_BR.json @@ -78,6 +78,7 @@ "Search tag" : "Pesquisar etiqueta", "Change tag color" : "Alterar cor da etiqueta", "Create new tag" : "Criar etiqueta nova", + "Choose tags for the selected files" : "Escolha tags para os arquivos selecionados", "Cancel" : "Cancelar", "Apply" : "Aplicar", "Failed to load tags" : "Erro ao carregar etiquetas", |