Merge branch 'dev' into reformat-fr-translation

This commit is contained in:
Alexandre Aubin 2021-09-03 15:25:34 +02:00 committed by GitHub
commit 13fd447b6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 783 additions and 243 deletions

View file

@ -2,7 +2,7 @@
# TRANSLATION
########################################
remove-stale-translated-strings:
autofix-translated-strings:
stage: translation
image: "before-install"
needs: []
@ -14,8 +14,9 @@ remove-stale-translated-strings:
script:
- cd tests # Maybe move this script location to another folder?
# create a local branch that will overwrite distant one
- git checkout -b "ci-remove-stale-translated-strings-${CI_COMMIT_REF_NAME}" --no-track
- git checkout -b "ci-autofix-translated-strings-${CI_COMMIT_REF_NAME}" --no-track
- python3 remove_stale_translated_strings.py
- python3 autofix_locale_format.py
- python3 reformat_locales.py
- '[ $(git diff -w | wc -l) != 0 ] || exit 0' # stop if there is nothing to commit
- git commit -am "[CI] Reformat / remove stale translated strings" || true

View file

@ -59,7 +59,7 @@ user:
api: GET /users
arguments:
--fields:
help: fields to fetch
help: fields to fetch (username, fullname, mail, mail-alias, mail-forward, mailbox-quota, groups, shell, home-path)
nargs: "+"
### user_create()
@ -199,6 +199,28 @@ user:
username:
help: Username or email to get information
### user_export()
export:
action_help: Export users into CSV
api: GET /users/export
### user_import()
import:
action_help: Import several users from CSV
api: POST /users/import
arguments:
csvfile:
help: "CSV file with columns username, firstname, lastname, password, mail, mailbox-quota, mail-alias, mail-forward, groups (separated by coma)"
type: open
-u:
full: --update
help: Update all existing users contained in the CSV file (by default existing users are ignored)
action: store_true
-d:
full: --delete
help: Delete all existing users that are not contained in the CSV file (by default existing users are kept)
action: store_true
subcategories:
group:
subcategory_help: Manage user groups

View file

@ -33,6 +33,7 @@ olcAuthzPolicy: none
olcConcurrency: 0
olcConnMaxPending: 100
olcConnMaxPendingAuth: 1000
olcSizeLimit: 50000
olcIdleTimeout: 0
olcIndexSubstrIfMaxLen: 4
olcIndexSubstrIfMinLen: 2
@ -188,7 +189,7 @@ olcDbIndex: memberUid eq
olcDbIndex: uniqueMember eq
olcDbIndex: virtualdomain eq
olcDbIndex: permission eq
olcDbMaxSize: 10485760
olcDbMaxSize: 104857600
structuralObjectClass: olcMdbConfig
#

View file

@ -416,6 +416,7 @@
"log_regen_conf": "Regenerate system configurations '{}'",
"log_user_create": "Add '{}' user",
"log_user_delete": "Delete '{}' user",
"log_user_import": "Import users",
"log_user_group_create": "Create '{}' group",
"log_user_group_delete": "Delete '{}' group",
"log_user_group_update": "Update '{}' group",
@ -641,10 +642,17 @@
"user_creation_failed": "Could not create user {user}: {error}",
"user_deleted": "User deleted",
"user_deletion_failed": "Could not delete user {user}: {error}",
"user_home_creation_failed": "Could not create 'home' folder for user",
"user_home_creation_failed": "Could not create home folder '{home}' for user",
"user_unknown": "Unknown user: {user}",
"user_update_failed": "Could not update user {user}: {error}",
"user_updated": "User info changed",
"user_import_bad_line": "Incorrect line {line}: {details}",
"user_import_bad_file": "Your CSV file is not correctly formatted it will be ignored to avoid potential data loss",
"user_import_missing_columns": "The following columns are missing: {columns}",
"user_import_partial_failed": "The users import operation partially failed",
"user_import_failed": "The users import operation completely failed",
"user_import_nothing_to_do": "No user needs to be imported",
"user_import_success": "Users successfully imported",
"yunohost_already_installed": "YunoHost is already installed",
"yunohost_configured": "YunoHost is now configured",
"yunohost_installing": "Installing YunoHost...",

View file

@ -602,7 +602,7 @@
"restore_removing_tmp_dir_failed": "پوشه موقت قدیمی حذف نشد",
"restore_nothings_done": "هیچ چیز ترمیم و بازسازی نشد",
"restore_not_enough_disk_space": "فضای کافی موجود نیست (فضا: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)",
"restore_may_be_not_enough_disk_space": "به نظر می رسد سیستم شما فضای کافی ندارد (فضای آزاد: {free_space} B ، فضای مورد نیاز: {space_space} B ، حاشیه امنیتی: {margin} B)",
"restore_may_be_not_enough_disk_space": "به نظر می رسد سیستم شما فضای کافی ندارد (فضای آزاد: {free_space} B ، فضای مورد نیاز: {needed_space} B ، حاشیه امنیتی: {margin} B)",
"restore_hook_unavailable": "اسکریپت ترمیم و بازسازی برای '{part}' در سیستم شما در دسترس نیست و همچنین در بایگانی نیز وجود ندارد",
"restore_failed": "سیستم بازیابی نشد",
"restore_extracting": "استخراج فایل های مورد نیاز از بایگانی…",

View file

@ -137,15 +137,15 @@
"upnp_dev_not_found": "Aucun périphérique compatible UPnP n'a été trouvé",
"upnp_disabled": "L'UPnP est désactivé",
"upnp_enabled": "L'UPnP est activé",
"upnp_port_open_failed": "Impossible d'ouvrir les ports UPnP",
"user_created": "L'utilisateur a été créé",
"user_creation_failed": "Impossible de créer l'utilisateur {user} : {error}",
"user_deleted": "L'utilisateur a été supprimé",
"user_deletion_failed": "Impossible de supprimer l'utilisateur {user} : {error}",
"user_home_creation_failed": "Impossible de créer le dossier personnel de l'utilisateur",
"user_unknown": "L'utilisateur {user} est inconnu",
"user_update_failed": "Impossible de mettre à jour l'utilisateur {user} : {error}",
"user_updated": "L'utilisateur a été modifié",
"upnp_port_open_failed": "Impossible douvrir les ports UPnP",
"user_created": "Lutilisateur a été créé",
"user_creation_failed": "Impossible de créer lutilisateur {user} : {error}",
"user_deleted": "Lutilisateur a été supprimé",
"user_deletion_failed": "Impossible de supprimer lutilisateur {user} : {error}",
"user_home_creation_failed": "Impossible de créer le dossier personnel '{home}' de lutilisateur",
"user_unknown": "Lutilisateur {user} est inconnu",
"user_update_failed": "Impossible de mettre à jour lutilisateur {user} : {error}",
"user_updated": "Lutilisateur a été modifié",
"yunohost_already_installed": "YunoHost est déjà installé",
"yunohost_configured": "YunoHost est maintenant configuré",
"yunohost_installing": "L'installation de YunoHost est en cours...",
@ -169,10 +169,10 @@
"certmanager_unable_to_parse_self_CA_name": "Impossible d'analyser le nom de l'autorité du certificat auto-signé (fichier : {file})",
"mailbox_used_space_dovecot_down": "Le service Dovecot doit être démarré si vous souhaitez voir l'espace disque occupé par la messagerie",
"domains_available": "Domaines disponibles :",
"backup_archive_broken_link": "Impossible d'accéder à l'archive de sauvegarde (lien invalide vers {path})",
"backup_archive_broken_link": "Impossible daccéder à larchive de sauvegarde (lien invalide vers {path})",
"certmanager_acme_not_configured_for_domain": "Le challenge ACME n'a pas pu être validé pour le domaine {domain} pour le moment car le code de la configuration NGINX est manquant... Merci de vérifier que votre configuration NGINX est à jour avec la commande : `yunohost tools regen-conf nginx --dry-run --with-diff`.",
"domain_hostname_failed": "Échec de l'utilisation d'un nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).",
"app_already_installed_cant_change_url": "Cette application est déjà installée. L'URL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.",
"domain_hostname_failed": "Échec de lutilisation dun nouveau nom d'hôte. Cela pourrait causer des soucis plus tard (cela n'en causera peut-être pas).",
"app_already_installed_cant_change_url": "Cette application est déjà installée. LURL ne peut pas être changé simplement par cette fonction. Vérifiez si cela est disponible avec `app changeurl`.",
"app_change_url_failed_nginx_reload": "Le redémarrage de NGINX a échoué. Voici la sortie de 'nginx -t' :\n{nginx_errors}",
"app_change_url_identical_domains": "L'ancien et le nouveau couple domaine/chemin_de_l'URL sont identiques pour ('{domain}{path}'), rien à faire.",
"app_change_url_no_script": "L'application '{app_name}' ne prend pas encore en charge le changement d'URL. Vous devriez peut-être la mettre à jour.",
@ -649,5 +649,13 @@
"diagnosis_apps_not_in_app_catalog": "Cette application est absente ou ne figure plus dans le catalogue d'applications de YunoHost. Vous devriez envisager de la désinstaller car elle ne recevra pas de mise à jour et pourrait compromettre l'intégrité et la sécurité de votre système.",
"diagnosis_apps_issue": "Un problème a été détecté pour l'application {app}",
"diagnosis_apps_allgood": "Toutes les applications installées respectent les pratiques de packaging de base",
"diagnosis_description_apps": "Applications"
"diagnosis_description_apps": "Applications",
"user_import_success": "Utilisateurs importés avec succès",
"user_import_nothing_to_do": "Aucun utilisateur n'a besoin d'être importé",
"user_import_failed": "L'opération d'importation des utilisateurs a totalement échoué",
"user_import_partial_failed": "L'opération d'importation des utilisateurs a partiellement échoué",
"user_import_missing_columns": "Les colonnes suivantes sont manquantes : {columns}",
"user_import_bad_file": "Votre fichier CSV n'est pas correctement formaté, il sera ignoré afin d'éviter une potentielle perte de données",
"user_import_bad_line": "Ligne incorrecte {line} : {details}",
"log_user_import": "Importer des utilisateurs"
}

View file

@ -546,7 +546,7 @@
"regenconf_file_remove_failed": "Non se puido eliminar o ficheiro de configuración '{conf}'",
"service_enable_failed": "Non se puido facer que o servizo '{service}' se inicie automáticamente no inicio.\n\nRexistros recentes do servizo: {logs}",
"service_disabled": "O servizo '{service}' xa non vai volver a ser iniciado ao inicio do sistema.",
"service_disable_failed": "Non se puido iniciar o servizo '{servizo}' ao inicio.\n\nRexistro recente do servizo: {logs}",
"service_disable_failed": "Non se puido iniciar o servizo '{service}' ao inicio.\n\nRexistro recente do servizo: {logs}",
"service_description_yunohost-firewall": "Xestiona, abre e pecha a conexións dos portos aos servizos",
"service_description_yunohost-api": "Xestiona as interaccións entre a interface web de YunoHost e o sistema",
"service_description_ssh": "Permíteche conectar de xeito remoto co teu servidor a través dun terminal (protocolo SSH)",
@ -563,7 +563,7 @@
"service_description_dnsmasq": "Xestiona a resolución de nomes de dominio (DNS)",
"service_description_yunomdns": "Permíteche chegar ao teu servidor utilizando 'yunohost.local' na túa rede local",
"service_cmd_exec_failed": "Non se puido executar o comando '{command}'",
"service_already_stopped": "O servizo '{sevice}' xa está detido",
"service_already_stopped": "O servizo '{service}' xa está detido",
"service_already_started": "O servizo '{service}' xa se está a executar",
"service_added": "Foi engadido o servizo '{service}'",
"service_add_failed": "Non se puido engadir o servizo '{service}'",

