Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,10 @@
[main]
host = https://www.transifex.com
[o:qsfera-eu:p:qsfera-eu:r:qsfera-settings]
file_filter = locale/<lang>/LC_MESSAGES/settings.po
minimum_perc = 75
resource_name = qsfera-settings
source_file = settings.pot
source_lang = en
type = PO
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Ivan Fustero, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Ivan Fustero, 2025\n"
"Language-Team: Catalan (https://app.transifex.com/qsfera-eu/teams/204053/ca/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ca\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "S'ha afegit com a membre de l'espai"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diàriament"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval d'enviament de correu"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "S'ha rebutjat el fitxer"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Immediat"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Mai"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notifica'm quan m'hagin afegit com a membre a un espai"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notifica'm quan m'hagin tret com a membre d'un espai"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notifica quan rebi una compartició"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notifica'm quan un fitxer que he pujat ha estat rebutjat per una infecció "
"per virus o una violació de la política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notifica'm quan s'hagi eliminat una compartició rebuda"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notifica'm quan hagi caducat una compartició rebuda"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notifica'm quan s'hagi suprimit un espai del qual soc membre"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notifica'm quan s'hagi desactivat un espai del qual soc membre"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notifica'm quan hagi caducat la pertinença a un espai"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eliminat com a membre de l'espai"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor seleccionat:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Compartició caducada"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Compartició rebuda"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Compartició eliminada"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espai eliminat"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espai desactivat"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "La pertinença a l'espai ha expirat"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Setmanalment"
@@ -0,0 +1,151 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jörn Friedrich Dreyer <jfd@butonic.de>, 2025
# Jannick Kuhr, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-20 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jannick Kuhr, 2026\n"
"Language-Team: German (https://app.transifex.com/qsfera-eu/teams/204053/de/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Als Space-Mitglied hinzugefügt"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Täglich"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "E-Mail-Versandintervall"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Datei abgelehnt"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Sofort"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nie"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Benachrichtigung, wenn ich zu einem Space als Mitglied hinzugefügt wurde"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr ""
"Benachrichtigung, wenn ich als Mitglied aus einem Space entfernt wurde"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Benachrichtigung, wenn ich eine Freigabe empfangen habe"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Benachrichtigung, wenn eine hochgeladene Datei aufgrund einer Vireninfektion"
" oder eines Verstoßes gegen die Richtlinien abgelehnt wurde"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Benachrichtigung, wenn eine empfangene Freigabe entfernt wurde"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Benachrichtigung, wenn eine empfangene Freigabe abgelaufen ist"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr ""
"Benachrichtigung, wenn ein Space, in dem ich Mitglied bin, gelöscht wurde"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Benachrichtigung, wenn ein Space, in dem ich Mitglied bin, deaktiviert wurde"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Benachrichtigung, wenn eine Space-Mitgliedschaft abgelaufen ist"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Space-Mitgliedschaft entfernt"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Eingestellter Wert:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Freigabe abgelaufen"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Freigabe empfangen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Freigabe entfernt"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Space gelöscht"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Space deaktiviert"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Space-Mitgliedschaft abgelaufen"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Wöchentlich"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Efstathios Iosifidis <eiosifidis@gmail.com>, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-23 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Efstathios Iosifidis <eiosifidis@gmail.com>, 2026\n"
"Language-Team: Greek (https://app.transifex.com/qsfera-eu/teams/204053/el/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: el\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Προσθήκη ως μέλος χώρου"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Καθημερινά"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Συχνότητα αποστολής email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Απόρριψη αρχείου"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Άμεσα"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ποτέ"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Ειδοποίηση όταν προστίθεμαι ως μέλος σε έναν χώρο"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Ειδοποίηση όταν αφαιρούμαι από μέλος ενός χώρου"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Ειδοποίηση όταν λαμβάνω έναν διαμοιρασμό"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Ειδοποίηση όταν ένα αρχείο που ανέβασα απορρίφθηκε λόγω μόλυνσης από ιό ή "
"παραβίασης πολιτικής"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Ειδοποίηση όταν ένας ληφθείς διαμοιρασμός έχει αφαιρεθεί"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Ειδοποίηση όταν ένας ληφθείς διαμοιρασμός έχει λήξει"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Ειδοποίηση όταν ένας χώρος στον οποίο είμαι μέλος έχει διαγραφεί"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Ειδοποίηση όταν ένας χώρος στον οποίο είμαι μέλος έχει απενεργοποιηθεί"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Ειδοποίηση όταν η ιδιότητα μέλους σε έναν χώρο έχει λήξει"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Αφαίρεση από μέλος χώρου"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Επιλεγμένη τιμή:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Λήξη διαμοιρασμού"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Λήψη διαμοιρασμού"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Αφαίρεση διαμοιρασμού"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Διαγραφή χώρου"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Απενεργοποίηση χώρου"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Λήξη ιδιότητας μέλους χώρου"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Εβδομαδιαία"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Elías Martín, 2025
# Alejandro Robles, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Alejandro Robles, 2025\n"
"Language-Team: Spanish (https://app.transifex.com/qsfera-eu/teams/204053/es/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: es\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Incluido como miembro del espacio"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diario"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalo de envío de correos"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Archivo rechazado"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantaneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nunca"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notificar cuando me han agregado como miembro a un espacio"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notificar cuando he sido eliminado como miembro de un espacio"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notificarme cuando he recibido un compartido"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notificar cuando un archivo que he subido ha sido rechazado por contener "
"virus o violar alguna política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notificar cuando un compartido que he recibido ha sido eliminado"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notificar cuando un compartido recibido ha expirado."
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notificar cuando un espacio del que soy miembro ha sido eliminado"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notificar cuando un espacio del que soy miembro ha sido deshabilitado"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notificar cuando la membresía a un espacio ha caducado"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eliminado como miembro del espacio"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor seleccionado:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Compartido expirado"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Compartido recibido"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Compartido eliminado"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espacio eliminado"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espacio inhabilitado"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Membresia al espacio expirada"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Semanal"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jiri Grönroos <jiri.gronroos@iki.fi>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-07 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Jiri Grönroos <jiri.gronroos@iki.fi>, 2025\n"
"Language-Team: Finnish (https://app.transifex.com/qsfera-eu/teams/204053/fi/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Lisätty avaruuden jäseneksi"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Päivittäin"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Sähköpostin lähetyksen aikaväli"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Tiedosto hylätty"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Välittömästi"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ei koskaan"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Ilmoita kun minut lisätty jäseneksi avaruuteen"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Ilmoita kun jäsenyyteni avaruudesta on poistettu"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Ilmoita kun olen vastaanottanut jaon"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Ilmoita, kun lähettämäni tiedosto hylättiin joko virustartunnan tai "
"käytäntöloukkauksen vuoksi"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Ilmoita kun vastaanotettu jako on poistettu"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Ilmoita kun vastaanotettu jako on vanhentunut"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Ilmoita, kun avaruus, jonka jäsen olen, on poistettu"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Ilmoita, kun avaruus, jonka jäsen olen, on poistettu käytöstä"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Ilmoita, kun avaruuden jäsenyys on vanhentunut"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Poistettu avaruuden jäsenyydestä"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valittu arvo:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Jako vanheni"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Jako vastaanotettu"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Jako poistettu"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Avaruus poistettu"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Avaruus poistettu käytöstä"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Avaruuden jäsenyys vanhentunut"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Viikottain"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# eric_G <junk.eg@free.fr>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: eric_G <junk.eg@free.fr>, 2025\n"
"Language-Team: French (https://app.transifex.com/qsfera-eu/teams/204053/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Ajouté comme membre de l'Espace"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Quotidiennement"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalle d'envoi des e-mails"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fichier rejeté"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantané"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Jamais"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Recevoir une notification lorsque j'ai été ajouté en tant que membre à un "
"espace"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "M'avertir lorsque j'ai été retiré d'un Espace en tant que membre"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Recevoir une notification lorsque j'ai reçu un partage"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notifier lorsqu'un fichier que j'ai téléchargé a été rejeté en raison d'une "
"infection virale ou d'une violation de politique."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notifier la suppression d'un partage reçu"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notifier l'expiration d'un partage reçue"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notifier lorsqu'un espace dont je suis membre a été supprimé"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notifier lorsqu'un Espace dont je suis membre a été désactivé"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notifier l'expiration d'une adhésion à un Espace"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Supprimé en tant que membre de l'Espace"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valeur sélectionnée :"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Partage expiré"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Partage reçue"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Partage supprimée"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espace supprimé"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espace désactivé"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "L'adhésion à l'espace a expiré"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Hebdomadaire"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# mitibor, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-04 07:46+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: mitibor, 2026\n"
"Language-Team: Hungarian (https://app.transifex.com/qsfera-eu/teams/204053/hu/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: hu\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Hozzáadva tárhelytagként"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Naponta"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "E-mail küldési időköz"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fájl elutasítva"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Azonnal"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Soha"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Értesítést kérek, ha hozzáadtak egy tárhelyhez tagként"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Értesítést kérek, ha eltávolítottak egy tárhely tagjai közül"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Értesítést kérek, ha megosztást kaptam"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Értesítést kérek, ha egy általam feltöltött fájl elutasításra került "
"vírusfertőzés vagy szabályszegés miatt"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Értesítést kérek, ha egy kapott megosztást eltávolítottak"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Értesítést kérek, ha egy kapott megosztás lejárt"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Értesítést kérek, ha egy tárhelyet, amelynek tagja vagyok, töröltek"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Értesítést kérek, ha egy tárhelyet, amelynek tagja vagyok, letiltottak"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Értesítést kérek, ha egy tárhelytagság lejárt"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Eltávolítva a tárhelytagok közül"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Kiválasztott érték:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Megosztás lejárt"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Megosztás érkezett"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Megosztás eltávolítva"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Tárhely törölve"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Tárhely letiltva"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Tárhelytagság lejárt"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Hetente"
@@ -0,0 +1,150 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# idoet <idoet@protonmail.ch>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: idoet <idoet@protonmail.ch>, 2025\n"
"Language-Team: Indonesian (https://app.transifex.com/qsfera-eu/teams/204053/id/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: id\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Ditambahkan menjadi anggota ruang penyimpanan"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Harian"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval pengiriman email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Berkas ditolak"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instan"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Tidak pernah"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr ""
"Beri tahu ketika saya telah ditambahkan menjadi anggota dari suatu ruang "
"penyimpanan"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr ""
"Beri tahu ketika saya telah dihapus dari keanggotaan suatu ruang penyimpanan"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Beri tahu ketika saya telah menerima berbagi"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Beri tahu jika file yang saya unggah ditolak karena infeksi virus atau "
"pelanggaran kebijakan"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Beri tahu saat berbagi yang diterima telah dihapus"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Beri tahu ketika berbagi yang diterima telah berakhir"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Beri tahu ketika ruang penyimpanan yang saya ikuti telah dihapus"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Beri tahu ketika ruang penyimpanan yang saya ikuti telah dinonaktifkan"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Beri tahu saat keanggotaan ruang penyimpanan telah berakhir"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Dihapus dari anggota ruang penyimpanan"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Yang dipilih:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Berbagi Berakhir"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Berbagi Diterima"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Berbagi Dihapus"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Ruang penyimpanan dihapus"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Ruang penyimpanan dinonaktifkan"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Keanggotaan ruang penyimpanan telah berakhir"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Mingguan"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Simone Pagano, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Simone Pagano, 2025\n"
"Language-Team: Italian (https://app.transifex.com/qsfera-eu/teams/204053/it/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: it\n"
"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Aggiunto come membro di uno spazio"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Giornaliero"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervallo di invio delle e-mail"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "File rifiutato"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Istantaneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Mai"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Inviami una notifica quando vengo aggiunto come membro di uno spazio"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Inviami una notifica quando vengo rimosso come membro di uno spazio"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Inviami una notifica quando ricevo una condivisione"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Inviami una notifica quando un file che ho caricato viene rifiutato a causa "
"di una violazione delle policy o a causa della presenza di virus."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Inviami una notifica quando una condivisione viene rimossa"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Inviami una notifica quando una condivisione ricevuta scade"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr ""
"Inviami una notifica quando uno spazio di cui sono membro viene eliminato"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Inviami una notifica quando uno spazio di cui sono membro viene disattivato"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Inviami una notifica quando uno spazio di cui sono membro scade"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Non più un membro dello spazio"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valore selezionato: "
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Condivisione scaduta"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Condivisione ricevuta"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Condivisione rimossa"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Spazio eliminato"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Spazio disattivato"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Partecipazione ad uno spazio scaduta"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Settimanale"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# iikaka88, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-28 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: iikaka88, 2025\n"
"Language-Team: Japanese (https://app.transifex.com/qsfera-eu/teams/204053/ja/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ja\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "スペースメンバーへの追加"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "毎日"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "メール送信間隔"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "ファイルの拒否"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "即時"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "しない"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "スペースのメンバーに追加された時に通知する"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "スペースのメンバーから削除された時に通知する"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "共有を受け取った時に通知する"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "アップロードしたファイルがウイルスやポリシー違反で拒否された時に通知する"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "受け取った共有が削除された時に通知する"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "受け取った共有の期限が切れた時に通知する"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "参加しているスペースが削除された時に通知する"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "参加しているスペースが無効化された時に通知する"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "スペースのメンバーシップ期限が切れた時に通知する"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "スペースメンバーからの削除"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "選択された値:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "共有期限切れ"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "共有受け取った時"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "共有削除"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "スペース削除"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "スペース無効化"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "スペース期限切れ"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "毎週"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# gapho shin, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: gapho shin, 2025\n"
"Language-Team: Korean (https://app.transifex.com/qsfera-eu/teams/204053/ko/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ko\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "스페이스 멤버로 추가됨"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "일간"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "이메일 전송 간격"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "파일 거부"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "즉시"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "지속"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "내가 공간에 멤버로 추가되었을 때 알림"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "내가 공간에서 멤버로 제거되었을 때 알림"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "공유 받으면 알림"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "바이러스 감염 또는 정책 위반으로 인해 업로드한 파일이 거부되었을 때 알림"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "허가된 공유가 제거되면 알림"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "허가된 공유가 만료되면 알림"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "내가 속한 공간이 삭제되었을 때 알림"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "내가 속한 공간이 비활성화되었을 때 알림"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "스페이스 멤버십이 만료되면 알림"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "스페이스 멤버가 제거됨"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "선택한 값:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "만료된 공유"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "공유됨"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "공유제거"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "스페이스 삭제"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "스페이스 비활성화"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "스페이스 멤버십 만료"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "주간"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# BoneNI, 2026
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-30 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: BoneNI, 2026\n"
"Language-Team: Lao (https://app.transifex.com/qsfera-eu/teams/204053/lo/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: lo\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "ຖືກເພີ່ມເປັນສະມາຊິກພື້ນທີ່"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "ປະຈຳວັນ"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "ໄລຍະຫ່າງການສົ່ງອີເມລ"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "ໄຟລ໌ຖືກປະຕິເສດ"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "ທັນທີ"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "ບໍ່ເຄີຍ"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍຖືກເພີ່ມເປັນສະມາຊິກໃນພື້ນທີ່"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍຖືກຖອນອອກຈາກການເປັນສະມາຊິກໃນພື້ນທີ່"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "ແຈ້ງເຕືອນເມື່ອຂ້ອຍໄດ້ຮັບການແບ່ງປັນ"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"ແຈ້ງເຕືອນເມື່ອໄຟລ໌ທີ່ຂ້ອຍອັບໂຫລດຖືກປະຕິເສດ ເນື່ອງຈາກກວດພົບໄວຣັສ ຫຼື "
"ລະເມີດນະໂຍບາຍ"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "ແຈ້ງເຕືອນເມື່ອການແບ່ງປັນທີ່ໄດ້ຮັບຖືກຖອນອອກ"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "ແຈ້ງເຕືອນເມື່ອການແບ່ງປັນທີ່ໄດ້ຮັບໝົດອາຍຸ"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "ແຈ້ງເຕືອນເມື່ອພື້ນທີ່ທີ່ຂ້ອຍເປັນສະມາຊິກຖືກລຶບ"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "ແຈ້ງເຕືອນເມື່ອພື້ນທີ່ທີ່ຂ້ອຍເປັນສະມາຊິກຖືກປິດການໃຊ້ງານ"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "ແຈ້ງເຕືອນເມື່ອການເປັນສະມາຊິກພື້ນທີ່ໝົດອາຍຸ"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "ຖືກຖອນອອກຈາກການເປັນສະມາຊິກພື້ນທີ່"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "ຄ່າທີ່ເລືອກ:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "ການແບ່ງປັນໝົດອາຍຸ"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "ໄດ້ຮັບການແບ່ງປັນ"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "ການແບ່ງປັນຖືກຖອນອອກ"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "ພື້ນທີ່ຖືກລຶບ"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "ພື້ນທີ່ຖືກປິດການໃຊ້ງານ"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "ການເປັນສະມາຊິກພື້ນທີ່ໝົດອາຍຸ"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "ປະຈຳອາທິດ"
@@ -0,0 +1,147 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Stephan Paternotte <stephan@paternottes.net>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-26 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Stephan Paternotte <stephan@paternottes.net>, 2025\n"
"Language-Team: Dutch (https://app.transifex.com/qsfera-eu/teams/204053/nl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: nl\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Toegevoegd als lid aan ruimte"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dagelijks"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interval van e-mails"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Bestand afgewezen"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantaan"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nooit"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Stuur een melding wanneer ik als lid aan een ruimte ben toegevoegd"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Meld me wanneer ik ben verwijderd als lid van een ruimte"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Stuur een melding wanneer iemand iets met mij deelt"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Stuur een melding wanneer een bestand dat ik heb geüpload is afgewezen "
"vanwege een virusinfectie of schending van het beleid."
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Stuur een melding wanneer een share met mij is ingetrokken"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Stuur een melding wanneer een share met mij is verlopen"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Stuur een melding wanneer een ruimte waarvan ik lid ben is verwijderd"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr ""
"Stuur een melding wanner een ruimte waarvan ik lid ben is uitgeschakeld"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Stuur een melding waneer het lidmaatschap aan een ruimte is verlopen"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Verwijderd als lid van een ruimte"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Geselecteerde waarde:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Share verlopen"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Share ontvangen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Share ingetrokken"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Ruimte verwijderd"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Ruimte uitgeschakeld"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Lidmaatschap van ruimte verlopen"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Wekelijks"
@@ -0,0 +1,148 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# skrzat <skrzacikus@gmail.com>, 2025
# Xiaomi Box, 2025
# Radoslaw Posim, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Radoslaw Posim, 2025\n"
"Language-Team: Polish (https://app.transifex.com/qsfera-eu/teams/204053/pl/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pl\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Dodano jako członek Przestrzeni"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dziennie"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Interwał wysyłania email"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Plik odrzucony"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Natychmiast"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nigdy"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Powiadom, jeśli dodano mnie jako członka przestrzeni"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Powiadom, jeśli usunięto mnie jako członka z przestrzeni."
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Powiadom o otrzymanym udostępnieniu."
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Poinformuj, jeśli przesłany plik został odrzucony z powodu wirusa lub "
"naruszenia zasad"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Powiadom, jeśli otrzymane udostępnienie zostało usunięte"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Powiadom, jeśli otrzymane udostępnienie wygasło"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Powiadom, jeśli Przestrzeń gdzie jestem członkiem została usunięta"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Poinformuj, jeśli przestrzeń gdzie jestem członkiem została wyłączona"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Powiadom, jeśli członkostwo w przestrzeni wygasło"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Usunięto z przestrzeni"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Wybrana wartość:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Udostępnienie wygasło"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Otrzymano udostępnienie"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Usunięto udostępnienie"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Przestrzeń usunięta"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Przestrzeń wyłączona"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Członkostwo w przestrzeni wygasło"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Co tydzień"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Mário Machado, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-06 00:01+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Mário Machado, 2025\n"
"Language-Team: Portuguese (https://app.transifex.com/qsfera-eu/teams/204053/pt/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Adicionado como membro do espaço"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Diário"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Intervalo de envio de e-mail"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Ficheiro rejeitado"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Instantâneo"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Nunca"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Notificar quando for adicionado como membro a um espaço"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Notificar quando for removido como membro de um espaço"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Notificar quando receber uma partilha"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Notificar quando um ficheiro que carreguei for rejeitado devido a uma "
"infeção por vírus ou violação de política"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Notificar quando uma partilha recebida for removida"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Notificar quando uma partilha recebida expirar"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Notificar quando um espaço do qual sou membro for eliminado"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Notificar quando um espaço do qual sou membro for desativado"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Notificar quando uma subscrição de espaço expirar"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Removido como membro do espaço"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valor selecionado:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Partilha expirada"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Partilha recebida"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Partilha removida"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Espaço eliminado"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Espaço desativado"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Subscrição de espaço expirada"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Semanal"
@@ -0,0 +1,151 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# yellow sky, 2025
# Lulufox, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Lulufox, 2025\n"
"Language-Team: Russian (https://app.transifex.com/qsfera-eu/teams/204053/ru/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: ru\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Добавлен как член пространства"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Ежедневно"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Интервал отправки электронных писем"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Файл отклонен"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Немедленно"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Никогда"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Уведомлять, когда меня добавляют в пространства"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Уведомлять, когда меня исключают из пространств"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr ""
"Уведомлять, когда мне предоставили доступ для совместного использования "
"ресура"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Уведомлять, когда в загруженный мной файл был отклонен из-за проверки на "
"вирусы"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr ""
"Уведомлять, когда у меня забрали доступ для совместного использования "
"ресурса"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Уведомлять, когда доступ для совместного использования ресурса истек"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Уведомлять, когда пространство, в котором я состою, было удалено"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Уведомлять, когда пространство, в котором я состою, было отключено"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Уведомлять, когда мое членство в пространстве истекло"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Удален как участник пространства"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Выбранное значение:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Доступ для совместного использования истек"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Получен доступ для совместного использования ресурса"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Доступ для совместного использования ресурса отозван"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Пространство удалено"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Пространство отключено"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Членство в пространстве истекло"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Еженедельно"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Davis Kaza, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: Davis Kaza, 2025\n"
"Language-Team: Swedish (https://app.transifex.com/qsfera-eu/teams/204053/sv/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: sv\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Tillagd som medlem i arbetsytan"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Dagligen"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Sändningsintervall för e-post"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Fil nekad"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Omedelbart"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Aldrig"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Meddela mig när jag blivit tillagd som medlem i en arbetsyta"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Meddela mig när jag tagits bort som medlem i en arbetsyta"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Meddela mig när jag tagit emot en delning"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Meddela mig när en fil jag laddat upp har nekats på grund av virus eller "
"policybrott"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Meddela när en mottagen delning har raderats"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Meddela när en mottagen delning har löpt ut"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Meddela när en arbetsyta jag är medlem i har raderats"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Meddela när en arbetsyta jag är medlem i har avaktiverats"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Meddela när ett medlemskap i en arbetsyta har löpt ut"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Borttagen som medlem i arbetsyta"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Valt värde:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Delning har löpt ut"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Delning mottagen"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Delning raderad"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Arbetsyta raderad"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Arbetsyta avaktiverad"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Medlemskap i arbetsyta har löpt ut"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Veckovis"
@@ -0,0 +1,146 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# LinkinWires <darkinsonic13@gmail.com>, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-05-08 00:02+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: LinkinWires <darkinsonic13@gmail.com>, 2025\n"
"Language-Team: Ukrainian (https://app.transifex.com/qsfera-eu/teams/204053/uk/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: uk\n"
"Plural-Forms: nplurals=4; plural=(n % 1 == 0 && n % 10 == 1 && n % 100 != 11 ? 0 : n % 1 == 0 && n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14) ? 1 : n % 1 == 0 && (n % 10 ==0 || (n % 10 >=5 && n % 10 <=9) || (n % 100 >=11 && n % 100 <=14 )) ? 2: 3);\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "Додавання до простору у якості учасника (-ці)"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "Щоденно"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "Інтервал надсилання електронних листів"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "Відхилення файлу"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "Миттєво"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "Ніколи"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "Сповіщати, коли мене додають як учасника (-цю) до простору"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "Сповіщати, коли мене видаляють з учасників простору"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "Сповіщати, коли зі мною поділилися файлом або папкою"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr ""
"Сповіщати, коли завантажений мною файл було відхилено через знайдений вірус "
"або порушення політик"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "Сповіщати, коли зі мною більше не діляться файлом або папкою"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "Сповіщати, коли термін доступу до файлу або папки сплинув"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "Сповіщати, коли простір, учасником (-цею) якого я є, був видалений"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "Сповіщати, коли простір, учасником (-цею) якого я є, був вимкнений"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "Сповіщати, коли термін доступу до простору сплинув"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "Видалення з простору"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "Обране значення:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "Сплинення доступу до файлу / папки"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "Надання доступу"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "Видалення доступу"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "Видалення простору"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "Вимкнення простору"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "Сплинення доступу до простору"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "Щотижнево"
@@ -0,0 +1,144 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# YQS Yang, 2025
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-04-22 00:03+0000\n"
"PO-Revision-Date: 2025-01-27 10:17+0000\n"
"Last-Translator: YQS Yang, 2025\n"
"Language-Team: Chinese (https://app.transifex.com/qsfera-eu/teams/204053/zh/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: zh\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#. name of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:20
msgid "Added as space member"
msgstr "添加为空间成员"
#. translation for the 'daily' email interval option
#: pkg/store/defaults/templates.go:50
msgid "Daily"
msgstr "每日"
#. name of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:44
msgid "Email sending interval"
msgstr "邮件发送频率"
#. name of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:40
msgid "File rejected"
msgstr "文件被拒绝"
#. translation for the 'instant' email interval option
#: pkg/store/defaults/templates.go:48
msgid "Instant"
msgstr "实时"
#. translation for the 'never' email interval option
#: pkg/store/defaults/templates.go:54
msgid "Never"
msgstr "从不"
#. description of the notification option 'Space Shared'
#: pkg/store/defaults/templates.go:22
msgid "Notify when I have been added as a member to a space"
msgstr "当我被添加为空间成员时通知我"
#. description of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:26
msgid "Notify when I have been removed as member from a space"
msgstr "当我被从空间中移除成员资格时通知我"
#. description of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:10
msgid "Notify when I have received a share"
msgstr "当我收到共享时通知我"
#. description of the notification option 'File Rejected'
#: pkg/store/defaults/templates.go:42
msgid ""
"Notify when a file I uploaded was rejected because of a virus infection or "
"policy violation"
msgstr "当我上传的文件因病毒感染或违反策略而被拒绝时通知我"
#. description of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:14
msgid "Notify when a received share has been removed"
msgstr "当收到的共享被移除时通知我"
#. description of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:18
msgid "Notify when a received share has expired"
msgstr "当收到的共享已过期时通知我"
#. description of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:38
msgid "Notify when a space I am member of has been deleted"
msgstr "当我所在的空间被删除时通知我"
#. description of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:34
msgid "Notify when a space I am member of has been disabled"
msgstr "当我所在的空间被禁用时通知我"
#. description of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:30
msgid "Notify when a space membership has expired"
msgstr "当空间成员资格已过期时通知我"
#. name of the notification option 'Space Unshared'
#: pkg/store/defaults/templates.go:24
msgid "Removed as space member"
msgstr "移除空间成员资格"
#. description of the notification option 'Email Interval'
#: pkg/store/defaults/templates.go:46
msgid "Selected value:"
msgstr "已选值:"
#. name of the notification option 'Share Expired'
#: pkg/store/defaults/templates.go:16
msgid "Share Expired"
msgstr "共享已过期"
#. name of the notification option 'Share Received'
#: pkg/store/defaults/templates.go:8
msgid "Share Received"
msgstr "收到共享"
#. name of the notification option 'Share Removed'
#: pkg/store/defaults/templates.go:12
msgid "Share Removed"
msgstr "共享已移除"
#. name of the notification option 'Space Deleted'
#: pkg/store/defaults/templates.go:36
msgid "Space deleted"
msgstr "空间已删除"
#. name of the notification option 'Space Disabled'
#: pkg/store/defaults/templates.go:32
msgid "Space disabled"
msgstr "空间已禁用"
#. name of the notification option 'Space Membership Expired'
#: pkg/store/defaults/templates.go:28
msgid "Space membership expired"
msgstr "空间成员资格已过期"
#. translation for the 'weekly' email interval option
#: pkg/store/defaults/templates.go:52
msgid "Weekly"
msgstr "每周"
@@ -0,0 +1,39 @@
package svc
import (
"net/http"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/settings/pkg/config"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Middleware provides a function to set the middleware option.
func Middleware(val ...func(http.Handler) http.Handler) Option {
return func(o *Options) {
o.Middleware = val
}
}
@@ -0,0 +1,59 @@
package svc
import (
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
)
func (g Service) hasPermission(
roleIDs []string,
resource *settingsmsg.Resource,
operations []settingsmsg.Permission_Operation,
constraint settingsmsg.Permission_Constraint,
) bool {
permissions, err := g.manager.ListPermissionsByResource(resource, roleIDs)
if err != nil {
g.logger.Debug().Err(err).
Str("resource-type", resource.Type.String()).
Str("resource-id", resource.Id).
Msg("permissions could not be loaded for resource")
return false
}
permissions = getFilteredPermissionsByOperations(permissions, operations)
return isConstraintFulfilled(permissions, constraint)
}
// filterPermissionsByOperations returns the subset of the given permissions, where at least one of the given operations is fulfilled.
func getFilteredPermissionsByOperations(permissions []*settingsmsg.Permission, operations []settingsmsg.Permission_Operation) []*settingsmsg.Permission {
var filteredPermissions []*settingsmsg.Permission
for _, permission := range permissions {
if isAnyOperationFulfilled(permission, operations) {
filteredPermissions = append(filteredPermissions, permission)
}
}
return filteredPermissions
}
// isAnyOperationFulfilled checks if the permissions is about any of the operations
func isAnyOperationFulfilled(permission *settingsmsg.Permission, operations []settingsmsg.Permission_Operation) bool {
for _, operation := range operations {
if operation == permission.Operation {
return true
}
}
return false
}
// isConstraintFulfilled checks if one of the permissions has the same or a parent of the constraint.
// this is only a comparison on ENUM level. More sophisticated checks cannot happen here...
func isConstraintFulfilled(permissions []*settingsmsg.Permission, constraint settingsmsg.Permission_Constraint) bool {
for _, permission := range permissions {
// comparing enum by order is not a feasible solution, because `SHARED` is not a superset of `OWN`.
if permission.Constraint == settingsmsg.Permission_CONSTRAINT_ALL {
return true
}
if permission.Constraint != settingsmsg.Permission_CONSTRAINT_UNKNOWN && permission.Constraint == constraint {
return true
}
}
return false
}
@@ -0,0 +1,720 @@
package svc
import (
"context"
"embed"
"errors"
"fmt"
"strings"
cs3permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/leonelquinteros/gotext"
"github.com/qsfera/server/pkg/l10n"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/pkg/middleware"
"github.com/qsfera/server/pkg/roles"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
metastore "github.com/qsfera/server/services/settings/pkg/store/metadata"
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
"google.golang.org/protobuf/types/known/emptypb"
)
//go:embed l10n/locale
var _translationFS embed.FS
var _domain = "settings"
// Service represents a service.
type Service struct {
id string
config *config.Config
logger log.Logger
manager settings.Manager
}
// NewService returns a service implementation for Service.
func NewService(cfg *config.Config, logger log.Logger) settings.ServiceHandler {
service := Service{
id: "qsfera-settings",
config: cfg,
logger: logger,
}
service.manager = metastore.New(cfg)
return service
}
// CheckPermission implements the CS3 API Permssions service.
// It's used to check if a subject (user or group) has a permission.
func (g Service) CheckPermission(ctx context.Context, req *cs3permissions.CheckPermissionRequest) (*cs3permissions.CheckPermissionResponse, error) {
spec := req.GetSubjectRef().GetSpec()
var accountID string
switch ref := spec.(type) {
case *cs3permissions.SubjectReference_UserId:
accountID = ref.UserId.GetOpaqueId()
case *cs3permissions.SubjectReference_GroupId:
accountID = ref.GroupId.GetOpaqueId()
}
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
return &cs3permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
roleIDs := make([]string, 0, len(assignments))
for _, a := range assignments {
roleIDs = append(roleIDs, a.GetRoleId())
}
permission, err := g.manager.ReadPermissionByName(req.GetPermission(), roleIDs)
if err != nil {
if !errors.Is(err, settings.ErrNotFound) {
return &cs3permissions.CheckPermissionResponse{
Status: status.NewInternal(ctx, err.Error()),
}, nil
}
}
if permission == nil {
return &cs3permissions.CheckPermissionResponse{
Status: &rpcv1beta1.Status{
Code: rpcv1beta1.Code_CODE_PERMISSION_DENIED,
},
}, nil
}
return &cs3permissions.CheckPermissionResponse{
Status: status.NewOK(ctx),
}, nil
}
// TODO: check permissions on every request
// SaveBundle implements the BundleServiceHandler interface
func (g Service) SaveBundle(ctx context.Context, req *settingssvc.SaveBundleRequest, res *settingssvc.SaveBundleResponse) error {
cleanUpResource(ctx, req.GetBundle().GetResource())
if err := g.checkStaticPermissionsByBundleType(ctx, req.GetBundle().GetType()); err != nil {
return err
}
if validationError := validateSaveBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.WriteBundle(req.GetBundle())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Bundle = r
return nil
}
// GetBundle implements the BundleServiceHandler interface
func (g Service) GetBundle(ctx context.Context, req *settingssvc.GetBundleRequest, res *settingssvc.GetBundleResponse) error {
if validationError := validateGetBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
bundle, err := g.manager.ReadBundle(req.GetBundleId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
filteredBundle := g.getFilteredBundle(g.getRoleIDs(ctx), bundle)
if len(filteredBundle.GetSettings()) == 0 {
err = fmt.Errorf("could not read bundle: %s", req.GetBundleId())
return merrors.NotFound(g.id, "%s", err)
}
res.Bundle = filteredBundle
return nil
}
// ListBundles implements the BundleServiceHandler interface
func (g Service) ListBundles(ctx context.Context, req *settingssvc.ListBundlesRequest, res *settingssvc.ListBundlesResponse) error {
// fetch all bundles
if validationError := validateListBundles(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
bundles, err := g.manager.ListBundles(settingsmsg.Bundle_TYPE_DEFAULT, req.GetBundleIds())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
roleIDs := g.getRoleIDs(ctx)
// find user locale
var locale string
if u, ok := ctxpkg.ContextGetUser(ctx); ok {
var err error
locale, err = g.getUserLocale(ctx, u.GetId().GetOpaqueId())
if err != nil {
g.logger.Error().Err(err).Str("userid", u.GetId().GetOpaqueId()).Msg("failed to get user locale")
}
}
// filter settings in bundles that are allowed according to roles
var filteredBundles []*settingsmsg.Bundle
for _, bundle := range bundles {
filteredBundle := g.getFilteredBundle(roleIDs, bundle)
if len(filteredBundle.GetSettings()) > 0 {
t := l10n.NewTranslatorFromCommonConfig(g.config.DefaultLanguage, _domain, g.config.TranslationPath, _translationFS, "l10n/locale").Locale(locale)
filteredBundles = append(filteredBundles, translateBundle(filteredBundle, t))
}
}
res.Bundles = filteredBundles
return nil
}
func (g Service) getFilteredBundle(roleIDs []string, bundle *settingsmsg.Bundle) *settingsmsg.Bundle {
// check if full bundle is whitelisted
bundleResource := &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_BUNDLE,
Id: bundle.GetId(),
}
if g.hasPermission(
roleIDs,
bundleResource,
[]settingsmsg.Permission_Operation{settingsmsg.Permission_OPERATION_READ, settingsmsg.Permission_OPERATION_READWRITE},
settingsmsg.Permission_CONSTRAINT_OWN,
) {
return bundle
}
// filter settings based on permissions
var filteredSettings []*settingsmsg.Setting
for _, setting := range bundle.GetSettings() {
settingResource := &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_SETTING,
Id: setting.GetId(),
}
if g.hasPermission(
roleIDs,
settingResource,
[]settingsmsg.Permission_Operation{settingsmsg.Permission_OPERATION_READ, settingsmsg.Permission_OPERATION_READWRITE},
settingsmsg.Permission_CONSTRAINT_OWN,
) {
filteredSettings = append(filteredSettings, setting)
}
}
bundle.Settings = filteredSettings
return bundle
}
// AddSettingToBundle implements the BundleServiceHandler interface
func (g Service) AddSettingToBundle(ctx context.Context, req *settingssvc.AddSettingToBundleRequest, res *settingssvc.AddSettingToBundleResponse) error {
cleanUpResource(ctx, req.GetSetting().GetResource())
if err := g.checkStaticPermissionsByBundleID(ctx, req.GetBundleId()); err != nil {
return err
}
if validationError := validateAddSettingToBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.AddSettingToBundle(req.GetBundleId(), req.GetSetting())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Setting = r
return nil
}
// RemoveSettingFromBundle implements the BundleServiceHandler interface
func (g Service) RemoveSettingFromBundle(ctx context.Context, req *settingssvc.RemoveSettingFromBundleRequest, _ *emptypb.Empty) error {
if err := g.checkStaticPermissionsByBundleID(ctx, req.GetBundleId()); err != nil {
return err
}
if validationError := validateRemoveSettingFromBundle(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
if err := g.manager.RemoveSettingFromBundle(req.GetBundleId(), req.GetSettingId()); err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
return nil
}
// SaveValue implements the ValueServiceHandler interface
func (g Service) SaveValue(ctx context.Context, req *settingssvc.SaveValueRequest, res *settingssvc.SaveValueResponse) error {
req.Value.AccountUuid = getValidatedAccountUUID(ctx, req.GetValue().GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetValue().GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't save value for another user")
}
cleanUpResource(ctx, req.GetValue().GetResource())
// TODO: we need to check, if the authenticated user has permission to write the value for the specified resource (e.g. global, file with id xy, ...)
if validationError := validateSaveValue(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError.Error())
}
r, err := g.manager.WriteValue(req.GetValue())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
valueWithIdentifier, err := g.getValueWithIdentifier(r)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
return nil
}
// GetValue implements the ValueServiceHandler interface
func (g Service) GetValue(_ context.Context, req *settingssvc.GetValueRequest, res *settingssvc.GetValueResponse) error {
if validationError := validateGetValue(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ReadValue(req.GetId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
valueWithIdentifier, err := g.getValueWithIdentifier(r)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
return nil
}
// GetValueByUniqueIdentifiers implements the ValueService interface
func (g Service) GetValueByUniqueIdentifiers(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, res *settingssvc.GetValueResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't get value of another user")
}
if validationError := validateGetValueByUniqueIdentifiers(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
v, err := g.manager.ReadValueByUniqueIdentifiers(req.GetAccountUuid(), req.GetSettingId())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
if v.GetBundleId() != "" {
valueWithIdentifier, err := g.getValueWithIdentifier(v)
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Value = valueWithIdentifier
}
return nil
}
// ListValues implements the ValueServiceHandler interface
func (g Service) ListValues(ctx context.Context, req *settingssvc.ListValuesRequest, res *settingssvc.ListValuesResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if !g.isCurrentUser(ctx, req.GetAccountUuid()) {
return merrors.Forbidden(g.id, "can't list values of another user")
}
if validationError := validateListValues(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
values, err := g.manager.ListValues(req.GetBundleId(), req.GetAccountUuid())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
result := make([]*settingsmsg.ValueWithIdentifier, 0, len(values))
for _, value := range values {
valueWithIdentifier, err := g.getValueWithIdentifier(value)
if err == nil {
result = append(result, valueWithIdentifier)
}
}
res.Values = result
return nil
}
// ListRoles implements the RoleServiceHandler interface
func (g Service) ListRoles(_ context.Context, req *settingssvc.ListBundlesRequest, res *settingssvc.ListBundlesResponse) error {
//accountUUID := getValidatedAccountUUID(c, "me")
if validationError := validateListRoles(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListBundles(settingsmsg.Bundle_TYPE_ROLE, req.GetBundleIds())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
// TODO: only allow listing roles when user has account/role/... management permissions
res.Bundles = r
return nil
}
// ListRoleAssignments implements the RoleServiceHandler interface
func (g Service) ListRoleAssignments(ctx context.Context, req *settingssvc.ListRoleAssignmentsRequest, res *settingssvc.ListRoleAssignmentsResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if validationError := validateListRoleAssignments(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
r, err := g.manager.ListRoleAssignments(req.GetAccountUuid())
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Assignments = r
return nil
}
// ListRoleAssignmentsFiltered implements the RoleServiceHandler interface. Who made this up? And why is everyone copying it? So this methods lists role assignments filtered by account or role.
func (g Service) ListRoleAssignmentsFiltered(ctx context.Context, req *settingssvc.ListRoleAssignmentsFilteredRequest, res *settingssvc.ListRoleAssignmentsResponse) error {
if validationError := validateListRoleAssignmentsFiltered(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
filters := req.GetFilters()
var r []*settingsmsg.UserRoleAssignment
var err error
switch filters[0].GetType() {
case settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT:
accountUUID := getValidatedAccountUUID(ctx, filters[0].GetAccountUuid())
r, err = g.manager.ListRoleAssignments(accountUUID)
case settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE:
roleID := filters[0].GetRoleId()
r, err = g.manager.ListRoleAssignmentsByRole(roleID)
}
if err != nil {
return merrors.NotFound(g.id, "%s", err)
}
res.Assignments = r
return nil
}
// AssignRoleToUser implements the RoleServiceHandler interface
func (g Service) AssignRoleToUser(ctx context.Context, req *settingssvc.AssignRoleToUserRequest, res *settingssvc.AssignRoleToUserResponse) error {
req.AccountUuid = getValidatedAccountUUID(ctx, req.GetAccountUuid())
if validationError := validateAssignRoleToUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
switch {
case ownAccountUUID == req.GetAccountUuid():
// Allow users to assign themself to the user or user light role
// deny any other attempt to change the user's own assignment
if r, err := g.manager.ListRoleAssignments(req.GetAccountUuid()); err == nil && len(r) > 0 {
return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
}
if req.GetRoleId() != defaults.BundleUUIDRoleUser && req.GetRoleId() != defaults.BundleUUIDRoleUserLight {
return merrors.Forbidden(g.id, "Changing own role assignment forbidden")
}
g.logger.Debug().Str("userid", ownAccountUUID).Msg("Self-assignment for default 'user' role permitted")
case g.canManageRoles(ctx):
default:
return merrors.Forbidden(g.id, "user has no role management permission")
}
r, err := g.manager.WriteRoleAssignment(req.GetAccountUuid(), req.GetRoleId())
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Assignment = r
return nil
}
// RemoveRoleFromUser implements the RoleServiceHandler interface
func (g Service) RemoveRoleFromUser(ctx context.Context, req *settingssvc.RemoveRoleFromUserRequest, _ *emptypb.Empty) error {
if !g.canManageRoles(ctx) {
return merrors.Forbidden(g.id, "user has no role management permission")
}
if validationError := validateRemoveRoleFromUser(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
al, err := g.manager.ListRoleAssignments(ownAccountUUID)
if err != nil {
g.logger.Debug().Err(err).Str("id", g.id).Msg("ListRoleAssignments failed")
return merrors.InternalServerError(g.id, "%s", err)
}
for _, a := range al {
if a.GetId() == req.GetId() {
g.logger.Debug().Str("id", g.id).Msg("Removing own role assignment forbidden")
return merrors.Forbidden(g.id, "Removing own role assignment forbidden")
}
}
if err := g.manager.RemoveRoleAssignment(req.GetId()); err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
return nil
}
// ListPermissions implements the PermissionServiceHandler interface
func (g Service) ListPermissions(ctx context.Context, req *settingssvc.ListPermissionsRequest, res *settingssvc.ListPermissionsResponse) error {
ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
g.logger.Debug().Str("id", g.id).Msg("user not in context")
return merrors.InternalServerError(g.id, "user not in context")
}
if ownAccountUUID != req.GetAccountUuid() {
return merrors.NotFound(g.id, "user not found: %s", req.GetAccountUuid())
}
assignments, err := g.manager.ListRoleAssignments(req.GetAccountUuid())
if err != nil {
return err
}
// deduplicate role ids
roleIDs := map[string]struct{}{}
for _, a := range assignments {
roleIDs[a.GetRoleId()] = struct{}{}
}
// deduplicate permission names
permissionNames := map[string]struct{}{}
for roleID := range roleIDs {
bundle, err := g.manager.ReadBundle(roleID)
if err != nil {
if !errors.Is(err, settings.ErrNotFound) {
return err
}
continue
}
if bundle != nil {
for _, setting := range bundle.GetSettings() {
permissionNames[formatPermissionName(setting)] = struct{}{}
}
}
}
res.Permissions = make([]string, 0, len(permissionNames))
for p := range permissionNames {
res.Permissions = append(res.Permissions, p)
}
return nil
}
// ListPermissionsByResource implements the PermissionServiceHandler interface
func (g Service) ListPermissionsByResource(ctx context.Context, req *settingssvc.ListPermissionsByResourceRequest, res *settingssvc.ListPermissionsByResourceResponse) error {
if validationError := validateListPermissionsByResource(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
permissions, err := g.manager.ListPermissionsByResource(req.GetResource(), g.getRoleIDs(ctx))
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
res.Permissions = permissions
return nil
}
// GetPermissionByID implements the PermissionServiceHandler interface
func (g Service) GetPermissionByID(ctx context.Context, req *settingssvc.GetPermissionByIDRequest, res *settingssvc.GetPermissionByIDResponse) error {
if validationError := validateGetPermissionByID(req); validationError != nil {
return merrors.BadRequest(g.id, "%s", validationError)
}
permission, err := g.manager.ReadPermissionByID(req.GetPermissionId(), g.getRoleIDs(ctx))
if err != nil {
return merrors.BadRequest(g.id, "%s", err)
}
if permission == nil {
return merrors.NotFound(g.id, "%s", fmt.Errorf("permission %s not found in roles", req.GetPermissionId()))
}
res.Permission = permission
return nil
}
// cleanUpResource makes sure that the account uuid of the authenticated user is injected if needed.
func cleanUpResource(ctx context.Context, resource *settingsmsg.Resource) {
if resource != nil && resource.GetType() == settingsmsg.Resource_TYPE_USER {
resource.Id = getValidatedAccountUUID(ctx, resource.GetId())
}
}
// getValidatedAccountUUID converts `me` into an actual account uuid from the context, if possible.
// the result of this function will always be a valid lower-case UUID or an empty string.
func getValidatedAccountUUID(ctx context.Context, accountUUID string) string {
if accountUUID == "me" {
if ownAccountUUID, ok := metadata.Get(ctx, middleware.AccountID); ok {
accountUUID = ownAccountUUID
}
}
if accountUUID == "me" {
// no matter what happens above, an accountUUID of `me` must not be passed on. Clear it instead.
accountUUID = ""
}
return accountUUID
}
// getRoleIDs extracts the roleIDs of the authenticated user from the context.
func (g Service) getRoleIDs(ctx context.Context) []string {
if ownRoleIDs, ok := roles.ReadRoleIDsFromContext(ctx); ok {
return ownRoleIDs
}
if accountID, ok := metadata.Get(ctx, middleware.AccountID); ok {
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
g.logger.Info().Err(err).Str("userid", accountID).Msg("failed to get roles for user")
return nil
}
ownRoleIDs := make([]string, 0, len(assignments))
for _, a := range assignments {
ownRoleIDs = append(ownRoleIDs, a.GetRoleId())
}
return ownRoleIDs
}
g.logger.Info().Msg("failed to get accountID from context")
return nil
}
func (g Service) getValueWithIdentifier(value *settingsmsg.Value) (*settingsmsg.ValueWithIdentifier, error) {
bundle, err := g.manager.ReadBundle(value.GetBundleId())
if err != nil {
return nil, err
}
setting, err := g.manager.ReadSetting(value.GetSettingId())
if err != nil {
return nil, err
}
return &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: bundle.GetExtension(),
Bundle: bundle.GetName(),
Setting: setting.GetName(),
},
Value: value,
}, nil
}
func (g Service) hasStaticPermission(ctx context.Context, permissionID string) bool {
roleIDs, ok := roles.ReadRoleIDsFromContext(ctx)
if !ok {
// TODO add system role for internal requests.
// - at least the proxy needs to look up account info
// - glauth needs to make bind requests
// tracked as OCIS-454
accountID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
return false
}
assignments, err := g.manager.ListRoleAssignments(accountID)
if err != nil {
return false
}
// deduplicate roleids
uniqueRoleIDs := make(map[string]struct{})
for _, a := range assignments {
uniqueRoleIDs[a.GetRoleId()] = struct{}{}
}
roleIDs = make([]string, 0, len(uniqueRoleIDs))
for a := range uniqueRoleIDs {
roleIDs = append(roleIDs, a)
}
}
p, err := g.manager.ReadPermissionByID(permissionID, roleIDs)
return err == nil && p != nil
}
func (g Service) checkStaticPermissionsByBundleID(ctx context.Context, bundleID string) error {
bundle, err := g.manager.ReadBundle(bundleID)
if err != nil {
return merrors.NotFound(g.id, "bundle not found: %s", err)
}
return g.checkStaticPermissionsByBundleType(ctx, bundle.GetType())
}
func (g Service) checkStaticPermissionsByBundleType(ctx context.Context, bundleType settingsmsg.Bundle_Type) error {
if bundleType == settingsmsg.Bundle_TYPE_ROLE {
if !g.hasStaticPermission(ctx, RoleManagementPermissionID) {
return merrors.Forbidden(g.id, "user has no role management permission")
}
return nil
}
if !g.hasStaticPermission(ctx, SettingsManagementPermissionID) {
return merrors.Forbidden(g.id, "user has no settings management permission")
}
return nil
}
func (g Service) isCurrentUser(ctx context.Context, accountID string) bool {
ownAccountID, ok := metadata.Get(ctx, middleware.AccountID)
if !ok {
return false
}
return accountID == ownAccountID
}
func (g Service) canManageRoles(ctx context.Context) bool {
return g.hasStaticPermission(ctx, RoleManagementPermissionID)
}
func (g Service) getUserLocale(ctx context.Context, userID string) (string, error) {
var resp settingssvc.GetValueResponse
err := g.GetValueByUniqueIdentifiers(
ctx,
&settingssvc.GetValueByUniqueIdentifiersRequest{
AccountUuid: userID,
SettingId: defaults.SettingUUIDProfileLanguage,
},
&resp,
)
if err != nil {
return "", err
}
val := resp.GetValue().GetValue().GetListValue().GetValues()
if len(val) == 0 {
return "", errors.New("no language setting found")
}
return val[0].GetStringValue(), nil
}
func formatPermissionName(setting *settingsmsg.Setting) string {
constraint := strings.TrimPrefix(setting.GetPermissionValue().GetConstraint().String(), "CONSTRAINT_")
return setting.GetName() + "." + strings.ToLower(constraint)
}
func translateBundle(bundle *settingsmsg.Bundle, t *gotext.Locale) *settingsmsg.Bundle {
for i, set := range bundle.GetSettings() {
switch set.GetId() {
default:
continue
case defaults.SettingUUIDProfileEmailSendingInterval:
// translate interval names ('Instant', 'Daily', 'Weekly', 'Never')
value := set.GetSingleChoiceValue()
for i, v := range value.GetOptions() {
value.Options[i].DisplayValue = t.Get(v.GetDisplayValue(), []any{}...)
}
set.Value = &settingsmsg.Setting_SingleChoiceValue{SingleChoiceValue: value}
fallthrough
case defaults.SettingUUIDProfileEventShareCreated,
defaults.SettingUUIDProfileEventShareRemoved,
defaults.SettingUUIDProfileEventShareExpired,
defaults.SettingUUIDProfileEventSpaceShared,
defaults.SettingUUIDProfileEventSpaceUnshared,
defaults.SettingUUIDProfileEventSpaceMembershipExpired,
defaults.SettingUUIDProfileEventSpaceDisabled,
defaults.SettingUUIDProfileEventSpaceDeleted:
// translate event names ('Share Received', 'Share Removed', ...)
set.DisplayName = t.Get(set.GetDisplayName(), []any{}...)
// translate event descriptions ('Notify me when I receive a share', ...)
set.Description = t.Get(set.GetDescription(), []any{}...)
bundle.Settings[i] = set
}
}
return bundle
}
@@ -0,0 +1,461 @@
package svc
import (
"context"
"net/http"
"testing"
"github.com/qsfera/server/pkg/middleware"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
v0 "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/settings/mocks"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
merrors "go-micro.dev/v4/errors"
"go-micro.dev/v4/metadata"
)
var (
ctxWithUUID = metadata.Set(context.Background(), middleware.AccountID, "61445573-4dbe-4d56-88dc-88ab47aceba7")
ctxWithEmptyUUID = metadata.Set(context.Background(), middleware.AccountID, "")
emptyCtx = context.Background()
scenarios = []struct {
name string
accountUUID string
ctx context.Context
expect string
}{
{
name: "context with UUID; identifier = 'me'",
ctx: ctxWithUUID,
accountUUID: "me",
expect: "61445573-4dbe-4d56-88dc-88ab47aceba7",
},
{
name: "context with empty UUID; identifier = 'me'",
ctx: ctxWithEmptyUUID,
accountUUID: "me",
expect: "",
},
{
name: "context without UUID; identifier = 'me'",
ctx: emptyCtx,
accountUUID: "me",
expect: "",
},
{
name: "context with UUID; identifier not 'me'",
ctx: ctxWithUUID,
accountUUID: "",
expect: "",
},
}
)
func TestGetValidatedAccountUUID(t *testing.T) {
for _, s := range scenarios {
scenario := s
t.Run(scenario.name, func(t *testing.T) {
got := getValidatedAccountUUID(scenario.ctx, scenario.accountUUID)
assert.NotPanics(t, func() {
getValidatedAccountUUID(emptyCtx, scenario.accountUUID)
})
assert.Equal(t, scenario.expect, got)
})
}
}
func TestEditOwnRoleAssignment(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
a := []*settingsmsg.UserRoleAssignment{}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
// Creating an initial self assignment is expected to succeed for UserRole when no assignment exists yet
req := v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUser,
}
res := v0.AssignRoleToUserResponse{}
err := svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
// Creating an initial self assignment is expected to succeed for UserLightRole when no assignment exists yet
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUserLight,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
// Creating an initial self assignment is expected to fail for non UserRole when no assignment exists yet
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleAdmin,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.NotNil(t, err)
manager = &mocks.Manager{}
svc = Service{
manager: manager,
}
a = []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
editRolePermission := &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
// Creating an self assignment is expect to fail if there is already an assingment
req = v0.AssignRoleToUserRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: defaults.BundleUUIDRoleUser,
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.NotNil(t, err)
manager.On("WriteRoleAssignment", mock.Anything, mock.Anything).Return(nil, nil)
// Creating an assignment for somebody else is expected to succeed, give the right permissions
req = v0.AssignRoleToUserRequest{
AccountUuid: "00000000-0000-0000-0000-000000000000",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
}
res = v0.AssignRoleToUserResponse{}
err = svc.AssignRoleToUser(ctxWithUUID, &req, &res)
assert.Nil(t, err)
}
func TestRemoveOwnRoleAssignment(t *testing.T) {
manager := &mocks.Manager{}
a := []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
editRolePermission := &settingsmsg.Permission{
Operation: settingsmsg.Permission_OPERATION_READWRITE,
Constraint: settingsmsg.Permission_CONSTRAINT_ALL,
}
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
svc := Service{
manager: manager,
}
// Removing a role for oneself is expected to fail
req := v0.RemoveRoleFromUserRequest{
Id: "00000000-0000-0000-0000-000000000001",
}
err := svc.RemoveRoleFromUser(ctxWithUUID, &req, nil)
assert.NotNil(t, err)
manager = &mocks.Manager{}
manager.On("ListRoleAssignments", mock.Anything).Return(nil, nil)
manager.On("RemoveRoleAssignment", mock.Anything).Return(nil)
manager.On("ReadPermissionByID", mock.Anything, mock.Anything).Return(editRolePermission, nil)
svc = Service{
manager: manager,
}
// Removing a role for someone else is expected to fail
req = v0.RemoveRoleFromUserRequest{
Id: "00000000-0000-0000-0000-000000000002",
}
err = svc.RemoveRoleFromUser(ctxWithUUID, &req, nil)
assert.Nil(t, err)
}
func TestListPermissionsOfCurrentUser(t *testing.T) {
manager := &mocks.Manager{}
a := []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
}
manager.On("ListRoleAssignments", mock.Anything).Return(a, nil)
b := &settingsmsg.Bundle{
Id: "aceb15b8-7486-479f-ae32-c91118e07a39",
Settings: []*settingsmsg.Setting{
{
Name: "some-permission",
},
{
Name: "other-permission",
},
{
Name: "duplicate-permission",
},
{
Name: "duplicate-permission",
},
},
}
manager.On("ReadBundle", mock.Anything).Return(b, nil)
svc := Service{
manager: manager,
}
// Listing permissions for yourself
req := v0.ListPermissionsRequest{
AccountUuid: "61445573-4dbe-4d56-88dc-88ab47aceba7",
}
res := v0.ListPermissionsResponse{}
err := svc.ListPermissions(ctxWithUUID, &req, &res)
assert.NoError(t, err)
assert.Len(t, res.Permissions, 3)
}
func TestListPermissionsOfOtherUser(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
// Listing permissions for another user produces a not found error
req := v0.ListPermissionsRequest{
AccountUuid: "66666666-4444-4444-8888-88ab47aceba7",
}
res := v0.ListPermissionsResponse{}
err := svc.ListPermissions(ctxWithUUID, &req, &res)
assert.Error(t, err)
// assert the requested account uuid was not found
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(http.StatusNotFound), merr.Code)
assert.Contains(t, err.Error(), req.AccountUuid)
}
func TestListRoleAssignmentsFilteredValidation(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
tests := map[string]struct {
req *v0.ListRoleAssignmentsFilteredRequest
statusCode int32
}{
"no filters": {
req: &v0.ListRoleAssignmentsFilteredRequest{},
statusCode: http.StatusBadRequest,
},
"multiple filters": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: "uid",
},
},
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: "rid",
},
},
},
},
statusCode: http.StatusBadRequest,
},
"bad filtertype": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_UNKNOWN,
},
},
},
statusCode: http.StatusBadRequest,
},
"account filter without term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
},
},
},
statusCode: http.StatusBadRequest,
},
"account filter with invalid term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: "invalid-&*&^%$#",
},
},
},
},
statusCode: http.StatusBadRequest,
},
"role filter without term": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
},
},
},
statusCode: http.StatusBadRequest,
},
"role filter with invalid uuid": {
req: &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: "this is no uuid",
},
},
},
},
statusCode: http.StatusBadRequest,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, test.req, &res)
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.statusCode), merr.Code)
})
}
}
func TestListRoleAssignmentsFilteredByAccount(t *testing.T) {
accountUUID := "61445573-4dbe-4d56-88dc-88ab47aceba7"
tests := map[string]struct {
result []*settingsmsg.UserRoleAssignment
err error
status int32
}{
"handles manager error": {
result: nil,
err: assert.AnError,
status: http.StatusNotFound,
},
"succeeds with results": {
result: []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: accountUUID,
RoleId: "aceb15b8-7486-479f-ae32-c91118e07a39",
},
},
err: nil,
status: http.StatusOK,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
manager.On("ListRoleAssignments", mock.Anything).Return(test.result, test.err)
req := &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
Term: &settingsmsg.UserRoleAssignmentFilter_AccountUuid{
AccountUuid: accountUUID,
},
},
},
}
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res)
switch test.err {
case nil:
assert.Nil(t, err)
default:
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.status), merr.Code)
}
})
}
}
func TestListRoleAssignmentsFilteredByRole(t *testing.T) {
roleID := "61445573-4dbe-4d56-88dc-88ab47aceba7"
tests := map[string]struct {
result []*settingsmsg.UserRoleAssignment
err error
status int32
}{
"handles manager error": {
result: nil,
err: assert.AnError,
status: http.StatusNotFound,
},
"succeeds with results": {
result: []*settingsmsg.UserRoleAssignment{
{
Id: "00000000-0000-0000-0000-000000000001",
AccountUuid: "aceb15b8-7486-479f-ae32-c91118e07a39",
RoleId: roleID,
},
},
err: nil,
status: http.StatusOK,
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
manager := &mocks.Manager{}
svc := Service{
manager: manager,
}
manager.On("ListRoleAssignmentsByRole", mock.Anything).Return(test.result, test.err)
req := &v0.ListRoleAssignmentsFilteredRequest{
Filters: []*settingsmsg.UserRoleAssignmentFilter{
{
Type: settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
Term: &settingsmsg.UserRoleAssignmentFilter_RoleId{
RoleId: roleID,
},
},
},
}
res := v0.ListRoleAssignmentsResponse{}
err := svc.ListRoleAssignmentsFiltered(ctxWithUUID, req, &res)
switch test.err {
case nil:
assert.Nil(t, err)
default:
merr, ok := merrors.As(err)
assert.True(t, ok)
assert.Equal(t, int32(test.status), merr.Code)
}
})
}
}
@@ -0,0 +1,197 @@
package svc
import (
"context"
"strings"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/config"
"github.com/qsfera/server/services/settings/pkg/settings"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
)
var _defaultLanguage = "en"
// NewDefaultLanguageService returns a default language decorator for ServiceHandler.
func NewDefaultLanguageService(cfg *config.Config, serviceHandler settings.ServiceHandler) settings.ServiceHandler {
defaultLanguage := cfg.DefaultLanguage
if defaultLanguage == "" {
defaultLanguage = _defaultLanguage
}
return &defaultLanguageDecorator{defaultLanguage: defaultLanguage, ServiceHandler: serviceHandler}
}
type defaultLanguageDecorator struct {
defaultLanguage string
settings.ServiceHandler
}
// GetValueByUniqueIdentifiers implements the ValueService interface
func (s *defaultLanguageDecorator) GetValueByUniqueIdentifiers(ctx context.Context, req *settingssvc.GetValueByUniqueIdentifiersRequest, res *settingssvc.GetValueResponse) error {
err := s.ServiceHandler.GetValueByUniqueIdentifiers(ctx, req, res)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") && res.GetValue() == nil {
defaultValueList := getDefaultValueList()
// Ensure the default values for profile settings
if _, ok := defaultValueList[req.GetSettingId()]; ok {
res.Value = s.withDefaultProfileValue(ctx, req.AccountUuid, req.GetSettingId())
return nil
}
}
return err
}
return nil
}
// ListValues implements the ValueServiceHandler interface
func (s *defaultLanguageDecorator) ListValues(ctx context.Context, req *settingssvc.ListValuesRequest, res *settingssvc.ListValuesResponse) error {
err := s.ServiceHandler.ListValues(ctx, req, res)
if err != nil {
return err
}
defaultValueList := getDefaultValueList()
for _, v := range res.Values {
delete(defaultValueList, v.GetValue().GetSettingId())
}
// Ensure the default values for profile settings
defaultValueList = s.withDefaultProfileValueList(ctx, req.AccountUuid, defaultValueList)
if len(defaultValueList) > 0 {
for _, v := range defaultValueList {
if v != nil {
res.Values = append(res.Values, v)
}
}
}
return nil
}
func (s *defaultLanguageDecorator) withDefaultLanguageSetting(accountUUID string) *settingsmsg.ValueWithIdentifier {
return &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: "qsfera-accounts",
Bundle: "profile",
Setting: "language",
},
Value: &settingsmsg.Value{
BundleId: defaults.BundleUUIDProfile,
SettingId: defaults.SettingUUIDProfileLanguage,
AccountUuid: accountUUID,
Resource: &settingsmsg.Resource{
Type: settingsmsg.Resource_TYPE_USER,
},
Value: &settingsmsg.Value_ListValue{
ListValue: &settingsmsg.ListValue{Values: []*settingsmsg.ListOptionValue{
{
Option: &settingsmsg.ListOptionValue_StringValue{
StringValue: s.defaultLanguage,
},
},
}},
},
},
}
}
func (s *defaultLanguageDecorator) withDefaultProfileValue(ctx context.Context, accountUUID string, settingId string) *settingsmsg.ValueWithIdentifier {
if settingId == defaults.SettingUUIDProfileLanguage {
return s.withDefaultLanguageSetting(accountUUID)
}
res := s.withDefaultProfileValueList(ctx, accountUUID, map[string]*settingsmsg.ValueWithIdentifier{settingId: nil})
if v, ok := res[settingId]; ok {
return v
}
return nil
}
func (s *defaultLanguageDecorator) withDefaultProfileValueList(ctx context.Context,
accountUUID string, requested map[string]*settingsmsg.ValueWithIdentifier) map[string]*settingsmsg.ValueWithIdentifier {
// we use the default profile bundle instead of s.GetBundle(ctx, req, resp)
bundle := defaults.GenerateDefaultProfileBundle()
for _, setting := range bundle.GetSettings() {
if v, ok := requested[setting.GetId()]; !ok || v != nil {
continue
}
if setting.GetId() == defaults.SettingUUIDProfileLanguage {
requested[setting.GetId()] = s.withDefaultLanguageSetting(accountUUID)
continue
}
newVal := &settingsmsg.ValueWithIdentifier{
Identifier: &settingsmsg.Identifier{
Extension: bundle.GetExtension(),
Bundle: bundle.GetName(),
Setting: setting.GetName(),
},
Value: &settingsmsg.Value{
BundleId: bundle.GetId(),
SettingId: setting.GetId(),
AccountUuid: accountUUID,
Resource: setting.GetResource(),
},
}
switch val := setting.GetValue().(type) {
case *settingsmsg.Setting_MultiChoiceCollectionValue:
newVal.Value.Value = multiChoiceCollectionToValue(val.MultiChoiceCollectionValue)
requested[setting.GetId()] = newVal
case *settingsmsg.Setting_SingleChoiceValue:
sv := &settingsmsg.Value_StringValue{}
for _, option := range val.SingleChoiceValue.Options {
if option.GetDefault() {
sv.StringValue = option.Value.GetStringValue()
break
}
}
newVal.Value.Value = sv
requested[setting.GetId()] = newVal
}
}
return requested
}
func multiChoiceCollectionToValue(collection *settingsmsg.MultiChoiceCollection) *settingsmsg.Value_CollectionValue {
values := make([]*settingsmsg.CollectionOption, 0, len(collection.GetOptions()))
for _, option := range collection.GetOptions() {
switch o := option.GetValue().GetOption().(type) {
case *settingsmsg.MultiChoiceCollectionOptionValue_BoolValue:
if o != nil {
values = append(values, &settingsmsg.CollectionOption{
Key: option.GetKey(),
Option: &settingsmsg.CollectionOption_BoolValue{
BoolValue: o.BoolValue.GetDefault(),
},
})
}
}
}
return &settingsmsg.Value_CollectionValue{
CollectionValue: &settingsmsg.CollectionValue{
Values: values,
},
}
}
func getDefaultValueList() map[string]*settingsmsg.ValueWithIdentifier {
return map[string]*settingsmsg.ValueWithIdentifier{
// specific profile settings should be handled individually
defaults.SettingUUIDProfileLanguage: nil,
// all other profile settings that populated from the bundle based on type
defaults.SettingUUIDProfileEventShareCreated: nil,
defaults.SettingUUIDProfileEventShareRemoved: nil,
defaults.SettingUUIDProfileEventShareExpired: nil,
defaults.SettingUUIDProfileEventSpaceShared: nil,
defaults.SettingUUIDProfileEventSpaceUnshared: nil,
defaults.SettingUUIDProfileEventSpaceMembershipExpired: nil,
defaults.SettingUUIDProfileEventSpaceDisabled: nil,
defaults.SettingUUIDProfileEventSpaceDeleted: nil,
defaults.SettingUUIDProfileEventPostprocessingStepFinished: nil,
defaults.SettingUUIDProfileEmailSendingInterval: nil,
}
}
@@ -0,0 +1,13 @@
package svc
const (
// RoleManagementPermissionID is the hardcoded setting UUID for the role management permission
RoleManagementPermissionID string = "a53e601e-571f-4f86-8fec-d4576ef49c62"
// SettingsManagementPermissionID is the hardcoded setting UUID for the settings management permission
SettingsManagementPermissionID string = "3d58f441-4a05-42f8-9411-ef5874528ae1"
// AccountManagementPermissionID is the hardcoded setting UUID for the account management permission
AccountManagementPermissionID string = "8e587774-d929-4215-910b-a317b1e80f73"
)
@@ -0,0 +1,225 @@
package svc
import (
"errors"
"fmt"
"regexp"
validation "github.com/invopop/validation"
"github.com/invopop/validation/is"
settingsmsg "github.com/qsfera/server/protogen/gen/qsfera/messages/settings/v0"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
)
var (
regexForAccountUUID = regexp.MustCompile(`^[A-Za-z0-9\-_.+@:]+$`)
requireAccountID = []validation.Rule{
// use rule for validation error message consistency (".. must not be blank" on empty strings)
validation.Required,
validation.Match(regexForAccountUUID),
}
regexForKeys = regexp.MustCompile(`^[A-Za-z0-9\-_]*$`)
requireAlphanumeric = []validation.Rule{
validation.Required,
validation.Match(regexForKeys),
}
)
func validateSaveBundle(req *settingssvc.SaveBundleRequest) error {
if err := validation.ValidateStruct(
req.Bundle,
validation.Field(&req.Bundle.Id, validation.When(req.Bundle.Id != "", is.UUID)),
validation.Field(&req.Bundle.Name, requireAlphanumeric...),
validation.Field(&req.Bundle.Type, validation.NotIn(settingsmsg.Bundle_TYPE_UNKNOWN)),
validation.Field(&req.Bundle.Extension, requireAlphanumeric...),
validation.Field(&req.Bundle.DisplayName, validation.Required),
validation.Field(&req.Bundle.Settings, validation.Required),
); err != nil {
return err
}
if err := validateResource(req.Bundle.Resource); err != nil {
return err
}
for i := range req.Bundle.Settings {
if err := validateSetting(req.Bundle.Settings[i]); err != nil {
return err
}
}
return nil
}
func validateGetBundle(req *settingssvc.GetBundleRequest) error {
return validation.Validate(&req.BundleId, is.UUID)
}
func validateListBundles(req *settingssvc.ListBundlesRequest) error {
return nil
}
func validateAddSettingToBundle(req *settingssvc.AddSettingToBundleRequest) error {
if err := validation.ValidateStruct(req, validation.Field(&req.BundleId, is.UUID)); err != nil {
return err
}
return validateSetting(req.Setting)
}
func validateRemoveSettingFromBundle(req *settingssvc.RemoveSettingFromBundleRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.BundleId, is.UUID),
validation.Field(&req.SettingId, is.UUID),
)
}
func validateSaveValue(req *settingssvc.SaveValueRequest) error {
if err := validation.ValidateStruct(
req.Value,
validation.Field(&req.Value.Id, validation.When(req.Value.Id != "", is.UUID)),
validation.Field(&req.Value.BundleId, is.UUID),
validation.Field(&req.Value.SettingId, is.UUID),
validation.Field(&req.Value.AccountUuid, requireAccountID...),
); err != nil {
return err
}
if err := validateResource(req.Value.Resource); err != nil {
return err
}
if v := req.GetValue().GetCollectionValue().GetValues(); v != nil {
for i := range v {
if err := validation.Validate(v[i].GetKey(), validation.Required); err != nil {
return fmt.Errorf("collectionValue.values[%d].key %w", i, err)
}
}
}
// TODO: validate values against the respective setting. need to check if constraints of the setting are fulfilled.
return nil
}
func validateGetValue(req *settingssvc.GetValueRequest) error {
return validation.Validate(req.Id, is.UUID)
}
func validateGetValueByUniqueIdentifiers(req *settingssvc.GetValueByUniqueIdentifiersRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.SettingId, is.UUID),
validation.Field(&req.AccountUuid, requireAccountID...),
)
}
func validateListValues(req *settingssvc.ListValuesRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.BundleId, validation.When(req.BundleId != "", is.UUID)),
validation.Field(&req.AccountUuid, validation.When(req.AccountUuid != "", validation.Match(regexForAccountUUID))),
)
}
func validateListRoles(req *settingssvc.ListBundlesRequest) error {
return nil
}
func validateListRoleAssignments(req *settingssvc.ListRoleAssignmentsRequest) error {
return validation.Validate(req.AccountUuid, requireAccountID...)
}
func validateListRoleAssignmentsFiltered(req *settingssvc.ListRoleAssignmentsFilteredRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.Filters,
validation.Required,
validation.Length(1, 1),
validation.Each(validation.By(validateUserRoleAssignmentFilter)),
),
)
}
func validateUserRoleAssignmentFilter(values any) error {
filter, ok := values.(*settingsmsg.UserRoleAssignmentFilter)
if !ok {
return errors.New("expected UserRoleAssignmentFilter")
}
return validation.ValidateStruct(
filter,
validation.Field(&filter.Type,
validation.Required,
validation.In(settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT, settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE),
),
validation.Field(&filter.Term,
validation.When(
filter.Type == settingsmsg.UserRoleAssignmentFilter_TYPE_ACCOUNT,
validation.By(validateFilterAccountUUID),
),
validation.When(
filter.Type == settingsmsg.UserRoleAssignmentFilter_TYPE_ROLE,
validation.By(validateFilterRoleID),
),
),
)
}
func validateFilterRoleID(value any) error {
roleTerm, ok := value.(*settingsmsg.UserRoleAssignmentFilter_RoleId)
if !ok {
return errors.New("expected UserRoleAssignmentFilter_RoleId")
}
return validation.Validate(&roleTerm.RoleId, is.UUID)
}
func validateFilterAccountUUID(value any) error {
accountTerm, ok := value.(*settingsmsg.UserRoleAssignmentFilter_AccountUuid)
if !ok {
return errors.New("expected UserRoleAssignmentFilter_AccountUuid")
}
return validation.Validate(&accountTerm.AccountUuid, requireAccountID...)
}
func validateAssignRoleToUser(req *settingssvc.AssignRoleToUserRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.AccountUuid, requireAccountID...),
validation.Field(&req.RoleId, is.UUID),
)
}
func validateRemoveRoleFromUser(req *settingssvc.RemoveRoleFromUserRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.Id, is.UUID),
)
}
func validateListPermissionsByResource(req *settingssvc.ListPermissionsByResourceRequest) error {
return validateResource(req.Resource)
}
func validateGetPermissionByID(req *settingssvc.GetPermissionByIDRequest) error {
return validation.ValidateStruct(
req,
validation.Field(&req.PermissionId, requireAlphanumeric...),
)
}
// validateResource is an internal helper for validating the content of a resource.
func validateResource(resource *settingsmsg.Resource) error {
if err := validation.Validate(&resource, validation.Required); err != nil {
return err
}
return validation.Validate(&resource, validation.NotIn(settingsmsg.Resource_TYPE_UNKNOWN))
}
// validateSetting is an internal helper for validating the content of a setting.
func validateSetting(setting *settingsmsg.Setting) error {
// TODO: make sanity checks, like for int settings, min <= default <= max.
if err := validation.ValidateStruct(
setting,
validation.Field(&setting.Id, validation.When(setting.Id != "", is.UUID)),
validation.Field(&setting.Name, requireAlphanumeric...),
); err != nil {
return err
}
return validateResource(setting.Resource)
}