View file

@ -55,20 +55,20 @@
"service_description_postfix": "Використовується для відправки та отримання електронної пошти",
"service_description_php7.3-fpm": "Запускає програми, написані на мові програмування PHP, за допомогою NGINX",
"service_description_nginx": "Обслуговує або надає доступ до всіх веб-сайтів, розміщених на вашому сервері",
"service_description_mysql": "Зберігає дані додатків (база даних SQL)",
"service_description_mysql": "Зберігає дані застосунків (база даних SQL)",
"service_description_metronome": "Служба захисту миттєвого обміну повідомленнями XMPP",
"service_description_fail2ban": "Захист від перебору та інших видів атак з Інтернету",
"service_description_dovecot": "Дозволяє поштовим клієнтам отримувати доступ до електронної пошти (через IMAP і POP3)",
"service_description_dnsmasq": "Обробляє дозвіл доменних імен (DNS)",
"service_description_yunomdns": "Дозволяє вам отримати доступ до вашого сервера, використовуючи 'yunohost.local' у вашій локальній мережі",
"service_cmd_exec_failed": "Не вдалося виконати команду '{команда}'",
"service_cmd_exec_failed": "Не вдалося виконати команду '{command}'",
"service_already_stopped": "Служба '{service}' вже зупинена",
"service_already_started": "Служба '{service}' вже запущена",
"service_added": "Служба '{service}' була додана",
"service_add_failed": "Не вдалося додати службу '{service}'",
"server_reboot_confirm": "Сервер негайно перезавантажиться, ви впевнені? [{Відповіді}]",
"server_reboot_confirm": "Сервер негайно перезавантажиться, ви впевнені? [{answers}]",
"server_reboot": "сервер перезавантажиться",
"server_shutdown_confirm": "Сервер буде негайно виключений, ви впевнені? [{Відповіді}].",
"server_shutdown_confirm": "Сервер буде негайно виключений, ви впевнені? [{answers}].",
"server_shutdown": "сервер вимкнеться",
"root_password_replaced_by_admin_password": "Ваш кореневої пароль був замінений на пароль адміністратора.",
"root_password_desynchronized": "Пароль адміністратора був змінений, але YunoHost не зміг поширити це на пароль root!",
@ -82,21 +82,21 @@
"restore_hook_unavailable": "Сценарій відновлення для '{part}' недоступним у вашій системі і в архіві його теж немає",
"restore_failed": "Не вдалося відновити систему",
"restore_extracting": "Витяг необхідних файлів з архіву…",
"restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{Відповіді}].",
"restore_confirm_yunohost_installed": "Ви дійсно хочете відновити вже встановлену систему? [{answers}].",
"restore_complete": "відновлення завершено",
"restore_cleaning_failed": "Не вдалося очистити тимчасовий каталог відновлення",
"restore_backup_too_old": "Цей архів резервних копій не може бути відновлений, бо він отриманий з дуже старою версією YunoHost.",
"restore_already_installed_apps": "Наступні програми не можуть бути відновлені, тому що вони вже встановлені: {apps}",
"restore_already_installed_app": "Додаток з ідентифікатором \"{app} 'вже встановлено",
"restore_already_installed_app": "Застосунок з ідентифікатором \"{app} 'вже встановлено",
"regex_with_only_domain": "Ви не можете використовувати regex для домену, тільки для шляху.",
"regex_incompatible_with_tile": "/! \\ Packagers! Дозвіл '{permission}' має значення show_tile 'true', тому ви не можете визначити regex URL в якості основного URL.",
"regenconf_need_to_explicitly_specify_ssh": "Конфігурація ssh була змінена вручну, але вам потрібно явно вказати категорію 'ssh' з --force, щоб застосувати зміни.",
"regenconf_pending_applying": "Застосування очікує конфігурації для категорії '{категорія}'...",
"regenconf_pending_applying": "Застосування очікує конфігурації для категорії '{category}'...",
"regenconf_failed": "Не вдалося відновити конфігурацію для категорії (категорій): {categories}",
"regenconf_dry_pending_applying": "Перевірка очікує конфігурації, яка була б застосована для категорії '{категорія}'…",
"regenconf_would_be_updated": "Конфігурація була б оновлена для категорії '{категорія}'",
"regenconf_dry_pending_applying": "Перевірка очікує конфігурації, яка була б застосована для категорії '{category}'…",
"regenconf_would_be_updated": "Конфігурація була б оновлена для категорії '{category}'",
"regenconf_updated": "Конфігурація оновлена для категорії '{category}'",
"regenconf_up_to_date": "Конфігурація вже оновлена для категорії '{категорія}'",
"regenconf_up_to_date": "Конфігурація вже оновлена для категорії '{category}'",
"regenconf_now_managed_by_yunohost": "Конфігураційний файл '{conf}' тепер управляється YunoHost (категорія {category}).",
"regenconf_file_updated": "Конфігураційний файл '{conf}' оновлений",
"regenconf_file_removed": "Конфігураційний файл '{conf}' видалений",
@ -117,7 +117,7 @@
"permission_deletion_failed": "Не вдалося видалити дозвіл '{permission}': {error}",
"permission_deleted": "Дозвіл '{permission}' видалено",
"permission_cant_add_to_all_users": "Дозвіл {permission} не може бути додано всім користувачам.",
"permission_currently_allowed_for_all_users": "В даний час цей дозвіл надається всім користувачам на додаток до інших груп. Ймовірно, вам потрібно або видалити дозвіл 'all_users', або видалити інші групи, яким воно зараз надано.",
"permission_currently_allowed_for_all_users": "Наразі цей дозвіл надається всім користувачам на додачу до інших груп. Імовірно, вам потрібно або видалити дозвіл 'all_users', або видалити інші групи, яким його зараз надано.",
"permission_creation_failed": "Не вдалося створити дозвіл '{permission}': {error}",
"permission_created": "Дозвіл '{permission}' створено",
"permission_cannot_remove_main": "Видалення основного дозволу заборонено",
@ -145,7 +145,7 @@
"packages_upgrade_failed": "Не вдалося оновити всі пакети",
"operation_interrupted": "Операція була перервана вручну?",
"invalid_number": "Повинно бути число",
"not_enough_disk_space": "Недостатньо вільного місця на \"{шлях} '.",
"not_enough_disk_space": "Недостатньо вільного місця на \"{path} '.",
"migrations_to_be_ran_manually": "Міграція {id} повинна бути запущена вручну. Будь ласка, перейдіть в розділ Інструменти → Міграції на сторінці веб-адміністратора або виконайте команду `yunohost tools migrations run`.",
"migrations_success_forward": "Міграція {id} завершена",
"migrations_skip_migration": "Пропуск міграції {id}...",
@ -175,8 +175,8 @@
"migration_0015_cleaning_up": "Очищення кеш-пам'яті і пакетів, які більше не потрібні...",
"migration_0015_specific_upgrade": "Початок поновлення системних пакетів, які повинні бути оновлені незалежно...",
"migration_0015_modified_files": "Зверніть увагу, що такі файли були змінені вручну і можуть бути перезаписані після поновлення: {manually_modified_files}.",
"migration_0015_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, можливо, проблемні встановлені додатки. Схоже, що вони не були встановлені з каталогу додатків YunoHost або не зазначені як \"робочі\". Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}.",
"migration_0015_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частина системи або її додатків. Тому рекомендується: - Виконати резервне копіювання всіх важливих даних або додатків. Більш детальна інформація на сайті https://yunohost.org/backup; - Наберіться терпіння після запуску міграції: В залежності від вашого підключення до Інтернету і апаратного забезпечення, оновлення може зайняти до декількох годин.",
"migration_0015_problematic_apps_warning": "Зверніть увагу, що були виявлені наступні, можливо, проблемні встановлені додатки. Схоже, що вони не були встановлені з каталогу застосунків YunoHost або не зазначені як \"робочі\". Отже, не можна гарантувати, що вони будуть працювати після оновлення: {problematic_apps}.",
"migration_0015_general_warning": "Будь ласка, зверніть увагу, що ця міграція є делікатною операцією. Команда YunoHost зробила все можливе, щоб перевірити і протестувати її, але міграція все ще може порушити частина системи або її застосунків. Тому рекомендується: - Виконати резервне копіювання всіх важливих даних або застосунків. Більш детальна інформація на сайті https://yunohost.org/backup; - Наберіться терпіння після запуску міграції: В залежності від вашого підключення до Інтернету і апаратного забезпечення, оновлення може зайняти до декількох годин.",
"migration_0015_system_not_fully_up_to_date": "Ваша система не повністю оновлена. Будь ласка, виконайте регулярне оновлення перед запуском міграції на Buster.",
"migration_0015_not_enough_free_space": "Вільного місця в/var/досить мало! У вас повинно бути не менше 1 ГБ вільного місця, щоб запустити цю міграцію.",
"migration_0015_not_stretch": "Поточний дистрибутив Debian не є Stretch!",
@ -189,9 +189,9 @@
"migration_ldap_rollback_success": "Система відкотилася.",
"migration_ldap_migration_failed_trying_to_rollback": "Не вдалося виконати міграцію... спроба відкату системи.",
"migration_ldap_can_not_backup_before_migration": "Не вдалося завершити резервне копіювання системи перед невдалої міграцією. Помилка: {error}",
"migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і установки додатків перед фактичної міграцією.",
"migration_ldap_backup_before_migration": "Створення резервної копії бази даних LDAP і установки застосунків перед фактичної міграцією.",
"migration_description_0020_ssh_sftp_permissions": "Додайте підтримку дозволів SSH і SFTP",
"migration_description_0019_extend_permissions_features": "Розширення/переробка системи управління дозволами додатків",
"migration_description_0019_extend_permissions_features": "Розширення/переробка системи управління дозволами застосунків",
"migration_description_0018_xtable_to_nftable": "Перенесення старих правил мережевого трафіку в нову систему nftable",
"migration_description_0017_postgresql_9p6_to_11": "Перенесення баз даних з PostgreSQL 9.6 на 11",
"migration_description_0016_php70_to_php73_pools": "Перенесіть php7.0-fpm 'pool' conf файли на php7.3",
@ -240,9 +240,9 @@
"log_app_config_show_panel": "Показати панель конфігурації програми \"{} '",
"log_app_action_run": "Активації дії додатка \"{} '",
"log_app_makedefault": "Зробити '{}' додатком за замовчуванням",
"log_app_upgrade": "Оновити додаток '{}'",
"log_app_upgrade": "Оновити застосунок '{}'",
"log_app_remove": "Для видалення програми '{}'",
"log_app_install": "Встановіть додаток '{}'",
"log_app_install": "Встановіть застосунок '{}'",
"log_app_change_url": "Змініть URL-адресу додатка \"{} '",
"log_operation_unit_unclosed_properly": "Блок операцій не був закритий належним чином",
"log_does_exists": "Немає журналу операцій з ім'ям '{log}', використовуйте 'yunohost log list', щоб подивитися всі публічні журнали операцій",
@ -258,7 +258,7 @@
"hook_name_unknown": "Невідоме ім'я хука '{name}'",
"hook_list_by_invalid": "Це властивість не може бути використано для перерахування хуков",
"hook_json_return_error": "Не вдалося розпізнати повернення з хука {path}. Помилка: {msg}. Необроблений контент: {raw_content}",
"hook_exec_not_terminated": "Скрипт не завершився належним чином: {шлях}",
"hook_exec_not_terminated": "Скрипт не завершився належним чином: {path}",
"hook_exec_failed": "Не вдалося запустити скрипт: {path}",
"group_user_not_in_group": "Користувач {user} не входить в групу {group}",
"group_user_already_in_group": "Користувач {user} вже в групі {group}",
@ -297,17 +297,17 @@
"global_settings_setting_security_password_admin_strength": "Надійність пароля адміністратора",
"global_settings_setting_security_nginx_compatibility": "Компроміс між сумісністю і безпекою для веб-сервера NGINX. Впливає на шифри (і інші аспекти, пов'язані з безпекою)",
"global_settings_setting_pop3_enabled": "Включити протокол POP3 для поштового сервера.",
"global_settings_reset_success": "Попередні настройки тепер збережені в {шлях}.",
"global_settings_reset_success": "Попередні настройки тепер збережені в {path}.",
"global_settings_key_doesnt_exists": "Ключ '{settings_key}' не існує в глобальних налаштуваннях, ви можете побачити всі доступні ключі, виконавши команду 'yunohost settings list'.",
"global_settings_cant_write_settings": "Неможливо зберегти файл настройок, причина: {причина}",
"global_settings_cant_serialize_settings": "Не вдалося серіалізовать дані налаштувань, причина: {причина}",
"global_settings_cant_open_settings": "Не вдалося відкрити файл настройок, причина: {причина}",
"global_settings_cant_write_settings": "Неможливо зберегти файл настройок, причина: {reason}",
"global_settings_cant_serialize_settings": "Не вдалося серіалізовать дані налаштувань, причина: {reason}",
"global_settings_cant_open_settings": "Не вдалося відкрити файл настройок, причина: {reason}",
"global_settings_bad_type_for_setting": "Поганий тип для настройки {setting}, отриманий {received_type}, очікується {expected_type}",
"global_settings_bad_choice_for_enum": "Поганий вибір для настройки {setting}, отримано '{choice}', але доступні наступні варіанти: {available_choices}.",
"firewall_rules_cmd_failed": "Деякі команди правил брандмауера не спрацювали. Більш детальна інформація в журналі.",
"firewall_reloaded": "брандмауер перезавантажений",
"firewall_reload_failed": "Не вдалося перезавантажити брандмауер",
"file_does_not_exist": "Файл {шлях} не існує.",
"file_does_not_exist": "Файл {path} не існує.",
"field_invalid": "Неприпустиме поле '{}'",
"experimental_feature": "Попередження: Ця функція є експериментальною і не вважається стабільною, ви не повинні використовувати її, якщо не знаєте, що робите.",
"extracting": "Витяг...",
@ -321,8 +321,8 @@
"dyndns_key_generating": "Генерація DNS-ключа... Це може зайняти деякий час.",
"dyndns_ip_updated": "Оновлення свій IP-адресу в DynDNS",
"dyndns_ip_update_failed": "Не вдалося оновити IP-адреса в DynDNS",
"dyndns_could_not_check_available": "Не вдалося перевірити наявність певної {домен} на {провайдера}.",
"dyndns_could_not_check_provide": "Не вдалося перевірити, чи може {провайдер} надати {домен}.",
"dyndns_could_not_check_available": "Не вдалося перевірити наявність певної {domain} на {provider}.",
"dyndns_could_not_check_provide": "Не вдалося перевірити, чи може {provider} надати {domain}.",
"dpkg_lock_not_available": "Ця команда не може бути виконана прямо зараз, тому що інша програма, схоже, використовує блокування dpkg (системного менеджера пакетів).",
"dpkg_is_broken": "Ви не можете зробити це прямо зараз, тому що dpkg/APT (системні менеджери пакетів), схоже, знаходяться в зламаному стані... Ви можете спробувати вирішити цю проблему, підключившись через SSH і виконавши `sudo apt install --fix-broken` і/або `sudo dpkg --configure -a`.",
"downloading": "Завантаження…",
@ -331,7 +331,7 @@
"domain_unknown": "невідомий домен",
"domain_name_unknown": "Домен '{domain}' невідомий",
"domain_uninstall_app_first": "Ці додатки все ще встановлені на вашому домені: {apps} ласка, видаліть їх за допомогою 'yunohost app remove the_app_id' або перемістити їх на інший домен за допомогою 'yunohost app change-url the_app_id', перш ніж приступити до видалення домену.",
"domain_remove_confirm_apps_removal": "Видалення цього домену призведе до видалення цих додатків: {apps} Ви впевнені, що хочете це зробити? [{Відповіді}].",
"domain_remove_confirm_apps_removal": "Видалення цього домену призведе до видалення цих застосунків: {apps} Ви впевнені, що хочете це зробити? [{answers}].",
"domain_hostname_failed": "Неможливо встановити нове ім'я хоста. Це може викликати проблеми в подальшому (можливо, все буде в порядку).",
"domain_exists": "Домен вже існує",
"domain_dyndns_root_unknown": "Невідомий кореневої домен DynDNS",
@ -367,7 +367,7 @@
"diagnosis_http_hairpinning_issue_details": "Можливо, це пов'язано з блоком/маршрутизатором вашого інтернет-провайдера. В результаті, люди ззовні вашої локальної мережі зможуть отримати доступ до вашого сервера, як і очікувалося, але не люди зсередини локальної мережі (як ви, ймовірно?) При використанні доменного імені або глобального IP. Можливо, ви зможете поліпшити ситуацію, глянувши на <a href='https://yunohost.org/dns_local_network'> https://yunohost.org/dns_local_network </a>.",
"diagnosis_http_hairpinning_issue": "Схоже, що у вашій локальній мережі не включена проброска.",
"diagnosis_ports_forwarding_tip": "Щоб вирішити цю проблему, вам, швидше за все, потрібно налаштувати кидок портів на вашому інтернет-маршрутизатор, як описано в <a href='https://yunohost.org/isp_box_config'> https://yunohost.org/isp_box_config</ a>.",
"diagnosis_ports_needed_by": "Відкриття цього порту необхідно для функцій {категорії} (служба {сервіс}).",
"diagnosis_ports_needed_by": "Відкриття цього порту необхідно для функцій {category} (служба {service}).",
"diagnosis_ports_ok": "Порт {port} доступний ззовні.",
"diagnosis_ports_partially_unreachable": "Порт {port} не доступний ззовні в IPv {failed}.",
"diagnosis_ports_unreachable": "Порт {port} недоступний ззовні.",
@ -383,8 +383,8 @@
"diagnosis_description_basesystem": "Базова система",
"diagnosis_security_vulnerable_to_meltdown_details": "Щоб виправити це, вам слід оновити систему і перезавантажитися, щоб завантажити нове ядро linux (або звернутися до вашого серверного провайдеру, якщо це не спрацює). Додаткову інформацію див. На сайті https://meltdownattack.com/.",
"diagnosis_security_vulnerable_to_meltdown": "Схоже, що ви уразливі до критичної уразливості безпеки Meltdown.",
"diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {простір}, що вельми тривожно! Швидше за все, дисковий простір закінчиться дуже швидко! Рекомендується мати не менше 16 ГБ для кореневої файлової системи.",
"diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {простір}. Це може бути нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендується мати не менше 16 ГБ для кореневої файлової системи.",
"diagnosis_rootfstotalspace_critical": "Коренева файлова система має тільки {space}, що вельми тривожно! Швидше за все, дисковий простір закінчиться дуже швидко! Рекомендується мати не менше 16 ГБ для кореневої файлової системи.",
"diagnosis_rootfstotalspace_warning": "Коренева файлова система має тільки {space}. Це може бути нормально, але будьте обережні, тому що в кінцевому підсумку дисковий простір може швидко закінчитися... Рекомендується мати не менше 16 ГБ для кореневої файлової системи.",
"diagnosis_regenconf_manually_modified_details": "Це можливо нормально, якщо ви знаєте, що робите! YunoHost перестане оновлювати цей файл автоматично... Але врахуйте, що поновлення YunoHost можуть містити важливі рекомендовані зміни. Якщо ви хочете, ви можете перевірити відмінності за допомогою команди <cmd> yunohost tools regen-conf {category} --dry-run --with-diff </ cmd> і примусово повернути рекомендовану конфігурацію за допомогою <cmd> yunohost tools regen- conf {category} --force </ cmd>.",
"diagnosis_regenconf_manually_modified": "Конфігураційний файл <code> {file} </ code>, схоже, був змінений вручну.",
"diagnosis_regenconf_allgood": "Всі конфігураційні файли відповідають рекомендованої конфігурації!",
@ -393,7 +393,7 @@
"diagnosis_mail_queue_unavailable": "Неможливо дізнатися кількість очікують листів в черзі",
"diagnosis_mail_queue_ok": "{nb_pending} відкладені листи в поштових чергах",
"diagnosis_mail_blacklist_website": "Після визначення причини, по якій ви потрапили в чорний список, і її усунення, ви можете попросити видалити ваш IP або домен на {blacklist_website}.",
"diagnosis_mail_blacklist_reason": "Причина внесення в чорний список: {причина}",
"diagnosis_mail_blacklist_reason": "Причина внесення в чорний список: {reason}",
"diagnosis_mail_blacklist_listed_by": "Ваш IP або домен <code> {item} </ code> знаходиться в чорному списку {blacklist_name}.",
"diagnosis_mail_blacklist_ok": "IP-адреси і домени, які використовуються цим сервером, не внесені в чорний список",
"diagnosis_mail_fcrdns_different_from_ehlo_domain_details": "Поточний зворотний DNS: <code> {rdns_domain} </ code> <br> Очікуване значення: <code> {ehlo_domain} </ code>.",
@ -439,11 +439,11 @@
"updating_apt_cache": "Вибірка доступних оновлень для системних пакетів...",
"update_apt_cache_warning": "Щось пішло не так при оновленні кеша APT (менеджера пакунків Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки: {sourceslist}",
"update_apt_cache_failed": "Неможливо оновити кеш APT (менеджер пакетів Debian). Ось дамп рядків sources.list, який може допомогти визначити проблемні рядки: {sourceslist}",
"unrestore_app": "{App} не буде поновлено",
"unrestore_app": "{app} не буде поновлено",
"unlimit": "немає квоти",
"unknown_main_domain_path": "Невідомий домен або шлях для '{app}'. Вам необхідно вказати домен і шлях, щоб мати можливість вказати URL для дозволу.",
"unexpected_error": "Щось пішло не так: {error}",
"unbackup_app": "{App} НЕ буде збережений",
"unbackup_app": "{app} НЕ буде збережений",
"tools_upgrade_special_packages_completed": "Оновлення пакета YunoHost завершено. Натисніть [Enter] для повернення командного рядка",
"tools_upgrade_special_packages_explanation": "Спеціальне оновлення триватиме у фоновому режимі. Будь ласка, не запускайте ніяких інших дій на вашому сервері протягом наступних ~ 10 хвилин (в залежності від швидкості обладнання). Після цього вам, можливо, доведеться заново увійти в веб-адмін. Журнал поновлення буде доступний в Інструменти → Журнал (в веб-адміном) або за допомогою 'yunohost log list' (з командного рядка).",
"tools_upgrade_special_packages": "Тепер оновлюємо \"спеціальні\" (пов'язані з yunohost) пакети…",
@ -467,12 +467,12 @@
"service_start_failed": "Не вдалося запустити службу '{service}' Recent service logs: {logs}",
"diagnosis_mail_outgoing_port_25_ok": "Поштовий сервер SMTP може відправляти електронні листи (вихідний порт 25 не заблокований).",
"diagnosis_swap_tip": "Будь ласка, будьте обережні і знайте, що якщо сервер розміщує своп на SD-карті або SSD-накопичувачі, це може різко скоротити термін служби устройства`.",
"diagnosis_swap_ok": "Система має {усього} свопу!",
"diagnosis_swap_notsomuch": "Система має тільки {усього} свопу. Щоб уникнути ситуацій, коли в системі закінчується пам'ять, слід передбачити наявність не менше {рекомендованого} обсягу підкачки.",
"diagnosis_swap_none": "В системі повністю відсутній своп. Ви повинні розглянути можливість додавання принаймні {рекомендованого} обсягу підкачки, щоб уникнути ситуацій, коли системі не вистачає пам'яті.",
"diagnosis_ram_ok": "Система все ще має {доступно} ({доступний_процент}%) оперативної пам'яті з {усього}.",
"diagnosis_ram_low": "У системі є {доступно} ({доступний_процент}%) оперативної пам'яті (з {усього}). Будьте уважні.",
"diagnosis_ram_verylow": "Система має тільки {доступне} ({доступний_процент}%) оперативної пам'яті! (З {усього})",
"diagnosis_swap_ok": "Система має {total} свопу!",
"diagnosis_swap_notsomuch": "Система має тільки {total} свопу. Щоб уникнути ситуацій, коли в системі закінчується пам'ять, слід передбачити наявність не менше {recommended} обсягу підкачки.",
"diagnosis_swap_none": "В системі повністю відсутній своп. Ви повинні розглянути можливість додавання принаймні {recommended} обсягу підкачки, щоб уникнути ситуацій, коли системі не вистачає пам'яті.",
"diagnosis_ram_ok": "Система все ще має {available} ({available_percent}%) оперативної пам'яті з {total}.",
"diagnosis_ram_low": "У системі є {available} ({available_percent}%) оперативної пам'яті (з {total}). Будьте уважні.",
"diagnosis_ram_verylow": "Система має тільки {available} ({available_percent}%) оперативної пам'яті! (З {total})",
"diagnosis_diskusage_ok": "У сховищі <code> {mountpoint} </ code> (на пристрої <code> {device} </ code>) залишилося {free} ({free_percent}%) вільного місця (з {total})!",
"diagnosis_diskusage_low": "Сховище <code> {mountpoint} </ code> (на пристрої <code> {device} </ code>) має тільки {free} ({free_percent}%) вільного місця (з {total}). Будьте уважні.",
"diagnosis_diskusage_verylow": "Сховище <code> {mountpoint} </ code> (на пристрої <code> {device} </ code>) має тільки {free} ({free_percent}%) вільного місця (з {total}). Вам дійсно варто подумати про очищення простору!",
@ -480,162 +480,162 @@
"diagnosis_services_bad_status": "Сервіс {service} знаходиться в {status} :(",
"diagnosis_services_conf_broken": "Конфігурація порушена для служби {service}!",
"diagnosis_services_running": "Служба {service} запущена!",
"diagnosis_domain_expires_in": "Термін дії {домену} закінчується через {днів} днів.",
"diagnosis_domain_expires_in": "Термін дії {domain} закінчується через {days} днів.",
"diagnosis_domain_expiration_error": "Термін дії деяких доменів закінчується ДУЖЕ СКОРО!",
"diagnosis_domain_expiration_warning": "Термін дії деяких доменів закінчиться найближчим часом!",
"diagnosis_domain_expiration_success": "Ваші домени зареєстровані і не збираються спливати найближчим часом.",
"diagnosis_domain_expiration_not_found_details": "Інформація WHOIS для домену {domain} не містить інформації про термін дії?",
"diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або термін його дії закінчився!",
"diagnosis_domain_expiration_not_found": "Неможливо перевірити термін дії деяких доменів",
"diagnosis_domain_not_found_details": "Домен {domain} не існує в базі даних WHOIS або строк його дії сплив!",
"diagnosis_domain_expiration_not_found": "Неможливо перевірити строк дії деяких доменів",
"diagnosis_dns_specialusedomain": "Домен {domain} заснований на домені верхнього рівня спеціального призначення (TLD) і тому не очікується, що у нього будуть актуальні записи DNS.",
"diagnosis_dns_try_dyndns_update_force": "Конфігурація DNS цього домену повинна автоматично управлятися YunoHost. Якщо це не так, ви можете спробувати примусово оновити її за допомогою команди <cmd>yunohost dyndns update --force</cmd>.",
"diagnosis_dns_point_to_doc": "Якщо вам потрібна допомога з налаштування DNS-записів, зверніться до документації на сайті <a href='https://yunohost.org/dns_config'>https://yunohost.org/dns_config</a>.",
"diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованої конфігурації: <br> Type: <code> {type} </ code> <br> Name: <code> {name} </ code> <br> Поточне значення: <code> {current} </ code> <br> Очікуване значення: <code> {value} </ code>",
"diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступною інформацією. <br> Тип: <code> {type} </ code> <br> Name: <code> {name} </ code> <br> Value: < code> {value} </ code>.",
"diagnosis_dns_bad_conf": "Деякі DNS-записи відсутні або невірні для домену {домен} (категорія {категорія})",
"diagnosis_dns_good_conf": "DNS-записи правильно налаштовані для домену {домен} (категорія {категорія})",
"diagnosis_ip_weird_resolvconf_details": "Файл <code> /etc/resolv.conf </ code> повинен бути симлінк на <code> /etc/resolvconf/run/resolv.conf </ code>, що вказує на <code> 127.0.0.1 </ code> (dnsmasq ). Якщо ви хочете вручну налаштувати DNS Резолвер, відредагуйте <code> /etc/resolv.dnsmasq.conf </ code>.",
"diagnosis_ip_weird_resolvconf": "Дозвіл DNS, схоже, працює, але схоже, що ви використовуєте для користувача <code> /etc/resolv.conf </ code>.",
"diagnosis_ip_broken_resolvconf": "Схоже, що дозвіл доменних імен на вашому сервері порушено, що пов'язано з тим, що <code> /etc/resolv.conf </ code> не вказує на <code> 127.0.0.1 </ code>.",
"diagnosis_ip_broken_dnsresolution": "Дозвіл доменних імен, схоже, з якоїсь причини не працює... Брандмауер блокує DNS-запити?",
"diagnosis_ip_dnsresolution_working": "Дозвіл доменних імен працює!",
"diagnosis_ip_not_connected_at_all": "Здається, що сервер взагалі не підключений до Інтернету !?",
"diagnosis_dns_discrepancy": "Наступний запис DNS, схоже, не відповідає рекомендованій конфігурації: <br>Тип: <code>{type}</code><br>Назва: <code>{name}</code><br>Поточне значення: <code>{current}</code><br>Очікуване значення: <code>{value}</code>",
"diagnosis_dns_missing_record": "Згідно рекомендованої конфігурації DNS, ви повинні додати запис DNS з наступними відомостями.\n<br>Тип: <code>{type}</code>\n<br>Назва: <code>{name}</code>\n<br>Значення: <code>{value}</code>",
"diagnosis_dns_bad_conf": "Деякі DNS-записи відсутні або неправильні для домену {domain} (категорія {category})",
"diagnosis_dns_good_conf": "DNS-записи правильно налаштовані для домену {domain} (категорія {category})",
"diagnosis_ip_weird_resolvconf_details": "Файл <code>/etc/resolv.conf</code> повинен бути символічним посиланням на <code>/etc/resolvconf/run/resolv.conf</code>, що вказує на <code>127.0.0.1</code>(dnsmasq). Якщо ви хочете вручну налаштувати DNS вирішувачі (resolvers), відредагуйте <code>/etc/resolv.dnsmasq.conf</code>.",
"diagnosis_ip_weird_resolvconf": "Роздільність DNS, схоже, працює, але схоже, що ви використовуєте користувацьку <code>/etc/resolv.conf</code>.",
"diagnosis_ip_broken_resolvconf": "Схоже, що роздільність доменних імен на вашому сервері порушено, що пов'язано з тим, що <code>/etc/resolv.conf</code> не вказує на <code>127.0.0.1</code>.",
"diagnosis_ip_broken_dnsresolution": "Роздільність доменних імен, схоже, з якоїсь причини не працює... Фаєрвол блокує DNS-запити?",
"diagnosis_ip_dnsresolution_working": "Роздільність доменних імен працює!",
"diagnosis_ip_not_connected_at_all": "Здається, сервер взагалі не під'єднаний до Інтернету!?",
"diagnosis_ip_local": "Локальний IP: <code>{local}</code>.",
"diagnosis_ip_global": "Глобальний IP: <code>{global}</code>",
"diagnosis_ip_no_ipv6_tip": "Наявність працюючого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: <a href='https://yunohost.org/#/ipv6'> https://yunohost.org/#/ipv6</ a>. Якщо ви не можете включити IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо ігнорувати це попередження.",
"diagnosis_ip_no_ipv6": "Сервер не має працюючого IPv6.",
"diagnosis_ip_connected_ipv6": "Сервер підключений до Інтернету через IPv6!",
"diagnosis_ip_no_ipv4": "Сервер не має працюючого IPv4.",
"diagnosis_ip_connected_ipv4": "Сервер підключений до Інтернету через IPv4!",
"diagnosis_no_cache": "Для категорії \"{категорія} 'ще немає кеша діагнозів.",
"diagnosis_failed": "Не вдалося результат діагностики для категорії '{категорія}': {error}",
"diagnosis_everything_ok": "Все виглядає добре для {категорії}!",
"diagnosis_found_warnings": "Знайдено {попередження} пунктів, які можна поліпшити для {категорії}.",
"diagnosis_found_errors_and_warnings": "Знайдено {errors} істотний (і) питання (и) (і {попередження} попередження (я)), що відносяться до {category}!",
"diagnosis_ip_no_ipv6_tip": "Наявність робочого IPv6 не є обов'язковим для роботи вашого сервера, але це краще для здоров'я Інтернету в цілому. IPv6 зазвичай автоматично налаштовується системою або вашим провайдером, якщо він доступний. В іншому випадку вам, можливо, доведеться налаштувати деякі речі вручну, як пояснюється в документації тут: <a href='https://yunohost.org/#/ipv6'> https://yunohost.org/#/ipv6</a>. Якщо ви не можете увімкнути IPv6 або якщо це здається вам занадто технічним, ви також можете сміливо нехтувати цим попередженням.",
"diagnosis_ip_no_ipv6": "Сервер не має робочого IPv6.",
"diagnosis_ip_connected_ipv6": "Сервер під'єднаний до Інтернету через IPv6!",
"diagnosis_ip_no_ipv4": "Сервер не має робочого IPv4.",
"diagnosis_ip_connected_ipv4": "Сервер під'єднаний до Інтернету через IPv4!",
"diagnosis_no_cache": "Для категорії \"{category} 'ще немає кеша діагностики.",
"diagnosis_failed": "Не вдалося отримати результат діагностики для категорії '{category}': {error}",
"diagnosis_everything_ok": "Усе виглядає добре для {category}!",
"diagnosis_found_warnings": "Знайдено {warnings} пунктів, які можна поліпшити для {category}.",
"diagnosis_found_errors_and_warnings": "Знайдено {errors} істотний (і) питання (и) (і {warnings} попередження (я)), що відносяться до {category}!",
"diagnosis_found_errors": "Знайдена {errors} важлива проблема (і), пов'язана з {category}!",
"diagnosis_ignored_issues": "(+ {Nb_ignored} проігнорована проблема (проблеми))",
"diagnosis_cant_run_because_of_dep": "Неможливо запустити діагностику для {категорії}, поки є важливі проблеми, пов'язані з {глибиною}.",
"diagnosis_cache_still_valid": "(Кеш все ще дійсний для діагностики {категорії}. Повторна діагностика поки не проводиться!)",
"diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{категорія}': {error}",
"diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Diagnosis в веб-адміном або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.",
"diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені з стороннього сховища під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили додатки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити цю ситуацію, спробуйте виконати наступну команду: <cmd> {cmd_to_fix} </ cmd>.",
"diagnosis_package_installed_from_sury": "Деякі системні пакети повинні бути знижені в статусі",
"diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання сховища backports. Якщо ви не знаєте, що робите, ми настійно не рекомендуємо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.",
"diagnosis_basesystem_ynh_inconsistent_versions": "Ви використовуєте несумісні версії пакетів YunoHost... швидше за все, через невдалий або часткового оновлення.",
"diagnosis_ignored_issues": "(+ {nb_ignored} знехтувана проблема (проблеми))",
"diagnosis_cant_run_because_of_dep": "Неможливо запустити діагностику для {category}, поки є важливі проблеми, пов'язані з {dep}.",
"diagnosis_cache_still_valid": "(Кеш все ще дійсний для діагностики {category}. Повторна діагностика поки не проводиться!)",
"diagnosis_failed_for_category": "Не вдалося провести діагностику для категорії '{category}': {error}",
"diagnosis_display_tip": "Щоб побачити знайдені проблеми, ви можете перейти в розділ Діагностика в вебадміністраторі або виконати команду 'yunohost diagnosis show --issues --human-readable' з командного рядка.",
"diagnosis_package_installed_from_sury_details": "Деякі пакети були ненавмисно встановлені зі стороннього репозиторію під назвою Sury. Команда YunoHost поліпшила стратегію роботи з цими пакетами, але очікується, що в деяких системах, які встановили застосунки PHP7.3 ще на Stretch, залишаться деякі невідповідності. Щоб виправити це становище, спробуйте виконати наступну команду: <cmd>{cmd_to_fix}</cmd>.",
"diagnosis_package_installed_from_sury": "Деякі системні пакети мають бути зістарені у версії",
"diagnosis_backports_in_sources_list": "Схоже, що apt (менеджер пакетів) налаштований на використання репозиторія backports. Якщо ви не знаєте, що робите, ми наполегливо не радимо встановлювати пакети з backports, тому що це може привести до нестабільності або конфліктів у вашій системі.",
"diagnosis_basesystem_ynh_inconsistent_versions": "Ви використовуєте несумісні версії пакетів YunoHost... швидше за все, через невдале або часткове оновлення.",
"diagnosis_basesystem_ynh_main_version": "Сервер працює під управлінням YunoHost {main_version} ({repo})",
"diagnosis_basesystem_ynh_single_version": "{Пакет} версія: {версія} ({repo})",
"diagnosis_basesystem_ynh_single_version": "{package} версія: {version} ({repo})",
"diagnosis_basesystem_kernel": "Сервер працює під управлінням ядра Linux {kernel_version}",
"diagnosis_basesystem_host": "Сервер працює під управлінням Debian {debian_version}",
"diagnosis_basesystem_hardware_model": "Модель сервера - {model}",
"diagnosis_basesystem_hardware": "Архітектура апаратного забезпечення сервера - {virt} {arch}",
"custom_app_url_required": "Ви повинні надати URL для оновлення вашого призначеного для користувача додатки {app}.",
"confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Ця програма не входить в каталог додатків YunoHost. Установлення сторонніх додатків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей додаток не буде працювати або зламає вашу систему... Якщо ви все одно готові піти на такий ризик, введіть '{answers}'.",
"confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що це додаток все ще експериментальне (якщо не сказати, що воно явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей додаток не буде працювати або зламає вашу систему... Якщо ви все одно готові ризикнути, введіть '{answers}'.",
"confirm_app_install_warning": "Попередження: Ця програма може працювати, але не дуже добре інтегровано в YunoHost. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{Відповіді}]. ",
"certmanager_unable_to_parse_self_CA_name": "Не вдалося розібрати ім'я самоподпісивающегося центру (файл: {file})",
"certmanager_self_ca_conf_file_not_found": "Не вдалося знайти файл конфігурації для самоподпісивающегося центру (файл: {file})",
"custom_app_url_required": "Ви повинні надати URL-адресу для оновлення вашого користувацького застосунку {app}.",
"confirm_app_install_thirdparty": "НЕБЕЗПЕЧНО! Цей застосунок не входить в каталог застосунків YunoHost. Установлення сторонніх застосунків може порушити цілісність і безпеку вашої системи. Вам не слід встановлювати його, якщо ви не знаєте, що робите. НІЯКОЇ ПІДТРИМКИ НЕ БУДЕ, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові піти на такий ризик, введіть '{answers}'.",
"confirm_app_install_danger": "НЕБЕЗПЕЧНО! Відомо, що цей застосунок все ще експериментальний (якщо не сказати, що він явно не працює)! Вам не слід встановлювати його, якщо ви не знаєте, що робите. Ніякої підтримки не буде надано, якщо цей застосунок не буде працювати або зламає вашу систему... Якщо ви все одно готові ризикнути, введіть '{answers}'.",
"confirm_app_install_warning": "Попередження: Цей застосунок може працювати, але він не дуже добре інтегрований в YunoHost. Деякі функції, такі як єдина реєстрація та резервне копіювання/відновлення, можуть бути недоступні. Все одно встановити? [{answers}]. ",
"certmanager_unable_to_parse_self_CA_name": "Не вдалося розібрати назву самопідписного центру (файл: {file})",
"certmanager_self_ca_conf_file_not_found": "Не вдалося знайти файл конфігурації для самопідписного центру (файл: {file})",
"certmanager_no_cert_file": "Не вдалося розпізнати файл сертифіката для домену {domain} (файл: {file})",
"certmanager_hit_rate_limit": "Для цього набору доменів {domain} недавно було випущено дуже багато сертифікатів. Будь ласка, спробуйте ще раз пізніше. Див. Https://letsencrypt.org/docs/rate-limits/ для отримання більш докладної інформації.",
"certmanager_warning_subdomain_dns_record": "Піддомен '{subdomain} \"не дозволяється на той же IP-адресу, що і' {domain} '. Деякі функції будуть недоступні, поки ви не виправите це і не перегенеріруете сертифікат.",
"certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Web' в діагностиці для отримання додаткової інформації. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб відключити ці перевірки).",
"certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткової інформації. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки вона пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб відключити ці перевірки).",
"certmanager_domain_cert_not_selfsigned": "Сертифікат для домену {domain} не є самоподпісанного. Ви впевнені, що хочете замінити його? (Для цього використовуйте '--force').",
"certmanager_domain_not_diagnosed_yet": "Поки немає результатів діагностики для домену {domain}. Будь ласка, повторно проведіть діагностику для категорій 'DNS-записи' і 'Web' в розділі діагностики, щоб перевірити, чи готовий домен до Let's Encrypt. (Або, якщо ви знаєте, що робите, використовуйте '--no-checks', щоб відключити ці перевірки).",
"certmanager_hit_rate_limit": "Для цього набору доменів {domain} недавно було випущено дуже багато сертифікатів. Будь ласка, спробуйте ще раз пізніше. Див. https://letsencrypt.org/docs/rate-limits/ для отримання подробиць.",
"certmanager_warning_subdomain_dns_record": "Піддомен '{subdomain}' не дозволяється на тій же IP-адресі, що і '{domain}'. Деякі функції будуть недоступні, поки ви не виправите це і не перестворите сертифікат.",
"certmanager_domain_http_not_working": "Домен {domain}, схоже, не доступний через HTTP. Будь ласка, перевірте категорію 'Мережа' в діагностиці для отримання додаткових даних. (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).",
"certmanager_domain_dns_ip_differs_from_public_ip": "DNS-записи для домену '{domain}' відрізняються від IP цього сервера. Будь ласка, перевірте категорію 'DNS-записи' (основні) в діагностиці для отримання додаткових даних. Якщо ви недавно змінили запис A, будь ласка, зачекайте, поки він пошириться (деякі програми перевірки поширення DNS доступні в Інтернеті). (Якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).",
"certmanager_domain_cert_not_selfsigned": "Сертифікат для домену {domain} не є самопідписаним. Ви впевнені, що хочете замінити його? (Для цього використовуйте '--force').",
"certmanager_domain_not_diagnosed_yet": "Поки немає результатів діагностики для домену {domain}. Будь ласка, повторно проведіть діагностику для категорій 'DNS-записи' і 'Мережа' в розділі діагностики, щоб перевірити, чи готовий домен до Let's Encrypt. (Або, якщо ви знаєте, що робите, використовуйте '--no-checks', щоб вимкнути ці перевірки).",
"certmanager_certificate_fetching_or_enabling_failed": "Спроба використовувати новий сертифікат для {domain} не спрацювала...",
"certmanager_cert_signing_failed": "Не вдалося підписати новий сертифікат",
"certmanager_cert_renew_success": "Сертифікат Let's Encrypt оновлений для домену '{domain}'",
"certmanager_cert_install_success_selfsigned": "Самоподпісанний сертифікат тепер встановлений для домену '{domain}'",
"certmanager_cert_install_success": "Сертифікат Let's Encrypt тепер встановлений для домену '{domain}'",
"certmanager_cannot_read_cert": "Щось не так сталося при спробі відкрити поточний сертифікат для домену {domain} (файл: {файл}), причина: {причина}",
"certmanager_attempt_to_replace_valid_cert": "Ви намагаєтеся перезаписати хороший і дійсний сертифікат для домену {domain}! (Використовуйте --force для обходу)",
"certmanager_attempt_to_renew_valid_cert": "Термін дії сертифіката для домену '{domain} \"не закінчується! (Ви можете використовувати --force, якщо знаєте, що робите)",
"certmanager_cert_install_success_selfsigned": "Самопідписаний сертифікат тепер встановлений для домену '{domain}'",
"certmanager_cert_install_success": "Сертифікат Let's Encrypt тепер встановлений для домена '{domain}'",
"certmanager_cannot_read_cert": "Щось не так сталося при спробі відкрити поточний сертифікат для домена {domain} (файл: {file}), причина: {reason}",
"certmanager_attempt_to_replace_valid_cert": "Ви намагаєтеся перезаписати хороший дійсний сертифікат для домену {domain}! (Використовуйте --force для обходу)",
"certmanager_attempt_to_renew_valid_cert": "Строк дії сертифіката для домена '{domain}' не закінчується! (Ви можете використовувати --force, якщо знаєте, що робите)",
"certmanager_attempt_to_renew_nonLE_cert": "Сертифікат для домену '{domain}' не випущено Let's Encrypt. Неможливо продовжити його автоматично!",
"certmanager_acme_not_configured_for_domain": "Завдання ACME не може бути запущена для {domain} прямо зараз, тому що в його nginx conf відсутній відповідний фрагмент коду... Будь ласка, переконайтеся, що конфігурація nginx оновлена за допомогою `yunohost tools regen-conf nginx --dry-run - with-diff`.",
"backup_with_no_restore_script_for_app": "{App} не має скрипта відновлення, ви не зможете автоматично відновити резервну копію цього додатка.",
"backup_with_no_backup_script_for_app": "Додаток '{app}' не має скрипта резервного копіювання. Ігнорування.",
"backup_unable_to_organize_files": "Неможливо використовувати швидкий метод для організації файлів в архіві",
"certmanager_acme_not_configured_for_domain": "Завдання ACME не може бути запущене для {domain} прямо зараз, тому що в його nginx-конфігурації відсутній відповідний фрагмент коду... Будь ласка, переконайтеся, що конфігурація nginx оновлена за допомогою `yunohost tools regen-conf nginx --dry-run --with-diff`.",
"backup_with_no_restore_script_for_app": "{app} не має скрипта відновлення, ви не зможете автоматично відновити резервну копію цього застосунку.",
"backup_with_no_backup_script_for_app": "Застосунок '{app}' не має скрипта резервного копіювання. Нехтую ним.",
"backup_unable_to_organize_files": "Неможливо використовувати швидкий спосіб для організації файлів в архіві",
"backup_system_part_failed": "Не вдалося створити резервну копію системної частини '{part}'.",
"backup_running_hooks": "Запуск гачків резервного копіювання...",
"backup_running_hooks": "Запуск гачків (hook) резервного копіювання...",
"backup_permission": "Дозвіл на резервне копіювання для {app}",
"backup_output_symlink_dir_broken": "Ваш архівний каталог '{path}' є непрацюючою симлінк. Можливо, ви забули перемонтувати або підключити носій, на який вона вказує.",
"backup_output_symlink_dir_broken": "Ваш архівний каталог '{path}' є неробочим символічним посиланням. Можливо, ви забули перемонтувати або підключити носій, на який вона вказує.",
"backup_output_directory_required": "Ви повинні вказати вихідний каталог для резервного копіювання",
"backup_output_directory_not_empty": "Ви повинні вибрати порожній вихідний каталог",
"backup_output_directory_forbidden": "Виберіть інший вихідний каталог. Резервні копії не можуть бути створені в підкаталогах /bin,/boot,/dev,/etc,/lib,/root,/run,/sbin,/sys,/usr,/var або /home/yunohost.backup/archives.",
"backup_nothings_done": "нічого зберігати",
"backup_nothings_done": "Нема що зберігати",
"backup_no_uncompress_archive_dir": "Немає такого каталогу нестислого архіву",
"backup_mount_archive_for_restore": "Підготовка архіву для відновлення...",
"backup_mount_archive_for_restore": "Підготовлення архіву для відновлення...",
"backup_method_tar_finished": "Створено архів резервного копіювання TAR",
"backup_method_custom_finished": "Призначений для користувача метод резервного копіювання '{метод}' завершено",
"backup_method_custom_finished": "Користувацький спосіб резервного копіювання '{method}' завершено",
"backup_method_copy_finished": "Резервне копіювання завершено",
"backup_hook_unknown": "Гачок резервного копіювання '{hook}' невідомий",
"backup_hook_unknown": "Гачок (hook) резервного копіювання '{hook}' невідомий",
"backup_deleted": "Резервна копія видалена",
"backup_delete_error": "Не вдалося видалити '{path}'",
"backup_custom_mount_error": "Призначений для користувача метод резервного копіювання не зміг пройти етап 'монтування'",
"backup_custom_backup_error": "Призначений для користувача метод резервного копіювання не зміг пройти етап 'резервне копіювання'",
"backup_custom_mount_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'монтування'",
"backup_custom_backup_error": "Користувацький спосіб резервного копіювання не зміг пройти етап 'резервне копіювання'",
"backup_csv_creation_failed": "Не вдалося створити CSV-файл, необхідний для відновлення",
"backup_csv_addition_failed": "Не вдалося додати файли для резервного копіювання в CSV-файл",
"backup_creation_failed": "Не вдалося створити архів резервного копіювання",
"backup_create_size_estimation": "Архів буде містити близько {розмір} даних.",
"backup_create_size_estimation": "Архів буде містити близько {size} даних.",
"backup_created": "Резервна копія створена",
"backup_couldnt_bind": "Не вдалося зв'язати {src} з {dest}.",
"backup_copying_to_organize_the_archive": "Копіювання {size} MB для організації архіву",
"backup_cleaning_failed": "Не вдалося очистити тимчасову папку резервного копіювання",
"backup_copying_to_organize_the_archive": "Копіювання {size} МБ для організації архіву",
"backup_cleaning_failed": "Не вдалося очистити тимчасовий каталог резервного копіювання",
"backup_cant_mount_uncompress_archive": "Не вдалося змонтувати нестислий архів як захищений від запису",
"backup_ask_for_copying_if_needed": "Чи хочете ви тимчасово виконати резервне копіювання з використанням {size} MB? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені більш ефективним методом).",
"backup_ask_for_copying_if_needed": "Ви бажаєте тимчасово виконати резервне копіювання з використанням {size} МБ? (Цей спосіб використовується, оскільки деякі файли не можуть бути підготовлені дієвіше).",
"backup_archive_writing_error": "Не вдалося додати файли '{source}' (названі в архіві '{dest}') для резервного копіювання в стислий архів '{archive}'.",
"backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервної копії",
"backup_archive_corrupted": "Схоже, що архів резервної копії \"{archive} 'пошкоджений: {error}",
"backup_archive_cant_retrieve_info_json": "Не вдалося завантажити інформацію для архіву '{archive}'... info.json не може бути отриманий (або не є коректним json).",
"backup_archive_open_failed": "Не вдалося відкрити архів резервних копій",
"backup_archive_name_unknown": "Невідомий локальний архів резервного копіювання з ім'ям '{name}'",
"backup_archive_name_exists": "Архів резервного копіювання з таким ім'ям вже існує.",
"backup_archive_broken_link": "Не вдалося отримати доступ до архіву резервного копіювання (непрацююча посилання на {path})",
"backup_archive_system_part_not_available": "Системна частина '{part}' недоступна в цій резервній копії",
"backup_archive_corrupted": "Схоже, що архів резервної копії '{archive}' пошкоджений: {error}",
"backup_archive_cant_retrieve_info_json": "Не вдалося завантажити відомості для архіву '{archive}'... info.json не може бути отриманий(або не є правильним json).",
"backup_archive_open_failed": "Не вдалося відкрити архів резервної копії",
"backup_archive_name_unknown": "Невідомий локальний архів резервного копіювання з назвою '{name}'",
"backup_archive_name_exists": "Архів резервного копіювання з такою назвою вже існує.",
"backup_archive_broken_link": "Не вдалося отримати доступ до архіву резервного копіювання (неробоче посилання на {path})",
"backup_archive_app_not_found": "Не вдалося знайти {app} в архіві резервного копіювання",
"backup_applying_method_tar": "Створення резервного TAR-архіву...",
"backup_applying_method_custom": "Виклик для користувача методу резервного копіювання '{метод}'...",
"backup_applying_method_copy": "Копіювання всіх файлів в резервну копію...",
"backup_applying_method_custom": "Виклик користувацького способу резервного копіювання '{method}'...",
"backup_applying_method_copy": "Копіювання всіх файлів у резервну копію...",
"backup_app_failed": "Не вдалося створити резервну копію {app}",
"backup_actually_backuping": "Створення резервного архіву з зібраних файлів...",
"backup_abstract_method": "Цей метод резервного копіювання ще не реалізований",
"ask_password": "пароль",
"backup_abstract_method": "Цей спосіб резервного копіювання ще не реалізований",
"ask_password": "Пароль",
"ask_new_path": "Новий шлях",
"ask_new_domain": "новий домен",
"ask_new_admin_password": "Новий адміністративний пароль",
"ask_main_domain": "основний домен",
"ask_new_domain": "Новий домен",
"ask_new_admin_password": "Новий пароль адміністратора",
"ask_main_domain": "Основний домен",
"ask_lastname": "Прізвище",
"ask_firstname": "ім'я",
"ask_user_domain": "Домен для адреси електронної пошти користувача і облікового запису XMPP",
"apps_catalog_update_success": "Каталог додатків був оновлений!",
"apps_catalog_obsolete_cache": "Кеш каталогу додатків порожній або застарів.",
"apps_catalog_failed_to_download": "Неможливо завантажити каталог додатків {apps_catalog}: {error}",
"apps_catalog_updating": "Оновлення каталогу додатків…",
"apps_catalog_init_success": "Система каталогу додатків инициализирована!",
"apps_already_up_to_date": "Всі додатки вже оновлені",
"app_packaging_format_not_supported": я програма не може бути встановлено, тому що формат його упаковки не підтримується вашою версією YunoHost. Можливо, вам слід оновити вашу систему.",
"app_upgraded": "{App} оновлено",
"app_upgrade_some_app_failed": "Деякі програми не можуть бути оновлені",
"app_upgrade_script_failed": "Сталася помилка в сценарії оновлення програми",
"ask_firstname": "Ім'я",
"ask_user_domain": "Домен для адреси е-пошти користувача і облікового запису XMPP",
"apps_catalog_update_success": "Каталог застосунків був оновлений!",
"apps_catalog_obsolete_cache": "Кеш каталогу застосунків порожній або застарів.",
"apps_catalog_failed_to_download": "Неможливо завантажити каталог застосунків {apps_catalog}: {error}",
"apps_catalog_updating": "Оновлення каталогу застосунків…",
"apps_catalog_init_success": "Систему каталогу застосунків ініціалізовано!",
"apps_already_up_to_date": "Усі застосунки вже оновлено",
"app_packaging_format_not_supported": ей застосунок не може бути встановлено, тому що формат його упакування не підтримується вашою версією YunoHost. Можливо, вам слід оновити систему.",
"app_upgraded": "{app} оновлено",
"app_upgrade_some_app_failed": "Деякі застосунки не можуть бути оновлені",
"app_upgrade_script_failed": "Сталася помилка в скрипті оновлення застосунку",
"app_upgrade_failed": "Не вдалося оновити {app}: {error}",
"app_upgrade_app_name": "Зараз оновлюємо {app}...",
"app_upgrade_several_apps": "Наступні додатки будуть оновлені: {apps}",
"app_unsupported_remote_type": "Для додатка використовується непідтримуваний віддалений тип.",
"app_unknown": "невідоме додаток",
"app_upgrade_several_apps": "Наступні застосунки буде оновлено: {apps}",
"app_unsupported_remote_type": "Для застосунку використовується непідтримуваний віддалений тип.",
"app_unknown": "Невідомий застосунок",
"app_start_restore": "Відновлення {app}...",
"app_start_backup": "Збір файлів для резервного копіювання {app}...",
"app_start_remove": "Видалення {app}...",
"app_start_backup": "Збирання файлів для резервного копіювання {app}...",
"app_start_remove": "Вилучення {app}...",
"app_start_install": "Установлення {app}...",
"app_sources_fetch_failed": "Не вдалося вихідні файли, URL коректний?",
"app_restore_script_failed": "Сталася помилка всередині скрипта відновити оригінальну програму",
"app_sources_fetch_failed": "Не вдалося отримати джерельні файли, URL-адреса правильна?",
"app_restore_script_failed": "Сталася помилка всередині скрипта відновлення застосунку",
"app_restore_failed": "Не вдалося відновити {app}: {error}",
"app_remove_after_failed_install": "Видалення програми після збою установки...",
"app_remove_after_failed_install": "Вилучення застосунку після збою встановлення...",
"app_requirements_unmeet": "Вимоги не виконані для {app}, пакет {pkgname} ({version}) повинен бути {spec}.",
"app_requirements_checking": "Перевірка необхідних пакетів для {app}...",
"app_removed": "{App} видалено",
"app_not_properly_removed": "{App} не було видалено належним чином",
"app_not_installed": "Не вдалося знайти {app} в списку встановлених додатків: {all_apps}",
"app_not_correctly_installed": "{App}, схоже, неправильно встановлено",
"app_not_upgraded": "Додаток '{failed_app}' не вдалося оновити, і, як наслідок, оновлення таких програмах було скасовано: {apps}",
"app_manifest_install_ask_is_public": "Чи повинно це додаток бути відкрито для анонімних відвідувачів?",
"app_manifest_install_ask_admin": "Виберіть користувача-адміністратора для цього додатка",
"app_manifest_install_ask_password": "Виберіть пароль адміністратора для цього додатка"
"app_requirements_checking": "Перевіряння необхідних пакетів для {app}...",
"app_removed": "{app} видалено",
"app_not_properly_removed": "{app} не було видалено належним чином",
"app_not_installed": "Не вдалося знайти {app} в списку встановлених застосунків: {all_apps}",
"app_not_correctly_installed": "{app}, схоже, неправильно встановлено",
"app_not_upgraded": "Застосунок '{failed_app}' не вдалося оновити, і, як наслідок, оновлення таких застосунків було скасовано: {apps}",
"app_manifest_install_ask_is_public": "Чи має цей застосунок бути відкритим для анонімних відвідувачів?",
"app_manifest_install_ask_admin": "Виберіть користувача-адміністратора для цього застосунку",
"app_manifest_install_ask_password": "Виберіть пароль адміністратора для цього застосунку"
}

View file

@ -32,6 +32,7 @@ import psutil
from datetime import datetime, timedelta
from logging import FileHandler, getLogger, Formatter
from io import IOBase
from moulinette import m18n, Moulinette
from moulinette.core import MoulinetteError
@ -370,6 +371,18 @@ def is_unit_operation(
for field in exclude:
if field in context:
context.pop(field, None)
# Context is made from args given to main function by argparse
# This context will be added in extra parameters in yml file, so this context should
# be serializable and short enough (it will be displayed in webadmin)
# Argparse can provide some File or Stream, so here we display the filename or
# the IOBase, if we have no name.
for field, value in context.items():
if isinstance(value, IOBase):
try:
context[field] = value.name
except:
context[field] = "IOBase"
operation_logger = OperationLogger(op_key, related_to, args=context)
try:

View file

@ -8,6 +8,10 @@ from yunohost.user import (
user_create,
user_delete,
user_update,
user_import,
user_export,
FIELDS_FOR_IMPORT,
FIRST_ALIASES,
user_group_list,
user_group_create,
user_group_delete,
@ -22,7 +26,7 @@ maindomain = ""
def clean_user_groups():
for u in user_list()["users"]:
user_delete(u)
user_delete(u, purge=True)
for g in user_group_list()["groups"]:
if g not in ["all_users", "visitors"]:
@ -110,6 +114,77 @@ def test_del_user(mocker):
assert "alice" not in group_res["all_users"]["members"]
def test_import_user(mocker):
import csv
from io import StringIO
fieldnames = [
"username",
"firstname",
"lastname",
"password",
"mailbox-quota",
"mail",
"mail-alias",
"mail-forward",
"groups",
]
with StringIO() as csv_io:
writer = csv.DictWriter(csv_io, fieldnames, delimiter=";", quotechar='"')
writer.writeheader()
writer.writerow(
{
"username": "albert",
"firstname": "Albert",
"lastname": "Good",
"password": "",
"mailbox-quota": "1G",
"mail": "albert@" + maindomain,
"mail-alias": "albert2@" + maindomain,
"mail-forward": "albert@example.com",
"groups": "dev",
}
)
writer.writerow(
{
"username": "alice",
"firstname": "Alice",
"lastname": "White",
"password": "",
"mailbox-quota": "1G",
"mail": "alice@" + maindomain,
"mail-alias": "alice1@" + maindomain + ",alice2@" + maindomain,
"mail-forward": "",
"groups": "apps",
}
)
csv_io.seek(0)
with message(mocker, "user_import_success"):
user_import(csv_io, update=True, delete=True)
group_res = user_group_list()["groups"]
user_res = user_list(list(FIELDS_FOR_IMPORT.keys()))["users"]
assert "albert" in user_res
assert "alice" in user_res
assert "bob" not in user_res
assert len(user_res["alice"]["mail-alias"]) == 2
assert "albert" in group_res["dev"]["members"]
assert "alice" in group_res["apps"]["members"]
assert "alice" not in group_res["dev"]["members"]
def test_export_user(mocker):
result = user_export()
aliases = ",".join([alias + maindomain for alias in FIRST_ALIASES])
should_be = (
"username;firstname;lastname;password;mail;mail-alias;mail-forward;mailbox-quota;groups\r\n"
f"alice;Alice;White;;alice@{maindomain};{aliases};;0;dev\r\n"
f"bob;Bob;Snow;;bob@{maindomain};;;0;apps\r\n"
f"jack;Jack;Black;;jack@{maindomain};;;0;"
)
assert result == should_be
def test_create_group(mocker):
with message(mocker, "group_created", group="adminsys"):

View file

@ -43,32 +43,71 @@ from yunohost.log import is_unit_operation
logger = getActionLogger("yunohost.user")
FIELDS_FOR_IMPORT = {
"username": r"^[a-z0-9_]+$",
"firstname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$",
"lastname": r"^([^\W\d_]{1,30}[ ,.\'-]{0,3})+$",
"password": r"^|(.{3,})$",
"mail": r"^([\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}))$",
"mail-alias": r"^|([\w.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}),?)+$",
"mail-forward": r"^|([\w\+.-]+@([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,}),?)+$",
"mailbox-quota": r"^(\d+[bkMGT])|0|$",
"groups": r"^|([a-z0-9_]+(,?[a-z0-9_]+)*)$",
}
FIRST_ALIASES = ["root@", "admin@", "webmaster@", "postmaster@", "abuse@"]
def user_list(fields=None):
from yunohost.utils.ldap import _get_ldap_interface
user_attrs = {
"uid": "username",
"cn": "fullname",
ldap_attrs = {
"username": "uid",
"password": "", # We can't request password in ldap
"fullname": "cn",
"firstname": "givenName",
"lastname": "sn",
"mail": "mail",
"maildrop": "mail-forward",
"homeDirectory": "home_path",
"mailuserquota": "mailbox-quota",
"mail-alias": "mail",
"mail-forward": "maildrop",
"mailbox-quota": "mailuserquota",
"groups": "memberOf",
"shell": "loginShell",
"home-path": "homeDirectory",
}
attrs = ["uid"]
def display_default(values, _):
return values[0] if len(values) == 1 else values
display = {
"password": lambda values, user: "",
"mail": lambda values, user: display_default(values[:1], user),
"mail-alias": lambda values, _: values[1:],
"mail-forward": lambda values, user: [
forward for forward in values if forward != user["uid"][0]
],
"groups": lambda values, user: [
group[3:].split(",")[0]
for group in values
if not group.startswith("cn=all_users,")
and not group.startswith("cn=" + user["uid"][0] + ",")
],
"shell": lambda values, _: len(values) > 0
and values[0].strip() == "/bin/false",
}
attrs = set(["uid"])
users = {}
if fields:
keys = user_attrs.keys()
for attr in fields:
if attr in keys:
attrs.append(attr)
if not fields:
fields = ["username", "fullname", "mail", "mailbox-quota"]
for field in fields:
if field in ldap_attrs:
attrs.add(ldap_attrs[field])
else:
raise YunohostError("field_invalid", attr)
else:
attrs = ["uid", "cn", "mail", "mailuserquota"]
raise YunohostError("field_invalid", field)
ldap = _get_ldap_interface()
result = ldap.search(
@ -79,12 +118,13 @@ def user_list(fields=None):
for user in result:
entry = {}
for attr, values in user.items():
if values:
entry[user_attrs[attr]] = values[0]
for field in fields:
values = []
if ldap_attrs[field] in user:
values = user[ldap_attrs[field]]
entry[field] = display.get(field, display_default)(values, user)
uid = entry[user_attrs["uid"]]
users[uid] = entry
users[user["uid"][0]] = entry
return {"users": users}
@ -99,6 +139,7 @@ def user_create(
password,
mailbox_quota="0",
mail=None,
from_import=False,
):
from yunohost.domain import domain_list, _get_maindomain
@ -156,17 +197,12 @@ def user_create(
raise YunohostValidationError("system_username_exists")
main_domain = _get_maindomain()
aliases = [
"root@" + main_domain,
"admin@" + main_domain,
"webmaster@" + main_domain,
"postmaster@" + main_domain,
"abuse@" + main_domain,
]
aliases = [alias + main_domain for alias in FIRST_ALIASES]
if mail in aliases:
raise YunohostValidationError("mail_unavailable")
if not from_import:
operation_logger.start()
# Get random UID/GID
@ -221,8 +257,9 @@ def user_create(
# Attempt to create user home folder
subprocess.check_call(["mkhomedir_helper", username])
except subprocess.CalledProcessError:
if not os.path.isdir("/home/{0}".format(username)):
logger.warning(m18n.n("user_home_creation_failed"), exc_info=1)
home = f"/home/{username}"
if not os.path.isdir(home):
logger.warning(m18n.n("user_home_creation_failed", home=home), exc_info=1)
try:
subprocess.check_call(
@ -247,13 +284,14 @@ def user_create(
hook_callback("post_user_create", args=[username, mail], env=env_dict)
# TODO: Send a welcome mail to user
if not from_import:
logger.success(m18n.n("user_created"))
return {"fullname": fullname, "username": username, "mail": mail}
@is_unit_operation([("username", "user")])
def user_delete(operation_logger, username, purge=False):
def user_delete(operation_logger, username, purge=False, from_import=False):
"""
Delete user
@ -268,6 +306,7 @@ def user_delete(operation_logger, username, purge=False):
if username not in user_list()["users"]:
raise YunohostValidationError("user_unknown", user=username)
if not from_import:
operation_logger.start()
user_group_update("all_users", remove=username, force=True, sync_perm=False)
@ -300,6 +339,7 @@ def user_delete(operation_logger, username, purge=False):
hook_callback("post_user_delete", args=[username, purge])
if not from_import:
logger.success(m18n.n("user_deleted"))
@ -316,6 +356,7 @@ def user_update(
add_mailalias=None,
remove_mailalias=None,
mailbox_quota=None,
from_import=False,
):
"""
Update user informations
@ -375,7 +416,7 @@ def user_update(
]
# change_password is None if user_update is not called to change the password
if change_password is not None:
if change_password is not None and change_password != "":
# when in the cli interface if the option to change the password is called
# without a specified value, change_password will be set to the const 0.
# In this case we prompt for the new password.
@ -389,38 +430,40 @@ def user_update(
if mail:
main_domain = _get_maindomain()
aliases = [
"root@" + main_domain,
"admin@" + main_domain,
"webmaster@" + main_domain,
"postmaster@" + main_domain,
]
aliases = [alias + main_domain for alias in FIRST_ALIASES]
# If the requested mail address is already as main address or as an alias by this user
if mail in user["mail"]:
user["mail"].remove(mail)
# Othewise, check that this mail address is not already used by this user
else:
try:
ldap.validate_uniqueness({"mail": mail})
except Exception as e:
raise YunohostValidationError("user_update_failed", user=username, error=e)
raise YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostValidationError(
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
if mail in aliases:
raise YunohostValidationError("mail_unavailable")
del user["mail"][0]
new_attr_dict["mail"] = [mail] + user["mail"]
new_attr_dict["mail"] = [mail] + user["mail"][1:]
if add_mailalias:
if not isinstance(add_mailalias, list):
add_mailalias = [add_mailalias]
for mail in add_mailalias:
# (c.f. similar stuff as before)
if mail in user["mail"]:
user["mail"].remove(mail)
else:
try:
ldap.validate_uniqueness({"mail": mail})
except Exception as e:
raise YunohostValidationError(
"user_update_failed", user=username, error=e
)
raise YunohostError("user_update_failed", user=username, error=e)
if mail[mail.find("@") + 1 :] not in domains:
raise YunohostValidationError(
raise YunohostError(
"mail_domain_unknown", domain=mail[mail.find("@") + 1 :]
)
user["mail"].append(mail)
@ -465,6 +508,7 @@ def user_update(
new_attr_dict["mailuserquota"] = [mailbox_quota]
env_dict["YNH_USER_MAILQUOTA"] = mailbox_quota
if not from_import:
operation_logger.start()
try:
@ -475,8 +519,9 @@ def user_update(
# Trigger post_user_update hooks
hook_callback("post_user_update", env=env_dict)
logger.success(m18n.n("user_updated"))
if not from_import:
app_ssowatconf()
logger.success(m18n.n("user_updated"))
return user_info(username)
@ -512,6 +557,8 @@ def user_info(username):
"firstname": user["givenName"][0],
"lastname": user["sn"][0],
"mail": user["mail"][0],
"mail-aliases": [],
"mail-forward": [],
}
if len(user["mail"]) > 1:
@ -566,6 +613,315 @@ def user_info(username):
return result_dict
def user_export():
"""
Export users into CSV
Keyword argument:
csv -- CSV file with columns username;firstname;lastname;password;mailbox-quota;mail;mail-alias;mail-forward;groups
"""
import csv # CSV are needed only in this function
from io import StringIO
with StringIO() as csv_io:
writer = csv.DictWriter(
csv_io, list(FIELDS_FOR_IMPORT.keys()), delimiter=";", quotechar='"'
)
writer.writeheader()
users = user_list(list(FIELDS_FOR_IMPORT.keys()))["users"]
for username, user in users.items():
user["mail-alias"] = ",".join(user["mail-alias"])
user["mail-forward"] = ",".join(user["mail-forward"])
user["groups"] = ",".join(user["groups"])
writer.writerow(user)
body = csv_io.getvalue().rstrip()
if Moulinette.interface.type == "api":
# We return a raw bottle HTTPresponse (instead of serializable data like
# list/dict, ...), which is gonna be picked and used directly by moulinette
from bottle import HTTPResponse
response = HTTPResponse(
body=body,
headers={
"Content-Disposition": "attachment; filename=users.csv",
"Content-Type": "text/csv",
},
)
return response
else:
return body
@is_unit_operation()
def user_import(operation_logger, csvfile, update=False, delete=False):
"""
Import users from CSV
Keyword argument:
csvfile -- CSV file with columns username;firstname;lastname;password;mailbox_quota;mail;alias;forward;groups
"""
import csv # CSV are needed only in this function
from moulinette.utils.text import random_ascii
from yunohost.permission import permission_sync_to_user
from yunohost.app import app_ssowatconf
from yunohost.domain import domain_list
# Pre-validate data and prepare what should be done
actions = {"created": [], "updated": [], "deleted": []}
is_well_formatted = True
def to_list(str_list):
L = str_list.split(",") if str_list else []
L = [l.strip() for l in L]
return L
existing_users = user_list()["users"]
existing_groups = user_group_list()["groups"]
existing_domains = domain_list()["domains"]
reader = csv.DictReader(csvfile, delimiter=";", quotechar='"')
users_in_csv = []
missing_columns = [
key for key in FIELDS_FOR_IMPORT.keys() if key not in reader.fieldnames
]
if missing_columns:
raise YunohostValidationError(
"user_import_missing_columns", columns=", ".join(missing_columns)
)
for user in reader:
# Validate column values against regexes
format_errors = [
f"{key}: '{user[key]}' doesn't match the expected format"
for key, validator in FIELDS_FOR_IMPORT.items()
if user[key] is None or not re.match(validator, user[key])
]
# Check for duplicated username lines
if user["username"] in users_in_csv:
format_errors.append(f"username '{user['username']}' duplicated")
users_in_csv.append(user["username"])
# Validate that groups exist
user["groups"] = to_list(user["groups"])
unknown_groups = [g for g in user["groups"] if g not in existing_groups]
if unknown_groups:
format_errors.append(
f"username '{user['username']}': unknown groups %s"
% ", ".join(unknown_groups)
)
# Validate that domains exist
user["mail-alias"] = to_list(user["mail-alias"])
user["mail-forward"] = to_list(user["mail-forward"])
user["domain"] = user["mail"].split("@")[1]
unknown_domains = []
if user["domain"] not in existing_domains:
unknown_domains.append(user["domain"])
unknown_domains += [
mail.split("@", 1)[1]
for mail in user["mail-alias"]
if mail.split("@", 1)[1] not in existing_domains
]
unknown_domains = set(unknown_domains)
if unknown_domains:
format_errors.append(
f"username '{user['username']}': unknown domains %s"
% ", ".join(unknown_domains)
)
if format_errors:
logger.error(
m18n.n(
"user_import_bad_line",
line=reader.line_num,
details=", ".join(format_errors),
)
)
is_well_formatted = False
continue
# Choose what to do with this line and prepare data
user["mailbox-quota"] = user["mailbox-quota"] or "0"
# User creation
if user["username"] not in existing_users:
# Generate password if not exists
# This could be used when reset password will be merged
if not user["password"]:
user["password"] = random_ascii(70)
actions["created"].append(user)
# User update
elif update:
actions["updated"].append(user)
if delete:
actions["deleted"] = [
user for user in existing_users if user not in users_in_csv
]
if delete and not users_in_csv:
logger.error(
"You used the delete option with an empty csv file ... You probably did not really mean to do that, did you !?"
)
is_well_formatted = False
if not is_well_formatted:
raise YunohostValidationError("user_import_bad_file")
total = len(actions["created"] + actions["updated"] + actions["deleted"])
if total == 0:
logger.info(m18n.n("user_import_nothing_to_do"))
return
# Apply creation, update and deletion operation
result = {"created": 0, "updated": 0, "deleted": 0, "errors": 0}
def progress(info=""):
progress.nb += 1
width = 20
bar = int(progress.nb * width / total)
bar = "[" + "#" * bar + "." * (width - bar) + "]"
if info:
bar += " > " + info
if progress.old == bar:
return
progress.old = bar
logger.info(bar)
progress.nb = 0
progress.old = ""
def on_failure(user, exception):
result["errors"] += 1
logger.error(user + ": " + str(exception))
def update(new_infos, old_infos=False):
remove_alias = None
remove_forward = None
remove_groups = []
add_groups = new_infos["groups"]
if old_infos:
new_infos["mail"] = (
None if old_infos["mail"] == new_infos["mail"] else new_infos["mail"]
)
remove_alias = list(
set(old_infos["mail-alias"]) - set(new_infos["mail-alias"])
)
remove_forward = list(
set(old_infos["mail-forward"]) - set(new_infos["mail-forward"])
)
new_infos["mail-alias"] = list(
set(new_infos["mail-alias"]) - set(old_infos["mail-alias"])
)
new_infos["mail-forward"] = list(
set(new_infos["mail-forward"]) - set(old_infos["mail-forward"])
)
remove_groups = list(set(old_infos["groups"]) - set(new_infos["groups"]))
add_groups = list(set(new_infos["groups"]) - set(old_infos["groups"]))
for group, infos in existing_groups.items():
# Loop only on groups in 'remove_groups'
# Ignore 'all_users' and primary group
if (
group in ["all_users", new_infos["username"]]
or group not in remove_groups
):
continue
# If the user is in this group (and it's not the primary group),
# remove the member from the group
if new_infos["username"] in infos["members"]:
user_group_update(
group,
remove=new_infos["username"],
sync_perm=False,
from_import=True,
)
user_update(
new_infos["username"],
new_infos["firstname"],
new_infos["lastname"],
new_infos["mail"],
new_infos["password"],
mailbox_quota=new_infos["mailbox-quota"],
mail=new_infos["mail"],
add_mailalias=new_infos["mail-alias"],
remove_mailalias=remove_alias,
remove_mailforward=remove_forward,
add_mailforward=new_infos["mail-forward"],
from_import=True,
)
for group in add_groups:
if group in ["all_users", new_infos["username"]]:
continue
user_group_update(
group, add=new_infos["username"], sync_perm=False, from_import=True
)
users = user_list(list(FIELDS_FOR_IMPORT.keys()))["users"]
operation_logger.start()
# We do delete and update before to avoid mail uniqueness issues
for user in actions["deleted"]:
try:
user_delete(user, purge=True, from_import=True)
result["deleted"] += 1
except YunohostError as e:
on_failure(user, e)
progress(f"Deleting {user}")
for user in actions["updated"]:
try:
update(user, users[user["username"]])
result["updated"] += 1
except YunohostError as e:
on_failure(user["username"], e)
progress(f"Updating {user['username']}")
for user in actions["created"]:
try:
user_create(
user["username"],
user["firstname"],
user["lastname"],
user["domain"],
user["password"],
user["mailbox-quota"],
from_import=True,
)
update(user)
result["created"] += 1
except YunohostError as e:
on_failure(user["username"], e)
progress(f"Creating {user['username']}")
permission_sync_to_user()
app_ssowatconf()
if result["errors"]:
msg = m18n.n("user_import_partial_failed")
if result["created"] + result["updated"] + result["deleted"] == 0:
msg = m18n.n("user_import_failed")
logger.error(msg)
operation_logger.error(msg)
else:
logger.success(m18n.n("user_import_success"))
operation_logger.success()
return result
#
# Group subcategory
#
@ -740,7 +1096,13 @@ def user_group_delete(operation_logger, groupname, force=False, sync_perm=True):
@is_unit_operation([("groupname", "group")])
def user_group_update(
operation_logger, groupname, add=None, remove=None, force=False, sync_perm=True
operation_logger,
groupname,
add=None,
remove=None,
force=False,
sync_perm=True,
from_import=False,
):
"""
Update user informations
@ -810,6 +1172,7 @@ def user_group_update(
]
if set(new_group) != set(current_group):
if not from_import:
operation_logger.start()
ldap = _get_ldap_interface()
try:
@ -820,13 +1183,15 @@ def user_group_update(
except Exception as e:
raise YunohostError("group_update_failed", group=groupname, error=e)
if sync_perm:
permission_sync_to_user()
if not from_import:
if groupname != "all_users":
logger.success(m18n.n("group_updated", group=groupname))
else:
logger.debug(m18n.n("group_updated", group=groupname))
if sync_perm:
permission_sync_to_user()
return user_group_info(groupname)

View file

@ -0,0 +1,47 @@
import re
import json
import glob
# List all locale files (except en.json being the ref)
locale_folder = "locales/"
locale_files = glob.glob(locale_folder + "*.json")
locale_files = [filename.split("/")[-1] for filename in locale_files]
locale_files.remove("en.json")
reference = json.loads(open(locale_folder + "en.json").read())
def fix_locale(locale_file):
this_locale = json.loads(open(locale_folder + locale_file).read())
fixed_stuff = False
# We iterate over all keys/string in en.json
for key, string in reference.items():
# Ignore check if there's no translation yet for this key
if key not in this_locale:
continue
# Then we check that every "{stuff}" (for python's .format())
# should also be in the translated string, otherwise the .format
# will trigger an exception!
subkeys_in_ref = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", string)]
subkeys_in_this_locale = [k[0] for k in re.findall(r"{(\w+)(:\w)?}", this_locale[key])]
if set(subkeys_in_ref) != set(subkeys_in_this_locale) and (len(subkeys_in_ref) == len(subkeys_in_this_locale)):
for i, subkey in enumerate(subkeys_in_ref):
this_locale[key] = this_locale[key].replace('{%s}' % subkeys_in_this_locale[i], '{%s}' % subkey)
fixed_stuff = True
if fixed_stuff:
json.dump(
this_locale,
open(locale_folder + locale_file, "w"),
indent=4,
ensure_ascii=False,
)
for locale_file in locale_files:
fix_locale(locale_file)